@readme/cli 0.0.26 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +2 -2
  2. package/package.json +5 -3
  3. package/src/cli.js +6 -1
  4. package/src/commands/components.js +288 -0
  5. package/src/commands/import.js +2456 -376
  6. package/src/commands/import.slug-planner.test.js +86 -0
  7. package/src/commands/import.test.js +136 -0
  8. package/src/commands/lint.js +32 -35
  9. package/src/commands/oas-sync.js +60 -75
  10. package/src/commands/pretty.js +314 -77
  11. package/src/prompts/index.js +187 -24
  12. package/src/utils/lint.js +33 -33
  13. package/src/utils/llms.js +224 -0
  14. package/src/utils/llms.test.js +257 -0
  15. package/src/utils/marketplace.js +208 -0
  16. package/src/utils/multi-spinner.js +71 -0
  17. package/src/utils/promise-pool.js +17 -0
  18. package/src/utils/url-segs.js +124 -0
  19. package/src/utils/url-segs.test.js +239 -0
  20. package/src/validators/components.js +1 -0
  21. package/src/validators/duplicates.js +6 -4
  22. package/src/validators/frontmatter.js +11 -0
  23. package/src/validators/oas-reference.js +1 -31
  24. package/src/validators/ordering.js +169 -83
  25. package/vendor/TOOLS.md +1 -1
  26. package/src/dev/.next/app-build-manifest.json +0 -20
  27. package/src/dev/.next/build-manifest.json +0 -31
  28. package/src/dev/.next/cache/.rscinfo +0 -1
  29. package/src/dev/.next/cache/next-devtools-config.json +0 -1
  30. package/src/dev/.next/cache/webpack/client-development/0.pack.gz +0 -0
  31. package/src/dev/.next/cache/webpack/client-development/1.pack.gz +0 -0
  32. package/src/dev/.next/cache/webpack/client-development/10.pack.gz +0 -0
  33. package/src/dev/.next/cache/webpack/client-development/11.pack.gz +0 -0
  34. package/src/dev/.next/cache/webpack/client-development/2.pack.gz +0 -0
  35. package/src/dev/.next/cache/webpack/client-development/3.pack.gz +0 -0
  36. package/src/dev/.next/cache/webpack/client-development/3.pack.gz_ +0 -0
  37. package/src/dev/.next/cache/webpack/client-development/4.pack.gz +0 -0
  38. package/src/dev/.next/cache/webpack/client-development/5.pack.gz +0 -0
  39. package/src/dev/.next/cache/webpack/client-development/5.pack.gz_ +0 -0
  40. package/src/dev/.next/cache/webpack/client-development/6.pack.gz +0 -0
  41. package/src/dev/.next/cache/webpack/client-development/7.pack.gz +0 -0
  42. package/src/dev/.next/cache/webpack/client-development/7.pack.gz_ +0 -0
  43. package/src/dev/.next/cache/webpack/client-development/8.pack.gz +0 -0
  44. package/src/dev/.next/cache/webpack/client-development/9.pack.gz +0 -0
  45. package/src/dev/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
  46. package/src/dev/.next/cache/webpack/client-development-fallback/0.pack.gz +0 -0
  47. package/src/dev/.next/cache/webpack/client-development-fallback/1.pack.gz +0 -0
  48. package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz +0 -0
  49. package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz.old +0 -0
  50. package/src/dev/.next/cache/webpack/edge-server-development/0.pack.gz +0 -0
  51. package/src/dev/.next/cache/webpack/edge-server-development/1.pack.gz +0 -0
  52. package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz +0 -0
  53. package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz.old +0 -0
  54. package/src/dev/.next/cache/webpack/server-development/0.pack.gz +0 -0
  55. package/src/dev/.next/cache/webpack/server-development/1.pack.gz +0 -0
  56. package/src/dev/.next/cache/webpack/server-development/10.pack.gz +0 -0
  57. package/src/dev/.next/cache/webpack/server-development/11.pack.gz +0 -0
  58. package/src/dev/.next/cache/webpack/server-development/12.pack.gz +0 -0
  59. package/src/dev/.next/cache/webpack/server-development/13.pack.gz +0 -0
  60. package/src/dev/.next/cache/webpack/server-development/14.pack.gz +0 -0
  61. package/src/dev/.next/cache/webpack/server-development/15.pack.gz +0 -0
  62. package/src/dev/.next/cache/webpack/server-development/2.pack.gz +0 -0
  63. package/src/dev/.next/cache/webpack/server-development/2.pack.gz_ +0 -0
  64. package/src/dev/.next/cache/webpack/server-development/3.pack.gz +0 -0
  65. package/src/dev/.next/cache/webpack/server-development/3.pack.gz_ +0 -0
  66. package/src/dev/.next/cache/webpack/server-development/4.pack.gz +0 -0
  67. package/src/dev/.next/cache/webpack/server-development/5.pack.gz +0 -0
  68. package/src/dev/.next/cache/webpack/server-development/6.pack.gz +0 -0
  69. package/src/dev/.next/cache/webpack/server-development/6.pack.gz_ +0 -0
  70. package/src/dev/.next/cache/webpack/server-development/7.pack.gz +0 -0
  71. package/src/dev/.next/cache/webpack/server-development/7.pack.gz_ +0 -0
  72. package/src/dev/.next/cache/webpack/server-development/8.pack.gz +0 -0
  73. package/src/dev/.next/cache/webpack/server-development/9.pack.gz +0 -0
  74. package/src/dev/.next/cache/webpack/server-development/9.pack.gz_ +0 -0
  75. package/src/dev/.next/cache/webpack/server-development/index.pack.gz +0 -0
  76. package/src/dev/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
  77. package/src/dev/.next/package.json +0 -1
  78. package/src/dev/.next/prerender-manifest.json +0 -11
  79. package/src/dev/.next/react-loadable-manifest.json +0 -1
  80. package/src/dev/.next/routes-manifest.json +0 -1
  81. package/src/dev/.next/server/app/[...slug]/page.js +0 -360
  82. package/src/dev/.next/server/app/[...slug]/page_client-reference-manifest.js +0 -1
  83. package/src/dev/.next/server/app/page.js +0 -349
  84. package/src/dev/.next/server/app/page_client-reference-manifest.js +0 -1
  85. package/src/dev/.next/server/app-paths-manifest.json +0 -3
  86. package/src/dev/.next/server/edge-runtime-webpack.js +0 -1151
  87. package/src/dev/.next/server/interception-route-rewrite-manifest.js +0 -1
  88. package/src/dev/.next/server/middleware-build-manifest.js +0 -33
  89. package/src/dev/.next/server/middleware-manifest.json +0 -32
  90. package/src/dev/.next/server/middleware-react-loadable-manifest.js +0 -1
  91. package/src/dev/.next/server/middleware.js +0 -1113
  92. package/src/dev/.next/server/next-font-manifest.js +0 -1
  93. package/src/dev/.next/server/next-font-manifest.json +0 -1
  94. package/src/dev/.next/server/pages-manifest.json +0 -5
  95. package/src/dev/.next/server/server-reference-manifest.js +0 -1
  96. package/src/dev/.next/server/server-reference-manifest.json +0 -5
  97. package/src/dev/.next/server/static/webpack/633457081244afec._.hot-update.json +0 -1
  98. package/src/dev/.next/server/vendor-chunks/@readme.js +0 -25
  99. package/src/dev/.next/server/vendor-chunks/@swc.js +0 -55
  100. package/src/dev/.next/server/vendor-chunks/next.js +0 -3659
  101. package/src/dev/.next/server/webpack-runtime.js +0 -209
  102. package/src/dev/.next/static/chunks/app/[...slug]/loading.js +0 -28
  103. package/src/dev/.next/static/chunks/app/[...slug]/page.js +0 -28
  104. package/src/dev/.next/static/chunks/app/layout.js +0 -171
  105. package/src/dev/.next/static/chunks/app/page.js +0 -28
  106. package/src/dev/.next/static/chunks/app-pages-internals.js +0 -182
  107. package/src/dev/.next/static/chunks/main-app.js +0 -1882
  108. package/src/dev/.next/static/chunks/polyfills.js +0 -1
  109. package/src/dev/.next/static/chunks/webpack.js +0 -1393
  110. package/src/dev/.next/static/css/app/layout.css +0 -559
  111. package/src/dev/.next/static/development/_buildManifest.js +0 -1
  112. package/src/dev/.next/static/development/_ssgManifest.js +0 -1
  113. package/src/dev/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
  114. package/src/dev/.next/static/webpack/ec52a3fce0f78db0.webpack.hot-update.json +0 -1
  115. package/src/dev/.next/static/webpack/webpack.ec52a3fce0f78db0.hot-update.js +0 -12
  116. package/src/dev/.next/trace +0 -21
  117. package/src/dev/.next/types/app/[...slug]/page.ts +0 -84
  118. package/src/dev/.next/types/app/layout.ts +0 -84
  119. package/src/dev/.next/types/app/page.ts +0 -84
  120. package/src/dev/.next/types/cache-life.d.ts +0 -141
  121. package/src/dev/.next/types/package.json +0 -1
  122. package/src/dev/.next/types/routes.d.ts +0 -55
  123. package/src/validators/numbering.js +0 -136
