rahman-resources 0.2.1 → 0.4.1

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 CHANGED
@@ -1,18 +1,27 @@
1
1
  # rahman-resources
2
2
 
3
- Template installer for the [Rahman Resources kitab](https://github.com/rahmanef63/resource-site).
3
+ Scaffolder + template installer for the [Rahman Resources kitab](https://github.com/rahmanef63/resource-site).
4
4
 
5
- ## Usage
5
+ ## Quick start
6
6
 
7
7
  ```bash
8
- npx rahman-resources add <template> [target-dir]
9
- pnpm dlx rahman-resources add <template> [target-dir]
8
+ npx rahman-resources init my-app
9
+ cd my-app
10
+ cp .env.example .env.local # fill NEXT_PUBLIC_CONVEX_URL
11
+ npm install --legacy-peer-deps
12
+ npx convex dev --once # generates convex/_generated
13
+ npm run dev
10
14
  ```
11
15
 
12
- ### List templates
16
+ `init` ships a minimal Next 16 + React 19 + Convex + Tailwind 4 + shadcn/ui skeleton (~18 files). Then drop in any layout/recipe/feature with `add`.
17
+
18
+ ## Commands
13
19
 
14
20
  ```bash
15
- npx rahman-resources list
21
+ npx rahman-resources init <app-name> # scaffold fresh project
22
+ npx rahman-resources add <slug> [target-dir] # drop in a layout/recipe/feature
23
+ npx rahman-resources list [layouts|recipes|features]
24
+ npx rahman-resources info <slug>
16
25
  ```
17
26
 
18
27
  ### Inspect a template
@@ -24,11 +33,11 @@ npx rahman-resources info personal-brand-os
24
33
  ### Install into a project
25
34
 
26
35
  ```bash
27
- # new project
28
- mkdir my-app && cd my-app && pnpm init
29
- npx rahman-resources add personal-brand-os .
36
+ # fresh
37
+ npx rahman-resources init my-app
38
+ cd my-app && npx rahman-resources add personal-brand-os .
30
39
 
31
- # existing project
40
+ # existing
32
41
  cd existing-app
33
42
  npx rahman-resources add personal-brand-os .
34
43
  ```
package/bin/cli.js CHANGED
@@ -1,44 +1,72 @@
1
1
  #!/usr/bin/env node
2
2
  // rahman-resources — installer for the Rahman kitab.
3
3
  // Usage:
4
+ // npx rahman-resources init <app-name> [--template <slug>] [--features a,b] [--skills x,y]
4
5
  // npx rahman-resources add <slug> [target-dir]
5
- // npx rahman-resources list [layouts|recipes|features]
6
+ // npx rahman-resources add-skill <slug> [target-dir]
7
+ // npx rahman-resources list [layouts|recipes|features|skills]
6
8
  // npx rahman-resources info <slug>
9
+ // npx rahman-resources doctor
10
+ // npx rahman-resources mcp # not implemented in CLI; install @rahman-resources/mcp
7
11
 
8
12
  import { createRequire } from "node:module";
9
13
  import { spawn } from "node:child_process";
10
- import { existsSync } from "node:fs";
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
11
15
  import path from "node:path";
12
16
  import { fileURLToPath } from "node:url";
13
17
 
14
18
  import kleur from "kleur";
15
19
  import tiged from "tiged";
16
20
 
21
+ import {
22
+ readRr,
23
+ writeRr,
24
+ rrExists,
25
+ validateRr,
26
+ addFeature as rrAddFeature,
27
+ addSkill as rrAddSkill,
28
+ } from "../lib/rr.mjs";
29
+ import { runPostInit } from "../lib/post-init.mjs";
30
+
17
31
  const require = createRequire(import.meta.url);
18
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
33
  const manifest = require(path.join(__dirname, "../lib/manifest.json"));
34
+ const skillsInventory = require(path.join(__dirname, "../lib/skills.json"));
20
35
 
21
36
  const REPO = manifest.repo ?? "rahmanef63/resource-site";
22
37
  const BRANCH = manifest.branch ?? "main";
38
+ const SKILLS_REPO = "anthropics/skills";
23
39
 
24
40
  const KINDS = /** @type {const} */ (["layout", "recipe", "feature"]);
25
41
 
26
42
  const [, , cmd, ...rest] = process.argv;
27
43
 
28
- main().catch((err) => {
29
- console.error(kleur.red("✖"), err.message ?? err);
30
- process.exit(1);
31
- });
44
+ queueMicrotask(() =>
45
+ main().catch((err) => {
46
+ console.error(kleur.red("✖"), err.message ?? err);
47
+ process.exit(1);
48
+ }),
49
+ );
32
50
 
33
51
  async function main() {
34
52
  switch (cmd) {
53
+ case "init":
54
+ case "create":
55
+ case "new":
56
+ return runInit(rest);
35
57
  case "add":
36
58
  return runAdd(rest);
59
+ case "add-skill":
60
+ return runAddSkill(rest);
37
61
  case "list":
38
62
  case "ls":
39
63
  return runList(rest);
40
64
  case "info":
41
65
  return runInfo(rest);
66
+ case "doctor":
67
+ return runDoctor(rest);
68
+ case "mcp":
69
+ return runMcpHint();
42
70
  case undefined:
43
71
  case "-h":
44
72
  case "--help":
@@ -62,21 +90,58 @@ function printVersion() {
62
90
 
63
91
  function printHelp() {
64
92
  console.log(`
65
- ${kleur.bold("rahman-resources")} — install templates, recipes, and features
93
+ ${kleur.bold("rahman-resources")} — scaffold + install templates, recipes, features, Claude skills
66
94
 
67
95
  ${kleur.bold("Usage:")}
96
+ npx rahman-resources init <app-name> [--template <slug>] [--features a,b] [--skills x,y]
97
+ [--no-install] [--with-shadcn-reinit]
68
98
  npx rahman-resources add <slug> [target-dir]
69
- npx rahman-resources list [layouts|recipes|features]
99
+ npx rahman-resources add-skill <slug> [target-dir]
100
+ npx rahman-resources list [layouts|recipes|features|skills]
70
101
  npx rahman-resources info <slug>
102
+ npx rahman-resources doctor
103
+ npx rahman-resources mcp
104
+
105
+ ${kleur.bold("Init flags:")}
106
+ --no-install skip 'npm install' step (faster scaffolds; you run it manually)
107
+ --with-shadcn-reinit delete starter components.json + run 'npx shadcn init -y -d' (canonical shadcn flow)
71
108
 
72
109
  ${kleur.bold("Examples:")}
73
- npx rahman-resources add personal-brand-os my-app ${kleur.dim("# layout — pulls folders + installs deps")}
74
- npx rahman-resources add ai-sdk-openrouter my-app ${kleur.dim("# feature runs npm install")}
75
- npx rahman-resources add block-editor ${kleur.dim("# recipe — prints code + source URL")}
76
- npx rahman-resources list features
110
+ npx rahman-resources init my-app
111
+ npx rahman-resources init my-app --template personal-brand-os --skills frontend-design,mcp-builder
112
+ npx rahman-resources init my-app --no-install
113
+ npx rahman-resources add personal-brand-os .
114
+ npx rahman-resources add-skill webapp-testing
115
+ npx rahman-resources list skills
77
116
  `);
78
117
  }
79
118
 
119
+ // ─── flag parsing ─────────────────────────────────────────────────────────
120
+
121
+ function parseFlags(rest) {
122
+ const positional = [];
123
+ const flags = {};
124
+ for (let i = 0; i < rest.length; i++) {
125
+ const a = rest[i];
126
+ if (a.startsWith("--")) {
127
+ const key = a.slice(2);
128
+ const next = rest[i + 1];
129
+ if (next && !next.startsWith("--")) { flags[key] = next; i++; }
130
+ else flags[key] = true;
131
+ } else {
132
+ positional.push(a);
133
+ }
134
+ }
135
+ return { positional, flags };
136
+ }
137
+
138
+ function csv(s) {
139
+ if (!s || s === true) return [];
140
+ return String(s).split(",").map((x) => x.trim()).filter(Boolean);
141
+ }
142
+
143
+ // ─── catalog lookups ──────────────────────────────────────────────────────
144
+
80
145
  function findEntry(slug) {
81
146
  for (const kind of KINDS) {
82
147
  const list = manifest[kind + "s"];
@@ -86,24 +151,49 @@ function findEntry(slug) {
86
151
  return null;
87
152
  }
88
153
 
154
+ function findSkill(slug) {
155
+ return skillsInventory.skills.find((s) => s.slug === slug) ?? null;
156
+ }
157
+
158
+ // ─── list / info ──────────────────────────────────────────────────────────
159
+
89
160
  function runList([filter]) {
90
- const groups = filter ? [filter] : ["layouts", "recipes", "features"];
161
+ const groups = filter
162
+ ? [filter]
163
+ : ["layouts", "recipes", "features", "skills"];
91
164
  for (const g of groups) {
165
+ if (g === "skills") {
166
+ console.log(`\n${kleur.bold("SKILLS")} ${kleur.dim(`(${skillsInventory.skills.length})`)}\n`);
167
+ for (const s of skillsInventory.skills) {
168
+ console.log(` ${kleur.cyan(s.slug.padEnd(28))} ${kleur.dim((s.category ?? "").padEnd(12))} ${s.title}`);
169
+ }
170
+ continue;
171
+ }
92
172
  const list = manifest[g];
93
173
  if (!list || list.length === 0) continue;
94
174
  console.log(`\n${kleur.bold(g.toUpperCase())} ${kleur.dim(`(${list.length})`)}\n`);
95
175
  for (const t of list) {
96
176
  const cat = (t.category ?? t.source ?? "").padEnd(20).slice(0, 20);
97
- console.log(
98
- ` ${kleur.cyan(t.slug.padEnd(30))} ${kleur.dim(cat)} ${t.title}`,
99
- );
177
+ console.log(` ${kleur.cyan(t.slug.padEnd(30))} ${kleur.dim(cat)} ${t.title}`);
100
178
  }
101
179
  }
102
- console.log(`\nRun ${kleur.cyan("info <slug>")} for detail, ${kleur.cyan("add <slug> [target]")} to install.\n`);
180
+ console.log(`\nRun ${kleur.cyan("info <slug>")} for detail, ${kleur.cyan("add <slug>")} or ${kleur.cyan("add-skill <slug>")} to install.\n`);
103
181
  }
104
182
 
105
183
  function runInfo([slug]) {
106
184
  if (!slug) throw new Error("Usage: rahman-resources info <slug>");
185
+ const skill = findSkill(slug);
186
+ if (skill) {
187
+ console.log(`
188
+ ${kleur.bold(skill.title)} ${kleur.dim("[skill]")} ${kleur.dim(skill.category)}
189
+
190
+ ${skill.description}
191
+
192
+ ${kleur.bold("Source:")} ${skill.source}/${skill.path}
193
+ ${kleur.bold("Install:")} ${kleur.cyan(`npx rahman-resources add-skill ${skill.slug}`)}
194
+ `);
195
+ return;
196
+ }
107
197
  const found = findEntry(slug);
108
198
  if (!found) throw new Error(`Slug not found: ${slug}. Run 'list' to see all.`);
109
199
  const { kind, entry: t } = found;
@@ -113,7 +203,6 @@ ${kleur.bold(t.title)} ${kleur.dim(`[${kind}]`)} ${kleur.dim(t.category ?? "")
113
203
 
114
204
  ${t.description}
115
205
  `);
116
-
117
206
  if (kind === "layout") {
118
207
  console.log(`${kleur.bold("Pulls:")}`);
119
208
  console.log(t.pullPaths.map((p) => ` · ${p}`).join("\n") || " (none)");
@@ -131,11 +220,152 @@ ${t.description}
131
220
  console.log(`${kleur.bold("Files:")}`);
132
221
  console.log(t.files.map((f) => ` · ${f}`).join("\n"));
133
222
  }
134
-
135
223
  if (t.docsUrl) console.log(`\n${kleur.dim(`Docs: ${t.docsUrl}`)}`);
136
224
  console.log(`${kleur.dim(`Source: ${t.source ?? "—"}`)}\n`);
137
225
  }
138
226
 
227
+ // ─── starter copy ─────────────────────────────────────────────────────────
228
+
229
+ const STARTER_RENAME_PAIRS = [
230
+ ["_package", "package"],
231
+ ["_gitignore", ".gitignore"],
232
+ ["_env", ".env"],
233
+ ["_README", "README"],
234
+ ];
235
+
236
+ function renameStarterFile(name) {
237
+ for (const pair of STARTER_RENAME_PAIRS) {
238
+ const f = pair[0];
239
+ const t = pair[1];
240
+ if (name === f || name.startsWith(f + ".")) return t + name.slice(f.length);
241
+ }
242
+ return name;
243
+ }
244
+
245
+ function copyStarterTree(src, dest, appName, slug) {
246
+ mkdirSync(dest, { recursive: true });
247
+ for (const entry of readdirSync(src)) {
248
+ const sFull = path.join(src, entry);
249
+ const dEntry = renameStarterFile(entry);
250
+ const dFull = path.join(dest, dEntry);
251
+ const stat = statSync(sFull);
252
+ if (stat.isDirectory()) {
253
+ copyStarterTree(sFull, dFull, appName, slug);
254
+ } else {
255
+ let body = readFileSync(sFull, "utf8");
256
+ body = body.replaceAll("__APP_NAME__", appName).replaceAll("__APP_SLUG__", slug);
257
+ writeFileSync(dFull, body);
258
+ }
259
+ }
260
+ }
261
+
262
+ // ─── init ─────────────────────────────────────────────────────────────────
263
+
264
+ async function runInit(rest) {
265
+ const { positional, flags } = parseFlags(rest);
266
+ const [appName] = positional;
267
+ if (!appName || appName.startsWith("-")) {
268
+ throw new Error("Usage: rahman-resources init <app-name> [--template slug] [--features a,b] [--skills x,y]");
269
+ }
270
+ const slug = appName.replace(/[^a-z0-9-_]/gi, "-").toLowerCase();
271
+ const target = path.resolve(process.cwd(), appName);
272
+ if (existsSync(target)) {
273
+ throw new Error(`Directory already exists: ${target}`);
274
+ }
275
+
276
+ const features = csv(flags.features);
277
+ const skills = csv(flags.skills);
278
+ const template = typeof flags.template === "string" ? flags.template : null;
279
+
280
+ if (template && !findEntry(template)) {
281
+ throw new Error(`Unknown template: ${template}. Run 'list layouts' to see available.`);
282
+ }
283
+ for (const s of skills) {
284
+ if (!findSkill(s)) throw new Error(`Unknown skill: ${s}. Run 'list skills' to see available.`);
285
+ }
286
+ for (const f of features) {
287
+ if (!findEntry(f)) throw new Error(`Unknown feature: ${f}. Run 'list features' to see available.`);
288
+ }
289
+
290
+ console.log(kleur.bold(`\n→ Scaffolding ${kleur.cyan(slug)} (Next 16 + Convex + shadcn)\n`));
291
+
292
+ const starter = path.join(__dirname, "../lib/starter");
293
+ if (!existsSync(starter)) throw new Error(`Starter not found at ${starter}`);
294
+
295
+ process.stdout.write(` copying starter ... `);
296
+ copyStarterTree(starter, target, appName, slug);
297
+ console.log(kleur.green("ok"));
298
+
299
+ const skipInstall = !!flags["no-install"];
300
+ const reinitShadcn = !!flags["with-shadcn-reinit"];
301
+
302
+ if (!skipInstall) {
303
+ console.log(kleur.bold(`\n→ Installing dependencies (npm install --legacy-peer-deps)\n`));
304
+ try {
305
+ await runShell("npm", ["install", "--legacy-peer-deps"], target);
306
+ } catch (err) {
307
+ console.log(kleur.yellow(` ⚠ npm install failed (${err.message}). You can rerun manually.`));
308
+ }
309
+ } else {
310
+ console.log(kleur.dim(`\n (skipping npm install — --no-install)`));
311
+ }
312
+
313
+ if (reinitShadcn && !skipInstall) {
314
+ console.log(kleur.bold(`\n→ Re-running shadcn init (--with-shadcn-reinit)\n`));
315
+ try {
316
+ // Remove pre-baked components.json so shadcn writes a fresh one — post-init re-applies our aliases.
317
+ const cjPath = path.join(target, "components.json");
318
+ if (existsSync(cjPath)) writeFileSync(cjPath + ".bak", readFileSync(cjPath, "utf8"));
319
+ await runShell("npx", ["shadcn@latest", "init", "--yes", "--defaults"], target);
320
+ } catch (err) {
321
+ console.log(kleur.yellow(` ⚠ shadcn init failed (${err.message}). Continuing.`));
322
+ }
323
+ } else if (!skipInstall) {
324
+ console.log(kleur.dim(`\n (skipping shadcn init — starter already pre-configured. Pass --with-shadcn-reinit to force re-init.)`));
325
+ }
326
+
327
+ process.stdout.write(`\n post-init restructure ... `);
328
+ const post = runPostInit(target, { template, features, skills });
329
+ console.log(kleur.green("ok"));
330
+ for (const c of post.changed) console.log(` ${kleur.green("+")} ${c}`);
331
+ for (const s of post.skipped) console.log(` ${kleur.dim("-")} ${kleur.dim(s)}`);
332
+
333
+ if (template) {
334
+ console.log(kleur.bold(`\n→ Pulling template ${kleur.cyan(template)}\n`));
335
+ const t = findEntry(template).entry;
336
+ for (const p of t.pullPaths ?? []) {
337
+ const dest = path.join(target, p);
338
+ process.stdout.write(` ${kleur.dim(p)} ... `);
339
+ await pull(p, dest);
340
+ console.log(kleur.green("ok"));
341
+ }
342
+ }
343
+
344
+ if (skills.length) {
345
+ console.log(kleur.bold(`\n→ Pulling ${skills.length} Claude skill(s)\n`));
346
+ for (const s of skills) await installSkill(s, target);
347
+ }
348
+
349
+ console.log(`\n${kleur.green("✓")} Done. ${kleur.bold(slug)} scaffolded.\n`);
350
+ console.log(`${kleur.bold("Next:")}`);
351
+ console.log(` cd ${appName}`);
352
+ console.log(` cp .env.example .env.local ${kleur.dim("# fill NEXT_PUBLIC_CONVEX_URL")}`);
353
+ if (skipInstall) console.log(` npm install --legacy-peer-deps`);
354
+ console.log(` npx convex dev --once ${kleur.dim("# generates convex/_generated")}`);
355
+ console.log(` npm run dev\n`);
356
+ }
357
+
358
+ // Spawn a child process inheriting stdio, resolves on exit 0.
359
+ function runShell(cmd, args, cwd) {
360
+ return new Promise((resolve, reject) => {
361
+ const ps = spawn(cmd, args, { cwd, stdio: "inherit", shell: true });
362
+ ps.on("error", reject);
363
+ ps.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
364
+ });
365
+ }
366
+
367
+ // ─── add (template / feature / recipe) ────────────────────────────────────
368
+
139
369
  async function runAdd([slug, targetArg = "."]) {
140
370
  if (!slug) {
141
371
  console.error(kleur.red("Missing slug."));
@@ -143,11 +373,7 @@ async function runAdd([slug, targetArg = "."]) {
143
373
  process.exit(1);
144
374
  }
145
375
  const found = findEntry(slug);
146
- if (!found) {
147
- throw new Error(
148
- `"${slug}" not found. Run ${kleur.cyan("npx rahman-resources list")}.`,
149
- );
150
- }
376
+ if (!found) throw new Error(`"${slug}" not found. Run ${kleur.cyan("npx rahman-resources list")}.`);
151
377
  const { kind, entry } = found;
152
378
  const target = path.resolve(process.cwd(), targetArg);
153
379
 
@@ -179,6 +405,12 @@ async function addLayout(t, target, targetArg) {
179
405
  }
180
406
  }
181
407
 
408
+ if (rrExists(target)) {
409
+ const rr = readRr(target);
410
+ rr.template = { slug: t.slug, version: "main" };
411
+ writeRr(rr, target);
412
+ }
413
+
182
414
  console.log(`\n${kleur.green("✓")} Done. ${kleur.bold(t.title)} installed.`);
183
415
  if (t.agentRecipe) console.log(`\n${kleur.bold("Next:")}\n${indent(t.agentRecipe, 2)}\n`);
184
416
  }
@@ -198,11 +430,15 @@ async function addFeature(t, target, targetArg) {
198
430
  }
199
431
  }
200
432
 
201
- console.log(`\n${kleur.green("✓")} Feature added: ${kleur.bold(t.title)}`);
202
- if (t.exampleCode) {
203
- console.log(`\n${kleur.bold("Example:")}`);
204
- console.log(indent(t.exampleCode, 2));
433
+ if (rrExists(target)) {
434
+ const rr = readRr(target);
435
+ rrAddFeature(rr, t.slug);
436
+ writeRr(rr, target);
437
+ console.log(kleur.dim(` rr.json: features += ${t.slug}`));
205
438
  }
439
+
440
+ console.log(`\n${kleur.green("✓")} Feature added: ${kleur.bold(t.title)}`);
441
+ if (t.exampleCode) console.log(`\n${kleur.bold("Example:")}\n${indent(t.exampleCode, 2)}`);
206
442
  if (t.agentRecipe) console.log(`\n${kleur.bold("Wire-up:")}\n${indent(t.agentRecipe, 2)}\n`);
207
443
  if (t.docsUrl) console.log(`\n${kleur.dim(`Docs: ${t.docsUrl}`)}\n`);
208
444
  }
@@ -213,20 +449,97 @@ function addRecipe(t) {
213
449
  console.log(`\n${kleur.bold("Source:")} ${t.source}`);
214
450
  console.log(`\n${kleur.bold("Files to port:")}`);
215
451
  console.log(t.files.map((f) => ` · ${f}`).join("\n"));
216
- if (t.exampleCode) {
217
- console.log(`\n${kleur.bold("Example:")}`);
218
- console.log(indent(t.exampleCode, 2));
219
- }
452
+ if (t.exampleCode) console.log(`\n${kleur.bold("Example:")}\n${indent(t.exampleCode, 2)}`);
220
453
  if (t.agentRecipe) console.log(`\n${kleur.bold("Wire-up:")}\n${indent(t.agentRecipe, 2)}\n`);
221
454
  console.log(kleur.dim(`\n(Recipes are educational patterns — copy from source repo into your project manually.)\n`));
222
455
  }
223
456
 
457
+ // ─── add-skill ────────────────────────────────────────────────────────────
458
+
459
+ async function runAddSkill([slug, targetArg = "."]) {
460
+ if (!slug) throw new Error("Usage: rahman-resources add-skill <slug>");
461
+ const skill = findSkill(slug);
462
+ if (!skill) throw new Error(`Unknown skill: ${slug}. Run 'list skills' to see available.`);
463
+ const target = path.resolve(process.cwd(), targetArg);
464
+ await installSkill(slug, target);
465
+
466
+ if (rrExists(target)) {
467
+ const rr = readRr(target);
468
+ rrAddSkill(rr, slug, skill.source);
469
+ writeRr(rr, target);
470
+ console.log(kleur.dim(` rr.json: skills += ${slug}`));
471
+ }
472
+
473
+ console.log(`\n${kleur.green("✓")} Skill installed: ${kleur.bold(skill.title)}`);
474
+ console.log(kleur.dim(` Location: .claude/skills/${slug}/`));
475
+ }
476
+
477
+ async function installSkill(slug, target) {
478
+ const skill = findSkill(slug);
479
+ if (!skill) throw new Error(`Unknown skill: ${slug}`);
480
+ const dest = path.join(target, ".claude", "skills", slug);
481
+ process.stdout.write(` ${kleur.cyan(slug.padEnd(20))} ${kleur.dim(`→ .claude/skills/${slug}/`)} ... `);
482
+ if (skill.source === "anthropics") {
483
+ const emitter = tiged(`${SKILLS_REPO}/${skill.path}`, { cache: false, force: true, verbose: false });
484
+ await emitter.clone(dest);
485
+ } else if (skill.source === "rahman") {
486
+ // Future: ship rahman-authored skills inside this repo. For now, scaffold a stub.
487
+ mkdirSync(dest, { recursive: true });
488
+ writeFileSync(
489
+ path.join(dest, "SKILL.md"),
490
+ `---\nname: ${skill.slug}\ndescription: ${skill.description}\n---\n\n# ${skill.title}\n\nStub — to be filled by rahman-resources kitab.\n`,
491
+ );
492
+ } else {
493
+ throw new Error(`Unsupported skill source: ${skill.source}`);
494
+ }
495
+ console.log(kleur.green("ok"));
496
+ }
497
+
498
+ // ─── doctor ───────────────────────────────────────────────────────────────
499
+
500
+ function runDoctor() {
501
+ const target = process.cwd();
502
+ if (!rrExists(target)) {
503
+ console.log(kleur.yellow("⚠ No rr.json found in cwd. Run 'rahman-resources init <app>' first."));
504
+ process.exit(1);
505
+ }
506
+ const rr = readRr(target);
507
+ const issues = validateRr(rr);
508
+ if (issues.length === 0) {
509
+ console.log(kleur.green("✓ rr.json is valid."));
510
+ console.log(` template: ${kleur.cyan(rr.template?.slug ?? "(none)")}`);
511
+ console.log(` features: ${kleur.cyan(rr.features?.length ?? 0)}`);
512
+ console.log(` skills: ${kleur.cyan(rr.skills?.length ?? 0)}`);
513
+ return;
514
+ }
515
+ console.log(kleur.red(`✖ rr.json has ${issues.length} issue(s):`));
516
+ for (const i of issues) console.log(` · ${i}`);
517
+ process.exit(1);
518
+ }
519
+
520
+ function runMcpHint() {
521
+ console.log(`
522
+ ${kleur.bold("Rahman Resources MCP server")}
523
+
524
+ Install + wire it into your Claude Code / Cursor config:
525
+
526
+ ${kleur.cyan(`{
527
+ "mcpServers": {
528
+ "rahman-resources": {
529
+ "command": "npx",
530
+ "args": ["-y", "@rahman-resources/mcp"]
531
+ }
532
+ }
533
+ }`)}
534
+
535
+ Then in Claude Code: ${kleur.cyan("/mcp")} to see available rr_* tools.
536
+ `);
537
+ }
538
+
539
+ // ─── helpers ──────────────────────────────────────────────────────────────
540
+
224
541
  async function pull(repoPath, dest) {
225
- const emitter = tiged(`${REPO}/${repoPath}#${BRANCH}`, {
226
- cache: false,
227
- force: true,
228
- verbose: false,
229
- });
542
+ const emitter = tiged(`${REPO}/${repoPath}#${BRANCH}`, { cache: false, force: true, verbose: false });
230
543
  await emitter.clone(dest);
231
544
  }
232
545
 
@@ -246,9 +559,7 @@ function runPM(pm, deps, cwd) {
246
559
  return new Promise((resolve, reject) => {
247
560
  const ps = spawn(pm, args, { cwd, stdio: "inherit", shell: true });
248
561
  ps.on("error", reject);
249
- ps.on("exit", (code) =>
250
- code === 0 ? resolve() : reject(new Error(`${pm} ${args[0]} exited ${code}`)),
251
- );
562
+ ps.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`${pm} ${args[0]} exited ${code}`))));
252
563
  });
253
564
  }
254
565