Skip to content

Commit 2de3fdd

Browse files
committed
chore(cli): migrate @fern-api/cli to use CliError
Made-with: Cursor
1 parent fd585dd commit 2de3fdd

42 files changed

Lines changed: 512 additions & 195 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/cli/cli/src/cli-context/StdoutRedirector.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CliError } from "@fern-api/task-context";
2+
13
/**
24
* Redirects process.stdout to process.stderr.
35
* Can be generalized to FileDescriptorRedirector or StreamRedirector
@@ -20,7 +22,10 @@ export class StdoutRedirector {
2022

2123
public redirect(): void {
2224
if (this.redirected) {
23-
throw new Error("StdoutRedirector: already redirected — did you forget to restore()?");
25+
throw new CliError({
26+
message: "StdoutRedirector: already redirected — did you forget to restore()?",
27+
code: "INTERNAL_ERROR"
28+
});
2429
}
2530
this.originalWrite = process.stdout.write;
2631
process.stdout.write = process.stderr.write.bind(process.stderr) as typeof process.stdout.write;

packages/cli/cli/src/cli-context/upgrade-utils/getFernUpgradeMessage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { CliError } from "@fern-api/task-context";
12
import { FernRegistryClient } from "@fern-fern/generators-sdk";
23
import boxen from "boxen";
34
import chalk from "chalk";
4-
55
import { FernUpgradeInfo } from "../CliContext.js";
66
import { CliEnvironment } from "../CliEnvironment.js";
77
import { FernGeneratorUpgradeInfo } from "./getGeneratorVersions.js";
@@ -127,7 +127,7 @@ async function normalizeGeneratorName(generatorImage: string): Promise<string> {
127127
});
128128
const generatorResponse = await client.generators.getGeneratorByImage({ dockerImage: generatorImage });
129129
if (!generatorResponse.ok || generatorResponse.body == null) {
130-
throw new Error(`Generator ${generatorImage} not found`);
130+
throw new CliError({ message: `Generator ${generatorImage} not found`, code: "INTERNAL_ERROR" });
131131
}
132132
return generatorResponse.body.displayName;
133133
}

packages/cli/cli/src/cli.ts

Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ async function tryRunCli(cliContext: CliContext) {
176176
cliContext.logger.info(cliContext.environment.packageVersion);
177177
} else {
178178
cli.showHelp();
179-
cliContext.failAndThrow();
179+
cliContext.failAndThrow(undefined, undefined, { code: "CONFIG_ERROR" });
180180
}
181181
}
182182
)
@@ -322,14 +322,22 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
322322
}
323323
}
324324
if (argv.api != null && argv.docs != null) {
325-
return cliContext.failWithoutThrowing("Cannot specify both --api and --docs. Please choose one.");
325+
return cliContext.failWithoutThrowing(
326+
"Cannot specify both --api and --docs. Please choose one.",
327+
undefined,
328+
{ code: "CONFIG_ERROR" }
329+
);
326330
} else if (argv.readme != null && argv.mintlify != null) {
327331
return cliContext.failWithoutThrowing(
328-
"Cannot specify both --readme and --mintlify. Please choose one."
332+
"Cannot specify both --readme and --mintlify. Please choose one.",
333+
undefined,
334+
{ code: "CONFIG_ERROR" }
329335
);
330336
} else if (argv.openapi != null && argv["fern-definition"] === true) {
331337
return cliContext.failWithoutThrowing(
332-
"Cannot specify both --openapi and --fern-definition. Please choose one."
338+
"Cannot specify both --openapi and --fern-definition. Please choose one.",
339+
undefined,
340+
{ code: "CONFIG_ERROR" }
333341
);
334342
} else if (argv.readme != null) {
335343
await cliContext.runTask(async (context) => {
@@ -364,7 +372,7 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
364372
const result = await loadOpenAPIFromUrl({ url: argv.openapi, logger: cliContext.logger });
365373

366374
if (result.status === LoadOpenAPIStatus.Failure) {
367-
cliContext.failAndThrow(result.errorMessage);
375+
cliContext.failAndThrow(result.errorMessage, undefined, { code: "NETWORK_ERROR" });
368376
}
369377

370378
const tmpFilepath = result.filePath;
@@ -374,7 +382,9 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
374382
}
375383
const pathExists = await doesPathExist(absoluteOpenApiPath);
376384
if (!pathExists) {
377-
cliContext.failAndThrow(`${absoluteOpenApiPath} does not exist`);
385+
cliContext.failAndThrow(`${absoluteOpenApiPath} does not exist`, undefined, {
386+
code: "CONFIG_ERROR"
387+
});
378388
}
379389
}
380390
await cliContext.runTask(async (context) => {
@@ -427,9 +437,11 @@ function addDiffCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
427437
})
428438
.middleware((argv) => {
429439
if (!haveSameNullishness(argv.fromGeneratorVersion, argv.toGeneratorVersion)) {
430-
throw new Error(
431-
"Both --from-generator-version and --to-generator-version must be provided together, or neither should be provided"
432-
);
440+
throw new CliError({
441+
message:
442+
"Both --from-generator-version and --to-generator-version must be provided together, or neither should be provided",
443+
code: "VALIDATION_ERROR"
444+
});
433445
}
434446
}),
435447
async (argv) => {
@@ -746,50 +758,84 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
746758
}),
747759
async (argv) => {
748760
if (argv.api != null && argv.docs != null) {
749-
return cliContext.failWithoutThrowing("Cannot specify both --api and --docs. Please choose one.");
761+
return cliContext.failWithoutThrowing(
762+
"Cannot specify both --api and --docs. Please choose one.",
763+
undefined,
764+
{ code: "CONFIG_ERROR" }
765+
);
750766
}
751767
if (argv.id != null && !argv.preview) {
752-
return cliContext.failWithoutThrowing("The --id flag can only be used with --preview.");
768+
return cliContext.failWithoutThrowing("The --id flag can only be used with --preview.", undefined, {
769+
code: "CONFIG_ERROR"
770+
});
753771
}
754772
if (argv.id != null && argv.docs == null) {
755-
return cliContext.failWithoutThrowing("The --id flag can only be used with --docs.");
773+
return cliContext.failWithoutThrowing("The --id flag can only be used with --docs.", undefined, {
774+
code: "CONFIG_ERROR"
775+
});
756776
}
757777
if (argv.skipUpload && !argv.preview) {
758-
return cliContext.failWithoutThrowing("The --skip-upload flag can only be used with --preview.");
778+
return cliContext.failWithoutThrowing(
779+
"The --skip-upload flag can only be used with --preview.",
780+
undefined,
781+
{ code: "CONFIG_ERROR" }
782+
);
759783
}
760784
if (argv.skipUpload && argv.docs == null) {
761-
return cliContext.failWithoutThrowing("The --skip-upload flag can only be used with --docs.");
785+
return cliContext.failWithoutThrowing(
786+
"The --skip-upload flag can only be used with --docs.",
787+
undefined,
788+
{ code: "CONFIG_ERROR" }
789+
);
762790
}
763791
if (argv.fernignore != null && (argv.local || argv.runner != null)) {
764792
return cliContext.failWithoutThrowing(
765-
"The --fernignore flag is not supported with local generation (--local or --runner). It can only be used with remote generation."
793+
"The --fernignore flag is not supported with local generation (--local or --runner). It can only be used with remote generation.",
794+
undefined,
795+
{ code: "CONFIG_ERROR" }
766796
);
767797
}
768798
if (argv["skip-fernignore"] && argv.fernignore != null) {
769799
return cliContext.failWithoutThrowing(
770-
"The --skip-fernignore and --fernignore flags cannot be used together."
800+
"The --skip-fernignore and --fernignore flags cannot be used together.",
801+
undefined,
802+
{ code: "CONFIG_ERROR" }
771803
);
772804
}
773805
if (argv["dynamic-ir-only"] && (argv.local || argv.runner != null)) {
774806
return cliContext.failWithoutThrowing(
775-
"The --dynamic-ir-only flag is not supported with local generation (--local or --runner). It can only be used with remote generation."
807+
"The --dynamic-ir-only flag is not supported with local generation (--local or --runner). It can only be used with remote generation.",
808+
undefined,
809+
{ code: "CONFIG_ERROR" }
776810
);
777811
}
778812
if (argv["dynamic-ir-only"] && argv.version == null) {
779813
return cliContext.failWithoutThrowing(
780-
"The --dynamic-ir-only flag requires a version to be specified with --version."
814+
"The --dynamic-ir-only flag requires a version to be specified with --version.",
815+
undefined,
816+
{ code: "CONFIG_ERROR" }
781817
);
782818
}
783819
if (argv["dynamic-ir-only"] && argv.docs != null) {
784820
return cliContext.failWithoutThrowing(
785-
"The --dynamic-ir-only flag can only be used for API generation, not docs generation."
821+
"The --dynamic-ir-only flag can only be used for API generation, not docs generation.",
822+
undefined,
823+
{ code: "CONFIG_ERROR" }
786824
);
787825
}
788826
if (argv.output != null && !argv.preview) {
789-
return cliContext.failWithoutThrowing("The --output flag currently only works with --preview.");
827+
return cliContext.failWithoutThrowing(
828+
"The --output flag currently only works with --preview.",
829+
undefined,
830+
{ code: "CONFIG_ERROR" }
831+
);
790832
}
791833
if (argv.output != null && argv.docs != null) {
792-
return cliContext.failWithoutThrowing("The --output flag is not supported for docs generation.");
834+
return cliContext.failWithoutThrowing(
835+
"The --output flag is not supported for docs generation.",
836+
undefined,
837+
{ code: "CONFIG_ERROR" }
838+
);
793839
}
794840
const correctedGeneratorFilter =
795841
argv.generator != null ? warnAndCorrectIncorrectDockerOrg(argv.generator, cliContext) : undefined;
@@ -1931,7 +1977,7 @@ function addDocsMdCheckCommand(cli: Argv<GlobalCliOptions>, cliContext: CliConte
19311977
});
19321978

19331979
if (project.docsWorkspaces == null) {
1934-
cliContext.failAndThrow("No docs workspace found");
1980+
cliContext.failAndThrow("No docs workspace found", undefined, { code: "CONFIG_ERROR" });
19351981
}
19361982

19371983
const docsWorkspace = project.docsWorkspaces;
@@ -1950,7 +1996,7 @@ function addDocsMdCheckCommand(cli: Argv<GlobalCliOptions>, cliContext: CliConte
19501996
});
19511997

19521998
if (hasErrors) {
1953-
cliContext.failWithoutThrowing();
1999+
cliContext.failWithoutThrowing(undefined, undefined, { code: "VALIDATION_ERROR" });
19542000
}
19552001
}
19562002
);
@@ -2205,7 +2251,7 @@ function addBetaCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
22052251
await runCliV2(v2Args);
22062252
} catch (error) {
22072253
cliContext.logger.error("CLI v2 failed:", String(error));
2208-
cliContext.failWithoutThrowing();
2254+
cliContext.failWithoutThrowing(undefined, error, { code: "INTERNAL_ERROR" });
22092255
}
22102256
}
22112257
);
@@ -2350,7 +2396,11 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
23502396

23512397
if (githubRepo == null) {
23522398
return cliContext.failAndThrow(
2353-
"Missing required github config. Either use --group to read from generators.yml, or provide --github directly."
2399+
"Missing required github config. Either use --group to read from generators.yml, or provide --github directly.",
2400+
undefined,
2401+
{
2402+
code: "CONFIG_ERROR"
2403+
}
23542404
);
23552405
}
23562406

@@ -2393,7 +2443,9 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
23932443
}
23942444

23952445
if (result.lockfileContent == null) {
2396-
return cliContext.failAndThrow("Bootstrap succeeded but lockfile content is missing.");
2446+
return cliContext.failAndThrow("Bootstrap succeeded but lockfile content is missing.", undefined, {
2447+
code: "INTERNAL_ERROR"
2448+
});
23972449
}
23982450

23992451
// Send lockfile to Fiddle for server-side PR creation
@@ -2421,18 +2473,24 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
24212473
if (response.status === 404) {
24222474
return cliContext.failAndThrow(
24232475
"The Fern GitHub App is not installed on this repository. " +
2424-
"Install it at https://github.com/apps/fern-api to enable server-side PR creation."
2476+
"Install it at https://github.com/apps/fern-api to enable server-side PR creation.",
2477+
undefined,
2478+
{ code: "CONFIG_ERROR" }
24252479
);
24262480
}
24272481
const body = await response.text();
2428-
return cliContext.failAndThrow(`Failed to create PR via Fern: ${body}`);
2482+
return cliContext.failAndThrow(`Failed to create PR via Fern: ${body}`, undefined, {
2483+
code: "NETWORK_ERROR"
2484+
});
24292485
}
24302486

24312487
const data = (await response.json()) as { prUrl: string };
24322488
cliContext.logger.info(`\nPR created: ${data.prUrl}`);
24332489
cliContext.logger.info("Merge the PR to enable Replay for this repository.");
24342490
} catch (error) {
2435-
cliContext.failAndThrow(`Failed to initialize Replay: ${extractErrorMessage(error)}`);
2491+
cliContext.failAndThrow(`Failed to initialize Replay: ${extractErrorMessage(error)}`, error, {
2492+
code: "NETWORK_ERROR"
2493+
});
24362494
}
24372495
}
24382496
);
@@ -2499,12 +2557,18 @@ function addReplayResolveCommand(cli: Argv<GlobalCliOptions>, cliContext: CliCon
24992557
}
25002558
cliContext.logger.warn(`Resolve them first, then run \`fern replay resolve\` again.`);
25012559
} else {
2502-
cliContext.failAndThrow(`Resolve failed: ${result.reason ?? "unknown error"}`);
2560+
cliContext.failAndThrow(
2561+
`Resolve failed: ${result.reason ?? "unknown error"}`,
2562+
undefined,
2563+
{ code: "INTERNAL_ERROR" }
2564+
);
25032565
}
25042566
}
25052567
}
25062568
} catch (error) {
2507-
cliContext.failAndThrow(`Failed to resolve: ${extractErrorMessage(error)}`);
2569+
cliContext.failAndThrow(`Failed to resolve: ${extractErrorMessage(error)}`, error, {
2570+
code: "INTERNAL_ERROR"
2571+
});
25082572
}
25092573
}
25102574
);
@@ -2764,7 +2828,7 @@ function parseOwnerRepo(githubRepo: string): { owner: string; repo: string } {
27642828
const owner = parts[parts.length - 2];
27652829
const repo = parts[parts.length - 1];
27662830
if (owner == null || repo == null) {
2767-
throw new Error(`Could not parse owner/repo from: ${githubRepo}`);
2831+
throw new CliError({ message: `Could not parse owner/repo from: ${githubRepo}`, code: "PARSE_ERROR" });
27682832
}
27692833
return { owner, repo };
27702834
}

packages/cli/cli/src/cliV2.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,9 @@ export function addGeneratorCommands(cli: Argv<GlobalCliOptions>, cliContext: Cl
312312
if (generator == null) {
313313
const maybeApiFilter = argv.api ? ` for API ${argv.api}` : "";
314314
cliContext.failAndThrow(
315-
`Generator ${argv.generator}, in group ${argv.group}${maybeApiFilter} was not found.`
315+
`Generator ${argv.generator}, in group ${argv.group}${maybeApiFilter} was not found.`,
316+
undefined,
317+
{ code: "CONFIG_ERROR" }
316318
);
317319
}
318320

@@ -365,7 +367,8 @@ export function addGeneratorCommands(cli: Argv<GlobalCliOptions>, cliContext: Cl
365367
} catch (error) {
366368
cliContext.failAndThrow(
367369
`Could not write file to the specified location: ${argv.output}`,
368-
error
370+
error,
371+
{ code: "CONFIG_ERROR" }
369372
);
370373
}
371374
}

packages/cli/cli/src/commands/diff/diff.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export async function diff({
5656

5757
const nextVersion = semver.inc(fromVersion, bump);
5858
if (!nextVersion) {
59-
context.failWithoutThrowing(`Invalid current version: ${fromVersion}`);
59+
context.failWithoutThrowing(`Invalid current version: ${fromVersion}`, undefined, { code: "VERSION_ERROR" });
6060
throw new TaskAbortSignal();
6161
}
6262
return { bump, nextVersion, errors };
@@ -73,13 +73,15 @@ async function readIr({
7373
}): Promise<IntermediateRepresentation> {
7474
const absoluteFilepath = AbsoluteFilePath.of(resolve(cwd(), filepath));
7575
if (!(await doesPathExist(absoluteFilepath, "file"))) {
76-
context.failWithoutThrowing(`File not found: ${absoluteFilepath}`);
76+
context.failWithoutThrowing(`File not found: ${absoluteFilepath}`, undefined, { code: "CONFIG_ERROR" });
7777
throw new TaskAbortSignal();
7878
}
7979
const ir = await streamObjectFromFile(absoluteFilepath);
8080
const parsed = serialization.IntermediateRepresentation.parse(ir);
8181
if (!parsed.ok) {
82-
context.failWithoutThrowing(`Invalid --${flagName}; expected a filepath containing a valid IR`);
82+
context.failWithoutThrowing(`Invalid --${flagName}; expected a filepath containing a valid IR`, undefined, {
83+
code: "PARSE_ERROR"
84+
});
8385
throw new TaskAbortSignal();
8486
}
8587
return parsed.value;
@@ -161,7 +163,9 @@ export function diffGeneratorVersions(
161163
errors
162164
};
163165
} catch (error) {
164-
context.failWithoutThrowing(`Error diffing generator versions ${from} and ${to}: ${error}`);
166+
context.failWithoutThrowing(`Error diffing generator versions ${from} and ${to}: ${error}`, undefined, {
167+
code: "INTERNAL_ERROR"
168+
});
165169
throw new TaskAbortSignal();
166170
}
167171
}

0 commit comments

Comments
 (0)