infernoflow 0.33.0 → 0.34.0

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