@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.
Files changed (144) hide show
  1. package/README.md +55 -0
  2. package/bin/readme.js +8 -0
  3. package/package.json +58 -0
  4. package/src/bootstrap.js +97 -0
  5. package/src/cli.js +189 -0
  6. package/src/commands/dev.js +119 -0
  7. package/src/commands/eyes.js +37 -0
  8. package/src/commands/import.js +2565 -0
  9. package/src/commands/lint.js +70 -0
  10. package/src/commands/oas-sync.js +364 -0
  11. package/src/commands/oas-validate.js +208 -0
  12. package/src/commands/play.js +17 -0
  13. package/src/commands/pretty.js +133 -0
  14. package/src/commands/setup.js +256 -0
  15. package/src/commands/versions.js +81 -0
  16. package/src/dev/.next/app-build-manifest.json +20 -0
  17. package/src/dev/.next/build-manifest.json +31 -0
  18. package/src/dev/.next/cache/.rscinfo +1 -0
  19. package/src/dev/.next/cache/next-devtools-config.json +1 -0
  20. package/src/dev/.next/cache/webpack/client-development/0.pack.gz +0 -0
  21. package/src/dev/.next/cache/webpack/client-development/1.pack.gz +0 -0
  22. package/src/dev/.next/cache/webpack/client-development/10.pack.gz +0 -0
  23. package/src/dev/.next/cache/webpack/client-development/11.pack.gz +0 -0
  24. package/src/dev/.next/cache/webpack/client-development/2.pack.gz +0 -0
  25. package/src/dev/.next/cache/webpack/client-development/3.pack.gz +0 -0
  26. package/src/dev/.next/cache/webpack/client-development/3.pack.gz_ +0 -0
  27. package/src/dev/.next/cache/webpack/client-development/4.pack.gz +0 -0
  28. package/src/dev/.next/cache/webpack/client-development/5.pack.gz +0 -0
  29. package/src/dev/.next/cache/webpack/client-development/5.pack.gz_ +0 -0
  30. package/src/dev/.next/cache/webpack/client-development/6.pack.gz +0 -0
  31. package/src/dev/.next/cache/webpack/client-development/7.pack.gz +0 -0
  32. package/src/dev/.next/cache/webpack/client-development/7.pack.gz_ +0 -0
  33. package/src/dev/.next/cache/webpack/client-development/8.pack.gz +0 -0
  34. package/src/dev/.next/cache/webpack/client-development/9.pack.gz +0 -0
  35. package/src/dev/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
  36. package/src/dev/.next/cache/webpack/client-development-fallback/0.pack.gz +0 -0
  37. package/src/dev/.next/cache/webpack/client-development-fallback/1.pack.gz +0 -0
  38. package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz +0 -0
  39. package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz.old +0 -0
  40. package/src/dev/.next/cache/webpack/edge-server-development/0.pack.gz +0 -0
  41. package/src/dev/.next/cache/webpack/edge-server-development/1.pack.gz +0 -0
  42. package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz +0 -0
  43. package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz.old +0 -0
  44. package/src/dev/.next/cache/webpack/server-development/0.pack.gz +0 -0
  45. package/src/dev/.next/cache/webpack/server-development/1.pack.gz +0 -0
  46. package/src/dev/.next/cache/webpack/server-development/10.pack.gz +0 -0
  47. package/src/dev/.next/cache/webpack/server-development/11.pack.gz +0 -0
  48. package/src/dev/.next/cache/webpack/server-development/12.pack.gz +0 -0
  49. package/src/dev/.next/cache/webpack/server-development/13.pack.gz +0 -0
  50. package/src/dev/.next/cache/webpack/server-development/14.pack.gz +0 -0
  51. package/src/dev/.next/cache/webpack/server-development/15.pack.gz +0 -0
  52. package/src/dev/.next/cache/webpack/server-development/2.pack.gz +0 -0
  53. package/src/dev/.next/cache/webpack/server-development/2.pack.gz_ +0 -0
  54. package/src/dev/.next/cache/webpack/server-development/3.pack.gz +0 -0
  55. package/src/dev/.next/cache/webpack/server-development/3.pack.gz_ +0 -0
  56. package/src/dev/.next/cache/webpack/server-development/4.pack.gz +0 -0
  57. package/src/dev/.next/cache/webpack/server-development/5.pack.gz +0 -0
  58. package/src/dev/.next/cache/webpack/server-development/6.pack.gz +0 -0
  59. package/src/dev/.next/cache/webpack/server-development/6.pack.gz_ +0 -0
  60. package/src/dev/.next/cache/webpack/server-development/7.pack.gz +0 -0
  61. package/src/dev/.next/cache/webpack/server-development/7.pack.gz_ +0 -0
  62. package/src/dev/.next/cache/webpack/server-development/8.pack.gz +0 -0
  63. package/src/dev/.next/cache/webpack/server-development/9.pack.gz +0 -0
  64. package/src/dev/.next/cache/webpack/server-development/9.pack.gz_ +0 -0
  65. package/src/dev/.next/cache/webpack/server-development/index.pack.gz +0 -0
  66. package/src/dev/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
  67. package/src/dev/.next/package.json +1 -0
  68. package/src/dev/.next/prerender-manifest.json +11 -0
  69. package/src/dev/.next/react-loadable-manifest.json +1 -0
  70. package/src/dev/.next/routes-manifest.json +1 -0
  71. package/src/dev/.next/server/app/[...slug]/page.js +360 -0
  72. package/src/dev/.next/server/app/[...slug]/page_client-reference-manifest.js +1 -0
  73. package/src/dev/.next/server/app/page.js +349 -0
  74. package/src/dev/.next/server/app/page_client-reference-manifest.js +1 -0
  75. package/src/dev/.next/server/app-paths-manifest.json +3 -0
  76. package/src/dev/.next/server/edge-runtime-webpack.js +1151 -0
  77. package/src/dev/.next/server/interception-route-rewrite-manifest.js +1 -0
  78. package/src/dev/.next/server/middleware-build-manifest.js +33 -0
  79. package/src/dev/.next/server/middleware-manifest.json +32 -0
  80. package/src/dev/.next/server/middleware-react-loadable-manifest.js +1 -0
  81. package/src/dev/.next/server/middleware.js +1113 -0
  82. package/src/dev/.next/server/next-font-manifest.js +1 -0
  83. package/src/dev/.next/server/next-font-manifest.json +1 -0
  84. package/src/dev/.next/server/pages-manifest.json +5 -0
  85. package/src/dev/.next/server/server-reference-manifest.js +1 -0
  86. package/src/dev/.next/server/server-reference-manifest.json +5 -0
  87. package/src/dev/.next/server/static/webpack/633457081244afec._.hot-update.json +1 -0
  88. package/src/dev/.next/server/vendor-chunks/@readme.js +25 -0
  89. package/src/dev/.next/server/vendor-chunks/@swc.js +55 -0
  90. package/src/dev/.next/server/vendor-chunks/next.js +3659 -0
  91. package/src/dev/.next/server/webpack-runtime.js +209 -0
  92. package/src/dev/.next/static/chunks/app/[...slug]/loading.js +28 -0
  93. package/src/dev/.next/static/chunks/app/[...slug]/page.js +28 -0
  94. package/src/dev/.next/static/chunks/app/layout.js +171 -0
  95. package/src/dev/.next/static/chunks/app/page.js +28 -0
  96. package/src/dev/.next/static/chunks/app-pages-internals.js +182 -0
  97. package/src/dev/.next/static/chunks/main-app.js +1882 -0
  98. package/src/dev/.next/static/chunks/polyfills.js +1 -0
  99. package/src/dev/.next/static/chunks/webpack.js +1393 -0
  100. package/src/dev/.next/static/css/app/layout.css +559 -0
  101. package/src/dev/.next/static/development/_buildManifest.js +1 -0
  102. package/src/dev/.next/static/development/_ssgManifest.js +1 -0
  103. package/src/dev/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
  104. package/src/dev/.next/static/webpack/ec52a3fce0f78db0.webpack.hot-update.json +1 -0
  105. package/src/dev/.next/static/webpack/webpack.ec52a3fce0f78db0.hot-update.js +12 -0
  106. package/src/dev/.next/trace +21 -0
  107. package/src/dev/.next/types/app/[...slug]/page.ts +84 -0
  108. package/src/dev/.next/types/app/layout.ts +84 -0
  109. package/src/dev/.next/types/app/page.ts +84 -0
  110. package/src/dev/.next/types/cache-life.d.ts +141 -0
  111. package/src/dev/.next/types/package.json +1 -0
  112. package/src/dev/.next/types/routes.d.ts +55 -0
  113. package/src/dev/app/Sidebar.js +149 -0
  114. package/src/dev/app/[...slug]/loading.js +16 -0
  115. package/src/dev/app/[...slug]/page.js +43 -0
  116. package/src/dev/app/globals.css +167 -0
  117. package/src/dev/app/layout.js +73 -0
  118. package/src/dev/app/page.js +19 -0
  119. package/src/dev/lib/docs.js +337 -0
  120. package/src/dev/middleware.js +7 -0
  121. package/src/dev/next.config.mjs +22 -0
  122. package/src/index.js +12 -0
  123. package/src/prompts/index.js +352 -0
  124. package/src/utils/claude.js +15 -0
  125. package/src/utils/eyes.js +365 -0
  126. package/src/utils/git.js +143 -0
  127. package/src/utils/lint.js +99 -0
  128. package/src/utils/reporter.js +319 -0
  129. package/src/utils/setup-templates.js +323 -0
  130. package/src/utils/styles.js +50 -0
  131. package/src/utils/tamagotchi.js +1139 -0
  132. package/src/utils/tips.js +90 -0
  133. package/src/validators/components.js +230 -0
  134. package/src/validators/content.js +53 -0
  135. package/src/validators/duplicates.js +45 -0
  136. package/src/validators/frontmatter.js +247 -0
  137. package/src/validators/links.js +68 -0
  138. package/src/validators/nesting.js +50 -0
  139. package/src/validators/numbering.js +136 -0
  140. package/src/validators/oas-reference.js +126 -0
  141. package/src/validators/oas-schema.js +106 -0
  142. package/src/validators/ordering.js +121 -0
  143. package/src/validators/recipes.js +143 -0
  144. 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
+ }