Skip to content

Commit 91dc41a

Browse files
committed
refactor(cli): unify CLI v2 CliError with shared @fern-api/task-context CliError
Delete the local cli-v2 CliError class and switch all 31 importing files to use the shared CliError from @fern-api/task-context. Remap v2 codes (AUTH_REQUIRED→AUTH_ERROR, EXIT→TaskAbortSignal, etc.), replace ~35 CliError.exit() calls with TaskAbortSignal, and classify ~62 untyped new CliError sites with appropriate error codes. Made-with: Cursor
1 parent a2e0e9b commit 91dc41a

32 files changed

Lines changed: 204 additions & 223 deletions

File tree

packages/cli/cli-v2/src/__test__/compile.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { AbsoluteFilePath, doesPathExist } from "@fern-api/fs-utils";
2+
import { CliError } from "@fern-api/task-context";
23
import { randomUUID } from "crypto";
34
import { readFile, rm } from "fs/promises";
45
import { join } from "path";
56
import { afterEach, beforeEach, describe, expect, it } from "vitest";
67
import { CompileCommand } from "../commands/api/compile/command.js";
7-
import { CliError } from "../errors/CliError.js";
88
import { createTestContextWithCapture } from "./utils/createTestContext.js";
99

1010
const FIXTURES_DIR = AbsoluteFilePath.of(join(__dirname, "fixtures"));

packages/cli/cli-v2/src/commands/api/check/command.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { CliError, TaskAbortSignal } from "@fern-api/task-context";
12
import chalk from "chalk";
23
import type { Argv } from "yargs";
34
import { ApiChecker } from "../../../api/checker/ApiChecker.js";
45
import type { Context } from "../../../context/Context.js";
56
import type { GlobalArgs } from "../../../context/GlobalArgs.js";
6-
import { CliError } from "../../../errors/CliError.js";
77
import { Icons } from "../../../ui/format.js";
88
import { command } from "../../_internal/command.js";
99
import { type JsonOutput, toJsonViolation } from "../../_internal/toJsonViolation.js";
@@ -26,7 +26,8 @@ export class CheckCommand {
2626
if (args.api != null && workspace.apis[args.api] == null) {
2727
const availableApis = Object.keys(workspace.apis).join(", ");
2828
throw new CliError({
29-
message: `API '${args.api}' not found. Available APIs: ${availableApis}`
29+
message: `API '${args.api}' not found. Available APIs: ${availableApis}`,
30+
code: "CONFIG_ERROR"
3031
});
3132
}
3233

@@ -43,7 +44,7 @@ export class CheckCommand {
4344
const response = this.buildJsonResponse({ apiCheckResult: result, hasErrors });
4445
context.stdout.info(JSON.stringify(response, null, 2));
4546
if (hasErrors) {
46-
throw CliError.exit();
47+
throw new TaskAbortSignal();
4748
}
4849
return;
4950
}
@@ -56,7 +57,7 @@ export class CheckCommand {
5657
}
5758

5859
if (hasErrors) {
59-
throw CliError.exit();
60+
throw new TaskAbortSignal();
6061
}
6162

6263
if (result.warningCount > 0) {

packages/cli/cli-v2/src/commands/api/compile/command.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Audiences } from "@fern-api/configuration";
22
import { streamObjectToFile } from "@fern-api/fs-utils";
3+
import { CliError, TaskAbortSignal } from "@fern-api/task-context";
34
import chalk from "chalk";
45
import { JsonStreamStringify } from "json-stream-stringify";
56
import type { Argv } from "yargs";
@@ -8,7 +9,6 @@ import { IrCompiler } from "../../../api/compiler/IrCompiler.js";
89
import type { ApiDefinition } from "../../../api/config/ApiDefinition.js";
910
import type { Context } from "../../../context/Context.js";
1011
import type { GlobalArgs } from "../../../context/GlobalArgs.js";
11-
import { CliError } from "../../../errors/CliError.js";
1212
import { LANGUAGES } from "../../../sdk/config/Language.js";
1313
import type { Workspace } from "../../../workspace/Workspace.js";
1414
import { command } from "../../_internal/command.js";
@@ -67,7 +67,8 @@ export class CompileCommand {
6767
if (definition == null) {
6868
const available = apiNames.join(", ");
6969
throw new CliError({
70-
message: `API '${args.api}' not found. Available APIs: ${available}`
70+
message: `API '${args.api}' not found. Available APIs: ${available}`,
71+
code: "CONFIG_ERROR"
7172
});
7273
}
7374
return { apiName: args.api, definition };
@@ -77,21 +78,24 @@ export class CompileCommand {
7778
const apiName = apiNames[0];
7879
if (apiName == null) {
7980
throw new CliError({
80-
message: "Internal error; no APIs found in workspace"
81+
message: "Internal error; no APIs found in workspace",
82+
code: "INTERNAL_ERROR"
8183
});
8284
}
8385
const definition = workspace.apis[apiName];
8486
if (definition == null) {
8587
throw new CliError({
86-
message: `Internal error; API '${apiName}' not found in workspace`
88+
message: `Internal error; API '${apiName}' not found in workspace`,
89+
code: "INTERNAL_ERROR"
8790
});
8891
}
8992
return { apiName, definition };
9093
}
9194

9295
const available = apiNames.join(", ");
9396
throw new CliError({
94-
message: `Multiple APIs found: ${available}. Use --api to select one.`
97+
message: `Multiple APIs found: ${available}. Use --api to select one.`,
98+
code: "CONFIG_ERROR"
9599
});
96100
}
97101

@@ -112,7 +116,7 @@ export class CompileCommand {
112116
`${violation.displayRelativeFilepath}:${violation.line}:${violation.column}: ${violation.message}`
113117
);
114118
}
115-
throw CliError.exit();
119+
throw new TaskAbortSignal();
116120
}
117121
}
118122

