@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,30 +1,40 @@
1
- import { execSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { createRequire } from "node:module";
5
- import { fileURLToPath } from "node:url";
6
- import ora from "ora";
7
- import * as styles from "./styles.js";
8
- import { hasClaude } from "./claude.js";
9
- import { hasGithubRemote, hasGithubWorkflow, getWorkflowVersion, WORKFLOW_VERSION, detectPlatform, hasReadmeWorkflow, PLATFORMS } from "./git.js";
10
- import { getRandomTip } from "./tips.js";
1
+ import { execSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import ora from 'ora';
8
+
9
+ import { hasClaude } from './claude.js';
10
+ import {
11
+ hasGithubRemote,
12
+ hasGithubWorkflow,
13
+ getWorkflowVersion,
14
+ WORKFLOW_VERSION,
15
+ detectPlatform,
16
+ hasReadmeWorkflow,
17
+ PLATFORMS,
18
+ } from './git.js';
19
+ import * as styles from './styles.js';
20
+ import { getRandomTip } from './tips.js';
11
21
 
12
22
  const require = createRequire(import.meta.url);
13
23
 
14
24
  const isRunningInClaude = !!process.env.CLAUDECODE;
15
25
 
16
26
  const CATEGORY_LABELS = {
17
- custom_blocks: "custom blocks",
18
- docs: "docs",
19
- reference: "reference",
20
- custom_pages: "custom pages",
21
- recipes: "recipes",
27
+ custom_blocks: 'custom blocks',
28
+ docs: 'docs',
29
+ reference: 'reference',
30
+ custom_pages: 'custom pages',
31
+ recipes: 'recipes',
22
32
  };
23
33
 
24
34
  function categorize(files) {
25
35
  const counts = new Map();
26
36
  for (const file of files) {
27
- const dir = file.split("/")[0];
37
+ const dir = file.split('/')[0];
28
38
  const label = CATEGORY_LABELS[dir] || dir;
29
39
  counts.set(label, (counts.get(label) || 0) + 1);
30
40
  }
@@ -32,8 +42,8 @@ function categorize(files) {
32
42
  }
33
43
 
34
44
  function splitResults(results) {
35
- const errors = results.filter((r) => r.severity !== "warning");
36
- const warnings = results.filter((r) => r.severity === "warning");
45
+ const errors = results.filter(r => r.severity !== 'warning');
46
+ const warnings = results.filter(r => r.severity === 'warning');
37
47
  return { errors, warnings };
38
48
  }
39
49
 
@@ -43,17 +53,15 @@ function splitResults(results) {
43
53
  */
44
54
  function styleMessage(message, color) {
45
55
  // Strip "(fixed)" suffix — handled separately via the icon.
46
- const isFixed = message.endsWith("(fixed)");
47
- const raw = isFixed ? message.slice(0, -"(fixed)".length).trimEnd() : message;
56
+ const isFixed = message.endsWith('(fixed)');
57
+ const raw = isFixed ? message.slice(0, -'(fixed)'.length).trimEnd() : message;
48
58
 
49
- const colonIdx = raw.indexOf(":");
59
+ const colonIdx = raw.indexOf(':');
50
60
  let styled;
51
61
 
52
62
  if (colonIdx !== -1) {
53
63
  const title = raw.slice(0, colonIdx);
54
- const detail = raw
55
- .slice(colonIdx + 1)
56
- .replace(/"([^"]+)"/g, (_, val) => styles.bold(`"${val}"`));
64
+ const detail = raw.slice(colonIdx + 1).replace(/"([^"]+)"/g, (_, val) => styles.bold(`"${val}"`));
57
65
  styled = `${color(title)}:${detail}`;
58
66
  } else {
59
67
  // No colon — highlight quoted values, keep rest normal.
@@ -65,20 +73,20 @@ function styleMessage(message, color) {
65
73
 
66
74
  function summaryLine(errorCount, warningCount) {
67
75
  const parts = [];
68
- if (errorCount > 0)
69
- parts.push(`${errorCount} ${errorCount === 1 ? "error" : "errors"}`);
70
- if (warningCount > 0)
71
- parts.push(
72
- `${warningCount} ${warningCount === 1 ? "warning" : "warnings"}`,
73
- );
74
- return parts.join(" and ");
76
+ if (errorCount > 0) {
77
+ parts.push(`${errorCount} ${errorCount === 1 ? 'error' : 'errors'}`);
78
+ }
79
+ if (warningCount > 0) {
80
+ parts.push(`${warningCount} ${warningCount === 1 ? 'warning' : 'warnings'}`);
81
+ }
82
+ return parts.join(' and ');
75
83
  }
76
84
 
77
85
  /**
78
86
  * Human-readable reporter with an animated spinner.
79
87
  */
80
88
  export function createHumanReporter() {
81
- const spinner = ora({ text: "Running linting...", color: "blue" }).start();
89
+ const spinner = ora({ text: 'Running linting...', color: 'blue' }).start();
82
90
 
83
91
  return {
84
92
  onFile(relativePath) {
@@ -90,13 +98,11 @@ export function createHumanReporter() {
90
98
  },
91
99
 
92
100
  finish(total, results, files, { fix, gitRoot } = {}) {
93
- spinner.suffixText = "";
101
+ spinner.suffixText = '';
94
102
 
95
103
  const { errors, warnings } = splitResults(results);
96
104
  const categories = categorize(files);
97
- const breakdown = categories
98
- .map(([label, count]) => `${count} ${label}`)
99
- .join(styles.dim(" · "));
105
+ const breakdown = categories.map(([label, count]) => `${count} ${label}`).join(styles.dim(' · '));
100
106
 
101
107
  if (errors.length === 0 && warnings.length === 0) {
102
108
  spinner.succeed(`${total} files checked — all good!`);
@@ -105,9 +111,7 @@ export function createHumanReporter() {
105
111
  }
106
112
 
107
113
  if (errors.length > 0) {
108
- spinner.fail(
109
- `${summaryLine(errors.length, warnings.length)} in ${total} files`,
110
- );
114
+ spinner.fail(`${summaryLine(errors.length, warnings.length)} in ${total} files`);
111
115
  } else {
112
116
  spinner.warn(`${summaryLine(0, warnings.length)} in ${total} files`);
113
117
  }
@@ -124,32 +128,29 @@ export function createHumanReporter() {
124
128
  for (const [file, fileResults] of grouped) {
125
129
  console.log(` ${styles.bold(file)}`);
126
130
  for (const r of fileResults) {
127
- const baseColor = r.severity === "warning" ? styles.warn : styles.err;
128
- const isFixed = r.message.endsWith("(fixed)");
131
+ const baseColor = r.severity === 'warning' ? styles.warn : styles.err;
132
+ const isFixed = r.message.endsWith('(fixed)');
129
133
  const color = isFixed ? styles.success : baseColor;
130
134
  const styled = styleMessage(r.message, color);
131
- const icon = isFixed ? styles.success("✔") : baseColor("●");
135
+ const icon = isFixed ? styles.success('✔') : baseColor('●');
132
136
  console.log(` ${icon} ${styled}`);
133
137
  }
134
138
  console.log();
135
139
  }
136
140
 
137
141
  // Tips section.
138
- const fixable = !fix
139
- ? results.filter((r) => r.fixable && !r.message.endsWith("(fixed)"))
140
- : [];
141
- const unfixed = results.filter((r) => !r.message.endsWith("(fixed)"));
142
- const showTips =
143
- fixable.length > 0 || (unfixed.length > 0 && !isRunningInClaude);
142
+ const fixable = !fix ? results.filter(r => r.fixable && !r.message.endsWith('(fixed)')) : [];
143
+ const unfixed = results.filter(r => !r.message.endsWith('(fixed)'));
144
+ const showTips = fixable.length > 0 || (unfixed.length > 0 && !isRunningInClaude);
144
145
 
145
146
  if (showTips) {
146
- console.log(` ${styles.dim("─".repeat(40))}`);
147
+ console.log(` ${styles.dim('─'.repeat(40))}`);
147
148
  console.log();
148
149
  }
149
150
 
150
151
  if (fixable.length > 0) {
151
152
  console.log(
152
- ` ${styles.dim("Run")} ${styles.binName()} lint --fix ${styles.dim("to automatically fix some of these.")}`,
153
+ ` ${styles.dim('Run')} ${styles.binName()} lint --fix ${styles.dim('to automatically fix some of these.')}`,
153
154
  );
154
155
  console.log();
155
156
  }
@@ -159,9 +160,7 @@ export function createHumanReporter() {
159
160
  const workflowVersion = gitRoot ? getWorkflowVersion(gitRoot) : null;
160
161
  const detection = gitRoot ? detectPlatform(gitRoot) : { recommended: null };
161
162
  const detectedPlatform = detection.recommended;
162
- const hasCiWorkflow = detectedPlatform && gitRoot
163
- ? hasReadmeWorkflow(gitRoot, detectedPlatform)
164
- : true;
163
+ const hasCiWorkflow = detectedPlatform && gitRoot ? hasReadmeWorkflow(gitRoot, detectedPlatform) : true;
165
164
  const tip = getRandomTip({
166
165
  isRunningInClaude,
167
166
  hasClaude: hasClaude(),
@@ -177,39 +176,36 @@ export function createHumanReporter() {
177
176
 
178
177
  // When running inside Claude, output instructions and structured issue list.
179
178
  if (isRunningInClaude && unfixed.length > 0) {
180
- const cliRoot = path.join(
181
- path.dirname(fileURLToPath(import.meta.url)),
182
- "../..",
183
- );
179
+ const cliRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '../..');
184
180
 
185
181
  const gitFormatDir = path.dirname(require.resolve('@readmeio/git-format/package.json'));
186
- const claudeMdPath = path.join(gitFormatDir, "CLAUDE.md");
187
- const toolsMdPath = path.join(cliRoot, "vendor/TOOLS.md");
188
- const schemaPath = path.join(gitFormatDir, "frontmatter.schema.json");
182
+ const claudeMdPath = path.join(gitFormatDir, 'CLAUDE.md');
183
+ const toolsMdPath = path.join(cliRoot, 'vendor/TOOLS.md');
184
+ const schemaPath = path.join(gitFormatDir, 'frontmatter.schema.json');
189
185
 
190
- console.log("\n<claude-instructions>");
186
+ console.log('\n<claude-instructions>');
191
187
 
192
188
  if (fs.existsSync(toolsMdPath)) {
193
- console.log(fs.readFileSync(toolsMdPath, "utf-8"));
189
+ console.log(fs.readFileSync(toolsMdPath, 'utf-8'));
194
190
  }
195
191
 
196
192
  if (fs.existsSync(claudeMdPath)) {
197
- console.log(fs.readFileSync(claudeMdPath, "utf-8"));
193
+ console.log(fs.readFileSync(claudeMdPath, 'utf-8'));
198
194
  }
199
195
 
200
196
  if (fs.existsSync(schemaPath)) {
201
- console.log("\n## Frontmatter JSON Schema\n");
202
- console.log("```json");
203
- console.log(fs.readFileSync(schemaPath, "utf-8"));
204
- console.log("```");
197
+ console.log('\n## Frontmatter JSON Schema\n');
198
+ console.log('```json');
199
+ console.log(fs.readFileSync(schemaPath, 'utf-8'));
200
+ console.log('```');
205
201
  }
206
202
 
207
- console.log("</claude-instructions>\n");
208
- console.log("<issues>");
203
+ console.log('</claude-instructions>\n');
204
+ console.log('<issues>');
209
205
  for (const r of unfixed) {
210
206
  console.log(`- [${r.severity}] ${r.file}: ${r.message}`);
211
207
  }
212
- console.log("</issues>");
208
+ console.log('</issues>');
213
209
  }
214
210
  },
215
211
  };
@@ -228,35 +224,36 @@ export function createGithubReporter() {
228
224
  const all = [...errors, ...warnings];
229
225
 
230
226
  // Build file links using GitHub Actions env vars
231
- const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
232
- const repo = process.env.GITHUB_REPOSITORY || "";
233
- const sha = process.env.GITHUB_SHA || "";
227
+ const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
228
+ const repo = process.env.GITHUB_REPOSITORY || '';
229
+ const sha = process.env.GITHUB_SHA || '';
234
230
  const canLink = repo && sha;
235
231
 
236
232
  function fileLink(filePath) {
237
- const name = filePath.split("/").pop();
233
+ const name = filePath.split('/').pop();
238
234
  if (canLink) {
239
- const encoded = filePath.split("/").map(encodeURIComponent).join("/");
235
+ const encoded = filePath.split('/').map(encodeURIComponent).join('/');
240
236
  return `[\`${name}\`](${serverUrl}/${repo}/blob/${sha}/${encoded})`;
241
237
  }
242
238
  return `\`${name}\``;
243
239
  }
244
240
 
245
- let body = "<!-- readme-lint-results -->\n";
246
- body += "## ReadMe Docs Lint\n\n";
241
+ let body = '<!-- readme-lint-results -->\n';
242
+ body += '## ReadMe Docs Lint\n\n';
247
243
 
248
244
  if (all.length === 0) {
249
- body += "> **All checks passed!** No lint issues found.\n";
245
+ body += '> **All checks passed!** No lint issues found.\n';
250
246
  } else {
251
- body += "| File | Message |\n";
252
- body += "|------|--------|\n";
247
+ body += '| File | Message |\n';
248
+ body += '|------|--------|\n';
253
249
  for (const r of all) {
254
- const isError = r.severity !== "warning";
255
- const label = isError ? "\u{1F534} **Error:**" : "\u{1F7E1} **Warning:**";
250
+ const isError = r.severity !== 'warning';
251
+ const label = isError ? '\u{1F534} **Error:**' : '\u{1F7E1} **Warning:**';
256
252
  body += `| ${fileLink(r.file)} | ${label} ${r.message} |\n`;
257
253
  }
258
- body += "\n";
259
- body += "> \u{1F4A1} **Tip:** Run `npx @readme/cli lint --fix` locally to automatically fix some of these issues.\n\n";
254
+ body += '\n';
255
+ body +=
256
+ '> \u{1F4A1} **Tip:** Run `npx @readme/cli lint --fix` locally to automatically fix some of these issues.\n\n';
260
257
  }
261
258
 
262
259
  // OAS change detection
@@ -265,25 +262,25 @@ export function createGithubReporter() {
265
262
  try {
266
263
  const changed = execSync(
267
264
  `git diff --name-only ${baseSha}..HEAD -- 'reference/*.json' 'reference/*.yaml' 'reference/*.yml'`,
268
- { cwd: gitRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
265
+ { cwd: gitRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
269
266
  ).trim();
270
267
 
271
268
  if (changed) {
272
- const files = changed.split("\n");
273
- body += "### OAS Changes Detected\n\n";
274
- body += "The following OpenAPI spec files were changed in this PR:\n\n";
269
+ const files = changed.split('\n');
270
+ body += '### OAS Changes Detected\n\n';
271
+ body += 'The following OpenAPI spec files were changed in this PR:\n\n';
275
272
  for (const f of files) {
276
273
  body += `- ${fileLink(f)}\n`;
277
274
  }
278
- body += "\nRun `npx @readme/cli oas:sync` to sync these changes to ReadMe.\n\n";
275
+ body += '\nRun `npx @readme/cli oas:sync` to sync these changes to ReadMe.\n\n';
279
276
  }
280
277
  } catch {
281
278
  // git diff failed — skip OAS section silently
282
279
  }
283
280
  }
284
281
 
285
- body += "---\n";
286
- body += "\u{1F989} Powered by [ReadMe](https://readme.com)\n";
282
+ body += '---\n';
283
+ body += '\u{1F989} Powered by [ReadMe](https://readme.com)\n';
287
284
 
288
285
  console.log(body);
289
286
  },
@@ -313,11 +313,17 @@ tasks:
313
313
 
314
314
  export function templateFor(platform, opts = {}) {
315
315
  switch (platform) {
316
- case 'github': return githubWorkflow(opts);
317
- case 'gitlab': return gitlabWorkflow();
318
- case 'bitbucket': return bitbucketWorkflow();
319
- case 'circleci': return circleciWorkflow();
320
- case 'rwx': return rwxWorkflow();
321
- default: throw new Error(`Unknown platform: ${platform}`);
316
+ case 'github':
317
+ return githubWorkflow(opts);
318
+ case 'gitlab':
319
+ return gitlabWorkflow();
320
+ case 'bitbucket':
321
+ return bitbucketWorkflow();
322
+ case 'circleci':
323
+ return circleciWorkflow();
324
+ case 'rwx':
325
+ return rwxWorkflow();
326
+ default:
327
+ throw new Error(`Unknown platform: ${platform}`);
322
328
  }
323
329
  }
@@ -1,5 +1,6 @@
1
- import path from 'node:path';
2
1
  import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+
3
4
  import chalk from 'chalk';
4
5
 
5
6
  /** Returns the CLI name depending on how it was invoked (e.g. "readme", "readme_", or "npx @readme/cli"). */