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 +165 -20
- package/lib/manifest.json +1 -1
- package/lib/rr-schema.json +14 -0
- package/lib/rr.mjs +19 -0
- package/package.json +1 -1
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
console.log(
|
|
729
|
-
console.log(`
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
|
1127
|
+
console.log(`\n${kleur.yellow("dry-run — pass --open-pr to scaffold the PR command.")}`);
|
|
1027
1128
|
return;
|
|
1028
1129
|
}
|
|
1029
1130
|
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
(
|
|
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
package/lib/rr-schema.json
CHANGED
|
@@ -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.
|
|
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",
|