Skip to content

Commit a1e5e34

Browse files
committed
chore(cli): migrate @fern-api/cli to use CliError
Made-with: Cursor
1 parent 6e6d684 commit a1e5e34

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
@@ -173,7 +173,7 @@ async function tryRunCli(cliContext: CliContext) {
173173
cliContext.logger.info(cliContext.environment.packageVersion);
174174
} else {
175175
cli.showHelp();
176-
cliContext.failAndThrow();
176+
cliContext.failAndThrow(undefined, undefined, { code: "CONFIG_ERROR" });
177177
}
178178
}
179179
)
@@ -319,14 +319,22 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
319319
}
320320
}
321321
if (argv.api != null && argv.docs != null) {
322-
return cliContext.failWithoutThrowing("Cannot specify both --api and --docs. Please choose one.");
322+
return cliContext.failWithoutThrowing(
323+
"Cannot specify both --api and --docs. Please choose one.",
324+
undefined,
325+
{ code: "CONFIG_ERROR" }
326+
);
323327
} else if (argv.readme != null && argv.mintlify != null) {
324328
return cliContext.failWithoutThrowing(
325-
"Cannot specify both --readme and --mintlify. Please choose one."
329+
"Cannot specify both --readme and --mintlify. Please choose one.",
330+
undefined,
331+
{ code: "CONFIG_ERROR" }
326332
);
327333
} else if (argv.openapi != null && argv["fern-definition"] === true) {
328334
return cliContext.failWithoutThrowing(
329-
"Cannot specify both --openapi and --fern-definition. Please choose one."
335+
"Cannot specify both --openapi and --fern-definition. Please choose one.",
336+
undefined,
337+
{ code: "CONFIG_ERROR" }
330338
);
331339
} else if (argv.readme != null) {
332340
await cliContext.runTask(async (context) => {
@@ -361,7 +369,7 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
361369
const result = await loadOpenAPIFromUrl({ url: argv.openapi, logger: cliContext.logger });
362370

363371
if (result.status === LoadOpenAPIStatus.Failure) {
364-
cliContext.failAndThrow(result.errorMessage);
372+
cliContext.failAndThrow(result.errorMessage, undefined, { code: "NETWORK_ERROR" });
365373
}
366374

367375
const tmpFilepath = result.filePath;
@@ -371,7 +379,9 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
371379
}
372380
const pathExists = await doesPathExist(absoluteOpenApiPath);
373381
if (!pathExists) {
374-
cliContext.failAndThrow(`${absoluteOpenApiPath} does not exist`);
382+
cliContext.failAndThrow(`${absoluteOpenApiPath} does not exist`, undefined, {
383+
code: "CONFIG_ERROR"
384+
});
375385
}
376386
}
377387
await cliContext.runTask(async (context) => {
@@ -424,9 +434,11 @@ function addDiffCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
424434
})
425435
.middleware((argv) => {
426436
if (!haveSameNullishness(argv.fromGeneratorVersion, argv.toGeneratorVersion)) {
427-
throw new Error(
428-
"Both --from-generator-version and --to-generator-version must be provided together, or neither should be provided"
429-
);
437+
throw new CliError({
438+
message:
439+
"Both --from-generator-version and --to-generator-version must be provided together, or neither should be provided",
440+
code: "VALIDATION_ERROR"
441+
});
430442
}
431443
}),
432444
async (argv) => {
@@ -743,50 +755,84 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
743755
}),
744756
async (argv) => {
745757
if (argv.api != null && argv.docs != null) {
746-
return cliContext.failWithoutThrowing("Cannot specify both --api and --docs. Please choose one.");
758+
return cliContext.failWithoutThrowing(
759+
"Cannot specify both --api and --docs. Please choose one.",
760+
undefined,
761+
{ code: "CONFIG_ERROR" }
762+
);
747763
}
748764
if (argv.id != null && !argv.preview) {
749-
return cliContext.failWithoutThrowing("The --id flag can only be used with --preview.");
765+
return cliContext.failWithoutThrowing("The --id flag can only be used with --preview.", undefined, {
766+
code: "CONFIG_ERROR"
767+
});
750768
}
751769
if (argv.id != null && argv.docs == null) {
752-
return cliContext.failWithoutThrowing("The --id flag can only be used with --docs.");
770+
return cliContext.failWithoutThrowing("The --id flag can only be used with --docs.", undefined, {
771+
code: "CONFIG_ERROR"
772+
});
753773
}
754774
if (argv.skipUpload && !argv.preview) {
755-
return cliContext.failWithoutThrowing("The --skip-upload flag can only be used with --preview.");
775+
return cliContext.failWithoutThrowing(
776+
"The --skip-upload flag can only be used with --preview.",
777+
undefined,
778+
{ code: "CONFIG_ERROR" }
779+
);
756780
}
757781
if (argv.skipUpload && argv.docs == null) {
758-
return cliContext.failWithoutThrowing("The --skip-upload flag can only be used with --docs.");
782+
return cliContext.failWithoutThrowing(
783+
"The --skip-upload flag can only be used with --docs.",
784+
undefined,
785+
{ code: "CONFIG_ERROR" }
786+
);
759787
}
760788
if (argv.fernignore != null && (argv.local || argv.runner != null)) {
761789
return cliContext.failWithoutThrowing(
762-
"The --fernignore flag is not supported with local generation (--local or --runner). It can only be used with remote generation."
790+
"The --fernignore flag is not supported with local generation (--local or --runner). It can only be used with remote generation.",
791+
undefined,
792+
{ code: "CONFIG_ERROR" }
763793
);
764794
}
765795
if (argv["skip-fernignore"] && argv.fernignore != null) {
766796
return cliContext.failWithoutThrowing(
767-
"The --skip-fernignore and --fernignore flags cannot be used together."
797+
"The --skip-fernignore and --fernignore flags cannot be used together.",
798+
undefined,
799+
{ code: "CONFIG_ERROR" }
768800
);
769801
}
770802
if (argv["dynamic-ir-only"] && (argv.local || argv.runner != null)) {
771803
return cliContext.failWithoutThrowing(
772-
"The --dynamic-ir-only flag is not supported with local generation (--local or --runner). It can only be used with remote generation."
804+
"The --dynamic-ir-only flag is not supported with local generation (--local or --runner). It can only be used with remote generation.",
805+
undefined,
806+
{ code: "CONFIG_ERROR" }
773807
);
774808
}
775809
if (argv["dynamic-ir-only"] && argv.version == null) {
776810
return cliContext.failWithoutThrowing(
777-
"The --dynamic-ir-only flag requires a version to be specified with --version."
811+
"The --dynamic-ir-only flag requires a version to be specified with --version.",
812+
undefined,
813+
{ code: "CONFIG_ERROR" }
778814
);
779815
}
780816
if (argv["dynamic-ir-only"] && argv.docs != null) {
781817
return cliContext.failWithoutThrowing(
782-
"The --dynamic-ir-only flag can only be used for API generation, not docs generation."
818+
"The --dynamic-ir-only flag can only be used for API generation, not docs generation.",
819+
undefined,
820+
{ code: "CONFIG_ERROR" }
783821
);
784822
}
785823
if (argv.output != null && !argv.preview) {
786-
return cliContext.failWithoutThrowing("The --output flag currently only works with --preview.");
824+
return cliContext.failWithoutThrowing(
825+
"The --output flag currently only works with --preview.",
826+
undefined,
827+
{ code: "CONFIG_ERROR" }
828+
);
787829
}
788830
if (argv.output != null && argv.docs != null) {
789-
return cliContext.failWithoutThrowing("The --output flag is not supported for docs generation.");
831+
return cliContext.failWithoutThrowing(
832+
"The --output flag is not supported for docs generation.",
833+
undefined,
834+
{ code: "CONFIG_ERROR" }
835+
);
790836
}
791837
const correctedGeneratorFilter =
792838
argv.generator != null ? warnAndCorrectIncorrectDockerOrg(argv.generator, cliContext) : undefined;
@@ -1928,7 +1974,7 @@ function addDocsMdCheckCommand(cli: Argv<GlobalCliOptions>, cliContext: CliConte
19281974
});
19291975

