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 +19 -10
- package/bin/cli.js +351 -40
- package/lib/manifest.json +245 -6
- package/lib/post-init.mjs +102 -0
- package/lib/rr-schema.json +94 -0
- package/lib/rr.mjs +134 -0
- package/lib/skills.json +154 -0
- package/lib/starter/_README.md +37 -0
- package/lib/starter/_env.example +16 -0
- package/lib/starter/_gitignore +12 -0
- package/lib/starter/_package.json +39 -0
- package/lib/starter/app/globals.css +85 -0
- package/lib/starter/app/layout.tsx +23 -0
- package/lib/starter/app/page.tsx +26 -0
- package/lib/starter/components/convex-provider.tsx +13 -0
- package/lib/starter/components/ui/button.tsx +43 -0
- package/lib/starter/components.json +21 -0
- package/lib/starter/convex/auth.ts +6 -0
- package/lib/starter/convex/http.ts +7 -0
- package/lib/starter/convex/schema.ts +14 -0
- package/lib/starter/lib/utils.ts +6 -0
- package/lib/starter/next.config.mjs +16 -0
- package/lib/starter/postcss.config.mjs +3 -0
- package/lib/starter/proxy.ts +12 -0
- package/lib/starter/tsconfig.json +23 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# rahman-resources
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scaffolder + template installer for the [Rahman Resources kitab](https://github.com/rahmanef63/resource-site).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx rahman-resources
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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
|
|
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
|
|
74
|
-
npx rahman-resources
|
|
75
|
-
npx rahman-resources
|
|
76
|
-
npx rahman-resources
|
|
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
|
|
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>
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|