Skip to content

Commit 0043b24

Browse files
committed
chore(cli): migrate @fern-api/cli-v2 to use CliError
1 parent f501e76 commit 0043b24

File tree

18 files changed

+141
-57
lines changed

18 files changed

+141
-57
lines changed

packages/cli/cli-v2/src/api/adapter/LegacyApiSpecAdapter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
import type { schemas } from "@fern-api/config";
99
import { generatorsYml } from "@fern-api/configuration";
1010
import { relativize } from "@fern-api/fs-utils";
11+
import { CliError } from "@fern-api/task-context";
1112
import type { Context } from "../../context/Context.js";
1213
import type { ApiSpec } from "../config/ApiSpec.js";
1314
import type { AsyncApiSpec } from "../config/AsyncApiSpec.js";
@@ -49,7 +50,10 @@ export class LegacyApiSpecAdapter {
4950
if (isOpenRpcSpec(spec)) {
5051
return this.adaptOpenRpcSpec(spec);
5152
}
52-
throw new Error(`Unsupported spec type: ${JSON.stringify(spec)}`);
53+
throw new CliError({
54+
message: `Unsupported spec type: ${JSON.stringify(spec)}`,
55+
code: CliError.Code.InternalError
56+
});
5357
}
5458

5559
public convertAll(specs: ApiSpec[]): Spec[] {

packages/cli/cli-v2/src/api/adapter/LegacyFernWorkspaceAdapter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { generatorsYml } from "@fern-api/configuration";
33
import { RawSchemas } from "@fern-api/fern-definition-schema";
44
import { AbsoluteFilePath, dirname, relativize } from "@fern-api/fs-utils";
55
import { ConjureWorkspace, LazyFernWorkspace } from "@fern-api/lazy-fern-workspace";
6+
import { CliError } from "@fern-api/task-context";
67
import { TaskContextAdapter } from "../../context/adapter/TaskContextAdapter.js";
78
import type { Context } from "../../context/Context.js";
89
import type { Task } from "../../ui/Task.js";
@@ -103,7 +104,10 @@ export class LegacyFernWorkspaceAdapter {
103104
});
104105

105106
if (ossWorkspace == null) {
106-
throw new Error("Internal error; failed to build API definitions");
107+
throw new CliError({
108+
message: "Internal error; failed to build API definitions",
109+
code: CliError.Code.InternalError
110+
});
107111
}
108112

109113
return ossWorkspace.toFernWorkspace({ context: this.taskContext });

packages/cli/cli-v2/src/api/config/converter/ApiDefinitionConverter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { schemas } from "@fern-api/config";
22
import { AbsoluteFilePath, dirname, doesPathExist, join, RelativeFilePath, relative } from "@fern-api/fs-utils";
33
import { isNullish, type Sourced } from "@fern-api/source";
4+
import { CliError } from "@fern-api/task-context";
45
import { type ReferenceResolver, ValidationIssue } from "@fern-api/yaml-loader";
56
import type { FernYmlSchemaLoader } from "../../../config/fern-yml/FernYmlSchemaLoader.js";
67
import type { ApiDefinition } from "../ApiDefinition.js";
@@ -271,7 +272,10 @@ export class ApiDefinitionConverter {
271272
});
272273
}
273274
// Unreachable; this should never happen if the schema validation is correct.
274-
throw new Error(`Unknown spec type: ${JSON.stringify(spec)}`);
275+
throw new CliError({
276+
message: `Unknown spec type: ${JSON.stringify(spec)}`,
277+
code: CliError.Code.InternalError
278+
});
275279
}
276280

