rahman-resources 0.3.0 → 0.4.2

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/bin/cli.js CHANGED
@@ -1,10 +1,13 @@
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> [--lite]
4
+ // npx rahman-resources init <app-name> [--template <slug>] [--features a,b] [--skills x,y]
5
5
  // npx rahman-resources add <slug> [target-dir]
6
- // 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]
7
8
  // npx rahman-resources info <slug>
9
+ // npx rahman-resources doctor
10
+ // npx rahman-resources mcp # not implemented in CLI; install rahman-resources-mcp
8
11
 
9
12
  import { createRequire } from "node:module";
10
13
  import { spawn } from "node:child_process";
@@ -15,19 +18,29 @@ import { fileURLToPath } from "node:url";
15
18
  import kleur from "kleur";
16
19
  import tiged from "tiged";
17
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
+
18
31
  const require = createRequire(import.meta.url);
19
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
33
  const manifest = require(path.join(__dirname, "../lib/manifest.json"));
34
+ const skillsInventory = require(path.join(__dirname, "../lib/skills.json"));
21
35
 
22
36
  const REPO = manifest.repo ?? "rahmanef63/resource-site";
23
37
  const BRANCH = manifest.branch ?? "main";
38
+ const SKILLS_REPO = "anthropics/skills";
24
39
 
25
40
  const KINDS = /** @type {const} */ (["layout", "recipe", "feature"]);
26
41
 
27
42
  const [, , cmd, ...rest] = process.argv;
28
43
 