packages/cli/cli-v2/src/commands/api/merge/command.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { applyOpenAPIOverlay, mergeWithOverrides } from "@fern-api/core-utils";
2+
import { CliError } from "@fern-api/task-context";
23
import chalk from "chalk";
34
import { unlink, writeFile } from "fs/promises";
45
import path from "path";
@@ -7,7 +8,6 @@ import { FERN_YML_FILENAME } from "../../../config/fern-yml/constants.js";
78
import { FernYmlEditor } from "../../../config/fern-yml/FernYmlEditor.js";
89
import type { Context } from "../../../context/Context.js";
910
import type { GlobalArgs } from "../../../context/GlobalArgs.js";
10-
import { CliError } from "../../../errors/CliError.js";
1111
import { Icons } from "../../../ui/format.js";
1212
import { command } from "../../_internal/command.js";
1313
import { type OverlayDocument, toOverlay } from "../split/diffSpecs.js";
@@ -27,7 +27,7 @@ export class MergeCommand {
2727
const workspace = await context.loadWorkspaceOrThrow();
2828

2929
if (Object.keys(workspace.apis).length === 0) {
30-
throw new CliError({ message: "No APIs found in workspace." });
30+
throw new CliError({ message: "No APIs found in workspace.", code: "CONFIG_ERROR" });
3131
}
3232

3333
const entries = filterSpecs(workspace, { api: args.api });
@@ -42,7 +42,8 @@ export class MergeCommand {
4242
const fernYmlPath = workspace.absoluteFilePath;
4343
if (fernYmlPath == null) {
4444
throw new CliError({
45-
message: `No ${FERN_YML_FILENAME} found. Run 'fern init' to initialize a project.`
45+
message: `No ${FERN_YML_FILENAME} found. Run 'fern init' to initialize a project.`,
46+
code: "CONFIG_ERROR"
4647
});
4748
}
4849
editor = await FernYmlEditor.load({ fernYmlPath });

packages/cli/cli-v2/src/commands/api/split/command.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { extractErrorMessage, mergeWithOverrides } from "@fern-api/core-utils";
22
import type { AbsoluteFilePath } from "@fern-api/fs-utils";
3+
import { CliError } from "@fern-api/task-context";
34
import chalk from "chalk";
45
import { execFile } from "child_process";
56
import { readFile, writeFile } from "fs/promises";
@@ -10,7 +11,6 @@ import { FERN_YML_FILENAME } from "../../../config/fern-yml/constants.js";
1011
import { FernYmlEditor } from "../../../config/fern-yml/FernYmlEditor.js";
1112
import type { Context } from "../../../context/Context.js";
1213
import type { GlobalArgs } from "../../../context/GlobalArgs.js";
13-
import { CliError } from "../../../errors/CliError.js";
1414
import { Icons } from "../../../ui/format.js";
1515
import { command } from "../../_internal/command.js";
1616
import type { SpecEntry } from "../utils/filterSpecs.js";
@@ -46,7 +46,7 @@ export class SplitCommand {
4646
const workspace = await context.loadWorkspaceOrThrow();
4747

4848
if (Object.keys(workspace.apis).length === 0) {
49-
throw new CliError({ message: "No APIs found in workspace." });
49+
throw new CliError({ message: "No APIs found in workspace.", code: "CONFIG_ERROR" });
5050
}
5151

5252
const entries = filterSpecs(workspace, { api: args.api });
@@ -60,7 +60,8 @@ export class SplitCommand {
6060
const fernYmlPath = workspace.absoluteFilePath;
6161
if (fernYmlPath == null) {
6262
throw new CliError({
63-
message: `No ${FERN_YML_FILENAME} found. Run 'fern init' to initialize a project.`
63+
message: `No ${FERN_YML_FILENAME} found. Run 'fern init' to initialize a project.`,
64+
code: "CONFIG_ERROR"
6465
});
6566
}
6667
const editor = await FernYmlEditor.load({ fernYmlPath });
@@ -240,7 +241,8 @@ export class SplitCommand {
240241
} catch (error: unknown) {
241242
const detail = extractErrorMessage(error);
242243
throw new CliError({
243-
message: `Failed to get file from git HEAD: ${absolutePath}. Is the file tracked by git and has at least one commit?\n Cause: ${detail}`
244+
message: `Failed to get file from git HEAD: ${absolutePath}. Is the file tracked by git and has at least one commit?\n Cause: ${detail}`,
245+
code: "PARSE_ERROR"
244246
});
245247
}
246248
}
@@ -257,7 +259,10 @@ export class SplitCommand {
257259
function resolvePathOrThrow(context: Context, outputPath: string): AbsoluteFilePath {
258260
const resolved = context.resolveOutputFilePath(outputPath);
259261
if (resolved == null) {
260-
throw new CliError({ message: `Could not resolve output path: ${outputPath}` });
262+
throw new CliError({
263+
message: `Could not resolve output path: ${outputPath}`,
264+
code: "CONFIG_ERROR"
265+
});
261266
}
262267
return resolved;
263268
}

packages/cli/cli-v2/src/commands/api/utils/loadSpec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AbsoluteFilePath } from "@fern-api/fs-utils";
2+
import { CliError } from "@fern-api/task-context";
23
import { readFile } from "fs/promises";
34
import yaml from "js-yaml";
4-
import { CliError } from "../../../errors/CliError.js";
55
import { isEnoentError } from "./isEnoentError.js";
66

77
// biome-ignore lint/suspicious/noExplicitAny: OpenAPI specs can have any shape
@@ -17,9 +17,9 @@ export async function loadSpec(filepath: AbsoluteFilePath): Promise<Spec> {
1717
contents = await readFile(filepath, "utf8");
1818
} catch (error) {
1919
if (isEnoentError(error)) {
20-
throw new CliError({ message: `File does not exist: ${filepath}` });
20+
throw new CliError({ message: `File does not exist: ${filepath}`, code: "CONFIG_ERROR" });
2121
}
22-
throw new CliError({ message: `Failed to read file: ${filepath}` });
22+
throw new CliError({ message: `Failed to read file: ${filepath}`, code: "PARSE_ERROR" });
2323
}
2424
return parseSpec(contents, filepath);
2525
}
@@ -35,7 +35,10 @@ export function parseSpec(contents: string, filepath: string): Spec {
3535
try {
3636
return yaml.load(contents) as Spec;
3737
} catch {
38-
throw new CliError({ message: `Failed to parse file as JSON or YAML: ${filepath}` });
38+
throw new CliError({
39+
message: `Failed to parse file as JSON or YAML: ${filepath}`,
40+
code: "PARSE_ERROR"
41+
});
3942
}
4043
}
4144
}

packages/cli/cli-v2/src/commands/auth/login/command.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Argv } from "yargs";
77
import { TaskContextAdapter } from "../../../context/adapter/TaskContextAdapter.js";
88
import type { Context } from "../../../context/Context.js";
99
import type { GlobalArgs } from "../../../context/GlobalArgs.js";
10-
import { CliError } from "../../../errors/CliError.js";
10+
import { TaskAbortSignal } from "@fern-api/task-context";
1111
import { Icons } from "../../../ui/format.js";
1212
import { command } from "../../_internal/command.js";
1313