19301976
if (project.docsWorkspaces == null) {
1931-
cliContext.failAndThrow("No docs workspace found");
1977+
cliContext.failAndThrow("No docs workspace found", undefined, { code: "CONFIG_ERROR" });
19321978
}
19331979

19341980
const docsWorkspace = project.docsWorkspaces;
@@ -1947,7 +1993,7 @@ function addDocsMdCheckCommand(cli: Argv<GlobalCliOptions>, cliContext: CliConte
19471993
});
19481994

19491995
if (hasErrors) {
1950-
cliContext.failWithoutThrowing();
1996+
cliContext.failWithoutThrowing(undefined, undefined, { code: "VALIDATION_ERROR" });
19511997
}
19521998
}
19531999
);
@@ -2194,7 +2240,7 @@ function addBetaCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
21942240
await runCliV2(v2Args);
21952241
} catch (error) {
21962242
cliContext.logger.error("CLI v2 failed:", String(error));
2197-
cliContext.failWithoutThrowing();
2243+
cliContext.failWithoutThrowing(undefined, error, { code: "INTERNAL_ERROR" });
21982244
}
21992245
}
22002246
);
@@ -2339,7 +2385,11 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
23392385

23402386
if (githubRepo == null) {
23412387
return cliContext.failAndThrow(
2342-
"Missing required github config. Either use --group to read from generators.yml, or provide --github directly."
2388+
"Missing required github config. Either use --group to read from generators.yml, or provide --github directly.",
2389+
undefined,
2390+
{
2391+
code: "CONFIG_ERROR"
2392+
}
23432393
);
23442394
}
23452395