29
- // Defer to next tick so all module-level const declarations finish initializing
30
- // before the dispatch reaches functions that reference them.
31
44
  queueMicrotask(() =>
32
45
  main().catch((err) => {
33
46
  console.error(kleur.red("✖"), err.message ?? err);
@@ -43,11 +56,17 @@ async function main() {
43
56
  return runInit(rest);
44
57
  case "add":
45
58
  return runAdd(rest);
59
+ case "add-skill":
60
+ return runAddSkill(rest);
46
61
  case "list":
47
62
  case "ls":
48
63
  return runList(rest);
49
64
  case "info":
50
65
  return runInfo(rest);
66
+ case "doctor":
67
+ return runDoctor(rest);
68
+ case "mcp":
69
+ return runMcpHint();
51
70
  case undefined:
52
71
  case "-h":
53
72
  case "--help":
@@ -71,23 +90,58 @@ function printVersion() {
71
90
 
72
91
  function printHelp() {
73
92
  console.log(`
74
- ${kleur.bold("rahman-resources")} — scaffold + install templates, recipes, features
93
+ ${kleur.bold("rahman-resources")} — scaffold + install templates, recipes, features, Claude skills
75
94
 
76
95
  ${kleur.bold("Usage:")}
77
- npx rahman-resources init <app-name> [--lite]
96
+ npx rahman-resources init <app-name> [--template <slug>] [--features a,b] [--skills x,y]
97
+ [--no-install] [--with-shadcn-reinit]
78
98
  npx rahman-resources add <slug> [target-dir]
79
- 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]
80
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)
81
108
 
82
109
  ${kleur.bold("Examples:")}
83
- npx rahman-resources init my-app --lite ${kleur.dim("# scaffold from template-base, prune notion+builder+communications")}
84
- npx rahman-resources add personal-brand-os my-app ${kleur.dim("# layout — pulls folders + installs deps")}
85
- npx rahman-resources add ai-sdk-openrouter my-app ${kleur.dim("# feature — runs npm install")}
86
- npx rahman-resources add block-editor ${kleur.dim("# recipe — prints code + source URL")}
87
- 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
88
116
  `);
89
117
  }
90
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
+
91
145
  function findEntry(slug) {
92
146
  for (const kind of KINDS) {
93
147
  const list = manifest[kind + "s"];
@@ -97,24 +151,49 @@ function findEntry(slug) {
97
151
  return null;
98
152
  }
99
153
 
154
+ function findSkill(slug) {
155
+ return skillsInventory.skills.find((s) => s.slug === slug) ?? null;
156
+ }
157
+
158
+ // ─── list / info ──────────────────────────────────────────────────────────
159
+
100
160
  function runList([filter]) {
101
- const groups = filter ? [filter] : ["layouts", "recipes", "features"];
161
+ const groups = filter
162
+ ? [filter]
163
+ : ["layouts", "recipes", "features", "skills"];
102
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
+ }
103
172
  const list = manifest[g];
104
173
  if (!list || list.length === 0) continue;
105
174
  console.log(`\n${kleur.bold(g.toUpperCase())} ${kleur.dim(`(${list.length})`)}\n`);
106
175
  for (const t of list) {
107
176
  const cat = (t.category ?? t.source ?? "").padEnd(20).slice(0, 20);
108
- console.log(
109
- ` ${kleur.cyan(t.slug.padEnd(30))} ${kleur.dim(cat)} ${t.title}`,
110
- );
177
+ console.log(` ${kleur.cyan(t.slug.padEnd(30))} ${kleur.dim(cat)} ${t.title}`);
111
178
  }
112
179
  }
113
- 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`);
114
181
  }
115
182
 
116
183
  function runInfo([slug]) {
117
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
+ }
118
197
  const found = findEntry(slug);
119
198
  if (!found) throw new Error(`Slug not found: ${slug}. Run 'list' to see all.`);
120
199
  const { kind, entry: t } = found;
@@ -124,7 +203,6 @@ ${kleur.bold(t.title)} ${kleur.dim(`[${kind}]`)} ${kleur.dim(t.category ?? "")
124
203
 
125
204
  ${t.description}
126
205
  `);
127
-
128
206
  if (kind === "layout") {
129
207
  console.log(`${kleur.bold("Pulls:")}`);
130
208
  console.log(t.pullPaths.map((p) => ` · ${p}`).join("\n") || " (none)");
@@ -142,13 +220,12 @@ ${t.description}
142
220
  console.log(`${kleur.bold("Files:")}`);
143
221
  console.log(t.files.map((f) => ` · ${f}`).join("\n"));
144
222
  }
145
-
146
223
  if (t.docsUrl) console.log(`\n${kleur.dim(`Docs: ${t.docsUrl}`)}`);
147
224
  console.log(`${kleur.dim(`Source: ${t.source ?? "—"}`)}\n`);
148
225
  }
149
226
 
150
- // Files in lib/starter prefixed with `_` get renamed on copy.
151
- // Done because npm pack filters .gitignore and warns on nested package.json.
227
+ // ─── starter copy ─────────────────────────────────────────────────────────
228
+
152
229
  const STARTER_RENAME_PAIRS = [
153
230
  ["_package", "package"],
154
231
  ["_gitignore", ".gitignore"],
@@ -182,9 +259,13 @@ function copyStarterTree(src, dest, appName, slug) {
182
259
  }
183
260
  }
184
261
 
185
- async function runInit([appName, ...flags]) {
262
+ // ─── init ─────────────────────────────────────────────────────────────────
263
+
264
+ async function runInit(rest) {
265
+ const { positional, flags } = parseFlags(rest);
266
+ const [appName] = positional;
186
267
  if (!appName || appName.startsWith("-")) {
187
- throw new Error("Usage: rahman-resources init <app-name>");
268
+ throw new Error("Usage: rahman-resources init <app-name> [--template slug] [--features a,b] [--skills x,y]");
188
269
  }
189
270
  const slug = appName.replace(/[^a-z0-9-_]/gi, "-").toLowerCase();
190
271
  const target = path.resolve(process.cwd(), appName);
@@ -192,34 +273,99 @@ async function runInit([appName, ...flags]) {
192
273
  throw new Error(`Directory already exists: ${target}`);
193
274
  }
194
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
+
195
290
  console.log(kleur.bold(`\n→ Scaffolding ${kleur.cyan(slug)} (Next 16 + Convex + shadcn)\n`));
196
291
 
197
292
  const starter = path.join(__dirname, "../lib/starter");
198
- if (!existsSync(starter)) {
199
- throw new Error(`Starter not found at ${starter}`);
200
- }
293
+ if (!existsSync(starter)) throw new Error(`Starter not found at ${starter}`);
201
294
 
202
295
  process.stdout.write(` copying starter ... `);
203
296
  copyStarterTree(starter, target, appName, slug);
204
297
  console.log(kleur.green("ok"));
205
298
 
206
- // Ignore --lite flag for now (no template-base pull yet — starter is minimal already).
207
- if (flags.includes("--lite")) {
208
- console.log(kleur.dim(` (--lite is a no-op — starter is already minimal)`));
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);
209
347
  }
210
348
 
211
349
  console.log(`\n${kleur.green("✓")} Done. ${kleur.bold(slug)} scaffolded.\n`);
212
350
  console.log(`${kleur.bold("Next:")}`);
213
351
  console.log(` cd ${appName}`);
214
352
  console.log(` cp .env.example .env.local ${kleur.dim("# fill NEXT_PUBLIC_CONVEX_URL")}`);
215
- console.log(` npm install --legacy-peer-deps`);
353
+ if (skipInstall) console.log(` npm install --legacy-peer-deps`);
216
354
  console.log(` npx convex dev --once ${kleur.dim("# generates convex/_generated")}`);
217
355
  console.log(` npm run dev\n`);
218
- console.log(
219
- `${kleur.dim("Then drop in a layout:")} ${kleur.cyan("npx rahman-resources add personal-brand-os .")}\n`,
220
- );
221
356
  }
222
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
+
223
369
  async function runAdd([slug, targetArg = "."]) {
224
370
  if (!slug) {
225
371
  console.error(kleur.red("Missing slug."));
@@ -227,11 +373,7 @@ async function runAdd([slug, targetArg = "."]) {
227
373
  process.exit(1);
228
374
  }
229
375
  const found = findEntry(slug);
230
- if (!found) {
231
- throw new Error(
232
- `"${slug}" not found. Run ${kleur.cyan("npx rahman-resources list")}.`,
233
- );
234
- }
376
+ if (!found) throw new Error(`"${slug}" not found. Run ${kleur.cyan("npx rahman-resources list")}.`);
235
377
  const { kind, entry } = found;
236
378
  const target = path.resolve(process.cwd(), targetArg);
237
379
 
@@ -263,6 +405,12 @@ async function addLayout(t, target, targetArg) {
263
405
  }
264
406
  }
265
407
 
408
+ if (rrExists(target)) {
409
+ const rr = readRr(target);
410
+ rr.template = { slug: t.slug, version: "main" };
411
+ writeRr(rr, target);
412
+ }
413
+
266
414
  console.log(`\n${kleur.green("✓")} Done. ${kleur.bold(t.title)} installed.`);
267
415
  if (t.agentRecipe) console.log(`\n${kleur.bold("Next:")}\n${indent(t.agentRecipe, 2)}\n`);
268
416
  }
@@ -282,11 +430,15 @@ async function addFeature(t, target, targetArg) {
282
430
  }
283
431
  }
284
432
 
285
- console.log(`\n${kleur.green("✓")} Feature added: ${kleur.bold(t.title)}`);
286
- if (t.exampleCode) {
287
- console.log(`\n${kleur.bold("Example:")}`);
288
- 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}`));
289
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)}`);
290
442
  if (t.agentRecipe) console.log(`\n${kleur.bold("Wire-up:")}\n${indent(t.agentRecipe, 2)}\n`);
291
443
  if (t.docsUrl) console.log(`\n${kleur.dim(`Docs: ${t.docsUrl}`)}\n`);
292
444
  }
@@ -297,20 +449,97 @@ function addRecipe(t) {
297
449
  console.log(`\n${kleur.bold("Source:")} ${t.source}`);
298
450
  console.log(`\n${kleur.bold("Files to port:")}`);
299
451
  console.log(t.files.map((f) => ` · ${f}`).join("\n"));
300
- if (t.exampleCode) {
301
- console.log(`\n${kleur.bold("Example:")}`);
302
- console.log(indent(t.exampleCode, 2));
303
- }
452
+ if (t.exampleCode) console.log(`\n${kleur.bold("Example:")}\n${indent(t.exampleCode, 2)}`);
304
453
  if (t.agentRecipe) console.log(`\n${kleur.bold("Wire-up:")}\n${indent(t.agentRecipe, 2)}\n`);
305
454
  console.log(kleur.dim(`\n(Recipes are educational patterns — copy from source repo into your project manually.)\n`));
306
455
  }
307
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
+
308
541
  async function pull(repoPath, dest) {
309
- const emitter = tiged(`${REPO}/${repoPath}#${BRANCH}`, {
310
- cache: false,
311
- force: true,
312
- verbose: false,
313
- });
542
+ const emitter = tiged(`${REPO}/${repoPath}#${BRANCH}`, { cache: false, force: true, verbose: false });
314
543
  await emitter.clone(dest);
315
544
  }
316
545
 
@@ -330,9 +559,7 @@ function runPM(pm, deps, cwd) {
330
559
  return new Promise((resolve, reject) => {
331
560
  const ps = spawn(pm, args, { cwd, stdio: "inherit", shell: true });
332
561
  ps.on("error", reject);
333
- ps.on("exit", (code) =>
334
- code === 0 ? resolve() : reject(new Error(`${pm} ${args[0]} exited ${code}`)),
335
- );
562
+ ps.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`${pm} ${args[0]} exited ${code}`))));
336
563
  });
