rahman-resources 0.1.0 → 0.3.0

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,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
- // rahman-resources — template installer for the Rahman kitab.
2
+ // rahman-resources — installer for the Rahman kitab.
3
3
  // Usage:
4
- // npx rahman-resources add <template> [target-dir]
5
- // npx rahman-resources list
6
- // npx rahman-resources info <template>
4
+ // npx rahman-resources init <app-name> [--lite]
5
+ // npx rahman-resources add <slug> [target-dir]
6
+ // npx rahman-resources list [layouts|recipes|features]
7
+ // npx rahman-resources info <slug>
7
8
 
8
9
  import { createRequire } from "node:module";
9
10
  import { spawn } from "node:child_process";
10
- import { existsSync } from "node:fs";
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
11
12
  import path from "node:path";
12
13
  import { fileURLToPath } from "node:url";
13
14
 
@@ -18,23 +19,33 @@ const require = createRequire(import.meta.url);
18
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
20
  const manifest = require(path.join(__dirname, "../lib/manifest.json"));
20
21
 
21
- const REPO = "rahmanef63/resource-site";
22
- const BRANCH = "main";
22
+ const REPO = manifest.repo ?? "rahmanef63/resource-site";
23
+ const BRANCH = manifest.branch ?? "main";
24
+
25
+ const KINDS = /** @type {const} */ (["layout", "recipe", "feature"]);
23
26
 
24
27
  const [, , cmd, ...rest] = process.argv;
25
28
 
26
- main().catch((err) => {
27
- console.error(kleur.red("✖"), err.message ?? err);
28
- process.exit(1);
29
- });
29
+ // Defer to next tick so all module-level const declarations finish initializing
30
+ // before the dispatch reaches functions that reference them.
31
+ queueMicrotask(() =>
32
+ main().catch((err) => {
33
+ console.error(kleur.red("✖"), err.message ?? err);
34
+ process.exit(1);
35
+ }),
36
+ );
30
37
 
