@readme/cli 0.1.0 → 0.3.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/git-format.js +80 -0
- package/bin/readme.js +1 -1
- package/package.json +54 -26
- 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 +2200 -2049
- package/src/commands/lint.js +33 -29
- package/src/commands/oas-bundle.js +6 -2
- package/src/commands/oas-sync.js +271 -161
- package/src/commands/oas-validate.js +10 -7
- package/src/commands/pretty.js +188 -165
- 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/containment.js +87 -0
- 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 -94
- 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 +16 -14
- package/src/validators/duplicates.js +6 -2
- package/src/validators/frontmatter.js +21 -29
- package/src/validators/links.js +4 -8
- package/src/validators/oas-reference.js +25 -18
- package/src/validators/oas-schema.js +2 -0
- package/src/validators/ordering.js +5 -5
- package/src/validators/recipes.js +4 -12
- package/vendor/git-format/CLAUDE.md +352 -0
- package/vendor/git-format/frontmatter.schema.json +199 -0
- 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
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
|
|
3
4
|
import OASNormalize from 'oas-normalize';
|
|
4
|
-
|
|
5
|
-
import * as styles from '../utils/styles.js';
|
|
5
|
+
|
|
6
6
|
import { writeGithubActionsOutputs } from '../utils/gha-output.js';
|
|
7
|
+
import * as styles from '../utils/styles.js';
|
|
8
|
+
|
|
9
|
+
import { findOasFiles, extractOperations } from './oas-sync.js';
|
|
7
10
|
|
|
8
11
|
export const command = 'oas:validate';
|
|
9
12
|
export const order = 3;
|
|
@@ -114,7 +117,7 @@ export async function validateOas({ cwd, dereference = false } = {}) {
|
|
|
114
117
|
continue;
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
const errors = normalizerResult.valid ? [] :
|
|
120
|
+
const errors = normalizerResult.valid ? [] : normalizerResult.errors || [];
|
|
118
121
|
const warnings = normalizerResult.warnings || [];
|
|
119
122
|
|
|
120
123
|
if (dereference) {
|
|
@@ -158,9 +161,7 @@ export async function validateOasFiles(gitRoot, { dereference = false } = {}) {
|
|
|
158
161
|
|
|
159
162
|
for (const r of summary.results) {
|
|
160
163
|
const meta = `${r.filename} · ${r.version} · ${r.opCount} ${r.opCount === 1 ? 'endpoint' : 'endpoints'}`;
|
|
161
|
-
const dot = r.errors.length > 0
|
|
162
|
-
? styles.err('●')
|
|
163
|
-
: r.warnings.length > 0 ? styles.warn('●') : styles.success('●');
|
|
164
|
+
const dot = r.errors.length > 0 ? styles.err('●') : r.warnings.length > 0 ? styles.warn('●') : styles.success('●');
|
|
164
165
|
|
|
165
166
|
console.log();
|
|
166
167
|
console.log(` ${dot} ${styles.bold(r.title)} ${styles.dim(`(${meta})`)}`);
|
|
@@ -224,7 +225,9 @@ export async function run(options, _cmd, ctx) {
|
|
|
224
225
|
styles.error(`${parts.join(' and ')} across ${fileCount} ${fileCount === 1 ? 'spec' : 'specs'}.`);
|
|
225
226
|
process.exit(1);
|
|
226
227
|
} else if (totalWarnings > 0) {
|
|
227
|
-
styles.warning(
|
|
228
|
+
styles.warning(
|
|
229
|
+
`${totalValid} of ${fileCount} specs valid with ${totalWarnings} ${totalWarnings === 1 ? 'warning' : 'warnings'}.`,
|
|
230
|
+
);
|
|
228
231
|
} else {
|
|
229
232
|
styles.ok(`${fileCount} ${fileCount === 1 ? 'spec' : 'specs'} validated — all good!`);
|
|
230
233
|
}
|
package/src/commands/pretty.js
CHANGED
|
@@ -1,72 +1,73 @@
|
|
|
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 {
|
|
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 { makeContainment, PathEscapeError } from '../utils/containment.js';
|
|
10
|
+
import { printHeader, isAgenticCli } from '../utils/eyes.js';
|
|
11
|
+
import { collectFiles } from '../utils/lint.js';
|
|
12
|
+
import { parseFrontmatter, toFrontmatter, frontmatterWriteError } from '../utils/markdown.js';
|
|
12
13
|
import {
|
|
13
14
|
listLocalComponents,
|
|
14
15
|
listMarketplaceComponents,
|
|
15
16
|
installComponent,
|
|
16
17
|
readCachedReadme,
|
|
17
18
|
summarizeReadme,
|
|
18
|
-
} from '../utils/marketplace.js'
|
|
19
|
-
import
|
|
19
|
+
} from '../utils/marketplace.js';
|
|
20
|
+
import { MultiSpinner } from '../utils/multi-spinner.js';
|
|
21
|
+
import { runPool } from '../utils/promise-pool.js';
|
|
22
|
+
import * as styles from '../utils/styles.js';
|
|
20
23
|
|
|
21
|
-
const require = createRequire(import.meta.url)
|
|
22
|
-
const pkg = require('../../package.json')
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const pkg = require('../../package.json');
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* Native ReadMe MDX tags that don't need a marketplace install — the renderer
|
|
26
29
|
* supports them out of the box. We exclude these from auto-install detection.
|
|
27
30
|
*/
|
|
28
|
-
const NATIVE_TAGS = new Set([
|
|
29
|
-
'Tabs', 'Tab',
|
|
30
|
-
'Accordion',
|
|
31
|
-
'Cards', 'Card',
|
|
32
|
-
'Columns', 'Column',
|
|
33
|
-
'Image',
|
|
34
|
-
])
|
|
31
|
+
const NATIVE_TAGS = new Set(['Tabs', 'Tab', 'Accordion', 'Cards', 'Card', 'Columns', 'Column', 'Image']);
|
|
35
32
|
|
|
36
33
|
/**
|
|
37
34
|
* Pull every PascalCase tag name out of a block of mdx. Matches both
|
|
38
35
|
* `<Foo ... />` and `<Foo>...</Foo>`. Returns a Set of unique names.
|
|
39
36
|
*/
|
|
40
37
|
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
|
|
38
|
+
const tags = new Set();
|
|
39
|
+
const re = /<([A-Z][A-Za-z0-9]*)\b/g;
|
|
40
|
+
let m;
|
|
41
|
+
while ((m = re.exec(mdx)) !== null) tags.add(m[1]);
|
|
42
|
+
return tags;
|
|
46
43
|
}
|
|
47
44
|
|
|
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
|
|
45
|
+
export const command = 'pretty [file]';
|
|
46
|
+
export const order = 2;
|
|
47
|
+
export const description = 'Polish docs into ReadMe-ready MDX with Claude';
|
|
48
|
+
export const beta = true;
|
|
52
49
|
|
|
53
50
|
export function args(cmd) {
|
|
54
|
-
cmd.addOption(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
cmd.addOption(
|
|
52
|
+
new Option('-m, --model <name>', 'Claude model alias: haiku, sonnet, opus')
|
|
53
|
+
.choices(['haiku', 'sonnet', 'opus'])
|
|
54
|
+
.default('sonnet'),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
cmd.option('--dry-run', 'Show what would change without writing files');
|
|
58
|
+
cmd.option('--no-auto-install', "Don't auto-install marketplace components used in output");
|
|
58
59
|
cmd.option(
|
|
59
60
|
'-j, --jobs <n>',
|
|
60
61
|
'Number of files to prettify in parallel',
|
|
61
|
-
|
|
62
|
-
const n = Number(raw)
|
|
62
|
+
raw => {
|
|
63
|
+
const n = Number(raw);
|
|
63
64
|
if (!Number.isInteger(n) || n < 1) {
|
|
64
|
-
throw new InvalidArgumentError('must be a positive integer')
|
|
65
|
+
throw new InvalidArgumentError('must be a positive integer');
|
|
65
66
|
}
|
|
66
|
-
return n
|
|
67
|
+
return n;
|
|
67
68
|
},
|
|
68
69
|
1,
|
|
69
|
-
)
|
|
70
|
+
);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
@@ -76,41 +77,41 @@ export function args(cmd) {
|
|
|
76
77
|
* through to the next strategy.
|
|
77
78
|
*/
|
|
78
79
|
async function fetchWithRetry(url, { headers = {}, retries = 3, baseDelayMs = 1000 } = {}) {
|
|
79
|
-
const maxAttempts = retries + 1
|
|
80
|
-
let lastError
|
|
80
|
+
const maxAttempts = retries + 1;
|
|
81
|
+
let lastError;
|
|
81
82
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
82
83
|
try {
|
|
83
84
|
const res = await fetch(url, {
|
|
84
85
|
redirect: 'follow',
|
|
85
86
|
headers: { 'User-Agent': 'readme-cli-pretty', ...headers },
|
|
86
|
-
})
|
|
87
|
+
});
|
|
87
88
|
if (!res.ok) {
|
|
88
|
-
const err = new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
89
|
-
err.status = res.status
|
|
90
|
-
throw err
|
|
89
|
+
const err = new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
90
|
+
err.status = res.status;
|
|
91
|
+
throw err;
|
|
91
92
|
}
|
|
92
|
-
return await res.text()
|
|
93
|
+
return await res.text();
|
|
93
94
|
} catch (err) {
|
|
94
|
-
lastError = err
|
|
95
|
-
if (err.status && err.status >= 400 && err.status < 500) break
|
|
95
|
+
lastError = err;
|
|
96
|
+
if (err.status && err.status >= 400 && err.status < 500) break;
|
|
96
97
|
if (attempt < maxAttempts) {
|
|
97
|
-
const delay = baseDelayMs *
|
|
98
|
-
await new Promise(
|
|
98
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
|
-
throw lastError
|
|
103
|
+
throw lastError;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
function withMdExtension(url) {
|
|
106
107
|
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()
|
|
108
|
+
const u = new URL(url);
|
|
109
|
+
if (u.pathname.endsWith('.md')) return null;
|
|
110
|
+
u.pathname = `${u.pathname.replace(/\/$/, '')}.md`;
|
|
111
|
+
return u.toString();
|
|
111
112
|
} catch {
|
|
112
|
-
if (url.endsWith('.md')) return null
|
|
113
|
-
return url.replace(/\/$/, '')
|
|
113
|
+
if (url.endsWith('.md')) return null;
|
|
114
|
+
return `${url.replace(/\/$/, '')}.md`;
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -122,21 +123,21 @@ function withMdExtension(url) {
|
|
|
122
123
|
* Each strategy gets its own retry budget for transient failures.
|
|
123
124
|
*/
|
|
124
125
|
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 })
|
|
126
|
+
const strategies = [];
|
|
127
|
+
const mdUrl = withMdExtension(url);
|
|
128
|
+
if (mdUrl) strategies.push({ url: mdUrl });
|
|
129
|
+
strategies.push({ url, headers: { Accept: 'text/markdown' } });
|
|
130
|
+
strategies.push({ url });
|
|
130
131
|
|
|
131
|
-
let lastError
|
|
132
|
+
let lastError;
|
|
132
133
|
for (const strategy of strategies) {
|
|
133
134
|
try {
|
|
134
|
-
return await fetchWithRetry(strategy.url, { ...opts, headers: strategy.headers })
|
|
135
|
+
return await fetchWithRetry(strategy.url, { ...opts, headers: strategy.headers });
|
|
135
136
|
} catch (err) {
|
|
136
|
-
lastError = err
|
|
137
|
+
lastError = err;
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
|
-
throw new Error(`failed to fetch ${url}: ${lastError?.message || lastError}`)
|
|
140
|
+
throw new Error(`failed to fetch ${url}: ${lastError?.message || lastError}`);
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
/**
|
|
@@ -152,10 +153,14 @@ async function fetchImport(url, opts = {}) {
|
|
|
152
153
|
*/
|
|
153
154
|
async function prettifyPage({ source, title, relativePath, model, localComponents, availableComponents }) {
|
|
154
155
|
const { systemPrompt, userPrompt } = prettifyPagePrompt({
|
|
155
|
-
source,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
source,
|
|
157
|
+
title,
|
|
158
|
+
relativePath,
|
|
159
|
+
localComponents,
|
|
160
|
+
availableComponents,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
let structured;
|
|
159
164
|
for await (const message of query({
|
|
160
165
|
prompt: userPrompt,
|
|
161
166
|
options: {
|
|
@@ -167,131 +172,145 @@ async function prettifyPage({ source, title, relativePath, model, localComponent
|
|
|
167
172
|
})) {
|
|
168
173
|
if (message.type === 'result') {
|
|
169
174
|
if (message.subtype !== 'success') {
|
|
170
|
-
throw new Error(`${message.subtype}${message.error?.message ?
|
|
175
|
+
throw new Error(`${message.subtype}${message.error?.message ? ` — ${message.error.message}` : ''}`);
|
|
171
176
|
}
|
|
172
|
-
structured = message.structured_output
|
|
173
|
-
break
|
|
177
|
+
structured = message.structured_output;
|
|
178
|
+
break;
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
if (!structured || typeof structured !== 'object') {
|
|
178
|
-
throw new Error('agent returned no structured output')
|
|
183
|
+
throw new Error('agent returned no structured output');
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
let oasPartial = null
|
|
186
|
+
let oasPartial = null;
|
|
182
187
|
if (typeof structured.oasPartialJson === 'string' && structured.oasPartialJson.length > 0) {
|
|
183
188
|
try {
|
|
184
|
-
oasPartial = JSON.parse(structured.oasPartialJson)
|
|
189
|
+
oasPartial = JSON.parse(structured.oasPartialJson);
|
|
185
190
|
} catch (e) {
|
|
186
|
-
throw new Error(
|
|
191
|
+
throw new Error(
|
|
192
|
+
`invalid oasPartialJson (${e.message}); first 200 chars: ${structured.oasPartialJson.slice(0, 200)}`,
|
|
193
|
+
{ cause: e },
|
|
194
|
+
);
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
|
|
190
|
-
return { body: structured.body, isApiRef: structured.isApiRef, oasPartial }
|
|
198
|
+
return { body: structured.body, isApiRef: structured.isApiRef, oasPartial };
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
export async function run(file, options, _cmd, ctx) {
|
|
194
|
-
const { gitRoot } = ctx
|
|
202
|
+
const { gitRoot } = ctx;
|
|
195
203
|
|
|
196
204
|
if (!isAgenticCli()) {
|
|
197
|
-
printHeader({ version: pkg.version, binName: styles.binName(), indent: ' ' })
|
|
198
|
-
console.log()
|
|
205
|
+
printHeader({ version: pkg.version, binName: styles.binName(), indent: ' ' });
|
|
206
|
+
console.log();
|
|
199
207
|
}
|
|
200
208
|
|
|
201
|
-
|
|
209
|
+
// Lexical `path.relative` treats a symlink inside the repo as inside it,
|
|
210
|
+
// and the later write follows the link. Same check `oas:sync` uses.
|
|
211
|
+
const assertContained = makeContainment(gitRoot, { label: 'repository' });
|
|
212
|
+
|
|
213
|
+
let files;
|
|
202
214
|
if (file) {
|
|
203
|
-
const absFile = path.resolve(file)
|
|
215
|
+
const absFile = path.resolve(file);
|
|
204
216
|
if (!fs.existsSync(absFile)) {
|
|
205
|
-
styles.error(`File not found: ${styles.bold(file)}`)
|
|
206
|
-
process.exit(1)
|
|
217
|
+
styles.error(`File not found: ${styles.bold(file)}`);
|
|
218
|
+
process.exit(1);
|
|
207
219
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
assertContained(absFile);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (!(err instanceof PathEscapeError)) throw err;
|
|
225
|
+
styles.error(err.message);
|
|
226
|
+
process.exit(1);
|
|
212
227
|
}
|
|
213
|
-
|
|
228
|
+
|
|
229
|
+
files = [path.relative(gitRoot, absFile)];
|
|
214
230
|
} else {
|
|
215
|
-
files = collectFiles(gitRoot)
|
|
231
|
+
files = collectFiles(gitRoot);
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
if (files.length === 0) {
|
|
219
|
-
styles.warning('No docs files found to prettify.')
|
|
220
|
-
return
|
|
235
|
+
styles.warning('No docs files found to prettify.');
|
|
236
|
+
return;
|
|
221
237
|
}
|
|
222
238
|
|
|
223
|
-
const localComponents = listLocalComponents(gitRoot)
|
|
224
|
-
const localNames = new Set(localComponents.map(
|
|
239
|
+
const localComponents = listLocalComponents(gitRoot);
|
|
240
|
+
const localNames = new Set(localComponents.map(c => c.name));
|
|
225
241
|
|
|
226
242
|
// Pull the full marketplace catalog so Claude knows what else it can use.
|
|
227
243
|
// Summaries come from disk cache (populated by `readme components list`);
|
|
228
244
|
// we don't fetch readmes here to keep the prompt path fast.
|
|
229
|
-
let marketplace = []
|
|
245
|
+
let marketplace = [];
|
|
230
246
|
if (options.autoInstall !== false) {
|
|
231
247
|
try {
|
|
232
|
-
marketplace = await listMarketplaceComponents()
|
|
248
|
+
marketplace = await listMarketplaceComponents();
|
|
233
249
|
} catch (err) {
|
|
234
|
-
styles.warning(`Could not load marketplace catalog: ${err.message} (continuing without it)`)
|
|
250
|
+
styles.warning(`Could not load marketplace catalog: ${err.message} (continuing without it)`);
|
|
235
251
|
}
|
|
236
252
|
}
|
|
237
|
-
const marketplaceNames = new Set(marketplace.map(
|
|
253
|
+
const marketplaceNames = new Set(marketplace.map(c => c.name));
|
|
238
254
|
const availableComponents = marketplace
|
|
239
|
-
.filter(
|
|
240
|
-
.map(
|
|
255
|
+
.filter(c => !localNames.has(c.name))
|
|
256
|
+
.map(c => ({ name: c.name, summary: summarizeReadme(readCachedReadme(c.name)) }));
|
|
241
257
|
|
|
242
258
|
// Concurrent workers may both try to install the same component. Coalesce
|
|
243
259
|
// duplicate installs through a Map<tag, Promise> — the second caller awaits
|
|
244
260
|
// the first's install instead of clobbering it.
|
|
245
|
-
const inFlightInstalls = new Map()
|
|
246
|
-
const autoInstalled = new Set()
|
|
261
|
+
const inFlightInstalls = new Map();
|
|
262
|
+
const autoInstalled = new Set();
|
|
247
263
|
function ensureInstalled(tag) {
|
|
248
|
-
if (inFlightInstalls.has(tag)) return inFlightInstalls.get(tag)
|
|
264
|
+
if (inFlightInstalls.has(tag)) return inFlightInstalls.get(tag);
|
|
249
265
|
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
|
|
266
|
+
await installComponent(gitRoot, tag);
|
|
267
|
+
localNames.add(tag);
|
|
268
|
+
autoInstalled.add(tag);
|
|
269
|
+
})();
|
|
270
|
+
inFlightInstalls.set(tag, p);
|
|
271
|
+
return p;
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
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
|
-
)
|
|
275
|
+
`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)') : ''}.`,
|
|
276
|
+
);
|
|
261
277
|
if (localComponents.length > 0) {
|
|
262
278
|
styles.info(
|
|
263
|
-
`Found ${styles.bold(String(localComponents.length))} local component${localComponents.length === 1 ? '' : 's'} in ${styles.bold('./components/')}: ${localComponents.map(
|
|
264
|
-
)
|
|
279
|
+
`Found ${styles.bold(String(localComponents.length))} local component${localComponents.length === 1 ? '' : 's'} in ${styles.bold('./components/')}: ${localComponents.map(c => c.name).join(', ')}`,
|
|
280
|
+
);
|
|
265
281
|
}
|
|
266
282
|
if (options.autoInstall !== false && availableComponents.length > 0) {
|
|
267
283
|
styles.info(
|
|
268
284
|
`${styles.bold(String(availableComponents.length))} additional marketplace component${availableComponents.length === 1 ? '' : 's'} will be auto-installed if used.`,
|
|
269
|
-
)
|
|
285
|
+
);
|
|
270
286
|
}
|
|
271
|
-
console.log()
|
|
287
|
+
console.log();
|
|
272
288
|
|
|
273
|
-
let updated = 0
|
|
274
|
-
let unchanged = 0
|
|
275
|
-
const failures = []
|
|
289
|
+
let updated = 0;
|
|
290
|
+
let unchanged = 0;
|
|
291
|
+
const failures = [];
|
|
276
292
|
|
|
277
|
-
const rows = Math.min(options.jobs, files.length)
|
|
278
|
-
const multi = new MultiSpinner({ rows, color: 'blue' })
|
|
279
|
-
multi.start()
|
|
293
|
+
const rows = Math.min(options.jobs, files.length);
|
|
294
|
+
const multi = new MultiSpinner({ rows, color: 'blue' });
|
|
295
|
+
multi.start();
|
|
280
296
|
|
|
281
297
|
await runPool(files, options.jobs, async (relativePath, _i, workerIdx) => {
|
|
282
|
-
multi.setLine(workerIdx, relativePath)
|
|
298
|
+
multi.setLine(workerIdx, relativePath);
|
|
283
299
|
|
|
284
300
|
try {
|
|
285
|
-
const filePath = path.join(gitRoot, relativePath)
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
301
|
+
const filePath = path.join(gitRoot, relativePath);
|
|
302
|
+
assertContained(filePath);
|
|
303
|
+
const original = fs.readFileSync(filePath, 'utf-8');
|
|
304
|
+
const parsed = parseFrontmatter(original);
|
|
305
|
+
const parseError = frontmatterWriteError(parsed);
|
|
306
|
+
if (parseError) throw parseError;
|
|
307
|
+
|
|
308
|
+
let source = parsed.content;
|
|
309
|
+
const importUrl = parsed.data?.['x-import'];
|
|
291
310
|
if (importUrl) {
|
|
292
|
-
multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(fetching ${importUrl})`)}`)
|
|
293
|
-
source = await fetchImport(importUrl)
|
|
294
|
-
multi.setLine(workerIdx, relativePath)
|
|
311
|
+
multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(fetching ${importUrl})`)}`);
|
|
312
|
+
source = await fetchImport(importUrl);
|
|
313
|
+
multi.setLine(workerIdx, relativePath);
|
|
295
314
|
}
|
|
296
315
|
|
|
297
316
|
const result = await prettifyPage({
|
|
@@ -301,10 +320,10 @@ export async function run(file, options, _cmd, ctx) {
|
|
|
301
320
|
model: options.model,
|
|
302
321
|
localComponents,
|
|
303
322
|
availableComponents: options.autoInstall === false ? [] : availableComponents,
|
|
304
|
-
})
|
|
323
|
+
});
|
|
305
324
|
|
|
306
325
|
if (typeof result?.body !== 'string') {
|
|
307
|
-
throw new
|
|
326
|
+
throw new TypeError('response missing "body" field');
|
|
308
327
|
}
|
|
309
328
|
|
|
310
329
|
// Auto-install marketplace components Claude used in the body. We do
|
|
@@ -312,59 +331,63 @@ export async function run(file, options, _cmd, ctx) {
|
|
|
312
331
|
// together — and we share the inFlightInstalls map across workers so a
|
|
313
332
|
// tag is only fetched once.
|
|
314
333
|
if (options.autoInstall !== false && !options.dryRun) {
|
|
315
|
-
const used = extractUsedTags(result.body)
|
|
334
|
+
const used = extractUsedTags(result.body);
|
|
316
335
|
for (const tag of used) {
|
|
317
|
-
if (NATIVE_TAGS.has(tag)) continue
|
|
318
|
-
if (localNames.has(tag)) continue
|
|
319
|
-
if (!marketplaceNames.has(tag)) continue
|
|
336
|
+
if (NATIVE_TAGS.has(tag)) continue;
|
|
337
|
+
if (localNames.has(tag)) continue;
|
|
338
|
+
if (!marketplaceNames.has(tag)) continue;
|
|
320
339
|
try {
|
|
321
|
-
multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(installing <${tag}>)`)}`)
|
|
322
|
-
await ensureInstalled(tag)
|
|
340
|
+
multi.setLine(workerIdx, `${relativePath} ${styles.dim(`(installing <${tag}>)`)}`);
|
|
341
|
+
await ensureInstalled(tag);
|
|
323
342
|
} catch (err) {
|
|
324
|
-
failures.push({ relativePath, message: `auto-install <${tag}> failed: ${err.message}` })
|
|
343
|
+
failures.push({ relativePath, message: `auto-install <${tag}> failed: ${err.message}` });
|
|
325
344
|
}
|
|
326
345
|
}
|
|
327
|
-
multi.setLine(workerIdx, relativePath)
|
|
346
|
+
multi.setLine(workerIdx, relativePath);
|
|
328
347
|
}
|
|
329
348
|
|
|
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 =
|
|
349
|
+
const newFm = { ...parsed.data };
|
|
350
|
+
delete newFm.hidden;
|
|
351
|
+
delete newFm['x-import'];
|
|
352
|
+
if (result.excerpt && !newFm.excerpt) newFm.excerpt = result.excerpt;
|
|
353
|
+
const next = toFrontmatter(newFm, result.body);
|
|
335
354
|
|
|
336
355
|
if (next === original) {
|
|
337
|
-
unchanged
|
|
338
|
-
if (!process.stderr.isTTY) multi.log(` ${styles.dim('·')} ${relativePath}`)
|
|
356
|
+
unchanged++;
|
|
357
|
+
if (!process.stderr.isTTY) multi.log(` ${styles.dim('·')} ${relativePath}`);
|
|
339
358
|
} else {
|
|
340
|
-
if (!options.dryRun) fs.writeFileSync(filePath, next, 'utf-8')
|
|
341
|
-
updated
|
|
342
|
-
if (!process.stderr.isTTY) multi.log(` ${styles.success('✓')} ${relativePath}`)
|
|
359
|
+
if (!options.dryRun) fs.writeFileSync(filePath, next, 'utf-8');
|
|
360
|
+
updated++;
|
|
361
|
+
if (!process.stderr.isTTY) multi.log(` ${styles.success('✓')} ${relativePath}`);
|
|
343
362
|
}
|
|
344
363
|
} catch (err) {
|
|
345
|
-
failures.push({ relativePath, message: err.message || String(err) })
|
|
364
|
+
failures.push({ relativePath, message: err.message || String(err) });
|
|
346
365
|
if (!process.stderr.isTTY) {
|
|
347
|
-
multi.log(` ${styles.err('●')} ${relativePath} ${styles.dim(
|
|
366
|
+
multi.log(` ${styles.err('●')} ${relativePath} ${styles.dim(`— ${err.message || String(err)}`)}`);
|
|
348
367
|
}
|
|
349
368
|
} finally {
|
|
350
|
-
multi.clearLine(workerIdx)
|
|
369
|
+
multi.clearLine(workerIdx);
|
|
351
370
|
}
|
|
352
|
-
})
|
|
371
|
+
});
|
|
353
372
|
|
|
354
|
-
multi.stop()
|
|
373
|
+
multi.stop();
|
|
355
374
|
|
|
356
|
-
console.log()
|
|
357
|
-
const verb = options.dryRun ? 'would update' : 'updated'
|
|
358
|
-
styles.ok(
|
|
375
|
+
console.log();
|
|
376
|
+
const verb = options.dryRun ? 'would update' : 'updated';
|
|
377
|
+
styles.ok(
|
|
378
|
+
`${styles.bold(String(updated))} ${verb} · ${styles.bold(String(unchanged))} unchanged${failures.length ? ` · ${styles.bold(String(failures.length))} failed` : ''}`,
|
|
379
|
+
);
|
|
359
380
|
if (autoInstalled.size > 0) {
|
|
360
|
-
styles.ok(
|
|
381
|
+
styles.ok(
|
|
382
|
+
`Auto-installed ${styles.bold(String(autoInstalled.size))} component${autoInstalled.size === 1 ? '' : 's'}: ${[...autoInstalled].join(', ')}`,
|
|
383
|
+
);
|
|
361
384
|
}
|
|
362
385
|
|
|
363
386
|
if (failures.length > 0) {
|
|
364
|
-
console.log()
|
|
387
|
+
console.log();
|
|
365
388
|
for (const f of failures) {
|
|
366
|
-
console.log(` ${styles.err('●')} ${styles.bold(f.relativePath)} ${styles.dim(
|
|
389
|
+
console.log(` ${styles.err('●')} ${styles.bold(f.relativePath)} ${styles.dim(`— ${f.message}`)}`);
|
|
367
390
|
}
|
|
368
|
-
process.exit(1)
|
|
391
|
+
process.exit(1);
|
|
369
392
|
}
|
|
370
393
|
}
|