337
564
  }
338
565
 
package/lib/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 2,
3
- "generatedAt": "2026-05-05T12:19:53.839Z",
3
+ "generatedAt": "2026-05-06T09:34:49.775Z",
4
4
  "repo": "rahmanef63/resource-site",
5
5
  "branch": "main",
6
6
  "layouts": [
@@ -13,6 +13,7 @@
13
13
  "repoPath": "app/preview/personal-brand-os",
14
14
  "pullPaths": [
15
15
  "app/preview/personal-brand-os",
16
+ "components/templates/_shared",
16
17
  "components/templates/personal-brand",
17
18
  "convex/templates/personal-brand-os"
18
19
  ],
@@ -48,14 +49,24 @@
48
49
  "app/preview/personal-brand-os/admin/settings/site/page.tsx",
49
50
  "app/preview/personal-brand-os/admin/settings/team/page.tsx",
50
51
  "app/preview/personal-brand-os/admin/settings/ai/page.tsx",
52
+ "components/templates/_shared/index.ts",
53
+ "components/templates/_shared/types/common.ts",
54
+ "components/templates/_shared/utils/index.ts",
55
+ "components/templates/_shared/hooks/create-template-store.tsx",
56
+ "components/templates/_shared/ui/section-head.tsx",
57
+ "components/templates/_shared/ui/site-nav.tsx",
58
+ "components/templates/_shared/ui/site-footer.tsx",
59
+ "components/templates/_shared/ui/site-shell.tsx",
60
+ "components/templates/_shared/ui/admin-sidebar.tsx",
61
+ "components/templates/_shared/ui/admin-topbar.tsx",
62
+ "components/templates/_shared/ui/admin-shell.tsx",
63
+ "components/templates/_shared/ui/stat-card.tsx",
51
64
  "components/templates/personal-brand/shared/types.ts",
52
65
  "components/templates/personal-brand/shared/store.tsx",
53
66
  "components/templates/personal-brand/shared/seed.ts",
54
67
  "components/templates/personal-brand/shared/site-config.ts",
55
- "components/templates/personal-brand/shared/ui/site-nav.tsx",
56
- "components/templates/personal-brand/shared/ui/site-footer.tsx",
68
+ "components/templates/personal-brand/shared/nav-config.ts",
57
69
  "components/templates/personal-brand/shared/ui/chat-fab.tsx",
58
- "components/templates/personal-brand/shared/ui/section-head.tsx",
59
70
  "components/templates/personal-brand/slices/home/HomePage.tsx",
60
71
  "components/templates/personal-brand/slices/home/NewsletterBlock.tsx",
61
72
  "components/templates/personal-brand/slices/blog/BlogList.tsx",
@@ -66,8 +77,7 @@
66
77
  "components/templates/personal-brand/slices/resources/ResourcesPage.tsx",
67
78
  "components/templates/personal-brand/slices/about/AboutPage.tsx",
68
79
  "components/templates/personal-brand/slices/contact/ContactPage.tsx",
69
- "components/templates/personal-brand/slices/admin/shell/admin-sidebar.tsx",
70
- "components/templates/personal-brand/slices/admin/shell/admin-topbar.tsx",
80
+ "app/preview/personal-brand-os/admin/admin-shell-client.tsx",
71
81
  "components/templates/personal-brand/slices/admin/dashboard/DashboardView.tsx",
72
82
  "components/templates/personal-brand/slices/admin/posts/PostsList.tsx",
73
83
  "components/templates/personal-brand/slices/admin/posts/PostEditor.tsx",
@@ -124,6 +134,94 @@
124
134
  ],
125
135
  "primaryFile": "app/preview/personal-brand-os/public/page.tsx"
126
136
  },