31
38
  async function main() {
32
39
  switch (cmd) {
40
+ case "init":
41
+ case "create":
42
+ case "new":
43
+ return runInit(rest);
33
44
  case "add":
34
45
  return runAdd(rest);
35
46
  case "list":
36
47
  case "ls":
37
- return runList();
48
+ return runList(rest);
38
49
  case "info":
39
50
  return runInfo(rest);
40
51
  case undefined:
@@ -42,6 +53,10 @@ async function main() {
42
53
  case "--help":
43
54
  case "help":
44
55
  return printHelp();
56
+ case "-v":
57
+ case "--version":
58
+ case "version":
59
+ return printVersion();
45
60
  default:
46
61
  console.error(kleur.red(`Unknown command: ${cmd}`));
47
62
  printHelp();
@@ -49,65 +64,187 @@ async function main() {
49
64
  }
50
65
  }
51
66
 
67
+ function printVersion() {
68
+ const pkg = require(path.join(__dirname, "../package.json"));
69
+ console.log(pkg.version);
70
+ }
71
+
52
72
  function printHelp() {
53
73
  console.log(`
54
- ${kleur.bold("rahman-resources")} — install templates from the Rahman kitab
74
+ ${kleur.bold("rahman-resources")} — scaffold + install templates, recipes, features
55
75
 
56
76
  ${kleur.bold("Usage:")}
57
- npx rahman-resources add <template> [target-dir]
58
- npx rahman-resources list
59
- npx rahman-resources info <template>
77
+ npx rahman-resources init <app-name> [--lite]
78
+ npx rahman-resources add <slug> [target-dir]
79
+ npx rahman-resources list [layouts|recipes|features]
80
+ npx rahman-resources info <slug>
60
81
 
61
82
  ${kleur.bold("Examples:")}
62
- npx rahman-resources add personal-brand-os my-app
63
- pnpm dlx rahman-resources add personal-brand-os .
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
64
88
  `);
65
89
  }
66
90
 
67
- function runList() {
68
- console.log(kleur.bold("\nAvailable templates:\n"));
69
- for (const t of manifest.templates) {
70
- console.log(
71
- ` ${kleur.cyan(t.slug.padEnd(28))} ${kleur.dim(t.category.padEnd(18))} ${t.title}`,
72
- );
91
+ function findEntry(slug) {
92
+ for (const kind of KINDS) {
93
+ const list = manifest[kind + "s"];
94
+ const e = list.find((x) => x.slug === slug);
95
+ if (e) return { kind, entry: e };
73
96
  }
74
- console.log(`\n${manifest.templates.length} total. Run ${kleur.cyan("info <slug>")} for detail.\n`);
97
+ return null;
98
+ }
99
+
100
+ function runList([filter]) {
101
+ const groups = filter ? [filter] : ["layouts", "recipes", "features"];
102
+ for (const g of groups) {
103
+ const list = manifest[g];
104
+ if (!list || list.length === 0) continue;
105
+ console.log(`\n${kleur.bold(g.toUpperCase())} ${kleur.dim(`(${list.length})`)}\n`);
106
+ for (const t of list) {
107
+ 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
+ );
111
+ }
112
+ }
113
+ console.log(`\nRun ${kleur.cyan("info <slug>")} for detail, ${kleur.cyan("add <slug> [target]")} to install.\n`);
75
114
  }
76
115
 
77
116
  function runInfo([slug]) {
78
- if (!slug) throw new Error("Usage: rahman-resources info <template>");
79
- const t = manifest.templates.find((x) => x.slug === slug);
80
- if (!t) throw new Error(`Template not found: ${slug}. Run 'list' to see all.`);
117
+ if (!slug) throw new Error("Usage: rahman-resources info <slug>");
118
+ const found = findEntry(slug);
119
+ if (!found) throw new Error(`Slug not found: ${slug}. Run 'list' to see all.`);
120
+ const { kind, entry: t } = found;
121
+
81
122
  console.log(`
82
- ${kleur.bold(t.title)} ${kleur.dim(`[${t.category}]`)}
123
+ ${kleur.bold(t.title)} ${kleur.dim(`[${kind}]`)} ${kleur.dim(t.category ?? "")}
83
124
 
84
125
  ${t.description}
126
+ `);
85
127
 
86
- ${kleur.bold("Pulls:")}
87
- ${t.pullPaths.map((p) => ` · ${p}`).join("\n")}
128
+ if (kind === "layout") {
129
+ console.log(`${kleur.bold("Pulls:")}`);
130
+ console.log(t.pullPaths.map((p) => ` · ${p}`).join("\n") || " (none)");
131
+ if (t.dependencies?.length) {
132
+ console.log(`\n${kleur.bold("Dependencies:")}`);
133
+ console.log(t.dependencies.map((d) => ` · ${d}`).join("\n"));
134
+ }
135
+ } else if (kind === "feature") {
136
+ console.log(`${kleur.bold("Install:")}\n ${t.install}`);
137
+ if (t.npmPackages?.length) {
138
+ console.log(`\n${kleur.bold("npm packages:")}`);
139
+ console.log(t.npmPackages.map((d) => ` · ${d}`).join("\n"));
140
+ }
141
+ } else if (kind === "recipe") {
142
+ console.log(`${kleur.bold("Files:")}`);
143
+ console.log(t.files.map((f) => ` · ${f}`).join("\n"));
144
+ }
88
145
 
89
- ${kleur.bold("Dependencies:")}
90
- ${(t.dependencies ?? []).map((d) => ` · ${d}`).join("\n") || " (none beyond base)"}
146
+ if (t.docsUrl) console.log(`\n${kleur.dim(`Docs: ${t.docsUrl}`)}`);
147
+ console.log(`${kleur.dim(`Source: ${t.source ?? "—"}`)}\n`);
148
+ }
91
149
 
92
- ${kleur.dim(`Page: https://github.com/${REPO}/tree/${BRANCH}/${t.repoPath}`)}
93
- `);
150
+ // Files in lib/starter prefixed with `_` get renamed on copy.
151
+ // Done because npm pack filters .gitignore and warns on nested package.json.
152
+ const STARTER_RENAME_PAIRS = [
153
+ ["_package", "package"],
154
+ ["_gitignore", ".gitignore"],
155
+ ["_env", ".env"],
156
+ ["_README", "README"],
157
+ ];
158
+
159
+ function renameStarterFile(name) {
160
+ for (const pair of STARTER_RENAME_PAIRS) {
161
+ const f = pair[0];
162
+ const t = pair[1];
163
+ if (name === f || name.startsWith(f + ".")) return t + name.slice(f.length);
164
+ }
165
+ return name;
166
+ }
167
+
168
+ function copyStarterTree(src, dest, appName, slug) {
169
+ mkdirSync(dest, { recursive: true });
170
+ for (const entry of readdirSync(src)) {
171
+ const sFull = path.join(src, entry);
172
+ const dEntry = renameStarterFile(entry);
173
+ const dFull = path.join(dest, dEntry);
174
+ const stat = statSync(sFull);
175
+ if (stat.isDirectory()) {
176
+ copyStarterTree(sFull, dFull, appName, slug);
177
+ } else {
178
+ let body = readFileSync(sFull, "utf8");
179
+ body = body.replaceAll("__APP_NAME__", appName).replaceAll("__APP_SLUG__", slug);
180
+ writeFileSync(dFull, body);
181
+ }
182
+ }
183
+ }
184
+
185
+ async function runInit([appName, ...flags]) {
186
+ if (!appName || appName.startsWith("-")) {
187
+ throw new Error("Usage: rahman-resources init <app-name>");
188
+ }
189
+ const slug = appName.replace(/[^a-z0-9-_]/gi, "-").toLowerCase();
190
+ const target = path.resolve(process.cwd(), appName);
191
+ if (existsSync(target)) {
192
+ throw new Error(`Directory already exists: ${target}`);
193
+ }
194
+
195
+ console.log(kleur.bold(`\n→ Scaffolding ${kleur.cyan(slug)} (Next 16 + Convex + shadcn)\n`));
196
+
197
+ const starter = path.join(__dirname, "../lib/starter");
198
+ if (!existsSync(starter)) {
199
+ throw new Error(`Starter not found at ${starter}`);
200
+ }
201
+
202
+ process.stdout.write(` copying starter ... `);
203
+ copyStarterTree(starter, target, appName, slug);
204
+ console.log(kleur.green("ok"));
205
+
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)`));
209
+ }
210
+
211
+ console.log(`\n${kleur.green("✓")} Done. ${kleur.bold(slug)} scaffolded.\n`);
212
+ console.log(`${kleur.bold("Next:")}`);
213
+ console.log(` cd ${appName}`);
214
+ console.log(` cp .env.example .env.local ${kleur.dim("# fill NEXT_PUBLIC_CONVEX_URL")}`);
215
+ console.log(` npm install --legacy-peer-deps`);
216
+ console.log(` npx convex dev --once ${kleur.dim("# generates convex/_generated")}`);
217
+ 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
+ );
94
221
  }
