Skip to content

Commit c92a3c7

Browse files
paarthfernclaude
andauthored
feat(cli): improve library docs generation with clean spinner UI (#12337)
* feat(cli): improve library docs generation with clean spinner UI Replace verbose polling logs with interactive task spinners for better UX: - Add "Initiating Library Generation" info message at start - Use clean library name spinners without subtitles - Process libraries sequentially with individual spinners - Move verbose logs to debug level only - Update tests to support new interactive task pattern Provides cleaner, professional UI similar to `fern generate` command. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> * refactor(cli): extract generateSingleLibrary function to reduce nesting Extract end-to-end library generation logic into dedicated function to improve code organization: - Create generateSingleLibrary() function handling complete generation process - Reduce main function nesting from 4+ levels to 2 levels - Improve code readability and maintainability - Bump version to 3.78.0 for spinner UI improvements Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> * feat(cli): enable parallel library docs generation for faster processing Switch from sequential to parallel library generation: - Use Promise.all() to process all libraries simultaneously - Show multiple spinners for concurrent generation - Remove "Initiating Library Generation" message for cleaner UI - Maintain error handling - failed libraries don't block others - Significantly faster completion time for multiple libraries Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> * fix(cli): add missing irVersion and createdAt to version 3.78.0 Add required fields to fix CLI changelog validation: - irVersion: 65 (same as 3.77.1 since no IR changes) - createdAt: "2026-02-13" Resolves: Missing required key "irVersion" validation error Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> * fix(cli): update changelog to reflect actual implementation Update version 3.78.0 changelog to accurately describe current features: - Remove outdated reference to "Initiating Library Generation" message (removed) - Change from "sequential processing" to "parallel processing" (current implementation) - Emphasize concurrent spinners for multiple libraries - Keep focus on cleaner UX and debug-level verbose logs Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com>
1 parent 2c500ab commit c92a3c7

File tree

3 files changed

+117
-54
lines changed

3 files changed

+117
-54
lines changed

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,33 @@ function makeMockCliContext() {
3232
runTask: vi.fn(
3333
async (
3434
fn: (ctx: {
35-
logger: { info: (m: string) => void };
35+
logger: { info: (m: string) => void; debug: (m: string) => void };
3636
failAndThrow: (...args: unknown[]) => never;
37+
runInteractiveTask: (
38+
{ name }: { name: string },
39+
taskFn: (interactiveCtx: {
40+
logger: { debug: (m: string) => void };
41+
setSubtitle: (subtitle: string) => void;
42+
failAndThrow: (...args: unknown[]) => never;
43+
}) => Promise<unknown>
44+
) => Promise<boolean>;
3745
}) => unknown
3846
) => {
3947
return fn({
40-
logger: { info: (m: string) => logs.push(m) },
41-
failAndThrow: fail
48+
logger: {
49+
info: (m: string) => logs.push(m),
50+
debug: (m: string) => logs.push(`[DEBUG] ${m}`)
51+
},
52+
failAndThrow: fail,
53+
runInteractiveTask: vi.fn(async ({ name }, taskFn) => {
54+
logs.push(`Starting task: ${name}`);
55+
await taskFn({
56+
logger: { debug: (m: string) => logs.push(`[DEBUG] ${m}`) },
57+
setSubtitle: (subtitle: string) => logs.push(`[SUBTITLE] ${subtitle}`),
58+
failAndThrow: fail
59+
});
60+
return true;
61+
})
4262
});
4363
}
4464
),
@@ -238,7 +258,7 @@ describe("generateLibraryDocs", () => {
238258
})
239259
);
240260

241-
expect(ctx.logs.some((l: string) => l.includes("Generated 1 pages"))).toBe(true);
261+
expect(ctx.logs.some((l: string) => l.includes("Generated library documentation for 1 libraries"))).toBe(true);
242262
});
243263