@@ -1,133 +1,370 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { createRequire } from 'node:module';
4
- import { query } from '@anthropic-ai/claude-agent-sdk';
5
- import matter from 'gray-matter';
6
- import ora from 'ora';
7
- import { collectFiles } from '../utils/lint.js';
8
- import { prettifyPagePrompt, stripCodeFences } from '../prompts/index.js';
9
- import { printHeader, isAgenticCli } from '../utils/eyes.js';
10
- import * as styles from '../utils/styles.js';
11
-
12
- const require = createRequire(import.meta.url);
13
- const pkg = require('../../package.json');
14
-
15
- export const command = 'pretty';
16
- export const order = 2;
17
- export const description = 'Polish docs into ReadMe-ready MDX with Claude';
18
- export const beta = true;
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { createRequire } from 'node:module'
4
+ import { query } from '@anthropic-ai/claude-agent-sdk'
5
+ import { InvalidArgumentError, Option } from 'commander'
6
+ import matter from 'gray-matter'
7
+ import { collectFiles } from '../utils/lint.js'
8
+ import { prettifyPagePrompt, prettifyPageOutputSchema } from '../prompts/index.js'
9
+ import { printHeader, isAgenticCli } from '../utils/eyes.js'
10
+ import { MultiSpinner } from '../utils/multi-spinner.js'
11
+ import { runPool } from '../utils/promise-pool.js'
12
+ import {
13
+ listLocalComponents,
14
+ listMarketplaceComponents,
15
+ installComponent,
16
+ readCachedReadme,
17
+ summarizeReadme,
18
+ } from '../utils/marketplace.js'
19
+ import * as styles from '../utils/styles.js'
20
+
21
+ const require = createRequire(import.meta.url)
22
+ const pkg = require('../../package.json')
23
+
24
+ /**
25
+ * Native ReadMe MDX tags that don't need a marketplace install — the renderer
26
+ * supports them out of the box. We exclude these from auto-install detection.
27
+ */
28
+ const NATIVE_TAGS = new Set([
29
+ 'Tabs', 'Tab',
30
+ 'Accordion',
31
+ 'Cards', 'Card',
32
+ 'Columns', 'Column',
33
+ 'Image',
34
+ ])
35
+
36
+ /**
37
+ * Pull every PascalCase tag name out of a block of mdx. Matches both
38
+ * `<Foo ... />` and `<Foo>...</Foo>`. Returns a Set of unique names.
39
+ */
40
+ function extractUsedTags(mdx) {
41
+ const tags = new Set()
42
+ const re = /<([A-Z][A-Za-z0-9]*)\b/g
43
+ let m
44
+ while ((m = re.exec(mdx)) !== null) tags.add(m[1])
45
+ return tags
46
+ }
47
+
48
+ export const command = 'pretty [file]'
49
+ export const order = 2
50
+ export const description = 'Polish docs into ReadMe-ready MDX with Claude'
51
+ export const beta = true
19
52
 
