@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,365 @@
1
+ import chalk from 'chalk';
2
+
3
+ const _ = null;
4
+
5
+ // True when invoked by an agentic coding CLI (Claude Code, OpenAI Codex).
6
+ // Used to skip the "fun" ASCII-eye header and other decorative output.
7
+ export function isAgenticCli() {
8
+ return !!(process.env.CLAUDECODE || process.env.CODEX_HOME || process.env.CODEX_SANDBOX);
9
+ }
10
+
11
+ // ── Palettes ────────────────────────────────────────────
12
+
13
+ export const palettes = {
14
+ blue: { name: 'Blue', body: '#018EF5', page: '#FFF5E6', pupil: '#003580', lid: '#63D2FF' },
15
+ green: { name: 'Green', body: '#2D9F3F', page: '#F0FFE6', pupil: '#0A4D1A', lid: '#7FE08A' },
16
+ purple: { name: 'Purple', body: '#8B5CF6', page: '#F3EEFF', pupil: '#3B1A8B', lid: '#C4A8FF' },
17
+ orange: { name: 'Orange', body: '#F97316', page: '#FFF5EB', pupil: '#7C2D12', lid: '#FDBA74' },
18
+ pink: { name: 'Pink', body: '#EC4899', page: '#FFF0F6', pupil: '#831843', lid: '#F9A8D4' },
19
+ red: { name: 'Red', body: '#EF4444', page: '#FFF5F5', pupil: '#7F1D1D', lid: '#FCA5A5' },
20
+ teal: { name: 'Teal', body: '#14B8A6', page: '#F0FFFE', pupil: '#134E4A', lid: '#5EEAD4' },
21
+ yellow: { name: 'Yellow', body: '#EAB308', page: '#FEFCE8', pupil: '#713F12', lid: '#FDE047' },
22
+ owl: { name: 'Owl', body: '#8B5A2B', page: '#FFF8EE', pupil: '#2A1000', lid: '#C4923A', nose: '#FFD700', hidden: true },
23
+ };
24
+
25
+ let currentPalette = palettes.blue;
26
+
27
+ export function setPalette(name) {
28
+ if (palettes[name]) {
29
+ currentPalette = palettes[name];
30
+ rebuildExpressions();
31
+ }
32
+ }
33
+
34
+ function cell(top, bot) {
35
+ if (top === bot) return top === null ? ' ' : chalk.hex(top)('██');
36
+ if (top === null) return chalk.hex(bot)('▄▄');
37
+ if (bot === null) return chalk.hex(top)('▀▀');
38
+ return chalk.hex(top).bgHex(bot)('▀▀');
39
+ }
40
+
41
+ function render(px) {
42
+ const lines = [];
43
+ for (let r = 0; r < px.length; r += 2) {
44
+ let line = '';
45
+ for (let c = 0; c < px[0].length; c++) {
46
+ line += cell(px[r][c], px[r + 1]?.[c] ?? null);
47
+ }
48
+ lines.push(line);
49
+ }
50
+ return lines;
51
+ }
52
+
53
+ // ── Pixel grids ─────────────────────────────────────────
54
+
55
+ function applyLid(eye, lid, L) {
56
+ // lid: 0 = open, 1 = half, 2 = squint, 3 = closed
57
+ const count = lid === 3 ? 2 : lid;
58
+ for (let i = 0; i < count && i < 2; i++) {
59
+ eye[i][0] = L;
60
+ eye[i][1] = L;
61
+ }
62
+ }
63
+
64
+ function makeFrame(eyeL, eyeR, lidL = 0, lidR = lidL) {
65
+ const { body: B, page: W, pupil: D, lid: L, nose: N = B } = currentPalette;
66
+ // eyeL/eyeR: [row, col] of pupil within the 2x2 page area
67
+ // row 0 = top, 1 = bottom; col 0 = left, 1 = right
68
+ // lidL/lidR: 0 = open, 1 = half, 2 = squint (most), 3 = closed
69
+ const page = [
70
+ [W, W],
71
+ [W, W],
72
+ ];
73
+
74
+ // Place pupils
75
+ const left = page.map(r => [...r]);
76
+ const right = page.map(r => [...r]);
77
+ left[eyeL[0]][eyeL[1]] = D;
78
+ right[eyeR[0]][eyeR[1]] = D;
79
+
80
+ // Apply eyelids per eye
81
+ applyLid(left, lidL, L);
82
+ applyLid(right, lidR, L);
83
+
84
+ return [
85
+ [B, B, B, _, B, B, B],
86
+ [B, left[0][0], left[0][1], B, right[0][0], right[0][1], B],
87
+ [B, left[1][0], left[1][1], B, right[1][0], right[1][1], B],
88
+ [B, left[1][0], left[1][1], B, right[1][0], right[1][1], B],
89
+ [B, B, B, N, B, B, B],
90
+ [_, _, _, N, _, _, _],
91
+ [_, _, _, _, _, _, _],
92
+ ];
93
+ }
94
+
95
+ // Bounce-up: prepend empty row to shift pixel pairing
96
+ function makeBounceUp(px) {
97
+ const empty = new Array(px[0].length).fill(_);
98
+ return [empty, ...px];
99
+ }
100
+
101
+ // ── Static expressions ──────────────────────────────────
102
+
103
+ function buildExpressions() {
104
+ return {
105
+ right: makeFrame([1, 1], [1, 1]),
106
+ left: makeFrame([1, 0], [1, 0]),
107
+ 'up-right': makeFrame([0, 1], [0, 1]),
108
+ 'up-left': makeFrame([0, 0], [0, 0]),
109
+ 'half-blink': makeFrame([1, 1], [1, 1], 1),
110
+ 'half-blink-left': makeFrame([1, 0], [1, 0], 1),
111
+ 'half-blink-right': makeFrame([1, 1], [1, 1], 1),
112
+ squint: makeFrame([1, 1], [1, 1], 2),
113
+ closed: makeFrame([1, 1], [1, 1], 3),
114
+ };
115
+ }
116
+
117
+ export let expressions = buildExpressions();
118
+
119
+ function rebuildExpressions() {
120
+ expressions = buildExpressions();
121
+ }
122
+
123
+ // ── Animations ──────────────────────────────────────────
124
+
125
+ export const animations = {
126
+ blink: {
127
+ frames: ['right', 'right', 'right', 'right', 'right', 'right', 'half-blink', 'squint', 'closed', 'closed', 'squint', 'half-blink', 'right'],
128
+ durations: [1500, 100, 100, 100, 100, 100, 50, 50, 50, 80, 50, 50, 100],
129
+ },
130
+
131
+ bounce: {
132
+ frames: ['right', 'right', 'right:up', 'right:up', 'right'],
133
+ durations: [400, 200, 200, 200, 200],
134
+ },
135
+
136
+ lookaround: {
137
+ frames: ['right', 'right', 'left', 'left', 'left', 'right', 'right', 'up-right', 'up-right', 'right', 'right', 'up-left', 'up-left', 'left', 'left', 'right'],
138
+ durations: [600, 200, 120, 400, 200, 120, 400, 120, 400, 120, 200, 120, 400, 120, 200, 400],
139
+ },
140
+
141
+ 'lookaround-blink': {
142
+ frames: [
143
+ 'right', 'right', 'right',
144
+ 'left', 'left', 'left',
145
+ 'half-blink', 'squint', 'closed', 'closed', 'squint', 'half-blink',
146
+ 'left', 'right', 'right',
147
+ 'up-right', 'up-right', 'right',
148
+ 'right', 'right',
149
+ 'up-left', 'up-left', 'left', 'left',
150
+ 'half-blink', 'squint', 'closed', 'closed', 'squint', 'half-blink',
151
+ 'right', 'right',
152
+ ],
153
+ durations: [
154
+ 800, 200, 200,
155
+ 120, 400, 200,
156
+ 50, 50, 50, 80, 50, 50,
157
+ 200, 120, 400,
158
+ 120, 500, 120,
159
+ 200, 200,
160
+ 120, 500, 120, 200,
161
+ 50, 50, 50, 80, 50, 50,
162
+ 120, 600,
163
+ ],
164
+ },
165
+
166
+ all: {
167
+ frames: [
168
+ // idle, look around
169
+ 'right', 'right', 'right',
170
+ 'left', 'left', 'left',
171
+ 'right', 'right',
172
+ // blink
173
+ 'half-blink', 'squint', 'closed', 'closed', 'squint', 'half-blink',
174
+ // look up + bounce
175
+ 'right', 'right',
176
+ 'up-right', 'up-right', 'right',
177
+ 'right:up', 'right:up', 'right', 'right:up', 'right:up', 'right',
178
+ // look the other way
179
+ 'right', 'left', 'left',
180
+ 'up-left', 'up-left', 'left',
181
+ // blink
182
+ 'half-blink', 'squint', 'closed', 'closed', 'squint', 'half-blink',
183
+ // bounce
184
+ 'left', 'left:up', 'left:up', 'left', 'left:up', 'left:up', 'left',
185
+ // settle back + bounce
186
+ 'right', 'right', 'right',
187
+ 'right:up', 'right:up', 'right',
188
+ // blink
189
+ 'half-blink', 'squint', 'closed', 'closed', 'squint', 'half-blink',
190
+ 'right', 'right',
191
+ ],
192
+ durations: [
193
+ // idle, look around
194
+ 800, 200, 200,
195
+ 120, 400, 200,
196
+ 120, 400,
197
+ // blink
198
+ 50, 50, 50, 80, 50, 50,
199
+ // look up + bounce
200
+ 200, 200,
201
+ 120, 500, 120,
202
+ 150, 150, 150, 150, 150, 200,
203
+ // look the other way
204
+ 300, 120, 400,
205
+ 120, 500, 120,
206
+ // blink
207
+ 50, 50, 50, 80, 50, 50,
208
+ // bounce
209
+ 200, 150, 150, 150, 150, 150, 200,
210
+ // settle back + bounce
211
+ 120, 200, 300,
212
+ 150, 150, 200,
213
+ // blink
214
+ 50, 50, 50, 80, 50, 50,
215
+ 200, 800,
216
+ ],
217
+ },
218
+ };
219
+
220
+ // ── Public API ──────────────────────────────────────────
221
+
222
+ /**
223
+ * Get rendered lines for a static expression.
224
+ * @param {'right'|'left'|'up-right'|'up-left'|'half-blink'|'squint'|'closed'} name
225
+ * @returns {string[]}
226
+ */
227
+ export function eyes(name = 'right') {
228
+ const px = expressions[name];
229
+ if (!px) throw new Error(`Unknown expression: ${name}. Valid: ${Object.keys(expressions).join(', ')}`);
230
+ return render(px);
231
+ }
232
+
233
+ /**
234
+ * Print a static expression to stdout.
235
+ * @param {'right'|'left'|'up-right'|'up-left'|'half-blink'|'squint'|'closed'} name
236
+ * @param {string} [indent='']
237
+ */
238
+ export function printEyes(name = 'right', indent = '') {
239
+ for (const line of eyes(name)) {
240
+ console.log(indent + line);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Get rendered lines for the header: eyes + "The ReadMe CLI" / version.
246
+ * @param {object} [opts]
247
+ * @param {string} [opts.expression='right']
248
+ * @param {string} [opts.version]
249
+ * @returns {string[]}
250
+ */
251
+ export function header(opts = {}) {
252
+ const { expression = 'right', version, binName, greeting } = opts;
253
+ const icon = eyes(expression);
254
+ const gap = ' ';
255
+
256
+ const title = chalk.bold.hex(currentPalette.body)('The ReadMe CLI') + (version ? ' ' + chalk.dim(`v${version}`) : '');
257
+ const bin = binName ? chalk.white(binName) : '';
258
+ const greetLine = greeting ? chalk.dim(greeting) : '';
259
+
260
+ return icon.map((line, i) => {
261
+ if (i === 0) return line + gap + title;
262
+ if (i === 1) return line + gap + bin;
263
+ if (i === 2 && greetLine) return line + gap + greetLine;
264
+ return line;
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Print the header to stdout.
270
+ * @param {object} [opts]
271
+ * @param {string} [opts.expression='right']
272
+ * @param {string} [opts.version]
273
+ * @param {string} [opts.indent='']
274
+ */
275
+ export function printHeader(opts = {}) {
276
+ const { indent = '', ...rest } = opts;
277
+ for (const line of header(rest)) {
278
+ console.log(indent + line);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Run an animation loop. Returns an abort function.
284
+ * @param {'blink'|'bounce'|'lookaround'|'lookaround-blink'} name
285
+ * @param {object} [opts]
286
+ * @param {string} [opts.indent='']
287
+ * @param {boolean} [opts.loop=true]
288
+ * @returns {{ stop: () => void }}
289
+ */
290
+ export function animate(name, opts = {}) {
291
+ const { indent = '', loop = true, version, binName: bin, subtitle } = opts;
292
+ const anim = animations[name];
293
+ if (!anim) throw new Error(`Unknown animation: ${name}. Valid: ${Object.keys(animations).join(', ')}`);
294
+
295
+ // Build static header text lines (appended to the right of each frame row)
296
+ const headerLines = [];
297
+ if (version || bin) {
298
+ const gap = ' ';
299
+ headerLines[0] = gap + chalk.bold.hex(currentPalette.body)('The ReadMe CLI') + (version ? ' ' + chalk.dim(`v${version}`) : '');
300
+ headerLines[1] = gap + (bin ? chalk.white(bin) : '');
301
+ if (subtitle) headerLines[2] = gap + subtitle;
302
+ }
303
+
304
+ let stopped = false;
305
+ let firstDraw = true;
306
+
307
+ // Resolve a frame name (possibly with :up suffix) to rendered lines
308
+ function resolveFrame(frameName) {
309
+ const isUp = frameName.endsWith(':up');
310
+ const expr = isUp ? frameName.slice(0, -3) : frameName;
311
+ let px = expressions[expr];
312
+ if (isUp) px = makeBounceUp(px);
313
+ return render(px);
314
+ }
315
+
316
+ // Pre-calculate the max height across all frames
317
+ const maxRows = Math.max(...anim.frames.map(f => resolveFrame(f).length));
318
+
319
+ async function run() {
320
+ // Hide cursor
321
+ process.stdout.write('\x1b[?25l');
322
+
323
+ let i = 0;
324
+ while (!stopped) {
325
+ const frameName = anim.frames[i % anim.frames.length];
326
+ const lines = resolveFrame(frameName);
327
+
328
+ // Pad to max height
329
+ while (lines.length < maxRows) lines.push(' '.repeat(7));
330
+
331
+ // Append header text if provided
332
+ if (headerLines.length) {
333
+ for (let j = 0; j < lines.length; j++) {
334
+ if (headerLines[j]) lines[j] += headerLines[j];
335
+ }
336
+ }
337
+
338
+ // Move cursor up to overwrite previous frame
339
+ if (!firstDraw) process.stdout.write(`\x1b[${maxRows}A`);
340
+ firstDraw = false;
341
+
342
+ for (const line of lines) {
343
+ process.stdout.write(indent + line + '\n');
344
+ }
345
+
346
+ const duration = anim.durations[i % anim.durations.length];
347
+ await new Promise(r => setTimeout(r, duration));
348
+
349
+ i++;
350
+ if (!loop && i >= anim.frames.length) break;
351
+ }
352
+
353
+ // Show cursor
354
+ process.stdout.write('\x1b[?25h');
355
+ }
356
+
357
+ run();
358
+
359
+ return {
360
+ stop() {
361
+ stopped = true;
362
+ process.stdout.write('\x1b[?25h');
363
+ },
364
+ };
365
+ }
@@ -0,0 +1,143 @@
1
+ import { execSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ export const WORKFLOW_VERSION = 2;
6
+
7
+ // Platform definitions: keep ordering — first match wins for filesystem detection.
8
+ export const PLATFORMS = {
9
+ github: {
10
+ label: 'GitHub Actions',
11
+ remoteHosts: ['github.com'],
12
+ workflowFile: '.github/workflows/readme-lint.yml',
13
+ fsMarkers: ['.github/workflows'],
14
+ },
15
+ gitlab: {
16
+ label: 'GitLab CI',
17
+ remoteHosts: ['gitlab.com'],
18
+ remotePathHints: ['/gitlab/'],
19
+ workflowFile: '.gitlab-ci.yml',
20
+ fsMarkers: ['.gitlab-ci.yml'],
21
+ },
22
+ bitbucket: {
23
+ label: 'Bitbucket Pipelines',
24
+ remoteHosts: ['bitbucket.org'],
25
+ workflowFile: 'bitbucket-pipelines.yml',
26
+ fsMarkers: ['bitbucket-pipelines.yml'],
27
+ },
28
+ circleci: {
29
+ label: 'CircleCI',
30
+ workflowFile: '.circleci/config.yml',
31
+ fsMarkers: ['.circleci/config.yml'],
32
+ },
33
+ rwx: {
34
+ label: 'RWX Mint',
35
+ workflowFile: '.mint/readme-lint.yml',
36
+ fsMarkers: ['.mint'],
37
+ },
38
+ };
39
+
40
+ let _remotesCache;
41
+
42
+ function readRemotes() {
43
+ if (_remotesCache !== undefined) return _remotesCache;
44
+ try {
45
+ _remotesCache = execSync('git remote -v', {
46
+ encoding: 'utf-8',
47
+ stdio: ['pipe', 'pipe', 'pipe'],
48
+ });
49
+ } catch {
50
+ _remotesCache = '';
51
+ }
52
+ return _remotesCache;
53
+ }
54
+
55
+ export function hasGithubRemote() {
56
+ return /github\.com/i.test(readRemotes());
57
+ }
58
+
59
+ /**
60
+ * Inspect the git remote and return the matching platform key, or null.
61
+ */
62
+ export function detectRemotePlatform() {
63
+ const remotes = readRemotes();
64
+ if (!remotes) return null;
65
+ for (const [key, def] of Object.entries(PLATFORMS)) {
66
+ if (def.remoteHosts && def.remoteHosts.some((h) => remotes.toLowerCase().includes(h))) {
67
+ return key;
68
+ }
69
+ if (def.remotePathHints && def.remotePathHints.some((p) => remotes.toLowerCase().includes(p))) {
70
+ return key;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Scan the filesystem for existing CI configuration files. Returns an array
78
+ * of platform keys (in PLATFORMS declaration order) that have markers present.
79
+ */
80
+ export function detectExistingCi(gitRoot) {
81
+ if (!gitRoot) return [];
82
+ const found = [];
83
+ for (const [key, def] of Object.entries(PLATFORMS)) {
84
+ const hit = def.fsMarkers.some((marker) => fs.existsSync(path.join(gitRoot, marker)));
85
+ if (hit) found.push(key);
86
+ }
87
+ return found;
88
+ }
89
+
90
+ /**
91
+ * Detect whether an existing GitHub workflow uses the Blacksmith runner.
92
+ */
93
+ export function usesBlacksmith(gitRoot) {
94
+ if (!gitRoot) return false;
95
+ const dir = path.join(gitRoot, '.github/workflows');
96
+ if (!fs.existsSync(dir)) return false;
97
+ try {
98
+ for (const f of fs.readdirSync(dir)) {
99
+ if (!/\.ya?ml$/.test(f)) continue;
100
+ const body = fs.readFileSync(path.join(dir, f), 'utf-8');
101
+ if (/blacksmith/i.test(body)) return true;
102
+ }
103
+ } catch {
104
+ // ignore
105
+ }
106
+ return false;
107
+ }
108
+
109
+ /**
110
+ * Combined detection for the bare `setup` command. Filesystem markers win
111
+ * over remote host (existing CI is the strongest signal of intent).
112
+ */
113
+ export function detectPlatform(gitRoot) {
114
+ const existing = detectExistingCi(gitRoot);
115
+ const remote = detectRemotePlatform();
116
+ return {
117
+ existing,
118
+ remote,
119
+ recommended: existing[0] || remote || null,
120
+ };
121
+ }
122
+
123
+ export function hasGithubWorkflow(gitRoot) {
124
+ return fs.existsSync(path.join(gitRoot, PLATFORMS.github.workflowFile));
125
+ }
126
+
127
+ export function hasReadmeWorkflow(gitRoot, platformKey) {
128
+ const def = PLATFORMS[platformKey];
129
+ if (!def) return false;
130
+ return fs.existsSync(path.join(gitRoot, def.workflowFile));
131
+ }
132
+
133
+ export function getWorkflowVersion(gitRoot) {
134
+ const file = path.join(gitRoot, PLATFORMS.github.workflowFile);
135
+ if (!fs.existsSync(file)) return null;
136
+ try {
137
+ const first = fs.readFileSync(file, 'utf-8').split('\n')[0];
138
+ const match = first.match(/^# readme-lint v(\d+)/);
139
+ return match ? Number(match[1]) : 0;
140
+ } catch {
141
+ return 0;
142
+ }
143
+ }
@@ -0,0 +1,99 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ const TARGET_PATTERNS = [
6
+ { dir: 'custom_blocks', ext: ['.mdx', '.md'] },
7
+ { dir: 'docs', ext: '.md' },
8
+ { dir: 'reference', ext: '.md' },
9
+ { dir: 'custom_pages', ext: '.md' },
10
+ { dir: 'recipes', ext: '.md' },
11
+ ];
12
+
13
+ /**
14
+ * Collect all target files from the repo root.
15
+ */
16
+ export function collectFiles(gitRoot) {
17
+ const files = [];
18
+
19
+ for (const { dir, ext } of TARGET_PATTERNS) {
20
+ const dirPath = path.join(gitRoot, dir);
21
+ if (!fs.existsSync(dirPath)) continue;
22
+
23
+ const exts = Array.isArray(ext) ? ext : [ext];
24
+ const entries = fs.readdirSync(dirPath, { recursive: true });
25
+ for (const entry of entries) {
26
+ if (exts.some((e) => entry.endsWith(e))) {
27
+ files.push(path.join(dir, entry));
28
+ }
29
+ }
30
+ }
31
+
32
+ return files.sort();
33
+ }
34
+
35
+ /**
36
+ * Auto-discover all validators from src/validators/.
37
+ * Validators can export `validate()` for per-file checks and/or
38
+ * `validateAll()` for cross-file checks (like ordering).
39
+ */
40
+ async function loadValidators() {
41
+ const validatorsDir = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'validators');
42
+ const files = fs.readdirSync(validatorsDir).filter((f) => f.endsWith('.js'));
43
+ const validators = [];
44
+
45
+ for (const file of files) {
46
+ const mod = await import(pathToFileURL(path.join(validatorsDir, file)).href);
47
+ if (mod.name && (mod.validate || mod.validateAll)) {
48
+ validators.push(mod);
49
+ }
50
+ }
51
+
52
+ return validators;
53
+ }
54
+
55
+ /**
56
+ * Run all validators against every file. Returns array of result objects.
57
+ * Each result has { file, rule, message, severity? } where severity defaults to 'error'.
58
+ * Calls `onFile(relativePath)` before processing each file (for progress reporting).
59
+ */
60
+ export async function runValidators(files, gitRoot, { onFile, onBeforeCrossFile, fix } = {}) {
61
+ const validators = await loadValidators();
62
+ const results = [];
63
+
64
+ // Per-file validators.
65
+ for (const relativePath of files) {
66
+ if (onFile) onFile(relativePath);
67
+
68
+ const filePath = path.join(gitRoot, relativePath);
69
+ const content = fs.readFileSync(filePath, 'utf-8');
70
+
71
+ for (const validator of validators) {
72
+ if (!validator.validate) continue;
73
+ const result = validator.validate({ filePath, content, relativePath, fix });
74
+ if (result) {
75
+ if (Array.isArray(result)) {
76
+ results.push(...result);
77
+ } else {
78
+ results.push(result);
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ // Cross-file validators — stop the spinner first so interactive prompts work.
85
+ if (onBeforeCrossFile) onBeforeCrossFile();
86
+ for (const validator of validators) {
87
+ if (!validator.validateAll) continue;
88
+ const result = await validator.validateAll(files, gitRoot, { fix });
89
+ if (result) {
90
+ if (Array.isArray(result)) {
91
+ results.push(...result);
92
+ } else {
93
+ results.push(result);
94
+ }
95
+ }
96
+ }
97
+
98
+ return results;
99
+ }