infernoflow 0.33.0 → 0.34.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/README.md +208 -120
- package/dist/bin/infernoflow.mjs +271 -85
- package/dist/lib/adopters/angular.mjs +128 -1
- package/dist/lib/adopters/css.mjs +111 -1
- package/dist/lib/adopters/react.mjs +104 -1
- package/dist/lib/ai/ideDetection.mjs +31 -1
- package/dist/lib/ai/localProvider.mjs +88 -1
- package/dist/lib/ai/providerRouter.mjs +295 -2
- package/dist/lib/commands/adopt.mjs +869 -20
- package/dist/lib/commands/adoptWizard.mjs +320 -9
- package/dist/lib/commands/agent.mjs +191 -5
- package/dist/lib/commands/ai.mjs +407 -2
- package/dist/lib/commands/ask.mjs +299 -0
- package/dist/lib/commands/audit.mjs +300 -13
- package/dist/lib/commands/changelog.mjs +594 -26
- package/dist/lib/commands/check.mjs +184 -3
- package/dist/lib/commands/ci.mjs +208 -3
- package/dist/lib/commands/claudeMd.mjs +139 -28
- package/dist/lib/commands/cloud.mjs +521 -5
- package/dist/lib/commands/context.mjs +346 -34
- package/dist/lib/commands/coverage.mjs +282 -2
- package/dist/lib/commands/dashboard.mjs +635 -123
- package/dist/lib/commands/demo.mjs +465 -8
- package/dist/lib/commands/diff.mjs +274 -5
- package/dist/lib/commands/docGate.mjs +81 -2
- package/dist/lib/commands/doctor.mjs +321 -3
- package/dist/lib/commands/explain.mjs +438 -8
- package/dist/lib/commands/export.mjs +239 -10
- package/dist/lib/commands/generateSkills.mjs +163 -38
- package/dist/lib/commands/graph.mjs +378 -11
- package/dist/lib/commands/health.mjs +309 -2
- package/dist/lib/commands/impact.mjs +325 -2
- package/dist/lib/commands/implement.mjs +103 -7
- package/dist/lib/commands/init.mjs +545 -23
- package/dist/lib/commands/installCursorHooks.mjs +36 -1
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
- package/dist/lib/commands/link.mjs +342 -2
- package/dist/lib/commands/log.mjs +164 -16
- package/dist/lib/commands/monorepo.mjs +428 -4
- package/dist/lib/commands/notify.mjs +258 -4
- package/dist/lib/commands/onboard.mjs +296 -4
- package/dist/lib/commands/prComment.mjs +361 -2
- package/dist/lib/commands/prImpact.mjs +157 -2
- package/dist/lib/commands/publish.mjs +316 -15
- package/dist/lib/commands/recap.mjs +359 -0
- package/dist/lib/commands/report.mjs +272 -28
- package/dist/lib/commands/review.mjs +223 -9
- package/dist/lib/commands/run.mjs +336 -8
- package/dist/lib/commands/scaffold.mjs +419 -54
- package/dist/lib/commands/scan.mjs +1118 -5
- package/dist/lib/commands/scout.mjs +291 -2
- package/dist/lib/commands/setup.mjs +310 -5
- package/dist/lib/commands/share.mjs +196 -13
- package/dist/lib/commands/snapshot.mjs +383 -3
- package/dist/lib/commands/stability.mjs +293 -2
- package/dist/lib/commands/stats.mjs +402 -0
- package/dist/lib/commands/status.mjs +172 -4
- package/dist/lib/commands/suggest.mjs +563 -21
- package/dist/lib/commands/switch.mjs +310 -0
- package/dist/lib/commands/syncAuto.mjs +96 -1
- package/dist/lib/commands/synthesize.mjs +228 -10
- package/dist/lib/commands/teamSync.mjs +388 -2
- package/dist/lib/commands/test.mjs +363 -6
- package/dist/lib/commands/theme.mjs +195 -18
- package/dist/lib/commands/upgrade.mjs +153 -0
- package/dist/lib/commands/version.mjs +282 -2
- package/dist/lib/commands/vibe.mjs +357 -7
- package/dist/lib/commands/watch.mjs +203 -4
- package/dist/lib/commands/why.mjs +358 -4
- package/dist/lib/cursorHooksInstall.mjs +60 -1
- package/dist/lib/draftToolingInstall.mjs +68 -7
- package/dist/lib/git/detect-drift.mjs +208 -4
- package/dist/lib/learning/adapt.mjs +101 -6
- package/dist/lib/learning/observe.mjs +119 -1
- package/dist/lib/learning/patternDetector.mjs +298 -1
- package/dist/lib/learning/profile.mjs +279 -2
- package/dist/lib/learning/skillSynthesizer.mjs +145 -24
- package/dist/lib/templates/index.mjs +131 -1
- package/dist/lib/theme/scanner.mjs +343 -4
- package/dist/lib/ui/errors.mjs +142 -1
- package/dist/lib/ui/output.mjs +72 -6
- package/dist/lib/ui/prompts.mjs +147 -6
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
- package/package.json +1 -1
|
@@ -1,37 +1,342 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow dashboard
|
|
3
|
+
*
|
|
4
|
+
* Launches a local web server on http://localhost:7337 showing:
|
|
5
|
+
* - Contract health status
|
|
6
|
+
* - Capability list with add/remove/change history
|
|
7
|
+
* - Drift timeline (last N sessions)
|
|
8
|
+
* - Agent activity log
|
|
9
|
+
* - Auto-refresh via SSE (server-sent events)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* infernoflow dashboard # open on port 7337
|
|
13
|
+
* infernoflow dashboard --port 8080 # custom port
|
|
14
|
+
* infernoflow dashboard --no-open # don't auto-open browser
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import * as http from "node:http";
|
|
20
|
+
import * as os from "node:os";
|
|
21
|
+
import { execSync, spawn } from "node:child_process";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { header, ok, info, warn, bold, cyan, gray } from "../ui/output.mjs";
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
// ── data loaders ──────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function loadContract(infernoDir) {
|
|
30
|
+
const contractPath = path.join(infernoDir, "contract.json");
|
|
31
|
+
if (!fs.existsSync(contractPath)) return null;
|
|
32
|
+
try { return JSON.parse(fs.readFileSync(contractPath, "utf8")); } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadCapabilities(infernoDir) {
|
|
36
|
+
for (const name of ["capabilities.json", "contract.json"]) {
|
|
37
|
+
const p = path.join(infernoDir, name);
|
|
38
|
+
if (!fs.existsSync(p)) continue;
|
|
39
|
+
try {
|
|
40
|
+
const obj = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
41
|
+
const raw = obj.capabilities || [];
|
|
42
|
+
return raw.map(c => typeof c === "string" ? { id: c, title: c } : c);
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadProfile(infernoDir) {
|
|
49
|
+
const p = path.join(infernoDir, "developer-profile.json");
|
|
50
|
+
if (!fs.existsSync(p)) return null;
|
|
51
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadAgents(infernoDir) {
|
|
55
|
+
const agentsDir = path.join(infernoDir, "agents");
|
|
56
|
+
if (!fs.existsSync(agentsDir)) return [];
|
|
57
|
+
return fs.readdirSync(agentsDir)
|
|
58
|
+
.filter(f => f.endsWith(".json"))
|
|
59
|
+
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(agentsDir, f), "utf8")); } catch { return null; } })
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadHookLog(infernoDir) {
|
|
64
|
+
const logPath = path.join(infernoDir, "HOOK.log");
|
|
65
|
+
if (!fs.existsSync(logPath)) return null;
|
|
66
|
+
try { return JSON.parse(fs.readFileSync(logPath, "utf8")); } catch { return null; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runCheck(infernoDir) {
|
|
70
|
+
try {
|
|
71
|
+
const out = execSync("npx infernoflow check --json", {
|
|
72
|
+
cwd: path.dirname(infernoDir),
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
timeout: 15_000,
|
|
75
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
76
|
+
});
|
|
77
|
+
return JSON.parse(out);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
try { return JSON.parse(err.stdout || "{}"); } catch { return { status: "error", error: "check failed" }; }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Analytics data loaders ────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function loadAudit(infernoDir) {
|
|
86
|
+
const p = path.join(infernoDir, "audit.json");
|
|
87
|
+
if (!fs.existsSync(p)) return null;
|
|
88
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadLinks(infernoDir) {
|
|
92
|
+
const p = path.join(infernoDir, "links.json");
|
|
93
|
+
if (!fs.existsSync(p)) return [];
|
|
94
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return []; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse git log for inferno/ directory to build analytics:
|
|
99
|
+
* - capability velocity (caps added/removed per week)
|
|
100
|
+
* - contributor activity (commits per author)
|
|
101
|
+
* - health score trend (from check logs or heuristic via commit frequency)
|
|
102
|
+
*/
|
|
103
|
+
function loadGitAnalytics(cwd, infernoDir) {
|
|
104
|
+
try {
|
|
105
|
+
// Commits touching inferno/ in past 90 days (iso date, author email, subject)
|
|
106
|
+
const raw = execSync(
|
|
107
|
+
`git log --since="90 days ago" --format="%aI|%ae|%s" -- inferno/`,
|
|
108
|
+
{ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 8000 }
|
|
109
|
+
).trim();
|
|
110
|
+
|
|
111
|
+
if (!raw) return { velocity: [], contributors: [], healthTrend: [] };
|
|
112
|
+
|
|
113
|
+
const commits = raw.split("\n").filter(Boolean).map(line => {
|
|
114
|
+
const [date, email, ...subjectParts] = line.split("|");
|
|
115
|
+
return { date: new Date(date), email: email || "unknown", subject: subjectParts.join("|") };
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Bucket by ISO week (YYYY-Www)
|
|
119
|
+
function isoWeek(d) {
|
|
120
|
+
const dt = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
121
|
+
const day = dt.getUTCDay() || 7;
|
|
122
|
+
dt.setUTCDate(dt.getUTCDate() + 4 - day);
|
|
123
|
+
const yearStart = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
|
|
124
|
+
const week = Math.ceil((((dt - yearStart) / 86400000) + 1) / 7);
|
|
125
|
+
return `${dt.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Velocity: commits per week
|
|
129
|
+
const weekMap = new Map();
|
|
130
|
+
for (const c of commits) {
|
|
131
|
+
const w = isoWeek(c.date);
|
|
132
|
+
weekMap.set(w, (weekMap.get(w) || 0) + 1);
|
|
133
|
+
}
|
|
134
|
+
// Fill in the last 13 weeks
|
|
135
|
+
const velocity = [];
|
|
136
|
+
const now = new Date();
|
|
137
|
+
for (let i = 12; i >= 0; i--) {
|
|
138
|
+
const d = new Date(now);
|
|
139
|
+
d.setDate(d.getDate() - i * 7);
|
|
140
|
+
const w = isoWeek(d);
|
|
141
|
+
velocity.push({ week: w, commits: weekMap.get(w) || 0 });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Contributors: unique authors, sorted by commit count
|
|
145
|
+
const authorMap = new Map();
|
|
146
|
+
for (const c of commits) {
|
|
147
|
+
const name = c.email.split("@")[0];
|
|
148
|
+
authorMap.set(name, (authorMap.get(name) || 0) + 1);
|
|
149
|
+
}
|
|
150
|
+
const contributors = [...authorMap.entries()]
|
|
151
|
+
.map(([name, count]) => ({ name, count }))
|
|
152
|
+
.sort((a, b) => b.count - a.count)
|
|
153
|
+
.slice(0, 8);
|
|
154
|
+
|
|
155
|
+
// Health trend: simple heuristic from commit density per week
|
|
156
|
+
// More commits → more drift activity. We mark weeks with >3 commits as "busy" (amber), 0 = stale, else ok
|
|
157
|
+
const healthTrend = velocity.map(v => ({
|
|
158
|
+
week: v.week,
|
|
159
|
+
score: v.commits === 0 ? 40 : v.commits <= 2 ? 75 : v.commits <= 5 ? 90 : 85,
|
|
160
|
+
label: v.commits === 0 ? "stale" : v.commits <= 2 ? "ok" : v.commits <= 5 ? "healthy" : "busy",
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
return { velocity, contributors, healthTrend };
|
|
164
|
+
} catch {
|
|
165
|
+
return { velocity: [], contributors: [], healthTrend: [] };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
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
|
+
|
|
181
|
+
function gatherData(infernoDir) {
|
|
182
|
+
const caps = loadCapabilities(infernoDir);
|
|
183
|
+
const contract = loadContract(infernoDir);
|
|
184
|
+
const profile = loadProfile(infernoDir);
|
|
185
|
+
const agents = loadAgents(infernoDir);
|
|
186
|
+
const hookLog = loadHookLog(infernoDir);
|
|
187
|
+
const check = runCheck(infernoDir);
|
|
188
|
+
const audit = loadAudit(infernoDir);
|
|
189
|
+
const links = loadLinks(infernoDir);
|
|
190
|
+
const sessions = profile?.recentSessions?.slice(-10) || [];
|
|
191
|
+
const candidates = [
|
|
192
|
+
...(profile?.agentCandidates || []),
|
|
193
|
+
...(profile?.skillCandidates || []),
|
|
194
|
+
];
|
|
195
|
+
const cwd = path.dirname(infernoDir);
|
|
196
|
+
const analytics = loadGitAnalytics(cwd, infernoDir);
|
|
197
|
+
const scan = loadScan(infernoDir);
|
|
198
|
+
const graph = loadGraph(infernoDir);
|
|
199
|
+
|
|
200
|
+
return { caps, contract, agents, hookLog, check, sessions, candidates, audit, links, analytics, scan, graph, infernoDir };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── HTML builder ──────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
// ── SVG chart builders ────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function barChart(values, labels, color = "#f97316", height = 80) {
|
|
208
|
+
const W = 600, H = height;
|
|
209
|
+
const n = values.length;
|
|
210
|
+
if (!n) return `<svg width="${W}" height="${H}"></svg>`;
|
|
211
|
+
const max = Math.max(...values, 1);
|
|
212
|
+
const bw = Math.floor(W / n) - 4;
|
|
213
|
+
const bars = values.map((v, i) => {
|
|
214
|
+
const bh = Math.max(2, Math.round((v / max) * (H - 20)));
|
|
215
|
+
const x = i * (W / n) + 2;
|
|
216
|
+
const y = H - bh - 10;
|
|
217
|
+
return `<rect x="${x}" y="${y}" width="${bw}" height="${bh}" fill="${color}" rx="2" opacity="0.85"/>
|
|
218
|
+
<title>${labels[i]}: ${v}</title>`;
|
|
219
|
+
}).join("\n");
|
|
220
|
+
return `<svg viewBox="0 0 ${W} ${H}" width="100%" height="${H}" xmlns="http://www.w3.org/2000/svg">${bars}</svg>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function lineChart(values, color = "#3b82f6", height = 80) {
|
|
224
|
+
const W = 600, H = height;
|
|
225
|
+
const n = values.length;
|
|
226
|
+
if (n < 2) return `<svg width="${W}" height="${H}"></svg>`;
|
|
227
|
+
const max = Math.max(...values, 1);
|
|
228
|
+
const min = Math.min(...values, 0);
|
|
229
|
+
const range = max - min || 1;
|
|
230
|
+
const pts = values.map((v, i) => {
|
|
231
|
+
const x = Math.round((i / (n - 1)) * (W - 20)) + 10;
|
|
232
|
+
const y = Math.round(H - 10 - ((v - min) / range) * (H - 20));
|
|
233
|
+
return `${x},${y}`;
|
|
234
|
+
}).join(" ");
|
|
235
|
+
return `<svg viewBox="0 0 ${W} ${H}" width="100%" height="${H}" xmlns="http://www.w3.org/2000/svg">
|
|
236
|
+
<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
237
|
+
${values.map((v, i) => {
|
|
238
|
+
const [px, py] = pts.split(" ")[i].split(",");
|
|
239
|
+
return `<circle cx="${px}" cy="${py}" r="4" fill="${color}"><title>${v}</title></circle>`;
|
|
240
|
+
}).join("")}
|
|
241
|
+
</svg>`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function heatRow(name, count, maxCount) {
|
|
245
|
+
const pct = maxCount > 0 ? Math.round((count / maxCount) * 100) : 0;
|
|
246
|
+
const fill = pct > 70 ? "#f97316" : pct > 40 ? "#f59e0b" : pct > 10 ? "#3b82f6" : "#2d3148";
|
|
247
|
+
return `<div class="heat-row">
|
|
248
|
+
<span class="heat-name">${esc(name)}</span>
|
|
249
|
+
<div class="heat-bar-wrap"><div class="heat-bar" style="width:${pct}%;background:${fill}"></div></div>
|
|
250
|
+
<span class="heat-count">${count}</span>
|
|
251
|
+
</div>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── HTML builder ──────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
function buildHtml(data, projectName) {
|
|
257
|
+
const { caps, agents, check, sessions, candidates, audit, links, analytics } = data;
|
|
258
|
+
|
|
259
|
+
const statusColor = check?.status === "ok" ? "#22c55e"
|
|
260
|
+
: check?.status === "warning" ? "#f59e0b"
|
|
261
|
+
: check?.status === "error" ? "#ef4444"
|
|
262
|
+
: "#6b7280";
|
|
263
|
+
|
|
264
|
+
const statusLabel = check?.status || "unknown";
|
|
265
|
+
const capCount = caps.length;
|
|
266
|
+
const agentCount = agents.length;
|
|
267
|
+
const issueCount = (check?.issues || []).length;
|
|
268
|
+
|
|
269
|
+
// Capability rows
|
|
270
|
+
const capRows = caps.map(c => {
|
|
271
|
+
const statusBadge = c.status ? `<span class="badge">${c.status}</span>` : "";
|
|
272
|
+
return `<tr>
|
|
273
|
+
<td><code>${esc(c.id)}</code></td>
|
|
274
|
+
<td>${esc(c.title || "")}${statusBadge}</td>
|
|
275
|
+
<td>${esc(c.since || "")}</td>
|
|
276
|
+
</tr>`;
|
|
277
|
+
}).join("\n");
|
|
278
|
+
|
|
279
|
+
// Agent rows
|
|
280
|
+
const agentRows = agents.map(a => {
|
|
281
|
+
const steps = (a.steps || []).map(s => typeof s === "string" ? s : s.command).join(" → ");
|
|
282
|
+
const conf = a.confidence ? `${Math.round(a.confidence * 100)}%` : "—";
|
|
283
|
+
return `<tr>
|
|
284
|
+
<td><strong>${esc(a.name)}</strong></td>
|
|
285
|
+
<td>${esc(a.description || steps)}</td>
|
|
286
|
+
<td><code>${esc(steps)}</code></td>
|
|
287
|
+
<td>${conf}</td>
|
|
288
|
+
</tr>`;
|
|
289
|
+
}).join("\n");
|
|
290
|
+
|
|
291
|
+
// Issues
|
|
292
|
+
const issueItems = (check?.issues || []).map(i =>
|
|
293
|
+
`<li class="issue">${esc(typeof i === "string" ? i : i.message || JSON.stringify(i))}</li>`
|
|
294
|
+
).join("\n");
|
|
295
|
+
|
|
296
|
+
// Session timeline
|
|
297
|
+
const sessionItems = sessions.slice().reverse().map(s => {
|
|
298
|
+
const cmds = (s.commands || []).join(", ");
|
|
299
|
+
const date = s.startedAt ? new Date(s.startedAt).toLocaleString() : "unknown";
|
|
300
|
+
return `<div class="session-item">
|
|
301
|
+
<span class="session-date">${esc(date)}</span>
|
|
302
|
+
<span class="session-cmds">${esc(cmds || "no commands recorded")}</span>
|
|
303
|
+
</div>`;
|
|
304
|
+
}).join("\n");
|
|
305
|
+
|
|
306
|
+
// Candidate suggestions
|
|
307
|
+
const candidateItems = candidates.map(c =>
|
|
308
|
+
`<li class="candidate">${esc(c.name || c.id || "unnamed")}: ${esc(c.description || "")}</li>`
|
|
309
|
+
).join("\n");
|
|
310
|
+
|
|
311
|
+
// ── Analytics ─────────────────────────────────────────────────────────────
|
|
312
|
+
const vel = analytics?.velocity || [];
|
|
313
|
+
const contribs = analytics?.contributors || [];
|
|
314
|
+
const trend = analytics?.healthTrend || [];
|
|
315
|
+
|
|
316
|
+
const velValues = vel.map(v => v.commits);
|
|
317
|
+
const velLabels = vel.map(v => v.week);
|
|
318
|
+
const velChart = barChart(velValues, velLabels, "#f97316", 90);
|
|
319
|
+
|
|
320
|
+
const trendValues = trend.map(t => t.score);
|
|
321
|
+
const trendChart = lineChart(trendValues, "#3b82f6", 80);
|
|
322
|
+
|
|
323
|
+
const maxContrib = contribs.length ? Math.max(...contribs.map(c => c.count)) : 1;
|
|
324
|
+
const heatRows = contribs.length
|
|
325
|
+
? contribs.map(c => heatRow(c.name, c.count, maxContrib)).join("\n")
|
|
326
|
+
: `<div class="empty">No git history in inferno/ yet</div>`;
|
|
327
|
+
|
|
328
|
+
// Audit summary card
|
|
329
|
+
const auditStats = audit?.stats || null;
|
|
330
|
+
const auditHigh = auditStats?.high ?? "—";
|
|
331
|
+
const auditMedium = auditStats?.medium ?? "—";
|
|
332
|
+
const linkedCount = links.length;
|
|
333
|
+
|
|
334
|
+
return `<!DOCTYPE html>
|
|
30
335
|
<html lang="en">
|
|
31
336
|
<head>
|
|
32
337
|
<meta charset="UTF-8">
|
|
33
338
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
34
|
-
<title>infernoflow
|
|
339
|
+
<title>infernoflow — ${esc(projectName)}</title>
|
|
35
340
|
<style>
|
|
36
341
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
37
342
|
:root {
|
|
@@ -94,10 +399,10 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
94
399
|
</head>
|
|
95
400
|
<body>
|
|
96
401
|
<header>
|
|
97
|
-
<span class="flame"
|
|
402
|
+
<span class="flame">🔥</span>
|
|
98
403
|
<div>
|
|
99
404
|
<h1>infernoflow</h1>
|
|
100
|
-
<div class="project">${
|
|
405
|
+
<div class="project">${esc(projectName)}</div>
|
|
101
406
|
</div>
|
|
102
407
|
<div class="live">Live</div>
|
|
103
408
|
</header>
|
|
@@ -107,150 +412,157 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
107
412
|
<div class="cards">
|
|
108
413
|
<div class="card">
|
|
109
414
|
<div class="label">Contract status</div>
|
|
110
|
-
<div class="value status-${
|
|
111
|
-
<div class="sub">${
|
|
415
|
+
<div class="value status-${statusLabel}" style="color:${statusColor}">${statusLabel.toUpperCase()}</div>
|
|
416
|
+
<div class="sub">${issueCount > 0 ? issueCount + " issue" + (issueCount !== 1 ? "s" : "") : "All checks passed"}</div>
|
|
112
417
|
</div>
|
|
113
418
|
<div class="card">
|
|
114
419
|
<div class="label">Capabilities</div>
|
|
115
|
-
<div class="value">${
|
|
420
|
+
<div class="value">${capCount}</div>
|
|
116
421
|
<div class="sub">tracked in contract</div>
|
|
117
422
|
</div>
|
|
118
423
|
<div class="card">
|
|
119
424
|
<div class="label">Agents</div>
|
|
120
|
-
<div class="value">${
|
|
425
|
+
<div class="value">${agentCount}</div>
|
|
121
426
|
<div class="sub">synthesized workflows</div>
|
|
122
427
|
</div>
|
|
123
428
|
<div class="card">
|
|
124
429
|
<div class="label">Sessions</div>
|
|
125
|
-
<div class="value">${
|
|
430
|
+
<div class="value">${sessions.length}</div>
|
|
126
431
|
<div class="sub">recent sessions logged</div>
|
|
127
432
|
</div>
|
|
128
|
-
${
|
|
433
|
+
${auditStats ? `
|
|
129
434
|
<div class="card">
|
|
130
435
|
<div class="label">Security surface</div>
|
|
131
|
-
<div class="value" style="color:${
|
|
132
|
-
<div class="sub">${
|
|
133
|
-
</div
|
|
436
|
+
<div class="value" style="color:${auditHigh > 0 ? "var(--red)" : "var(--green)"}">${auditHigh}</div>
|
|
437
|
+
<div class="sub">${auditHigh} high · ${auditMedium} medium risk caps</div>
|
|
438
|
+
</div>` : ""}
|
|
134
439
|
<div class="card">
|
|
135
440
|
<div class="label">Linked tickets</div>
|
|
136
|
-
<div class="value" style="color:var(--blue)">${
|
|
441
|
+
<div class="value" style="color:var(--blue)">${linkedCount}</div>
|
|
137
442
|
<div class="sub">caps linked to Jira/Linear/GitHub</div>
|
|
138
443
|
</div>
|
|
139
444
|
</div>
|
|
140
445
|
|
|
141
|
-
${
|
|
446
|
+
${issueCount > 0 ? `
|
|
142
447
|
<!-- Issues -->
|
|
143
448
|
<section>
|
|
144
|
-
<h2
|
|
145
|
-
<ul class="issues-list">${
|
|
146
|
-
</section
|
|
449
|
+
<h2>⚠ Issues</h2>
|
|
450
|
+
<ul class="issues-list">${issueItems}</ul>
|
|
451
|
+
</section>` : ""}
|
|
147
452
|
|
|
148
453
|
<!-- Capabilities -->
|
|
149
454
|
<section>
|
|
150
|
-
<h2>Capabilities (${
|
|
151
|
-
${
|
|
455
|
+
<h2>Capabilities (${capCount})</h2>
|
|
456
|
+
${capCount > 0 ? `
|
|
152
457
|
<table>
|
|
153
458
|
<thead><tr><th>ID</th><th>Title</th><th>Since</th></tr></thead>
|
|
154
|
-
<tbody>${
|
|
155
|
-
</table
|
|
459
|
+
<tbody>${capRows}</tbody>
|
|
460
|
+
</table>` : `<div class="empty">No capabilities found in inferno/capabilities.json</div>`}
|
|
156
461
|
</section>
|
|
157
462
|
|
|
158
463
|
<!-- Agents -->
|
|
159
464
|
<section>
|
|
160
|
-
<h2>Synthesized Agents (${
|
|
161
|
-
${
|
|
465
|
+
<h2>Synthesized Agents (${agentCount})</h2>
|
|
466
|
+
${agentCount > 0 ? `
|
|
162
467
|
<table>
|
|
163
468
|
<thead><tr><th>Name</th><th>Description</th><th>Steps</th><th>Confidence</th></tr></thead>
|
|
164
|
-
<tbody>${
|
|
165
|
-
</table
|
|
469
|
+
<tbody>${agentRows}</tbody>
|
|
470
|
+
</table>` : `<div class="empty">No agents yet — run <code>infernoflow synthesize</code> to generate them</div>`}
|
|
166
471
|
</section>
|
|
167
472
|
|
|
168
|
-
${
|
|
473
|
+
${candidates.length > 0 ? `
|
|
169
474
|
<!-- Candidates -->
|
|
170
475
|
<section>
|
|
171
|
-
<h2>Workflow Candidates (${
|
|
172
|
-
<ul class="candidates-list">${
|
|
173
|
-
</section
|
|
476
|
+
<h2>Workflow Candidates (${candidates.length})</h2>
|
|
477
|
+
<ul class="candidates-list">${candidateItems}</ul>
|
|
478
|
+
</section>` : ""}
|
|
174
479
|
|
|
175
480
|
<!-- Session timeline -->
|
|
176
481
|
<section>
|
|
177
482
|
<h2>Recent Sessions</h2>
|
|
178
|
-
${
|
|
483
|
+
${sessions.length > 0 ? `<div>${sessionItems}</div>`
|
|
484
|
+
: `<div class="empty">No session data yet — sessions are logged automatically as you use infernoflow</div>`}
|
|
179
485
|
</section>
|
|
180
486
|
|
|
181
487
|
<!-- Analytics: velocity + health trend -->
|
|
182
|
-
${
|
|
488
|
+
${vel.length > 0 ? `
|
|
183
489
|
<div class="analytics-grid">
|
|
184
490
|
<section>
|
|
185
|
-
<h2
|
|
491
|
+
<h2>📈 Capability Velocity (13 weeks)</h2>
|
|
186
492
|
<div class="chart-wrap">
|
|
187
|
-
${
|
|
493
|
+
${velChart}
|
|
188
494
|
<div class="chart-label">Commits touching inferno/ per week</div>
|
|
189
495
|
</div>
|
|
190
496
|
</section>
|
|
191
497
|
<section>
|
|
192
|
-
<h2
|
|
498
|
+
<h2>💚 Health Score Trend</h2>
|
|
193
499
|
<div class="chart-wrap">
|
|
194
|
-
${
|
|
500
|
+
${trendChart}
|
|
195
501
|
<div class="chart-label">Heuristic health score over last 13 weeks</div>
|
|
196
502
|
</div>
|
|
197
503
|
</section>
|
|
198
|
-
</div
|
|
504
|
+
</div>` : ""}
|
|
199
505
|
|
|
200
506
|
<!-- Contributor heatmap -->
|
|
201
|
-
${
|
|
507
|
+
${contribs.length > 0 ? `
|
|
202
508
|
<section>
|
|
203
|
-
<h2
|
|
204
|
-
${
|
|
205
|
-
</section
|
|
509
|
+
<h2>👥 Contributor Heatmap (90 days)</h2>
|
|
510
|
+
${heatRows}
|
|
511
|
+
</section>` : ""}
|
|
206
512
|
|
|
207
513
|
<!-- Audit surface map (if audit.json exists) -->
|
|
208
|
-
${
|
|
514
|
+
${auditStats ? `
|
|
209
515
|
<section>
|
|
210
|
-
<h2
|
|
516
|
+
<h2>🔐 Security Surface (last audit)</h2>
|
|
211
517
|
<div class="audit-tags">
|
|
212
|
-
<span class="tag tag-high"
|
|
213
|
-
<span class="tag tag-medium"
|
|
214
|
-
<span class="tag tag-low"
|
|
215
|
-
${
|
|
518
|
+
<span class="tag tag-high">🔴 ${auditStats.high} HIGH</span>
|
|
519
|
+
<span class="tag tag-medium">🟡 ${auditStats.medium} MEDIUM</span>
|
|
520
|
+
<span class="tag tag-low">🟢 ${auditStats.low} LOW</span>
|
|
521
|
+
${linkedCount > 0 ? `<span class="tag tag-link">🔗 ${linkedCount} linked to tickets</span>` : ""}
|
|
216
522
|
</div>
|
|
217
|
-
${
|
|
523
|
+
${audit.capabilities ? `
|
|
218
524
|
<table>
|
|
219
525
|
<thead><tr><th>Severity</th><th>Capability</th><th>Tags</th></tr></thead>
|
|
220
526
|
<tbody>
|
|
221
|
-
${
|
|
527
|
+
${audit.capabilities.filter(c => c.severity === "high" || c.severity === "medium").slice(0, 10).map(c => `
|
|
222
528
|
<tr>
|
|
223
|
-
<td style="color:${
|
|
224
|
-
<td><code>${
|
|
225
|
-
<td>${
|
|
529
|
+
<td style="color:${c.severity === "high" ? "var(--red)" : "var(--yellow)"}">${c.severity}</td>
|
|
530
|
+
<td><code>${esc(c.id)}</code></td>
|
|
531
|
+
<td>${esc((c.tags || []).join(", "))}</td>
|
|
226
532
|
</tr>`).join("")}
|
|
227
533
|
</tbody>
|
|
228
|
-
</table
|
|
229
|
-
<div style="padding:8px 18px;font-size:11px;color:var(--muted)">Run <code>infernoflow audit</code> to refresh
|
|
230
|
-
</section
|
|
534
|
+
</table>` : ""}
|
|
535
|
+
<div style="padding:8px 18px;font-size:11px;color:var(--muted)">Run <code>infernoflow audit</code> to refresh · Last run: ${esc(audit.runAt ? new Date(audit.runAt).toLocaleString() : "unknown")}</div>
|
|
536
|
+
</section>` : `
|
|
231
537
|
<section>
|
|
232
|
-
<h2
|
|
233
|
-
<div class="empty">No audit data yet
|
|
538
|
+
<h2>🔐 Security Surface</h2>
|
|
539
|
+
<div class="empty">No audit data yet — run <code>infernoflow audit</code> to classify capabilities by security sensitivity</div>
|
|
234
540
|
</section>`}
|
|
235
541
|
|
|
236
542
|
|
|
237
|
-
<!--
|
|
543
|
+
<!-- ── Command Center ────────────────────────────────────────────────────── -->
|
|
238
544
|
<section id="command-center">
|
|
239
|
-
<h2
|
|
545
|
+
<h2>🎛️ Command Center</h2>
|
|
240
546
|
<div class="cc-layout">
|
|
241
547
|
<!-- Left: capability list -->
|
|
242
548
|
<div class="cc-caps">
|
|
243
549
|
<h3>Capabilities</h3>
|
|
244
550
|
<div class="cc-cap-list" id="cc-cap-list">
|
|
245
|
-
${
|
|
246
|
-
|
|
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>
|
|
247
558
|
<div class="cc-cap-info">
|
|
248
|
-
<span class="cc-cap-id">${
|
|
249
|
-
${
|
|
559
|
+
<span class="cc-cap-id">${esc(c.id)}</span>
|
|
560
|
+
${files.length ? `<span class="cc-cap-file">${esc(files[0])}</span>` : ""}
|
|
250
561
|
</div>
|
|
251
|
-
<span class="cc-stab cc-stab-${
|
|
252
|
-
</div
|
|
253
|
-
|
|
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>` : ""}
|
|
254
566
|
</div>
|
|
255
567
|
</div>
|
|
256
568
|
|
|
@@ -258,23 +570,23 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
258
570
|
<div class="cc-commands">
|
|
259
571
|
<h3>Quick Commands</h3>
|
|
260
572
|
<div class="cc-btn-grid">
|
|
261
|
-
<button class="cc-btn cc-btn-blue" onclick="runCmd('scan')"
|
|
262
|
-
<button class="cc-btn cc-btn-blue" onclick="runCmd('graph')"
|
|
263
|
-
<button class="cc-btn cc-btn-blue" onclick="runCmd('stability')"
|
|
264
|
-
<button class="cc-btn cc-btn-blue" onclick="runCmd('check')"
|
|
265
|
-
<button class="cc-btn cc-btn-orange" onclick="runCmd('doctor')"
|
|
266
|
-
<button class="cc-btn cc-btn-orange" onclick="runCmd('coverage')"
|
|
267
|
-
<button class="cc-btn cc-btn-green" onclick="runCmd('status')"
|
|
268
|
-
<button class="cc-btn cc-btn-green" onclick="runCmd('health')"
|
|
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>
|
|
269
581
|
</div>
|
|
270
582
|
|
|
271
583
|
<h3 style="margin-top:18px">Capability Actions</h3>
|
|
272
584
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
|
|
273
585
|
<input id="cc-capinput" class="cc-input" placeholder="capability-id" style="flex:1;min-width:120px"/>
|
|
274
|
-
<button class="cc-btn cc-btn-blue" onclick="runCapCmd('why')"
|
|
275
|
-
<button class="cc-btn cc-btn-blue" onclick="runCapCmd('impact')"
|
|
276
|
-
<button class="cc-btn cc-btn-red" onclick="runCapCmd('freeze')"
|
|
277
|
-
<button class="cc-btn cc-btn-green" onclick="runCapCmd('thaw')"
|
|
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>
|
|
278
590
|
</div>
|
|
279
591
|
|
|
280
592
|
<!-- Terminal output -->
|
|
@@ -293,7 +605,7 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
293
605
|
</section>
|
|
294
606
|
|
|
295
607
|
</main>
|
|
296
|
-
<footer>infernoflow dashboard
|
|
608
|
+
<footer>infernoflow dashboard · auto-refreshes when inferno/ changes · <a href="/" style="color:var(--muted)">refresh now</a></footer>
|
|
297
609
|
<style>
|
|
298
610
|
/* Command Center styles */
|
|
299
611
|
.cc-layout { display:grid; grid-template-columns:220px 1fr 280px; gap:16px; margin-top:12px; min-height:420px; }
|
|
@@ -338,7 +650,7 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
338
650
|
es.onmessage = () => window.location.reload();
|
|
339
651
|
es.onerror = () => {};
|
|
340
652
|
|
|
341
|
-
//
|
|
653
|
+
// ── Command runner ──────────────────────────────────────────────────────────
|
|
342
654
|
const terminal = document.getElementById('cc-terminal');
|
|
343
655
|
|
|
344
656
|
async function runCmd(command, args = []) {
|
|
@@ -368,11 +680,11 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
368
680
|
runCmd(command, [capId]);
|
|
369
681
|
}
|
|
370
682
|
|
|
371
|
-
//
|
|
683
|
+
// ── Capability detail panel ─────────────────────────────────────────────────
|
|
372
684
|
async function capDetail(capId) {
|
|
373
685
|
document.getElementById('cc-capinput').value = capId;
|
|
374
686
|
const detail = document.getElementById('cc-detail-inner');
|
|
375
|
-
detail.innerHTML = '<div class="empty">Loading
|
|
687
|
+
detail.innerHTML = '<div class="empty">Loading…</div>';
|
|
376
688
|
|
|
377
689
|
try {
|
|
378
690
|
const [why, impact] = await Promise.all([
|
|
@@ -387,7 +699,7 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
387
699
|
|
|
388
700
|
if (w) {
|
|
389
701
|
html += '<div class="cc-detail-section">';
|
|
390
|
-
html += '<h4
|
|
702
|
+
html += '<h4>📍 ' + (w.name || w.capId) + '</h4>';
|
|
391
703
|
html += '<div class="cc-detail-row"><span>Stability</span><span class="cc-stab cc-stab-' + w.stability + '">' + w.stability + '</span></div>';
|
|
392
704
|
if (w.sourceFiles?.length) html += '<div class="cc-detail-row"><span>Files</span><span style="color:#7dd3fc">' + w.sourceFiles.join(', ') + '</span></div>';
|
|
393
705
|
if (w.services?.length) html += '<div class="cc-detail-row"><span>Uses</span><span style="color:#a78bfa">' + w.services.join(', ') + '</span></div>';
|
|
@@ -398,32 +710,32 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
398
710
|
if (im) {
|
|
399
711
|
const riskCls = 'cc-risk-' + im.risk;
|
|
400
712
|
html += '<div class="cc-detail-section">';
|
|
401
|
-
html += '<h4
|
|
713
|
+
html += '<h4>💥 Impact</h4>';
|
|
402
714
|
html += '<div class="cc-detail-row"><span>Risk</span><span class="' + riskCls + '">' + im.risk.toUpperCase() + '</span></div>';
|
|
403
715
|
html += '<div class="cc-detail-row"><span>Direct deps</span><span>' + im.summary.directCount + '</span></div>';
|
|
404
716
|
html += '<div class="cc-detail-row"><span>Transitive</span><span>' + im.summary.transitiveCount + '</span></div>';
|
|
405
717
|
if (im.direct?.length) {
|
|
406
718
|
html += '<h4 style="margin-top:10px">Direct dependents</h4>';
|
|
407
|
-
im.direct.forEach(d => { html += '<div class="cc-detail-dep"
|
|
719
|
+
im.direct.forEach(d => { html += '<div class="cc-detail-dep">→ <code>' + d + '</code></div>'; });
|
|
408
720
|
}
|
|
409
721
|
if (im.affectedScenarios?.length) {
|
|
410
722
|
html += '<h4 style="margin-top:10px">Scenarios at risk</h4>';
|
|
411
|
-
im.affectedScenarios.forEach(s => { html += '<div class="cc-detail-dep"
|
|
723
|
+
im.affectedScenarios.forEach(s => { html += '<div class="cc-detail-dep">⚠️ ' + s + '</div>'; });
|
|
412
724
|
}
|
|
413
725
|
html += '</div>';
|
|
414
726
|
}
|
|
415
727
|
|
|
416
|
-
if (!html) html = '<div class="empty">No data found for ' + capId + '
|
|
728
|
+
if (!html) html = '<div class="empty">No data found for ' + capId + ' — run infernoflow scan first.</div>';
|
|
417
729
|
detail.innerHTML = html;
|
|
418
730
|
} catch (e) {
|
|
419
731
|
detail.innerHTML = '<div class="empty">Error: ' + e.message + '</div>';
|
|
420
732
|
}
|
|
421
733
|
}
|
|
422
734
|
|
|
423
|
-
//
|
|
735
|
+
// ── Stability cycle ─────────────────────────────────────────────────────────
|
|
424
736
|
async function cycleStability(capId, current) {
|
|
425
737
|
const next = current === 'experimental' ? 'stable' : current === 'stable' ? 'frozen' : 'experimental';
|
|
426
|
-
if (!confirm('Change ' + capId + ' from ' + current + '
|
|
738
|
+
if (!confirm('Change ' + capId + ' from ' + current + ' → ' + next + '?')) return;
|
|
427
739
|
await fetch('/api/freeze', {
|
|
428
740
|
method: 'POST',
|
|
429
741
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -433,10 +745,210 @@ import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http
|
|
|
433
745
|
}
|
|
434
746
|
</script>
|
|
435
747
|
</body>
|
|
436
|
-
</html
|
|
748
|
+
</html>`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function esc(str) {
|
|
752
|
+
return String(str || "")
|
|
753
|
+
.replace(/&/g, "&")
|
|
754
|
+
.replace(/</g, "<")
|
|
755
|
+
.replace(/>/g, ">")
|
|
756
|
+
.replace(/"/g, """);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ── HTTP server ───────────────────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
function startServer(infernoDir, port) {
|
|
762
|
+
const cwd = path.dirname(infernoDir);
|
|
763
|
+
const projectName = path.basename(cwd);
|
|
764
|
+
const sseClients = new Set();
|
|
765
|
+
|
|
766
|
+
// Watch inferno/ for changes → notify SSE clients
|
|
767
|
+
let watchTimer = null;
|
|
768
|
+
try {
|
|
769
|
+
fs.watch(infernoDir, { recursive: true }, () => {
|
|
770
|
+
clearTimeout(watchTimer);
|
|
771
|
+
watchTimer = setTimeout(() => {
|
|
772
|
+
for (const res of sseClients) {
|
|
773
|
+
try { res.write("data: reload\n\n"); } catch {}
|
|
774
|
+
}
|
|
775
|
+
}, 500);
|
|
776
|
+
});
|
|
777
|
+
} catch {}
|
|
778
|
+
|
|
779
|
+
const server = http.createServer((req, res) => {
|
|
780
|
+
// SSE endpoint
|
|
781
|
+
if (req.url === "/events") {
|
|
782
|
+
res.writeHead(200, {
|
|
783
|
+
"Content-Type": "text/event-stream",
|
|
784
|
+
"Cache-Control": "no-cache",
|
|
785
|
+
"Connection": "keep-alive",
|
|
786
|
+
});
|
|
787
|
+
sseClients.add(res);
|
|
788
|
+
req.on("close", () => sseClients.delete(res));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// JSON API
|
|
793
|
+
if (req.url === "/api/data") {
|
|
794
|
+
const data = gatherData(infernoDir);
|
|
795
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
796
|
+
res.end(JSON.stringify(data, null, 2));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
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
|
+
|
|
898
|
+
// Dashboard HTML
|
|
899
|
+
try {
|
|
900
|
+
const data = gatherData(infernoDir);
|
|
901
|
+
const html = buildHtml(data, projectName);
|
|
902
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
903
|
+
res.end(html);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
906
|
+
res.end(`Error: ${err.message}`);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
server.listen(port, "127.0.0.1", () => {});
|
|
911
|
+
return server;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function openBrowser(url) {
|
|
915
|
+
const platform = os.platform();
|
|
916
|
+
try {
|
|
917
|
+
if (platform === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
|
|
918
|
+
else if (platform === "win32") execSync(`start "" "${url}"`, { stdio: "ignore", shell: true });
|
|
919
|
+
else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
920
|
+
} catch {}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ── main ──────────────────────────────────────────────────────────────────────
|
|
924
|
+
|
|
925
|
+
export async function dashboardCommand(rawArgs) {
|
|
926
|
+
const args = rawArgs.slice(1);
|
|
927
|
+
const noOpen = args.includes("--no-open");
|
|
928
|
+
const portIdx = args.indexOf("--port");
|
|
929
|
+
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 7337;
|
|
930
|
+
|
|
931
|
+
const cwd = process.cwd();
|
|
932
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
933
|
+
|
|
934
|
+
header("infernoflow dashboard");
|
|
935
|
+
|
|
936
|
+
if (!fs.existsSync(infernoDir)) {
|
|
937
|
+
warn("inferno/ not found — run: infernoflow init");
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const url = `http://localhost:${port}`;
|
|
942
|
+
|
|
943
|
+
startServer(infernoDir, port);
|
|
944
|
+
|
|
945
|
+
ok(`Dashboard running → ${cyan(url)}`);
|
|
946
|
+
info("Auto-refreshes when inferno/ files change");
|
|
947
|
+
info("Press Ctrl+C to stop");
|
|
948
|
+
console.log();
|
|
949
|
+
|
|
950
|
+
if (!noOpen) openBrowser(url);
|
|
437
951
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
Error spawning command: ${x.message}
|
|
442
|
-
`),o.end()})}catch(e){o.writeHead(400,{"Content-Type":"text/plain"}),o.end("Bad request: "+e.message)}});return}const b=s.url?.match(/^\/api\/cap\/([^/]+)\/why$/);if(b){const c=decodeURIComponent(b[1]),e=g.join(T,"../../bin/infernoflow.mjs");let i="";const p=z(process.execPath,[e,"why",c,"--json"],{cwd:l,env:{...process.env,FORCE_COLOR:"0"}});p.stdout.on("data",v=>{i+=v}),p.stderr.on("data",()=>{}),p.on("close",()=>{try{o.writeHead(200,{"Content-Type":"application/json"}),o.end(i.trim()||"[]")}catch{}});return}const y=s.url?.match(/^\/api\/cap\/([^/]+)\/impact$/);if(y){const c=decodeURIComponent(y[1]),e=g.join(T,"../../bin/infernoflow.mjs");let i="";const p=z(process.execPath,[e,"impact",c,"--json"],{cwd:l,env:{...process.env,FORCE_COLOR:"0"}});p.stdout.on("data",v=>{i+=v}),p.stderr.on("data",()=>{}),p.on("close",()=>{try{o.writeHead(200,{"Content-Type":"application/json"}),o.end(i.trim()||"{}")}catch{}});return}if(s.url==="/api/freeze"&&s.method==="POST"){let c="";s.on("data",e=>{c+=e}),s.on("end",()=>{try{const{capId:e,level:i}=JSON.parse(c),p=g.join(T,"../../bin/infernoflow.mjs"),v=i==="experimental"?"thaw":"freeze",x=i==="stable"?[e,"--stable"]:[e];z(process.execPath,[p,v,...x],{cwd:l}).on("close",()=>{o.writeHead(200,{"Content-Type":"application/json"}),o.end(JSON.stringify({ok:!0}))})}catch(e){o.writeHead(400,{"Content-Type":"application/json"}),o.end(JSON.stringify({ok:!1,error:e.message}))}});return}try{const c=E(t),e=ft(c,u);o.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),o.end(e)}catch(c){o.writeHead(500,{"Content-Type":"text/plain"}),o.end(`Error: ${c.message}`)}});return h.listen(n,"127.0.0.1",()=>{}),h}function bt(t){const n=K.platform();try{n==="darwin"?S(`open "${t}"`,{stdio:"ignore"}):n==="win32"?S(`start "" "${t}"`,{stdio:"ignore",shell:!0}):S(`xdg-open "${t}"`,{stdio:"ignore"})}catch{}}async function $t(t){const n=t.slice(1),l=n.includes("--no-open"),u=n.indexOf("--port"),r=u!==-1?parseInt(n[u+1],10):7337,d=process.cwd(),h=g.join(d,"inferno");X("infernoflow dashboard"),m.existsSync(h)||(q("inferno/ not found \u2014 run: infernoflow init"),process.exit(1));const s=`http://localhost:${r}`;gt(h,r),Z(`Dashboard running \u2192 ${tt(s)}`),P("Auto-refreshes when inferno/ files change"),P("Press Ctrl+C to stop"),console.log(),l||bt(s),await new Promise(()=>{})}export{$t as dashboardCommand};
|
|
952
|
+
// Keep alive
|
|
953
|
+
await new Promise(() => {});
|
|
954
|
+
}
|