Skip to content

Commit 16fa7e1

Browse files
committed
chore(cli): migrate @fern-api/cli to use CliError
Made-with: Cursor
1 parent 8e60a36 commit 16fa7e1

42 files changed

Lines changed: 509 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: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ async function tryRunCli(cliContext: CliContext) {
192192
cliContext.logger.info(cliContext.environment.packageVersion);
193193
} else {
194194
cli.showHelp();
195-
cliContext.failAndThrow();
195+
cliContext.failAndThrow(undefined, undefined, { code: "CONFIG_ERROR" });
196196
}
197197
}
198198
)
@@ -338,14 +338,22 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
338338
}
339339
}
340340
if (argv.api != null && argv.docs != null) {
341-
return cliContext.failWithoutThrowing("Cannot specify both --api and --docs. Please choose one.");
341+
return cliContext.failWithoutThrowing(
342+
"Cannot specify both --api and --docs. Please choose one.",
343+
undefined,
344+
{ code: "CONFIG_ERROR" }
345+
);
342346
} else if (argv.readme != null && argv.mintlify != null) {
343347
return cliContext.failWithoutThrowing(
344-
"Cannot specify both --readme and --mintlify. Please choose one."
348+
"Cannot specify both --readme and --mintlify. Please choose one.",
349+
undefined,
350+
{ code: "CONFIG_ERROR" }
345351
);
346352
} else if (argv.openapi != null && argv["fern-definition"] === true) {
347353
return cliContext.failWithoutThrowing(
348-
"Cannot specify both --openapi and --fern-definition. Please choose one."
354+
"Cannot specify both --openapi and --fern-definition. Please choose one.",
355+
undefined,
356+
{ code: "CONFIG_ERROR" }
349357
);
350358
} else if (argv.readme != null) {
351359
await cliContext.runTask(async (context) => {
@@ -380,7 +388,7 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
380388
const result = await loadOpenAPIFromUrl({ url: argv.openapi, logger: cliContext.logger });
381389

382390
if (result.status === LoadOpenAPIStatus.Failure) {
383-
cliContext.failAndThrow(result.errorMessage);
391+
cliContext.failAndThrow(result.errorMessage, undefined, { code: "NETWORK_ERROR" });
384392
}
385393

386394
const tmpFilepath = result.filePath;
@@ -390,7 +398,9 @@ function addInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
390398
}
391399
const pathExists = await doesPathExist(absoluteOpenApiPath);
392400
if (!pathExists) {
393-
cliContext.failAndThrow(`${absoluteOpenApiPath} does not exist`);
401+
cliContext.failAndThrow(`${absoluteOpenApiPath} does not exist`, undefined, {
402+
code: "CONFIG_ERROR"
403+
});
394404
}
395405
}
396406
await cliContext.runTask(async (context) => {
@@ -443,9 +453,11 @@ function addDiffCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
443453
})
444454
.middleware((argv) => {
445455
if (!haveSameNullishness(argv.fromGeneratorVersion, argv.toGeneratorVersion)) {
446-
throw new Error(
447-
"Both --from-generator-version and --to-generator-version must be provided together, or neither should be provided"
448-
);
456+
throw new CliError({
457+
message:
458+
"Both --from-generator-version and --to-generator-version must be provided together, or neither should be provided",
459+
code: "VALIDATION_ERROR"
460+
});
449461
}
450462
}),
451463
async (argv) => {
@@ -762,50 +774,84 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
762774
}),
763775
async (argv) => {
764776
if (argv.api != null && argv.docs != null) {
765-
return cliContext.failWithoutThrowing("Cannot specify both --api and --docs. Please choose one.");
777+
return cliContext.failWithoutThrowing(
778+
"Cannot specify both --api and --docs. Please choose one.",
779+
undefined,
780+
{ code: "CONFIG_ERROR" }
781+
);
766782
}
767783
if (argv.id != null && !argv.preview) {
768-
return cliContext.failWithoutThrowing("The --id flag can only be used with --preview.");
784+
return cliContext.failWithoutThrowing("The --id flag can only be used with --preview.", undefined, {
785+
code: "CONFIG_ERROR"
786+
});
769787
}
770788
if (argv.id != null && argv.docs == null) {
771-
return cliContext.failWithoutThrowing("The --id flag can only be used with --docs.");
789+
return cliContext.failWithoutThrowing("The --id flag can only be used with --docs.", undefined, {
790+
code: "CONFIG_ERROR"
791+
});
772792
}
773793
if (argv.skipUpload && !argv.preview) {
774-
return cliContext.failWithoutThrowing("The --skip-upload flag can only be used with --preview.");
794+
return cliContext.failWithoutThrowing(
795+
"The --skip-upload flag can only be used with --preview.",
796+
undefined,
797+
{ code: "CONFIG_ERROR" }
798+
);
775799
}
776800
if (argv.skipUpload && argv.docs == null) {
777-
return cliContext.failWithoutThrowing("The --skip-upload flag can only be used with --docs.");
801+
return cliContext.failWithoutThrowing(
802+
"The --skip-upload flag can only be used with --docs.",
803+
undefined,
804+
{ code: "CONFIG_ERROR" }
805+
);
778806
}
779807
if (argv.fernignore != null && (argv.local || argv.runner != null)) {
780808
return cliContext.failWithoutThrowing(
781-
"The --fernignore flag is not supported with local generation (--local or --runner). It can only be used with remote generation."
809+
"The --fernignore flag is not supported with local generation (--local or --runner). It can only be used with remote generation.",
810+
undefined,
811+
{ code: "CONFIG_ERROR" }
782812
);
783813
}
784814
if (argv["skip-fernignore"] && argv.fernignore != null) {
785815
return cliContext.failWithoutThrowing(
786-
"The --skip-fernignore and --fernignore flags cannot be used together."
816+
"The --skip-fernignore and --fernignore flags cannot be used together.",
817+
undefined,
818+
{ code: "CONFIG_ERROR" }
787819
);
788820
}
789821
if (argv["dynamic-ir-only"] && (argv.local || argv.runner != null)) {
790822
return cliContext.failWithoutThrowing(
791-
"The --dynamic-ir-only flag is not supported with local generation (--local or --runner). It can only be used with remote generation."
823+
"The --dynamic-ir-only flag is not supported with local generation (--local or --runner). It can only be used with remote generation.",
824+
undefined,
825+
{ code: "CONFIG_ERROR" }
792826
);
793827
}
794828
if (argv["dynamic-ir-only"] && argv.version == null) {
795829
return cliContext.failWithoutThrowing(
796-
"The --dynamic-ir-only flag requires a version to be specified with --version."
830+
"The --dynamic-ir-only flag requires a version to be specified with --version.",
831+
undefined,
832+
{ code: "CONFIG_ERROR" }
797833
);
798834
}
799835
if (argv["dynamic-ir-only"] && argv.docs != null) {
800836
return cliContext.failWithoutThrowing(
801-
"The --dynamic-ir-only flag can only be used for API generation, not docs generation."
837+
"The --dynamic-ir-only flag can only be used for API generation, not docs generation.",
838+
undefined,
839+
{ code: "CONFIG_ERROR" }
802840
);
803841
}
804842
if (argv.output != null && !argv.preview) {
805-
return cliContext.failWithoutThrowing("The --output flag currently only works with --preview.");
843+
return cliContext.failWithoutThrowing(
844+
"The --output flag currently only works with --preview.",
845+
undefined,
846+
{ code: "CONFIG_ERROR" }
847+
);
806848
}
807849
if (argv.output != null && argv.docs != null) {
808-
return cliContext.failWithoutThrowing("The --output flag is not supported for docs generation.");
850+
return cliContext.failWithoutThrowing(
851+
"The --output flag is not supported for docs generation.",
852+
undefined,
853+
{ code: "CONFIG_ERROR" }
854+
);
809855
}
810856
const correctedGeneratorFilter =
811857
argv.generator != null ? warnAndCorrectIncorrectDockerOrg(argv.generator, cliContext) : undefined;
@@ -1942,7 +1988,7 @@ function addDocsMdCheckCommand(cli: Argv<GlobalCliOptions>, cliContext: CliConte
19421988
});
19431989