137
+ {
138
+ "slug": "agency-studio-os",
139
+ "title": "Agency Studio OS",
140
+ "category": "website-template",
141
+ "description": "Full-app B2B agency / studio site — public (home, services, work + project detail, about, contact) + admin (dashboard, projects pipeline, clients, services, leads, settings). Inspired by saudivisuals.com + cescadesigns.",
142
+ "source": "saudivisuals.com + cescadesigns",
143
+ "repoPath": "app/preview/agency-studio-os",
144
+ "pullPaths": [
145
+ "app/preview/agency-studio-os",
146
+ "components/templates/_shared",
147
+ "components/templates/agency-studio",
148
+ "convex/templates/agency-studio-os"
149
+ ],
150
+ "files": [
151
+ "app/preview/agency-studio-os/robots.ts",
152
+ "app/preview/agency-studio-os/sitemap.ts",
153
+ "app/preview/agency-studio-os/opengraph-image.tsx",
154
+ "app/preview/agency-studio-os/public/layout.tsx",
155
+ "app/preview/agency-studio-os/public/page.tsx",
156
+ "app/preview/agency-studio-os/public/services/page.tsx",
157
+ "app/preview/agency-studio-os/public/portfolio/page.tsx",
158
+ "app/preview/agency-studio-os/public/portfolio/[slug]/page.tsx",
159
+ "app/preview/agency-studio-os/public/about/page.tsx",
160
+ "app/preview/agency-studio-os/public/contact/page.tsx",
161
+ "app/preview/agency-studio-os/admin/layout.tsx",
162
+ "app/preview/agency-studio-os/admin/page.tsx",
163
+ "app/preview/agency-studio-os/admin/projects/page.tsx",
164
+ "app/preview/agency-studio-os/admin/projects/new/page.tsx",
165
+ "app/preview/agency-studio-os/admin/projects/[id]/page.tsx",
166
+ "app/preview/agency-studio-os/admin/clients/page.tsx",
167
+ "app/preview/agency-studio-os/admin/services/page.tsx",
168
+ "app/preview/agency-studio-os/admin/leads/page.tsx",
169
+ "app/preview/agency-studio-os/admin/settings/page.tsx",
170
+ "components/templates/agency-studio/shared/types.ts",
171
+ "components/templates/agency-studio/shared/store.tsx",
172
+ "components/templates/agency-studio/shared/seed.ts",
173
+ "components/templates/agency-studio/shared/site-config.ts",
174
+ "components/templates/agency-studio/shared/nav-config.ts",
175
+ "components/templates/agency-studio/slices/home/HomePage.tsx",
176
+ "components/templates/agency-studio/slices/services/ServicesPage.tsx",
177
+ "components/templates/agency-studio/slices/portfolio/PortfolioListPage.tsx",
178
+ "components/templates/agency-studio/slices/portfolio/PortfolioDetailPage.tsx",
179
+ "components/templates/agency-studio/slices/about/AboutPage.tsx",
180
+ "components/templates/agency-studio/slices/contact/ContactPage.tsx",
181
+ "app/preview/agency-studio-os/admin/admin-shell-client.tsx",
182
+ "components/templates/agency-studio/slices/admin/dashboard/DashboardView.tsx",
183
+ "components/templates/agency-studio/slices/admin/projects/ProjectsList.tsx",
184
+ "components/templates/agency-studio/slices/admin/projects/ProjectEditor.tsx",
185
+ "components/templates/agency-studio/slices/admin/clients/ClientsList.tsx",
186
+ "components/templates/agency-studio/slices/admin/services/ServicesAdminView.tsx",
187
+ "components/templates/agency-studio/slices/admin/leads/LeadsView.tsx",
188
+ "components/templates/agency-studio/slices/admin/settings/SettingsView.tsx",
189
+ "convex/templates/agency-studio-os/schema.ts",
190
+ "convex/templates/agency-studio-os/projects.ts",
191
+ "convex/templates/agency-studio-os/clients.ts",
192
+ "convex/templates/agency-studio-os/services.ts",
193
+ "convex/templates/agency-studio-os/leads.ts",
194
+ "convex/templates/agency-studio-os/README.md"
195
+ ],
196
+ "dependencies": [
197
+ "next@^16",
198
+ "react@^19",
199
+ "react-dom@^19",
200
+ "lucide-react",
201
+ "@tabler/icons-react",
202
+ "sonner",
203
+ "next-themes",
204
+ "tailwindcss@^4",
205
+ "convex",
206
+ "@convex-dev/auth",
207
+ "@radix-ui/react-avatar",
208
+ "@radix-ui/react-dialog",
209
+ "@radix-ui/react-label",
210
+ "@radix-ui/react-separator",
211
+ "@radix-ui/react-slot"
212
+ ],
213
+ "agentRecipe": "Agency Studio OS = full-app B2B agency template (public + admin). 1) Move app/preview/agency-studio-os/{robots,sitemap,opengraph-image}.* to app root. 2) Copy public into app/(public)/, admin into app/(admin)/. 3) Edit components/templates/agency-studio/shared/site-config.ts — set studioName, brandName, baseUrl, twitter, email. 4) Wire convex/templates/agency-studio-os/* to convex/_generated and add @convex-dev/auth on admin routes. 5) Replace localStorage StoreProvider with Convex queries.",
214
+ "tags": [
215
+ "template",
216
+ "agency",
217
+ "studio",
218
+ "portfolio",
219
+ "b2b",
220
+ "admin",
221
+ "saas"
222
+ ],
223
+ "primaryFile": "app/preview/agency-studio-os/public/page.tsx"
224
+ },
127
225
  {
128
226
  "slug": "landing-hero-carousel",
129
227
  "title": "Landing — Hero Carousel",
@@ -286,6 +384,147 @@
286
384
  "storefront"
287
385
  ],
288
386
  "primaryFile": "README.md"
