@readme/cli 0.0.26
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/README.md +55 -0
- package/bin/readme.js +8 -0
- package/package.json +58 -0
- package/src/bootstrap.js +97 -0
- package/src/cli.js +189 -0
- package/src/commands/dev.js +119 -0
- package/src/commands/eyes.js +37 -0
- package/src/commands/import.js +2565 -0
- package/src/commands/lint.js +70 -0
- package/src/commands/oas-sync.js +364 -0
- package/src/commands/oas-validate.js +208 -0
- package/src/commands/play.js +17 -0
- package/src/commands/pretty.js +133 -0
- package/src/commands/setup.js +256 -0
- package/src/commands/versions.js +81 -0
- package/src/dev/.next/app-build-manifest.json +20 -0
- package/src/dev/.next/build-manifest.json +31 -0
- package/src/dev/.next/cache/.rscinfo +1 -0
- package/src/dev/.next/cache/next-devtools-config.json +1 -0
- package/src/dev/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/12.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/13.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/14.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/15.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/package.json +1 -0
- package/src/dev/.next/prerender-manifest.json +11 -0
- package/src/dev/.next/react-loadable-manifest.json +1 -0
- package/src/dev/.next/routes-manifest.json +1 -0
- package/src/dev/.next/server/app/[...slug]/page.js +360 -0
- package/src/dev/.next/server/app/[...slug]/page_client-reference-manifest.js +1 -0
- package/src/dev/.next/server/app/page.js +349 -0
- package/src/dev/.next/server/app/page_client-reference-manifest.js +1 -0
- package/src/dev/.next/server/app-paths-manifest.json +3 -0
- package/src/dev/.next/server/edge-runtime-webpack.js +1151 -0
- package/src/dev/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/src/dev/.next/server/middleware-build-manifest.js +33 -0
- package/src/dev/.next/server/middleware-manifest.json +32 -0
- package/src/dev/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/src/dev/.next/server/middleware.js +1113 -0
- package/src/dev/.next/server/next-font-manifest.js +1 -0
- package/src/dev/.next/server/next-font-manifest.json +1 -0
- package/src/dev/.next/server/pages-manifest.json +5 -0
- package/src/dev/.next/server/server-reference-manifest.js +1 -0
- package/src/dev/.next/server/server-reference-manifest.json +5 -0
- package/src/dev/.next/server/static/webpack/633457081244afec._.hot-update.json +1 -0
- package/src/dev/.next/server/vendor-chunks/@readme.js +25 -0
- package/src/dev/.next/server/vendor-chunks/@swc.js +55 -0
- package/src/dev/.next/server/vendor-chunks/next.js +3659 -0
- package/src/dev/.next/server/webpack-runtime.js +209 -0
- package/src/dev/.next/static/chunks/app/[...slug]/loading.js +28 -0
- package/src/dev/.next/static/chunks/app/[...slug]/page.js +28 -0
- package/src/dev/.next/static/chunks/app/layout.js +171 -0
- package/src/dev/.next/static/chunks/app/page.js +28 -0
- package/src/dev/.next/static/chunks/app-pages-internals.js +182 -0
- package/src/dev/.next/static/chunks/main-app.js +1882 -0
- package/src/dev/.next/static/chunks/polyfills.js +1 -0
- package/src/dev/.next/static/chunks/webpack.js +1393 -0
- package/src/dev/.next/static/css/app/layout.css +559 -0
- package/src/dev/.next/static/development/_buildManifest.js +1 -0
- package/src/dev/.next/static/development/_ssgManifest.js +1 -0
- package/src/dev/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
- package/src/dev/.next/static/webpack/ec52a3fce0f78db0.webpack.hot-update.json +1 -0
- package/src/dev/.next/static/webpack/webpack.ec52a3fce0f78db0.hot-update.js +12 -0
- package/src/dev/.next/trace +21 -0
- package/src/dev/.next/types/app/[...slug]/page.ts +84 -0
- package/src/dev/.next/types/app/layout.ts +84 -0
- package/src/dev/.next/types/app/page.ts +84 -0
- package/src/dev/.next/types/cache-life.d.ts +141 -0
- package/src/dev/.next/types/package.json +1 -0
- package/src/dev/.next/types/routes.d.ts +55 -0
- package/src/dev/app/Sidebar.js +149 -0
- package/src/dev/app/[...slug]/loading.js +16 -0
- package/src/dev/app/[...slug]/page.js +43 -0
- package/src/dev/app/globals.css +167 -0
- package/src/dev/app/layout.js +73 -0
- package/src/dev/app/page.js +19 -0
- package/src/dev/lib/docs.js +337 -0
- package/src/dev/middleware.js +7 -0
- package/src/dev/next.config.mjs +22 -0
- package/src/index.js +12 -0
- package/src/prompts/index.js +352 -0
- package/src/utils/claude.js +15 -0
- package/src/utils/eyes.js +365 -0
- package/src/utils/git.js +143 -0
- package/src/utils/lint.js +99 -0
- package/src/utils/reporter.js +319 -0
- package/src/utils/setup-templates.js +323 -0
- package/src/utils/styles.js +50 -0
- package/src/utils/tamagotchi.js +1139 -0
- package/src/utils/tips.js +90 -0
- package/src/validators/components.js +230 -0
- package/src/validators/content.js +53 -0
- package/src/validators/duplicates.js +45 -0
- package/src/validators/frontmatter.js +247 -0
- package/src/validators/links.js +68 -0
- package/src/validators/nesting.js +50 -0
- package/src/validators/numbering.js +136 -0
- package/src/validators/oas-reference.js +126 -0
- package/src/validators/oas-schema.js +106 -0
- package/src/validators/ordering.js +121 -0
- package/src/validators/recipes.js +143 -0
- package/vendor/TOOLS.md +19 -0
|
@@ -0,0 +1,319 @@
|
|
|
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 ora from "ora";
|
|
6
|
+
import * as styles from "./styles.js";
|
|
7
|
+
import { hasClaude } from "./claude.js";
|
|
8
|
+
import { hasGithubRemote, hasGithubWorkflow, getWorkflowVersion, WORKFLOW_VERSION, detectPlatform, hasReadmeWorkflow, PLATFORMS } from "./git.js";
|
|
9
|
+
import { getRandomTip } from "./tips.js";
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
const isRunningInClaude = !!process.env.CLAUDECODE;
|
|
14
|
+
|
|
15
|
+
const CATEGORY_LABELS = {
|
|
16
|
+
custom_blocks: "custom blocks",
|
|
17
|
+
docs: "docs",
|
|
18
|
+
reference: "reference",
|
|
19
|
+
custom_pages: "custom pages",
|
|
20
|
+
recipes: "recipes",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function categorize(files) {
|
|
24
|
+
const counts = new Map();
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const dir = file.split("/")[0];
|
|
27
|
+
const label = CATEGORY_LABELS[dir] || dir;
|
|
28
|
+
counts.set(label, (counts.get(label) || 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
return [...counts.entries()];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function splitResults(results) {
|
|
34
|
+
const errors = results.filter((r) => r.severity !== "warning");
|
|
35
|
+
const warnings = results.filter((r) => r.severity === "warning");
|
|
36
|
+
return { errors, warnings };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Style a result message for human display.
|
|
41
|
+
* Title (before first ":") is colored, rest is normal with bold quoted values.
|
|
42
|
+
*/
|
|
43
|
+
function styleMessage(message, color) {
|
|
44
|
+
// Strip "(fixed)" suffix — handled separately via the icon.
|
|
45
|
+
const isFixed = message.endsWith("(fixed)");
|
|
46
|
+
const raw = isFixed ? message.slice(0, -"(fixed)".length).trimEnd() : message;
|
|
47
|
+
|
|
48
|
+
const colonIdx = raw.indexOf(":");
|
|
49
|
+
let styled;
|
|
50
|
+
|
|
51
|
+
if (colonIdx !== -1) {
|
|
52
|
+
const title = raw.slice(0, colonIdx);
|
|
53
|
+
const detail = raw
|
|
54
|
+
.slice(colonIdx + 1)
|
|
55
|
+
.replace(/"([^"]+)"/g, (_, val) => styles.bold(`"${val}"`));
|
|
56
|
+
styled = `${color(title)}:${detail}`;
|
|
57
|
+
} else {
|
|
58
|
+
// No colon — highlight quoted values, keep rest normal.
|
|
59
|
+
styled = raw.replace(/"([^"]+)"/g, (_, val) => styles.bold(`"${val}"`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return styled;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function summaryLine(errorCount, warningCount) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (errorCount > 0)
|
|
68
|
+
parts.push(`${errorCount} ${errorCount === 1 ? "error" : "errors"}`);
|
|
69
|
+
if (warningCount > 0)
|
|
70
|
+
parts.push(
|
|
71
|
+
`${warningCount} ${warningCount === 1 ? "warning" : "warnings"}`,
|
|
72
|
+
);
|
|
73
|
+
return parts.join(" and ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Human-readable reporter with an animated spinner.
|
|
78
|
+
*/
|
|
79
|
+
export function createHumanReporter() {
|
|
80
|
+
const spinner = ora({ text: "Running linting...", color: "blue" }).start();
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
onFile(relativePath) {
|
|
84
|
+
spinner.suffixText = styles.dim(relativePath);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
pause() {
|
|
88
|
+
spinner.stop();
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
finish(total, results, files, { fix, gitRoot } = {}) {
|
|
92
|
+
spinner.suffixText = "";
|
|
93
|
+
|
|
94
|
+
const { errors, warnings } = splitResults(results);
|
|
95
|
+
const categories = categorize(files);
|
|
96
|
+
const breakdown = categories
|
|
97
|
+
.map(([label, count]) => `${count} ${label}`)
|
|
98
|
+
.join(styles.dim(" · "));
|
|
99
|
+
|
|
100
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
101
|
+
spinner.succeed(`${total} files checked — all good!`);
|
|
102
|
+
console.log(` ${styles.dim(breakdown)}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (errors.length > 0) {
|
|
107
|
+
spinner.fail(
|
|
108
|
+
`${summaryLine(errors.length, warnings.length)} in ${total} files`,
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
spinner.warn(`${summaryLine(0, warnings.length)} in ${total} files`);
|
|
112
|
+
}
|
|
113
|
+
console.log(` ${styles.dim(breakdown)}`);
|
|
114
|
+
console.log();
|
|
115
|
+
|
|
116
|
+
// Group all results by file
|
|
117
|
+
const grouped = new Map();
|
|
118
|
+
for (const r of [...errors, ...warnings]) {
|
|
119
|
+
if (!grouped.has(r.file)) grouped.set(r.file, []);
|
|
120
|
+
grouped.get(r.file).push(r);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const [file, fileResults] of grouped) {
|
|
124
|
+
console.log(` ${styles.bold(file)}`);
|
|
125
|
+
for (const r of fileResults) {
|
|
126
|
+
const baseColor = r.severity === "warning" ? styles.warn : styles.err;
|
|
127
|
+
const isFixed = r.message.endsWith("(fixed)");
|
|
128
|
+
const color = isFixed ? styles.success : baseColor;
|
|
129
|
+
const styled = styleMessage(r.message, color);
|
|
130
|
+
const icon = isFixed ? styles.success("✔") : baseColor("●");
|
|
131
|
+
console.log(` ${icon} ${styled}`);
|
|
132
|
+
}
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Tips section.
|
|
137
|
+
const fixable = !fix
|
|
138
|
+
? results.filter((r) => r.fixable && !r.message.endsWith("(fixed)"))
|
|
139
|
+
: [];
|
|
140
|
+
const unfixed = results.filter((r) => !r.message.endsWith("(fixed)"));
|
|
141
|
+
const showTips =
|
|
142
|
+
fixable.length > 0 || (unfixed.length > 0 && !isRunningInClaude);
|
|
143
|
+
|
|
144
|
+
if (showTips) {
|
|
145
|
+
console.log(` ${styles.dim("─".repeat(40))}`);
|
|
146
|
+
console.log();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (fixable.length > 0) {
|
|
150
|
+
console.log(
|
|
151
|
+
` ${styles.dim("Run")} ${styles.binName()} lint --fix ${styles.dim("to automatically fix some of these.")}`,
|
|
152
|
+
);
|
|
153
|
+
console.log();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (unfixed.length > 0 && !isRunningInClaude) {
|
|
157
|
+
const hasWorkflow = gitRoot ? hasGithubWorkflow(gitRoot) : true;
|
|
158
|
+
const workflowVersion = gitRoot ? getWorkflowVersion(gitRoot) : null;
|
|
159
|
+
const detection = gitRoot ? detectPlatform(gitRoot) : { recommended: null };
|
|
160
|
+
const detectedPlatform = detection.recommended;
|
|
161
|
+
const hasCiWorkflow = detectedPlatform && gitRoot
|
|
162
|
+
? hasReadmeWorkflow(gitRoot, detectedPlatform)
|
|
163
|
+
: true;
|
|
164
|
+
const tip = getRandomTip({
|
|
165
|
+
isRunningInClaude,
|
|
166
|
+
hasClaude: hasClaude(),
|
|
167
|
+
hasGithubRemote: hasGithubRemote(),
|
|
168
|
+
hasGithubWorkflow: hasWorkflow,
|
|
169
|
+
workflowOutdated: hasWorkflow && workflowVersion !== null && workflowVersion < WORKFLOW_VERSION,
|
|
170
|
+
detectedPlatform,
|
|
171
|
+
detectedPlatformLabel: detectedPlatform ? PLATFORMS[detectedPlatform].label : null,
|
|
172
|
+
hasCiWorkflow,
|
|
173
|
+
});
|
|
174
|
+
if (tip) tip.render();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// When running inside Claude, output instructions and structured issue list.
|
|
178
|
+
if (isRunningInClaude && unfixed.length > 0) {
|
|
179
|
+
const cliRoot = path.join(
|
|
180
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
181
|
+
"../..",
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const gitFormatDir = path.dirname(require.resolve('@readmeio/git-format/package.json'));
|
|
185
|
+
const claudeMdPath = path.join(gitFormatDir, "CLAUDE.md");
|
|
186
|
+
const toolsMdPath = path.join(cliRoot, "vendor/TOOLS.md");
|
|
187
|
+
const schemaPath = path.join(gitFormatDir, "frontmatter.schema.json");
|
|
188
|
+
|
|
189
|
+
console.log("\n<claude-instructions>");
|
|
190
|
+
|
|
191
|
+
if (fs.existsSync(toolsMdPath)) {
|
|
192
|
+
console.log(fs.readFileSync(toolsMdPath, "utf-8"));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
196
|
+
console.log(fs.readFileSync(claudeMdPath, "utf-8"));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (fs.existsSync(schemaPath)) {
|
|
200
|
+
console.log("\n## Frontmatter JSON Schema\n");
|
|
201
|
+
console.log("```json");
|
|
202
|
+
console.log(fs.readFileSync(schemaPath, "utf-8"));
|
|
203
|
+
console.log("```");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log("</claude-instructions>\n");
|
|
207
|
+
console.log("<issues>");
|
|
208
|
+
for (const r of unfixed) {
|
|
209
|
+
console.log(`- [${r.severity}] ${r.file}: ${r.message}`);
|
|
210
|
+
}
|
|
211
|
+
console.log("</issues>");
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* GitHub reporter — outputs a PR comment body as markdown.
|
|
219
|
+
*/
|
|
220
|
+
export function createGithubReporter() {
|
|
221
|
+
return {
|
|
222
|
+
onFile() {},
|
|
223
|
+
pause() {},
|
|
224
|
+
|
|
225
|
+
finish(total, results, _files, { gitRoot } = {}) {
|
|
226
|
+
const { errors, warnings } = splitResults(results);
|
|
227
|
+
const all = [...errors, ...warnings];
|
|
228
|
+
|
|
229
|
+
// Build file links using GitHub Actions env vars
|
|
230
|
+
const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
|
|
231
|
+
const repo = process.env.GITHUB_REPOSITORY || "";
|
|
232
|
+
const sha = process.env.GITHUB_SHA || "";
|
|
233
|
+
const canLink = repo && sha;
|
|
234
|
+
|
|
235
|
+
function fileLink(filePath) {
|
|
236
|
+
const name = filePath.split("/").pop();
|
|
237
|
+
if (canLink) {
|
|
238
|
+
const encoded = filePath.split("/").map(encodeURIComponent).join("/");
|
|
239
|
+
return `[\`${name}\`](${serverUrl}/${repo}/blob/${sha}/${encoded})`;
|
|
240
|
+
}
|
|
241
|
+
return `\`${name}\``;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let body = "<!-- readme-lint-results -->\n";
|
|
245
|
+
body += "## ReadMe Docs Lint\n\n";
|
|
246
|
+
|
|
247
|
+
if (all.length === 0) {
|
|
248
|
+
body += "> **All checks passed!** No lint issues found.\n";
|
|
249
|
+
} else {
|
|
250
|
+
body += "| File | Message |\n";
|
|
251
|
+
body += "|------|--------|\n";
|
|
252
|
+
for (const r of all) {
|
|
253
|
+
const isError = r.severity !== "warning";
|
|
254
|
+
const label = isError ? "\u{1F534} **Error:**" : "\u{1F7E1} **Warning:**";
|
|
255
|
+
body += `| ${fileLink(r.file)} | ${label} ${r.message} |\n`;
|
|
256
|
+
}
|
|
257
|
+
body += "\n";
|
|
258
|
+
body += "> \u{1F4A1} **Tip:** Run `npx @readme/cli lint --fix` locally to automatically fix some of these issues.\n\n";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// OAS change detection
|
|
262
|
+
const baseSha = process.env.GITHUB_BASE_SHA;
|
|
263
|
+
if (baseSha && gitRoot) {
|
|
264
|
+
try {
|
|
265
|
+
const changed = execSync(
|
|
266
|
+
`git diff --name-only ${baseSha}..HEAD -- 'reference/*.json' 'reference/*.yaml' 'reference/*.yml'`,
|
|
267
|
+
{ cwd: gitRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
|
|
268
|
+
).trim();
|
|
269
|
+
|
|
270
|
+
if (changed) {
|
|
271
|
+
const files = changed.split("\n");
|
|
272
|
+
body += "### OAS Changes Detected\n\n";
|
|
273
|
+
body += "The following OpenAPI spec files were changed in this PR:\n\n";
|
|
274
|
+
for (const f of files) {
|
|
275
|
+
body += `- ${fileLink(f)}\n`;
|
|
276
|
+
}
|
|
277
|
+
body += "\nRun `npx @readme/cli oas:sync` to sync these changes to ReadMe.\n\n";
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// git diff failed — skip OAS section silently
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
body += "---\n";
|
|
285
|
+
body += "\u{1F989} Powered by [ReadMe](https://readme.com)\n";
|
|
286
|
+
|
|
287
|
+
console.log(body);
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* JSON reporter for machine consumption.
|
|
294
|
+
*/
|
|
295
|
+
export function createJsonReporter() {
|
|
296
|
+
return {
|
|
297
|
+
onFile() {},
|
|
298
|
+
pause() {},
|
|
299
|
+
|
|
300
|
+
finish(total, results, _files) {
|
|
301
|
+
const { errors, warnings } = splitResults(results);
|
|
302
|
+
const output = {
|
|
303
|
+
ok: errors.length === 0,
|
|
304
|
+
total,
|
|
305
|
+
errors: errors.map(({ file, rule, message }) => ({
|
|
306
|
+
file,
|
|
307
|
+
rule,
|
|
308
|
+
message,
|
|
309
|
+
})),
|
|
310
|
+
warnings: warnings.map(({ file, rule, message }) => ({
|
|
311
|
+
file,
|
|
312
|
+
rule,
|
|
313
|
+
message,
|
|
314
|
+
})),
|
|
315
|
+
};
|
|
316
|
+
console.log(JSON.stringify(output));
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { WORKFLOW_VERSION } from './git.js';
|
|
2
|
+
|
|
3
|
+
const VERSION_HEADER = `# readme-lint v${WORKFLOW_VERSION}`;
|
|
4
|
+
|
|
5
|
+
const GITHUB_DEFAULT_RUNNER = 'ubuntu-latest';
|
|
6
|
+
const GITHUB_BLACKSMITH_RUNNER = 'blacksmith-2vcpu-ubuntu-2404';
|
|
7
|
+
|
|
8
|
+
export function githubWorkflow({ blacksmith = false } = {}) {
|
|
9
|
+
const runner = blacksmith ? GITHUB_BLACKSMITH_RUNNER : GITHUB_DEFAULT_RUNNER;
|
|
10
|
+
return `${VERSION_HEADER}
|
|
11
|
+
name: ReadMe Docs Lint
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
pull_request:
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
pull-requests: write
|
|
19
|
+
packages: read
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
lint:
|
|
23
|
+
name: Lint docs
|
|
24
|
+
runs-on: ${runner}
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- name: Checkout
|
|
28
|
+
uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
|
|
32
|
+
- name: Fix PR base branch
|
|
33
|
+
id: fix-base
|
|
34
|
+
if: github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'master'
|
|
35
|
+
uses: actions/github-script@v7
|
|
36
|
+
with:
|
|
37
|
+
script: |
|
|
38
|
+
const head = context.payload.pull_request.head.ref;
|
|
39
|
+
const match = head.match(/^(v\\d+(?:\\.\\d+)*)/);
|
|
40
|
+
if (!match) return;
|
|
41
|
+
|
|
42
|
+
const versionBranch = match[1];
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await github.rest.repos.getBranch({
|
|
46
|
+
owner: context.repo.owner,
|
|
47
|
+
repo: context.repo.repo,
|
|
48
|
+
branch: versionBranch,
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await github.rest.pulls.update({
|
|
55
|
+
owner: context.repo.owner,
|
|
56
|
+
repo: context.repo.repo,
|
|
57
|
+
pull_number: context.issue.number,
|
|
58
|
+
base: versionBranch,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
core.setOutput('changed', 'true');
|
|
62
|
+
core.setOutput('new_base', versionBranch);
|
|
63
|
+
core.setOutput('old_base', context.payload.pull_request.base.ref);
|
|
64
|
+
|
|
65
|
+
- name: Set up Node.js
|
|
66
|
+
uses: actions/setup-node@v4
|
|
67
|
+
with:
|
|
68
|
+
node-version: 20
|
|
69
|
+
registry-url: https://npm.pkg.github.com
|
|
70
|
+
|
|
71
|
+
- name: Lint docs
|
|
72
|
+
id: lint
|
|
73
|
+
continue-on-error: true
|
|
74
|
+
env:
|
|
75
|
+
NODE_AUTH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
76
|
+
GITHUB_BASE_SHA: \${{ github.event.pull_request.base.sha }}
|
|
77
|
+
run: npx -y @readme/cli --no-check lint --github > comment.md
|
|
78
|
+
|
|
79
|
+
- name: Comment on PR
|
|
80
|
+
uses: actions/github-script@v7
|
|
81
|
+
with:
|
|
82
|
+
script: |
|
|
83
|
+
const fs = require('fs');
|
|
84
|
+
const marker = '<!-- readme-lint-results -->';
|
|
85
|
+
let body = '';
|
|
86
|
+
try { body = fs.readFileSync('comment.md', 'utf-8'); } catch {}
|
|
87
|
+
if (!body.includes(marker)) return;
|
|
88
|
+
|
|
89
|
+
const baseChanged = '\${{ steps.fix-base.outputs.changed }}' === 'true';
|
|
90
|
+
if (baseChanged) {
|
|
91
|
+
const oldBase = '\${{ steps.fix-base.outputs.old_base }}';
|
|
92
|
+
const newBase = '\${{ steps.fix-base.outputs.new_base }}';
|
|
93
|
+
body = body.replace('---', \`> **Base branch updated:** This PR was targeting \\\`\${oldBase}\\\` but has been updated to target \\\`\${newBase}\\\`.\\n\\n---\`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
97
|
+
owner: context.repo.owner,
|
|
98
|
+
repo: context.repo.repo,
|
|
99
|
+
issue_number: context.issue.number,
|
|
100
|
+
});
|
|
101
|
+
const existing = comments.find(c => c.body.includes(marker));
|
|
102
|
+
|
|
103
|
+
if (existing) {
|
|
104
|
+
await github.rest.issues.updateComment({
|
|
105
|
+
owner: context.repo.owner,
|
|
106
|
+
repo: context.repo.repo,
|
|
107
|
+
comment_id: existing.id,
|
|
108
|
+
body,
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
await github.rest.issues.createComment({
|
|
112
|
+
owner: context.repo.owner,
|
|
113
|
+
repo: context.repo.repo,
|
|
114
|
+
issue_number: context.issue.number,
|
|
115
|
+
body,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
- name: Fail if lint errors
|
|
121
|
+
if: steps.lint.outcome == 'failure'
|
|
122
|
+
run: exit 1
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function gitlabWorkflow() {
|
|
127
|
+
return `${VERSION_HEADER}
|
|
128
|
+
# Lints ReadMe docs on every merge request and posts results as an MR note.
|
|
129
|
+
# Requires a CI/CD variable named GITLAB_TOKEN with api scope (Settings → CI/CD → Variables)
|
|
130
|
+
# so the job can post comments back to the merge request.
|
|
131
|
+
|
|
132
|
+
readme-lint:
|
|
133
|
+
stage: test
|
|
134
|
+
image: node:20
|
|
135
|
+
rules:
|
|
136
|
+
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
|
137
|
+
variables:
|
|
138
|
+
GIT_DEPTH: 0
|
|
139
|
+
script:
|
|
140
|
+
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
141
|
+
- export GITHUB_BASE_SHA="$(git rev-parse origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME)"
|
|
142
|
+
- npx -y @readme/cli --no-check lint --github > comment.md || echo "LINT_FAILED=1" >> lint.env
|
|
143
|
+
- |
|
|
144
|
+
if [ -s comment.md ] && grep -q '<!-- readme-lint-results -->' comment.md; then
|
|
145
|
+
BODY=$(cat comment.md)
|
|
146
|
+
AUTH_HEADER="PRIVATE-TOKEN: $GITLAB_TOKEN"
|
|
147
|
+
API="$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"
|
|
148
|
+
EXISTING=$(curl -s -H "$AUTH_HEADER" "$API" | grep -o '"id":[0-9]*,"type":[^,]*,"body":"<!-- readme-lint-results -->' | head -1 | grep -o '"id":[0-9]*' | cut -d: -f2)
|
|
149
|
+
if [ -n "$EXISTING" ]; then
|
|
150
|
+
curl -s -X PUT -H "$AUTH_HEADER" --data-urlencode "body=$BODY" "$API/$EXISTING" > /dev/null
|
|
151
|
+
else
|
|
152
|
+
curl -s -X POST -H "$AUTH_HEADER" --data-urlencode "body=$BODY" "$API" > /dev/null
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
- if [ -f lint.env ]; then exit 1; fi
|
|
156
|
+
artifacts:
|
|
157
|
+
when: always
|
|
158
|
+
paths:
|
|
159
|
+
- comment.md
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function bitbucketWorkflow() {
|
|
164
|
+
return `${VERSION_HEADER}
|
|
165
|
+
# Lints ReadMe docs on every pull request and posts results as a PR comment.
|
|
166
|
+
# Requires a repository variable named BITBUCKET_TOKEN (an app password with
|
|
167
|
+
# pullrequest:write scope) so the job can post comments back to the PR.
|
|
168
|
+
|
|
169
|
+
image: node:20
|
|
170
|
+
|
|
171
|
+
pipelines:
|
|
172
|
+
pull-requests:
|
|
173
|
+
'**':
|
|
174
|
+
- step:
|
|
175
|
+
name: Lint ReadMe docs
|
|
176
|
+
clone:
|
|
177
|
+
depth: full
|
|
178
|
+
script:
|
|
179
|
+
- export GITHUB_BASE_SHA="$(git rev-parse origin/$BITBUCKET_PR_DESTINATION_BRANCH)"
|
|
180
|
+
- LINT_RC=0
|
|
181
|
+
- npx -y @readme/cli --no-check lint --github > comment.md || LINT_RC=$?
|
|
182
|
+
- |
|
|
183
|
+
if [ -s comment.md ] && grep -q '<!-- readme-lint-results -->' comment.md; then
|
|
184
|
+
BODY=$(cat comment.md)
|
|
185
|
+
API="https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pullrequests/$BITBUCKET_PR_ID/comments"
|
|
186
|
+
AUTH="-u $BITBUCKET_USERNAME:$BITBUCKET_TOKEN"
|
|
187
|
+
EXISTING=$(curl -s $AUTH "$API?pagelen=100" | grep -o '"id":[0-9]*[^}]*<!-- readme-lint-results -->' | head -1 | grep -o '"id":[0-9]*' | cut -d: -f2)
|
|
188
|
+
JSON=$(node -e "console.log(JSON.stringify({content:{raw:require('fs').readFileSync('comment.md','utf-8')}}))")
|
|
189
|
+
if [ -n "$EXISTING" ]; then
|
|
190
|
+
curl -s -X PUT $AUTH -H 'Content-Type: application/json' -d "$JSON" "$API/$EXISTING" > /dev/null
|
|
191
|
+
else
|
|
192
|
+
curl -s -X POST $AUTH -H 'Content-Type: application/json' -d "$JSON" "$API" > /dev/null
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
- exit $LINT_RC
|
|
196
|
+
artifacts:
|
|
197
|
+
- comment.md
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function circleciWorkflow() {
|
|
202
|
+
return `${VERSION_HEADER}
|
|
203
|
+
# Lints ReadMe docs on every PR and posts results as a PR comment.
|
|
204
|
+
# Requires CIRCLE_PROJECT_GITHUB_TOKEN (or your VCS-equivalent) as an env var
|
|
205
|
+
# in the project settings, with permission to comment on pull requests.
|
|
206
|
+
|
|
207
|
+
version: 2.1
|
|
208
|
+
|
|
209
|
+
jobs:
|
|
210
|
+
readme-lint:
|
|
211
|
+
docker:
|
|
212
|
+
- image: cimg/node:20.11
|
|
213
|
+
steps:
|
|
214
|
+
- checkout
|
|
215
|
+
- run:
|
|
216
|
+
name: Lint ReadMe docs
|
|
217
|
+
command: |
|
|
218
|
+
BASE_BRANCH="\${CIRCLE_PR_BASE_BRANCH:-main}"
|
|
219
|
+
git fetch origin "$BASE_BRANCH" || true
|
|
220
|
+
export GITHUB_BASE_SHA="$(git rev-parse origin/$BASE_BRANCH 2>/dev/null || echo '')"
|
|
221
|
+
set +e
|
|
222
|
+
npx -y @readme/cli --no-check lint --github > comment.md
|
|
223
|
+
LINT_RC=$?
|
|
224
|
+
set -e
|
|
225
|
+
if [ -n "$CIRCLE_PULL_REQUEST" ] && grep -q '<!-- readme-lint-results -->' comment.md; then
|
|
226
|
+
PR_NUM=$(echo "$CIRCLE_PULL_REQUEST" | awk -F/ '{print $NF}')
|
|
227
|
+
REPO="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME"
|
|
228
|
+
API="https://api.github.com/repos/$REPO/issues/$PR_NUM/comments"
|
|
229
|
+
AUTH="Authorization: token $CIRCLE_PROJECT_GITHUB_TOKEN"
|
|
230
|
+
EXISTING=$(curl -s -H "$AUTH" "$API?per_page=100" | grep -B1 '<!-- readme-lint-results -->' | grep -o '"id": [0-9]*' | head -1 | awk '{print $2}')
|
|
231
|
+
JSON=$(node -e "console.log(JSON.stringify({body: require('fs').readFileSync('comment.md','utf-8')}))")
|
|
232
|
+
if [ -n "$EXISTING" ]; then
|
|
233
|
+
curl -s -X PATCH -H "$AUTH" -H 'Content-Type: application/json' -d "$JSON" "https://api.github.com/repos/$REPO/issues/comments/$EXISTING" > /dev/null
|
|
234
|
+
else
|
|
235
|
+
curl -s -X POST -H "$AUTH" -H 'Content-Type: application/json' -d "$JSON" "$API" > /dev/null
|
|
236
|
+
fi
|
|
237
|
+
fi
|
|
238
|
+
exit $LINT_RC
|
|
239
|
+
environment:
|
|
240
|
+
NODE_OPTIONS: --max-old-space-size=4096
|
|
241
|
+
|
|
242
|
+
workflows:
|
|
243
|
+
readme:
|
|
244
|
+
jobs:
|
|
245
|
+
- readme-lint:
|
|
246
|
+
filters:
|
|
247
|
+
branches:
|
|
248
|
+
ignore:
|
|
249
|
+
- main
|
|
250
|
+
- master
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function rwxWorkflow() {
|
|
255
|
+
return `${VERSION_HEADER}
|
|
256
|
+
# Lints ReadMe docs on every PR and posts results as a PR comment.
|
|
257
|
+
# Requires a vault secret named GITHUB_TOKEN with permission to comment on PRs.
|
|
258
|
+
|
|
259
|
+
on:
|
|
260
|
+
github:
|
|
261
|
+
pull_request:
|
|
262
|
+
init:
|
|
263
|
+
commit-sha: \${{ event.pull_request.head.sha }}
|
|
264
|
+
pr-number: \${{ event.pull_request.number }}
|
|
265
|
+
repo: \${{ event.repository.full_name }}
|
|
266
|
+
base-sha: \${{ event.pull_request.base.sha }}
|
|
267
|
+
|
|
268
|
+
tasks:
|
|
269
|
+
- key: code
|
|
270
|
+
call: mint/git-clone 1.6.6
|
|
271
|
+
with:
|
|
272
|
+
repository: https://github.com/\${{ init.repo }}.git
|
|
273
|
+
ref: \${{ init.commit-sha }}
|
|
274
|
+
|
|
275
|
+
- key: node
|
|
276
|
+
call: mint/install-node 1.1.5
|
|
277
|
+
with:
|
|
278
|
+
node-version: '20'
|
|
279
|
+
|
|
280
|
+
- key: lint
|
|
281
|
+
use: [code, node]
|
|
282
|
+
run: |
|
|
283
|
+
set +e
|
|
284
|
+
GITHUB_BASE_SHA="\${{ init.base-sha }}" npx -y @readme/cli --no-check lint --github > comment.md
|
|
285
|
+
echo $? > lint.rc
|
|
286
|
+
filter:
|
|
287
|
+
- comment.md
|
|
288
|
+
- lint.rc
|
|
289
|
+
|
|
290
|
+
- key: comment
|
|
291
|
+
use: lint
|
|
292
|
+
env:
|
|
293
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
294
|
+
REPO: \${{ init.repo }}
|
|
295
|
+
PR: \${{ init.pr-number }}
|
|
296
|
+
run: |
|
|
297
|
+
if ! grep -q '<!-- readme-lint-results -->' comment.md; then exit 0; fi
|
|
298
|
+
API="https://api.github.com/repos/$REPO/issues/$PR/comments"
|
|
299
|
+
AUTH="Authorization: token $GITHUB_TOKEN"
|
|
300
|
+
EXISTING=$(curl -s -H "$AUTH" "$API?per_page=100" | grep -B1 '<!-- readme-lint-results -->' | grep -o '"id": [0-9]*' | head -1 | awk '{print $2}')
|
|
301
|
+
JSON=$(node -e "console.log(JSON.stringify({body: require('fs').readFileSync('comment.md','utf-8')}))")
|
|
302
|
+
if [ -n "$EXISTING" ]; then
|
|
303
|
+
curl -s -X PATCH -H "$AUTH" -H 'Content-Type: application/json' -d "$JSON" "https://api.github.com/repos/$REPO/issues/comments/$EXISTING" > /dev/null
|
|
304
|
+
else
|
|
305
|
+
curl -s -X POST -H "$AUTH" -H 'Content-Type: application/json' -d "$JSON" "$API" > /dev/null
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
- key: report
|
|
309
|
+
use: lint
|
|
310
|
+
run: exit "$(cat lint.rc)"
|
|
311
|
+
`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function templateFor(platform, opts = {}) {
|
|
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}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
/** Returns the CLI name depending on how it was invoked (e.g. "readme", "readme_", or "npx @readme/cli"). */
|
|
6
|
+
export function binName() {
|
|
7
|
+
const base = path.basename(process.argv[1] || 'readme');
|
|
8
|
+
|
|
9
|
+
// When run via npx, npm sets npm_command=exec. Use our own package.json name
|
|
10
|
+
// (not npm_package_name, which refers to the CWD project, not the CLI).
|
|
11
|
+
if (process.env.npm_command === 'exec') {
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const pkg = require('../../package.json');
|
|
14
|
+
return `npx ${pkg.name}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return base;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const brand = chalk.hex('#018ef5'); // ReadMe blue
|
|
21
|
+
export const success = chalk.green;
|
|
22
|
+
export const warn = chalk.yellow;
|
|
23
|
+
export const err = chalk.red;
|
|
24
|
+
export const orange = chalk.hex('#63D2FF');
|
|
25
|
+
export const dim = chalk.dim;
|
|
26
|
+
export const bold = chalk.bold;
|
|
27
|
+
|
|
28
|
+
export function logo() {
|
|
29
|
+
return brand('🦉 readme');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function heading(text) {
|
|
33
|
+
return bold(brand(text));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function info(message) {
|
|
37
|
+
console.log(`${dim('›')} ${message}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function error(message) {
|
|
41
|
+
console.error(`${err('✘')} ${message}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ok(message) {
|
|
45
|
+
console.log(`${success('✔')} ${message}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function warning(message) {
|
|
49
|
+
console.log(`${warn('⚠')} ${message}`);
|
|
50
|
+
}
|