rahman-resources 1.13.3 → 1.14.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/bin/cli.js +5 -1
- package/bin/migrate-load.mjs +19 -9
- package/bin/migrate.mjs +1 -1
- package/lib/compose-solver-loader.mjs +14 -40
- package/lib/compose-solver-resolve.mjs +3 -3
- package/lib/load-contract.mjs +33 -0
- package/lib/slice-schema.json +4 -0
- package/lib/snapshot.mjs +11 -57
- package/package.json +4 -4
package/bin/cli.js
CHANGED
|
@@ -167,6 +167,10 @@ ${kleur.dim("Consumer's components/ui/ + lib/utils.ts (shadcn) are never touched
|
|
|
167
167
|
|
|
168
168
|
// ─── flag parsing ─────────────────────────────────────────────────────────
|
|
169
169
|
|
|
170
|
+
// Flags that take a value (`--flag x`). Anything else is boolean, so a boolean
|
|
171
|
+
// flag placed before a positional no longer swallows that positional.
|
|
172
|
+
const VALUE_FLAGS = new Set(["target", "template", "category", "at", "skills", "features"]);
|
|
173
|
+
|
|
170
174
|
function parseFlags(rest) {
|
|
171
175
|
const positional = [];
|
|
172
176
|
const flags = {};
|
|
@@ -175,7 +179,7 @@ function parseFlags(rest) {
|
|
|
175
179
|
if (a.startsWith("--")) {
|
|
176
180
|
const key = a.slice(2);
|
|
177
181
|
const next = rest[i + 1];
|
|
178
|
-
if (next && !next.startsWith("--")) { flags[key] = next; i++; }
|
|
182
|
+
if (VALUE_FLAGS.has(key) && next && !next.startsWith("--")) { flags[key] = next; i++; }
|
|
179
183
|
else flags[key] = true;
|
|
180
184
|
} else {
|
|
181
185
|
positional.push(a);
|
package/bin/migrate-load.mjs
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { loadSliceContract } from "../lib/load-contract.mjs";
|
|
12
|
+
|
|
13
|
+
// Flags that take a value; anything else is boolean (mirrors cli.js parseFlags).
|
|
14
|
+
const VALUE_FLAGS = new Set(["from", "to", "repo-root"]);
|
|
11
15
|
|
|
12
16
|
export function parseFlags(rest) {
|
|
13
17
|
const positional = [];
|
|
@@ -17,7 +21,7 @@ export function parseFlags(rest) {
|
|
|
17
21
|
if (a.startsWith("--")) {
|
|
18
22
|
const key = a.slice(2);
|
|
19
23
|
const next = rest[i + 1];
|
|
20
|
-
if (next && !next.startsWith("--")) {
|
|
24
|
+
if (VALUE_FLAGS.has(key) && next && !next.startsWith("--")) {
|
|
21
25
|
flags[key] = next;
|
|
22
26
|
i++;
|
|
23
27
|
} else {
|
|
@@ -51,26 +55,32 @@ const SLICE_ROOTS = [
|
|
|
51
55
|
["template-base", "frontend", "slices"],
|
|
52
56
|
];
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
// The CURRENT contract is folded into slice.json — locate the slice dir by its
|
|
59
|
+
// slice.json (the .ts no longer exists in the working tree post-cutover).
|
|
60
|
+
function resolveSliceDir(repoRoot, slug) {
|
|
55
61
|
for (const segs of SLICE_ROOTS) {
|
|
56
|
-
const
|
|
57
|
-
if (existsSync(
|
|
62
|
+
const dir = path.join(repoRoot, ...segs, slug);
|
|
63
|
+
if (existsSync(path.join(dir, "slice.json"))) return dir;
|
|
58
64
|
}
|
|
59
65
|
return null;
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
// HISTORIC leg still pulls the OLD slice.contract.ts from git history, so the
|
|
69
|
+
// rel path is the .ts path — but gate on the slice's slice.json existing today
|
|
70
|
+
// (NOT the working-tree .ts, which is gone post-cutover).
|
|
62
71
|
function resolveContractRelPath(repoRoot, slug) {
|
|
63
72
|
for (const segs of SLICE_ROOTS) {
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
if (existsSync(path.join(repoRoot, ...segs, slug, "slice.json"))) {
|
|
74
|
+
return [...segs, slug, "slice.contract.ts"].join("/");
|
|
75
|
+
}
|
|
66
76
|
}
|
|
67
77
|
return null;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
export function loadCurrentContract(repoRoot, slug) {
|
|
71
|
-
const
|
|
72
|
-
if (!
|
|
73
|
-
return
|
|
81
|
+
const dir = resolveSliceDir(repoRoot, slug);
|
|
82
|
+
if (!dir) return null;
|
|
83
|
+
return loadSliceContract(dir);
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
/**
|
package/bin/migrate.mjs
CHANGED
|
@@ -80,7 +80,7 @@ export async function runMigrate(rest) {
|
|
|
80
80
|
process.stderr.write(
|
|
81
81
|
kleur.red(
|
|
82
82
|
`migrate: cannot load current contract for "${slug}". ` +
|
|
83
|
-
`Expected frontend/slices/${slug}/slice.
|
|
83
|
+
`Expected a contract block in frontend/slices/${slug}/slice.json (or template-base/frontend/slices/${slug}/slice.json).\n`,
|
|
84
84
|
),
|
|
85
85
|
);
|
|
86
86
|
process.exit(1);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
// compose-solver-loader.mjs — contract discovery
|
|
1
|
+
// compose-solver-loader.mjs — contract discovery loader.
|
|
2
2
|
//
|
|
3
3
|
// Split out from compose-solver.mjs to keep the pure solver < 200 LOC.
|
|
4
|
-
// I/O entry point only — discovers
|
|
5
|
-
//
|
|
4
|
+
// I/O entry point only — discovers slices under known roots and reads each
|
|
5
|
+
// folded contract from slice.json (`contract` block) via the shared adapter.
|
|
6
|
+
// No tsx subprocess: the contract lives in slice.json since the Phase-2 fold.
|
|
6
7
|
|
|
7
8
|
import { readdir } from "node:fs/promises";
|
|
8
9
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { spawnSync } from "node:child_process";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { loadSliceContract } from "./load-contract.mjs";
|
|
11
12
|
|
|
12
13
|
const SLICE_ROOT_GLOBS = [
|
|
13
14
|
["frontend", "slices"],
|
|
@@ -15,11 +16,10 @@ const SLICE_ROOT_GLOBS = [
|
|
|
15
16
|
];
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
* Discover every
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* down the
|
|
22
|
-
* those errors.
|
|
19
|
+
* Discover every slice with a folded contract under the kitab's known slice
|
|
20
|
+
* roots. Returns a Map<slug, SliceContract>. Slices without a slice.json
|
|
21
|
+
* contract block are silently skipped so a single uncontracted slice doesn't
|
|
22
|
+
* take down the solver — `npm run audit:slices` surfaces shape errors.
|
|
23
23
|
*
|
|
24
24
|
* @param {string} repoRoot Absolute path to the kitab repo root.
|
|
25
25
|
* @returns {Promise<Map<string, import("./contract").SliceContract>>}
|
|
@@ -27,9 +27,8 @@ const SLICE_ROOT_GLOBS = [
|
|
|
27
27
|
export async function loadAllContracts(repoRoot) {
|
|
28
28
|
/** @type {Map<string, import("./contract").SliceContract>} */
|
|
29
29
|
const out = new Map();
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const contract = loadContractFile(repoRoot, filePath);
|
|
30
|
+
for (const dir of await discoverSliceDirs(repoRoot)) {
|
|
31
|
+
const contract = loadSliceContract(dir);
|
|
33
32
|
if (contract && typeof contract.id === "string") {
|
|
34
33
|
out.set(contract.id, contract);
|
|
35
34
|
}
|
|
@@ -37,7 +36,7 @@ export async function loadAllContracts(repoRoot) {
|
|
|
37
36
|
return out;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
async function
|
|
39
|
+
async function discoverSliceDirs(repoRoot) {
|
|
41
40
|
const found = [];
|
|
42
41
|
for (const segs of SLICE_ROOT_GLOBS) {
|
|
43
42
|
const root = path.join(repoRoot, ...segs);
|
|
@@ -46,34 +45,9 @@ async function discoverContractFiles(repoRoot) {
|
|
|
46
45
|
for (const entry of entries) {
|
|
47
46
|
if (!entry.isDirectory()) continue;
|
|
48
47
|
if (entry.name.startsWith("_")) continue;
|
|
49
|
-
const
|
|
50
|
-
if (existsSync(
|
|
48
|
+
const dir = path.join(root, entry.name);
|
|
49
|
+
if (existsSync(path.join(dir, "slice.json"))) found.push(dir);
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
return found;
|
|
54
53
|
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Dynamic-import a .ts contract file via `npx tsx -e` and JSON.parse the
|
|
58
|
-
* stringified `contract` export. Returns null on any failure.
|
|
59
|
-
*/
|
|
60
|
-
function loadContractFile(repoRoot, filePath) {
|
|
61
|
-
const rel = "./" + path.relative(repoRoot, filePath);
|
|
62
|
-
const code = [
|
|
63
|
-
`import(${JSON.stringify(rel)})`,
|
|
64
|
-
` .then(m => { const c = m.contract || (m.default && m.default.contract); if (!c) { process.exit(2); } process.stdout.write(JSON.stringify(c)); })`,
|
|
65
|
-
` .catch(() => process.exit(3));`,
|
|
66
|
-
].join("\n");
|
|
67
|
-
const res = spawnSync("npx", ["--no-install", "tsx", "-e", code], {
|
|
68
|
-
cwd: repoRoot,
|
|
69
|
-
encoding: "utf8",
|
|
70
|
-
});
|
|
71
|
-
if (res.status === 0 && res.stdout) {
|
|
72
|
-
try {
|
|
73
|
-
return JSON.parse(res.stdout);
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
@@ -32,7 +32,7 @@ export function buildInitialCandidates({
|
|
|
32
32
|
{
|
|
33
33
|
type: "uncontracted",
|
|
34
34
|
slug,
|
|
35
|
-
detail: `Slice "${slug}" has no registered
|
|
35
|
+
detail: `Slice "${slug}" has no registered contract — accepted under allowUnknownSlices, but conflict checks are skipped for it.`,
|
|
36
36
|
severity: "warning",
|
|
37
37
|
},
|
|
38
38
|
slug,
|
|
@@ -41,13 +41,13 @@ export function buildInitialCandidates({
|
|
|
41
41
|
candidateSet.add(slug);
|
|
42
42
|
candidateOrder.push(slug);
|
|
43
43
|
notes.set(slug, "uncontracted");
|
|
44
|
-
proof.push(`! ${slug}: accepted as uncontracted (no slice.
|
|
44
|
+
proof.push(`! ${slug}: accepted as uncontracted (no contract (slice.json); skipping conflict checks)`);
|
|
45
45
|
} else {
|
|
46
46
|
record(
|
|
47
47
|
{
|
|
48
48
|
type: "missing-dep",
|
|
49
49
|
slug,
|
|
50
|
-
detail: `Contract not found for "${slug}" — no
|
|
50
|
+
detail: `Contract not found for "${slug}" — no registered contract (strict mode).`,
|
|
51
51
|
severity: "blocker",
|
|
52
52
|
},
|
|
53
53
|
slug,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// load-contract.mjs — read a slice's folded composition contract from its
|
|
2
|
+
// slice.json (Phase 2 SSOT). Replaces parsing slice.contract.ts: the contract
|
|
3
|
+
// payload now lives under slice.json `contract`, with id/version omitted there
|
|
4
|
+
// (they are the slice.json scalars). This adapter reattaches them so callers
|
|
5
|
+
// get the same SliceContract shape the .ts used to export.
|
|
6
|
+
//
|
|
7
|
+
// Returns undefined when the slice has no slice.json or no contract block —
|
|
8
|
+
// callers treat that exactly like a missing slice.contract.ts before.
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} sliceDir Absolute path to frontend/slices/<slug>/.
|
|
15
|
+
* @returns {object|undefined} { id, version, requires?, provides?, conflicts?,
|
|
16
|
+
* migrationFrom?, generalization? } or undefined.
|
|
17
|
+
*/
|
|
18
|
+
export function loadSliceContract(sliceDir) {
|
|
19
|
+
return contractFromJson(path.join(sliceDir, "slice.json"));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Same, given a direct path to a slice.json file. */
|
|
23
|
+
export function contractFromJson(jsonPath) {
|
|
24
|
+
if (!existsSync(jsonPath)) return undefined;
|
|
25
|
+
let j;
|
|
26
|
+
try {
|
|
27
|
+
j = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (!j.contract || typeof j.contract !== "object") return undefined;
|
|
32
|
+
return { id: j.slug, version: j.version, ...j.contract };
|
|
33
|
+
}
|
package/lib/slice-schema.json
CHANGED
|
@@ -315,6 +315,10 @@
|
|
|
315
315
|
"deprecated": {
|
|
316
316
|
"type": "string",
|
|
317
317
|
"description": "Successor slug. This slice was merged into the named slice as variants; install resolves there via manifest.aliases."
|
|
318
|
+
},
|
|
319
|
+
"contract": {
|
|
320
|
+
"type": "object",
|
|
321
|
+
"description": "Folded Phase-A composition contract (Phase 2 hybrid fold, 2026-06-21): requires/provides/conflicts/migrationFrom/generalization moved from slice.contract.ts. id+version omitted (== slug/version). requires.deps is verbatim pending per-slice normalization, so this is intentionally loose; strict invariants move to validate-slice when the .ts is retired."
|
|
318
322
|
}
|
|
319
323
|
}
|
|
320
324
|
}
|
package/lib/snapshot.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// snapshot.mjs — build a SliceSnapshot from a slice directory.
|
|
2
2
|
//
|
|
3
3
|
// Walks the directory recursively, keeps only typical slice file extensions,
|
|
4
|
-
// reads
|
|
5
|
-
//
|
|
6
|
-
// dotfiles to keep snapshots stable across machines.
|
|
4
|
+
// and reads the folded composition contract from slice.json (`contract` block,
|
|
5
|
+
// id/version reattached from the slice.json scalars). Skips node_modules,
|
|
6
|
+
// .kitab, and dotfiles to keep snapshots stable across machines.
|
|
7
7
|
|
|
8
8
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
-
import { spawnSync } from "node:child_process";
|
|
11
10
|
import path from "node:path";
|
|
12
11
|
|
|
13
12
|
const INCLUDED_EXT = new Set([".ts", ".tsx", ".mjs", ".js", ".jsx", ".json", ".md", ".css"]);
|
|
@@ -33,18 +32,18 @@ export async function snapshotFromDir(slug, dir) {
|
|
|
33
32
|
const files = {};
|
|
34
33
|
await walk(dir, dir, files);
|
|
35
34
|
|
|
35
|
+
// The composition contract is folded into slice.json (`contract` block) since
|
|
36
|
+
// Phase 2; reattach id/version from the slice.json scalars to keep the same
|
|
37
|
+
// SliceContract shape the old slice.contract.ts exported.
|
|
36
38
|
let contract;
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
contract = loadContract(contractPath) ?? regexLoadContract(await readFile(contractPath, "utf8"));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Version preference: contract.version → slice.json.version → "0.0.0".
|
|
43
|
-
let version = contract?.version ?? "0.0.0";
|
|
44
|
-
if (!contract?.version && files["slice.json"]) {
|
|
39
|
+
let version = "0.0.0";
|
|
40
|
+
if (files["slice.json"]) {
|
|
45
41
|
try {
|
|
46
42
|
const meta = JSON.parse(files["slice.json"]);
|
|
47
43
|
if (typeof meta.version === "string") version = meta.version;
|
|
44
|
+
if (meta.contract && typeof meta.contract === "object") {
|
|
45
|
+
contract = { id: meta.slug, version: meta.version, ...meta.contract };
|
|
46
|
+
}
|
|
48
47
|
} catch {
|
|
49
48
|
/* ignore */
|
|
50
49
|
}
|
|
@@ -79,48 +78,3 @@ async function walk(root, dir, out) {
|
|
|
79
78
|
out[rel] = await readFile(full, "utf8");
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Mirror of validate-contract.mjs's tsx loader. Returns the parsed contract
|
|
85
|
-
* or undefined on failure.
|
|
86
|
-
*/
|
|
87
|
-
function loadContract(filePath) {
|
|
88
|
-
const code = [
|
|
89
|
-
`import(${JSON.stringify(filePath)})`,
|
|
90
|
-
` .then(m => { const c = m.contract || (m.default && m.default.contract); if (!c) { process.exit(2); } process.stdout.write(JSON.stringify(c)); })`,
|
|
91
|
-
` .catch(e => { process.stderr.write(String(e && e.message || e)); process.exit(3); });`,
|
|
92
|
-
].join("\n");
|
|
93
|
-
try {
|
|
94
|
-
const res = spawnSync("npx", ["--no-install", "tsx", "-e", code], {
|
|
95
|
-
encoding: "utf8",
|
|
96
|
-
});
|
|
97
|
-
if (res.status === 0 && res.stdout) {
|
|
98
|
-
return JSON.parse(res.stdout);
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
/* tsx missing or failed */
|
|
102
|
-
}
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Best-effort regex fallback: extract the literal-ish object passed to
|
|
108
|
-
* `defineSliceContract({ ... })`. We can't fully parse TS, but for the kitab's
|
|
109
|
-
* contracts (small static literals) a JSON5-ish coerce works for the basic
|
|
110
|
-
* shape we need here.
|
|
111
|
-
*/
|
|
112
|
-
function regexLoadContract(src) {
|
|
113
|
-
const m = src.match(/defineSliceContract\s*\(\s*(\{[\s\S]*?\})\s*\)\s*;?\s*$/m);
|
|
114
|
-
if (!m) return undefined;
|
|
115
|
-
// Replace single quotes, strip trailing commas, quote keys.
|
|
116
|
-
const body = m[1]
|
|
117
|
-
.replace(/'([^'\\]*)'/g, '"$1"')
|
|
118
|
-
.replace(/,(\s*[}\]])/g, "$1")
|
|
119
|
-
.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":')
|
|
120
|
-
.replace(/\/\/.*$/gm, "");
|
|
121
|
-
try {
|
|
122
|
-
return JSON.parse(body);
|
|
123
|
-
} catch {
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rahman-resources",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"description": "Rahman Resources (rr) — shadcn-style installer for vertical slices. `npx resources add <slug>` copies slice into your project's `slices/<slug>/`. You own the files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"gen": "node scripts/gen-manifest.mjs",
|
|
30
30
|
"validate": "node scripts/validate.mjs",
|
|
31
31
|
"validate:slices": "node scripts/validate-slice.mjs",
|
|
32
|
-
"validate:parity": "node scripts/
|
|
32
|
+
"validate:parity": "node ../../scripts/features/gen-slice-catalog.mjs --check",
|
|
33
33
|
"validate:structure": "node scripts/validate-structure.mjs",
|
|
34
|
-
"validate:all": "node scripts/validate.mjs && node scripts/validate-slice.mjs --check && node scripts/
|
|
34
|
+
"validate:all": "node scripts/validate.mjs && node scripts/validate-slice.mjs --check && node ../../scripts/features/gen-slice-catalog.mjs --check && node scripts/validate-structure.mjs",
|
|
35
35
|
"sync:skills": "node scripts/sync-skills.mjs",
|
|
36
36
|
"sync:skills:check": "node scripts/sync-skills.mjs --check",
|
|
37
|
-
"prepublishOnly": "node scripts/sync-skills.mjs --check && node scripts/validate.mjs && node scripts/validate-slice.mjs --check && node scripts/
|
|
37
|
+
"prepublishOnly": "node scripts/sync-skills.mjs --check && node scripts/validate.mjs && node scripts/validate-slice.mjs --check && node ../../scripts/features/gen-slice-catalog.mjs --check && node scripts/validate-structure.mjs"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"kleur": "^4.1.5",
|