infernoflow 0.24.0 → 0.28.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 +33 -0
- 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/scaffold.mjs +489 -0
- package/dist/lib/commands/scan.mjs +45 -26
- package/dist/lib/commands/why.mjs +358 -0
- package/package.json +1 -1
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -55,9 +55,13 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
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
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",
|
|
58
59
|
stability: "Show solid/liquid stability level for every capability (frozen/stable/experimental)",
|
|
59
60
|
freeze: "Mark a capability as frozen (solid) — AI will not modify it without explicit instruction",
|
|
60
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",
|
|
64
|
+
scaffold: "Generate a new capability — source skeleton, contract registration, and placeholder scenario in one command",
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
const COMMAND_HANDLERS = {
|
|
@@ -106,9 +110,13 @@ const COMMAND_HANDLERS = {
|
|
|
106
110
|
coverage: async (args) => (await import("../lib/commands/coverage.mjs")).coverageCommand(args),
|
|
107
111
|
review: async (args) => (await import("../lib/commands/review.mjs")).reviewCommand(args),
|
|
108
112
|
scan: async (args) => (await import("../lib/commands/scan.mjs")).scanCommand(args),
|
|
113
|
+
graph: async (args) => (await import("../lib/commands/graph.mjs")).graphCommand(args),
|
|
109
114
|
stability: async (args) => (await import("../lib/commands/stability.mjs")).stabilityCommand(args),
|
|
110
115
|
freeze: async (args) => (await import("../lib/commands/stability.mjs")).freezeCommand(args),
|
|
111
116
|
thaw: async (args) => (await import("../lib/commands/stability.mjs")).thawCommand(args),
|
|
117
|
+
why: async (args) => (await import("../lib/commands/why.mjs")).whyCommand(args),
|
|
118
|
+
impact: async (args) => (await import("../lib/commands/impact.mjs")).impactCommand(args),
|
|
119
|
+
scaffold: async (args) => (await import("../lib/commands/scaffold.mjs")).scaffoldCommand(args),
|
|
112
120
|
};
|
|
113
121
|
|
|
114
122
|
function formatCommandsHelp() {
|
|
@@ -369,6 +377,11 @@ ${formatCommandsHelp()}
|
|
|
369
377
|
--dry-run Print results without writing files
|
|
370
378
|
--json Machine-readable scan output
|
|
371
379
|
|
|
380
|
+
${bold("graph options:")}
|
|
381
|
+
--cap <id> Show dependency view for a single capability
|
|
382
|
+
--check Exit 1 if breaking dependency changes detected (CI gate)
|
|
383
|
+
--json Machine-readable graph output
|
|
384
|
+
|
|
372
385
|
${bold("stability / freeze / thaw options:")}
|
|
373
386
|
infernoflow stability List all capabilities with their stability level
|
|
374
387
|
infernoflow freeze <id> Mark capability as frozen (AI won't touch it)
|
|
@@ -382,6 +395,26 @@ ${formatCommandsHelp()}
|
|
|
382
395
|
--dry-run Print the AI prompt only — no API call made
|
|
383
396
|
--json Machine-readable output (affectedCaps, summary, provider)
|
|
384
397
|
|
|
398
|
+
${bold("why options:")}
|
|
399
|
+
infernoflow why <file> Show capability for a source file
|
|
400
|
+
infernoflow why <functionName> Show capability for a function name
|
|
401
|
+
--function <name> Filter to a specific function when multiple caps match
|
|
402
|
+
--json Machine-readable output
|
|
403
|
+
|
|
404
|
+
${bold("impact options:")}
|
|
405
|
+
infernoflow impact <cap-id> Show blast radius for a capability
|
|
406
|
+
--depth <n> Max transitive depth to traverse (default: 10)
|
|
407
|
+
--check Exit 1 if risk level is HIGH or CRITICAL (CI gate)
|
|
408
|
+
--json Machine-readable output
|
|
409
|
+
|
|
410
|
+
${bold("scaffold options:")}
|
|
411
|
+
infernoflow scaffold <cap-id> Generate a new capability skeleton
|
|
412
|
+
--dir <path> Output directory for the source file (default: auto-detected)
|
|
413
|
+
--lang ts|js|py|go Language override (default: auto-detected from project)
|
|
414
|
+
--description "..." Capability description to embed in the file
|
|
415
|
+
--dry-run Preview what would be generated without writing files
|
|
416
|
+
--json Machine-readable output including generated code
|
|
417
|
+
|
|
385
418
|
${bold("Machine output:")}
|
|
386
419
|
${gray("status --json")}
|
|
387
420
|
${gray("check --json")}
|
|
@@ -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);
|