277281
private async convertOpenApiSpec({

packages/cli/cli-v2/src/api/resolver/ApiSpecDetector.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AbsoluteFilePath } from "@fern-api/fs-utils";
2+
import { CliError } from "@fern-api/task-context";
23
import yaml from "js-yaml";
34
import type { ApiSpecType } from "../config/ApiSpec.js";
45

@@ -25,29 +26,34 @@ export class ApiSpecDetector {
2526
public async detect({ absoluteFilePath, reference, content }: ApiSpecDetector.Args): Promise<ApiSpecType> {
2627
const parsed = this.parseOrThrow({ content, reference });
2728
if (typeof parsed !== "object" || parsed == null) {
28-
throw new Error(
29-
`Could not determine API type for "${reference}". ` +
30-
`File must contain a top-level "openapi" or "asyncapi" key.`
31-
);
29+
throw new CliError({
30+
message:
31+
`Could not determine API type for "${reference}". ` +
32+
`File must contain a top-level "openapi" or "asyncapi" key.`,
33+
code: CliError.Code.InternalError
34+
});
3235
}
3336
for (const specType of DETECTABLE_SPEC_TYPES) {
3437
if (specType in parsed) {
3538
return specType;
3639
}
3740
}
38-
throw new Error(
39-
`Could not determine API type for "${reference}". ` +
40-
`File must contain a top-level "openapi" or "asyncapi" key.`
41-
);
41+
throw new CliError({
42+
message:
43+
`Could not determine API type for "${reference}". ` +
44+
`File must contain a top-level "openapi" or "asyncapi" key.`,
45+
code: CliError.Code.InternalError
46+
});
4247
}
4348

4449
private parseOrThrow({ content, reference }: { content: string; reference: string }): unknown {
4550
try {
4651
return yaml.load(content);
4752
} catch {
48-
throw new Error(
49-
`Could not parse "${reference}" as YAML or JSON. Ensure the file is a valid OpenAPI or AsyncAPI specification.`
50-
);
53+
throw new CliError({
54+
message: `Could not parse "${reference}" as YAML or JSON. Ensure the file is a valid OpenAPI or AsyncAPI specification.`,
55+
code: CliError.Code.ParseError
56+
});
5157
}
5258
}
5359
}

packages/cli/cli-v2/src/api/resolver/ApiSpecResolver.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AbsoluteFilePath, doesPathExist, resolve } from "@fern-api/fs-utils";
2+
import { CliError } from "@fern-api/task-context";
23
import { mkdtemp, readFile, writeFile } from "fs/promises";
34
import { tmpdir } from "os";
45
import path from "path";
@@ -59,7 +60,10 @@ export class ApiSpecResolver {
5960
private async resolveLocal({ reference }: { reference: string }): Promise<ApiSpecResolver.Result> {
6061
const absoluteFilePath = resolve(this.context.cwd, reference);
6162
if (!(await doesPathExist(absoluteFilePath))) {
62-
throw new Error(`API spec file does not exist: ${reference}`);
63+
throw new CliError({
64+
message: `API spec file does not exist: ${reference}`,
65+
code: CliError.Code.ConfigError
66+
});
6367
}
6468

6569
const content = await readFile(absoluteFilePath, "utf-8");
@@ -74,14 +78,19 @@ export class ApiSpecResolver {
7478
private async fetchContent({ url }: { url: string }): Promise<{ content: string; contentType: string }> {
7579
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_API_SPEC_REQUEST_TIMEOUT_MS) });
7680
if (!response.ok) {
77-
throw new Error(`Failed to fetch "${url}": HTTP ${response.status} ${response.statusText}`);
81+
throw new CliError({
82+
message: `Failed to fetch "${url}": HTTP ${response.status} ${response.statusText}`,
83+
code: CliError.Code.NetworkError
84+
});
7885
}
7986
const contentType = response.headers.get("content-type") ?? "";
8087
if (contentType.includes("text/html")) {
81-
throw new Error(
82-
`The URL "${url}" returned HTML content. ` +
83-
`Ensure you're pointing to a raw spec URL, not a documentation page.`
84-
);
88+
throw new CliError({
89+
message:
90+
`The URL "${url}" returned HTML content. ` +
91+
`Ensure you're pointing to a raw spec URL, not a documentation page.`,
92+
code: CliError.Code.ConfigError
93+
});
8594
}
8695
const content = await response.text();
8796
return { content, contentType };
@@ -116,7 +125,10 @@ export class ApiSpecResolver {
116125
case "asyncapi":
117126
return { asyncapi: absoluteFilePath, origin };
118127
default:
119-
throw new Error(`Unsupported spec type for flags mode: "${specType}". Supported: openapi, asyncapi`);
128+
throw new CliError({
129+
message: `Unsupported spec type for flags mode: "${specType}". Supported: openapi, asyncapi`,
130+
code: CliError.Code.ConfigError
131+
});
120132
}
121133
}
122134

packages/cli/cli-v2/src/auth/PlatformKeyring.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { loggingExeca, runExeca } from "@fern-api/logging-execa";
2+
import { CliError } from "@fern-api/task-context";
23

