infernoflow 0.37.0 → 0.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -517
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,342 +1,37 @@
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>
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 ${esc(projectName)}</title>
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">🔥</span>
97
+ <span class="flame">\u{1F525}</span>
403
98
  <div>
404
99
  <h1>infernoflow</h1>
405
- <div class="project">${esc(projectName)}</div>
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-${statusLabel}" style="color:${statusColor}">${statusLabel.toUpperCase()}</div>
416
- <div class="sub">${issueCount > 0 ? issueCount + " issue" + (issueCount !== 1 ? "s" : "") : "All checks passed"}</div>
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">${capCount}</div>
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">${agentCount}</div>
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">${sessions.length}</div>
125
+ <div class="value">${d.length}</div>
431
126
  <div class="sub">recent sessions logged</div>
432
127
  </div>
433
- ${auditStats ? `
128
+ ${k?`
434
129
  <div class="card">
435
130
  <div class="label">Security surface</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>` : ""}
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)">${linkedCount}</div>
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
- ${issueCount > 0 ? `
141
+ ${p>0?`
447
142
  <!-- Issues -->
448
143
  <section>
449
- <h2>⚠ Issues</h2>
450
- <ul class="issues-list">${issueItems}</ul>
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 (${capCount})</h2>
456
- ${capCount > 0 ? `
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>${capRows}</tbody>
460
- </table>` : `<div class="empty">No capabilities found in inferno/capabilities.json</div>`}
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 (${agentCount})</h2>
466
- ${agentCount > 0 ? `
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>${agentRows}</tbody>
470
- </table>` : `<div class="empty">No agents yet run <code>infernoflow synthesize</code> to generate them</div>`}
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
- ${candidates.length > 0 ? `
168
+ ${h.length>0?`
474
169
  <!-- Candidates -->
475
170
  <section>
476
- <h2>Workflow Candidates (${candidates.length})</h2>
477
- <ul class="candidates-list">${candidateItems}</ul>
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
- ${sessions.length > 0 ? `<div>${sessionItems}</div>`
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
- ${vel.length > 0 ? `
182
+ ${O.length>0?`
489
183
  <div class="analytics-grid">
490
184
  <section>
491
- <h2>📈 Capability Velocity (13 weeks)</h2>
185
+ <h2>\u{1F4C8} Capability Velocity (13 weeks)</h2>
492
186
  <div class="chart-wrap">
493
- ${velChart}
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>💚 Health Score Trend</h2>
192
+ <h2>\u{1F49A} Health Score Trend</h2>
499
193
  <div class="chart-wrap">
500
- ${trendChart}
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
- ${contribs.length > 0 ? `
201
+ ${$.length>0?`
508
202
  <section>
509
- <h2>👥 Contributor Heatmap (90 days)</h2>
510
- ${heatRows}
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
- ${auditStats ? `
208
+ ${k?`
515
209
  <section>
516
- <h2>🔐 Security Surface (last audit)</h2>
210
+ <h2>\u{1F510} Security Surface (last audit)</h2>
517
211
  <div class="audit-tags">
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>` : ""}
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
- ${audit.capabilities ? `
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
- ${audit.capabilities.filter(c => c.severity === "high" || c.severity === "medium").slice(0, 10).map(c => `
221
+ ${s.capabilities.filter(a=>a.severity==="high"||a.severity==="medium").slice(0,10).map(a=>`
528
222
  <tr>
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>
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 · Last run: ${esc(audit.runAt ? new Date(audit.runAt).toLocaleString() : "unknown")}</div>
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>🔐 Security Surface</h2>
539
- <div class="empty">No audit data yet run <code>infernoflow audit</code> to classify capabilities by security sensitivity</div>
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
- <!-- ── Command Center ────────────────────────────────────────────────────── -->
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>🎛️ Command Center</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
- ${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>
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">${esc(c.id)}</span>
560
- ${files.length ? `<span class="cc-cap-file">${esc(files[0])}</span>` : ""}
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-${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>` : ""}
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')">🔬 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>
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')">🔍 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>
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 · auto-refreshes when inferno/ changes · <a href="/" style="color:var(--muted)">refresh now</a></footer>
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
- // ── Command runner ──────────────────────────────────────────────────────────
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
- // ── Capability detail panel ─────────────────────────────────────────────────
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…</div>';
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>📍 ' + (w.name || w.capId) + '</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>💥 Impact</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">→ <code>' + d + '</code></div>'; });
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">⚠️ ' + s + '</div>'; });
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 + ' run infernoflow scan first.</div>';
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
- // ── Stability cycle ─────────────────────────────────────────────────────────
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 + ' ' + next + '?')) return;
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, "&amp;")
754
- .replace(/</g, "&lt;")
755
- .replace(/>/g, "&gt;")
756
- .replace(/"/g, "&quot;");
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}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
- // Keep alive
953
- await new Promise(() => {});
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};