@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.
- package/README.md +2 -2
- package/package.json +5 -3
- package/src/cli.js +6 -1
- package/src/commands/components.js +288 -0
- package/src/commands/import.js +2456 -376
- package/src/commands/import.slug-planner.test.js +86 -0
- package/src/commands/import.test.js +136 -0
- package/src/commands/lint.js +32 -35
- package/src/commands/oas-sync.js +60 -75
- package/src/commands/pretty.js +314 -77
- package/src/prompts/index.js +187 -24
- package/src/utils/lint.js +33 -33
- package/src/utils/llms.js +224 -0
- package/src/utils/llms.test.js +257 -0
- package/src/utils/marketplace.js +208 -0
- package/src/utils/multi-spinner.js +71 -0
- package/src/utils/promise-pool.js +17 -0
- package/src/utils/url-segs.js +124 -0
- package/src/utils/url-segs.test.js +239 -0
- package/src/validators/components.js +1 -0
- package/src/validators/duplicates.js +6 -4
- package/src/validators/frontmatter.js +11 -0
- package/src/validators/oas-reference.js +1 -31
- package/src/validators/ordering.js +169 -83
- package/vendor/TOOLS.md +1 -1
- package/src/dev/.next/app-build-manifest.json +0 -20
- package/src/dev/.next/build-manifest.json +0 -31
- package/src/dev/.next/cache/.rscinfo +0 -1
- package/src/dev/.next/cache/next-devtools-config.json +0 -1
- package/src/dev/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/12.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/13.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/14.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/15.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/package.json +0 -1
- package/src/dev/.next/prerender-manifest.json +0 -11
- package/src/dev/.next/react-loadable-manifest.json +0 -1
- package/src/dev/.next/routes-manifest.json +0 -1
- package/src/dev/.next/server/app/[...slug]/page.js +0 -360
- package/src/dev/.next/server/app/[...slug]/page_client-reference-manifest.js +0 -1
- package/src/dev/.next/server/app/page.js +0 -349
- package/src/dev/.next/server/app/page_client-reference-manifest.js +0 -1
- package/src/dev/.next/server/app-paths-manifest.json +0 -3
- package/src/dev/.next/server/edge-runtime-webpack.js +0 -1151
- package/src/dev/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/src/dev/.next/server/middleware-build-manifest.js +0 -33
- package/src/dev/.next/server/middleware-manifest.json +0 -32
- package/src/dev/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/src/dev/.next/server/middleware.js +0 -1113
- package/src/dev/.next/server/next-font-manifest.js +0 -1
- package/src/dev/.next/server/next-font-manifest.json +0 -1
- package/src/dev/.next/server/pages-manifest.json +0 -5
- package/src/dev/.next/server/server-reference-manifest.js +0 -1
- package/src/dev/.next/server/server-reference-manifest.json +0 -5
- package/src/dev/.next/server/static/webpack/633457081244afec._.hot-update.json +0 -1
- package/src/dev/.next/server/vendor-chunks/@readme.js +0 -25
- package/src/dev/.next/server/vendor-chunks/@swc.js +0 -55
- package/src/dev/.next/server/vendor-chunks/next.js +0 -3659
- package/src/dev/.next/server/webpack-runtime.js +0 -209
- package/src/dev/.next/static/chunks/app/[...slug]/loading.js +0 -28
- package/src/dev/.next/static/chunks/app/[...slug]/page.js +0 -28
- package/src/dev/.next/static/chunks/app/layout.js +0 -171
- package/src/dev/.next/static/chunks/app/page.js +0 -28
- package/src/dev/.next/static/chunks/app-pages-internals.js +0 -182
- package/src/dev/.next/static/chunks/main-app.js +0 -1882
- package/src/dev/.next/static/chunks/polyfills.js +0 -1
- package/src/dev/.next/static/chunks/webpack.js +0 -1393
- package/src/dev/.next/static/css/app/layout.css +0 -559
- package/src/dev/.next/static/development/_buildManifest.js +0 -1
- package/src/dev/.next/static/development/_ssgManifest.js +0 -1
- package/src/dev/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
- package/src/dev/.next/static/webpack/ec52a3fce0f78db0.webpack.hot-update.json +0 -1
- package/src/dev/.next/static/webpack/webpack.ec52a3fce0f78db0.hot-update.js +0 -12
- package/src/dev/.next/trace +0 -21
- package/src/dev/.next/types/app/[...slug]/page.ts +0 -84
- package/src/dev/.next/types/app/layout.ts +0 -84
- package/src/dev/.next/types/app/page.ts +0 -84
- package/src/dev/.next/types/cache-life.d.ts +0 -141
- package/src/dev/.next/types/package.json +0 -1
- package/src/dev/.next/types/routes.d.ts +0 -55
- package/src/validators/numbering.js +0 -136
package/src/commands/pretty.js
CHANGED
|
@@ -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
|
|
6
|
-
import
|
|
7
|
-
import { collectFiles } from '../utils/lint.js'
|
|
8
|
-
import { prettifyPagePrompt,
|
|
9
|
-
import { printHeader, isAgenticCli } from '../utils/eyes.js'
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
22
|
-
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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
|
|
30
|
-
const
|
|
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
|
-
|
|
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 === '
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
354
|
+
multi.stop()
|
|
119
355
|
|
|
120
|
-
console.log()
|
|
121
|
-
const verb = options.dryRun ? 'would update' : 'updated'
|
|
122
|
-
styles.ok(
|
|
123
|
-
|
|
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
|
}
|