rahman-resources 0.7.0 → 0.8.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/bin/cli.js CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  rrExists,
26
26
  validateRr,
27
27
  addFeature as rrAddFeature,
28
+ addSlice as rrAddSlice,
28
29
  addSkill as rrAddSkill,
29
30
  } from "../lib/rr.mjs";
30
31
  import { runPostInit } from "../lib/post-init.mjs";
@@ -714,7 +715,7 @@ async function installSkill(slug, target) {
714
715
 
715
716
  // ─── doctor ───────────────────────────────────────────────────────────────
716
717
 
717
- function runDoctor() {
718
+ function runDoctor(rest = []) {
718
719
  const target = process.cwd();
719
720
  if (!rrExists(target)) {
720
721
  console.log(kleur.yellow("⚠ No rr.json found in cwd. Run 'rahman-resources init <app>' first."));
@@ -722,16 +723,102 @@ function runDoctor() {
722
723
  }
723
724
  const rr = readRr(target);
724
725
  const issues = validateRr(rr);
725
- if (issues.length === 0) {
726
- console.log(kleur.green("✓ rr.json is valid."));
727
- console.log(` template: ${kleur.cyan(rr.template?.slug ?? "(none)")}`);
728
- console.log(` features: ${kleur.cyan(rr.features?.length ?? 0)}`);
729
- console.log(` skills: ${kleur.cyan(rr.skills?.length ?? 0)}`);
726
+ const sliceCheck = rest.includes("--slices");
727
+
728
+ if (issues.length > 0) {
729
+ console.log(kleur.red(`✖ rr.json has ${issues.length} issue(s):`));
730
+ for (const i of issues) console.log(` · ${i}`);
731
+ process.exit(1);
732
+ }
733
+
734
+ console.log(kleur.green("✓ rr.json is valid."));
735
+ console.log(` template: ${kleur.cyan(rr.template?.slug ?? "(none)")}`);
736
+ console.log(` features: ${kleur.cyan(rr.features?.length ?? 0)}`);
737
+ console.log(` slices: ${kleur.cyan(rr.slices?.length ?? 0)}`);
738
+ console.log(` skills: ${kleur.cyan(rr.skills?.length ?? 0)}`);
739
+
740
+ if (sliceCheck) doctorSlices(target, rr);
741
+ }
742
+
743
+ function doctorSlices(target, rr) {
744
+ console.log(kleur.bold(`\n→ Slice composition check\n`));
745
+ const installed = rr.slices ?? [];
746
+ if (installed.length === 0) {
747
+ console.log(kleur.dim(" (no slices installed — nothing to check)"));
730
748
  return;
731
749
  }
732
- console.log(kleur.red(`✖ rr.json has ${issues.length} issue(s):`));
733
- for (const i of issues) console.log(` · ${i}`);
734
- process.exit(1);
750
+
751
+ const errors = [];
752
+ const warnings = [];
753
+ const sliceMap = new Map((manifest.slices ?? []).map((s) => [s.slug, s]));
754
+ const present = new Set(installed.map((s) => s.slug));
755
+
756
+ for (const inst of installed) {
757
+ const def = sliceMap.get(inst.slug);
758
+ if (!def) {
759
+ warnings.push(`${inst.slug}: not in kitab manifest (custom slice — skipping peer check)`);
760
+ continue;
761
+ }
762
+
763
+ // 1. Slice path exists locally
764
+ const slicePath = path.join(target, def.slicePath);
765
+ if (!existsSync(slicePath)) {
766
+ errors.push(`${inst.slug}: missing on disk at ${def.slicePath}`);
767
+ }
768
+ const sliceJsonPath = path.join(slicePath, "slice.json");
769
+ if (existsSync(slicePath) && !existsSync(sliceJsonPath)) {
770
+ errors.push(`${inst.slug}: slice.json missing at ${def.slicePath}/slice.json`);
771
+ }
772
+
773
+ // 2. Convex paths exist locally (when declared)
774
+ for (const cp of def.convexPaths ?? []) {
775
+ const cpAbs = path.join(target, cp);
776
+ if (!existsSync(cpAbs)) {
777
+ warnings.push(`${inst.slug}: convex path missing at ${cp} (lift again with 'npx rr add ${inst.slug}')`);
778
+ }
779
+ }
780
+
781
+ // 3. Peers transitively present
782
+ for (const peer of def.peers ?? []) {
783
+ if (!present.has(peer.slug)) {
784
+ errors.push(`${inst.slug} requires peer ${peer.slug} ${peer.range} — not installed`);
785
+ }
786
+ }
787
+ }
788
+
789
+ // 4. Convex table-name collision across installed slices
790
+ const tableOwners = new Map();
791
+ for (const inst of installed) {
792
+ const def = sliceMap.get(inst.slug);
793
+ if (!def) continue;
794
+ for (const cp of def.convexPaths ?? []) {
795
+ const schemaFile = path.join(target, cp, "schema.ts");
796
+ if (!existsSync(schemaFile)) continue;
797
+ const body = readFileSync(schemaFile, "utf8");
798
+ const matches = [...body.matchAll(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*defineTable\(/gm)];
799
+ for (const m of matches) {
800
+ const tName = m[1];
801
+ if (tableOwners.has(tName)) {
802
+ errors.push(`convex table "${tName}" declared by both ${tableOwners.get(tName)} and ${inst.slug}`);
803
+ } else {
804
+ tableOwners.set(tName, inst.slug);
805
+ }
806
+ }
807
+ }
808
+ }
809
+
810
+ if (warnings.length > 0) {
811
+ console.log(kleur.yellow(` ⚠ ${warnings.length} warning(s):`));
812
+ for (const w of warnings) console.log(` · ${w}`);
813
+ }
814
+ if (errors.length > 0) {
815
+ console.log(kleur.red(`\n ✖ ${errors.length} error(s):`));
816
+ for (const e of errors) console.log(` · ${e}`);
817
+ process.exit(1);
818
+ }
819
+ if (errors.length === 0 && warnings.length === 0) {
820
+ console.log(kleur.green(` ✓ ${installed.length} slice(s) — composition healthy`));
821
+ }
735
822
  }
736
823
 
737
824
  function runMcpHint() {
@@ -910,6 +997,17 @@ async function runLift(rest) {
910
997
  await maybeRunShadcnAdd({ slug: parsed.slug ?? "lifted", shadcnComponents: plan.shadcn }, target, false);
911
998
  }
912
999
 
1000
+ // Register the slice in the consumer's rr.json (if present).
1001
+ if (parsed.kind === "rahman" && rrExists(target)) {
1002
+ const slice = (manifest.slices ?? []).find((s) => s.slug === parsed.slug);
1003
+ if (slice) {
1004
+ const rr = readRr(target);
1005
+ rrAddSlice(rr, parsed.slug, { version: slice.version, category: slice.category });
1006
+ writeRr(rr, target);
1007
+ console.log(kleur.dim(` rr.json: slices += ${parsed.slug}@${slice.version}`));
1008
+ }
1009
+ }
1010
+
913
1011
  console.log(`\n${kleur.green("✓")} Lift complete.`);
914
1012
  if (plan.env.length > 0) {
915
1013
  console.log(`${kleur.bold("Don't forget:")} set the env vars listed above before running.\n`);
@@ -1022,25 +1120,72 @@ async function runPublishSlice(rest) {
1022
1120
  ps.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`validate-slice exited ${code}`))));
1023
1121
  });
1024
1122
 
1123
+ // Read the slice's metadata so we can prefill the PR title + body.
1124
+ const sliceMeta = JSON.parse(readFileSync(path.join(abs, "slice.json"), "utf8"));
1125
+
1025
1126
  if (!flags["open-pr"]) {
1026
- console.log(`\n${kleur.yellow("dry-run — pass --open-pr to actually open a PR upstream.")}`);
1127
+ console.log(`\n${kleur.yellow("dry-run — pass --open-pr to scaffold the PR command.")}`);
1027
1128
  return;
1028
1129
  }
1029
1130
 
1030
- // Open a PR. We don't push branches automatically — instruct the user.
1031
- console.log(`\n${kleur.bold("Opening PR upstream:")}`);
1131
+ const ghAvailable = await checkGhInstalled();
1132
+
1133
+ if (!ghAvailable) {
1134
+ console.log(`\n${kleur.yellow("⚠ `gh` CLI not found.")}`);
1135
+ console.log(`Install it: ${kleur.cyan("https://cli.github.com")} then re-run with --open-pr.`);
1136
+ console.log(`Manual fallback steps:`);
1137
+ console.log(`
1138
+ 1. Fork ${kleur.cyan("https://github.com/rahmanef63/resource-site")} on GitHub.
1139
+ 2. Clone your fork: ${kleur.cyan("git clone https://github.com/<you>/resource-site && cd resource-site")}
1140
+ 3. Copy slice: ${kleur.cyan(`cp -r ${abs} frontend/slices/${sliceMeta.slug}`)}
1141
+ 4. Copy convex half: ${kleur.cyan(`cp -r ${abs.replace("frontend/slices", "convex/features")} convex/features/${sliceMeta.slug}`)} (if exists)
1142
+ 5. Add entry to ${kleur.cyan("lib/content/slices.ts")}
1143
+ 6. ${kleur.cyan("npm run slices:check")}
1144
+ 7. ${kleur.cyan(`git checkout -b feat/slice-${sliceMeta.slug} && git add -A && git commit -m "feat(slices): add ${sliceMeta.slug}" && git push -u origin feat/slice-${sliceMeta.slug}`)}
1145
+ 8. Open PR via GitHub UI.
1146
+ `);
1147
+ return;
1148
+ }
1149
+
1150
+ // gh installed — emit ready-to-run gh commands.
1151
+ const branchName = `feat/slice-${sliceMeta.slug}`;
1152
+ const title = `feat(slices): add ${sliceMeta.slug}`;
1153
+ const body = [
1154
+ `Adds the **${sliceMeta.title}** slice (${sliceMeta.category}, v${sliceMeta.version}).`,
1155
+ "",
1156
+ sliceMeta.description,
1157
+ "",
1158
+ "Validated locally with `npm run slices:check`.",
1159
+ "",
1160
+ "🤖 Generated with [`rahman-resources publish-slice`](https://github.com/rahmanef63/resource-site)",
1161
+ ].join("\n");
1162
+
1163
+ console.log(`\n${kleur.bold("✓")} ${kleur.green("gh CLI detected.")} Run these from inside YOUR fork of the kitab repo:`);
1032
1164
  console.log(`
1033
- 1. Fork ${kleur.cyan("https://github.com/rahmanef63/resource-site")} (if not already).
1034
- 2. Clone your fork, copy this slice into ${kleur.cyan(`frontend/slices/<slug>/`)} and the
1035
- convex half into ${kleur.cyan(`convex/features/<slug>/`)}.
1036
- 3. Add an entry to ${kleur.cyan("lib/content/slices.ts")}.
1037
- 4. Run ${kleur.cyan("npm run slices:check")} locally.
1038
- 5. ${kleur.cyan(`gh pr create --title "feat(slices): add <slug>" --body "..."`)}.
1039
-
1040
- (Auto-PR via gh CLI not yet implemented — coming in a future release.)
1165
+ # Inside your forked clone of rahmanef63/resource-site, copy the slice in:
1166
+ ${kleur.cyan(`cp -r ${abs} frontend/slices/${sliceMeta.slug}`)}
1167
+ ${kleur.cyan(`# Add ${sliceMeta.slug} entry to lib/content/slices.ts`)}
1168
+ ${kleur.cyan(`npm run slices:check`)}
1169
+
1170
+ # Then open the PR (gh handles fork + push):
1171
+ ${kleur.cyan(`git checkout -b ${branchName}`)}
1172
+ ${kleur.cyan(`git add -A && git commit -m "${title}"`)}
1173
+ ${kleur.cyan(`gh pr create --repo rahmanef63/resource-site \\
1174
+ --title "${title}" \\
1175
+ --body ${JSON.stringify(body)}`)}
1176
+
1177
+ ${kleur.dim("(If you haven't forked yet, run `gh repo fork rahmanef63/resource-site --clone` first.)")}
1041
1178
  `);
1042
1179
  }
1043
1180
 
1181
+ async function checkGhInstalled() {
1182
+ return new Promise((resolve) => {
1183
+ const ps = spawn("gh", ["--version"], { stdio: "ignore", shell: true });
1184
+ ps.on("error", () => resolve(false));
1185
+ ps.on("exit", (code) => resolve(code === 0));
1186
+ });
1187
+ }
1188
+
1044
1189
  // ─── helpers ──────────────────────────────────────────────────────────────
1045
1190
 
1046
1191
  async function pull(repoPath, dest) {
package/lib/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 2,
3
- "generatedAt": "2026-05-09T13:25:41.865Z",
3
+ "generatedAt": "2026-05-09T13:46:31.069Z",
4
4
  "repo": "rahmanef63/resource-site",
5
5
  "branch": "main",
6
6
  "layouts": [
@@ -64,6 +64,20 @@
64
64
  }
65
65
  }
66
66
  },
67
+ "slices": {
68
+ "type": "array",
69
+ "description": "Tier-3 portable slice references — each entry mirrors a slice that has been lifted into this project.",
70
+ "items": {
71
+ "type": "object",
72
+ "required": ["slug"],
73
+ "properties": {
74
+ "slug": { "type": "string" },
75
+ "version": { "type": "string" },
76
+ "category": { "type": "string" },
77
+ "addedAt": { "type": "string", "format": "date" }
78
+ }
79
+ }
80
+ },
67
81
  "skills": {
68
82
  "type": "array",
69
83
  "items": {
package/lib/rr.mjs CHANGED
@@ -36,6 +36,7 @@ export const DEFAULT_RR = {
36
36
  },
37
37
  template: null,
38
38
  features: [],
39
+ slices: [],
39
40
  skills: [],
40
41
  auth: { provider: "convex-auth" },
41
42
  convex: { self_hosted: true },
@@ -91,6 +92,23 @@ export function addFeature(rr, slug, version = "main") {
91
92
  return rr;
92
93
  }
93
94
 
95
+ export function addSlice(rr, slug, opts = {}) {
96
+ rr.slices = rr.slices ?? [];
97
+ const existing = rr.slices.find((s) => s.slug === slug);
98
+ if (existing) {
99
+ if (opts.version) existing.version = opts.version;
100
+ if (opts.category) existing.category = opts.category;
101
+ return rr;
102
+ }
103
+ rr.slices.push({ slug, version: opts.version ?? "main", category: opts.category, addedAt: today() });
104
+ return rr;
105
+ }
106
+
107
+ export function removeSlice(rr, slug) {
108
+ rr.slices = (rr.slices ?? []).filter((s) => s.slug !== slug);
109
+ return rr;
110
+ }
111
+
94
112
  export function addSkill(rr, slug, source = "anthropics", version = "main") {
95
113
  rr.skills = rr.skills ?? [];
96
114
  if (rr.skills.find((s) => s.slug === slug && s.source === source)) return rr;
@@ -125,6 +143,7 @@ export function validateRr(rr) {
125
143
  issues.push(`layout.kind must be vertical-slice|feature-folders (got ${rr.layout.kind})`);
126
144
  }
127
145
  if (rr.features && !Array.isArray(rr.features)) issues.push("features must be an array");
146
+ if (rr.slices && !Array.isArray(rr.slices)) issues.push("slices must be an array");
128
147
  if (rr.skills && !Array.isArray(rr.skills)) issues.push("skills must be an array");
129
148
  return issues;
130
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rahman-resources",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Scaffolder + installer for Rahman Resources kitab — npx rahman-resources init/add/lift/scaffold-slice/publish-slice. Tier-3 portable feature slices + manifest + skills + CRUD workflows.",
5
5
  "type": "module",
6
6
  "license": "MIT",