19441990
if (project.docsWorkspaces == null) {
1945-
cliContext.failAndThrow("No docs workspace found");
1991+
cliContext.failAndThrow("No docs workspace found", undefined, { code: "CONFIG_ERROR" });
19461992
}
19471993

19481994
const docsWorkspace = project.docsWorkspaces;
@@ -1961,7 +2007,7 @@ function addDocsMdCheckCommand(cli: Argv<GlobalCliOptions>, cliContext: CliConte
19612007
});
19622008

19632009
if (hasErrors) {
1964-
cliContext.failWithoutThrowing();
2010+
cliContext.failWithoutThrowing(undefined, undefined, { code: "VALIDATION_ERROR" });
19652011
}
19662012
}
19672013
);
@@ -2208,7 +2254,7 @@ function addBetaCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
22082254
await runCliV2(v2Args);
22092255
} catch (error) {
22102256
cliContext.logger.error("CLI v2 failed:", String(error));
2211-
cliContext.failWithoutThrowing();
2257+
cliContext.failWithoutThrowing(undefined, error, { code: "INTERNAL_ERROR" });
22122258
}
22132259
}
22142260
);
@@ -2354,7 +2400,9 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
23542400
githubRepo != null
23552401
? "Repository found but no token. Pass --token or set GITHUB_TOKEN environment variable."
23562402
: "Either use --group to read from generators.yml, or provide --github and --token directly.";
2357-
return cliContext.failAndThrow(`Missing required github config. ${hint}`);
2403+
return cliContext.failAndThrow(`Missing required github config. ${hint}`, undefined, {
2404+
code: "CONFIG_ERROR"
2405+
});
23582406
}
23592407