95
222
 
96
223
  async function runAdd([slug, targetArg = "."]) {
97
224
  if (!slug) {
98
- console.error(kleur.red("Missing template slug."));
225
+ console.error(kleur.red("Missing slug."));
99
226
  printHelp();
100
227
  process.exit(1);
101
228
  }
102
- const t = manifest.templates.find((x) => x.slug === slug);
103
- if (!t) {
229
+ const found = findEntry(slug);
230
+ if (!found) {
104
231
  throw new Error(
105
- `Template "${slug}" not found. Run ${kleur.cyan("npx rahman-resources list")} to see available templates.`,
232
+ `"${slug}" not found. Run ${kleur.cyan("npx rahman-resources list")}.`,
106
233
  );
107
234
  }
235
+ const { kind, entry } = found;
108
236
  const target = path.resolve(process.cwd(), targetArg);
109
237
 
238
+ if (kind === "layout") return addLayout(entry, target, targetArg);
239
+ if (kind === "feature") return addFeature(entry, target, targetArg);
240
+ if (kind === "recipe") return addRecipe(entry);
241
+ }
242
+
243
+ async function addLayout(t, target, targetArg) {
110
244
  console.log(kleur.bold(`\n→ Installing ${kleur.cyan(t.title)} into ${kleur.dim(target)}\n`));
245
+ if (!t.pullPaths || t.pullPaths.length === 0) {
246
+ throw new Error(`Layout "${t.slug}" has no valid pullPaths in manifest.`);
247
+ }
111
248
  for (const p of t.pullPaths) {
112
249
  const dest = path.join(target, p);
113
250
  process.stdout.write(` pulling ${kleur.dim(p)} ... `);
@@ -119,19 +256,53 @@ async function runAdd([slug, targetArg = "."]) {
119
256
  const pm = detectPM(target);
120
257
  console.log(kleur.bold(`\n→ Installing dependencies via ${kleur.cyan(pm)}\n`));
121
258
  if (!hasPackageJson(target)) {
122
- console.log(
123
- kleur.yellow(` ${target}/package.json not found skipping install.`),
124
- );
125
- console.log(kleur.dim(` Run later: cd ${targetArg} && ${pm} add ${t.dependencies.join(" ")}`));
259
+ console.log(kleur.yellow(` ${target}/package.json not found — skipping install.`));
260
+ console.log(kleur.dim(` Run later: cd ${targetArg} && ${pm} ${pm === "npm" ? "install" : "add"} ${t.dependencies.join(" ")}`));
126
261
  } else {
127
- await runInstall(pm, t.dependencies, target);
262
+ await runPM(pm, t.dependencies, target);
128
263
  }
129
264
  }
130
265
 
131
266
  console.log(`\n${kleur.green("✓")} Done. ${kleur.bold(t.title)} installed.`);
132
- if (t.agentRecipe) {
133
- console.log(`\n${kleur.bold("Next:")}\n${indent(t.agentRecipe, 2)}\n`);
267
+ if (t.agentRecipe) console.log(`\n${kleur.bold("Next:")}\n${indent(t.agentRecipe, 2)}\n`);
268
+ }
269
+
270
+ async function addFeature(t, target, targetArg) {
271
+ console.log(kleur.bold(`\n→ Adding feature ${kleur.cyan(t.title)} to ${kleur.dim(target)}\n`));
272
+ if (!t.npmPackages || t.npmPackages.length === 0) {
273
+ console.log(kleur.dim(` No npm packages to install (${t.install}).`));
274
+ } else {
275
+ const pm = detectPM(target);
276
+ if (!hasPackageJson(target)) {
277
+ console.log(kleur.yellow(` ${target}/package.json not found — skipping install.`));
278
+ console.log(kleur.dim(` Run later: cd ${targetArg} && ${pm} ${pm === "npm" ? "install" : "add"} ${t.npmPackages.join(" ")}`));
279
+ } else {
280
+ console.log(kleur.dim(` via ${pm}: ${t.npmPackages.join(" ")}\n`));
281
+ await runPM(pm, t.npmPackages, target);
282
+ }
283
+ }
284
+
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));
289
+ }
290
+ if (t.agentRecipe) console.log(`\n${kleur.bold("Wire-up:")}\n${indent(t.agentRecipe, 2)}\n`);
291
+ if (t.docsUrl) console.log(`\n${kleur.dim(`Docs: ${t.docsUrl}`)}\n`);
292
+ }
293
+
294
+ function addRecipe(t) {
295
+ console.log(`\n${kleur.bold(t.title)} ${kleur.dim("(recipe — manual port)")}\n`);
296
+ console.log(t.description);
297
+ console.log(`\n${kleur.bold("Source:")} ${t.source}`);
298
+ console.log(`\n${kleur.bold("Files to port:")}`);
299
+ 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));
134
303
  }
304
+ if (t.agentRecipe) console.log(`\n${kleur.bold("Wire-up:")}\n${indent(t.agentRecipe, 2)}\n`);
305
+ console.log(kleur.dim(`\n(Recipes are educational patterns — copy from source repo into your project manually.)\n`));
135
306
  }
136
307
 
137
308
  async function pull(repoPath, dest) {
@@ -154,7 +325,7 @@ function hasPackageJson(target) {
154
325
  return existsSync(path.join(target, "package.json"));
155
326
  }
156
327
 
157
- function runInstall(pm, deps, cwd) {
328
+ function runPM(pm, deps, cwd) {
158
329
  const args = pm === "npm" ? ["install", ...deps] : ["add", ...deps];
159
330
  return new Promise((resolve, reject) => {
160
331
  const ps = spawn(pm, args, { cwd, stdio: "inherit", shell: true });