387
+ },
388
+ {
389
+ "slug": "saas-marketing-os",
390
+ "title": "SaaS Marketing OS",
391
+ "category": "website-template",
392
+ "status": "stable",
393
+ "description": "Public-only marketing site for a SaaS product — landing, pricing, features, blog, changelog, about, contact. MDX-driven blog + changelog. No admin (CMS via MDX files in repo).",
394
+ "source": "synthesized + mdx-blog feature",
395
+ "repoPath": "app/preview/saas-marketing-os",
396
+ "pullPaths": [
397
+ "app/preview/saas-marketing-os",
398
+ "components/templates/_shared",
399
+ "components/templates/saas-marketing"
400
+ ],
401
+ "files": [
402
+ "app/preview/saas-marketing-os/public/layout.tsx",
403
+ "app/preview/saas-marketing-os/public/page.tsx",
404
+ "app/preview/saas-marketing-os/public/pricing/page.tsx",
405
+ "app/preview/saas-marketing-os/public/features/page.tsx",
406
+ "app/preview/saas-marketing-os/public/blog/page.tsx",
407
+ "app/preview/saas-marketing-os/public/blog/[slug]/page.tsx",
408
+ "app/preview/saas-marketing-os/public/changelog/page.tsx",
409
+ "app/preview/saas-marketing-os/public/about/page.tsx",
410
+ "app/preview/saas-marketing-os/public/contact/page.tsx",
411
+ "components/templates/saas-marketing/shared/site-config.ts",
412
+ "components/templates/saas-marketing/shared/nav-config.ts",
413
+ "components/templates/saas-marketing/shared/types.ts",
414
+ "components/templates/saas-marketing/shared/store.tsx",
415
+ "components/templates/saas-marketing/shared/seed.ts",
416
+ "components/templates/saas-marketing/slices/home/HomePage.tsx",
417
+ "components/templates/saas-marketing/slices/pricing/PricingPage.tsx",
418
+ "components/templates/saas-marketing/slices/features/FeaturesPage.tsx",
419
+ "components/templates/saas-marketing/slices/blog/BlogList.tsx",
420
+ "components/templates/saas-marketing/slices/blog/BlogDetail.tsx",
421
+ "components/templates/saas-marketing/slices/changelog/ChangelogPage.tsx",
422
+ "components/templates/saas-marketing/slices/about/AboutPage.tsx",
423
+ "components/templates/saas-marketing/slices/contact/ContactPage.tsx"
424
+ ],
425
+ "dependencies": [
426
+ "next@^16",
427
+ "react@^19",
428
+ "react-dom@^19",
429
+ "lucide-react",
430
+ "sonner",
431
+ "next-themes",
432
+ "tailwindcss@^4",
433
+ "@radix-ui/react-slot"
434
+ ],
435
+ "agentRecipe": "SaaS Marketing OS = public-only marketing template. Blog + changelog use MDX (add the mdx-blog feature). Edit components/templates/saas-marketing/shared/site-config.ts to set product name, tagline, pricing tiers, contact email.",
436
+ "tags": [
437
+ "template",
438
+ "saas",
439
+ "marketing",
440
+ "mdx",
441
+ "blog",
442
+ "changelog"
443
+ ],
444
+ "primaryFile": "app/preview/saas-marketing-os/public/page.tsx"
445
+ },
446
+ {
447
+ "slug": "kreator-studio-os",
448
+ "title": "Kreator Studio OS",
449
+ "category": "website-template",
450
+ "status": "coming-soon",
451
+ "description": "Creator/influencer studio — newsletter-first public site + admin for posts, drops, audience segments. Resend + Midtrans tip jar. Coming soon.",
452
+ "source": "synthesized",
453
+ "repoPath": "app/preview/kreator-studio-os",
454
+ "pullPaths": [],
455
+ "files": [],
456
+ "dependencies": [],
457
+ "agentRecipe": "Coming soon — track at /build for updates.",
458
+ "tags": [
459
+ "template",
460
+ "creator",
461
+ "newsletter",
462
+ "coming-soon"
463
+ ],
464
+ "primaryFile": "README.md"
465
+ },
466
+ {
467
+ "slug": "konsultan-os",
468
+ "title": "Konsultan OS",
469
+ "category": "website-template",
470
+ "status": "coming-soon",
471
+ "description": "Consultancy site — services catalog, case studies, booking via Cal.com, Midtrans deposits. Public + lightweight admin. Coming soon.",
472
+ "source": "synthesized",
473
+ "repoPath": "app/preview/konsultan-os",
474
+ "pullPaths": [],
475
+ "files": [],
476
+ "dependencies": [],
477
+ "agentRecipe": "Coming soon — track at /build for updates.",
478
+ "tags": [
479
+ "template",
480
+ "consultant",
481
+ "booking",
482
+ "coming-soon"
483
+ ],
484
+ "primaryFile": "README.md"
485
+ },
486
+ {
487
+ "slug": "wirausaha-os",
488
+ "title": "Wirausaha OS",
489
+ "category": "website-template",
490
+ "status": "coming-soon",
491
+ "description": "Indonesian SMB toolkit — public storefront + admin (catalog, orders, Midtrans + QRIS, leads). Bahasa Indonesia first. Coming soon.",
492
+ "source": "synthesized",
493
+ "repoPath": "app/preview/wirausaha-os",
494
+ "pullPaths": [],
495
+ "files": [],
496
+ "dependencies": [],
497
+ "agentRecipe": "Coming soon — track at /build for updates.",
498
+ "tags": [
499
+ "template",
500
+ "smb",
501
+ "ecommerce",
502
+ "midtrans",
503
+ "indonesia",
504
+ "coming-soon"
505
+ ],
506
+ "primaryFile": "README.md"
507
+ },
508
+ {
509
+ "slug": "riset-kit",
510
+ "title": "Riset Kit",
511
+ "category": "website-template",
512
+ "status": "coming-soon",
513
+ "description": "Research / knowledge-base template — Convex vector search + MDX + threaded comments. Public reading + admin authoring. Coming soon.",
514
+ "source": "synthesized",
515
+ "repoPath": "app/preview/riset-kit",
516
+ "pullPaths": [],
517
+ "files": [],
518
+ "dependencies": [],
519
+ "agentRecipe": "Coming soon — track at /build for updates.",
520
+ "tags": [
521
+ "template",
522
+ "research",
523
+ "knowledge-base",
524
+ "vector-search",
525
+ "coming-soon"
526
+ ],
527
+ "primaryFile": "README.md"
289
528
  }
290
529
  ],