34
/**
45
* Platform-specific keyring access via CLI subprocesses.
@@ -35,7 +36,10 @@ export class PlatformKeyring {
3536
await this.setPasswordWindows(service, account, password);
3637
break;
3738
default:
38-
throw new Error(`Unsupported platform: ${process.platform}`);
39+
throw new CliError({
40+
message: `Unsupported platform: ${process.platform}`,
41+
code: CliError.Code.EnvironmentError
42+
});
3943
}
4044
}
4145

@@ -48,7 +52,10 @@ export class PlatformKeyring {
4852
case "win32":
4953
return this.getPasswordWindows(service, account);
5054
default:
51-
throw new Error(`Unsupported platform: ${process.platform}`);
55+
throw new CliError({
56+
message: `Unsupported platform: ${process.platform}`,
57+
code: CliError.Code.EnvironmentError
58+
});
5259
}
5360
}
5461

@@ -61,7 +68,10 @@ export class PlatformKeyring {
6168
case "win32":
6269
return this.deletePasswordWindows(service, account);
6370
default:
64-
throw new Error(`Unsupported platform: ${process.platform}`);
71+
throw new CliError({
72+
message: `Unsupported platform: ${process.platform}`,
73+
code: CliError.Code.EnvironmentError
74+
});
6575
}
6676
}
6777

@@ -74,7 +84,10 @@ export class PlatformKeyring {
7484
case "win32":
7585
return this.findCredentialsWindows(service);
7686
default:
77-
throw new Error(`Unsupported platform: ${process.platform}`);
87+
throw new CliError({
88+
message: `Unsupported platform: ${process.platform}`,
89+
code: CliError.Code.EnvironmentError
90+
});
7891
}
7992
}
8093

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { extractErrorMessage, mergeWithOverrides } from "@fern-api/core-utils";
22
import type { AbsoluteFilePath } from "@fern-api/fs-utils";
33
import { CliError } from "@fern-api/task-context";
4+
45
import chalk from "chalk";
56
import { execFile } from "child_process";
67
import { readFile, writeFile } from "fs/promises";
@@ -291,9 +292,10 @@ export function addSplitCommand(cli: Argv<GlobalArgs>): void {
291292
})
292293
.coerce("format", (value: string): SplitFormatInput => {
293294
if (!(ALL_FORMAT_NAMES as readonly string[]).includes(value)) {
294-
throw new Error(
295-
`Invalid format '${value}'. Expected one of: ${OVERLAY_NAME}, ${OVERRIDES_NAME}`
296-
);
295+
throw new CliError({
296+
message: `Invalid format '${value}'. Expected one of: ${OVERLAY_NAME}, ${OVERRIDES_NAME}`,
297+
code: CliError.Code.ValidationError
298+
});
297299
}
298300
return value as SplitFormatInput;
299301
})

packages/cli/cli-v2/src/commands/sdk/generate/command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export class GenerateCommand {
198198
forceLocal: boolean;
199199
}): Promise<void> {
200200
if (workspace.sdks == null) {
201-
throw new Error("No SDKs configured");
201+
throw new CliError({ message: "No SDKs configured", code: CliError.Code.InternalError });
202202
}
203203

204204
// Check that the APIs referenced by each target are valid.

packages/cli/cli-v2/src/commands/sdk/generate/parseOutputArg.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { schemas } from "@fern-api/config";
2-
2+
import { CliError } from "@fern-api/task-context";
33
import { isGitUrl } from "../utils/gitUrl.js";
44

55
/**
@@ -13,13 +13,15 @@ export function parseOutputArg(outputArg: string): schemas.OutputObjectSchema {
1313
if (isGitUrl(outputArg)) {
1414
const token = process.env.GITHUB_TOKEN ?? process.env.GIT_TOKEN;
1515
if (token == null) {
16-
throw new Error(
17-
`A git token is required when --output is a git URL.\n\n` +
16+
throw new CliError({
17+
message:
18+
`A git token is required when --output is a git URL.\n\n` +
1819
` Set GITHUB_TOKEN or GIT_TOKEN:\n` +
1920
` export GITHUB_TOKEN=ghp_xxx\n\n` +
2021
` Or use a local path:\n` +
21-
` --output ./my-sdk`
22-
);
22+
` --output ./my-sdk`,
23+
code: CliError.Code.InternalError
24+
});
2325
}
2426
return {
2527
git: {

packages/cli/cli-v2/src/config/FileFinder.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AbsoluteFilePath, doesPathExist } from "@fern-api/fs-utils";
2+
import { CliError } from "@fern-api/task-context";
23
import { findUp } from "find-up";
34

45
export namespace FileFinder {
@@ -36,7 +37,10 @@ export class FileFinder {
3637
}): Promise<AbsoluteFilePath> {
3738
const filepath = await this.find({ filename });
3839
if (filepath == null) {
39-
throw new Error(`${filename} file not found in any parent directory from ${this.from}`);
40+
throw new CliError({
41+
message: `${filename} file not found in any parent directory from ${this.from}`,
42+
code: CliError.Code.InternalError
43+
});
4044
}
4145
return filepath;
4246
}

0 commit comments

Comments
 (0)