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