23602408
cliContext.logger.info(`Initializing Replay for: ${githubRepo}`);
@@ -2390,7 +2438,9 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
23902438
}
23912439

23922440
if (result.lockfileContent == null) {
2393-
return cliContext.failAndThrow("Bootstrap succeeded but lockfile content is missing.");
2441+
return cliContext.failAndThrow("Bootstrap succeeded but lockfile content is missing.", undefined, {
2442+
code: "INTERNAL_ERROR"
2443+
});
23942444
}
23952445

23962446
// Send lockfile to Fiddle for server-side PR creation
@@ -2418,18 +2468,24 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
24182468
if (response.status === 404) {
24192469
return cliContext.failAndThrow(
24202470
"The Fern GitHub App is not installed on this repository. " +
2421-
"Install it at https://github.com/apps/fern-api to enable server-side PR creation."
2471+
"Install it at https://github.com/apps/fern-api to enable server-side PR creation.",
2472+
undefined,
2473+
{ code: "CONFIG_ERROR" }
24222474
);
24232475
}
24242476
const body = await response.text();
2425-
return cliContext.failAndThrow(`Failed to create PR via Fern: ${body}`);
2477+
return cliContext.failAndThrow(`Failed to create PR via Fern: ${body}`, undefined, {
2478+
code: "NETWORK_ERROR"
2479+
});
24262480
}
24272481

24282482
const data = (await response.json()) as { prUrl: string };
24292483
cliContext.logger.info(`\nPR created: ${data.prUrl}`);
24302484
cliContext.logger.info("Merge the PR to enable Replay for this repository.");
24312485
} catch (error) {
2432-
cliContext.failAndThrow(`Failed to initialize Replay: ${extractErrorMessage(error)}`);
2486+
cliContext.failAndThrow(`Failed to initialize Replay: ${extractErrorMessage(error)}`, error, {
2487+
code: "NETWORK_ERROR"
2488+
});
24332489
}
24342490
}
24352491
);
@@ -2496,12 +2552,18 @@ function addReplayResolveCommand(cli: Argv<GlobalCliOptions>, cliContext: CliCon
24962552
}
24972553
cliContext.logger.warn(`Resolve them first, then run \`fern replay resolve\` again.`);
24982554
} else {
2499-
cliContext.failAndThrow(`Resolve failed: ${result.reason ?? "unknown error"}`);
2555+
cliContext.failAndThrow(
2556+
`Resolve failed: ${result.reason ?? "unknown error"}`,
2557+
undefined,
2558+
{ code: "INTERNAL_ERROR" }
2559+
);
25002560
}
25012561
}
25022562
}
25032563
} catch (error) {
2504-
cliContext.failAndThrow(`Failed to resolve: ${extractErrorMessage(error)}`);
2564+
cliContext.failAndThrow(`Failed to resolve: ${extractErrorMessage(error)}`, error, {
2565+
code: "INTERNAL_ERROR"
2566+
});
25052567
}
25062568
}
25072569
);
@@ -2516,7 +2578,7 @@ function parseOwnerRepo(githubRepo: string): { owner: string; repo: string } {
25162578
const owner = parts[parts.length - 2];
25172579
const repo = parts[parts.length - 1];
25182580
if (owner == null || repo == null) {
2519-
throw new Error(`Could not parse owner/repo from: ${githubRepo}`);
2581+
throw new CliError({ message: `Could not parse owner/repo from: ${githubRepo}`, code: "PARSE_ERROR" });
25202582
}
25212583
return { owner, repo };
25222584
}

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)