@readme/cli 0.0.26 → 0.0.27
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 +2 -2
- package/package.json +3 -1
- package/src/cli.js +5 -0
- package/src/commands/components.js +288 -0
- package/src/commands/import.js +410 -34
- package/src/commands/lint.js +32 -35
- package/src/commands/pretty.js +314 -77
- package/src/prompts/index.js +86 -11
- package/src/utils/lint.js +33 -33
- package/src/utils/marketplace.js +208 -0
- package/src/utils/multi-spinner.js +71 -0
- package/src/utils/promise-pool.js +17 -0
- package/src/validators/numbering.js +64 -66
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A command-line tool for writing, previewing, and managing your [ReadMe](https://readme.com) docs from your terminal.
|
|
4
4
|
|
|
5
|
-
> **
|
|
5
|
+
> **We want feedback!** If you need a new feature or find a bug, please [open an issue](https://github.com/readmeio/cli/issues).
|
|
6
6
|
|
|
7
7
|
## Commands
|
|
8
8
|
|
|
@@ -52,4 +52,4 @@ npx @readme/cli dev
|
|
|
52
52
|
|
|
53
53
|
## Feedback
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
If something doesn't work right, or if you have suggestions, please [open an issue](https://github.com/readmeio/cli/issues). We're actively building this and want to hear from you.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@readme/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "The ReadMe CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,8 +46,10 @@
|
|
|
46
46
|
"@readmeio/git-format": "readmeio/git-format",
|
|
47
47
|
"ajv": "^8.18.0",
|
|
48
48
|
"chalk": "^5.4.1",
|
|
49
|
+
"cli-spinners": "^3.4.0",
|
|
49
50
|
"commander": "^13.1.0",
|
|
50
51
|
"gray-matter": "^4.0.3",
|
|
52
|
+
"log-update": "^8.0.0",
|
|
51
53
|
"next": "^15.0.0",
|
|
52
54
|
"oas-normalize": "^15.7.1",
|
|
53
55
|
"ora": "^9.3.0",
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
+
import updateNotifier from 'update-notifier';
|
|
6
7
|
import bootstrap from './bootstrap.js';
|
|
7
8
|
import * as styles from './utils/styles.js';
|
|
8
9
|
import { header, setPalette, isAgenticCli } from './utils/eyes.js';
|
|
@@ -15,6 +16,10 @@ const pkg = require('../package.json');
|
|
|
15
16
|
const isRunningInClaude = isAgenticCli();
|
|
16
17
|
|
|
17
18
|
export async function main() {
|
|
19
|
+
if (!isRunningInClaude) {
|
|
20
|
+
updateNotifier({ pkg }).notify({ defer: true, isGlobal: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
const program = new Command();
|
|
19
24
|
|
|
20
25
|
// Load tamagotchi state if it exists for personalized header
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `readme components` — browse, install, view, and generate MDX components
|
|
3
|
+
* for a ReadMe project.
|
|
4
|
+
*
|
|
5
|
+
* The canonical catalog lives at https://github.com/readmeio/marketplace.
|
|
6
|
+
* Components aren't auto-loaded: copying one into the user's `components/`
|
|
7
|
+
* folder is what makes it available in their docs (and to the slash menu in
|
|
8
|
+
* the ReadMe admin). The `add` subcommand does that copy; `new` asks Claude
|
|
9
|
+
* to generate a brand-new component using the marketplace entries as
|
|
10
|
+
* exemplars (tailwind + arbitrary JSX is supported).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import readline from 'node:readline';
|
|
16
|
+
import ora from 'ora';
|
|
17
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
18
|
+
import * as styles from '../utils/styles.js';
|
|
19
|
+
import {
|
|
20
|
+
listMarketplaceComponents,
|
|
21
|
+
fetchMarketplaceComponent,
|
|
22
|
+
summarizeReadme,
|
|
23
|
+
localComponentsDir,
|
|
24
|
+
listLocalComponents,
|
|
25
|
+
} from '../utils/marketplace.js';
|
|
26
|
+
import { stripCodeFences } from '../prompts/index.js';
|
|
27
|
+
|
|
28
|
+
export const command = 'components [action] [name...]';
|
|
29
|
+
export const order = 3;
|
|
30
|
+
export const category = 'Other';
|
|
31
|
+
export const description = 'Browse, install, and generate MDX components';
|
|
32
|
+
export const beta = true;
|
|
33
|
+
export const helpHint = [
|
|
34
|
+
'readme components # list marketplace + installed',
|
|
35
|
+
'readme components add <Name> # copy a marketplace component into ./components/',
|
|
36
|
+
'readme components show <Name> # print the component\'s source',
|
|
37
|
+
'readme components new "<idea>" # ask Claude to generate a new component',
|
|
38
|
+
].join('\n');
|
|
39
|
+
|
|
40
|
+
export function args(cmd) {
|
|
41
|
+
cmd.option('-f, --force', 'Bypass the 24h cache and refetch from GitHub');
|
|
42
|
+
cmd.option('--model <name>', 'Claude model for `new`: haiku, sonnet, opus', 'sonnet');
|
|
43
|
+
cmd.option('-y, --yes', 'Overwrite existing files without prompting');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function run(action, names, options, _cmd, ctx) {
|
|
47
|
+
const { gitRoot } = ctx;
|
|
48
|
+
const sub = (action || 'list').toLowerCase();
|
|
49
|
+
const arg = (names && names.length) ? names.join(' ') : null;
|
|
50
|
+
|
|
51
|
+
switch (sub) {
|
|
52
|
+
case 'list':
|
|
53
|
+
case 'ls':
|
|
54
|
+
return runList(gitRoot, options);
|
|
55
|
+
case 'add':
|
|
56
|
+
case 'install':
|
|
57
|
+
if (!arg) return missingArg('add', '<Name>');
|
|
58
|
+
return runAdd(gitRoot, names, options);
|
|
59
|
+
case 'show':
|
|
60
|
+
case 'view':
|
|
61
|
+
if (!arg) return missingArg('show', '<Name>');
|
|
62
|
+
return runShow(arg, options);
|
|
63
|
+
case 'new':
|
|
64
|
+
case 'create':
|
|
65
|
+
case 'gen':
|
|
66
|
+
if (!arg) return missingArg('new', '"<description>"');
|
|
67
|
+
return runNew(gitRoot, arg, options);
|
|
68
|
+
default:
|
|
69
|
+
// First arg wasn't an action — treat the whole thing as a name to show.
|
|
70
|
+
return runShow([action, ...(names || [])].filter(Boolean).join(' '), options);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function missingArg(sub, hint) {
|
|
75
|
+
styles.error(`Missing argument for ${styles.bold(sub)}. Usage: ${styles.bold(`readme components ${sub} ${hint}`)}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── list ────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async function runList(gitRoot, options) {
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(` ${styles.logo()} ${styles.dim('components')}`);
|
|
84
|
+
console.log();
|
|
85
|
+
|
|
86
|
+
const spinner = ora({ color: 'blue', text: 'Fetching marketplace…' }).start();
|
|
87
|
+
let marketplace;
|
|
88
|
+
try {
|
|
89
|
+
marketplace = await listMarketplaceComponents({ force: !!options.force });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
spinner.stop();
|
|
92
|
+
styles.error(`Could not load marketplace: ${err.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Pull readmes for any components we already have cached — gives us
|
|
97
|
+
// descriptions without a fetch storm on the first run.
|
|
98
|
+
const summaries = new Map();
|
|
99
|
+
for (const c of marketplace) {
|
|
100
|
+
try {
|
|
101
|
+
const { readme } = await fetchMarketplaceComponent(c.name, { force: false });
|
|
102
|
+
summaries.set(c.name, summarizeReadme(readme));
|
|
103
|
+
} catch {
|
|
104
|
+
summaries.set(c.name, '');
|
|
105
|
+
}
|
|
106
|
+
spinner.text = `Loading ${c.name}…`;
|
|
107
|
+
}
|
|
108
|
+
spinner.stop();
|
|
109
|
+
|
|
110
|
+
const local = new Set(listLocalComponents(gitRoot).map((c) => c.name.toLowerCase()));
|
|
111
|
+
|
|
112
|
+
const nameWidth = Math.max(...marketplace.map((c) => c.name.length));
|
|
113
|
+
for (const c of marketplace) {
|
|
114
|
+
const installed = local.has(c.name.toLowerCase());
|
|
115
|
+
const marker = installed ? styles.success('●') : styles.dim('○');
|
|
116
|
+
const summary = summaries.get(c.name) || styles.dim('(no description)');
|
|
117
|
+
console.log(` ${marker} ${styles.bold(c.name.padEnd(nameWidth))} ${styles.dim(summary)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log();
|
|
121
|
+
const installedCount = [...local].filter((n) => marketplace.some((c) => c.name.toLowerCase() === n)).length;
|
|
122
|
+
styles.info(`${marketplace.length} components in the marketplace · ${installedCount} already installed.`);
|
|
123
|
+
styles.info(`Run ${styles.bold('readme components add <Name>')} to copy one into ./components/.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── add ─────────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
async function runAdd(gitRoot, names, options) {
|
|
129
|
+
const targets = (names || []).filter(Boolean);
|
|
130
|
+
if (targets.length === 0) return missingArg('add', '<Name>');
|
|
131
|
+
|
|
132
|
+
const dest = localComponentsDir(gitRoot);
|
|
133
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
134
|
+
|
|
135
|
+
for (const name of targets) {
|
|
136
|
+
let comp;
|
|
137
|
+
try {
|
|
138
|
+
comp = await fetchMarketplaceComponent(name, { force: !!options.force });
|
|
139
|
+
} catch (err) {
|
|
140
|
+
styles.error(`${styles.bold(name)} — ${err.message}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const file = path.join(dest, `${comp.name}.mdx`);
|
|
145
|
+
if (fs.existsSync(file) && !options.yes) {
|
|
146
|
+
const ok = await confirm(`${styles.bold(comp.name)}.mdx already exists. Overwrite?`);
|
|
147
|
+
if (!ok) {
|
|
148
|
+
styles.info(`Skipped ${styles.bold(comp.name)}.`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fs.writeFileSync(file, comp.mdx, 'utf-8');
|
|
154
|
+
styles.ok(`Installed ${styles.bold(comp.name)} → ${styles.dim(path.relative(gitRoot, file))}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log();
|
|
158
|
+
styles.info('Components are available to docs in this repo. Use them like `<Name ... />` in any MDX page.');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── show ────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
async function runShow(name, options) {
|
|
164
|
+
let comp;
|
|
165
|
+
try {
|
|
166
|
+
comp = await fetchMarketplaceComponent(name, { force: !!options.force });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
styles.error(err.message);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(` ${styles.heading(comp.name)} ${styles.dim(comp.htmlUrl)}`);
|
|
174
|
+
if (comp.readme) {
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(comp.readme.split('\n').map((l) => ' ' + l).join('\n'));
|
|
177
|
+
}
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(` ${styles.bold('Source')} ${styles.dim(`(${comp.name}.mdx)`)}`);
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(comp.mdx.split('\n').map((l) => ' ' + l).join('\n'));
|
|
182
|
+
console.log();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── new (Claude-generated) ──────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
async function runNew(gitRoot, description, options) {
|
|
188
|
+
const dest = localComponentsDir(gitRoot);
|
|
189
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
190
|
+
|
|
191
|
+
styles.info(`Generating a new MDX component for: ${styles.bold(description)}`);
|
|
192
|
+
|
|
193
|
+
// Pull a handful of marketplace components as exemplars. We pick a spread of
|
|
194
|
+
// simple + complex ones so Claude sees the range of patterns.
|
|
195
|
+
const exemplarNames = ['Compatibility', 'Banner', 'Terminal', 'Steps', 'KeyPress'];
|
|
196
|
+
const exemplars = [];
|
|
197
|
+
for (const n of exemplarNames) {
|
|
198
|
+
try {
|
|
199
|
+
const c = await fetchMarketplaceComponent(n);
|
|
200
|
+
exemplars.push({ name: c.name, mdx: c.mdx });
|
|
201
|
+
} catch { /* skip */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const systemPrompt = [
|
|
205
|
+
'You write self-contained ReadMe MDX components.',
|
|
206
|
+
'',
|
|
207
|
+
'Format requirements:',
|
|
208
|
+
'- A single .mdx file. Top: `import React from "react";`.',
|
|
209
|
+
'- Export an arrow-function component as `export const <Name> = (props) => (...)`.',
|
|
210
|
+
'- Tailwind utility classes for styling — no <style> blocks, no external CSS.',
|
|
211
|
+
'- Inline SVG or FontAwesome (`<i className="fa-solid fa-..."/>`) for icons.',
|
|
212
|
+
'- End the file with one example invocation `<Name ... />` showing realistic props.',
|
|
213
|
+
'- No `<script>`, no event handlers requiring app state, no external imports beyond React.',
|
|
214
|
+
'- Keep the component small and readable. ~30-80 lines.',
|
|
215
|
+
'',
|
|
216
|
+
'Output: ONLY the raw .mdx source. No JSON, no code fences, no commentary.',
|
|
217
|
+
].join('\n');
|
|
218
|
+
|
|
219
|
+
const userPrompt = [
|
|
220
|
+
`Design brief: ${description}`,
|
|
221
|
+
'',
|
|
222
|
+
'For style and conventions, follow these existing components (do NOT copy them — match their structure):',
|
|
223
|
+
'',
|
|
224
|
+
...exemplars.map((e) => `### ${e.name}\n\`\`\`mdx\n${e.mdx}\n\`\`\``),
|
|
225
|
+
'',
|
|
226
|
+
'Now write the new component .mdx. Pick a good PascalCase name that matches the brief.',
|
|
227
|
+
].join('\n');
|
|
228
|
+
|
|
229
|
+
const spinner = ora({ color: 'blue', text: 'Asking Claude…' }).start();
|
|
230
|
+
let text = '';
|
|
231
|
+
try {
|
|
232
|
+
for await (const message of query({
|
|
233
|
+
prompt: userPrompt,
|
|
234
|
+
options: {
|
|
235
|
+
systemPrompt,
|
|
236
|
+
allowedTools: [],
|
|
237
|
+
...(options.model ? { model: options.model } : {}),
|
|
238
|
+
},
|
|
239
|
+
})) {
|
|
240
|
+
if (message.type === 'assistant' && message.message?.content) {
|
|
241
|
+
for (const block of message.message.content) {
|
|
242
|
+
if (block.type === 'text' && block.text) text += block.text;
|
|
243
|
+
}
|
|
244
|
+
} else if (message.type === 'result') {
|
|
245
|
+
if (message.subtype && message.subtype !== 'success') {
|
|
246
|
+
throw new Error(`${message.subtype}${message.error?.message ? ' — ' + message.error.message : ''}`);
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
spinner.stop();
|
|
253
|
+
styles.error(`Generation failed: ${err.message}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
spinner.stop();
|
|
257
|
+
|
|
258
|
+
const mdx = stripCodeFences(text).trim();
|
|
259
|
+
const nameMatch = mdx.match(/export\s+const\s+([A-Z][A-Za-z0-9]+)\s*=/);
|
|
260
|
+
if (!nameMatch) {
|
|
261
|
+
styles.error('Claude\'s response did not include `export const <Name> = ...`. Try again.');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const name = nameMatch[1];
|
|
266
|
+
const file = path.join(dest, `${name}.mdx`);
|
|
267
|
+
if (fs.existsSync(file) && !options.yes) {
|
|
268
|
+
const ok = await confirm(`${styles.bold(name)}.mdx already exists. Overwrite?`);
|
|
269
|
+
if (!ok) { styles.info('Aborted.'); return; }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fs.writeFileSync(file, mdx + '\n', 'utf-8');
|
|
273
|
+
console.log();
|
|
274
|
+
styles.ok(`Created ${styles.bold(name)} → ${styles.dim(path.relative(gitRoot, file))}`);
|
|
275
|
+
styles.info(`Use it in any docs page as ${styles.bold(`<${name} ... />`)}.`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── tiny readline confirm ───────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
function confirm(message) {
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
283
|
+
rl.question(`${styles.warn('?')} ${message} ${styles.dim('[y/N]')} `, (ans) => {
|
|
284
|
+
rl.close();
|
|
285
|
+
resolve(/^y(es)?$/i.test(ans.trim()));
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|