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/lib/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 2,
3
- "generatedAt": "2026-05-05T11:50:32.844Z",
3
+ "generatedAt": "2026-05-06T09:30:28.726Z",
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
+ }