@@ -2382,7 +2432,9 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
23822432
}
23832433

23842434
if (result.lockfileContent == null) {
2385-
return cliContext.failAndThrow("Bootstrap succeeded but lockfile content is missing.");
2435+
return cliContext.failAndThrow("Bootstrap succeeded but lockfile content is missing.", undefined, {
2436+
code: "INTERNAL_ERROR"
2437+
});
23862438
}
23872439

23882440
// Send lockfile to Fiddle for server-side PR creation
@@ -2410,18 +2462,24 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
24102462
if (response.status === 404) {
24112463
return cliContext.failAndThrow(
24122464
"The Fern GitHub App is not installed on this repository. " +
2413-
"Install it at https://github.com/apps/fern-api to enable server-side PR creation."
2465+
"Install it at https://github.com/apps/fern-api to enable server-side PR creation.",
2466+
undefined,
2467+
{ code: "CONFIG_ERROR" }
24142468
);
24152469
}
24162470
const body = await response.text();
2417-
return cliContext.failAndThrow(`Failed to create PR via Fern: ${body}`);
2471+
return cliContext.failAndThrow(`Failed to create PR via Fern: ${body}`, undefined, {
2472+
code: "NETWORK_ERROR"
2473+
});
24182474
}
24192475

24202476
const data = (await response.json()) as { prUrl: string };
24212477
cliContext.logger.info(`\nPR created: ${data.prUrl}`);
24222478
cliContext.logger.info("Merge the PR to enable Replay for this repository.");
24232479
} catch (error) {
2424-
cliContext.failAndThrow(`Failed to initialize Replay: ${extractErrorMessage(error)}`);
2480+
cliContext.failAndThrow(`Failed to initialize Replay: ${extractErrorMessage(error)}`, error, {
2481+
code: "NETWORK_ERROR"
2482+
});
24252483
}
24262484
}
24272485
);
@@ -2488,12 +2546,18 @@ function addReplayResolveCommand(cli: Argv<GlobalCliOptions>, cliContext: CliCon
24882546
}
24892547
cliContext.logger.warn(`Resolve them first, then run \`fern replay resolve\` again.`);
24902548
} else {
2491-
cliContext.failAndThrow(`Resolve failed: ${result.reason ?? "unknown error"}`);
2549+
cliContext.failAndThrow(
2550+
`Resolve failed: ${result.reason ?? "unknown error"}`,
2551+
undefined,
2552+
{ code: "INTERNAL_ERROR" }
2553+
);
24922554
}
24932555
}
24942556
}
24952557
} catch (error) {
2496-
cliContext.failAndThrow(`Failed to resolve: ${extractErrorMessage(error)}`);
2558+
cliContext.failAndThrow(`Failed to resolve: ${extractErrorMessage(error)}`, error, {
2559+
code: "INTERNAL_ERROR"
2560+
});
24972561
}
24982562
}
24992563
);
@@ -2753,7 +2817,7 @@ function parseOwnerRepo(githubRepo: string): { owner: string; repo: string } {
27532817
const owner = parts[parts.length - 2];
27542818
const repo = parts[parts.length - 1];
27552819
if (owner == null || repo == null) {
2756-
throw new Error(`Could not parse owner/repo from: ${githubRepo}`);
2820+
throw new CliError({ message: `Could not parse owner/repo from: ${githubRepo}`, code: "PARSE_ERROR" });
27572821
}
27582822
return { owner, repo };
27592823
}

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)