Skip to content

Commit

Permalink
fix(cli): when error opening a link - show it in console (#1385)
Browse files Browse the repository at this point in the history
* fix(cli): when error opening a link - show it in console

* fix: when coder env variable is set - print open url

* fix: ensure shim for keytar works

* fix: show url in cli

* fix: print url

* chore: Bump codemod cli

* test(cli): add unit tests for CredentialsStorage functionality (#1401)

* test(cli): add unit tests for CredentialsStorage functionality

* test(cli): expand unit test coverage of credentialsStorage + keytar

---------

Co-authored-by: Cameron Seebach <[email protected]>
  • Loading branch information
arybitskiy and spirulence authored Dec 5, 2024
1 parent 9c9f898 commit 006e9f8
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 109 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-zoos-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"codemod": patch
---

Fixed fallback in case keytar cannot be used (fixes issue with authorization in headless environment)
13 changes: 3 additions & 10 deletions apps/cli/src/auth-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { backOff } from "exponential-backoff";
import inquirer from "inquirer";
import open from "open";
import { open } from "./utils/open.js";

import type { GetUserDataResponse } from "@codemod-com/api-types";
import { type Printer, chalk } from "@codemod-com/printer";
Expand Down Expand Up @@ -108,17 +108,10 @@ const routeUserToStudioForPermissions = ({
printer: Printer;
scopes: string[];
}) => {
const success = open(
open(
`${process.env.CODEMOD_HOME_PAGE_URL}?permissions=github&scopes=${scopes.join("&scopes=")}`,
printer,
);

if (!success) {
printer.printOperationMessage({
kind: "error",
message:
"An unexpected error occurred while redirecting to the permissions page. Please submit a GitHub issue (github.com/codemod-com/codemod/issues/new) or report it to us (codemod.com/community).",
});
}
};

export const requestGithubPermissions = async (options: {
Expand Down
12 changes: 2 additions & 10 deletions apps/cli/src/commands/feedback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import open from "open";
import { open } from "../utils/open.js";

import { type Printer, chalk } from "@codemod-com/printer";

Expand All @@ -13,13 +13,5 @@ export const handleFeedbackCommand = async (options: {
chalk.cyan("Redirecting to the feedback page..."),
);

const success = await open(feedbackUrl);

if (!success) {
printer.printOperationMessage({
kind: "error",
message:
"Unexpected error occurred while redirecting to the feedback page.",
});
}
await open(feedbackUrl, printer);
};
12 changes: 2 additions & 10 deletions apps/cli/src/commands/learn.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execSync } from "node:child_process";
import { dirname, extname } from "node:path";
import open from "open";
import { Project } from "ts-morph";
import { open } from "../utils/open.js";

import { type Printer, chalk } from "@codemod-com/printer";
import {
Expand Down Expand Up @@ -272,13 +272,5 @@ export const handleLearnCliCommand = async (options: {
chalk.cyan("Learning went successful! Opening the Codemod Studio...\n"),
);

const success = open(url);

if (!success) {
printer.printOperationMessage({
kind: "error",
message: "Unexpected error occurred while opening the Codemod Studio.",
});
return;
}
open(url, printer);
};
13 changes: 3 additions & 10 deletions apps/cli/src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { backOff } from "exponential-backoff";
import open from "open";
import { open } from "../utils/open.js";

import { type Printer, chalk } from "@codemod-com/printer";
import {
Expand All @@ -20,17 +20,10 @@ const routeUserToStudioForLogin = (
sessionId: string,
iv: string,
) => {
const success = open(
open(
`${process.env.CODEMOD_HOME_PAGE_URL}?command=${ACCESS_TOKEN_REQUESTED_BY_CLI_KEY}&sessionId=${sessionId}&iv=${iv}`,
printer,
);

if (!success) {
printer.printOperationMessage({
kind: "error",
message:
"An unexpected error occurred while redirecting to the sign-in page. Please submit a GitHub issue (github.com/codemod-com/codemod/issues/new) or report it to us (codemod.com/community).",
});
}
};
export const handleLoginCliCommand = async (options: {
printer: Printer;
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
execPromise,
getCodemodRc,
} from "@codemod-com/utilities";
import open from "open";
import { version as cliVersion } from "#/../package.json";
import { getDiff, getDiffScreen } from "#dryrun-diff.js";
import { fetchCodemod, populateCodemodArgs } from "#fetch-codemod.js";
Expand All @@ -27,6 +26,7 @@ import type { TelemetryEvent } from "#telemetry.js";
import type { NamedFileCommand } from "#types/commands.js";
import { originalStdoutWrite } from "#utils/constants.js";
import { logsPath, writeLogs } from "#utils/logs.js";
import { open } from "../utils/open.js";

const checkFileTreeVersioning = async (target: string) => {
let force = true;
Expand Down Expand Up @@ -102,7 +102,7 @@ export const handleRunCliCommand = async (options: {
const nameOrPath = args._.at(0)?.toString() ?? args.source ?? null;
if (nameOrPath === null) {
if (args.logs) {
return open(logsPath);
return open(logsPath, printer);
}

throw new Error("Codemod to run was not specified!");
Expand Down
157 changes: 90 additions & 67 deletions apps/cli/src/credentials-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,71 +7,100 @@ export enum CredentialsStorageType {
ACCOUNT = "user-account",
}

const keytarShim = {
default: {
setPassword: async (service: string, account: string, password: string) =>
writeFile(join(codemodDirectoryPath, `${service}:${account}`), password),
findCredentials: async (service: string) => {
const entries = await readdir(codemodDirectoryPath).then((dir) =>
dir.filter((file) => file.startsWith(`${service}:`)),
);

return Promise.all(
entries.map(async (file) => ({
account: file.split(":")[1],
password: await readFile(join(codemodDirectoryPath, file), {
encoding: "utf-8",
}),
})),
);
},
deletePassword: async (service: string, account: string) =>
unlink(join(codemodDirectoryPath, `${service}:${account}`))
.then(() => true)
.catch(() => false),
},
const keytar = async () => {
return (await import("keytar")).default;
};

//Sometimes we running production CLI from local build, credentials should be stored in different places.
const SERVICE = `codemod.com${process.env.NODE_ENV === "production" ? "" : `-${process.env.NODE_ENV}`}`;
let alreadyWarned = false;

const getKeytar = async () => {
try {
return await import("keytar");
} catch (err) {
const isShimLoggedIn = await keytarShim.default
.findCredentials(SERVICE)
.then((creds) => creds.length > 0);

if (!alreadyWarned && !isShimLoggedIn) {
alreadyWarned = true;
console.warn(
chalk.red(
String(err),
`\n\nCodemod CLI uses "keytar" to store your credentials securely.`,
`\nPlease make sure you have "libsecret" installed on your system.`,
"\nDepending on your distribution, you will need to run the following command",
"\nDebian/Ubuntu:",
chalk.bold("sudo apt-get install libsecret-1-dev"),
"\nFedora:",
chalk.bold("sudo dnf install libsecret"),
"\nArch Linux:",
chalk.bold("sudo pacman -S libsecret"),
chalk.cyan(
"\n\nIf you were not able to install the necessary package or CLI was not able to detect the installation" +
"please reach out to us at our Community Slack channel.",
),
chalk.yellow(
"\nYou can still use the CLI with file-based replacement that will store your credentials at your home directory.",
),
async function prepareShim() {
await mkdir(codemodDirectoryPath, { recursive: true });
}

async function findShimCredentials(service: string) {
const entries = (await readdir(codemodDirectoryPath)).filter((file) =>
file.startsWith(`${service}:`),
);

return await Promise.all(
entries.map(async (file) => {
return {
account: file.split(":")[1] as string,
password: await readFile(join(codemodDirectoryPath, file), {
encoding: "utf-8",
}),
};
}),
);
}

async function warnMessage(error: any) {
const isShimLoggedIn = (await findShimCredentials(SERVICE)).length > 0;

if (!alreadyWarned && !isShimLoggedIn) {
alreadyWarned = true;
console.warn(
chalk.red(
String(error),
`\n\nCodemod CLI uses "keytar" to store your credentials securely.`,
`\nPlease make sure you have "libsecret" installed on your system.`,
"\nDepending on your distribution, you will need to run the following command",
"\nDebian/Ubuntu:",
chalk.bold("sudo apt-get install libsecret-1-dev"),
"\nFedora:",
chalk.bold("sudo dnf install libsecret"),
"\nArch Linux:",
chalk.bold("sudo pacman -S libsecret"),
chalk.cyan(
"\n\nIf you were not able to install the necessary package or CLI was not able to detect the installation " +
"please reach out to us at our Community Slack channel.",
),
chalk.yellow(
"\nYou can still use the CLI with file-based replacement that will store your credentials at your home directory.",
),
),
);
}
}

const keytarShim = {
setPassword: async (service: string, account: string, password: string) => {
try {
return await (await keytar()).setPassword(service, account, password);
} catch (error: any) {
await prepareShim();
await warnMessage(error);
await writeFile(
join(codemodDirectoryPath, `${service}:${account}`),
password,
);
}

await mkdir(codemodDirectoryPath, { recursive: true });
return keytarShim;
}
},
findCredentials: async (service: string) => {
try {
return await (await keytar()).findCredentials(service);
} catch (error: any) {
await prepareShim();
await warnMessage(error);
return await findShimCredentials(service);
}
},
deletePassword: async (service: string, account: string) => {
try {
return await (await keytar()).deletePassword(service, account);
} catch (error: any) {
await prepareShim();
await warnMessage(error);
try {
await unlink(join(codemodDirectoryPath, `${service}:${account}`));
return true;
} catch {
return false;
}
}
},
};

/**
Expand All @@ -83,18 +112,14 @@ export class CredentialsStorage {
private _credentials: { [key in CredentialsStorageType]?: string } = {};

async set(type: CredentialsStorageType, value: string) {
await getKeytar().then(({ default: keytar }) =>
keytar.setPassword(SERVICE, type, value),
);
await keytarShim.setPassword(SERVICE, type, value);
this._credentials[type] = value;
}

async get(type: CredentialsStorageType) {
const credentials = (
await getKeytar().then(({ default: keytar }) =>
keytar.findCredentials(SERVICE),
)
).find(({ account }) => account === type);
const credentials = (await keytarShim.findCredentials(SERVICE)).find(
({ account }) => account === type,
);

if (credentials) {
this._credentials[type] = credentials.password;
Expand All @@ -105,9 +130,7 @@ export class CredentialsStorage {
}

async delete(type: CredentialsStorageType) {
await getKeytar().then(({ default: keytar }) =>
keytar.deletePassword(SERVICE, type),
);
await keytarShim.deletePassword(SERVICE, type);
delete this._credentials[type];
}
}
Expand Down
14 changes: 14 additions & 0 deletions apps/cli/src/utils/open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Printer } from "@codemod-com/printer";
import openOriginal from "open";

export async function open(url: string, printer: Printer) {
try {
console.log(`Opening the following URL in your browser: ${url}\n`);
return await openOriginal(url);
} catch (error: any) {
printer.printOperationMessage({
kind: "error",
message: `Please open the following URL in your browser: ${url}`,
});
}
}
Loading

0 comments on commit 006e9f8

Please sign in to comment.