Skip to content

Commit aa6da92

Browse files
committed
chore(cli): migrate @fern-api/cli to use CliError
Made-with: Cursor
1 parent 813f087 commit aa6da92

42 files changed

Lines changed: 567 additions & 212 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: CliError.Code.InternalError
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: CliError.Code.InternalError });
131131
}
132132
return generatorResponse.body.displayName;
133133
}

packages/cli/cli/src/cli.ts

Lines changed: 117 additions & 43 deletions
Large diffs are not rendered by default.

packages/cli/cli/src/cliV2.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { GenerationModeFilter, getGeneratorList } from "./commands/generator-lis
1515
import { getGeneratorMetadata } from "./commands/generator-metadata/getGeneratorMetadata.js";
1616
import { getOrganization } from "./commands/organization/getOrganization.js";
1717
import { upgradeGenerator } from "./commands/upgrade/upgradeGenerator.js";
18+
import { CliError } from "@fern-api/task-context";
1819

1920
/**
2021
* Corrects the incorrect "fern-api/" Docker org prefix to "fernapi/" and logs a warning.
@@ -312,7 +313,9 @@ export function addGeneratorCommands(cli: Argv<GlobalCliOptions>, cliContext: Cl
312313
if (generator == null) {
313314
const maybeApiFilter = argv.api ? ` for API ${argv.api}` : "";
314315
cliContext.failAndThrow(
315-
`Generator ${argv.generator}, in group ${argv.group}${maybeApiFilter} was not found.`
316+
`Generator ${argv.generator}, in group ${argv.group}${maybeApiFilter} was not found.`,
317+
undefined,
318+
{ code: CliError.Code.ConfigError }
316319
);
317320
}
318321

@@ -365,7 +368,8 @@ export function addGeneratorCommands(cli: Argv<GlobalCliOptions>, cliContext: Cl
365368
} catch (error) {
366369
cliContext.failAndThrow(
367370
`Could not write file to the specified location: ${argv.output}`,
368-
error
371+
error,
372+
{ code: CliError.Code.ConfigError }
369373
);
370374
}
371375
}

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

Lines changed: 19 additions & 5 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: CliError.Code.VersionError });
6060
throw new TaskAbortSignal();
6161
}
6262
return { bump, nextVersion, errors };
@@ -73,13 +73,25 @@ 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: CliError.Code.ConfigError });
77+
throw new TaskAbortSignal();
78+
}
79+
let ir: unknown;
80+
try {
81+
ir = await streamObjectFromFile(absoluteFilepath);
82+
} catch (error) {
83+
context.failWithoutThrowing(
84+
`Failed to parse IR file ${absoluteFilepath}: ${error instanceof Error ? error.message : String(error)}`,
85+
error,
86+
{ code: CliError.Code.ParseError }
87+
);
7788
throw new TaskAbortSignal();
7889
}
79-
const ir = await streamObjectFromFile(absoluteFilepath);
8090
const parsed = serialization.IntermediateRepresentation.parse(ir);
8191
if (!parsed.ok) {
82-
context.failWithoutThrowing(`Invalid --${flagName}; expected a filepath containing a valid IR`);
92+
context.failWithoutThrowing(`Invalid --${flagName}; expected a filepath containing a valid IR`, undefined, {
93+
code: CliError.Code.ParseError
94+
});
8395
throw new TaskAbortSignal();
8496
}
8597
return parsed.value;
@@ -161,7 +173,9 @@ export function diffGeneratorVersions(
161173
errors
162174
};
163175
} catch (error) {
164-
context.failWithoutThrowing(`Error diffing generator versions ${from} and ${to}: ${error}`);
176+
context.failWithoutThrowing(`Error diffing generator versions ${from} and ${to}: ${error}`, undefined, {
177+
code: CliError.Code.InternalError
178+
});
165179
throw new TaskAbortSignal();
166180
}
167181
}

packages/cli/cli/src/commands/docs-diff/docsDiff.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { FernToken } from "@fern-api/auth";
22
import { AbsoluteFilePath, cwd, doesPathExist, join, RelativeFilePath } from "@fern-api/fs-utils";
33
import { askToLogin } from "@fern-api/login";
44
import { Project } from "@fern-api/project-loader";
5+
import { CliError} from "@fern-api/task-context";
56
import { mkdir, readFile, writeFile } from "fs/promises";
67
import { PNG } from "pngjs";
78
import { Browser, launch } from "puppeteer";
8-
99
import { CliContext } from "../../cli-context/CliContext.js";
1010

1111
interface FileSlugMapping {
@@ -51,7 +51,10 @@ async function getSlugForFiles({
5151

5252
if (!response.ok) {
5353
const errorText = await response.text();
54-
throw new Error(`Failed to get slugs for files: ${response.status} ${response.statusText} - ${errorText}`);
54+
throw new CliError({
55+
message: `Failed to get slugs for files: ${response.status} ${response.statusText} - ${errorText}`,
56+
code: CliError.Code.InternalError
57+
});
5558
}
5659

5760
return (await response.json()) as GetSlugForFileResponse;
@@ -482,12 +485,12 @@ function getProductionUrlInfo(docsConfig: {
482485
instances: Array<{ url: string; "custom-domain"?: string }> | undefined;
483486
}): ProductionUrlInfo {
484487
if (docsConfig.instances == null || docsConfig.instances.length === 0) {
485-
throw new Error("No docs instances configured in docs.yml");
488+
throw new CliError({ message: "No docs instances configured in docs.yml", code: CliError.Code.InternalError });
486489
}
487490

488491
const firstInstance = docsConfig.instances[0];
489492
if (firstInstance == null) {
490-
throw new Error("No docs instances configured in docs.yml");
493+
throw new CliError({ message: "No docs instances configured in docs.yml", code: CliError.Code.InternalError });
491494
}
492495

493496
// Prefer custom-domain if available, otherwise use url
@@ -525,7 +528,9 @@ export async function docsDiff({
525528
}): Promise<DocsDiffOutput> {
526529
const docsWorkspace = project.docsWorkspaces;
527530
if (docsWorkspace == null) {
528-
cliContext.failAndThrow("No docs workspace found. Make sure you have a docs.yml file.");
531+
cliContext.failAndThrow("No docs workspace found. Make sure you have a docs.yml file.", undefined, {
532+
code: CliError.Code.ConfigError
533+
});
529534
return { diffs: [] };
530535
}
531536

@@ -534,7 +539,9 @@ export async function docsDiff({
534539
});
535540

536541
if (token == null) {
537-
cliContext.failAndThrow("Failed to authenticate. Please run 'fern login' first.");
542+
cliContext.failAndThrow("Failed to authenticate. Please run 'fern login' first.", undefined, {
543+
code: CliError.Code.AuthError
544+
});
538545
return { diffs: [] };
539546
}
540547

packages/cli/cli/src/commands/docs-md-generate/generateLibraryDocs.ts

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import type { CppLibraryDocsIr } from "@fern-api/library-docs-generator";
77
import { generate, generateCpp } from "@fern-api/library-docs-generator";
88
import { askToLogin } from "@fern-api/login";
99
import { Project } from "@fern-api/project-loader";
10-
import { InteractiveTaskContext, TaskContext } from "@fern-api/task-context";
10+
import { CliError, InteractiveTaskContext, TaskContext } from "@fern-api/task-context";
11+
1112
import chalk from "chalk";
1213
import { readFile } from "fs/promises";
1314
import { CliContext } from "../../cli-context/CliContext.js";
@@ -77,7 +78,7 @@ function createLibraryDocsClient({ token }: { token: string }): LibraryDocsClien
7778

7879
if (!response.ok) {
7980
const text = await response.text().catch(() => "");
80-
throw new Error(`HTTP ${response.status}: ${text}`);
81+
throw new CliError({ message: `HTTP ${response.status}: ${text}`, code: CliError.Code.NetworkError });
8182
}
8283

8384
return (await response.json()) as T;
@@ -116,7 +117,9 @@ export async function generateLibraryDocs({ project, cliContext, library }: Gene
116117
const docsWorkspace = project.docsWorkspaces;
117118

118119
if (docsWorkspace == null) {
119-
cliContext.failAndThrow("No docs workspace found. Make sure you have a docs.yml file.");
120+
cliContext.failAndThrow("No docs workspace found. Make sure you have a docs.yml file.", undefined, {
121+
code: CliError.Code.ConfigError
122+
});
120123
return;
121124
}
122125

@@ -125,7 +128,9 @@ export async function generateLibraryDocs({ project, cliContext, library }: Gene
125128

126129
if (libraries == null || Object.keys(libraries).length === 0) {
127130
cliContext.failAndThrow(
128-
"No libraries configured in docs.yml. Add a `libraries` section to configure library documentation."
131+
"No libraries configured in docs.yml. Add a `libraries` section to configure library documentation.",
132+
undefined,
133+
{ code: CliError.Code.ConfigError }
129134
);
130135
return;
131136
}
@@ -134,7 +139,9 @@ export async function generateLibraryDocs({ project, cliContext, library }: Gene
134139

135140
if (library != null && libraries[library] == null) {
136141
cliContext.failAndThrow(
137-
`Library '${library}' not found in docs.yml. Available libraries: ${Object.keys(libraries).join(", ")}`
142+
`Library '${library}' not found in docs.yml. Available libraries: ${Object.keys(libraries).join(", ")}`,
143+
undefined,
144+
{ code: CliError.Code.ConfigError }
138145
);
139146
return;
140147
}
@@ -144,7 +151,9 @@ export async function generateLibraryDocs({ project, cliContext, library }: Gene
144151
});
145152

146153
if (token == null) {
147-
cliContext.failAndThrow("Failed to authenticate. Please run 'fern login' first.");
154+
cliContext.failAndThrow("Failed to authenticate. Please run 'fern login' first.", undefined, {
155+
code: CliError.Code.AuthError
156+
});
148157
return;
149158
}
150159

@@ -158,7 +167,9 @@ export async function generateLibraryDocs({ project, cliContext, library }: Gene
158167
}
159168
if (!isGitLibraryInput(config.input)) {
160169
context.failAndThrow(
161-
`Library '${name}' uses 'path' input which is not yet supported. Please use 'git' input.`
170+
`Library '${name}' uses 'path' input which is not yet supported. Please use 'git' input.`,
171+
undefined,
172+
{ code: CliError.Code.ConfigError }
162173
);
163174
return false;
164175
}
@@ -204,7 +215,9 @@ async function generateSingleLibrary({
204215
// Validate language-specific config
205216
if (config.config?.doxyfile != null && config.lang !== "cpp") {
206217
return interactiveTaskContext.failAndThrow(
207-
`Library '${name}': 'doxyfile' config is only valid for lang: cpp`
218+
`Library '${name}': 'doxyfile' config is only valid for lang: cpp`,
219+
undefined,
220+
{ code: CliError.Code.ConfigError }
208221
);
209222
}
210223

@@ -216,14 +229,20 @@ async function generateSingleLibrary({
216229
doxyfileContent = await readFile(doxyfilePath, "utf-8");
217230
} catch {
218231
return interactiveTaskContext.failAndThrow(
219-
`Library '${name}': Could not read Doxyfile at '${config.config.doxyfile}' (resolved to ${doxyfilePath})`
232+
`Library '${name}': Could not read Doxyfile at '${config.config.doxyfile}' (resolved to ${doxyfilePath})`,
233+
undefined,
234+
{ code: CliError.Code.ConfigError }
220235
);
221236
}
222237
}
223238

224239
const language = config.lang === "python" ? "PYTHON" : config.lang === "cpp" ? "CPP" : undefined;
225240
if (language == null) {
226-
return interactiveTaskContext.failAndThrow(`Library '${name}': unsupported language '${config.lang}'`);
241+
return interactiveTaskContext.failAndThrow(
242+
`Library '${name}': unsupported language '${config.lang}'`,
243+
undefined,
244+
{ code: CliError.Code.ConfigError }
245+
);
227246
}
228247

229248
const client = createLibraryDocsClient({ token: token.value });
@@ -268,7 +287,11 @@ async function generateSingleLibrary({
268287
});
269288
interactiveTaskContext.logger.debug(`Generated ${generateResult.pageCount} pages at ${resolvedOutputPath}`);
270289
} else {
271-
return interactiveTaskContext.failAndThrow(`Library '${name}': unsupported language '${config.lang}'`);
290+
return interactiveTaskContext.failAndThrow(
291+
`Library '${name}': unsupported language '${config.lang}'`,
292+
undefined,
293+
{ code: CliError.Code.ConfigError }
294+
);
272295
}
273296
});
274297
}
@@ -302,7 +325,9 @@ async function startGeneration(
302325
return result.jobId;
303326
} catch (error) {
304327
return context.failAndThrow(
305-
`Failed to start generation for library '${opts.name}': ${extractErrorMessage(error)}`
328+
`Failed to start generation for library '${opts.name}': ${extractErrorMessage(error)}`,
329+
error,
330+
{ code: CliError.Code.NetworkError }
306331
);
307332
}
308333
}
@@ -323,7 +348,9 @@ async function pollForCompletion(
323348
status = await client.getLibraryDocsGenerationStatus({ jobId });
324349
} catch (error) {
325350
return context.failAndThrow(
326-
`Failed to check generation status for library '${libraryName}': ${extractErrorMessage(error)}`
351+
`Failed to check generation status for library '${libraryName}': ${extractErrorMessage(error)}`,
352+
error,
353+
{ code: CliError.Code.NetworkError }
327354
);
328355
}
329356

@@ -339,16 +366,24 @@ async function pollForCompletion(
339366
return;
340367
case "FAILED":
341368
return context.failAndThrow(
342-
`Generation failed for library '${libraryName}': ${status.error?.message ?? "Unknown error"} (${status.error?.code ?? "UNKNOWN"})`
369+
`Generation failed for library '${libraryName}': ${status.error?.message ?? "Unknown error"} (${status.error?.code ?? "UNKNOWN"})`,
370+
undefined,
371+
{ code: CliError.Code.InternalError }
343372
);
344373
default:
345374
return context.failAndThrow(
346-
`Unexpected generation status for library '${libraryName}': ${status.status}`
375+
`Unexpected generation status for library '${libraryName}': ${status.status}`,
376+
undefined,
377+
{ code: CliError.Code.InternalError }
347378
);
348379
}
349380
}
350381

351-
return context.failAndThrow(`Generation timed out for library '${libraryName}' after ${POLL_TIMEOUT_MS / 1000}s`);
382+
return context.failAndThrow(
383+
`Generation timed out for library '${libraryName}' after ${POLL_TIMEOUT_MS / 1000}s`,
384+
undefined,
385+
{ code: CliError.Code.NetworkError }
386+
);
352387
}
353388

354389
async function downloadIr(
@@ -364,33 +399,41 @@ async function downloadIr(
364399
resultUrl = result.resultUrl;
365400
} catch (error) {
366401
return context.failAndThrow(
367-
`Failed to fetch generation result for library '${libraryName}': ${extractErrorMessage(error)}`
402+
`Failed to fetch generation result for library '${libraryName}': ${extractErrorMessage(error)}`,
403+
error,
404+
{ code: CliError.Code.NetworkError }
368405
);
369406
}
370407

371408
context.logger.debug(`Fetching IR from ${resultUrl}`);
372409
const irFetchResponse = await fetch(resultUrl);
373410
if (!irFetchResponse.ok) {
374411
return context.failAndThrow(
375-
`Failed to download IR for library '${libraryName}': HTTP ${irFetchResponse.status}`
412+
`Failed to download IR for library '${libraryName}': HTTP ${irFetchResponse.status}`,
413+
undefined,
414+
{ code: CliError.Code.NetworkError }
376415
);
377416
}
378417

379418
const irWrapper = await irFetchResponse.json();
380419
const ir = irWrapper.ir;
381420

382421
if (ir == null) {
383-
return context.failAndThrow(`IR is empty for library '${libraryName}'`);
422+
return context.failAndThrow(`IR is empty for library '${libraryName}'`, undefined, { code: CliError.Code.InternalError });
384423
}
385424

386425
if (language === "CPP") {
387426
if (ir.rootNamespace == null) {
388-
return context.failAndThrow(`IR has no rootNamespace for C++ library '${libraryName}'`);
427+
return context.failAndThrow(`IR has no rootNamespace for C++ library '${libraryName}'`, undefined, {
428+
code: CliError.Code.InternalError
429+
});
389430
}
390431
context.logger.debug(`Downloaded C++ IR for '${libraryName}'`);
391432
} else {
392433
if (ir.rootModule == null) {
393-
return context.failAndThrow(`IR has no rootModule for library '${libraryName}'`);
434+
return context.failAndThrow(`IR has no rootModule for library '${libraryName}'`, undefined, {
435+
code: CliError.Code.InternalError
436+
});
394437
}
395438
context.logger.debug(`Downloaded IR with ${Object.keys(ir.rootModule).length} top-level keys`);
396439
}

0 commit comments

Comments
 (0)