infernoflow 0.37.1 → 0.37.4
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/CHANGELOG.md +71 -0
- package/dist/bin/infernoflow.mjs +29 -277
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/ask.mjs +4 -299
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +30 -135
- package/dist/lib/commands/cloud.mjs +10 -773
- package/dist/lib/commands/context.mjs +34 -346
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/feedback.mjs +12 -216
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +11 -378
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +45 -631
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/log.mjs +18 -248
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/recap.mjs +6 -380
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +11 -1118
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/stats.mjs +5 -402
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/switch.mjs +13 -520
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/theme.mjs +18 -195
- package/dist/lib/commands/uninstall.mjs +13 -406
- package/dist/lib/commands/upgrade.mjs +20 -153
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/telemetry.mjs +19 -269
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/theme/scanner.mjs +4 -343
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -95
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/package.json +2 -4
- package/scripts/postinstall.js +2 -2
|
@@ -1,342 +1,37 @@
|
|
|
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
|
-
|
|
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>
|
|
1
|
+
import*as m from"node:fs";import*as g from"node:path";import*as V from"node:http";import*as K from"node:os";import{execSync as S,spawn as z}from"node:child_process";import{fileURLToPath as Q}from"node:url";import{header as X,ok as Z,info as P,warn as q,cyan as tt}from"../ui/output.mjs";const T=g.dirname(Q(import.meta.url));function et(t){const n=g.join(t,"contract.json");if(!m.existsSync(n))return null;try{return JSON.parse(m.readFileSync(n,"utf8"))}catch{return null}}function nt(t){for(const n of["capabilities.json","contract.json"]){const l=g.join(t,n);if(m.existsSync(l))try{return(JSON.parse(m.readFileSync(l,"utf8")).capabilities||[]).map(d=>typeof d=="string"?{id:d,title:d}:d)}catch{}}return[]}function at(t){const n=g.join(t,"developer-profile.json");if(!m.existsSync(n))return null;try{return JSON.parse(m.readFileSync(n,"utf8"))}catch{return null}}function it(t){const n=g.join(t,"agents");return m.existsSync(n)?m.readdirSync(n).filter(l=>l.endsWith(".json")).map(l=>{try{return JSON.parse(m.readFileSync(g.join(n,l),"utf8"))}catch{return null}}).filter(Boolean):[]}function ot(t){const n=g.join(t,"HOOK.log");if(!m.existsSync(n))return null;try{return JSON.parse(m.readFileSync(n,"utf8"))}catch{return null}}function st(t){try{const n=S("npx infernoflow check --json",{cwd:g.dirname(t),encoding:"utf8",timeout:15e3,stdio:["ignore","pipe","pipe"]});return JSON.parse(n)}catch(n){try{return JSON.parse(n.stdout||"{}")}catch{return{status:"error",error:"check failed"}}}}function ct(t){const n=g.join(t,"audit.json");if(!m.existsSync(n))return null;try{return JSON.parse(m.readFileSync(n,"utf8"))}catch{return null}}function rt(t){const n=g.join(t,"links.json");if(!m.existsSync(n))return[];try{return JSON.parse(m.readFileSync(n,"utf8"))}catch{return[]}}function dt(t,n){try{let d=function(e){const i=new Date(Date.UTC(e.getFullYear(),e.getMonth(),e.getDate())),p=i.getUTCDay()||7;i.setUTCDate(i.getUTCDate()+4-p);const v=new Date(Date.UTC(i.getUTCFullYear(),0,1)),x=Math.ceil(((i-v)/864e5+1)/7);return`${i.getUTCFullYear()}-W${String(x).padStart(2,"0")}`};var l=d;const u=S('git log --since="90 days ago" --format="%aI|%ae|%s" -- inferno/',{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"],timeout:8e3}).trim();if(!u)return{velocity:[],contributors:[],healthTrend:[]};const r=u.split(`
|
|
2
|
+
`).filter(Boolean).map(e=>{const[i,p,...v]=e.split("|");return{date:new Date(i),email:p||"unknown",subject:v.join("|")}}),h=new Map;for(const e of r){const i=d(e.date);h.set(i,(h.get(i)||0)+1)}const s=[],o=new Date;for(let e=12;e>=0;e--){const i=new Date(o);i.setDate(i.getDate()-e*7);const p=d(i);s.push({week:p,commits:h.get(p)||0})}const b=new Map;for(const e of r){const i=e.email.split("@")[0];b.set(i,(b.get(i)||0)+1)}const y=[...b.entries()].map(([e,i])=>({name:e,count:i})).sort((e,i)=>i.count-e.count).slice(0,8),c=s.map(e=>({week:e.week,score:e.commits===0?40:e.commits<=2?75:e.commits<=5?90:85,label:e.commits===0?"stale":e.commits<=2?"ok":e.commits<=5?"healthy":"busy"}));return{velocity:s,contributors:y,healthTrend:c}}catch{return{velocity:[],contributors:[],healthTrend:[]}}}function lt(t){const n=g.join(t,"scan.json");if(!m.existsSync(n))return null;try{return JSON.parse(m.readFileSync(n,"utf8"))}catch{return null}}function pt(t){const n=g.join(t,"graph.json");if(!m.existsSync(n))return null;try{return JSON.parse(m.readFileSync(n,"utf8"))}catch{return null}}function E(t){const n=nt(t),l=et(t),u=at(t),r=it(t),d=ot(t),h=st(t),s=ct(t),o=rt(t),b=u?.recentSessions?.slice(-10)||[],y=[...u?.agentCandidates||[],...u?.skillCandidates||[]],c=g.dirname(t),e=dt(c,t),i=lt(t),p=pt(t);return{caps:n,contract:l,agents:r,hookLog:d,check:h,sessions:b,candidates:y,audit:s,links:o,analytics:e,scan:i,graph:p,infernoDir:t}}function ut(t,n,l="#f97316",u=80){const d=u,h=t.length;if(!h)return`<svg width="600" height="${d}"></svg>`;const s=Math.max(...t,1),o=Math.floor(600/h)-4,b=t.map((y,c)=>{const e=Math.max(2,Math.round(y/s*(d-20))),i=c*(600/h)+2,p=d-e-10;return`<rect x="${i}" y="${p}" width="${o}" height="${e}" fill="${l}" rx="2" opacity="0.85"/>
|
|
3
|
+
<title>${n[c]}: ${y}</title>`}).join(`
|
|
4
|
+
`);return`<svg viewBox="0 0 600 ${d}" width="100%" height="${d}" xmlns="http://www.w3.org/2000/svg">${b}</svg>`}function ht(t,n="#3b82f6",l=80){const r=l,d=t.length;if(d<2)return`<svg width="600" height="${r}"></svg>`;const h=Math.max(...t,1),s=Math.min(...t,0),o=h-s||1,b=t.map((y,c)=>{const e=Math.round(c/(d-1)*580)+10,i=Math.round(r-10-(y-s)/o*(r-20));return`${e},${i}`}).join(" ");return`<svg viewBox="0 0 600 ${r}" width="100%" height="${r}" xmlns="http://www.w3.org/2000/svg">
|
|
5
|
+
<polyline points="${b}" fill="none" stroke="${n}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
${t.map((y,c)=>{const[e,i]=b.split(" ")[c].split(",");return`<circle cx="${e}" cy="${i}" r="4" fill="${n}"><title>${y}</title></circle>`}).join("")}
|
|
7
|
+
</svg>`}function mt(t,n,l){const u=l>0?Math.round(n/l*100):0,r=u>70?"#f97316":u>40?"#f59e0b":u>10?"#3b82f6":"#2d3148";return`<div class="heat-row">
|
|
8
|
+
<span class="heat-name">${f(t)}</span>
|
|
9
|
+
<div class="heat-bar-wrap"><div class="heat-bar" style="width:${u}%;background:${r}"></div></div>
|
|
10
|
+
<span class="heat-count">${n}</span>
|
|
11
|
+
</div>`}function ft(t,n){const{caps:l,agents:u,check:r,sessions:d,candidates:h,audit:s,links:o,analytics:b}=t,y=r?.status==="ok"?"#22c55e":r?.status==="warning"?"#f59e0b":r?.status==="error"?"#ef4444":"#6b7280",c=r?.status||"unknown",e=l.length,i=u.length,p=(r?.issues||[]).length,v=l.map(a=>{const w=a.status?`<span class="badge">${a.status}</span>`:"";return`<tr>
|
|
12
|
+
<td><code>${f(a.id)}</code></td>
|
|
13
|
+
<td>${f(a.title||"")}${w}</td>
|
|
14
|
+
<td>${f(a.since||"")}</td>
|
|
15
|
+
</tr>`}).join(`
|
|
16
|
+
`),x=u.map(a=>{const w=(a.steps||[]).map(j=>typeof j=="string"?j:j.command).join(" \u2192 "),C=a.confidence?`${Math.round(a.confidence*100)}%`:"\u2014";return`<tr>
|
|
17
|
+
<td><strong>${f(a.name)}</strong></td>
|
|
18
|
+
<td>${f(a.description||w)}</td>
|
|
19
|
+
<td><code>${f(w)}</code></td>
|
|
20
|
+
<td>${C}</td>
|
|
21
|
+
</tr>`}).join(`
|
|
22
|
+
`),H=(r?.issues||[]).map(a=>`<li class="issue">${f(typeof a=="string"?a:a.message||JSON.stringify(a))}</li>`).join(`
|
|
23
|
+
`),L=d.slice().reverse().map(a=>{const w=(a.commands||[]).join(", "),C=a.startedAt?new Date(a.startedAt).toLocaleString():"unknown";return`<div class="session-item">
|
|
24
|
+
<span class="session-date">${f(C)}</span>
|
|
25
|
+
<span class="session-cmds">${f(w||"no commands recorded")}</span>
|
|
26
|
+
</div>`}).join(`
|
|
27
|
+
`),R=h.map(a=>`<li class="candidate">${f(a.name||a.id||"unnamed")}: ${f(a.description||"")}</li>`).join(`
|
|
28
|
+
`),O=b?.velocity||[],$=b?.contributors||[],F=b?.healthTrend||[],J=O.map(a=>a.commits),A=O.map(a=>a.week),U=ut(J,A,"#f97316",90),D=F.map(a=>a.score),W=ht(D,"#3b82f6",80),B=$.length?Math.max(...$.map(a=>a.count)):1,_=$.length?$.map(a=>mt(a.name,a.count,B)).join(`
|
|
29
|
+
`):'<div class="empty">No git history in inferno/ yet</div>',k=s?.stats||null,I=k?.high??"\u2014",G=k?.medium??"\u2014",N=o.length;return`<!DOCTYPE html>
|
|
335
30
|
<html lang="en">
|
|
336
31
|
<head>
|
|
337
32
|
<meta charset="UTF-8">
|
|
338
33
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
339
|
-
<title>infernoflow
|
|
34
|
+
<title>infernoflow \u2014 ${f(n)}</title>
|
|
340
35
|
<style>
|
|
341
36
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
342
37
|
:root {
|
|
@@ -399,10 +94,10 @@ function buildHtml(data, projectName) {
|
|
|
399
94
|
</head>
|
|
400
95
|
<body>
|
|
401
96
|
<header>
|
|
402
|
-
<span class="flame"
|
|
97
|
+
<span class="flame">\u{1F525}</span>
|
|
403
98
|
<div>
|
|
404
99
|
<h1>infernoflow</h1>
|
|
405
|
-
<div class="project">${
|
|
100
|
+
<div class="project">${f(n)}</div>
|
|
406
101
|
</div>
|
|
407
102
|
<div class="live">Live</div>
|
|
408
103
|
</header>
|
|
@@ -412,157 +107,150 @@ function buildHtml(data, projectName) {
|
|
|
412
107
|
<div class="cards">
|
|
413
108
|
<div class="card">
|
|
414
109
|
<div class="label">Contract status</div>
|
|
415
|
-
<div class="value status-${
|
|
416
|
-
<div class="sub">${
|
|
110
|
+
<div class="value status-${c}" style="color:${y}">${c.toUpperCase()}</div>
|
|
111
|
+
<div class="sub">${p>0?p+" issue"+(p!==1?"s":""):"All checks passed"}</div>
|
|
417
112
|
</div>
|
|
418
113
|
<div class="card">
|
|
419
114
|
<div class="label">Capabilities</div>
|
|
420
|
-
<div class="value">${
|
|
115
|
+
<div class="value">${e}</div>
|
|
421
116
|
<div class="sub">tracked in contract</div>
|
|
422
117
|
</div>
|
|
423
118
|
<div class="card">
|
|
424
119
|
<div class="label">Agents</div>
|
|
425
|
-
<div class="value">${
|
|
120
|
+
<div class="value">${i}</div>
|
|
426
121
|
<div class="sub">synthesized workflows</div>
|
|
427
122
|
</div>
|
|
428
123
|
<div class="card">
|
|
429
124
|
<div class="label">Sessions</div>
|
|
430
|
-
<div class="value">${
|
|
125
|
+
<div class="value">${d.length}</div>
|
|
431
126
|
<div class="sub">recent sessions logged</div>
|
|
432
127
|
</div>
|
|
433
|
-
${
|
|
128
|
+
${k?`
|
|
434
129
|
<div class="card">
|
|
435
130
|
<div class="label">Security surface</div>
|
|
436
|
-
<div class="value" style="color:${
|
|
437
|
-
<div class="sub">${
|
|
438
|
-
</div
|
|
131
|
+
<div class="value" style="color:${I>0?"var(--red)":"var(--green)"}">${I}</div>
|
|
132
|
+
<div class="sub">${I} high \xB7 ${G} medium risk caps</div>
|
|
133
|
+
</div>`:""}
|
|
439
134
|
<div class="card">
|
|
440
135
|
<div class="label">Linked tickets</div>
|
|
441
|
-
<div class="value" style="color:var(--blue)">${
|
|
136
|
+
<div class="value" style="color:var(--blue)">${N}</div>
|
|
442
137
|
<div class="sub">caps linked to Jira/Linear/GitHub</div>
|
|
443
138
|
</div>
|
|
444
139
|
</div>
|
|
445
140
|
|
|
446
|
-
${
|
|
141
|
+
${p>0?`
|
|
447
142
|
<!-- Issues -->
|
|
448
143
|
<section>
|
|
449
|
-
<h2
|
|
450
|
-
<ul class="issues-list">${
|
|
451
|
-
</section
|
|
144
|
+
<h2>\u26A0 Issues</h2>
|
|
145
|
+
<ul class="issues-list">${H}</ul>
|
|
146
|
+
</section>`:""}
|
|
452
147
|
|
|
453
148
|
<!-- Capabilities -->
|
|
454
149
|
<section>
|
|
455
|
-
<h2>Capabilities (${
|
|
456
|
-
${
|
|
150
|
+
<h2>Capabilities (${e})</h2>
|
|
151
|
+
${e>0?`
|
|
457
152
|
<table>
|
|
458
153
|
<thead><tr><th>ID</th><th>Title</th><th>Since</th></tr></thead>
|
|
459
|
-
<tbody>${
|
|
460
|
-
</table
|
|
154
|
+
<tbody>${v}</tbody>
|
|
155
|
+
</table>`:'<div class="empty">No capabilities found in inferno/capabilities.json</div>'}
|
|
461
156
|
</section>
|
|
462
157
|
|
|
463
158
|
<!-- Agents -->
|
|
464
159
|
<section>
|
|
465
|
-
<h2>Synthesized Agents (${
|
|
466
|
-
${
|
|
160
|
+
<h2>Synthesized Agents (${i})</h2>
|
|
161
|
+
${i>0?`
|
|
467
162
|
<table>
|
|
468
163
|
<thead><tr><th>Name</th><th>Description</th><th>Steps</th><th>Confidence</th></tr></thead>
|
|
469
|
-
<tbody>${
|
|
470
|
-
</table
|
|
164
|
+
<tbody>${x}</tbody>
|
|
165
|
+
</table>`:'<div class="empty">No agents yet \u2014 run <code>infernoflow synthesize</code> to generate them</div>'}
|
|
471
166
|
</section>
|
|
472
167
|
|
|
473
|
-
${
|
|
168
|
+
${h.length>0?`
|
|
474
169
|
<!-- Candidates -->
|
|
475
170
|
<section>
|
|
476
|
-
<h2>Workflow Candidates (${
|
|
477
|
-
<ul class="candidates-list">${
|
|
478
|
-
</section
|
|
171
|
+
<h2>Workflow Candidates (${h.length})</h2>
|
|
172
|
+
<ul class="candidates-list">${R}</ul>
|
|
173
|
+
</section>`:""}
|
|
479
174
|
|
|
480
175
|
<!-- Session timeline -->
|
|
481
176
|
<section>
|
|
482
177
|
<h2>Recent Sessions</h2>
|
|
483
|
-
${
|
|
484
|
-
: `<div class="empty">No session data yet — sessions are logged automatically as you use infernoflow</div>`}
|
|
178
|
+
${d.length>0?`<div>${L}</div>`:'<div class="empty">No session data yet \u2014 sessions are logged automatically as you use infernoflow</div>'}
|
|
485
179
|
</section>
|
|
486
180
|
|
|
487
181
|
<!-- Analytics: velocity + health trend -->
|
|
488
|
-
${
|
|
182
|
+
${O.length>0?`
|
|
489
183
|
<div class="analytics-grid">
|
|
490
184
|
<section>
|
|
491
|
-
<h2
|
|
185
|
+
<h2>\u{1F4C8} Capability Velocity (13 weeks)</h2>
|
|
492
186
|
<div class="chart-wrap">
|
|
493
|
-
${
|
|
187
|
+
${U}
|
|
494
188
|
<div class="chart-label">Commits touching inferno/ per week</div>
|
|
495
189
|
</div>
|
|
496
190
|
</section>
|
|
497
191
|
<section>
|
|
498
|
-
<h2
|
|
192
|
+
<h2>\u{1F49A} Health Score Trend</h2>
|
|
499
193
|
<div class="chart-wrap">
|
|
500
|
-
${
|
|
194
|
+
${W}
|
|
501
195
|
<div class="chart-label">Heuristic health score over last 13 weeks</div>
|
|
502
196
|
</div>
|
|
503
197
|
</section>
|
|
504
|
-
</div
|
|
198
|
+
</div>`:""}
|
|
505
199
|
|
|
506
200
|
<!-- Contributor heatmap -->
|
|
507
|
-
${
|
|
201
|
+
${$.length>0?`
|
|
508
202
|
<section>
|
|
509
|
-
<h2
|
|
510
|
-
${
|
|
511
|
-
</section
|
|
203
|
+
<h2>\u{1F465} Contributor Heatmap (90 days)</h2>
|
|
204
|
+
${_}
|
|
205
|
+
</section>`:""}
|
|
512
206
|
|
|
513
207
|
<!-- Audit surface map (if audit.json exists) -->
|
|
514
|
-
${
|
|
208
|
+
${k?`
|
|
515
209
|
<section>
|
|
516
|
-
<h2
|
|
210
|
+
<h2>\u{1F510} Security Surface (last audit)</h2>
|
|
517
211
|
<div class="audit-tags">
|
|
518
|
-
<span class="tag tag-high"
|
|
519
|
-
<span class="tag tag-medium"
|
|
520
|
-
<span class="tag tag-low"
|
|
521
|
-
${
|
|
212
|
+
<span class="tag tag-high">\u{1F534} ${k.high} HIGH</span>
|
|
213
|
+
<span class="tag tag-medium">\u{1F7E1} ${k.medium} MEDIUM</span>
|
|
214
|
+
<span class="tag tag-low">\u{1F7E2} ${k.low} LOW</span>
|
|
215
|
+
${N>0?`<span class="tag tag-link">\u{1F517} ${N} linked to tickets</span>`:""}
|
|
522
216
|
</div>
|
|
523
|
-
${
|
|
217
|
+
${s.capabilities?`
|
|
524
218
|
<table>
|
|
525
219
|
<thead><tr><th>Severity</th><th>Capability</th><th>Tags</th></tr></thead>
|
|
526
220
|
<tbody>
|
|
527
|
-
${
|
|
221
|
+
${s.capabilities.filter(a=>a.severity==="high"||a.severity==="medium").slice(0,10).map(a=>`
|
|
528
222
|
<tr>
|
|
529
|
-
<td style="color:${
|
|
530
|
-
<td><code>${
|
|
531
|
-
<td>${
|
|
223
|
+
<td style="color:${a.severity==="high"?"var(--red)":"var(--yellow)"}">${a.severity}</td>
|
|
224
|
+
<td><code>${f(a.id)}</code></td>
|
|
225
|
+
<td>${f((a.tags||[]).join(", "))}</td>
|
|
532
226
|
</tr>`).join("")}
|
|
533
227
|
</tbody>
|
|
534
|
-
</table
|
|
535
|
-
<div style="padding:8px 18px;font-size:11px;color:var(--muted)">Run <code>infernoflow audit</code> to refresh
|
|
536
|
-
</section
|
|
228
|
+
</table>`:""}
|
|
229
|
+
<div style="padding:8px 18px;font-size:11px;color:var(--muted)">Run <code>infernoflow audit</code> to refresh \xB7 Last run: ${f(s.runAt?new Date(s.runAt).toLocaleString():"unknown")}</div>
|
|
230
|
+
</section>`:`
|
|
537
231
|
<section>
|
|
538
|
-
<h2
|
|
539
|
-
<div class="empty">No audit data yet
|
|
232
|
+
<h2>\u{1F510} Security Surface</h2>
|
|
233
|
+
<div class="empty">No audit data yet \u2014 run <code>infernoflow audit</code> to classify capabilities by security sensitivity</div>
|
|
540
234
|
</section>`}
|
|
541
235
|
|
|
542
236
|
|
|
543
|
-
<!--
|
|
237
|
+
<!-- \u2500\u2500 Command Center \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->
|
|
544
238
|
<section id="command-center">
|
|
545
|
-
<h2
|
|
239
|
+
<h2>\u{1F39B}\uFE0F Command Center</h2>
|
|
546
240
|
<div class="cc-layout">
|
|
547
241
|
<!-- Left: capability list -->
|
|
548
242
|
<div class="cc-caps">
|
|
549
243
|
<h3>Capabilities</h3>
|
|
550
244
|
<div class="cc-cap-list" id="cc-cap-list">
|
|
551
|
-
${
|
|
552
|
-
|
|
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>
|
|
245
|
+
${t.caps.map(a=>{const w=a.stability||"experimental",C=w==="frozen"?"\u{1F9CA}":w==="stable"?"\u3030\uFE0F":"\u{1F30A}",M=t.scan?.capabilities?.find(Y=>Y.id===a.id)?.codeAnalysis?.sourceFiles||[];return`<div class="cc-cap-row" onclick="capDetail('${f(a.id)}')">
|
|
246
|
+
<span class="cc-icon">${C}</span>
|
|
558
247
|
<div class="cc-cap-info">
|
|
559
|
-
<span class="cc-cap-id">${
|
|
560
|
-
${
|
|
248
|
+
<span class="cc-cap-id">${f(a.id)}</span>
|
|
249
|
+
${M.length?`<span class="cc-cap-file">${f(M[0])}</span>`:""}
|
|
561
250
|
</div>
|
|
562
|
-
<span class="cc-stab cc-stab-${
|
|
563
|
-
</div
|
|
564
|
-
|
|
565
|
-
${data.caps.length === 0 ? `<div class="empty">No capabilities — run <code>infernoflow init</code></div>` : ""}
|
|
251
|
+
<span class="cc-stab cc-stab-${w}" onclick="event.stopPropagation();cycleStability('${f(a.id)}','${w}')" title="Click to change stability">${w}</span>
|
|
252
|
+
</div>`}).join("")}
|
|
253
|
+
${t.caps.length===0?'<div class="empty">No capabilities \u2014 run <code>infernoflow init</code></div>':""}
|
|
566
254
|
</div>
|
|
567
255
|
</div>
|
|
568
256
|
|
|
@@ -570,23 +258,23 @@ function buildHtml(data, projectName) {
|
|
|
570
258
|
<div class="cc-commands">
|
|
571
259
|
<h3>Quick Commands</h3>
|
|
572
260
|
<div class="cc-btn-grid">
|
|
573
|
-
<button class="cc-btn cc-btn-blue" onclick="runCmd('scan')"
|
|
574
|
-
<button class="cc-btn cc-btn-blue" onclick="runCmd('graph')"
|
|
575
|
-
<button class="cc-btn cc-btn-blue" onclick="runCmd('stability')"
|
|
576
|
-
<button class="cc-btn cc-btn-blue" onclick="runCmd('check')"
|
|
577
|
-
<button class="cc-btn cc-btn-orange" onclick="runCmd('doctor')"
|
|
578
|
-
<button class="cc-btn cc-btn-orange" onclick="runCmd('coverage')"
|
|
579
|
-
<button class="cc-btn cc-btn-green" onclick="runCmd('status')"
|
|
580
|
-
<button class="cc-btn cc-btn-green" onclick="runCmd('health')"
|
|
261
|
+
<button class="cc-btn cc-btn-blue" onclick="runCmd('scan')">\u{1F52C} scan</button>
|
|
262
|
+
<button class="cc-btn cc-btn-blue" onclick="runCmd('graph')">\u{1F578}\uFE0F graph</button>
|
|
263
|
+
<button class="cc-btn cc-btn-blue" onclick="runCmd('stability')">\u{1F4A7} stability</button>
|
|
264
|
+
<button class="cc-btn cc-btn-blue" onclick="runCmd('check')">\u2705 check</button>
|
|
265
|
+
<button class="cc-btn cc-btn-orange" onclick="runCmd('doctor')">\u{1FA7A} doctor</button>
|
|
266
|
+
<button class="cc-btn cc-btn-orange" onclick="runCmd('coverage')">\u{1F4CA} coverage</button>
|
|
267
|
+
<button class="cc-btn cc-btn-green" onclick="runCmd('status')">\u{1F4E1} status</button>
|
|
268
|
+
<button class="cc-btn cc-btn-green" onclick="runCmd('health')">\u2764\uFE0F health</button>
|
|
581
269
|
</div>
|
|
582
270
|
|
|
583
271
|
<h3 style="margin-top:18px">Capability Actions</h3>
|
|
584
272
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
|
|
585
273
|
<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')"
|
|
587
|
-
<button class="cc-btn cc-btn-blue" onclick="runCapCmd('impact')"
|
|
588
|
-
<button class="cc-btn cc-btn-red" onclick="runCapCmd('freeze')"
|
|
589
|
-
<button class="cc-btn cc-btn-green" onclick="runCapCmd('thaw')"
|
|
274
|
+
<button class="cc-btn cc-btn-blue" onclick="runCapCmd('why')">\u{1F50D} why</button>
|
|
275
|
+
<button class="cc-btn cc-btn-blue" onclick="runCapCmd('impact')">\u{1F4A5} impact</button>
|
|
276
|
+
<button class="cc-btn cc-btn-red" onclick="runCapCmd('freeze')">\u{1F9CA} freeze</button>
|
|
277
|
+
<button class="cc-btn cc-btn-green" onclick="runCapCmd('thaw')">\u{1F30A} thaw</button>
|
|
590
278
|
</div>
|
|
591
279
|
|
|
592
280
|
<!-- Terminal output -->
|
|
@@ -605,7 +293,7 @@ function buildHtml(data, projectName) {
|
|
|
605
293
|
</section>
|
|
606
294
|
|
|
607
295
|
</main>
|
|
608
|
-
<footer>infernoflow dashboard
|
|
296
|
+
<footer>infernoflow dashboard \xB7 auto-refreshes when inferno/ changes \xB7 <a href="/" style="color:var(--muted)">refresh now</a></footer>
|
|
609
297
|
<style>
|
|
610
298
|
/* Command Center styles */
|
|
611
299
|
.cc-layout { display:grid; grid-template-columns:220px 1fr 280px; gap:16px; margin-top:12px; min-height:420px; }
|
|
@@ -650,7 +338,7 @@ function buildHtml(data, projectName) {
|
|
|
650
338
|
es.onmessage = () => window.location.reload();
|
|
651
339
|
es.onerror = () => {};
|
|
652
340
|
|
|
653
|
-
//
|
|
341
|
+
// \u2500\u2500 Command runner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
654
342
|
const terminal = document.getElementById('cc-terminal');
|
|
655
343
|
|
|
656
344
|
async function runCmd(command, args = []) {
|
|
@@ -680,11 +368,11 @@ function buildHtml(data, projectName) {
|
|
|
680
368
|
runCmd(command, [capId]);
|
|
681
369
|
}
|
|
682
370
|
|
|
683
|
-
//
|
|
371
|
+
// \u2500\u2500 Capability detail panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
684
372
|
async function capDetail(capId) {
|
|
685
373
|
document.getElementById('cc-capinput').value = capId;
|
|
686
374
|
const detail = document.getElementById('cc-detail-inner');
|
|
687
|
-
detail.innerHTML = '<div class="empty">Loading
|
|
375
|
+
detail.innerHTML = '<div class="empty">Loading\u2026</div>';
|
|
688
376
|
|
|
689
377
|
try {
|
|
690
378
|
const [why, impact] = await Promise.all([
|
|
@@ -699,7 +387,7 @@ function buildHtml(data, projectName) {
|
|
|
699
387
|
|
|
700
388
|
if (w) {
|
|
701
389
|
html += '<div class="cc-detail-section">';
|
|
702
|
-
html += '<h4
|
|
390
|
+
html += '<h4>\u{1F4CD} ' + (w.name || w.capId) + '</h4>';
|
|
703
391
|
html += '<div class="cc-detail-row"><span>Stability</span><span class="cc-stab cc-stab-' + w.stability + '">' + w.stability + '</span></div>';
|
|
704
392
|
if (w.sourceFiles?.length) html += '<div class="cc-detail-row"><span>Files</span><span style="color:#7dd3fc">' + w.sourceFiles.join(', ') + '</span></div>';
|
|
705
393
|
if (w.services?.length) html += '<div class="cc-detail-row"><span>Uses</span><span style="color:#a78bfa">' + w.services.join(', ') + '</span></div>';
|
|
@@ -710,32 +398,32 @@ function buildHtml(data, projectName) {
|
|
|
710
398
|
if (im) {
|
|
711
399
|
const riskCls = 'cc-risk-' + im.risk;
|
|
712
400
|
html += '<div class="cc-detail-section">';
|
|
713
|
-
html += '<h4
|
|
401
|
+
html += '<h4>\u{1F4A5} Impact</h4>';
|
|
714
402
|
html += '<div class="cc-detail-row"><span>Risk</span><span class="' + riskCls + '">' + im.risk.toUpperCase() + '</span></div>';
|
|
715
403
|
html += '<div class="cc-detail-row"><span>Direct deps</span><span>' + im.summary.directCount + '</span></div>';
|
|
716
404
|
html += '<div class="cc-detail-row"><span>Transitive</span><span>' + im.summary.transitiveCount + '</span></div>';
|
|
717
405
|
if (im.direct?.length) {
|
|
718
406
|
html += '<h4 style="margin-top:10px">Direct dependents</h4>';
|
|
719
|
-
im.direct.forEach(d => { html += '<div class="cc-detail-dep"
|
|
407
|
+
im.direct.forEach(d => { html += '<div class="cc-detail-dep">\u2192 <code>' + d + '</code></div>'; });
|
|
720
408
|
}
|
|
721
409
|
if (im.affectedScenarios?.length) {
|
|
722
410
|
html += '<h4 style="margin-top:10px">Scenarios at risk</h4>';
|
|
723
|
-
im.affectedScenarios.forEach(s => { html += '<div class="cc-detail-dep"
|
|
411
|
+
im.affectedScenarios.forEach(s => { html += '<div class="cc-detail-dep">\u26A0\uFE0F ' + s + '</div>'; });
|
|
724
412
|
}
|
|
725
413
|
html += '</div>';
|
|
726
414
|
}
|
|
727
415
|
|
|
728
|
-
if (!html) html = '<div class="empty">No data found for ' + capId + '
|
|
416
|
+
if (!html) html = '<div class="empty">No data found for ' + capId + ' \u2014 run infernoflow scan first.</div>';
|
|
729
417
|
detail.innerHTML = html;
|
|
730
418
|
} catch (e) {
|
|
731
419
|
detail.innerHTML = '<div class="empty">Error: ' + e.message + '</div>';
|
|
732
420
|
}
|
|
733
421
|
}
|
|
734
422
|
|
|
735
|
-
//
|
|
423
|
+
// \u2500\u2500 Stability cycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
736
424
|
async function cycleStability(capId, current) {
|
|
737
425
|
const next = current === 'experimental' ? 'stable' : current === 'stable' ? 'frozen' : 'experimental';
|
|
738
|
-
if (!confirm('Change ' + capId + ' from ' + current + '
|
|
426
|
+
if (!confirm('Change ' + capId + ' from ' + current + ' \u2192 ' + next + '?')) return;
|
|
739
427
|
await fetch('/api/freeze', {
|
|
740
428
|
method: 'POST',
|
|
741
429
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -745,210 +433,10 @@ function buildHtml(data, projectName) {
|
|
|
745
433
|
}
|
|
746
434
|
</script>
|
|
747
435
|
</body>
|
|
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);
|
|
436
|
+
</html>`}function f(t){return String(t||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function gt(t,n){const l=g.dirname(t),u=g.basename(l),r=new Set;let d=null;try{m.watch(t,{recursive:!0},()=>{clearTimeout(d),d=setTimeout(()=>{for(const s of r)try{s.write(`data: reload
|
|
951
437
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
438
|
+
`)}catch{}},500)})}catch{}const h=V.createServer((s,o)=>{if(s.url==="/events"){o.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),r.add(o),s.on("close",()=>r.delete(o));return}if(s.url==="/api/data"){const c=E(t);o.writeHead(200,{"Content-Type":"application/json"}),o.end(JSON.stringify(c,null,2));return}if(s.url==="/api/run"&&s.method==="POST"){let c="";s.on("data",e=>{c+=e}),s.on("end",()=>{try{const{command:e="",args:i=[]}=JSON.parse(c),p=g.join(T,"../../bin/infernoflow.mjs");o.writeHead(200,{"Content-Type":"text/plain; charset=utf-8","Transfer-Encoding":"chunked","Cache-Control":"no-cache"});const v=z(process.execPath,[p,e,...i],{cwd:l,env:{...process.env,FORCE_COLOR:"0"}});v.stdout.on("data",x=>o.write(x)),v.stderr.on("data",x=>o.write(x)),v.on("close",x=>{o.write(`
|
|
439
|
+
[exit ${x}]
|
|
440
|
+
`),o.end()}),v.on("error",x=>{o.write(`
|
|
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};
|