20
53
  export function args(cmd) {
21
- cmd.option('--model <name>', 'Claude model alias: haiku, sonnet, opus', 'sonnet');
22
- cmd.option('--dry-run', 'Show what would change without writing files');
54
+ cmd.addOption(new Option('-m, --model <name>', 'Claude model alias: haiku, sonnet, opus').choices(['haiku', 'sonnet', 'opus']).default('sonnet'))
55
+
56
+ cmd.option('--dry-run', 'Show what would change without writing files')
57
+ cmd.option('--no-auto-install', 'Don\'t auto-install marketplace components used in output')
58
+ cmd.option(
59
+ '-j, --jobs <n>',
60
+ 'Number of files to prettify in parallel',
61
+ (raw) => {
62
+ const n = Number(raw)
63
+ if (!Number.isInteger(n) || n < 1) {
64
+ throw new InvalidArgumentError('must be a positive integer')
65
+ }
66
+ return n
67
+ },
68
+ 1,
69
+ )
23
70
  }
24
71
 
25
72
  /**
26
- * Send one page through Claude using the prettify system prompt and parse the
27
- * JSON response. Returns the parsed object or throws on failure.
73
+ * Fetch with exponential backoff for transient (5xx / network) failures.
74
+ * `retries: 3` does up to 4 attempts with delays of 1s, 2s, 4s before each
75
+ * retry. 4xx responses are definitive and short-circuit so callers can fall
76
+ * through to the next strategy.
28
77
  */
