infernoflow 0.37.1 → 0.37.3

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,77 +1,11 @@
1
- /**
2
- * infernoflow share
3
- *
4
- * Generate a public read-only snapshot of your capability contract — no cloud
5
- * account needed. Creates a self-contained HTML file you can host anywhere,
6
- * or uploads to a paste service and prints a short link.
7
- *
8
- * Usage:
9
- * infernoflow share Generate share.html locally
10
- * infernoflow share --open Open in browser immediately
11
- * infernoflow share --upload Upload to dpaste.com and print URL
12
- * infernoflow share --copy Copy HTML to clipboard
13
- * infernoflow share --json Machine-readable: { ok, file, url }
14
- * infernoflow share --out <path> Custom output path
15
- */
16
-
17
- import * as fs from "node:fs";
18
- import * as path from "node:path";
19
- import * as https from "node:https";
20
- import { execSync } from "node:child_process";
21
- import { done, warn, info, bold, cyan, gray, green } from "../ui/output.mjs";
22
-
23
- // ── Helpers ───────────────────────────────────────────────────────────────────
24
-
25
- function readContract(infernoDir) {
26
- for (const f of ["contract.json", "capabilities.json"]) {
27
- const p = path.join(infernoDir, f);
28
- if (fs.existsSync(p)) {
29
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
30
- }
31
- }
32
- return null;
33
- }
34
-
35
- function readChangelog(cwd) {
36
- const p = path.join(cwd, "CHANGELOG.md");
37
- if (!fs.existsSync(p)) return null;
38
- try {
39
- const lines = fs.readFileSync(p, "utf8").split("\n");
40
- const start = lines.findIndex(l => l.startsWith("## "));
41
- if (start === -1) return null;
42
- const end = lines.findIndex((l, i) => i > start && l.startsWith("## "));
43
- return lines.slice(start, end === -1 ? start + 30 : end).join("\n");
44
- } catch { return null; }
45
- }
46
-
47
- // ── HTML generator ────────────────────────────────────────────────────────────
48
-
49
- function buildHtml(contract, changelog, generatedAt) {
50
- const caps = contract.capabilities || [];
51
- const policyId = contract.policyId || "project";
52
- const version = contract.policyVersion || "?";
53
-
54
- const capRows = caps.map(c => {
55
- const id = typeof c === "string" ? c : c.id;
56
- const title = typeof c === "string" ? c : (c.title || c.id);
57
- const cov = typeof c === "object" ? c.covered : undefined;
58
- const badge = cov === true ? `<span class="cov yes">✔</span>`
59
- : cov === false ? `<span class="cov no">✗</span>`
60
- : `<span class="cov uk">·</span>`;
61
- const desc = typeof c === "object" && c.description ? `<div class="cap-desc">${c.description}</div>` : "";
62
- return `<div class="cap-row">${badge}<div class="cap-body"><div class="cap-id">${id}</div><div class="cap-title">${title !== id ? title : ""}</div>${desc}</div></div>`;
63
- }).join("");
64
-
65
- const changelogHtml = changelog
66
- ? `<section><h2>Latest changes</h2><pre class="changelog">${changelog.replace(/</g, "&lt;")}</pre></section>`
67
- : "";
68
-
69
- return `<!doctype html>
1
+ import*as g from"node:fs";import*as b from"node:path";import*as j from"node:https";import{execSync as x}from"node:child_process";import{done as z,warn as v,info as $,bold as C,cyan as w,gray as k}from"../ui/output.mjs";function U(r){for(const t of["contract.json","capabilities.json"]){const o=b.join(r,t);if(g.existsSync(o))try{return JSON.parse(g.readFileSync(o,"utf8"))}catch{}}return null}function L(r){const t=b.join(r,"CHANGELOG.md");if(!g.existsSync(t))return null;try{const o=g.readFileSync(t,"utf8").split(`
2
+ `),n=o.findIndex(s=>s.startsWith("## "));if(n===-1)return null;const a=o.findIndex((s,c)=>c>n&&s.startsWith("## "));return o.slice(n,a===-1?n+30:a).join(`
3
+ `)}catch{return null}}function N(r,t,o){const n=r.capabilities||[],a=r.policyId||"project",s=r.policyVersion||"?",c=n.map(e=>{const p=typeof e=="string"?e:e.id,h=typeof e=="string"?e:e.title||e.id,u=typeof e=="object"?e.covered:void 0,d=u===!0?'<span class="cov yes">\u2714</span>':u===!1?'<span class="cov no">\u2717</span>':'<span class="cov uk">\xB7</span>',f=typeof e=="object"&&e.description?`<div class="cap-desc">${e.description}</div>`:"";return`<div class="cap-row">${d}<div class="cap-body"><div class="cap-id">${p}</div><div class="cap-title">${h!==p?h:""}</div>${f}</div></div>`}).join(""),l=t?`<section><h2>Latest changes</h2><pre class="changelog">${t.replace(/</g,"&lt;")}</pre></section>`:"";return`<!doctype html>
70
4
  <html lang="en">
71
5
  <head>
72
6
  <meta charset="UTF-8">
73
7
  <meta name="viewport" content="width=device-width,initial-scale=1">
74
- <title>${policyId} v${version} infernoflow snapshot</title>
8
+ <title>${a} v${s} \u2014 infernoflow snapshot</title>
75
9
  <style>
76
10
  *{box-sizing:border-box;margin:0;padding:0}
77
11
  body{background:#0f0f1a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;line-height:1.5;padding:0 0 3rem}
@@ -99,138 +33,21 @@ function buildHtml(contract, changelog, generatedAt) {
99
33
  </head>
100
34
  <body>
101
35
  <header>
102
- <h1>🔥 ${policyId}</h1>
103
- <div class="meta">v${version} · ${caps.length} capabilities · snapshot ${generatedAt}</div>
36
+ <h1>\u{1F525} ${a}</h1>
37
+ <div class="meta">v${s} \xB7 ${n.length} capabilities \xB7 snapshot ${o}</div>
104
38
  </header>
105
39
  <main>
106
40
  <div class="stats">
107
- <div class="stat"><div class="n">${caps.length}</div><div class="l">capabilities</div></div>
108
- <div class="stat"><div class="n">${caps.filter(c => typeof c === "object" && c.covered).length || ""}</div><div class="l">covered</div></div>
109
- <div class="stat"><div class="n">v${version}</div><div class="l">version</div></div>
41
+ <div class="stat"><div class="n">${n.length}</div><div class="l">capabilities</div></div>
42
+ <div class="stat"><div class="n">${n.filter(e=>typeof e=="object"&&e.covered).length||"\u2014"}</div><div class="l">covered</div></div>
43
+ <div class="stat"><div class="n">v${s}</div><div class="l">version</div></div>
110
44
  </div>
111
45
  <section>
112
46
  <h2>Capabilities</h2>
113
- <div>${capRows || '<p style="color:#475569">No capabilities yet.</p>'}</div>
47
+ <div>${c||'<p style="color:#475569">No capabilities yet.</p>'}</div>
114
48
  </section>
115
- ${changelogHtml}
49
+ ${l}
116
50
  </main>
117
- <footer>Generated by <a href="https://github.com/ronmiz/infernoflow">infernoflow</a> on ${generatedAt}</footer>
51
+ <footer>Generated by <a href="https://github.com/ronmiz/infernoflow">infernoflow</a> on ${o}</footer>
118
52
  </body>
119
- </html>`;
120
- }
121
-
122
- // ── Upload to dpaste ──────────────────────────────────────────────────────────
123
-
124
- function uploadToDpaste(content) {
125
- return new Promise((resolve, reject) => {
126
- const body = `content=${encodeURIComponent(content)}&syntax=html&expiry_days=365`;
127
- const req = https.request({
128
- hostname: "dpaste.com",
129
- path: "/api/v2/",
130
- method: "POST",
131
- headers: {
132
- "Content-Type": "application/x-www-form-urlencoded",
133
- "Content-Length": Buffer.byteLength(body),
134
- "User-Agent": "infernoflow-cli",
135
- },
136
- }, (res) => {
137
- let data = "";
138
- res.on("data", d => (data += d));
139
- res.on("end", () => {
140
- const url = data.trim();
141
- if (url.startsWith("http")) resolve(url + ".html");
142
- else reject(new Error("Unexpected response: " + data));
143
- });
144
- });
145
- req.on("error", reject);
146
- req.write(body);
147
- req.end();
148
- });
149
- }
150
-
151
- // ── Main ──────────────────────────────────────────────────────────────────────
152
-
153
- export async function shareCommand(rawArgs) {
154
- const args = rawArgs.slice(1);
155
- const jsonMode = args.includes("--json");
156
- const openBrowser = args.includes("--open");
157
- const upload = args.includes("--upload");
158
- const copyToClip = args.includes("--copy");
159
- const outIdx = args.indexOf("--out");
160
- const cwd = process.cwd();
161
- const infernoDir = path.join(cwd, "inferno");
162
-
163
- if (!fs.existsSync(infernoDir)) {
164
- const msg = "inferno/ not found. Run: infernoflow init";
165
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
166
- process.exit(1);
167
- }
168
-
169
- const contract = readContract(infernoDir);
170
- if (!contract) {
171
- const msg = "No contract.json found. Run: infernoflow init";
172
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
173
- process.exit(1);
174
- }
175
-
176
- const changelog = readChangelog(cwd);
177
- const generatedAt = new Date().toLocaleString();
178
- const htmlContent = buildHtml(contract, changelog, generatedAt);
179
-
180
- const outPath = outIdx !== -1 ? args[outIdx + 1] : path.join(cwd, "inferno", "share.html");
181
- fs.writeFileSync(outPath, htmlContent, "utf8");
182
-
183
- let url = null;
184
-
185
- if (upload) {
186
- if (!jsonMode) info("Uploading to dpaste.com…");
187
- try {
188
- url = await uploadToDpaste(htmlContent);
189
- } catch (err) {
190
- if (!jsonMode) warn(`Upload failed: ${err.message}`);
191
- }
192
- }
193
-
194
- if (copyToClip) {
195
- try {
196
- const cmd = process.platform === "win32" ? `echo ${JSON.stringify(htmlContent)} | clip`
197
- : process.platform === "darwin" ? `pbcopy`
198
- : `xclip -selection clipboard`;
199
- if (process.platform === "darwin") {
200
- const { execSync: ex } = await import("node:child_process");
201
- const proc = ex;
202
- require("child_process").execSync("pbcopy", { input: htmlContent });
203
- } else {
204
- execSync(cmd, { input: htmlContent });
205
- }
206
- if (!jsonMode) info("HTML copied to clipboard");
207
- } catch {}
208
- }
209
-
210
- if (openBrowser) {
211
- try {
212
- const target = url || outPath;
213
- const cmd = process.platform === "win32" ? `start "" "${target}"`
214
- : process.platform === "darwin" ? `open "${target}"`
215
- : `xdg-open "${target}"`;
216
- execSync(cmd, { stdio: "ignore" });
217
- } catch {}
218
- }
219
-
220
- if (jsonMode) {
221
- console.log(JSON.stringify({ ok: true, file: outPath, url }));
222
- return;
223
- }
224
-
225
- const caps = (contract.capabilities || []).length;
226
- done(`Snapshot created — ${bold(String(caps))} capabilities`);
227
- console.log();
228
- console.log(` File: ${cyan(outPath)}`);
229
- if (url) console.log(` URL: ${cyan(url)}`);
230
- console.log();
231
- if (!url) {
232
- console.log(` ${gray("Share the file or run with --upload to get a public URL:")}`);
233
- console.log(` ${cyan("infernoflow share --upload --open")}`);
234
- }
235
- console.log();
236
- }
53
+ </html>`}function O(r){return new Promise((t,o)=>{const n=`content=${encodeURIComponent(r)}&syntax=html&expiry_days=365`,a=j.request({hostname:"dpaste.com",path:"/api/v2/",method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","Content-Length":Buffer.byteLength(n),"User-Agent":"infernoflow-cli"}},s=>{let c="";s.on("data",l=>c+=l),s.on("end",()=>{const l=c.trim();l.startsWith("http")?t(l+".html"):o(new Error("Unexpected response: "+c))})});a.on("error",o),a.write(n),a.end()})}async function J(r){const t=r.slice(1),o=t.includes("--json"),n=t.includes("--open"),a=t.includes("--upload"),s=t.includes("--copy"),c=t.indexOf("--out"),l=process.cwd(),e=b.join(l,"inferno");if(!g.existsSync(e)){const i="inferno/ not found. Run: infernoflow init";o?console.log(JSON.stringify({ok:!1,error:i})):v(i),process.exit(1)}const p=U(e);if(!p){const i="No contract.json found. Run: infernoflow init";o?console.log(JSON.stringify({ok:!1,error:i})):v(i),process.exit(1)}const h=L(l),u=new Date().toLocaleString(),d=N(p,h,u),f=c!==-1?t[c+1]:b.join(l,"inferno","share.html");g.writeFileSync(f,d,"utf8");let m=null;if(a){o||$("Uploading to dpaste.com\u2026");try{m=await O(d)}catch(i){o||v(`Upload failed: ${i.message}`)}}if(s)try{const i=process.platform==="win32"?`echo ${JSON.stringify(d)} | clip`:process.platform==="darwin"?"pbcopy":"xclip -selection clipboard";if(process.platform==="darwin"){const{execSync:y}=await import("node:child_process"),I=y;require("child_process").execSync("pbcopy",{input:d})}else x(i,{input:d});o||$("HTML copied to clipboard")}catch{}if(n)try{const i=m||f,y=process.platform==="win32"?`start "" "${i}"`:process.platform==="darwin"?`open "${i}"`:`xdg-open "${i}"`;x(y,{stdio:"ignore"})}catch{}if(o){console.log(JSON.stringify({ok:!0,file:f,url:m}));return}const S=(p.capabilities||[]).length;z(`Snapshot created \u2014 ${C(String(S))} capabilities`),console.log(),console.log(` File: ${w(f)}`),m&&console.log(` URL: ${w(m)}`),console.log(),m||(console.log(` ${k("Share the file or run with --upload to get a public URL:")}`),console.log(` ${w("infernoflow share --upload --open")}`)),console.log()}export{J as shareCommand};
@@ -1,383 +1,3 @@
1
- /**
2
- * infernoflow snapshot
3
- *
4
- * Named, timestamped snapshots of the full capability contract.
5
- * Stored in inferno/snapshots/ — travel with the repo.
6
- *
7
- * Like git tags, but for the capability contract specifically.
8
- *
9
- * Usage:
10
- * infernoflow snapshot save <name> Save current contract as a named snapshot
11
- * infernoflow snapshot list List all snapshots
12
- * infernoflow snapshot show <name> Print a snapshot's capabilities
13
- * infernoflow snapshot diff <name1> <name2> Diff two snapshots (or name vs current)
14
- * infernoflow snapshot restore <name> Overwrite contract with a snapshot
15
- * infernoflow snapshot delete <name> Delete a snapshot
16
- * infernoflow snapshot --json Machine-readable output on any subcommand
17
- *
18
- * Snapshot file format (inferno/snapshots/<name>.json):
19
- * {
20
- * "name": "v1.2-release",
21
- * "savedAt": "2025-06-01T12:00:00Z",
22
- * "capabilities": [...],
23
- * "meta": { "version": "1.2.0", "capabilityCount": 12 }
24
- * }
25
- */
26
-
27
- import * as fs from "node:fs";
28
- import * as path from "node:path";
29
- import { done, warn, info, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
30
-
31
- const SNAPSHOTS_DIR = "snapshots";
32
-
33
- // ── Storage ───────────────────────────────────────────────────────────────────
34
-
35
- function snapshotsDir(infernoDir) {
36
- return path.join(infernoDir, SNAPSHOTS_DIR);
37
- }
38
-
39
- function snapshotPath(infernoDir, name) {
40
- return path.join(infernoDir, SNAPSHOTS_DIR, `${name}.json`);
41
- }
42
-
43
- function ensureSnapshotsDir(infernoDir) {
44
- const d = snapshotsDir(infernoDir);
45
- if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
46
- return d;
47
- }
48
-
49
- function listSnapshots(infernoDir) {
50
- const d = snapshotsDir(infernoDir);
51
- if (!fs.existsSync(d)) return [];
52
- return fs.readdirSync(d)
53
- .filter(f => f.endsWith(".json"))
54
- .map(f => {
55
- try { return JSON.parse(fs.readFileSync(path.join(d, f), "utf8")); } catch { return null; }
56
- })
57
- .filter(Boolean)
58
- .sort((a, b) => new Date(b.savedAt) - new Date(a.savedAt));
59
- }
60
-
61
- function readSnapshot(infernoDir, name) {
62
- const p = snapshotPath(infernoDir, name);
63
- if (!fs.existsSync(p)) return null;
64
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
65
- }
66
-
67
- function readContract(infernoDir) {
68
- for (const f of ["contract.json", "capabilities.json"]) {
69
- const p = path.join(infernoDir, f);
70
- if (!fs.existsSync(p)) continue;
71
- try { return { file: f, data: JSON.parse(fs.readFileSync(p, "utf8")) }; } catch {}
72
- }
73
- return null;
74
- }
75
-
76
- function normaliseCaps(contract) {
77
- const raw = contract?.capabilities || contract?.data?.capabilities || contract || [];
78
- return raw.map(c => (typeof c === "string" ? { id: c } : c));
79
- }
80
-
81
- function readPackageVersion(cwd) {
82
- try { return JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")).version || ""; } catch { return ""; }
83
- }
84
-
85
- // ── Diff engine ───────────────────────────────────────────────────────────────
86
-
87
- function diffCaps(capsBefore, capsAfter) {
88
- const beforeIds = new Map(capsBefore.map(c => [c.id, c]));
89
- const afterIds = new Map(capsAfter.map(c => [c.id, c]));
90
-
91
- const added = capsAfter.filter(c => !beforeIds.has(c.id));
92
- const removed = capsBefore.filter(c => !afterIds.has(c.id));
93
- const changed = capsAfter.filter(c => {
94
- const prev = beforeIds.get(c.id);
95
- if (!prev) return false;
96
- return JSON.stringify(c) !== JSON.stringify(prev);
97
- });
98
-
99
- return { added, removed, changed };
100
- }
101
-
102
- // ── Sub-commands ──────────────────────────────────────────────────────────────
103
-
104
- function subcmdSave(name, infernoDir, cwd, jsonMode) {
105
- if (!name || name.startsWith("--")) {
106
- const msg = "Usage: infernoflow snapshot save <name>";
107
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
108
- else warn(msg);
109
- return;
110
- }
111
-
112
- // Validate name (no spaces, no slashes)
113
- if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
114
- const msg = `Invalid snapshot name "${name}" — use letters, digits, dots, dashes, underscores only`;
115
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
116
- else warn(msg);
117
- return;
118
- }
119
-
120
- const contract = readContract(infernoDir);
121
- if (!contract) {
122
- const msg = "No contract found. Run: infernoflow init";
123
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
124
- else warn(msg);
125
- return;
126
- }
127
-
128
- ensureSnapshotsDir(infernoDir);
129
-
130
- const caps = normaliseCaps(contract.data);
131
- const snapshot = {
132
- name,
133
- savedAt: new Date().toISOString(),
134
- capabilities: caps,
135
- meta: {
136
- version: readPackageVersion(cwd),
137
- capabilityCount: caps.length,
138
- contractFile: contract.file,
139
- },
140
- };
141
-
142
- const p = snapshotPath(infernoDir, name);
143
- const overwriting = fs.existsSync(p);
144
- fs.writeFileSync(p, JSON.stringify(snapshot, null, 2) + "\n");
145
-
146
- if (jsonMode) {
147
- console.log(JSON.stringify({ ok: true, action: "saved", name, capabilities: caps.length }));
148
- } else {
149
- done(`${overwriting ? "Updated" : "Saved"} snapshot ${bold(cyan(name))} — ${bold(String(caps.length))} capabilities`);
150
- console.log();
151
- }
152
- }
153
-
154
- function subcmdList(infernoDir, jsonMode) {
155
- const snapshots = listSnapshots(infernoDir);
156
-
157
- if (jsonMode) {
158
- console.log(JSON.stringify({ ok: true, snapshots: snapshots.map(s => ({ name: s.name, savedAt: s.savedAt, capabilities: s.meta?.capabilityCount ?? s.capabilities?.length ?? 0 })) }));
159
- return;
160
- }
161
-
162
- if (!snapshots.length) {
163
- info("No snapshots yet. Use: infernoflow snapshot save <name>");
164
- return;
165
- }
166
-
167
- console.log();
168
- console.log(` ${bold(`${snapshots.length} snapshot${snapshots.length !== 1 ? "s" : ""}`)}`);
169
- console.log();
170
-
171
- const w = Math.max(...snapshots.map(s => s.name.length), 8) + 2;
172
- for (const s of snapshots) {
173
- const date = new Date(s.savedAt).toLocaleString();
174
- const caps = s.meta?.capabilityCount ?? s.capabilities?.length ?? "?";
175
- console.log(` ${bold(s.name.padEnd(w))} ${gray(date)} ${cyan(String(caps))} caps`);
176
- }
177
- console.log();
178
- }
179
-
180
- function subcmdShow(name, infernoDir, jsonMode) {
181
- if (!name || name.startsWith("--")) {
182
- const msg = "Usage: infernoflow snapshot show <name>";
183
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
184
- else warn(msg);
185
- return;
186
- }
187
-
188
- const snap = readSnapshot(infernoDir, name);
189
- if (!snap) {
190
- const msg = `Snapshot not found: ${name}`;
191
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
192
- else warn(msg);
193
- return;
194
- }
195
-
196
- if (jsonMode) {
197
- console.log(JSON.stringify({ ok: true, snapshot: snap }));
198
- return;
199
- }
200
-
201
- console.log();
202
- console.log(` ${bold("Snapshot:")} ${cyan(snap.name)}`);
203
- console.log(` ${bold("Saved:")} ${gray(new Date(snap.savedAt).toLocaleString())}`);
204
- console.log(` ${bold("Version:")} ${gray(snap.meta?.version || "—")}`);
205
- console.log(` ${bold("Caps:")} ${snap.capabilities.length}`);
206
- console.log();
207
-
208
- const caps = normaliseCaps(snap.capabilities);
209
- for (const c of caps) {
210
- console.log(` ${cyan("·")} ${bold(c.id)}${c.description ? gray(" " + c.description) : ""}`);
211
- }
212
- console.log();
213
- }
214
-
215
- function subcmdDiff(nameA, nameB, infernoDir, jsonMode) {
216
- if (!nameA) {
217
- const msg = "Usage: infernoflow snapshot diff <name1> [<name2>] (omit name2 to diff against current)";
218
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
219
- else warn(msg);
220
- return;
221
- }
222
-
223
- const snapA = readSnapshot(infernoDir, nameA);
224
- if (!snapA) {
225
- const msg = `Snapshot not found: ${nameA}`;
226
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
227
- else warn(msg);
228
- return;
229
- }
230
-
231
- let capsBefore = normaliseCaps(snapA.capabilities);
232
- let capsAfter;
233
- let labelAfter;
234
-
235
- if (nameB) {
236
- const snapB = readSnapshot(infernoDir, nameB);
237
- if (!snapB) {
238
- const msg = `Snapshot not found: ${nameB}`;
239
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
240
- else warn(msg);
241
- return;
242
- }
243
- capsAfter = normaliseCaps(snapB.capabilities);
244
- labelAfter = nameB;
245
- } else {
246
- const contract = readContract(infernoDir);
247
- if (!contract) {
248
- const msg = "No current contract found.";
249
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
250
- else warn(msg);
251
- return;
252
- }
253
- capsAfter = normaliseCaps(contract.data);
254
- labelAfter = "current";
255
- }
256
-
257
- const diff = diffCaps(capsBefore, capsAfter);
258
-
259
- if (jsonMode) {
260
- console.log(JSON.stringify({ ok: true, from: nameA, to: labelAfter, ...diff }));
261
- return;
262
- }
263
-
264
- console.log();
265
- console.log(` ${bold("Diff:")} ${cyan(nameA)} → ${cyan(labelAfter)}`);
266
- console.log();
267
-
268
- if (!diff.added.length && !diff.removed.length && !diff.changed.length) {
269
- console.log(` ${gray("No differences — snapshots are identical")}`);
270
- console.log();
271
- return;
272
- }
273
-
274
- if (diff.added.length) {
275
- console.log(` ${green("+")} ${bold(`${diff.added.length} added`)}`);
276
- diff.added.forEach(c => console.log(` ${green("+")} ${c.id}`));
277
- console.log();
278
- }
279
- if (diff.removed.length) {
280
- console.log(` ${red("-")} ${bold(`${diff.removed.length} removed`)}`);
281
- diff.removed.forEach(c => console.log(` ${red("-")} ${c.id}`));
282
- console.log();
283
- }
284
- if (diff.changed.length) {
285
- console.log(` ${yellow("~")} ${bold(`${diff.changed.length} changed`)}`);
286
- diff.changed.forEach(c => console.log(` ${yellow("~")} ${c.id}`));
287
- console.log();
288
- }
289
- }
290
-
291
- function subcmdRestore(name, infernoDir, jsonMode) {
292
- if (!name || name.startsWith("--")) {
293
- const msg = "Usage: infernoflow snapshot restore <name>";
294
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
295
- else warn(msg);
296
- return;
297
- }
298
-
299
- const snap = readSnapshot(infernoDir, name);
300
- if (!snap) {
301
- const msg = `Snapshot not found: ${name}`;
302
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
303
- else warn(msg);
304
- return;
305
- }
306
-
307
- const contract = readContract(infernoDir);
308
- const contractFile = contract?.file || "capabilities.json";
309
- const contractPath = path.join(infernoDir, contractFile);
310
-
311
- const data = contract?.data || {};
312
- data.capabilities = snap.capabilities;
313
- fs.writeFileSync(contractPath, JSON.stringify(data, null, 2) + "\n");
314
-
315
- if (jsonMode) {
316
- console.log(JSON.stringify({ ok: true, action: "restored", name, capabilities: snap.capabilities.length }));
317
- } else {
318
- done(`Restored ${bold(cyan(name))} → ${bold(contractFile)} (${snap.capabilities.length} capabilities)`);
319
- console.log();
320
- }
321
- }
322
-
323
- function subcmdDelete(name, infernoDir, jsonMode) {
324
- if (!name || name.startsWith("--")) {
325
- const msg = "Usage: infernoflow snapshot delete <name>";
326
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
327
- else warn(msg);
328
- return;
329
- }
330
-
331
- const p = snapshotPath(infernoDir, name);
332
- if (!fs.existsSync(p)) {
333
- const msg = `Snapshot not found: ${name}`;
334
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
335
- else warn(msg);
336
- return;
337
- }
338
-
339
- fs.unlinkSync(p);
340
-
341
- if (jsonMode) {
342
- console.log(JSON.stringify({ ok: true, action: "deleted", name }));
343
- } else {
344
- done(`Deleted snapshot ${bold(name)}`);
345
- console.log();
346
- }
347
- }
348
-
349
- // ── Entry ─────────────────────────────────────────────────────────────────────
350
-
351
- export async function snapshotCommand(rawArgs) {
352
- const args = rawArgs.slice(1);
353
- const jsonMode = args.includes("--json");
354
- const cwd = process.cwd();
355
- const infernoDir = path.join(cwd, "inferno");
356
-
357
- if (!fs.existsSync(infernoDir)) {
358
- const msg = "inferno/ not found. Run: infernoflow init";
359
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
360
- else warn(msg);
361
- process.exit(1);
362
- }
363
-
364
- const subcmd = args.find(a => !a.startsWith("--"));
365
-
366
- if (!subcmd || subcmd === "list") {
367
- return subcmdList(infernoDir, jsonMode);
368
- }
369
-
370
- const positional = args.filter(a => !a.startsWith("--"));
371
-
372
- if (subcmd === "save") return subcmdSave(positional[1], infernoDir, cwd, jsonMode);
373
- if (subcmd === "show") return subcmdShow(positional[1], infernoDir, jsonMode);
374
- if (subcmd === "restore") return subcmdRestore(positional[1], infernoDir, jsonMode);
375
- if (subcmd === "delete") return subcmdDelete(positional[1], infernoDir, jsonMode);
376
-
377
- if (subcmd === "diff") {
378
- return subcmdDiff(positional[1], positional[2], infernoDir, jsonMode);
379
- }
380
-
381
- warn(`Unknown snapshot sub-command: ${subcmd}`);
382
- info("Available: save | list | show | diff | restore | delete");
383
- }
1
+ import*as f from"node:fs";import*as u from"node:path";import{done as m,warn as d,info as w,bold as g,cyan as p,gray as S,green as O,yellow as v,red as J}from"../ui/output.mjs";const k="snapshots";function x(o){return u.join(o,k)}function b(o,e){return u.join(o,k,`${e}.json`)}function C(o){const e=x(o);return f.existsSync(e)||f.mkdirSync(e,{recursive:!0}),e}function F(o){const e=x(o);return f.existsSync(e)?f.readdirSync(e).filter(t=>t.endsWith(".json")).map(t=>{try{return JSON.parse(f.readFileSync(u.join(e,t),"utf8"))}catch{return null}}).filter(Boolean).sort((t,s)=>new Date(s.savedAt)-new Date(t.savedAt)):[]}function $(o,e){const t=b(o,e);if(!f.existsSync(t))return null;try{return JSON.parse(f.readFileSync(t,"utf8"))}catch{return null}}function N(o){for(const e of["contract.json","capabilities.json"]){const t=u.join(o,e);if(f.existsSync(t))try{return{file:e,data:JSON.parse(f.readFileSync(t,"utf8"))}}catch{}}return null}function y(o){return(o?.capabilities||o?.data?.capabilities||o||[]).map(t=>typeof t=="string"?{id:t}:t)}function U(o){try{return JSON.parse(f.readFileSync(u.join(o,"package.json"),"utf8")).version||""}catch{return""}}function A(o,e){const t=new Map(o.map(r=>[r.id,r])),s=new Map(e.map(r=>[r.id,r])),n=e.filter(r=>!t.has(r.id)),i=o.filter(r=>!s.has(r.id)),c=e.filter(r=>{const a=t.get(r.id);return a?JSON.stringify(r)!==JSON.stringify(a):!1});return{added:n,removed:i,changed:c}}function D(o,e,t,s){if(!o||o.startsWith("--")){const l="Usage: infernoflow snapshot save <name>";s?console.log(JSON.stringify({ok:!1,error:l})):d(l);return}if(!/^[a-zA-Z0-9._-]+$/.test(o)){const l=`Invalid snapshot name "${o}" \u2014 use letters, digits, dots, dashes, underscores only`;s?console.log(JSON.stringify({ok:!1,error:l})):d(l);return}const n=N(e);if(!n){const l="No contract found. Run: infernoflow init";s?console.log(JSON.stringify({ok:!1,error:l})):d(l);return}C(e);const i=y(n.data),c={name:o,savedAt:new Date().toISOString(),capabilities:i,meta:{version:U(t),capabilityCount:i.length,contractFile:n.file}},r=b(e,o),a=f.existsSync(r);f.writeFileSync(r,JSON.stringify(c,null,2)+`
2
+ `),s?console.log(JSON.stringify({ok:!0,action:"saved",name:o,capabilities:i.length})):(m(`${a?"Updated":"Saved"} snapshot ${g(p(o))} \u2014 ${g(String(i.length))} capabilities`),console.log())}function W(o,e){const t=F(o);if(e){console.log(JSON.stringify({ok:!0,snapshots:t.map(n=>({name:n.name,savedAt:n.savedAt,capabilities:n.meta?.capabilityCount??n.capabilities?.length??0}))}));return}if(!t.length){w("No snapshots yet. Use: infernoflow snapshot save <name>");return}console.log(),console.log(` ${g(`${t.length} snapshot${t.length!==1?"s":""}`)}`),console.log();const s=Math.max(...t.map(n=>n.name.length),8)+2;for(const n of t){const i=new Date(n.savedAt).toLocaleString(),c=n.meta?.capabilityCount??n.capabilities?.length??"?";console.log(` ${g(n.name.padEnd(s))} ${S(i)} ${p(String(c))} caps`)}console.log()}function I(o,e,t){if(!o||o.startsWith("--")){const i="Usage: infernoflow snapshot show <name>";t?console.log(JSON.stringify({ok:!1,error:i})):d(i);return}const s=$(e,o);if(!s){const i=`Snapshot not found: ${o}`;t?console.log(JSON.stringify({ok:!1,error:i})):d(i);return}if(t){console.log(JSON.stringify({ok:!0,snapshot:s}));return}console.log(),console.log(` ${g("Snapshot:")} ${p(s.name)}`),console.log(` ${g("Saved:")} ${S(new Date(s.savedAt).toLocaleString())}`),console.log(` ${g("Version:")} ${S(s.meta?.version||"\u2014")}`),console.log(` ${g("Caps:")} ${s.capabilities.length}`),console.log();const n=y(s.capabilities);for(const i of n)console.log(` ${p("\xB7")} ${g(i.id)}${i.description?S(" "+i.description):""}`);console.log()}function R(o,e,t,s){if(!o){const l="Usage: infernoflow snapshot diff <name1> [<name2>] (omit name2 to diff against current)";s?console.log(JSON.stringify({ok:!1,error:l})):d(l);return}const n=$(t,o);if(!n){const l=`Snapshot not found: ${o}`;s?console.log(JSON.stringify({ok:!1,error:l})):d(l);return}let i=y(n.capabilities),c,r;if(e){const l=$(t,e);if(!l){const h=`Snapshot not found: ${e}`;s?console.log(JSON.stringify({ok:!1,error:h})):d(h);return}c=y(l.capabilities),r=e}else{const l=N(t);if(!l){const h="No current contract found.";s?console.log(JSON.stringify({ok:!1,error:h})):d(h);return}c=y(l.data),r="current"}const a=A(i,c);if(s){console.log(JSON.stringify({ok:!0,from:o,to:r,...a}));return}if(console.log(),console.log(` ${g("Diff:")} ${p(o)} \u2192 ${p(r)}`),console.log(),!a.added.length&&!a.removed.length&&!a.changed.length){console.log(` ${S("No differences \u2014 snapshots are identical")}`),console.log();return}a.added.length&&(console.log(` ${O("+")} ${g(`${a.added.length} added`)}`),a.added.forEach(l=>console.log(` ${O("+")} ${l.id}`)),console.log()),a.removed.length&&(console.log(` ${J("-")} ${g(`${a.removed.length} removed`)}`),a.removed.forEach(l=>console.log(` ${J("-")} ${l.id}`)),console.log()),a.changed.length&&(console.log(` ${v("~")} ${g(`${a.changed.length} changed`)}`),a.changed.forEach(l=>console.log(` ${v("~")} ${l.id}`)),console.log())}function E(o,e,t){if(!o||o.startsWith("--")){const a="Usage: infernoflow snapshot restore <name>";t?console.log(JSON.stringify({ok:!1,error:a})):d(a);return}const s=$(e,o);if(!s){const a=`Snapshot not found: ${o}`;t?console.log(JSON.stringify({ok:!1,error:a})):d(a);return}const n=N(e),i=n?.file||"capabilities.json",c=u.join(e,i),r=n?.data||{};r.capabilities=s.capabilities,f.writeFileSync(c,JSON.stringify(r,null,2)+`
3
+ `),t?console.log(JSON.stringify({ok:!0,action:"restored",name:o,capabilities:s.capabilities.length})):(m(`Restored ${g(p(o))} \u2192 ${g(i)} (${s.capabilities.length} capabilities)`),console.log())}function P(o,e,t){if(!o||o.startsWith("--")){const n="Usage: infernoflow snapshot delete <name>";t?console.log(JSON.stringify({ok:!1,error:n})):d(n);return}const s=b(e,o);if(!f.existsSync(s)){const n=`Snapshot not found: ${o}`;t?console.log(JSON.stringify({ok:!1,error:n})):d(n);return}f.unlinkSync(s),t?console.log(JSON.stringify({ok:!0,action:"deleted",name:o})):(m(`Deleted snapshot ${g(o)}`),console.log())}async function j(o){const e=o.slice(1),t=e.includes("--json"),s=process.cwd(),n=u.join(s,"inferno");if(!f.existsSync(n)){const r="inferno/ not found. Run: infernoflow init";t?console.log(JSON.stringify({ok:!1,error:r})):d(r),process.exit(1)}const i=e.find(r=>!r.startsWith("--"));if(!i||i==="list")return W(n,t);const c=e.filter(r=>!r.startsWith("--"));if(i==="save")return D(c[1],n,s,t);if(i==="show")return I(c[1],n,t);if(i==="restore")return E(c[1],n,t);if(i==="delete")return P(c[1],n,t);if(i==="diff")return R(c[1],c[2],n,t);d(`Unknown snapshot sub-command: ${i}`),w("Available: save | list | show | diff | restore | delete")}export{j as snapshotCommand};