@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
package/src/utils/reporter.js
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import fs from
|
|
3
|
-
import path from
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import ora from
|
|
7
|
-
|
|
8
|
-
import { hasClaude } from
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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:
|
|
18
|
-
docs:
|
|
19
|
-
reference:
|
|
20
|
-
custom_pages:
|
|
21
|
-
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(
|
|
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(
|
|
36
|
-
const warnings = results.filter(
|
|
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(
|
|
47
|
-
const raw = isFixed ? message.slice(0, -
|
|
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 ?
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return parts.join(
|
|
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:
|
|
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 ===
|
|
128
|
-
const isFixed = r.message.endsWith(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
186
|
-
const claudeMdPath = path.join(gitFormatDir,
|
|
187
|
-
const toolsMdPath = path.join(cliRoot,
|
|
188
|
-
const schemaPath = path.join(gitFormatDir,
|
|
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(
|
|
183
|
+
console.log('\n<claude-instructions>');
|
|
191
184
|
|
|
192
185
|
if (fs.existsSync(toolsMdPath)) {
|
|
193
|
-
console.log(fs.readFileSync(toolsMdPath,
|
|
186
|
+
console.log(fs.readFileSync(toolsMdPath, 'utf-8'));
|
|
194
187
|
}
|
|
195
188
|
|
|
196
189
|
if (fs.existsSync(claudeMdPath)) {
|
|
197
|
-
console.log(fs.readFileSync(claudeMdPath,
|
|
190
|
+
console.log(fs.readFileSync(claudeMdPath, 'utf-8'));
|
|
198
191
|
}
|
|
199
192
|
|
|
200
193
|
if (fs.existsSync(schemaPath)) {
|
|
201
|
-
console.log(
|
|
202
|
-
console.log(
|
|
203
|
-
console.log(fs.readFileSync(schemaPath,
|
|
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(
|
|
208
|
-
console.log(
|
|
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(
|
|
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 ||
|
|
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(
|
|
230
|
+
const name = filePath.split('/').pop();
|
|
238
231
|
if (canLink) {
|
|
239
|
-
const encoded = filePath.split(
|
|
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 =
|
|
246
|
-
body +=
|
|
238
|
+
let body = '<!-- readme-lint-results -->\n';
|
|
239
|
+
body += '## ReadMe Docs Lint\n\n';
|
|
247
240
|
|
|
248
241
|
if (all.length === 0) {
|
|
249
|
-
body +=
|
|
242
|
+
body += '> **All checks passed!** No lint issues found.\n';
|
|
250
243
|
} else {
|
|
251
|
-
body +=
|
|
252
|
-
body +=
|
|
244
|
+
body += '| File | Message |\n';
|
|
245
|
+
body += '|------|--------|\n';
|
|
253
246
|
for (const r of all) {
|
|
254
|
-
const isError = r.severity !==
|
|
255
|
-
const label = isError ?
|
|
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 +=
|
|
259
|
-
body +=
|
|
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:
|
|
262
|
+
{ cwd: gitRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
269
263
|
).trim();
|
|
270
264
|
|
|
271
265
|
if (changed) {
|
|
272
|
-
const files = changed.split(
|
|
273
|
-
body +=
|
|
274
|
-
body +=
|
|
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 +=
|
|
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 +=
|
|
286
|
-
body +=
|
|
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':
|
|
317
|
-
|
|
318
|
-
case '
|
|
319
|
-
|
|
320
|
-
case '
|
|
321
|
-
|
|
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
|
}
|
package/src/utils/styles.js
CHANGED