29
- async function prettifyPage({ source, title, relativePath, model }) {
30
- const { systemPrompt, userPrompt } = prettifyPagePrompt({ source, title, relativePath });
78
+ async function fetchWithRetry(url, { headers = {}, retries = 3, baseDelayMs = 1000 } = {}) {
79
+ const maxAttempts = retries + 1
80
+ let lastError
81
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
82
+ try {
83
+ const res = await fetch(url, {
84
+ redirect: 'follow',
85
+ headers: { 'User-Agent': 'readme-cli-pretty', ...headers },
86
+ })
87
+ if (!res.ok) {
88
+ const err = new Error(`HTTP ${res.status} ${res.statusText}`)
89
+ err.status = res.status
90
+ throw err
91
+ }
92
+ return await res.text()
93
+ } catch (err) {
94
+ lastError = err
95
+ if (err.status && err.status >= 400 && err.status < 500) break
96
+ if (attempt < maxAttempts) {
97
+ const delay = baseDelayMs * Math.pow(2, attempt - 1)
98
+ await new Promise((resolve) => setTimeout(resolve, delay))
99
+ }
100
+ }
101
+ }
102
+ throw lastError
103
+ }
104
+
105
+ function withMdExtension(url) {
106
+ try {
107
+ const u = new URL(url)
108
+ if (u.pathname.endsWith('.md')) return null
109
+ u.pathname = u.pathname.replace(/\/$/, '') + '.md'
110
+ return u.toString()
111
+ } catch {
112
+ if (url.endsWith('.md')) return null
113
+ return url.replace(/\/$/, '') + '.md'
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Fetch a doc URL, preferring markdown. Falls through in order:
119
+ * 1. <url>.md (when not already .md)
120
+ * 2. <url> with Accept: text/markdown
121
+ * 3. <url> directly
122
+ * Each strategy gets its own retry budget for transient failures.
123
+ */
124
+ async function fetchImport(url, opts = {}) {
125
+ const strategies = []
126
+ const mdUrl = withMdExtension(url)
127
+ if (mdUrl) strategies.push({ url: mdUrl })
128
+ strategies.push({ url, headers: { Accept: 'text/markdown' } })
129
+ strategies.push({ url })
130
+
131
+ let lastError
132
+ for (const strategy of strategies) {
133
+ try {
134
+ return await fetchWithRetry(strategy.url, { ...opts, headers: strategy.headers })
135
+ } catch (err) {
136
+ lastError = err
137
+ }
138
+ }
139
+ throw new Error(`failed to fetch ${url}: ${lastError?.message || lastError}`)
140
+ }
31
141
 
32
- let text = '';
142
+ /**
143
+ * Send one page through Claude using the prettify system prompt and the agent
144
+ * SDK's structured-output mode. The runtime enforces `prettifyPageOutputSchema`
145
+ * and surfaces the parsed object on the result message's `structured_output`
146
+ * field — no fence-stripping or manual JSON.parse needed.
147
+ *
148
+ * `oasPartialJson` arrives as a string (the schema models it as such to dodge
149
+ * structured outputs' `additionalProperties: false` rule, which is
150
+ * incompatible with OpenAPI's free-form path/schema keys). We parse it here
151
+ * and re-expose it as `oasPartial` so callers see the conventional shape.
152
+ */
153
+ async function prettifyPage({ source, title, relativePath, model, localComponents, availableComponents }) {
154
+ const { systemPrompt, userPrompt } = prettifyPagePrompt({
155
+ source, title, relativePath, localComponents, availableComponents,
156
+ })
157
+
158
+ let structured
33
159
  for await (const message of query({
34
160
  prompt: userPrompt,
35
161
  options: {
36
162
  systemPrompt,
37
163
  allowedTools: [],
164
+ outputFormat: { type: 'json_schema', schema: prettifyPageOutputSchema },
38
165
  ...(model ? { model } : {}),
39
166
  },
40
167
  })) {
41
- if (message.type === 'assistant' && message.message?.content) {
42
- for (const block of message.message.content) {
43
- if (block.type === 'text' && block.text) text += block.text;
168
+ if (message.type === 'result') {
169
+ if (message.subtype !== 'success') {
170
+ throw new Error(`${message.subtype}${message.error?.message ? ' — ' + message.error.message : ''}`)
44
171
  }
45
- } else if (message.type === 'result') {
46
- if (message.subtype && message.subtype !== 'success') {
47
- throw new Error(`${message.subtype}${message.error?.message ? ' — ' + message.error.message : ''}`);
48
- }
49
- break;
172
+ structured = message.structured_output
173
+ break
50
174
  }
51
175
  }
52
176
 
53
- const stripped = stripCodeFences(text);
54
- try {
55
- return JSON.parse(stripped);
56
- } catch (e) {
57
- throw new Error(`invalid JSON (${e.message}); first 200 chars: ${stripped.slice(0, 200)}`);
177
+ if (!structured || typeof structured !== 'object') {
178
+ throw new Error('agent returned no structured output')
179
+ }
180
+
181
+ let oasPartial = null
182
+ if (typeof structured.oasPartialJson === 'string' && structured.oasPartialJson.length > 0) {
183
+ try {
184
+ oasPartial = JSON.parse(structured.oasPartialJson)
185
+ } catch (e) {
186
+ throw new Error(`invalid oasPartialJson (${e.message}); first 200 chars: ${structured.oasPartialJson.slice(0, 200)}`)
187
+ }
58
188
  }
189
+
190
+ return { body: structured.body, isApiRef: structured.isApiRef, oasPartial }
59
191
  }
60
192
 
61
- export async function run(options, _cmd, ctx) {
62
- const { gitRoot } = ctx;
193
+ export async function run(file, options, _cmd, ctx) {
194
+ const { gitRoot } = ctx
63
195
 
64
196
  if (!isAgenticCli()) {
65
- printHeader({ version: pkg.version, binName: styles.binName(), indent: ' ' });
66
- console.log();
197
+ printHeader({ version: pkg.version, binName: styles.binName(), indent: ' ' })
198
+ console.log()
199
+ }
200
+
201
+ let files
202
+ if (file) {
203
+ const absFile = path.resolve(file)
204
+ if (!fs.existsSync(absFile)) {
205
+ styles.error(`File not found: ${styles.bold(file)}`)
206
+ process.exit(1)
207
+ }
208
+ const relFile = path.relative(gitRoot, absFile)
209
+ if (relFile.startsWith('..') || path.isAbsolute(relFile)) {
210
+ styles.error(`File must be inside the repo: ${styles.bold(file)}`)
211
+ process.exit(1)
212
+ }
213
+ files = [relFile]
214
+ } else {
215
+ files = collectFiles(gitRoot)
67
216
  }
68
217
 
69
- const files = collectFiles(gitRoot);
70
218
  if (files.length === 0) {
71
- styles.warning('No docs files found to prettify.');
72
- return;
219
+ styles.warning('No docs files found to prettify.')
220
+ return
221
+ }
222
+
223
+ const localComponents = listLocalComponents(gitRoot)
224
+ const localNames = new Set(localComponents.map((c) => c.name))
225
+
226
+ // Pull the full marketplace catalog so Claude knows what else it can use.
227
+ // Summaries come from disk cache (populated by `readme components list`);
228
+ // we don't fetch readmes here to keep the prompt path fast.
229
+ let marketplace = []
230
+ if (options.autoInstall !== false) {
231
+ try {
232
+ marketplace = await listMarketplaceComponents()
233
+ } catch (err) {
234
+ styles.warning(`Could not load marketplace catalog: ${err.message} (continuing without it)`)
235
+ }
236
+ }
237
+ const marketplaceNames = new Set(marketplace.map((c) => c.name))
238
+ const availableComponents = marketplace
239
+ .filter((c) => !localNames.has(c.name))
240
+ .map((c) => ({ name: c.name, summary: summarizeReadme(readCachedReadme(c.name)) }))
241
+
242
+ // Concurrent workers may both try to install the same component. Coalesce
243
+ // duplicate installs through a Map<tag, Promise> — the second caller awaits
244
+ // the first's install instead of clobbering it.
245
+ const inFlightInstalls = new Map()
246
+ const autoInstalled = new Set()
247
+ function ensureInstalled(tag) {
248
+ if (inFlightInstalls.has(tag)) return inFlightInstalls.get(tag)
249
+ const p = (async () => {
250
+ await installComponent(gitRoot, tag)
251
+ localNames.add(tag)
252
+ autoInstalled.add(tag)
253
+ })()
254
+ inFlightInstalls.set(tag, p)
255
+ return p
73
256
  }
74
257
 
75
258
  styles.info(
76
- `Prettifying ${styles.bold(String(files.length))} file${files.length === 1 ? '' : 's'} with ${styles.bold(options.model)}${options.dryRun ? styles.dim(' (dry run)') : ''}.`,
77
- );
78
- console.log();
259
+ `Prettifying ${styles.bold(String(files.length))} file${files.length === 1 ? '' : 's'} with ${styles.bold(options.model)}${options.jobs > 1 ? ` (${styles.bold('-j ' + options.jobs)})` : ''}${options.dryRun ? styles.dim(' (dry run)') : ''}.`,
260
+ )
261
+ if (localComponents.length > 0) {
262
+ styles.info(
263
+ `Found ${styles.bold(String(localComponents.length))} local component${localComponents.length === 1 ? '' : 's'} in ${styles.bold('./components/')}: ${localComponents.map((c) => c.name).join(', ')}`,
264
+ )
265
+ }
266
+ if (options.autoInstall !== false && availableComponents.length > 0) {
267
+ styles.info(
268
+ `${styles.bold(String(availableComponents.length))} additional marketplace component${availableComponents.length === 1 ? '' : 's'} will be auto-installed if used.`,
269
+ )
270
+ }
271
+ console.log()
272
+
273
+ let updated = 0
274
+ let unchanged = 0
275
+ const failures = []
79
276
 
80
- const spinner = ora({ color: 'blue' }).start();
81
- let updated = 0;
82
- let unchanged = 0;
83
- const failures = [];
277
+ const rows = Math.min(options.jobs, files.length)
278
+ const multi = new MultiSpinner({ rows, color: 'blue' })
279
+ multi.start()
84
280
 
85
- for (const relativePath of files) {
86
- spinner.text = relativePath;
87
- const filePath = path.join(gitRoot, relativePath);
88
- const original = fs.readFileSync(filePath, 'utf-8');
89
- const parsed = matter(original);
281
+ await runPool(files, options.jobs, async (relativePath, _i, workerIdx) => {
282
+ multi.setLine(workerIdx, relativePath)
90
283
 
91
284
  try {
285
+ const filePath = path.join(gitRoot, relativePath)
286
+ const original = fs.readFileSync(filePath, 'utf-8')
287
+ const parsed = matter(original)
288
+
289
+ let source = parsed.content
290
+ const importUrl = parsed.data?.['x-import']
291
+ if (importUrl) {
292
+ multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(fetching ${importUrl})`)}`)
293
+ source = await fetchImport(importUrl)
294
+ multi.setLine(workerIdx, relativePath)
295
+ }
296
+
92
297
  const result = await prettifyPage({
93
- source: parsed.content,
298
+ source,
94
299
  title: parsed.data?.title,
95
300
  relativePath,
96
301
  model: options.model,
97
- });
302
+ localComponents,
303
+ availableComponents: options.autoInstall === false ? [] : availableComponents,
304
+ })
98
305
 
99
306
  if (typeof result?.body !== 'string') {
100
- throw new Error('response missing "body" field');
307
+ throw new Error('response missing "body" field')
308
+ }
309
+
310
+ // Auto-install marketplace components Claude used in the body. We do
311
+ // this before writing the file so the docs page and its components land
312
+ // together — and we share the inFlightInstalls map across workers so a
313
+ // tag is only fetched once.
314
+ if (options.autoInstall !== false && !options.dryRun) {
315
+ const used = extractUsedTags(result.body)
316
+ for (const tag of used) {
317
+ if (NATIVE_TAGS.has(tag)) continue
318
+ if (localNames.has(tag)) continue
319
+ if (!marketplaceNames.has(tag)) continue
320
+ try {
321
+ multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(installing <${tag}>)`)}`)
322
+ await ensureInstalled(tag)
323
+ } catch (err) {
324
+ failures.push({ relativePath, message: `auto-install <${tag}> failed: ${err.message}` })
325
+ }
326
+ }
327
+ multi.setLine(workerIdx, relativePath)
101
328
  }
102
329
 
103
- const newFm = { ...parsed.data };
104
- if (result.excerpt && !newFm.excerpt) newFm.excerpt = result.excerpt;
105
- const next = matter.stringify(result.body, newFm);
330
+ const newFm = { ...parsed.data }
331
+ delete newFm.hidden
332
+ delete newFm['x-import']
333
+ if (result.excerpt && !newFm.excerpt) newFm.excerpt = result.excerpt
334
+ const next = matter.stringify(result.body, newFm)
106
335
 
107
336
  if (next === original) {
108
- unchanged++;
337
+ unchanged++
338
+ if (!process.stderr.isTTY) multi.log(` ${styles.dim('·')} ${relativePath}`)
109
339
  } else {
110
- if (!options.dryRun) fs.writeFileSync(filePath, next, 'utf-8');
111
- updated++;
340
+ if (!options.dryRun) fs.writeFileSync(filePath, next, 'utf-8')
341
+ updated++
342
+ if (!process.stderr.isTTY) multi.log(` ${styles.success('✓')} ${relativePath}`)
112
343
  }
113
344
  } catch (err) {
114
- failures.push({ relativePath, message: err.message || String(err) });
345
+ failures.push({ relativePath, message: err.message || String(err) })
346
+ if (!process.stderr.isTTY) {
347
+ multi.log(` ${styles.err('●')} ${relativePath} ${styles.dim('— ' + (err.message || String(err)))}`)
348
+ }
349
+ } finally {
350
+ multi.clearLine(workerIdx)
115
351
  }
116
- }
352
+ })
117
353
 
118
- spinner.stop();
354
+ multi.stop()
119
355
 
120
- console.log();
121
- const verb = options.dryRun ? 'would update' : 'updated';
122
- styles.ok(
123
- `${styles.bold(String(updated))} ${verb} · ${styles.bold(String(unchanged))} unchanged${failures.length ? ` · ${styles.bold(String(failures.length))} failed` : ''}`,
124
- );
356
+ console.log()
357
+ const verb = options.dryRun ? 'would update' : 'updated'
358
+ styles.ok(`${styles.bold(String(updated))} ${verb} · ${styles.bold(String(unchanged))} unchanged${failures.length ? ` · ${styles.bold(String(failures.length))} failed` : ''}`)
359
+ if (autoInstalled.size > 0) {
360
+ styles.ok(`Auto-installed ${styles.bold(String(autoInstalled.size))} component${autoInstalled.size === 1 ? '' : 's'}: ${[...autoInstalled].join(', ')}`)
361
+ }
125
362
 
126
363
  if (failures.length > 0) {
127
- console.log();
364
+ console.log()
128
365
  for (const f of failures) {
129
- console.log(` ${styles.err('●')} ${styles.bold(f.relativePath)} ${styles.dim('— ' + f.message)}`);
366
+ console.log(` ${styles.err('●')} ${styles.bold(f.relativePath)} ${styles.dim('— ' + f.message)}`)
130
367
  }
131
- process.exit(1);
368
+ process.exit(1)
132
369
  }
133
370
  }