@@ -42,13 +42,13 @@ export class LoginCommand {
4242
const payload = await verifyAndDecodeJwt(idToken);
4343
if (payload == null) {
4444
context.stdout.error(`${Icons.error} Internal error; could not verify ID token`);
45-
throw CliError.exit();
45+
throw new TaskAbortSignal();
4646
}
4747

4848
const email = payload.email;
4949
if (email == null) {
5050
context.stdout.error(`${Icons.error} Internal error; ID token does not contain email claim`);
51-
throw CliError.exit();
51+
throw new TaskAbortSignal();
5252
}
5353

5454
const { isNew, totalAccounts } = await context.tokenService.login(email, accessToken);

packages/cli/cli-v2/src/commands/auth/logout/command.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Argv } from "yargs";
55
import { TokenService } from "../../../auth/TokenService.js";
66
import type { Context } from "../../../context/Context.js";
77
import type { GlobalArgs } from "../../../context/GlobalArgs.js";
8-
import { CliError } from "../../../errors/CliError.js";
8+
import { TaskAbortSignal } from "@fern-api/task-context";
99
import { Icons } from "../../../ui/format.js";
1010
import { command } from "../../_internal/command.js";
1111

@@ -65,7 +65,7 @@ export class LogoutCommand {
6565

6666
if (!context.isTTY) {
6767
context.stdout.error(`${Icons.error} Use --force to skip confirmation in non-interactive mode`);
68-
throw CliError.exit();
68+
throw new TaskAbortSignal();
6969
}
7070

7171
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
@@ -91,7 +91,7 @@ export class LogoutCommand {
9191

9292
if (!removed) {
9393
context.stdout.error(`${Icons.error} Account not found: ${user}`);
94-
throw CliError.exit();
94+
throw new TaskAbortSignal();
9595
}
9696

9797
context.stdout.info(`${Icons.success} Logged out of ${chalk.bold(user)}`);
@@ -106,7 +106,7 @@ export class LogoutCommand {
106106
context.stdout.error(
107107
`${Icons.error} Multiple accounts found. Use --user or --all in non-interactive mode.`
108108
);
109-
throw CliError.exit();
109+
throw new TaskAbortSignal();
110110
}
111111

112112
const choices = accounts.map((account) => ({

packages/cli/cli-v2/src/commands/auth/switch/command.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Argv } from "yargs";
44

55
import type { Context } from "../../../context/Context.js";
66
import type { GlobalArgs } from "../../../context/GlobalArgs.js";
7-
import { CliError } from "../../../errors/CliError.js";
7+
import { TaskAbortSignal } from "@fern-api/task-context";
88
import { Icons } from "../../../ui/format.js";
99
import { command } from "../../_internal/command.js";
1010

@@ -21,14 +21,14 @@ export class SwitchCommand {
2121
context.stdout.warn(`${chalk.yellow("⚠")} You are not logged in to Fern.`);
2222
context.stdout.info("");
2323
context.stdout.info(chalk.dim(" To log in, run: fern auth login"));
24-
throw CliError.exit();
24+
throw new TaskAbortSignal();
2525
}
2626

2727
if (accounts.length === 1) {
2828
const account = accounts[0];
2929
if (account == null) {
3030
context.stdout.error(`${Icons.error} Internal error; no accounts found`);
31-
throw CliError.exit();
31+
throw new TaskAbortSignal();
3232
}
3333

3434
context.stdout.warn(
@@ -60,7 +60,7 @@ export class SwitchCommand {
6060
): Promise<void> {
6161
if (!context.isTTY) {
6262
context.stdout.error(`${Icons.error} Use --user to specify account in non-interactive mode`);
63-
throw CliError.exit();
63+
throw new TaskAbortSignal();
6464
}
6565

6666
const choices = accounts.map((account) => ({
@@ -85,7 +85,7 @@ export class SwitchCommand {
8585
const success = await context.tokenService.switchAccount(user);
8686
if (!success) {
8787
context.stdout.error(`${Icons.error} Account not found: ${user}`);
88-
throw CliError.exit();
88+
throw new TaskAbortSignal();
8989
}
9090
context.stdout.info(`${Icons.success} Switched to ${chalk.bold(user)}`);
9191
}

packages/cli/cli-v2/src/commands/auth/token/command.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Argv } from "yargs";
66
import { TaskContextAdapter } from "../../../context/adapter/TaskContextAdapter.js";
77
import type { Context } from "../../../context/Context.js";
88
import type { GlobalArgs } from "../../../context/GlobalArgs.js";
9-
import { CliError } from "../../../errors/CliError.js";
9+
import { TaskAbortSignal } from "@fern-api/task-context";
1010
import { Icons } from "../../../ui/format.js";
1111
import { command } from "../../_internal/command.js";
1212

@@ -43,18 +43,18 @@ export class TokenCommand {
4343
response.error._visit({
4444
organizationNotFoundError: () => {
4545
process.stderr.write(`${Icons.error} Organization "${orgId}" was not found.\n`);
46-
throw CliError.exit();
46+
throw new TaskAbortSignal();
4747
},
4848
unauthorizedError: () => {
4949
process.stderr.write(`${Icons.error} You do not have access to organization "${orgId}".\n`);
50-
throw CliError.exit();
50+
throw new TaskAbortSignal();
5151
},
5252
_other: () => {
5353
process.stderr.write(
5454
`${Icons.error} Failed to generate token.\n` +
5555
`\n Please contact support@buildwithfern.com for assistance.\n`
5656
);
57-
throw CliError.exit();
57+
throw new TaskAbortSignal();
5858
}
5959
});
6060
}
@@ -71,7 +71,7 @@ export class TokenCommand {
7171
`${Icons.error} No organization specified.\n` +
7272
`\n Run fern init or specify an organization with --org, then run this command again.\n`
7373
);
74-
throw CliError.exit();
74+
throw new TaskAbortSignal();
7575
}
7676
}
7777
}

0 commit comments

Comments
 (0)