291
530
  "recipes": [
@@ -0,0 +1,102 @@
1
+ // Post-init restructure — runs after `rahman-resources init` finishes copying
2
+ // the bundled starter into the target dir. Steps:
3
+ //
4
+ // 1. Patch components.json with vertical-slice aliases (shared, templates,
5
+ // slices, features) so future shadcn add commands respect our layout.
6
+ // 2. Write rr.json (project manifest).
7
+ // 3. Patch tsconfig.json paths to mirror the rr.json aliases.
8
+ //
9
+ // Idempotent — safe to re-run. Each step compares before mutating.
10
+
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ import path from "node:path";
13
+
14
+ import { DEFAULT_RR, writeRr, buildRr } from "./rr.mjs";
15
+
16
+ const RR_ALIASES_TO_TSCONFIG = {
17
+ // alias -> tsconfig key target relative-path
18
+ "@/components/templates/_shared": "components/templates/_shared",
19
+ "@/components/templates": "components/templates",
20
+ "@/features": "features",
21
+ };
22
+
23
+ export function runPostInit(targetDir, opts = {}) {
24
+ const out = { changed: [], skipped: [] };
25
+
26
+ patchComponentsJson(targetDir, out);
27
+ writeRrJson(targetDir, opts, out);
28
+ patchTsconfig(targetDir, out);
29
+
30
+ return out;
31
+ }
32
+
33
+ function patchComponentsJson(targetDir, out) {
34
+ const p = path.join(targetDir, "components.json");
35
+ if (!existsSync(p)) {
36
+ out.skipped.push("components.json (not found — run shadcn init first)");
37
+ return;
38
+ }
39
+ const cj = JSON.parse(readFileSync(p, "utf8"));
40
+ cj.aliases = cj.aliases ?? {};
41
+ const before = JSON.stringify(cj.aliases);
42
+ cj.aliases.shared = cj.aliases.shared ?? "@/components/templates/_shared";
43
+ cj.aliases.templates = cj.aliases.templates ?? "@/components/templates";
44
+ cj.aliases.slices = cj.aliases.slices ?? "@/components/templates/{template}/slices";
45
+ cj.aliases.features = cj.aliases.features ?? "@/features";
46
+ if (JSON.stringify(cj.aliases) !== before) {
47
+ writeFileSync(p, JSON.stringify(cj, null, 2) + "\n");
48
+ out.changed.push("components.json (aliases)");
49
+ } else {
50
+ out.skipped.push("components.json (already patched)");
51
+ }
52
+ }
53
+
54
+ function writeRrJson(targetDir, opts, out) {
55
+ const p = path.join(targetDir, "rr.json");
56
+ if (existsSync(p)) {
57
+ out.skipped.push("rr.json (already exists)");
58
+ return;
59
+ }
60
+ const rr = buildRr({
61
+ template: opts.template,
62
+ features: opts.features,
63
+ skills: opts.skills,
64
+ templateVersion: opts.templateVersion,
65
+ });
66
+ writeRr(rr, targetDir);
67
+ out.changed.push("rr.json (created)");
68
+ }
69
+
70
+ function patchTsconfig(targetDir, out) {
71
+ const p = path.join(targetDir, "tsconfig.json");
72
+ if (!existsSync(p)) {
73
+ out.skipped.push("tsconfig.json (not found)");
74
+ return;
75
+ }
76
+ let raw = readFileSync(p, "utf8");
77
+ let cfg;
78
+ try { cfg = JSON.parse(raw); } catch {
79
+ out.skipped.push("tsconfig.json (parse failed — likely has comments; skipped)");
80
+ return;
81
+ }
82
+ cfg.compilerOptions = cfg.compilerOptions ?? {};
83
+ cfg.compilerOptions.paths = cfg.compilerOptions.paths ?? {};
84
+ const paths = cfg.compilerOptions.paths;
85
+ const before = JSON.stringify(paths);
86
+ paths["@/*"] = paths["@/*"] ?? ["./*"];
87
+ // Keep tsconfig minimal — @/* covers all aliases. Don't bloat with redundant entries.
88
+ if (JSON.stringify(paths) !== before) {
89
+ writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n");
90
+ out.changed.push("tsconfig.json (paths)");
91
+ } else {
92
+ out.skipped.push("tsconfig.json (already configured)");
93
+ }
94
+ }
95
+
96
+ // Allow running as a CLI script for re-restructure on existing projects.
97
+ if (import.meta.url === `file://${process.argv[1]}`) {
98
+ const target = process.argv[2] ?? process.cwd();
99
+ const result = runPostInit(target);
100
+ for (const c of result.changed) console.log(" +", c);
101
+ for (const s of result.skipped) console.log(" -", s);
102
+ }
@@ -0,0 +1,94 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://rahmanef63.github.io/resource-site/rr.schema.json",
4
+ "title": "rr.json — Rahman Resources project manifest",
5
+ "description": "Project-local manifest written by `rahman-resources init` and patched by `add` / `add-skill` / `upgrade`. Mirrors what shadcn's components.json does for shadcn, but for the vertical-slice kitab flow (templates + features + Claude skills).",
6
+ "type": "object",
7
+ "required": ["version", "framework", "aliases", "layout"],
8
+ "additionalProperties": true,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "version": { "type": "integer", "minimum": 1 },
12
+ "framework": { "type": "string", "enum": ["next-16", "next-15", "vite", "remix"] },
13
+ "style": { "type": "string", "default": "shadcn-new-york" },
14
+ "rsc": { "type": "boolean", "default": true },
15
+ "tailwind": {
16
+ "type": "object",
17
+ "properties": {
18
+ "config": { "type": "string" },
19
+ "css": { "type": "string" }
20
+ }
21
+ },
22
+ "aliases": {
23
+ "type": "object",
24
+ "required": ["components", "ui", "shared", "templates", "lib"],
25
+ "properties": {
26
+ "components": { "type": "string" },
27
+ "ui": { "type": "string" },
28
+ "shared": { "type": "string", "description": "Cross-template _shared scaffolding (site-shell, admin-shell, store factory)." },
29
+ "templates": { "type": "string", "description": "Per-template folders root (e.g. components/templates)." },
30
+ "slices": { "type": "string", "description": "Optional slice-root pattern, may use {template} placeholder." },
31
+ "features": { "type": "string" },
32
+ "convex": { "type": "string" },
33
+ "lib": { "type": "string" },
34
+ "hooks": { "type": "string" }
35
+ }
36
+ },
37
+ "layout": {
38
+ "type": "object",
39
+ "required": ["kind"],
40
+ "properties": {
41
+ "kind": { "type": "string", "enum": ["vertical-slice", "feature-folders"] },
42
+ "publicRoute": { "type": "string" },
43
+ "adminRoute": { "type": "string" },
44
+ "sliceRoot": { "type": "string" }
45
+ }
46
+ },
47
+ "template": {
48
+ "type": "object",
49
+ "required": ["slug", "version"],
50
+ "properties": {
51
+ "slug": { "type": "string" },
52
+ "version": { "type": "string" }
53
+ }
54
+ },
55
+ "features": {
56
+ "type": "array",
57
+ "items": {
58
+ "type": "object",
59
+ "required": ["slug"],
60
+ "properties": {
61
+ "slug": { "type": "string" },
62
+ "version": { "type": "string" },
63
+ "addedAt": { "type": "string", "format": "date" }
64
+ }
65
+ }
66
+ },
67
+ "skills": {
68
+ "type": "array",
69
+ "items": {
70
+ "type": "object",
71
+ "required": ["slug", "source"],
72
+ "properties": {
73
+ "slug": { "type": "string" },
74
+ "source": { "type": "string", "enum": ["anthropics", "rahman", "custom"] },
75
+ "version": { "type": "string", "default": "main" },
76
+ "addedAt": { "type": "string", "format": "date" }
77
+ }
78
+ }
79
+ },
80
+ "auth": {
81
+ "type": "object",
82
+ "properties": {
83
+ "provider": { "type": "string", "enum": ["convex-auth", "clerk", "next-auth", "none"] }
84
+ }
85
+ },
86
+ "convex": {
87
+ "type": "object",
88
+ "properties": {
89
+ "self_hosted": { "type": "boolean" },
90
+ "deploymentUrl": { "type": "string" }
91
+ }
92
+ }
93
+ }
94
+ }
package/lib/rr.mjs ADDED
@@ -0,0 +1,134 @@
1
+ // Read / write / validate rr.json — the project-local Rahman Resources manifest.
2
+ //
3
+ // Schema lives at lib/rr-schema.json (JSON Schema draft-07). Validation is
4
+ // hand-rolled (shape-only) to avoid pulling in ajv as a runtime dep — the CLI
5
+ // stays small. Full schema validation is available via `npx ajv` if desired.
6
+
7
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
+ import path from "node:path";
9
+
10
+ const RR_FILENAME = "rr.json";
11
+ const SCHEMA_URL = "https://rahmanef63.github.io/resource-site/rr.schema.json";
12
+
13
+ export const DEFAULT_RR = {
14
+ $schema: SCHEMA_URL,
15
+ version: 1,
16
+ framework: "next-16",
17
+ style: "shadcn-new-york",
18
+ rsc: true,
19
+ tailwind: { config: "tailwind.config.ts", css: "app/globals.css" },
20
+ aliases: {
21
+ components: "@/components",
22
+ ui: "@/components/ui",
23
+ shared: "@/components/templates/_shared",
24
+ templates: "@/components/templates",
25
+ slices: "@/components/templates/{template}/slices",
26
+ features: "@/features",
27
+ convex: "@/convex",
28
+ lib: "@/lib",
29
+ hooks: "@/hooks",
30
+ },
31
+ layout: {
32
+ kind: "vertical-slice",
33
+ publicRoute: "app/(public)",
34
+ adminRoute: "app/(admin)",
35
+ sliceRoot: "components/templates",
36
+ },
37
+ template: null,
38
+ features: [],
39
+ skills: [],
40
+ auth: { provider: "convex-auth" },
41
+ convex: { self_hosted: true },
42
+ };
43
+
44
+ export function rrPath(targetDir = process.cwd()) {
45
+ return path.join(targetDir, RR_FILENAME);
46
+ }
47
+
48
+ export function rrExists(targetDir = process.cwd()) {
49
+ return existsSync(rrPath(targetDir));
50
+ }
51
+
52
+ export function readRr(targetDir = process.cwd()) {
53
+ const p = rrPath(targetDir);
54
+ if (!existsSync(p)) throw new Error(`No rr.json at ${p}. Run 'rahman-resources init' first.`);
55
+ try {
56
+ return JSON.parse(readFileSync(p, "utf8"));
57
+ } catch (err) {
58
+ throw new Error(`Failed to parse ${p}: ${err.message}`);
59
+ }
60
+ }
61
+
62
+ export function writeRr(rr, targetDir = process.cwd()) {
63
+ const p = rrPath(targetDir);
64
+ writeFileSync(p, JSON.stringify(rr, null, 2) + "\n");
65
+ return p;
66
+ }
67
+
68
+ export function buildRr(opts = {}) {
69
+ const rr = JSON.parse(JSON.stringify(DEFAULT_RR));
70
+ if (opts.template) {
71
+ rr.template = { slug: opts.template, version: opts.templateVersion ?? "main" };
72
+ }
73
+ if (opts.features?.length) {
74
+ const stamp = today();
75
+ rr.features = opts.features.map((slug) => ({ slug, version: opts.templateVersion ?? "main", addedAt: stamp }));
76
+ }
77
+ if (opts.skills?.length) {
78
+ const stamp = today();
79
+ rr.skills = opts.skills.map((s) => {
80
+ if (typeof s === "string") return { slug: s, source: "anthropics", version: "main", addedAt: stamp };
81
+ return { source: "anthropics", version: "main", addedAt: stamp, ...s };
82
+ });
83
+ }
84
+ return rr;
85
+ }
86
+
87
+ export function addFeature(rr, slug, version = "main") {
88
+ rr.features = rr.features ?? [];
89
+ if (rr.features.find((f) => f.slug === slug)) return rr;
90
+ rr.features.push({ slug, version, addedAt: today() });
91
+ return rr;
92
+ }
93
+
94
+ export function addSkill(rr, slug, source = "anthropics", version = "main") {
95
+ rr.skills = rr.skills ?? [];
96
+ if (rr.skills.find((s) => s.slug === slug && s.source === source)) return rr;
97
+ rr.skills.push({ slug, source, version, addedAt: today() });
98
+ return rr;
99
+ }
100
+
101
+ export function removeFeature(rr, slug) {
102
+ rr.features = (rr.features ?? []).filter((f) => f.slug !== slug);
103
+ return rr;
104
+ }
105
+
106
+ export function removeSkill(rr, slug) {
107
+ rr.skills = (rr.skills ?? []).filter((s) => s.slug !== slug);
108
+ return rr;
109
+ }
110
+
111
+ // Shape-only validation — returns string[] of issues (empty = ok).
112
+ export function validateRr(rr) {
113
+ const issues = [];
114
+ if (!rr || typeof rr !== "object") return ["rr.json is not an object"];
115
+ if (typeof rr.version !== "number") issues.push("version must be a number");
116
+ if (!rr.framework) issues.push("framework is required");
117
+ if (!rr.aliases || typeof rr.aliases !== "object") issues.push("aliases is required");
118
+ else {
119
+ for (const k of ["components", "ui", "shared", "templates", "lib"]) {
120
+ if (typeof rr.aliases[k] !== "string") issues.push(`aliases.${k} must be a string`);
121
+ }
122
+ }
123
+ if (!rr.layout || typeof rr.layout !== "object") issues.push("layout is required");
124
+ else if (!["vertical-slice", "feature-folders"].includes(rr.layout.kind)) {
125
+ issues.push(`layout.kind must be vertical-slice|feature-folders (got ${rr.layout.kind})`);
126
+ }
127
+ if (rr.features && !Array.isArray(rr.features)) issues.push("features must be an array");
128
+ if (rr.skills && !Array.isArray(rr.skills)) issues.push("skills must be an array");
129
+ return issues;
130
+ }
131
+
132
+ function today() {
133
+ return new Date().toISOString().slice(0, 10);
134
+ }
@@ -0,0 +1,154 @@
1
+ {
2
+ "_meta": {
3
+ "description": "Claude Skills inventory — 18 entries (sync'd from site/lib/content/claude-skills.ts). Used by CLI add-skill + builder UI + MCP server.",
4
+ "anthropicsRepo": "anthropics/skills",
5
+ "branch": "main",
6
+ "lastSynced": "2026-05-05"
7
+ },
8
+ "skills": [
9
+ {
10
+ "slug": "algorithmic-art",
11
+ "title": "Algorithmic Art",
12
+ "category": "creative",
13
+ "source": "anthropics",
14
+ "path": "skills/algorithmic-art",
15
+ "description": "Generate generative-art pieces (p5.js, processing-style algorithms)."
16
+ },
17
+ {
18
+ "slug": "brand-guidelines",
19
+ "title": "Brand Guidelines",
20
+ "category": "design",
21
+ "source": "anthropics",
22
+ "path": "skills/brand-guidelines",
23
+ "description": "Apply a brand's visual language consistently across artifacts."
24
+ },
25
+ {
26
+ "slug": "canvas-design",
27
+ "title": "Canvas Design",
28
+ "category": "design",
29
+ "source": "anthropics",
30
+ "path": "skills/canvas-design",
31
+ "description": "Design canvas layouts (Canva-style) — slides, posters, social posts."
32
+ },
33
+ {
34
+ "slug": "claude-api",
35
+ "title": "Claude API",
36
+ "category": "development",
37
+ "source": "anthropics",
38
+ "path": "skills/claude-api",
39
+ "description": "Call the Claude API correctly — auth, models, tools, streaming patterns."
40
+ },
41
+ {
42
+ "slug": "doc-coauthoring",
43
+ "title": "Doc Co-authoring",
44
+ "category": "enterprise",
45
+ "source": "anthropics",
46
+ "path": "skills/doc-coauthoring",
47
+ "description": "Collaborate on long-form documents with section-aware editing."
48
+ },
49
+ {
50
+ "slug": "docx",
51
+ "title": "DOCX",
52
+ "category": "documents",
53
+ "source": "anthropics",
54
+ "path": "skills/docx",
55
+ "description": "Read, write, and edit Microsoft Word .docx files."
56
+ },
57
+ {
58
+ "slug": "frontend-design",
59
+ "title": "Frontend Design",
60
+ "category": "development",
61
+ "source": "anthropics",
62
+ "path": "skills/frontend-design",
63
+ "description": "Design and build polished frontend UIs with modern patterns."
64
+ },
65
+ {
66
+ "slug": "internal-comms",
67
+ "title": "Internal Comms",
68
+ "category": "enterprise",
69
+ "source": "anthropics",
70
+ "path": "skills/internal-comms",
71
+ "description": "Draft internal company communications (announcements, memos, all-hands)."
72
+ },
73
+ {
74
+ "slug": "mcp-builder",
75
+ "title": "MCP Builder",
76
+ "category": "development",
77
+ "source": "anthropics",
78
+ "path": "skills/mcp-builder",
79
+ "description": "Build Model Context Protocol servers — tools, resources, transports."
80
+ },
81
+ {
82
+ "slug": "pdf",
83
+ "title": "PDF",
84
+ "category": "documents",
85
+ "source": "anthropics",
86
+ "path": "skills/pdf",
87
+ "description": "Read, fill, and generate PDF files."
88
+ },
89
+ {
90
+ "slug": "pptx",
91
+ "title": "PPTX",
92
+ "category": "documents",
93
+ "source": "anthropics",
94
+ "path": "skills/pptx",
95
+ "description": "Read, write, and edit Microsoft PowerPoint .pptx files."
96
+ },
97
+ {
98
+ "slug": "skill-creator",
99
+ "title": "Skill Creator",
100
+ "category": "development",
101
+ "source": "anthropics",
102
+ "path": "skills/skill-creator",
103
+ "description": "Create new Claude Skills — scaffold SKILL.md, references, scripts."
104
+ },
105
+ {
106
+ "slug": "slack-gif-creator",
107
+ "title": "Slack GIF Creator",
108
+ "category": "creative",
109
+ "source": "anthropics",
110
+ "path": "skills/slack-gif-creator",
111
+ "description": "Make small animated GIFs for Slack reactions and team messaging."
112
+ },
113
+ {
114
+ "slug": "theme-factory",
115
+ "title": "Theme Factory",
116
+ "category": "design",
117
+ "source": "anthropics",
118
+ "path": "skills/theme-factory",
119
+ "description": "Generate cohesive design-system themes (colors, typography, spacing)."
120
+ },
121
+ {
122
+ "slug": "web-artifacts-builder",
123
+ "title": "Web Artifacts Builder",
124
+ "category": "development",
125
+ "source": "anthropics",
126
+ "path": "skills/web-artifacts-builder",
127
+ "description": "Build self-contained web artifacts (HTML/CSS/JS) for Claude artifacts surface."
128
+ },
129
+ {
130
+ "slug": "webapp-testing",
131
+ "title": "Webapp Testing",
132
+ "category": "development",
133
+ "source": "anthropics",
134
+ "path": "skills/webapp-testing",
135
+ "description": "Test web applications — Playwright/Puppeteer flows, snapshot, a11y."
136
+ },
137
+ {
138
+ "slug": "xlsx",
139
+ "title": "XLSX",
140
+ "category": "documents",
141
+ "source": "anthropics",
142
+ "path": "skills/xlsx",
143
+ "description": "Read, write, and edit Microsoft Excel .xlsx files."
144
+ },
145
+ {
146
+ "slug": "rahman-resources",
147
+ "title": "Rahman Resources",
148
+ "category": "development",
149
+ "source": "rahman",
150
+ "path": "skills/rahman-resources",
151
+ "description": "Use the Rahman Resources kitab — discover templates, features, recipes; assemble bundles; emit npx commands."
152
+ }
153
+ ]
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rahman-resources",
3
- "version": "0.3.0",
3
+ "version": "0.4.2",
4
4
  "description": "Scaffolder + installer for Rahman Resources kitab — npx rahman-resources init <app>; add <template>; list; info",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,7 +25,9 @@
25
25
  "scripts": {
26
26
  "gen": "node scripts/gen-manifest.mjs",
27
27
  "validate": "node scripts/validate.mjs",
28
- "prepublishOnly": "node scripts/validate.mjs"
28
+ "sync:skills": "node scripts/sync-skills.mjs",
29
+ "sync:skills:check": "node scripts/sync-skills.mjs --check",
30
+ "prepublishOnly": "node scripts/sync-skills.mjs --check && node scripts/validate.mjs"
29
31
  },
30
32
  "dependencies": {
31
33
  "kleur": "^4.1.5",