@readme/cli 0.0.26 → 0.0.27
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 +3 -1
- package/src/cli.js +5 -0
- package/src/commands/components.js +288 -0
- package/src/commands/import.js +410 -34
- package/src/commands/lint.js +32 -35
- package/src/commands/pretty.js +314 -77
- package/src/prompts/index.js +86 -11
- package/src/utils/lint.js +33 -33
- 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/validators/numbering.js +64 -66
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
|
}
|
package/src/prompts/index.js
CHANGED
|
@@ -296,16 +296,44 @@ If the page IS an API reference, extract the endpoints into a partial OpenAPI 3.
|
|
|
296
296
|
- Use realistic types inferred from the documentation (don't use "object" or "string" for everything)
|
|
297
297
|
- Include the server/base URL if mentioned
|
|
298
298
|
|
|
299
|
-
## Output
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
299
|
+
## Output
|
|
300
|
+
The runtime enforces the response shape via a JSON schema, so don't worry about formatting — just populate every field with the right content.
|
|
301
|
+
|
|
302
|
+
- \`body\`: the full MDX-fixed markdown content with no frontmatter. This is the load-bearing field — get the content right, render it human-readable, and don't leave broken MDX.
|
|
303
|
+
- \`isApiRef\`: true if this page primarily documents API endpoints (per the rules above), false otherwise.
|
|
304
|
+
- \`oasPartialJson\`: when \`isApiRef\` is true, set this to a JSON-stringified partial OpenAPI 3.0 fragment of the form \`{"paths": {"/endpoint": {"get": {...}}}, "schemas": {"ModelName": {...}}}\`. The caller will \`JSON.parse\` it. When \`isApiRef\` is false, set it to \`null\`.
|
|
305
|
+
`;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* JSON schema describing the structured output shape returned by the prettify
|
|
309
|
+
* agent. Pass to the Claude Agent SDK's \`outputFormat\` option so the runtime
|
|
310
|
+
* enforces the shape and returns parsed data on the result message's
|
|
311
|
+
* \`structured_output\` field — no manual fence-stripping or JSON.parse needed.
|
|
312
|
+
*
|
|
313
|
+
* \`oasPartialJson\` is a stringified OAS fragment rather than a nested object
|
|
314
|
+
* because Anthropic's structured outputs require \`additionalProperties: false\`
|
|
315
|
+
* on every object, which is incompatible with OpenAPI's free-form path/schema
|
|
316
|
+
* keys. The caller parses the string client-side.
|
|
317
|
+
*/
|
|
318
|
+
export const prettifyPageOutputSchema = {
|
|
319
|
+
type: 'object',
|
|
320
|
+
additionalProperties: false,
|
|
321
|
+
required: ['body', 'isApiRef', 'oasPartialJson'],
|
|
322
|
+
properties: {
|
|
323
|
+
body: {
|
|
324
|
+
type: 'string',
|
|
325
|
+
description: 'The MDX-fixed markdown content for the page (no frontmatter).',
|
|
326
|
+
},
|
|
327
|
+
isApiRef: {
|
|
328
|
+
type: 'boolean',
|
|
329
|
+
description: 'True if the page primarily documents REST/GraphQL/gRPC endpoints.',
|
|
330
|
+
},
|
|
331
|
+
oasPartialJson: {
|
|
332
|
+
anyOf: [{ type: 'string' }, { type: 'null' }],
|
|
333
|
+
description: 'JSON-stringified partial OpenAPI 3.0 fragment when isApiRef is true; null otherwise.',
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
309
337
|
|
|
310
338
|
/**
|
|
311
339
|
* @param {object} input
|
|
@@ -326,9 +354,56 @@ export function buildPrettifyPageUserPrompt({ source, title, relativePath }) {
|
|
|
326
354
|
].join('\n');
|
|
327
355
|
}
|
|
328
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Build a short addendum describing the user's locally-installed marketplace
|
|
359
|
+
* components so Claude can use them in prettified output. We pull the first
|
|
360
|
+
* usage signature out of each component's mdx (a `<Name ... />` line near the
|
|
361
|
+
* bottom of the file).
|
|
362
|
+
*
|
|
363
|
+
* @param {Array<{ name: string, source: string }>} components
|
|
364
|
+
* @returns {string} — the addendum, or '' if there are no components.
|
|
365
|
+
*/
|
|
366
|
+
export function buildLocalComponentsSection(components) {
|
|
367
|
+
if (!components || components.length === 0) return '';
|
|
368
|
+
const lines = ['', '### Project-specific components (installed in ./components/)'];
|
|
369
|
+
lines.push(
|
|
370
|
+
'The following custom components are installed in this project and may be used in prettified output. Prefer them when they fit the content better than plain markdown — same liberal-upgrade bias as the built-in components above. Do NOT invent components that are not listed here or in the built-in set.',
|
|
371
|
+
'',
|
|
372
|
+
);
|
|
373
|
+
for (const c of components) {
|
|
374
|
+
const sig = (c.source.match(new RegExp(`<${c.name}[\\s\\S]*?(?:/>|</${c.name}>)`, 'm')) || [])[0];
|
|
375
|
+
lines.push(`- \`<${c.name}>\`${sig ? ` — e.g. \`${sig.replace(/\s+/g, ' ').slice(0, 200)}\`` : ''}`);
|
|
376
|
+
}
|
|
377
|
+
return lines.join('\n');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Build a short section listing additional marketplace components that are
|
|
382
|
+
* NOT yet installed but can be — the CLI will auto-install any of them that
|
|
383
|
+
* the model uses in its output.
|
|
384
|
+
*
|
|
385
|
+
* @param {Array<{ name: string, summary?: string }>} available
|
|
386
|
+
* Marketplace components not currently in ./components/.
|
|
387
|
+
* @returns {string} — the addendum, or '' if there are none.
|
|
388
|
+
*/
|
|
389
|
+
export function buildAvailableComponentsSection(available) {
|
|
390
|
+
if (!available || available.length === 0) return '';
|
|
391
|
+
const lines = ['', '### Marketplace components (auto-installed on use)'];
|
|
392
|
+
lines.push(
|
|
393
|
+
'These components are NOT yet installed locally, but the CLI will copy them into ./components/ automatically if you use any of them. Use them when they clearly fit the content — same upgrade bias as built-in components. Do NOT invent components outside this list and the built-in set.',
|
|
394
|
+
'',
|
|
395
|
+
);
|
|
396
|
+
for (const c of available) {
|
|
397
|
+
lines.push(`- \`<${c.name}>\`${c.summary ? ` — ${c.summary}` : ''}`);
|
|
398
|
+
}
|
|
399
|
+
return lines.join('\n');
|
|
400
|
+
}
|
|
401
|
+
|
|
329
402
|
export function prettifyPagePrompt(input) {
|
|
403
|
+
const localAddendum = buildLocalComponentsSection(input.localComponents);
|
|
404
|
+
const availableAddendum = buildAvailableComponentsSection(input.availableComponents);
|
|
330
405
|
return {
|
|
331
|
-
systemPrompt: prettifyPageSystemPrompt,
|
|
406
|
+
systemPrompt: prettifyPageSystemPrompt + localAddendum + availableAddendum,
|
|
332
407
|
userPrompt: buildPrettifyPageUserPrompt(input),
|
|
333
408
|
};
|
|
334
409
|
}
|