@readme/cli 0.1.0 → 0.2.0
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/bin/readme.js +1 -1
- package/package.json +33 -24
- package/src/bootstrap.js +9 -13
- package/src/cli.js +17 -11
- package/src/commands/components.js +35 -18
- package/src/commands/dev.js +12 -8
- package/src/commands/eyes.js +12 -8
- package/src/commands/import.js +2190 -2048
- package/src/commands/lint.js +33 -29
- package/src/commands/oas-bundle.js +6 -2
- package/src/commands/oas-sync.js +96 -93
- package/src/commands/oas-validate.js +10 -7
- package/src/commands/pretty.js +177 -164
- package/src/commands/setup.js +24 -26
- package/src/commands/versions.js +6 -2
- package/src/dev/app/[...slug]/page.js +1 -0
- package/src/dev/app/layout.js +18 -11
- package/src/dev/app/page.js +1 -0
- package/src/dev/lib/docs.js +27 -19
- package/src/dev/next.config.mjs +2 -2
- package/src/gha.js +7 -7
- package/src/prompts/index.js +16 -6
- package/src/utils/eyes.js +143 -51
- package/src/utils/git.js +3 -3
- package/src/utils/lint.js +39 -38
- package/src/utils/llms.js +94 -91
- package/src/utils/markdown.js +256 -0
- package/src/utils/marketplace.js +12 -5
- package/src/utils/multi-spinner.js +5 -7
- package/src/utils/promise-pool.js +7 -5
- package/src/utils/reporter.js +88 -91
- package/src/utils/setup-templates.js +12 -6
- package/src/utils/styles.js +2 -1
- package/src/utils/tamagotchi.js +257 -91
- package/src/utils/tips.js +18 -9
- package/src/utils/url-segs.js +46 -41
- package/src/validators/components.js +55 -63
- package/src/validators/content.js +5 -10
- package/src/validators/duplicates.js +6 -2
- package/src/validators/frontmatter.js +19 -21
- package/src/validators/links.js +4 -8
- package/src/validators/oas-reference.js +7 -14
- package/src/validators/oas-schema.js +2 -0
- package/src/validators/ordering.js +5 -5
- package/src/validators/recipes.js +4 -12
- package/src/commands/import-oas.test.js +0 -277
- package/src/commands/import.classify.test.js +0 -172
- package/src/commands/import.landing.test.js +0 -217
- package/src/commands/import.slug-planner.test.js +0 -86
- package/src/commands/import.staging.test.js +0 -230
- package/src/commands/import.test.js +0 -382
- package/src/utils/llms.test.js +0 -302
- package/src/utils/url-segs.test.js +0 -239
package/src/commands/pretty.js
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
import { prettifyPagePrompt, prettifyPageOutputSchema } from '../prompts/index.js'
|
|
9
|
-
import { printHeader, isAgenticCli } from '../utils/eyes.js'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
+
import { InvalidArgumentError, Option } from 'commander';
|
|
7
|
+
|
|
8
|
+
import { prettifyPagePrompt, prettifyPageOutputSchema } from '../prompts/index.js';
|
|
9
|
+
import { printHeader, isAgenticCli } from '../utils/eyes.js';
|
|
10
|
+
import { collectFiles } from '../utils/lint.js';
|
|
11
|
+
import { parseFrontmatter, toFrontmatter, frontmatterWriteError } from '../utils/markdown.js';
|
|
12
12
|
import {
|
|
13
13
|
listLocalComponents,
|
|
14
14
|
listMarketplaceComponents,
|
|
15
15
|
installComponent,
|
|
16
16
|
readCachedReadme,
|
|
17
17
|
summarizeReadme,
|
|
18
|
-
} from '../utils/marketplace.js'
|
|
19
|
-
import
|
|
18
|
+
} from '../utils/marketplace.js';
|
|
19
|
+
import { MultiSpinner } from '../utils/multi-spinner.js';
|
|
20
|
+
import { runPool } from '../utils/promise-pool.js';
|
|
21
|
+
import * as styles from '../utils/styles.js';
|
|
20
22
|
|
|
21
|
-
const require = createRequire(import.meta.url)
|
|
22
|
-
const pkg = require('../../package.json')
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const pkg = require('../../package.json');
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Native ReadMe MDX tags that don't need a marketplace install — the renderer
|
|
26
28
|
* supports them out of the box. We exclude these from auto-install detection.
|
|
27
29
|
*/
|
|
28
|
-
const NATIVE_TAGS = new Set([
|
|
29
|
-
'Tabs', 'Tab',
|
|
30
|
-
'Accordion',
|
|
31
|
-
'Cards', 'Card',
|
|
32
|
-
'Columns', 'Column',
|
|
33
|
-
'Image',
|
|
34
|
-
])
|
|
30
|
+
const NATIVE_TAGS = new Set(['Tabs', 'Tab', 'Accordion', 'Cards', 'Card', 'Columns', 'Column', 'Image']);
|
|
35
31
|
|
|
36
32
|
/**
|
|
37
33
|
* Pull every PascalCase tag name out of a block of mdx. Matches both
|
|
38
34
|
* `<Foo ... />` and `<Foo>...</Foo>`. Returns a Set of unique names.
|
|
39
35
|
*/
|
|
40
36
|
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
|
|
37
|
+
const tags = new Set();
|
|
38
|
+
const re = /<([A-Z][A-Za-z0-9]*)\b/g;
|
|
39
|
+
let m;
|
|
40
|
+
while ((m = re.exec(mdx)) !== null) tags.add(m[1]);
|
|
41
|
+
return tags;
|
|
46
42
|
}
|
|
47
43
|
|
|
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
|
|
44
|
+
export const command = 'pretty [file]';
|
|
45
|
+
export const order = 2;
|
|
46
|
+
export const description = 'Polish docs into ReadMe-ready MDX with Claude';
|
|
47
|
+
export const beta = true;
|
|
52
48
|
|
|
53
49
|
export function args(cmd) {
|
|
54
|
-
cmd.addOption(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
cmd.addOption(
|
|
51
|
+
new Option('-m, --model <name>', 'Claude model alias: haiku, sonnet, opus')
|
|
52
|
+
.choices(['haiku', 'sonnet', 'opus'])
|
|
53
|
+
.default('sonnet'),
|
|
54
|
+
);
|
|
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
58
|
cmd.option(
|
|
59
59
|
'-j, --jobs <n>',
|
|
60
60
|
'Number of files to prettify in parallel',
|
|
61
|
-
|
|
62
|
-
const n = Number(raw)
|
|
61
|
+
raw => {
|
|
62
|
+
const n = Number(raw);
|
|
63
63
|
if (!Number.isInteger(n) || n < 1) {
|
|
64
|
-
throw new InvalidArgumentError('must be a positive integer')
|
|
64
|
+
throw new InvalidArgumentError('must be a positive integer');
|
|
65
65
|
}
|
|
66
|
-
return n
|
|
66
|
+
return n;
|
|
67
67
|
},
|
|
68
68
|
1,
|
|
69
|
-
)
|
|
69
|
+
);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -76,41 +76,41 @@ export function args(cmd) {
|
|
|
76
76
|
* through to the next strategy.
|
|
77
77
|
*/
|
|
78
78
|
async function fetchWithRetry(url, { headers = {}, retries = 3, baseDelayMs = 1000 } = {}) {
|
|
79
|
-
const maxAttempts = retries + 1
|
|
80
|
-
let lastError
|
|
79
|
+
const maxAttempts = retries + 1;
|
|
80
|
+
let lastError;
|
|
81
81
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
82
82
|
try {
|
|
83
83
|
const res = await fetch(url, {
|
|
84
84
|
redirect: 'follow',
|
|
85
85
|
headers: { 'User-Agent': 'readme-cli-pretty', ...headers },
|
|
86
|
-
})
|
|
86
|
+
});
|
|
87
87
|
if (!res.ok) {
|
|
88
|
-
const err = new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
89
|
-
err.status = res.status
|
|
90
|
-
throw err
|
|
88
|
+
const err = new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
89
|
+
err.status = res.status;
|
|
90
|
+
throw err;
|
|
91
91
|
}
|
|
92
|
-
return await res.text()
|
|
92
|
+
return await res.text();
|
|
93
93
|
} catch (err) {
|
|
94
|
-
lastError = err
|
|
95
|
-
if (err.status && err.status >= 400 && err.status < 500) break
|
|
94
|
+
lastError = err;
|
|
95
|
+
if (err.status && err.status >= 400 && err.status < 500) break;
|
|
96
96
|
if (attempt < maxAttempts) {
|
|
97
|
-
const delay = baseDelayMs *
|
|
98
|
-
await new Promise(
|
|
97
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
throw lastError
|
|
102
|
+
throw lastError;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function withMdExtension(url) {
|
|
106
106
|
try {
|
|
107
|
-
const u = new URL(url)
|
|
108
|
-
if (u.pathname.endsWith('.md')) return null
|
|
109
|
-
u.pathname = u.pathname.replace(/\/$/, '')
|
|
110
|
-
return u.toString()
|
|
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
111
|
} catch {
|
|
112
|
-
if (url.endsWith('.md')) return null
|
|
113
|
-
return url.replace(/\/$/, '')
|
|
112
|
+
if (url.endsWith('.md')) return null;
|
|
113
|
+
return `${url.replace(/\/$/, '')}.md`;
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -122,21 +122,21 @@ function withMdExtension(url) {
|
|
|
122
122
|
* Each strategy gets its own retry budget for transient failures.
|
|
123
123
|
*/
|
|
124
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 })
|
|
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
130
|
|
|
131
|
-
let lastError
|
|
131
|
+
let lastError;
|
|
132
132
|
for (const strategy of strategies) {
|
|
133
133
|
try {
|
|
134
|
-
return await fetchWithRetry(strategy.url, { ...opts, headers: strategy.headers })
|
|
134
|
+
return await fetchWithRetry(strategy.url, { ...opts, headers: strategy.headers });
|
|
135
135
|
} catch (err) {
|
|
136
|
-
lastError = err
|
|
136
|
+
lastError = err;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
throw new Error(`failed to fetch ${url}: ${lastError?.message || lastError}`)
|
|
139
|
+
throw new Error(`failed to fetch ${url}: ${lastError?.message || lastError}`);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
@@ -152,10 +152,14 @@ async function fetchImport(url, opts = {}) {
|
|
|
152
152
|
*/
|
|
153
153
|
async function prettifyPage({ source, title, relativePath, model, localComponents, availableComponents }) {
|
|
154
154
|
const { systemPrompt, userPrompt } = prettifyPagePrompt({
|
|
155
|
-
source,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
source,
|
|
156
|
+
title,
|
|
157
|
+
relativePath,
|
|
158
|
+
localComponents,
|
|
159
|
+
availableComponents,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let structured;
|
|
159
163
|
for await (const message of query({
|
|
160
164
|
prompt: userPrompt,
|
|
161
165
|
options: {
|
|
@@ -167,131 +171,136 @@ async function prettifyPage({ source, title, relativePath, model, localComponent
|
|
|
167
171
|
})) {
|
|
168
172
|
if (message.type === 'result') {
|
|
169
173
|
if (message.subtype !== 'success') {
|
|
170
|
-
throw new Error(`${message.subtype}${message.error?.message ?
|
|
174
|
+
throw new Error(`${message.subtype}${message.error?.message ? ` — ${message.error.message}` : ''}`);
|
|
171
175
|
}
|
|
172
|
-
structured = message.structured_output
|
|
173
|
-
break
|
|
176
|
+
structured = message.structured_output;
|
|
177
|
+
break;
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
if (!structured || typeof structured !== 'object') {
|
|
178
|
-
throw new Error('agent returned no structured output')
|
|
182
|
+
throw new Error('agent returned no structured output');
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
let oasPartial = null
|
|
185
|
+
let oasPartial = null;
|
|
182
186
|
if (typeof structured.oasPartialJson === 'string' && structured.oasPartialJson.length > 0) {
|
|
183
187
|
try {
|
|
184
|
-
oasPartial = JSON.parse(structured.oasPartialJson)
|
|
188
|
+
oasPartial = JSON.parse(structured.oasPartialJson);
|
|
185
189
|
} catch (e) {
|
|
186
|
-
throw new Error(
|
|
190
|
+
throw new Error(
|
|
191
|
+
`invalid oasPartialJson (${e.message}); first 200 chars: ${structured.oasPartialJson.slice(0, 200)}`,
|
|
192
|
+
{ cause: e },
|
|
193
|
+
);
|
|
187
194
|
}
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
return { body: structured.body, isApiRef: structured.isApiRef, oasPartial }
|
|
197
|
+
return { body: structured.body, isApiRef: structured.isApiRef, oasPartial };
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
export async function run(file, options, _cmd, ctx) {
|
|
194
|
-
const { gitRoot } = ctx
|
|
201
|
+
const { gitRoot } = ctx;
|
|
195
202
|
|
|
196
203
|
if (!isAgenticCli()) {
|
|
197
|
-
printHeader({ version: pkg.version, binName: styles.binName(), indent: ' ' })
|
|
198
|
-
console.log()
|
|
204
|
+
printHeader({ version: pkg.version, binName: styles.binName(), indent: ' ' });
|
|
205
|
+
console.log();
|
|
199
206
|
}
|
|
200
207
|
|
|
201
|
-
let files
|
|
208
|
+
let files;
|
|
202
209
|
if (file) {
|
|
203
|
-
const absFile = path.resolve(file)
|
|
210
|
+
const absFile = path.resolve(file);
|
|
204
211
|
if (!fs.existsSync(absFile)) {
|
|
205
|
-
styles.error(`File not found: ${styles.bold(file)}`)
|
|
206
|
-
process.exit(1)
|
|
212
|
+
styles.error(`File not found: ${styles.bold(file)}`);
|
|
213
|
+
process.exit(1);
|
|
207
214
|
}
|
|
208
|
-
const relFile = path.relative(gitRoot, absFile)
|
|
215
|
+
const relFile = path.relative(gitRoot, absFile);
|
|
209
216
|
if (relFile.startsWith('..') || path.isAbsolute(relFile)) {
|
|
210
|
-
styles.error(`File must be inside the repo: ${styles.bold(file)}`)
|
|
211
|
-
process.exit(1)
|
|
217
|
+
styles.error(`File must be inside the repo: ${styles.bold(file)}`);
|
|
218
|
+
process.exit(1);
|
|
212
219
|
}
|
|
213
|
-
files = [relFile]
|
|
220
|
+
files = [relFile];
|
|
214
221
|
} else {
|
|
215
|
-
files = collectFiles(gitRoot)
|
|
222
|
+
files = collectFiles(gitRoot);
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
if (files.length === 0) {
|
|
219
|
-
styles.warning('No docs files found to prettify.')
|
|
220
|
-
return
|
|
226
|
+
styles.warning('No docs files found to prettify.');
|
|
227
|
+
return;
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
const localComponents = listLocalComponents(gitRoot)
|
|
224
|
-
const localNames = new Set(localComponents.map(
|
|
230
|
+
const localComponents = listLocalComponents(gitRoot);
|
|
231
|
+
const localNames = new Set(localComponents.map(c => c.name));
|
|
225
232
|
|
|
226
233
|
// Pull the full marketplace catalog so Claude knows what else it can use.
|
|
227
234
|
// Summaries come from disk cache (populated by `readme components list`);
|
|
228
235
|
// we don't fetch readmes here to keep the prompt path fast.
|
|
229
|
-
let marketplace = []
|
|
236
|
+
let marketplace = [];
|
|
230
237
|
if (options.autoInstall !== false) {
|
|
231
238
|
try {
|
|
232
|
-
marketplace = await listMarketplaceComponents()
|
|
239
|
+
marketplace = await listMarketplaceComponents();
|
|
233
240
|
} catch (err) {
|
|
234
|
-
styles.warning(`Could not load marketplace catalog: ${err.message} (continuing without it)`)
|
|
241
|
+
styles.warning(`Could not load marketplace catalog: ${err.message} (continuing without it)`);
|
|
235
242
|
}
|
|
236
243
|
}
|
|
237
|
-
const marketplaceNames = new Set(marketplace.map(
|
|
244
|
+
const marketplaceNames = new Set(marketplace.map(c => c.name));
|
|
238
245
|
const availableComponents = marketplace
|
|
239
|
-
.filter(
|
|
240
|
-
.map(
|
|
246
|
+
.filter(c => !localNames.has(c.name))
|
|
247
|
+
.map(c => ({ name: c.name, summary: summarizeReadme(readCachedReadme(c.name)) }));
|
|
241
248
|
|
|
242
249
|
// Concurrent workers may both try to install the same component. Coalesce
|
|
243
250
|
// duplicate installs through a Map<tag, Promise> — the second caller awaits
|
|
244
251
|
// the first's install instead of clobbering it.
|
|
245
|
-
const inFlightInstalls = new Map()
|
|
246
|
-
const autoInstalled = new Set()
|
|
252
|
+
const inFlightInstalls = new Map();
|
|
253
|
+
const autoInstalled = new Set();
|
|
247
254
|
function ensureInstalled(tag) {
|
|
248
|
-
if (inFlightInstalls.has(tag)) return inFlightInstalls.get(tag)
|
|
255
|
+
if (inFlightInstalls.has(tag)) return inFlightInstalls.get(tag);
|
|
249
256
|
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
|
|
257
|
+
await installComponent(gitRoot, tag);
|
|
258
|
+
localNames.add(tag);
|
|
259
|
+
autoInstalled.add(tag);
|
|
260
|
+
})();
|
|
261
|
+
inFlightInstalls.set(tag, p);
|
|
262
|
+
return p;
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
styles.info(
|
|
259
|
-
`Prettifying ${styles.bold(String(files.length))} file${files.length === 1 ? '' : 's'} with ${styles.bold(options.model)}${options.jobs > 1 ? ` (${styles.bold(
|
|
260
|
-
)
|
|
266
|
+
`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)') : ''}.`,
|
|
267
|
+
);
|
|
261
268
|
if (localComponents.length > 0) {
|
|
262
269
|
styles.info(
|
|
263
|
-
`Found ${styles.bold(String(localComponents.length))} local component${localComponents.length === 1 ? '' : 's'} in ${styles.bold('./components/')}: ${localComponents.map(
|
|
264
|
-
)
|
|
270
|
+
`Found ${styles.bold(String(localComponents.length))} local component${localComponents.length === 1 ? '' : 's'} in ${styles.bold('./components/')}: ${localComponents.map(c => c.name).join(', ')}`,
|
|
271
|
+
);
|
|
265
272
|
}
|
|
266
273
|
if (options.autoInstall !== false && availableComponents.length > 0) {
|
|
267
274
|
styles.info(
|
|
268
275
|
`${styles.bold(String(availableComponents.length))} additional marketplace component${availableComponents.length === 1 ? '' : 's'} will be auto-installed if used.`,
|
|
269
|
-
)
|
|
276
|
+
);
|
|
270
277
|
}
|
|
271
|
-
console.log()
|
|
278
|
+
console.log();
|
|
272
279
|
|
|
273
|
-
let updated = 0
|
|
274
|
-
let unchanged = 0
|
|
275
|
-
const failures = []
|
|
280
|
+
let updated = 0;
|
|
281
|
+
let unchanged = 0;
|
|
282
|
+
const failures = [];
|
|
276
283
|
|
|
277
|
-
const rows = Math.min(options.jobs, files.length)
|
|
278
|
-
const multi = new MultiSpinner({ rows, color: 'blue' })
|
|
279
|
-
multi.start()
|
|
284
|
+
const rows = Math.min(options.jobs, files.length);
|
|
285
|
+
const multi = new MultiSpinner({ rows, color: 'blue' });
|
|
286
|
+
multi.start();
|
|
280
287
|
|
|
281
288
|
await runPool(files, options.jobs, async (relativePath, _i, workerIdx) => {
|
|
282
|
-
multi.setLine(workerIdx, relativePath)
|
|
289
|
+
multi.setLine(workerIdx, relativePath);
|
|
283
290
|
|
|
284
291
|
try {
|
|
285
|
-
const filePath = path.join(gitRoot, relativePath)
|
|
286
|
-
const original = fs.readFileSync(filePath, 'utf-8')
|
|
287
|
-
const parsed =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
const filePath = path.join(gitRoot, relativePath);
|
|
293
|
+
const original = fs.readFileSync(filePath, 'utf-8');
|
|
294
|
+
const parsed = parseFrontmatter(original);
|
|
295
|
+
const parseError = frontmatterWriteError(parsed);
|
|
296
|
+
if (parseError) throw parseError;
|
|
297
|
+
|
|
298
|
+
let source = parsed.content;
|
|
299
|
+
const importUrl = parsed.data?.['x-import'];
|
|
291
300
|
if (importUrl) {
|
|
292
|
-
multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(fetching ${importUrl})`)}`)
|
|
293
|
-
source = await fetchImport(importUrl)
|
|
294
|
-
multi.setLine(workerIdx, relativePath)
|
|
301
|
+
multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(fetching ${importUrl})`)}`);
|
|
302
|
+
source = await fetchImport(importUrl);
|
|
303
|
+
multi.setLine(workerIdx, relativePath);
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
const result = await prettifyPage({
|
|
@@ -301,10 +310,10 @@ export async function run(file, options, _cmd, ctx) {
|
|
|
301
310
|
model: options.model,
|
|
302
311
|
localComponents,
|
|
303
312
|
availableComponents: options.autoInstall === false ? [] : availableComponents,
|
|
304
|
-
})
|
|
313
|
+
});
|
|
305
314
|
|
|
306
315
|
if (typeof result?.body !== 'string') {
|
|
307
|
-
throw new
|
|
316
|
+
throw new TypeError('response missing "body" field');
|
|
308
317
|
}
|
|
309
318
|
|
|
310
319
|
// Auto-install marketplace components Claude used in the body. We do
|
|
@@ -312,59 +321,63 @@ export async function run(file, options, _cmd, ctx) {
|
|
|
312
321
|
// together — and we share the inFlightInstalls map across workers so a
|
|
313
322
|
// tag is only fetched once.
|
|
314
323
|
if (options.autoInstall !== false && !options.dryRun) {
|
|
315
|
-
const used = extractUsedTags(result.body)
|
|
324
|
+
const used = extractUsedTags(result.body);
|
|
316
325
|
for (const tag of used) {
|
|
317
|
-
if (NATIVE_TAGS.has(tag)) continue
|
|
318
|
-
if (localNames.has(tag)) continue
|
|
319
|
-
if (!marketplaceNames.has(tag)) continue
|
|
326
|
+
if (NATIVE_TAGS.has(tag)) continue;
|
|
327
|
+
if (localNames.has(tag)) continue;
|
|
328
|
+
if (!marketplaceNames.has(tag)) continue;
|
|
320
329
|
try {
|
|
321
|
-
multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(installing <${tag}>)`)}`)
|
|
322
|
-
await ensureInstalled(tag)
|
|
330
|
+
multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(installing <${tag}>)`)}`);
|
|
331
|
+
await ensureInstalled(tag);
|
|
323
332
|
} catch (err) {
|
|
324
|
-
failures.push({ relativePath, message: `auto-install <${tag}> failed: ${err.message}` })
|
|
333
|
+
failures.push({ relativePath, message: `auto-install <${tag}> failed: ${err.message}` });
|
|
325
334
|
}
|
|
326
335
|
}
|
|
327
|
-
multi.setLine(workerIdx, relativePath)
|
|
336
|
+
multi.setLine(workerIdx, relativePath);
|
|
328
337
|
}
|
|
329
338
|
|
|
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 =
|
|
339
|
+
const newFm = { ...parsed.data };
|
|
340
|
+
delete newFm.hidden;
|
|
341
|
+
delete newFm['x-import'];
|
|
342
|
+
if (result.excerpt && !newFm.excerpt) newFm.excerpt = result.excerpt;
|
|
343
|
+
const next = toFrontmatter(newFm, result.body);
|
|
335
344
|
|
|
336
345
|
if (next === original) {
|
|
337
|
-
unchanged
|
|
338
|
-
if (!process.stderr.isTTY) multi.log(` ${styles.dim('·')} ${relativePath}`)
|
|
346
|
+
unchanged++;
|
|
347
|
+
if (!process.stderr.isTTY) multi.log(` ${styles.dim('·')} ${relativePath}`);
|
|
339
348
|
} else {
|
|
340
|
-
if (!options.dryRun) fs.writeFileSync(filePath, next, 'utf-8')
|
|
341
|
-
updated
|
|
342
|
-
if (!process.stderr.isTTY) multi.log(` ${styles.success('✓')} ${relativePath}`)
|
|
349
|
+
if (!options.dryRun) fs.writeFileSync(filePath, next, 'utf-8');
|
|
350
|
+
updated++;
|
|
351
|
+
if (!process.stderr.isTTY) multi.log(` ${styles.success('✓')} ${relativePath}`);
|
|
343
352
|
}
|
|
344
353
|
} catch (err) {
|
|
345
|
-
failures.push({ relativePath, message: err.message || String(err) })
|
|
354
|
+
failures.push({ relativePath, message: err.message || String(err) });
|
|
346
355
|
if (!process.stderr.isTTY) {
|
|
347
|
-
multi.log(` ${styles.err('●')} ${relativePath} ${styles.dim(
|
|
356
|
+
multi.log(` ${styles.err('●')} ${relativePath} ${styles.dim(`— ${err.message || String(err)}`)}`);
|
|
348
357
|
}
|
|
349
358
|
} finally {
|
|
350
|
-
multi.clearLine(workerIdx)
|
|
359
|
+
multi.clearLine(workerIdx);
|
|
351
360
|
}
|
|
352
|
-
})
|
|
361
|
+
});
|
|
353
362
|
|
|
354
|
-
multi.stop()
|
|
363
|
+
multi.stop();
|
|
355
364
|
|
|
356
|
-
console.log()
|
|
357
|
-
const verb = options.dryRun ? 'would update' : 'updated'
|
|
358
|
-
styles.ok(
|
|
365
|
+
console.log();
|
|
366
|
+
const verb = options.dryRun ? 'would update' : 'updated';
|
|
367
|
+
styles.ok(
|
|
368
|
+
`${styles.bold(String(updated))} ${verb} · ${styles.bold(String(unchanged))} unchanged${failures.length ? ` · ${styles.bold(String(failures.length))} failed` : ''}`,
|
|
369
|
+
);
|
|
359
370
|
if (autoInstalled.size > 0) {
|
|
360
|
-
styles.ok(
|
|
371
|
+
styles.ok(
|
|
372
|
+
`Auto-installed ${styles.bold(String(autoInstalled.size))} component${autoInstalled.size === 1 ? '' : 's'}: ${[...autoInstalled].join(', ')}`,
|
|
373
|
+
);
|
|
361
374
|
}
|
|
362
375
|
|
|
363
376
|
if (failures.length > 0) {
|
|
364
|
-
console.log()
|
|
377
|
+
console.log();
|
|
365
378
|
for (const f of failures) {
|
|
366
|
-
console.log(` ${styles.err('●')} ${styles.bold(f.relativePath)} ${styles.dim(
|
|
379
|
+
console.log(` ${styles.err('●')} ${styles.bold(f.relativePath)} ${styles.dim(`— ${f.message}`)}`);
|
|
367
380
|
}
|
|
368
|
-
process.exit(1)
|
|
381
|
+
process.exit(1);
|
|
369
382
|
}
|
|
370
383
|
}
|
package/src/commands/setup.js
CHANGED
|
@@ -2,14 +2,10 @@ import { execSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import readline from 'node:readline';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
PLATFORMS,
|
|
8
|
-
detectExistingCi,
|
|
9
|
-
detectPlatform,
|
|
10
|
-
usesBlacksmith,
|
|
11
|
-
} from '../utils/git.js';
|
|
5
|
+
|
|
6
|
+
import { PLATFORMS, detectExistingCi, detectPlatform, usesBlacksmith } from '../utils/git.js';
|
|
12
7
|
import { templateFor } from '../utils/setup-templates.js';
|
|
8
|
+
import * as styles from '../utils/styles.js';
|
|
13
9
|
|
|
14
10
|
// Detect the user's CI platform at module load so the top-level `--help`
|
|
15
11
|
// can advertise the right `setup:<platform>` form (e.g. `setup:github` for
|
|
@@ -43,12 +39,8 @@ export const helpHint = _hasExistingCi
|
|
|
43
39
|
// Hidden aliases cover every other form: bare `setup` (with optional positional),
|
|
44
40
|
// the generic `setup:ci`, and one entry per platform. Whichever form is the
|
|
45
41
|
// primary above is filtered out so it isn't double-registered.
|
|
46
|
-
const ALL_FORMS = [
|
|
47
|
-
|
|
48
|
-
'setup:ci',
|
|
49
|
-
...Object.keys(PLATFORMS).map((k) => `setup:${k}`),
|
|
50
|
-
];
|
|
51
|
-
export const aliases = ALL_FORMS.filter((a) => a.split(' ')[0] !== _primaryName);
|
|
42
|
+
const ALL_FORMS = ['setup [platform]', 'setup:ci', ...Object.keys(PLATFORMS).map(k => `setup:${k}`)];
|
|
43
|
+
export const aliases = ALL_FORMS.filter(a => a.split(' ')[0] !== _primaryName);
|
|
52
44
|
|
|
53
45
|
export function args(cmd) {
|
|
54
46
|
cmd.option('--blacksmith', 'Use Blacksmith runners (GitHub Actions only)');
|
|
@@ -56,9 +48,9 @@ export function args(cmd) {
|
|
|
56
48
|
|
|
57
49
|
cmd.addHelpText('after', () => {
|
|
58
50
|
const bin = styles.binName();
|
|
59
|
-
const cmdStrs = Object.keys(PLATFORMS).map(
|
|
60
|
-
const cmdWidth = Math.max(...cmdStrs.map(
|
|
61
|
-
const labelWidth = Math.max(...Object.values(PLATFORMS).map(
|
|
51
|
+
const cmdStrs = Object.keys(PLATFORMS).map(k => `${bin} setup:${k}`);
|
|
52
|
+
const cmdWidth = Math.max(...cmdStrs.map(s => s.length));
|
|
53
|
+
const labelWidth = Math.max(...Object.values(PLATFORMS).map(d => d.label.length));
|
|
62
54
|
const rows = Object.entries(PLATFORMS).map(([key, def], i) => {
|
|
63
55
|
const cmdStr = cmdStrs[i].padEnd(cmdWidth);
|
|
64
56
|
const label = def.label.padEnd(labelWidth);
|
|
@@ -153,12 +145,20 @@ export async function run(...callArgs) {
|
|
|
153
145
|
|
|
154
146
|
console.log(` This will create a workflow that runs on every pull request:`);
|
|
155
147
|
console.log();
|
|
156
|
-
console.log(
|
|
148
|
+
console.log(
|
|
149
|
+
` ${styles.success('✔')} ${styles.bold('Lint docs')} — runs ${styles.orange(`${styles.binName()} lint`)} and posts results as a PR comment`,
|
|
150
|
+
);
|
|
157
151
|
if (platform === 'github') {
|
|
158
|
-
console.log(
|
|
159
|
-
|
|
152
|
+
console.log(
|
|
153
|
+
` ${styles.success('✔')} ${styles.bold('OAS change detection')} — flags modified OpenAPI spec files and suggests syncing`,
|
|
154
|
+
);
|
|
155
|
+
console.log(
|
|
156
|
+
` ${styles.success('✔')} ${styles.bold('Auto-fix base branch')} — redirects PRs from ${styles.orange('main')} to the correct version branch`,
|
|
157
|
+
);
|
|
160
158
|
if (useBlacksmith) {
|
|
161
|
-
console.log(
|
|
159
|
+
console.log(
|
|
160
|
+
` ${styles.success('✔')} ${styles.bold('Blacksmith runner')} — uses ${styles.orange('blacksmith-2vcpu-ubuntu-2404')} instead of ${styles.orange('ubuntu-latest')}`,
|
|
161
|
+
);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
console.log();
|
|
@@ -169,9 +169,7 @@ export async function run(...callArgs) {
|
|
|
169
169
|
|
|
170
170
|
// 4. Confirm.
|
|
171
171
|
if (!opts.yes) {
|
|
172
|
-
const prompt = isOverwrite
|
|
173
|
-
? 'This will overwrite the existing workflow. Continue?'
|
|
174
|
-
: 'Set up this workflow?';
|
|
172
|
+
const prompt = isOverwrite ? 'This will overwrite the existing workflow. Continue?' : 'Set up this workflow?';
|
|
175
173
|
const confirmed = await confirm(prompt);
|
|
176
174
|
if (!confirmed) {
|
|
177
175
|
styles.info('Setup cancelled.');
|
|
@@ -203,7 +201,7 @@ function normalizePlatform(input) {
|
|
|
203
201
|
function printDetectionSummary(detection, chosen) {
|
|
204
202
|
const lines = [];
|
|
205
203
|
if (detection.existing.length > 0) {
|
|
206
|
-
const labels = detection.existing.map(
|
|
204
|
+
const labels = detection.existing.map(k => PLATFORMS[k].label).join(', ');
|
|
207
205
|
lines.push(`${styles.dim('Detected existing CI:')} ${labels}`);
|
|
208
206
|
}
|
|
209
207
|
if (detection.remote) {
|
|
@@ -246,8 +244,8 @@ function confirm(message) {
|
|
|
246
244
|
output: process.stdout,
|
|
247
245
|
});
|
|
248
246
|
|
|
249
|
-
return new Promise(
|
|
250
|
-
rl.question(`${styles.brand('?')} ${message} ${styles.dim('(Y/n)')} `,
|
|
247
|
+
return new Promise(resolve => {
|
|
248
|
+
rl.question(`${styles.brand('?')} ${message} ${styles.dim('(Y/n)')} `, answer => {
|
|
251
249
|
rl.close();
|
|
252
250
|
const val = answer.trim().toLowerCase();
|
|
253
251
|
resolve(val === '' || val === 'y' || val === 'yes');
|
package/src/commands/versions.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
2
3
|
import * as styles from '../utils/styles.js';
|
|
3
4
|
|
|
4
5
|
export const command = 'versions';
|
|
@@ -18,7 +19,10 @@ export function run(_options, _cmd, ctx) {
|
|
|
18
19
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
19
20
|
}).trim();
|
|
20
21
|
|
|
21
|
-
const branches = raw
|
|
22
|
+
const branches = raw
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map(b => b.trim())
|
|
25
|
+
.filter(Boolean);
|
|
22
26
|
|
|
23
27
|
// Strip "origin/" prefix and deduplicate.
|
|
24
28
|
const seen = new Set();
|
|
@@ -73,7 +77,7 @@ export function run(_options, _cmd, ctx) {
|
|
|
73
77
|
for (let i = 0; i < subs.length; i++) {
|
|
74
78
|
const isLast = i === subs.length - 1;
|
|
75
79
|
const connector = isLast ? '└─' : '├─';
|
|
76
|
-
console.log(` ${styles.dim(connector)} ${styles.dim(version
|
|
80
|
+
console.log(` ${styles.dim(connector)} ${styles.dim(`${version}_`)}${subs[i].name} ${styles.dim('(branch)')}`);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
|