infernoflow 0.23.0 → 0.27.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/dist/bin/infernoflow.mjs +38 -2
- package/dist/lib/commands/claudeMd.mjs +14 -3
- package/dist/lib/commands/dashboard.mjs +312 -3
- package/dist/lib/commands/graph.mjs +337 -0
- package/dist/lib/commands/impact.mjs +325 -0
- package/dist/lib/commands/scan.mjs +45 -26
- package/dist/lib/commands/stability.mjs +293 -0
- package/dist/lib/commands/why.mjs +358 -0
- package/package.json +1 -1
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -54,7 +54,13 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
54
54
|
doctor: "Diagnose your infernoflow setup — checks Node, git, contract, AI providers, MCP, hooks",
|
|
55
55
|
coverage: "Map test files to capabilities — show which caps have test coverage and which don't",
|
|
56
56
|
review: "AI-powered capability impact review for staged or recent git changes",
|
|
57
|
-
scan:
|
|
57
|
+
scan: "Deep AST scan — reads actual function bodies, extracts calls, DB ops, external services",
|
|
58
|
+
graph: "Build capability dependency graph — shows which caps call which, detects breaking changes",
|
|
59
|
+
stability: "Show solid/liquid stability level for every capability (frozen/stable/experimental)",
|
|
60
|
+
freeze: "Mark a capability as frozen (solid) — AI will not modify it without explicit instruction",
|
|
61
|
+
thaw: "Reset a capability to experimental (liquid) — free to evolve",
|
|
62
|
+
why: "Given a file or function name — show which capability it serves, scenarios, stability, and git history",
|
|
63
|
+
impact: "Blast radius analysis — see every cap, scenario, and risk level affected before you change anything",
|
|
58
64
|
};
|
|
59
65
|
|
|
60
66
|
const COMMAND_HANDLERS = {
|
|
@@ -102,7 +108,13 @@ const COMMAND_HANDLERS = {
|
|
|
102
108
|
doctor: async (args) => (await import("../lib/commands/doctor.mjs")).doctorCommand(args),
|
|
103
109
|
coverage: async (args) => (await import("../lib/commands/coverage.mjs")).coverageCommand(args),
|
|
104
110
|
review: async (args) => (await import("../lib/commands/review.mjs")).reviewCommand(args),
|
|
105
|
-
scan:
|
|
111
|
+
scan: async (args) => (await import("../lib/commands/scan.mjs")).scanCommand(args),
|
|
112
|
+
graph: async (args) => (await import("../lib/commands/graph.mjs")).graphCommand(args),
|
|
113
|
+
stability: async (args) => (await import("../lib/commands/stability.mjs")).stabilityCommand(args),
|
|
114
|
+
freeze: async (args) => (await import("../lib/commands/stability.mjs")).freezeCommand(args),
|
|
115
|
+
thaw: async (args) => (await import("../lib/commands/stability.mjs")).thawCommand(args),
|
|
116
|
+
why: async (args) => (await import("../lib/commands/why.mjs")).whyCommand(args),
|
|
117
|
+
impact: async (args) => (await import("../lib/commands/impact.mjs")).impactCommand(args),
|
|
106
118
|
};
|
|
107
119
|
|
|
108
120
|
function formatCommandsHelp() {
|
|
@@ -363,12 +375,36 @@ ${formatCommandsHelp()}
|
|
|
363
375
|
--dry-run Print results without writing files
|
|
364
376
|
--json Machine-readable scan output
|
|
365
377
|
|
|
378
|
+
${bold("graph options:")}
|
|
379
|
+
--cap <id> Show dependency view for a single capability
|
|
380
|
+
--check Exit 1 if breaking dependency changes detected (CI gate)
|
|
381
|
+
--json Machine-readable graph output
|
|
382
|
+
|
|
383
|
+
${bold("stability / freeze / thaw options:")}
|
|
384
|
+
infernoflow stability List all capabilities with their stability level
|
|
385
|
+
infernoflow freeze <id> Mark capability as frozen (AI won't touch it)
|
|
386
|
+
infernoflow freeze <id> --stable Mark as stable (careful, not forbidden)
|
|
387
|
+
infernoflow thaw <id> Reset to experimental (liquid — free to change)
|
|
388
|
+
--json Machine-readable stability list
|
|
389
|
+
|
|
366
390
|
${bold("review options:")}
|
|
367
391
|
--unstaged Review all working-tree changes (not just staged)
|
|
368
392
|
--last Review last commit (git diff HEAD~1)
|
|
369
393
|
--dry-run Print the AI prompt only — no API call made
|
|
370
394
|
--json Machine-readable output (affectedCaps, summary, provider)
|
|
371
395
|
|
|
396
|
+
${bold("why options:")}
|
|
397
|
+
infernoflow why <file> Show capability for a source file
|
|
398
|
+
infernoflow why <functionName> Show capability for a function name
|
|
399
|
+
--function <name> Filter to a specific function when multiple caps match
|
|
400
|
+
--json Machine-readable output
|
|
401
|
+
|
|
402
|
+
${bold("impact options:")}
|
|
403
|
+
infernoflow impact <cap-id> Show blast radius for a capability
|
|
404
|
+
--depth <n> Max transitive depth to traverse (default: 10)
|
|
405
|
+
--check Exit 1 if risk level is HIGH or CRITICAL (CI gate)
|
|
406
|
+
--json Machine-readable output
|
|
407
|
+
|
|
372
408
|
${bold("Machine output:")}
|
|
373
409
|
${gray("status --json")}
|
|
374
410
|
${gray("check --json")}
|
|
@@ -13,6 +13,7 @@ import * as fs from "node:fs";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { readProfile } from "../learning/profile.mjs";
|
|
15
15
|
import { ok, warn, info, bold, done } from "../ui/output.mjs";
|
|
16
|
+
import { buildStabilitySummary } from "./stability.mjs";
|
|
16
17
|
|
|
17
18
|
// ── CLAUDE.md template ────────────────────────────────────────────────────────
|
|
18
19
|
//
|
|
@@ -21,7 +22,7 @@ import { ok, warn, info, bold, done } from "../ui/output.mjs";
|
|
|
21
22
|
// session. By putting infernoflow behavior instructions here, Claude becomes
|
|
22
23
|
// the automatic executor of infernoflow — no developer involvement needed.
|
|
23
24
|
//
|
|
24
|
-
function buildClaudeMd(profile, contract) {
|
|
25
|
+
function buildClaudeMd(profile, contract, caps) {
|
|
25
26
|
const framework = profile?.stack?.framework !== "unknown" ? profile.stack.framework : null;
|
|
26
27
|
const language = profile?.stack?.language !== "unknown" ? profile.stack.language : null;
|
|
27
28
|
const stackLine = framework
|
|
@@ -33,6 +34,8 @@ function buildClaudeMd(profile, contract) {
|
|
|
33
34
|
? `The contract currently tracks **${capCount} capabilities**.`
|
|
34
35
|
: "";
|
|
35
36
|
|
|
37
|
+
const stabilitySummary = caps ? buildStabilitySummary(caps) : null;
|
|
38
|
+
|
|
36
39
|
return `# infernoflow — Automatic AI Behavior
|
|
37
40
|
|
|
38
41
|
> This file is managed by infernoflow. Edit the sections marked "customizable".
|
|
@@ -114,6 +117,8 @@ multi-step task repeatedly:
|
|
|
114
117
|
|
|
115
118
|
---
|
|
116
119
|
|
|
120
|
+
${stabilitySummary ? stabilitySummary + "\n\n---" : ""}
|
|
121
|
+
|
|
117
122
|
## Rules
|
|
118
123
|
|
|
119
124
|
| Rule | Why |
|
|
@@ -123,6 +128,7 @@ multi-step task repeatedly:
|
|
|
123
128
|
| Never block the developer waiting for infernoflow | Run tools async when possible |
|
|
124
129
|
| Always call \`infernoflow_context\` at session start | It keeps your context current |
|
|
125
130
|
| Treat contract drift like a linting error — fix it silently | Not the developer's problem |
|
|
131
|
+
| Never modify frozen capabilities without explicit instruction | Stability contract must be respected |
|
|
126
132
|
|
|
127
133
|
---
|
|
128
134
|
|
|
@@ -156,13 +162,18 @@ multi-step task repeatedly:
|
|
|
156
162
|
export function writeClaudeMd(cwd, infernoDir, { force = false } = {}) {
|
|
157
163
|
const claudeMdPath = path.join(cwd, "CLAUDE.md");
|
|
158
164
|
|
|
159
|
-
// Load project profile + contract for context
|
|
165
|
+
// Load project profile + contract + capabilities for context
|
|
160
166
|
let profile = null;
|
|
161
167
|
let contract = null;
|
|
168
|
+
let caps = null;
|
|
162
169
|
try { profile = readProfile(infernoDir); } catch {}
|
|
163
170
|
try { contract = JSON.parse(fs.readFileSync(path.join(infernoDir, "contract.json"), "utf8")); } catch {}
|
|
171
|
+
try {
|
|
172
|
+
const raw = JSON.parse(fs.readFileSync(path.join(infernoDir, "capabilities.json"), "utf8"));
|
|
173
|
+
caps = Array.isArray(raw) ? raw : (raw.capabilities || []);
|
|
174
|
+
} catch {}
|
|
164
175
|
|
|
165
|
-
const newContent = buildClaudeMd(profile, contract);
|
|
176
|
+
const newContent = buildClaudeMd(profile, contract, caps);
|
|
166
177
|
|
|
167
178
|
// If file exists and not forcing, preserve the customizable section
|
|
168
179
|
if (fs.existsSync(claudeMdPath) && !force) {
|
|
@@ -18,7 +18,7 @@ import * as fs from "node:fs";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import * as http from "node:http";
|
|
20
20
|
import * as os from "node:os";
|
|
21
|
-
import { execSync } from "node:child_process";
|
|
21
|
+
import { execSync, spawn } from "node:child_process";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { header, ok, info, warn, bold, cyan, gray } from "../ui/output.mjs";
|
|
24
24
|
|
|
@@ -166,6 +166,18 @@ function loadGitAnalytics(cwd, infernoDir) {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
function loadScan(infernoDir) {
|
|
170
|
+
const p = path.join(infernoDir, "scan.json");
|
|
171
|
+
if (!fs.existsSync(p)) return null;
|
|
172
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function loadGraph(infernoDir) {
|
|
176
|
+
const p = path.join(infernoDir, "graph.json");
|
|
177
|
+
if (!fs.existsSync(p)) return null;
|
|
178
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
179
|
+
}
|
|
180
|
+
|
|
169
181
|
function gatherData(infernoDir) {
|
|
170
182
|
const caps = loadCapabilities(infernoDir);
|
|
171
183
|
const contract = loadContract(infernoDir);
|
|
@@ -182,8 +194,10 @@ function gatherData(infernoDir) {
|
|
|
182
194
|
];
|
|
183
195
|
const cwd = path.dirname(infernoDir);
|
|
184
196
|
const analytics = loadGitAnalytics(cwd, infernoDir);
|
|
197
|
+
const scan = loadScan(infernoDir);
|
|
198
|
+
const graph = loadGraph(infernoDir);
|
|
185
199
|
|
|
186
|
-
return { caps, contract, agents, hookLog, check, sessions, candidates, audit, links, analytics, infernoDir };
|
|
200
|
+
return { caps, contract, agents, hookLog, check, sessions, candidates, audit, links, analytics, scan, graph, infernoDir };
|
|
187
201
|
}
|
|
188
202
|
|
|
189
203
|
// ── HTML builder ──────────────────────────────────────────────────────────────
|
|
@@ -525,13 +539,210 @@ function buildHtml(data, projectName) {
|
|
|
525
539
|
<div class="empty">No audit data yet — run <code>infernoflow audit</code> to classify capabilities by security sensitivity</div>
|
|
526
540
|
</section>`}
|
|
527
541
|
|
|
542
|
+
|
|
543
|
+
<!-- ── Command Center ────────────────────────────────────────────────────── -->
|
|
544
|
+
<section id="command-center">
|
|
545
|
+
<h2>🎛️ Command Center</h2>
|
|
546
|
+
<div class="cc-layout">
|
|
547
|
+
<!-- Left: capability list -->
|
|
548
|
+
<div class="cc-caps">
|
|
549
|
+
<h3>Capabilities</h3>
|
|
550
|
+
<div class="cc-cap-list" id="cc-cap-list">
|
|
551
|
+
${data.caps.map(c => {
|
|
552
|
+
const stability = c.stability || "experimental";
|
|
553
|
+
const icon = stability === "frozen" ? "🧊" : stability === "stable" ? "〰️" : "🌊";
|
|
554
|
+
const scanEntry = data.scan?.capabilities?.find(s => s.id === c.id);
|
|
555
|
+
const files = scanEntry?.codeAnalysis?.sourceFiles || [];
|
|
556
|
+
return `<div class="cc-cap-row" onclick="capDetail('${esc(c.id)}')">
|
|
557
|
+
<span class="cc-icon">${icon}</span>
|
|
558
|
+
<div class="cc-cap-info">
|
|
559
|
+
<span class="cc-cap-id">${esc(c.id)}</span>
|
|
560
|
+
${files.length ? `<span class="cc-cap-file">${esc(files[0])}</span>` : ""}
|
|
561
|
+
</div>
|
|
562
|
+
<span class="cc-stab cc-stab-${stability}" onclick="event.stopPropagation();cycleStability('${esc(c.id)}','${stability}')" title="Click to change stability">${stability}</span>
|
|
563
|
+
</div>`;
|
|
564
|
+
}).join("")}
|
|
565
|
+
${data.caps.length === 0 ? `<div class="empty">No capabilities — run <code>infernoflow init</code></div>` : ""}
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<!-- Middle: quick command buttons -->
|
|
570
|
+
<div class="cc-commands">
|
|
571
|
+
<h3>Quick Commands</h3>
|
|
572
|
+
<div class="cc-btn-grid">
|
|
573
|
+
<button class="cc-btn cc-btn-blue" onclick="runCmd('scan')">🔬 scan</button>
|
|
574
|
+
<button class="cc-btn cc-btn-blue" onclick="runCmd('graph')">🕸️ graph</button>
|
|
575
|
+
<button class="cc-btn cc-btn-blue" onclick="runCmd('stability')">💧 stability</button>
|
|
576
|
+
<button class="cc-btn cc-btn-blue" onclick="runCmd('check')">✅ check</button>
|
|
577
|
+
<button class="cc-btn cc-btn-orange" onclick="runCmd('doctor')">🩺 doctor</button>
|
|
578
|
+
<button class="cc-btn cc-btn-orange" onclick="runCmd('coverage')">📊 coverage</button>
|
|
579
|
+
<button class="cc-btn cc-btn-green" onclick="runCmd('status')">📡 status</button>
|
|
580
|
+
<button class="cc-btn cc-btn-green" onclick="runCmd('health')">❤️ health</button>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
<h3 style="margin-top:18px">Capability Actions</h3>
|
|
584
|
+
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
|
|
585
|
+
<input id="cc-capinput" class="cc-input" placeholder="capability-id" style="flex:1;min-width:120px"/>
|
|
586
|
+
<button class="cc-btn cc-btn-blue" onclick="runCapCmd('why')">🔍 why</button>
|
|
587
|
+
<button class="cc-btn cc-btn-blue" onclick="runCapCmd('impact')">💥 impact</button>
|
|
588
|
+
<button class="cc-btn cc-btn-red" onclick="runCapCmd('freeze')">🧊 freeze</button>
|
|
589
|
+
<button class="cc-btn cc-btn-green" onclick="runCapCmd('thaw')">🌊 thaw</button>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<!-- Terminal output -->
|
|
593
|
+
<h3>Terminal Output</h3>
|
|
594
|
+
<div class="cc-terminal" id="cc-terminal">
|
|
595
|
+
<span class="cc-prompt">Ready. Click a command or capability to begin.</span>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
|
|
599
|
+
<!-- Right: cap detail -->
|
|
600
|
+
<div class="cc-detail" id="cc-detail">
|
|
601
|
+
<h3>Capability Detail</h3>
|
|
602
|
+
<div class="empty" id="cc-detail-inner">Click a capability to see its impact analysis.</div>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
</section>
|
|
606
|
+
|
|
528
607
|
</main>
|
|
529
|
-
<footer>infernoflow dashboard · auto-refreshes
|
|
608
|
+
<footer>infernoflow dashboard · auto-refreshes when inferno/ changes · <a href="/" style="color:var(--muted)">refresh now</a></footer>
|
|
609
|
+
<style>
|
|
610
|
+
/* Command Center styles */
|
|
611
|
+
.cc-layout { display:grid; grid-template-columns:220px 1fr 280px; gap:16px; margin-top:12px; min-height:420px; }
|
|
612
|
+
.cc-caps { background:var(--card); border-radius:8px; padding:12px; overflow:hidden; }
|
|
613
|
+
.cc-caps h3, .cc-commands h3, .cc-detail h3 { font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:0 0 10px 0; }
|
|
614
|
+
.cc-cap-list { display:flex; flex-direction:column; gap:4px; max-height:360px; overflow-y:auto; }
|
|
615
|
+
.cc-cap-row { display:flex; align-items:center; gap:8px; padding:7px 8px; border-radius:6px; cursor:pointer; transition:background .15s; }
|
|
616
|
+
.cc-cap-row:hover { background:rgba(255,255,255,.06); }
|
|
617
|
+
.cc-icon { font-size:14px; flex-shrink:0; }
|
|
618
|
+
.cc-cap-info { flex:1; min-width:0; }
|
|
619
|
+
.cc-cap-id { display:block; font-size:12px; font-weight:600; color:var(--fg); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
620
|
+
.cc-cap-file { display:block; font-size:10px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
621
|
+
.cc-stab { font-size:10px; padding:2px 6px; border-radius:10px; cursor:pointer; white-space:nowrap; flex-shrink:0; }
|
|
622
|
+
.cc-stab-frozen { background:#7f1d1d; color:#fca5a5; }
|
|
623
|
+
.cc-stab-stable { background:#78350f; color:#fcd34d; }
|
|
624
|
+
.cc-stab-experimental { background:#14532d; color:#86efac; }
|
|
625
|
+
.cc-commands { background:var(--card); border-radius:8px; padding:12px; display:flex; flex-direction:column; }
|
|
626
|
+
.cc-btn-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
|
627
|
+
.cc-btn { border:none; border-radius:6px; padding:8px 12px; font-size:12px; font-weight:600; cursor:pointer; transition:opacity .15s; }
|
|
628
|
+
.cc-btn:hover { opacity:.85; }
|
|
629
|
+
.cc-btn-blue { background:#1d4ed8; color:#fff; }
|
|
630
|
+
.cc-btn-orange { background:#c2410c; color:#fff; }
|
|
631
|
+
.cc-btn-green { background:#15803d; color:#fff; }
|
|
632
|
+
.cc-btn-red { background:#b91c1c; color:#fff; }
|
|
633
|
+
.cc-input { background:#1e2030; border:1px solid #374151; border-radius:6px; color:var(--fg); padding:7px 10px; font-size:12px; outline:none; }
|
|
634
|
+
.cc-input:focus { border-color:#3b82f6; }
|
|
635
|
+
.cc-terminal { background:#0d0f1a; border:1px solid #1e2030; border-radius:6px; padding:12px; font-family:monospace; font-size:11px; line-height:1.6; color:#a3e635; flex:1; min-height:180px; max-height:240px; overflow-y:auto; white-space:pre-wrap; word-break:break-all; margin-top:4px; }
|
|
636
|
+
.cc-prompt { color:var(--muted); }
|
|
637
|
+
.cc-detail { background:var(--card); border-radius:8px; padding:12px; overflow-y:auto; }
|
|
638
|
+
.cc-detail-section { margin-bottom:14px; }
|
|
639
|
+
.cc-detail-section h4 { font-size:11px; color:var(--muted); margin:0 0 6px 0; text-transform:uppercase; letter-spacing:.05em; }
|
|
640
|
+
.cc-detail-row { display:flex; justify-content:space-between; font-size:12px; padding:3px 0; border-bottom:1px solid #1e2030; }
|
|
641
|
+
.cc-detail-dep { font-size:12px; padding:3px 0; }
|
|
642
|
+
.cc-risk-low { color:#22c55e; font-weight:700; }
|
|
643
|
+
.cc-risk-medium { color:#f59e0b; font-weight:700; }
|
|
644
|
+
.cc-risk-high { color:#ef4444; font-weight:700; }
|
|
645
|
+
.cc-risk-critical { color:#ef4444; font-weight:700; text-transform:uppercase; }
|
|
646
|
+
</style>
|
|
530
647
|
<script>
|
|
531
648
|
// SSE live reload
|
|
532
649
|
const es = new EventSource('/events');
|
|
533
650
|
es.onmessage = () => window.location.reload();
|
|
534
651
|
es.onerror = () => {};
|
|
652
|
+
|
|
653
|
+
// ── Command runner ──────────────────────────────────────────────────────────
|
|
654
|
+
const terminal = document.getElementById('cc-terminal');
|
|
655
|
+
|
|
656
|
+
async function runCmd(command, args = []) {
|
|
657
|
+
terminal.textContent = '$ infernoflow ' + command + (args.length ? ' ' + args.join(' ') : '') + '\\n';
|
|
658
|
+
try {
|
|
659
|
+
const res = await fetch('/api/run', {
|
|
660
|
+
method: 'POST',
|
|
661
|
+
headers: { 'Content-Type': 'application/json' },
|
|
662
|
+
body: JSON.stringify({ command, args }),
|
|
663
|
+
});
|
|
664
|
+
const reader = res.body.getReader();
|
|
665
|
+
const dec = new TextDecoder();
|
|
666
|
+
while (true) {
|
|
667
|
+
const { done, value } = await reader.read();
|
|
668
|
+
if (done) break;
|
|
669
|
+
terminal.textContent += dec.decode(value);
|
|
670
|
+
terminal.scrollTop = terminal.scrollHeight;
|
|
671
|
+
}
|
|
672
|
+
} catch (e) {
|
|
673
|
+
terminal.textContent += '\\nError: ' + e.message;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function runCapCmd(command) {
|
|
678
|
+
const capId = document.getElementById('cc-capinput').value.trim();
|
|
679
|
+
if (!capId) { terminal.textContent = 'Enter a capability ID first.'; return; }
|
|
680
|
+
runCmd(command, [capId]);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ── Capability detail panel ─────────────────────────────────────────────────
|
|
684
|
+
async function capDetail(capId) {
|
|
685
|
+
document.getElementById('cc-capinput').value = capId;
|
|
686
|
+
const detail = document.getElementById('cc-detail-inner');
|
|
687
|
+
detail.innerHTML = '<div class="empty">Loading…</div>';
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
const [why, impact] = await Promise.all([
|
|
691
|
+
fetch('/api/cap/' + encodeURIComponent(capId) + '/why').then(r => r.json()),
|
|
692
|
+
fetch('/api/cap/' + encodeURIComponent(capId) + '/impact').then(r => r.json()),
|
|
693
|
+
]);
|
|
694
|
+
|
|
695
|
+
const w = Array.isArray(why) ? why[0] : why;
|
|
696
|
+
const im = impact?.capId ? impact : null;
|
|
697
|
+
|
|
698
|
+
let html = '';
|
|
699
|
+
|
|
700
|
+
if (w) {
|
|
701
|
+
html += '<div class="cc-detail-section">';
|
|
702
|
+
html += '<h4>📍 ' + (w.name || w.capId) + '</h4>';
|
|
703
|
+
html += '<div class="cc-detail-row"><span>Stability</span><span class="cc-stab cc-stab-' + w.stability + '">' + w.stability + '</span></div>';
|
|
704
|
+
if (w.sourceFiles?.length) html += '<div class="cc-detail-row"><span>Files</span><span style="color:#7dd3fc">' + w.sourceFiles.join(', ') + '</span></div>';
|
|
705
|
+
if (w.services?.length) html += '<div class="cc-detail-row"><span>Uses</span><span style="color:#a78bfa">' + w.services.join(', ') + '</span></div>';
|
|
706
|
+
if (w.throws?.length) html += '<div class="cc-detail-row"><span>Throws</span><span style="color:#f97316">' + w.throws.join(', ') + '</span></div>';
|
|
707
|
+
html += '</div>';
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (im) {
|
|
711
|
+
const riskCls = 'cc-risk-' + im.risk;
|
|
712
|
+
html += '<div class="cc-detail-section">';
|
|
713
|
+
html += '<h4>💥 Impact</h4>';
|
|
714
|
+
html += '<div class="cc-detail-row"><span>Risk</span><span class="' + riskCls + '">' + im.risk.toUpperCase() + '</span></div>';
|
|
715
|
+
html += '<div class="cc-detail-row"><span>Direct deps</span><span>' + im.summary.directCount + '</span></div>';
|
|
716
|
+
html += '<div class="cc-detail-row"><span>Transitive</span><span>' + im.summary.transitiveCount + '</span></div>';
|
|
717
|
+
if (im.direct?.length) {
|
|
718
|
+
html += '<h4 style="margin-top:10px">Direct dependents</h4>';
|
|
719
|
+
im.direct.forEach(d => { html += '<div class="cc-detail-dep">→ <code>' + d + '</code></div>'; });
|
|
720
|
+
}
|
|
721
|
+
if (im.affectedScenarios?.length) {
|
|
722
|
+
html += '<h4 style="margin-top:10px">Scenarios at risk</h4>';
|
|
723
|
+
im.affectedScenarios.forEach(s => { html += '<div class="cc-detail-dep">⚠️ ' + s + '</div>'; });
|
|
724
|
+
}
|
|
725
|
+
html += '</div>';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!html) html = '<div class="empty">No data found for ' + capId + ' — run infernoflow scan first.</div>';
|
|
729
|
+
detail.innerHTML = html;
|
|
730
|
+
} catch (e) {
|
|
731
|
+
detail.innerHTML = '<div class="empty">Error: ' + e.message + '</div>';
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ── Stability cycle ─────────────────────────────────────────────────────────
|
|
736
|
+
async function cycleStability(capId, current) {
|
|
737
|
+
const next = current === 'experimental' ? 'stable' : current === 'stable' ? 'frozen' : 'experimental';
|
|
738
|
+
if (!confirm('Change ' + capId + ' from ' + current + ' → ' + next + '?')) return;
|
|
739
|
+
await fetch('/api/freeze', {
|
|
740
|
+
method: 'POST',
|
|
741
|
+
headers: { 'Content-Type': 'application/json' },
|
|
742
|
+
body: JSON.stringify({ capId, level: next }),
|
|
743
|
+
});
|
|
744
|
+
window.location.reload();
|
|
745
|
+
}
|
|
535
746
|
</script>
|
|
536
747
|
</body>
|
|
537
748
|
</html>`;
|
|
@@ -586,6 +797,104 @@ function startServer(infernoDir, port) {
|
|
|
586
797
|
return;
|
|
587
798
|
}
|
|
588
799
|
|
|
800
|
+
// ── Command runner: POST /api/run { command, args[] } ─────────────────────
|
|
801
|
+
if (req.url === "/api/run" && req.method === "POST") {
|
|
802
|
+
let body = "";
|
|
803
|
+
req.on("data", chunk => { body += chunk; });
|
|
804
|
+
req.on("end", () => {
|
|
805
|
+
try {
|
|
806
|
+
const { command = "", args = [] } = JSON.parse(body);
|
|
807
|
+
const binPath = path.join(__dirname, "../../bin/infernoflow.mjs");
|
|
808
|
+
res.writeHead(200, {
|
|
809
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
810
|
+
"Transfer-Encoding": "chunked",
|
|
811
|
+
"Cache-Control": "no-cache",
|
|
812
|
+
});
|
|
813
|
+
const child = spawn(process.execPath, [binPath, command, ...args], {
|
|
814
|
+
cwd,
|
|
815
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
816
|
+
});
|
|
817
|
+
child.stdout.on("data", d => res.write(d));
|
|
818
|
+
child.stderr.on("data", d => res.write(d));
|
|
819
|
+
child.on("close", code => {
|
|
820
|
+
res.write(`\n[exit ${code}]\n`);
|
|
821
|
+
res.end();
|
|
822
|
+
});
|
|
823
|
+
child.on("error", err => {
|
|
824
|
+
res.write(`\nError spawning command: ${err.message}\n`);
|
|
825
|
+
res.end();
|
|
826
|
+
});
|
|
827
|
+
} catch (err) {
|
|
828
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
829
|
+
res.end("Bad request: " + err.message);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ── Capability why: GET /api/cap/:id/why ──────────────────────────────────
|
|
836
|
+
const whyMatch = req.url?.match(/^\/api\/cap\/([^/]+)\/why$/);
|
|
837
|
+
if (whyMatch) {
|
|
838
|
+
const capId = decodeURIComponent(whyMatch[1]);
|
|
839
|
+
const binPath = path.join(__dirname, "../../bin/infernoflow.mjs");
|
|
840
|
+
let output = "";
|
|
841
|
+
const child = spawn(process.execPath, [binPath, "why", capId, "--json"], {
|
|
842
|
+
cwd, env: { ...process.env, FORCE_COLOR: "0" },
|
|
843
|
+
});
|
|
844
|
+
child.stdout.on("data", d => { output += d; });
|
|
845
|
+
child.stderr.on("data", () => {});
|
|
846
|
+
child.on("close", () => {
|
|
847
|
+
try {
|
|
848
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
849
|
+
res.end(output.trim() || "[]");
|
|
850
|
+
} catch {}
|
|
851
|
+
});
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ── Capability impact: GET /api/cap/:id/impact ────────────────────────────
|
|
856
|
+
const impactMatch = req.url?.match(/^\/api\/cap\/([^/]+)\/impact$/);
|
|
857
|
+
if (impactMatch) {
|
|
858
|
+
const capId = decodeURIComponent(impactMatch[1]);
|
|
859
|
+
const binPath = path.join(__dirname, "../../bin/infernoflow.mjs");
|
|
860
|
+
let output = "";
|
|
861
|
+
const child = spawn(process.execPath, [binPath, "impact", capId, "--json"], {
|
|
862
|
+
cwd, env: { ...process.env, FORCE_COLOR: "0" },
|
|
863
|
+
});
|
|
864
|
+
child.stdout.on("data", d => { output += d; });
|
|
865
|
+
child.stderr.on("data", () => {});
|
|
866
|
+
child.on("close", () => {
|
|
867
|
+
try {
|
|
868
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
869
|
+
res.end(output.trim() || "{}");
|
|
870
|
+
} catch {}
|
|
871
|
+
});
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ── Freeze/thaw: POST /api/freeze { capId, level } ───────────────────────
|
|
876
|
+
if (req.url === "/api/freeze" && req.method === "POST") {
|
|
877
|
+
let body = "";
|
|
878
|
+
req.on("data", chunk => { body += chunk; });
|
|
879
|
+
req.on("end", () => {
|
|
880
|
+
try {
|
|
881
|
+
const { capId, level } = JSON.parse(body);
|
|
882
|
+
const binPath = path.join(__dirname, "../../bin/infernoflow.mjs");
|
|
883
|
+
const cmd = level === "experimental" ? "thaw" : "freeze";
|
|
884
|
+
const args = level === "stable" ? [capId, "--stable"] : [capId];
|
|
885
|
+
const child = spawn(process.execPath, [binPath, cmd, ...args], { cwd });
|
|
886
|
+
child.on("close", () => {
|
|
887
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
888
|
+
res.end(JSON.stringify({ ok: true }));
|
|
889
|
+
});
|
|
890
|
+
} catch (err) {
|
|
891
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
892
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
589
898
|
// Dashboard HTML
|
|
590
899
|
try {
|
|
591
900
|
const data = gatherData(infernoDir);
|