@readme/cli 0.0.31 → 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.
Files changed (54) hide show
  1. package/README.md +10 -0
  2. package/bin/readme.js +1 -1
  3. package/package.json +34 -24
  4. package/src/bootstrap.js +9 -13
  5. package/src/cli.js +17 -11
  6. package/src/commands/components.js +35 -18
  7. package/src/commands/dev.js +12 -8
  8. package/src/commands/eyes.js +12 -8
  9. package/src/commands/import.js +2263 -2066
  10. package/src/commands/lint.js +33 -29
  11. package/src/commands/oas-bundle.js +89 -0
  12. package/src/commands/oas-sync.js +96 -93
  13. package/src/commands/oas-validate.js +10 -7
  14. package/src/commands/pretty.js +177 -164
  15. package/src/commands/setup.js +24 -26
  16. package/src/commands/versions.js +6 -2
  17. package/src/dev/app/[...slug]/page.js +1 -0
  18. package/src/dev/app/layout.js +18 -11
  19. package/src/dev/app/page.js +1 -0
  20. package/src/dev/lib/docs.js +27 -19
  21. package/src/dev/next.config.mjs +2 -2
  22. package/src/gha.js +7 -7
  23. package/src/prompts/index.js +16 -6
  24. package/src/utils/eyes.js +143 -51
  25. package/src/utils/git.js +3 -3
  26. package/src/utils/lint.js +39 -38
  27. package/src/utils/llms.js +94 -91
  28. package/src/utils/markdown.js +256 -0
  29. package/src/utils/marketplace.js +12 -5
  30. package/src/utils/multi-spinner.js +5 -7
  31. package/src/utils/promise-pool.js +7 -5
  32. package/src/utils/reporter.js +88 -91
  33. package/src/utils/setup-templates.js +12 -6
  34. package/src/utils/styles.js +2 -1
  35. package/src/utils/tamagotchi.js +257 -91
  36. package/src/utils/tips.js +18 -9
  37. package/src/utils/url-segs.js +46 -41
  38. package/src/validators/components.js +55 -63
  39. package/src/validators/content.js +5 -10
  40. package/src/validators/duplicates.js +6 -2
  41. package/src/validators/frontmatter.js +19 -21
  42. package/src/validators/links.js +4 -8
  43. package/src/validators/oas-reference.js +7 -14
  44. package/src/validators/oas-schema.js +2 -0
  45. package/src/validators/ordering.js +5 -5
  46. package/src/validators/recipes.js +4 -12
  47. package/src/commands/import-oas.test.js +0 -277
  48. package/src/commands/import.classify.test.js +0 -172
  49. package/src/commands/import.landing.test.js +0 -217
  50. package/src/commands/import.slug-planner.test.js +0 -86
  51. package/src/commands/import.staging.test.js +0 -158
  52. package/src/commands/import.test.js +0 -382
  53. package/src/utils/llms.test.js +0 -302
  54. package/src/utils/url-segs.test.js +0 -239
@@ -1,72 +1,72 @@
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'
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 * as styles from '../utils/styles.js'
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(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')
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
- (raw) => {
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 * Math.pow(2, attempt - 1)
98
- await new Promise((resolve) => setTimeout(resolve, delay))
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(/\/$/, '') + '.md'
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(/\/$/, '') + '.md'
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, title, relativePath, localComponents, availableComponents,
156
- })
157
-
158
- let structured
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 ? ' — ' + 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(`invalid oasPartialJson (${e.message}); first 200 chars: ${structured.oasPartialJson.slice(0, 200)}`)
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((c) => c.name))
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((c) => c.name))
244
+ const marketplaceNames = new Set(marketplace.map(c => c.name));
238
245
  const availableComponents = marketplace
239
- .filter((c) => !localNames.has(c.name))
240
- .map((c) => ({ name: c.name, summary: summarizeReadme(readCachedReadme(c.name)) }))
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('-j ' + options.jobs)})` : ''}${options.dryRun ? styles.dim(' (dry run)') : ''}.`,
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((c) => c.name).join(', ')}`,
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 = matter(original)
288
-
289
- let source = parsed.content
290
- const importUrl = parsed.data?.['x-import']
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 Error('response missing "body" field')
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 = matter.stringify(result.body, newFm)
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('— ' + (err.message || String(err)))}`)
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(`${styles.bold(String(updated))} ${verb} · ${styles.bold(String(unchanged))} unchanged${failures.length ? ` · ${styles.bold(String(failures.length))} failed` : ''}`)
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(`Auto-installed ${styles.bold(String(autoInstalled.size))} component${autoInstalled.size === 1 ? '' : 's'}: ${[...autoInstalled].join(', ')}`)
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('— ' + f.message)}`)
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
  }
@@ -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
- import * as styles from '../utils/styles.js';
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
- 'setup [platform]',
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((k) => `${bin} setup:${k}`);
60
- const cmdWidth = Math.max(...cmdStrs.map((s) => s.length));
61
- const labelWidth = Math.max(...Object.values(PLATFORMS).map((d) => d.label.length));
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(` ${styles.success('✔')} ${styles.bold('Lint docs')} — runs ${styles.orange(`${styles.binName()} lint`)} and posts results as a PR comment`);
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(` ${styles.success('✔')} ${styles.bold('OAS change detection')} — flags modified OpenAPI spec files and suggests syncing`);
159
- console.log(` ${styles.success('✔')} ${styles.bold('Auto-fix base branch')} — redirects PRs from ${styles.orange('main')} to the correct version branch`);
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(` ${styles.success('✔')} ${styles.bold('Blacksmith runner')} — uses ${styles.orange('blacksmith-2vcpu-ubuntu-2404')} instead of ${styles.orange('ubuntu-latest')}`);
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((k) => PLATFORMS[k].label).join(', ');
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((resolve) => {
250
- rl.question(`${styles.brand('?')} ${message} ${styles.dim('(Y/n)')} `, (answer) => {
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');
@@ -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.split('\n').map((b) => b.trim()).filter(Boolean);
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 + '_')}${subs[i].name} ${styles.dim('(branch)')}`);
80
+ console.log(` ${styles.dim(connector)} ${styles.dim(`${version}_`)}${subs[i].name} ${styles.dim('(branch)')}`);
77
81
  }
78
82
  }
79
83
 
@@ -1,4 +1,5 @@
1
1
  import { redirect } from 'next/navigation';
2
+
2
3
  import { getPage, collectSidebar } from '../../lib/docs';
3
4
 
4
5
  export const dynamic = 'force-dynamic';