@readme/cli 0.1.0 → 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.
- package/bin/readme.js +1 -1
- package/package.json +33 -24
- 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 +2190 -2048
- package/src/commands/lint.js +33 -29
- package/src/commands/oas-bundle.js +6 -2
- package/src/commands/oas-sync.js +96 -93
- package/src/commands/oas-validate.js +10 -7
- package/src/commands/pretty.js +177 -164
- 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/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 -91
- 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 +5 -10
- package/src/validators/duplicates.js +6 -2
- package/src/validators/frontmatter.js +19 -21
- package/src/validators/links.js +4 -8
- package/src/validators/oas-reference.js +7 -14
- package/src/validators/oas-schema.js +2 -0
- package/src/validators/ordering.js +5 -5
- package/src/validators/recipes.js +4 -12
- 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,40 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import fs from
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { fileURLToPath } from
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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:
|
|
18
|
-
docs:
|
|
19
|
-
reference:
|
|
20
|
-
custom_pages:
|
|
21
|
-
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(
|
|
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(
|
|
36
|
-
const warnings = results.filter(
|
|
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(
|
|
47
|
-
const raw = isFixed ? message.slice(0, -
|
|
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 ?
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return parts.join(
|
|
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:
|
|
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 ===
|
|
128
|
-
const isFixed = r.message.endsWith(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
187
|
-
const toolsMdPath = path.join(cliRoot,
|
|
188
|
-
const schemaPath = path.join(gitFormatDir,
|
|
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(
|
|
186
|
+
console.log('\n<claude-instructions>');
|
|
191
187
|
|
|
192
188
|
if (fs.existsSync(toolsMdPath)) {
|
|
193
|
-
console.log(fs.readFileSync(toolsMdPath,
|
|
189
|
+
console.log(fs.readFileSync(toolsMdPath, 'utf-8'));
|
|
194
190
|
}
|
|
195
191
|
|
|
196
192
|
if (fs.existsSync(claudeMdPath)) {
|
|
197
|
-
console.log(fs.readFileSync(claudeMdPath,
|
|
193
|
+
console.log(fs.readFileSync(claudeMdPath, 'utf-8'));
|
|
198
194
|
}
|
|
199
195
|
|
|
200
196
|
if (fs.existsSync(schemaPath)) {
|
|
201
|
-
console.log(
|
|
202
|
-
console.log(
|
|
203
|
-
console.log(fs.readFileSync(schemaPath,
|
|
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(
|
|
208
|
-
console.log(
|
|
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(
|
|
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 ||
|
|
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(
|
|
233
|
+
const name = filePath.split('/').pop();
|
|
238
234
|
if (canLink) {
|
|
239
|
-
const encoded = filePath.split(
|
|
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 =
|
|
246
|
-
body +=
|
|
241
|
+
let body = '<!-- readme-lint-results -->\n';
|
|
242
|
+
body += '## ReadMe Docs Lint\n\n';
|
|
247
243
|
|
|
248
244
|
if (all.length === 0) {
|
|
249
|
-
body +=
|
|
245
|
+
body += '> **All checks passed!** No lint issues found.\n';
|
|
250
246
|
} else {
|
|
251
|
-
body +=
|
|
252
|
-
body +=
|
|
247
|
+
body += '| File | Message |\n';
|
|
248
|
+
body += '|------|--------|\n';
|
|
253
249
|
for (const r of all) {
|
|
254
|
-
const isError = r.severity !==
|
|
255
|
-
const label = isError ?
|
|
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 +=
|
|
259
|
-
body +=
|
|
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:
|
|
265
|
+
{ cwd: gitRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
269
266
|
).trim();
|
|
270
267
|
|
|
271
268
|
if (changed) {
|
|
272
|
-
const files = changed.split(
|
|
273
|
-
body +=
|
|
274
|
-
body +=
|
|
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 +=
|
|
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 +=
|
|
286
|
-
body +=
|
|
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':
|
|
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