rahman-resources 1.4.0 → 1.6.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/graph.mjs CHANGED
@@ -6,15 +6,10 @@
6
6
  // rr graph <slug> → full DNA for a single slice + ASCII lineage tree
7
7
  // rr graph <slug?> --json → emit JSON instead of ASCII
8
8
  //
9
- // Dispatched from bin/cli.js. Imports from ../lib/dna.mjs.
9
+ // Dispatched from bin/cli.js. ASCII renderers live in graph-render.mjs.
10
10
 
11
- import kleur from "kleur";
12
-
13
- import {
14
- buildLineageGraph,
15
- listAllDNA,
16
- readDNA,
17
- } from "../lib/dna.mjs";
11
+ import { buildLineageGraph, listAllDNA, readDNA } from "../lib/dna.mjs";
12
+ import { printSummary, printSliceTree } from "./graph-render.mjs";
18
13
 
19
14
  /**
20
15
  * Entry point invoked by cli.js with the post-`graph` argv tail.
@@ -71,177 +66,3 @@ function parseFlags(rest) {
71
66
  }
72
67
  return { positional, flags };
73
68
  }
74
-
75
- // ─── ASCII renderers ──────────────────────────────────────────────────────
76
-
77
- function printSummary() {
78
- const slices = listAllDNA();
79
- if (slices.length === 0) {
80
- console.log(
81
- kleur.yellow("No DNA files in .kitab/lineage/. ") +
82
- kleur.dim("Seed some via `rr graph` integrations or manual write."),
83
- );
84
- return;
85
- }
86
- console.log(kleur.bold(`\nDNA graph — ${slices.length} slice(s)\n`));
87
-
88
- // Header table: slug | lineage count | consumers | newest source
89
- const rows = [];
90
- for (const dna of slices) {
91
- const newest = dna.lineage[dna.lineage.length - 1];
92
- rows.push({
93
- slug: dna.id,
94
- lineage: dna.lineage.length,
95
- consumers: Object.keys(dna.consumers).length,
96
- source: newest ? newest.from : "—",
97
- });
98
- }
99
- const w1 = Math.max(4, ...rows.map((r) => r.slug.length));
100
- const w4 = Math.max(6, ...rows.map((r) => String(r.source).length));
101
- console.log(
102
- " " +
103
- kleur.bold("slug".padEnd(w1)) +
104
- " " +
105
- kleur.bold("hops".padEnd(4)) +
106
- " " +
107
- kleur.bold("adopts".padEnd(6)) +
108
- " " +
109
- kleur.bold("latest source".padEnd(w4)),
110
- );
111
- console.log(
112
- " " +
113
- kleur.dim("-".repeat(w1)) +
114
- " " +
115
- kleur.dim("----") +
116
- " " +
117
- kleur.dim("------") +
118
- " " +
119
- kleur.dim("-".repeat(w4)),
120
- );
121
- for (const r of rows) {
122
- console.log(
123
- " " +
124
- kleur.cyan(r.slug.padEnd(w1)) +
125
- " " +
126
- String(r.lineage).padEnd(4) +
127
- " " +
128
- String(r.consumers).padEnd(6) +
129
- " " +
130
- kleur.dim(r.source),
131
- );
132
- }
133
-
134
- // Consumer adoption matrix.
135
- /** @type {Set<string>} */
136
- const consumerSet = new Set();
137
- for (const dna of slices) {
138
- for (const c of Object.keys(dna.consumers)) consumerSet.add(c);
139
- }
140
- const consumers = [...consumerSet].sort();
141
- if (consumers.length > 0) {
142
- console.log(kleur.bold(`\nAdoption matrix\n`));
143
- const cw = Math.max(4, ...rows.map((r) => r.slug.length));
144
- console.log(
145
- " " +
146
- " ".repeat(cw) +
147
- " " +
148
- consumers
149
- .map((c) => kleur.bold(c.slice(0, 10).padEnd(10)))
150
- .join(" "),
151
- );
152
- for (const dna of slices) {
153
- const cells = consumers
154
- .map((c) => {
155
- const ad = dna.consumers[c];
156
- if (!ad) return kleur.dim("·".padEnd(10));
157
- const drift = ad.drift_score;
158
- const tag = `v${ad.version} ${drift}%`.slice(0, 10).padEnd(10);
159
- if (drift >= 40) return kleur.red(tag);
160
- if (drift >= 15) return kleur.yellow(tag);
161
- return kleur.green(tag);
162
- })
163
- .join(" ");
164
- console.log(" " + kleur.cyan(dna.id.padEnd(cw)) + " " + cells);
165
- }
166
- console.log(
167
- kleur.dim(
168
- `\n legend: green <15% drift, yellow 15-39%, red >=40%, · = not adopted`,
169
- ),
170
- );
171
- }
172
-
173
- console.log(
174
- kleur.dim(
175
- `\nRun \`rr graph <slug>\` for the full lineage tree, \`--json\` for machine output.\n`,
176
- ),
177
- );
178
- }
179
-
180
- function printSliceTree(slug) {
181
- const dna = readDNA(slug);
182
- if (!dna) {
183
- console.error(
184
- kleur.red(`✖ No DNA found for "${slug}".`) +
185
- kleur.dim(` (expected .kitab/lineage/${slug}.dna.json)`),
186
- );
187
- process.exit(1);
188
- }
189
- console.log(
190
- `\n${kleur.bold(dna.id)} ${kleur.dim(
191
- `created ${dna.created_at}`,
192
- )}\n`,
193
- );
194
-
195
- // Lineage chain — chronological top-down tree.
196
- console.log(kleur.bold("Lineage"));
197
- if (dna.lineage.length === 0) {
198
- console.log(" " + kleur.dim("(no lineage entries)"));
199
- } else {
200
- const sorted = [...dna.lineage].sort((a, b) =>
201
- String(a.at).localeCompare(String(b.at)),
202
- );
203
- sorted.forEach((entry, i) => {
204
- const isLast = i === sorted.length - 1;
205
- const bullet = isLast ? "└─" : "├─";
206
- const cont = isLast ? " " : "│ ";
207
- console.log(
208
- ` ${kleur.dim(bullet)} ${kleur.cyan(entry.from)}` +
209
- (entry.to ? ` ${kleur.dim("→")} ${kleur.green(entry.to)}` : "") +
210
- ` ${kleur.dim(entry.at)}`,
211
- );
212
- if (entry.transforms?.length) {
213
- console.log(
214
- ` ${kleur.dim(cont)} ${kleur.dim("transforms:")} ${entry.transforms.join(", ")}`,
215
- );
216
- }
217
- if (entry.actor) {
218
- console.log(` ${kleur.dim(cont)} ${kleur.dim("actor:")} ${entry.actor}`);
219
- }
220
- });
221
- }
222
-
223
- // Consumer adoption rows.
224
- console.log(`\n${kleur.bold("Consumers")}`);
225
- const consumers = Object.entries(dna.consumers);
226
- if (consumers.length === 0) {
227
- console.log(" " + kleur.dim("(no consumers recorded)"));
228
- } else {
229
- const w = Math.max(4, ...consumers.map(([c]) => c.length));
230
- for (const [name, ad] of consumers) {
231
- const drift = ad.drift_score;
232
- const driftStr =
233
- drift >= 40
234
- ? kleur.red(`${drift}%`)
235
- : drift >= 15
236
- ? kleur.yellow(`${drift}%`)
237
- : kleur.green(`${drift}%`);
238
- const sync = ad.last_synced_at ? ` synced ${ad.last_synced_at}` : "";
239
- console.log(
240
- ` ${kleur.cyan(name.padEnd(w))} v${ad.version} drift ${driftStr} ${kleur.dim(
241
- `adopted ${ad.adopted_at}${sync}`,
242
- )}`,
243
- );
244
- }
245
- }
246
- console.log("");
247
- }
@@ -0,0 +1,189 @@
1
+ // migrate-load.mjs — contract-loading helpers for `rr migrate`.
2
+ //
3
+ // Extracted from migrate.mjs. Resolves on-disk vs historic contract files
4
+ // (via git show) and evaluates them with `npx tsx` to JSON-serialize the
5
+ // named `contract` export. Also exposes argv parsing + repo-root finder
6
+ // shared with the dispatcher.
7
+
8
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
9
+ import { spawnSync } from "node:child_process";
10
+ import path from "node:path";
11
+
12
+ export function parseFlags(rest) {
13
+ const positional = [];
14
+ const flags = {};
15
+ for (let i = 0; i < rest.length; i++) {
16
+ const a = rest[i];
17
+ if (a.startsWith("--")) {
18
+ const key = a.slice(2);
19
+ const next = rest[i + 1];
20
+ if (next && !next.startsWith("--")) {
21
+ flags[key] = next;
22
+ i++;
23
+ } else {
24
+ flags[key] = true;
25
+ }
26
+ } else {
27
+ positional.push(a);
28
+ }
29
+ }
30
+ return { positional, flags };
31
+ }
32
+
33
+ export function findRepoRoot(start) {
34
+ let dir = start;
35
+ for (let i = 0; i < 8; i++) {
36
+ if (
37
+ existsSync(path.join(dir, "packages")) &&
38
+ existsSync(path.join(dir, "package.json"))
39
+ ) {
40
+ return dir;
41
+ }
42
+ const parent = path.dirname(dir);
43
+ if (parent === dir) break;
44
+ dir = parent;
45
+ }
46
+ return process.cwd();
47
+ }
48
+
49
+ const SLICE_ROOTS = [
50
+ ["frontend", "slices"],
51
+ ["template-base", "frontend", "slices"],
52
+ ];
53
+
54
+ function resolveContractPath(repoRoot, slug) {
55
+ for (const segs of SLICE_ROOTS) {
56
+ const p = path.join(repoRoot, ...segs, slug, "slice.contract.ts");
57
+ if (existsSync(p)) return p;
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function resolveContractRelPath(repoRoot, slug) {
63
+ for (const segs of SLICE_ROOTS) {
64
+ const rel = [...segs, slug, "slice.contract.ts"].join("/");
65
+ if (existsSync(path.join(repoRoot, rel))) return rel;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ export function loadCurrentContract(repoRoot, slug) {
71
+ const p = resolveContractPath(repoRoot, slug);
72
+ if (!p) return null;
73
+ return evalContract(repoRoot, p, null);
74
+ }
75
+
76
+ /**
77
+ * Resolve the contract at a historic version. Strategy:
78
+ * 1. Try git tags `v<ver>`, `<ver>`, `<slug>-v<ver>` against the contract file.
79
+ * 2. If none have the right version, scan recent commits touching the file
80
+ * and pick the most recent one whose contract.version === fromVersion.
81
+ */
82
+ export function loadHistoricContract(repoRoot, slug, fromVersion) {
83
+ const rel = resolveContractRelPath(repoRoot, slug);
84
+ if (!rel) return null;
85
+
86
+ const candidates = [`v${fromVersion}`, fromVersion, `${slug}-v${fromVersion}`];
87
+ for (const ref of candidates) {
88
+ const text = gitShowFile(repoRoot, ref, rel);
89
+ if (text) {
90
+ const c = evalContract(repoRoot, null, text);
91
+ if (c && c.version === fromVersion) return c;
92
+ }
93
+ }
94
+
95
+ // Fallback: scan commit history for the file.
96
+ const log = spawnSync("git", ["log", "--format=%H", "--", rel], {
97
+ cwd: repoRoot,
98
+ encoding: "utf8",
99
+ });
100
+ if (log.status === 0 && log.stdout) {
101
+ const hashes = log.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
102
+ for (const sha of hashes) {
103
+ const text = gitShowFile(repoRoot, sha, rel);
104
+ if (!text) continue;
105
+ const c = evalContract(repoRoot, null, text);
106
+ if (c && c.version === fromVersion) return c;
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+
112
+ function gitShowFile(repoRoot, ref, relPath) {
113
+ const res = spawnSync("git", ["show", `${ref}:${relPath}`], {
114
+ cwd: repoRoot,
115
+ encoding: "utf8",
116
+ });
117
+ if (res.status === 0 && typeof res.stdout === "string") return res.stdout;
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Eval a contract file by spawning `npx tsx` and reading the JSON output.
123
+ * Pass `filePath` (absolute) for the on-disk version OR `inlineText` for a
124
+ * historic version pulled via git show.
125
+ */
126
+ function evalContract(repoRoot, filePath, inlineText) {
127
+ if (filePath) {
128
+ const rel = "./" + path.relative(repoRoot, filePath);
129
+ const code = [
130
+ `import(${JSON.stringify(rel)})`,
131
+ ` .then(m => { const c = m.contract || (m.default && m.default.contract); if (!c) { process.exit(2); } process.stdout.write(JSON.stringify(c)); })`,
132
+ ` .catch((e) => { process.stderr.write(String(e && e.stack || e)); process.exit(3); });`,
133
+ ].join("\n");
134
+ const res = spawnSync("npx", ["--no-install", "tsx", "-e", code], {
135
+ cwd: repoRoot,
136
+ encoding: "utf8",
137
+ });
138
+ if (res.status === 0 && res.stdout) {
139
+ try {
140
+ return JSON.parse(res.stdout);
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ // Inline mode: write to a temp file under the kitab root (so its tsconfig +
149
+ // path aliases resolve) and eval. We use a `.migration-tmp/` folder so the
150
+ // temp file is colocated with packages/ but easy to ignore.
151
+ if (typeof inlineText !== "string") return null;
152
+ const tmpDir = path.join(repoRoot, ".migration-tmp");
153
+ mkdirSync(tmpDir, { recursive: true });
154
+ const tmpFile = path.join(
155
+ tmpDir,
156
+ `contract-${Date.now()}-${Math.random().toString(36).slice(2)}.ts`,
157
+ );
158
+ try {
159
+ // Rewrite the import path back to the absolute kitab `contract` module so
160
+ // `../../../packages/cli/lib/contract` (from a historic file pulled via
161
+ // git show) keeps resolving after we move it to a different directory.
162
+ const adjusted = adjustContractImport(inlineText, repoRoot, tmpDir);
163
+ writeFileSync(tmpFile, adjusted);
164
+ return evalContract(repoRoot, tmpFile, null);
165
+ } finally {
166
+ try {
167
+ if (existsSync(tmpFile)) unlinkSync(tmpFile);
168
+ } catch {
169
+ // ignore
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Replace any `from "..contract"` import in the contract body with an
176
+ * absolute path so the temp-file copy still resolves the same module.
177
+ */
178
+ function adjustContractImport(text, repoRoot, tmpDir) {
179
+ const contractMod = path.join(repoRoot, "packages", "cli", "lib", "contract");
180
+ const relFromTmp = path
181
+ .relative(tmpDir, contractMod)
182
+ .split(path.sep)
183
+ .join("/");
184
+ // Match imports ending in /contract or /contract.ts (with or without .ts).
185
+ return text.replace(
186
+ /from\s+["'][^"']*\/contract(?:\.ts)?["']/g,
187
+ `from "${relFromTmp}"`,
188
+ );
189
+ }
@@ -0,0 +1,75 @@
1
+ // migrate-print.mjs — ASCII rendering of a MigrationPlan.
2
+ //
3
+ // Extracted from migrate.mjs.
4
+
5
+ import kleur from "kleur";
6
+
7
+ export function printPlan(plan) {
8
+ const head = plan.summary.totalSteps === 0
9
+ ? kleur.green("✓ no migration needed")
10
+ : kleur.bold(`→ ${plan.slug}: v${plan.fromVersion} → v${plan.toVersion}`);
11
+ process.stdout.write(`\n${head}\n\n`);
12
+
13
+ if (plan.summary.totalSteps === 0) {
14
+ process.stdout.write(kleur.dim(" (contracts are equivalent — nothing to migrate)\n\n"));
15
+ return;
16
+ }
17
+
18
+ // Header row.
19
+ const cols = ["id", "kind", "risk", "rev", "description"];
20
+ const widths = [28, 26, 6, 4, 50];
21
+ const sep =
22
+ "+" + widths.map((w) => "-".repeat(w + 2)).join("+") + "+\n";
23
+ const row = (vals) =>
24
+ "|" +
25
+ vals
26
+ .map((v, i) => " " + String(v).padEnd(widths[i]).slice(0, widths[i]) + " ")
27
+ .join("|") +
28
+ "|\n";
29
+
30
+ process.stdout.write(sep);
31
+ process.stdout.write(row(cols));
32
+ process.stdout.write(sep);
33
+ for (const step of plan.steps) {
34
+ process.stdout.write(
35
+ row([
36
+ step.id,
37
+ step.kind,
38
+ step.risk,
39
+ step.reversible ? "yes" : "NO",
40
+ step.description.replace(/\s+/g, " ").slice(0, widths[4]),
41
+ ]),
42
+ );
43
+ }
44
+ process.stdout.write(sep);
45
+
46
+ // Per-step detail.
47
+ for (const step of plan.steps) {
48
+ process.stdout.write(`\n${kleur.bold(step.id)} ${kleur.dim("(" + step.kind + ")")}\n`);
49
+ process.stdout.write(` ${step.description}\n`);
50
+ if (step.artifacts.note) {
51
+ process.stdout.write(` ${kleur.dim("note:")} ${step.artifacts.note}\n`);
52
+ }
53
+ if (step.artifacts.envExample) {
54
+ process.stdout.write(` ${kleur.dim(".env.example:")} ${step.artifacts.envExample}\n`);
55
+ }
56
+ }
57
+
58
+ process.stdout.write(
59
+ `\n${kleur.bold("summary")}: ${plan.summary.totalSteps} step(s) — ` +
60
+ `${kleur.red(plan.summary.highRisk + " high-risk")}, ` +
61
+ `${kleur.yellow(plan.summary.irreversible + " irreversible")}\n`,
62
+ );
63
+
64
+ if (plan.warnings.length > 0) {
65
+ process.stdout.write(`\n${kleur.bold("warnings:")}\n`);
66
+ for (const w of plan.warnings) {
67
+ process.stdout.write(` ${kleur.yellow("⚠")} ${w}\n`);
68
+ }
69
+ }
70
+ process.stdout.write(
71
+ `\n${kleur.dim(
72
+ "pass --json for machine-readable output, --write-files to materialize convex/migrations/.",
73
+ )}\n\n`,
74
+ );
75
+ }