244264
it("reports failure when generation status is FAILED", async () => {

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

Lines changed: 83 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { FernToken } from "@fern-api/auth";
22
import { docsYml } from "@fern-api/configuration";
33
import { createFdrService } from "@fern-api/core";
44
import { FdrAPI } from "@fern-api/fdr-sdk";
5-
import { resolve } from "@fern-api/fs-utils";
5+
import { AbsoluteFilePath, resolve } from "@fern-api/fs-utils";
66
import { generate } from "@fern-api/library-docs-generator";
77
import { askToLogin } from "@fern-api/login";
88
import { Project } from "@fern-api/project-loader";
9-
import { TaskContext } from "@fern-api/task-context";
9+
import { InteractiveTaskContext, TaskContext } from "@fern-api/task-context";
1010
import chalk from "chalk";
1111

1212
import { CliContext } from "../../cli-context/CliContext.js";
@@ -74,56 +74,87 @@ export async function generateLibraryDocs({ project, cliContext, library }: Gene
7474

7575
const orgId = project.config.organization;
7676

77-
for (const [name, config] of Object.entries(librariesToGenerate)) {
78-
if (config == null) {
79-
continue;
80-
}
81-
if (!isGitLibraryInput(config.input)) {
82-
cliContext.failAndThrow(
83-
`Library '${name}' uses 'path' input which is not yet supported. Please use 'git' input.`
84-
);
85-
return;
86-
}
87-
88-
const resolvedOutputPath = resolve(docsWorkspace.absoluteFilePath, config.output.path);
89-
const gitInput = config.input as docsYml.RawSchemas.GitLibraryInputSchema;
90-
91-
await cliContext.runTask(async (context) => {
92-
const fdr = createFdrService({ token: token.value });
93-
94-
context.logger.info(`Starting generation for library '${name}'...`);
95-
96-
const jobId = await startGeneration(fdr, context, {
97-
orgId,
98-
githubUrl: gitInput.git,
99-
language: config.lang === "python" ? "PYTHON" : "CPP",
100-
packagePath: gitInput.subpath,
101-
name
102-
});
77+
await cliContext.runTask(async (context) => {
78+
const results = await Promise.all(
79+
Object.entries(librariesToGenerate).map(async ([name, config]) => {
80+
if (config == null) {
81+
return false;
82+
}
83+
if (!isGitLibraryInput(config.input)) {
84+
context.failAndThrow(
85+
`Library '${name}' uses 'path' input which is not yet supported. Please use 'git' input.`
86+
);
87+
return false;
88+
}
89+
90+
return generateSingleLibrary({
91+
name,
92+
config,
93+
docsWorkspace,
94+
orgId,
95+
token,
96+
context
97+
});
98+
})
99+
);
103100

104-
await pollForCompletion(fdr, jobId, name, context);
101+
// Log summary of successful generations
102+
const successful = results.filter(Boolean).length;
103+
if (successful > 0) {
104+
context.logger.info(chalk.green(`✓ Generated library documentation for ${successful} libraries`));
105+
}
106+
});
107+
}
105108

106-
const ir = await downloadIr(fdr, jobId, name, context);
109+
async function generateSingleLibrary({
110+
name,
111+
config,
112+
docsWorkspace,
113+
orgId,
114+
token,
115+
context
116+
}: {
117+
name: string;
118+
config: docsYml.RawSchemas.LibraryConfiguration;
119+
docsWorkspace: { absoluteFilePath: AbsoluteFilePath };
120+
orgId: string;
121+
token: FernToken;
122+
context: TaskContext;
123+
}): Promise<boolean> {
124+
const resolvedOutputPath = resolve(docsWorkspace.absoluteFilePath, config.output.path);
125+
const gitInput = config.input as docsYml.RawSchemas.GitLibraryInputSchema;
126+
127+
return context.runInteractiveTask({ name }, async (interactiveTaskContext) => {
128+
const fdr = createFdrService({ token: token.value });
129+
130+
interactiveTaskContext.logger.debug(`Starting generation for library '${name}' from ${gitInput.git}`);
131+
132+
const jobId = await startGeneration(fdr, interactiveTaskContext, {
133+
orgId,
134+
githubUrl: gitInput.git,
135+
language: config.lang === "python" ? "PYTHON" : "CPP",
136+
packagePath: gitInput.subpath,
137+
name
138+
});
107139

108-
context.logger.info("Generating MDX files...");
140+
await pollForCompletion(fdr, jobId, name, interactiveTaskContext);
109141

110-
const generateResult = generate({
111-
ir,
112-
outputDir: resolvedOutputPath,
113-
slug: name,
114-
title: name
115-
});
142+
const ir = await downloadIr(fdr, jobId, name, interactiveTaskContext);
116143

117-
context.logger.info(
118-
chalk.green(`Generated ${generateResult.pageCount} pages for '${name}' at ${resolvedOutputPath}`)
119-
);
144+
const generateResult = generate({
145+
ir,
146+
outputDir: resolvedOutputPath,
147+
slug: name,
148+
title: name
120149
});
121-
}
150+
151+
interactiveTaskContext.logger.debug(`Generated ${generateResult.pageCount} pages at ${resolvedOutputPath}`);
152+
});
122153
}
123154

124155
async function startGeneration(
125156
fdr: FdrService,
126-
context: TaskContext,
157+
context: InteractiveTaskContext,
127158
opts: { orgId: string; githubUrl: string; language: "PYTHON" | "CPP"; packagePath?: string; name: string }
128159
): Promise<FdrAPI.docs.v2.write.LibraryDocsJobId> {
129160
const startResponse = await fdr.docs.v2.write.startLibraryDocsGeneration({
@@ -145,15 +176,15 @@ async function startGeneration(
145176
}
146177

147178
const jobId = startResponse.body.jobId;
148-
context.logger.info(`Generation job started (${jobId}). Polling for completion...`);
179+
context.logger.debug(`Generation job started with ID: ${jobId}`);
149180
return jobId;
150181
}
151182

152183
async function pollForCompletion(
153184
fdr: FdrService,
154185
jobId: FdrAPI.docs.v2.write.LibraryDocsJobId,
155186
libraryName: string,
156-
context: TaskContext
187+
context: InteractiveTaskContext
157188
): Promise<void> {
158189
const deadline = Date.now() + POLL_TIMEOUT_MS;
159190

@@ -172,9 +203,12 @@ async function pollForCompletion(
172203

173204
switch (status.status) {
174205
case "PENDING":
175-
case "PARSING":
176-
context.logger.info(`Status: ${status.status}${status.progress ? ` — ${status.progress}` : ""}`);
206+
context.logger.debug(`Status: PENDING`);
177207
break;
208+
case "PARSING": {
209+
context.logger.debug(`Status: PARSING${status.progress ? ` — ${status.progress}` : ""}`);
210+
break;
211+
}
178212
case "COMPLETED":
179213
return;
180214
case "FAILED":
@@ -195,10 +229,8 @@ async function downloadIr(
195229
fdr: FdrService,
196230
jobId: FdrAPI.docs.v2.write.LibraryDocsJobId,
197231
libraryName: string,
198-
context: TaskContext
232+
context: InteractiveTaskContext
199233
): Promise<FdrAPI.libraryDocs.PythonLibraryDocsIr> {
200-
context.logger.info("Downloading generated IR...");
201-
202234
const resultResponse = await fdr.docs.v2.write.getLibraryDocsResult(jobId);
203235

204236
if (!resultResponse.ok) {
@@ -207,6 +239,7 @@ async function downloadIr(
207239
);
208240
}
209241

242+
context.logger.debug(`Fetching IR from ${resultResponse.body.resultUrl}`);
210243
const irFetchResponse = await fetch(resultResponse.body.resultUrl);
211244
if (!irFetchResponse.ok) {
212245
return context.failAndThrow(
@@ -224,5 +257,6 @@ async function downloadIr(
224257
return context.failAndThrow(`IR has no rootModule for library '${libraryName}'`);
225258
}
226259

260+
context.logger.debug(`Downloaded IR with ${Object.keys(ir.rootModule.submodules).length} submodules`);
227261
return ir;
228262
}

packages/cli/cli/versions.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
2+
- version: 3.78.0
3+
changelogEntry:
4+
- summary: |
5+
Improve library docs generation UI with clean spinner interface.
6+
Replace verbose polling logs with interactive task spinners for better UX.
7+
Enable parallel processing of multiple libraries with concurrent spinners.
8+
Verbose logs moved to debug level only for cleaner output.
9+
type: feat
10+
createdAt: "2026-02-13"
11+
irVersion: 65
212
- version: 3.77.1
313
changelogEntry:
414
- summary: |
@@ -8,7 +18,6 @@
818
type: fix
919
createdAt: "2026-02-13"
1020
irVersion: 65
11-
1221
- version: 3.77.0
1322
changelogEntry:
1423
- summary: |

0 commit comments

Comments
 (0)