infernoflow 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/infernoflow.mjs +23 -0
- package/dist/lib/commands/dashboard.mjs +399 -0
- package/dist/lib/commands/prComment.mjs +361 -0
- package/dist/lib/commands/publish.mjs +25 -2
- package/dist/lib/commands/setup.mjs +1 -0
- package/dist/lib/commands/teamSync.mjs +388 -0
- package/dist/lib/commands/version.mjs +282 -0
- package/dist/templates/ci/github-pr-comment.yml +50 -0
- package/dist/templates/cursor/inferno-mcp-server.mjs +13 -0
- package/package.json +5 -4
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -31,6 +31,10 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
31
31
|
"generate-skills": "Generate personalised Cursor rules + skill files from your developer profile",
|
|
32
32
|
synthesize: "Auto-detect workflow patterns and synthesize reusable skills + agents",
|
|
33
33
|
agent: "Manage and run auto-synthesized agents (list | run | show | delete)",
|
|
34
|
+
version: "Smart semver bump recommendation based on capability changes (--apply to write)",
|
|
35
|
+
"pr-comment": "Post capability drift analysis as a GitHub PR comment (works in CI automatically)",
|
|
36
|
+
dashboard: "Launch local web dashboard on localhost:7337 — live contract health, capabilities, agents",
|
|
37
|
+
"team-sync": "Sync capability contract across a team via a shared git branch (push | pull | status | init)",
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
const COMMAND_HANDLERS = {
|
|
@@ -55,6 +59,10 @@ const COMMAND_HANDLERS = {
|
|
|
55
59
|
"generate-skills": async (args) => (await import("../lib/commands/generateSkills.mjs")).generateSkillsCommand(args),
|
|
56
60
|
synthesize: async (args) => (await import("../lib/commands/synthesize.mjs")).synthesizeCommand(args),
|
|
57
61
|
agent: async (args) => (await import("../lib/commands/agent.mjs")).agentCommand(args),
|
|
62
|
+
version: async (args) => (await import("../lib/commands/version.mjs")).versionCommand(args),
|
|
63
|
+
"pr-comment": async (args) => (await import("../lib/commands/prComment.mjs")).prCommentCommand(args),
|
|
64
|
+
dashboard: async (args) => (await import("../lib/commands/dashboard.mjs")).dashboardCommand(args),
|
|
65
|
+
"team-sync": async (args) => (await import("../lib/commands/teamSync.mjs")).teamSyncCommand(args),
|
|
58
66
|
};
|
|
59
67
|
|
|
60
68
|
function formatCommandsHelp() {
|
|
@@ -160,6 +168,19 @@ ${formatCommandsHelp()}
|
|
|
160
168
|
--response <json|@file> Provide AI response directly (use with --json)
|
|
161
169
|
--apply Apply the response changes when using --json --response
|
|
162
170
|
|
|
171
|
+
${bold("version options:")}
|
|
172
|
+
--ref <tag|commit> Compare against a specific ref (default: last git tag)
|
|
173
|
+
--apply Write recommended version bump to package.json
|
|
174
|
+
--json Machine-readable output
|
|
175
|
+
|
|
176
|
+
${bold("pr-comment options:")}
|
|
177
|
+
--pr <number> PR number to comment on (auto-detected in GitHub Actions)
|
|
178
|
+
--repo <owner/repo> GitHub repository (auto-detected in GitHub Actions)
|
|
179
|
+
--token <ghp_...> GitHub token (auto-detected from GITHUB_TOKEN env var)
|
|
180
|
+
--ref <ref> Base ref to diff against (auto-detected from GITHUB_BASE_REF)
|
|
181
|
+
--dry-run Print the comment without posting it
|
|
182
|
+
--json Machine-readable output
|
|
183
|
+
|
|
163
184
|
${bold("Machine output:")}
|
|
164
185
|
${gray("status --json")}
|
|
165
186
|
${gray("check --json")}
|
|
@@ -169,6 +190,8 @@ ${formatCommandsHelp()}
|
|
|
169
190
|
${gray('run "task" --json')}
|
|
170
191
|
${gray('suggest "what changed" --json')}
|
|
171
192
|
${gray('suggest "what changed" --json --response \'{"newCapabilities":[...]}\' --apply')}
|
|
193
|
+
${gray("version --json")}
|
|
194
|
+
${gray("version --apply")}
|
|
172
195
|
`;
|
|
173
196
|
|
|
174
197
|
// ── Silent behavior observation ───────────────────────────────────────────
|
|
@@ -0,0 +1,399 @@
|
|
|
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 } 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
|
+
function gatherData(infernoDir) {
|
|
84
|
+
const caps = loadCapabilities(infernoDir);
|
|
85
|
+
const contract = loadContract(infernoDir);
|
|
86
|
+
const profile = loadProfile(infernoDir);
|
|
87
|
+
const agents = loadAgents(infernoDir);
|
|
88
|
+
const hookLog = loadHookLog(infernoDir);
|
|
89
|
+
const check = runCheck(infernoDir);
|
|
90
|
+
const sessions = profile?.recentSessions?.slice(-10) || [];
|
|
91
|
+
const candidates = [
|
|
92
|
+
...(profile?.agentCandidates || []),
|
|
93
|
+
...(profile?.skillCandidates || []),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
return { caps, contract, agents, hookLog, check, sessions, candidates, infernoDir };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── HTML builder ──────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
function buildHtml(data, projectName) {
|
|
102
|
+
const { caps, agents, check, sessions, candidates } = data;
|
|
103
|
+
|
|
104
|
+
const statusColor = check?.status === "ok" ? "#22c55e"
|
|
105
|
+
: check?.status === "warning" ? "#f59e0b"
|
|
106
|
+
: check?.status === "error" ? "#ef4444"
|
|
107
|
+
: "#6b7280";
|
|
108
|
+
|
|
109
|
+
const statusLabel = check?.status || "unknown";
|
|
110
|
+
const capCount = caps.length;
|
|
111
|
+
const agentCount = agents.length;
|
|
112
|
+
const issueCount = (check?.issues || []).length;
|
|
113
|
+
|
|
114
|
+
// Capability rows
|
|
115
|
+
const capRows = caps.map(c => {
|
|
116
|
+
const statusBadge = c.status ? `<span class="badge">${c.status}</span>` : "";
|
|
117
|
+
return `<tr>
|
|
118
|
+
<td><code>${esc(c.id)}</code></td>
|
|
119
|
+
<td>${esc(c.title || "")}${statusBadge}</td>
|
|
120
|
+
<td>${esc(c.since || "")}</td>
|
|
121
|
+
</tr>`;
|
|
122
|
+
}).join("\n");
|
|
123
|
+
|
|
124
|
+
// Agent rows
|
|
125
|
+
const agentRows = agents.map(a => {
|
|
126
|
+
const steps = (a.steps || []).map(s => typeof s === "string" ? s : s.command).join(" → ");
|
|
127
|
+
const conf = a.confidence ? `${Math.round(a.confidence * 100)}%` : "—";
|
|
128
|
+
return `<tr>
|
|
129
|
+
<td><strong>${esc(a.name)}</strong></td>
|
|
130
|
+
<td>${esc(a.description || steps)}</td>
|
|
131
|
+
<td><code>${esc(steps)}</code></td>
|
|
132
|
+
<td>${conf}</td>
|
|
133
|
+
</tr>`;
|
|
134
|
+
}).join("\n");
|
|
135
|
+
|
|
136
|
+
// Issues
|
|
137
|
+
const issueItems = (check?.issues || []).map(i =>
|
|
138
|
+
`<li class="issue">${esc(typeof i === "string" ? i : i.message || JSON.stringify(i))}</li>`
|
|
139
|
+
).join("\n");
|
|
140
|
+
|
|
141
|
+
// Session timeline
|
|
142
|
+
const sessionItems = sessions.slice().reverse().map(s => {
|
|
143
|
+
const cmds = (s.commands || []).join(", ");
|
|
144
|
+
const date = s.startedAt ? new Date(s.startedAt).toLocaleString() : "unknown";
|
|
145
|
+
return `<div class="session-item">
|
|
146
|
+
<span class="session-date">${esc(date)}</span>
|
|
147
|
+
<span class="session-cmds">${esc(cmds || "no commands recorded")}</span>
|
|
148
|
+
</div>`;
|
|
149
|
+
}).join("\n");
|
|
150
|
+
|
|
151
|
+
// Candidate suggestions
|
|
152
|
+
const candidateItems = candidates.map(c =>
|
|
153
|
+
`<li class="candidate">${esc(c.name || c.id || "unnamed")}: ${esc(c.description || "")}</li>`
|
|
154
|
+
).join("\n");
|
|
155
|
+
|
|
156
|
+
return `<!DOCTYPE html>
|
|
157
|
+
<html lang="en">
|
|
158
|
+
<head>
|
|
159
|
+
<meta charset="UTF-8">
|
|
160
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
161
|
+
<title>infernoflow — ${esc(projectName)}</title>
|
|
162
|
+
<style>
|
|
163
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
164
|
+
:root {
|
|
165
|
+
--bg: #0f1117; --surface: #1a1d27; --border: #2d3148;
|
|
166
|
+
--text: #e2e8f0; --muted: #64748b; --accent: #f97316;
|
|
167
|
+
--green: #22c55e; --yellow: #f59e0b; --red: #ef4444; --blue: #3b82f6;
|
|
168
|
+
}
|
|
169
|
+
body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; font-size: 14px; line-height: 1.5; }
|
|
170
|
+
header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; gap: 12px; }
|
|
171
|
+
header h1 { font-size: 18px; font-weight: 700; }
|
|
172
|
+
header .flame { font-size: 22px; }
|
|
173
|
+
header .project { color: var(--muted); font-size: 13px; }
|
|
174
|
+
header .live { margin-left: auto; font-size: 11px; color: var(--green); display: flex; align-items: center; gap: 4px; }
|
|
175
|
+
header .live::before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
|
|
176
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
177
|
+
main { max-width: 1100px; margin: 0 auto; padding: 24px; display: grid; gap: 20px; }
|
|
178
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; }
|
|
179
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 18px; }
|
|
180
|
+
.card .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: 8px; }
|
|
181
|
+
.card .value { font-size: 28px; font-weight: 700; }
|
|
182
|
+
.card .sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
183
|
+
.status-ok { color: var(--green); }
|
|
184
|
+
.status-warning { color: var(--yellow); }
|
|
185
|
+
.status-error { color: var(--red); }
|
|
186
|
+
section { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
|
|
187
|
+
section h2 { font-size: 13px; font-weight: 600; padding: 14px 18px; border-bottom: 1px solid var(--border); color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
188
|
+
table { width: 100%; border-collapse: collapse; }
|
|
189
|
+
th, td { padding: 10px 18px; text-align: left; border-bottom: 1px solid var(--border); }
|
|
190
|
+
th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); background: rgba(255,255,255,0.02); }
|
|
191
|
+
tr:last-child td { border-bottom: none; }
|
|
192
|
+
tr:hover td { background: rgba(255,255,255,0.03); }
|
|
193
|
+
code { font-family: monospace; font-size: 12px; background: rgba(255,255,255,0.07); padding: 1px 5px; border-radius: 4px; }
|
|
194
|
+
.badge { font-size: 10px; background: rgba(249,115,22,0.15); color: var(--accent); padding: 1px 6px; border-radius: 9px; margin-left: 6px; }
|
|
195
|
+
.issues-list, .candidates-list { list-style: none; padding: 14px 18px; display: flex; flex-direction: column; gap: 8px; }
|
|
196
|
+
.issue { color: var(--red); font-size: 13px; }
|
|
197
|
+
.candidate { color: var(--yellow); font-size: 13px; }
|
|
198
|
+
.empty { padding: 24px 18px; color: var(--muted); text-align: center; font-size: 13px; }
|
|
199
|
+
.session-item { display: flex; gap: 16px; align-items: baseline; padding: 9px 18px; border-bottom: 1px solid var(--border); }
|
|
200
|
+
.session-item:last-child { border-bottom: none; }
|
|
201
|
+
.session-date { font-size: 11px; color: var(--muted); white-space: nowrap; min-width: 140px; }
|
|
202
|
+
.session-cmds { font-size: 12px; color: var(--text); }
|
|
203
|
+
footer { text-align: center; color: var(--muted); font-size: 11px; padding: 24px; }
|
|
204
|
+
</style>
|
|
205
|
+
</head>
|
|
206
|
+
<body>
|
|
207
|
+
<header>
|
|
208
|
+
<span class="flame">🔥</span>
|
|
209
|
+
<div>
|
|
210
|
+
<h1>infernoflow</h1>
|
|
211
|
+
<div class="project">${esc(projectName)}</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="live">Live</div>
|
|
214
|
+
</header>
|
|
215
|
+
<main>
|
|
216
|
+
|
|
217
|
+
<!-- Stat cards -->
|
|
218
|
+
<div class="cards">
|
|
219
|
+
<div class="card">
|
|
220
|
+
<div class="label">Contract status</div>
|
|
221
|
+
<div class="value status-${statusLabel}" style="color:${statusColor}">${statusLabel.toUpperCase()}</div>
|
|
222
|
+
<div class="sub">${issueCount > 0 ? issueCount + " issue" + (issueCount !== 1 ? "s" : "") : "All checks passed"}</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="card">
|
|
225
|
+
<div class="label">Capabilities</div>
|
|
226
|
+
<div class="value">${capCount}</div>
|
|
227
|
+
<div class="sub">tracked in contract</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="card">
|
|
230
|
+
<div class="label">Agents</div>
|
|
231
|
+
<div class="value">${agentCount}</div>
|
|
232
|
+
<div class="sub">synthesized workflows</div>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="card">
|
|
235
|
+
<div class="label">Sessions</div>
|
|
236
|
+
<div class="value">${sessions.length}</div>
|
|
237
|
+
<div class="sub">recent sessions logged</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
${issueCount > 0 ? `
|
|
242
|
+
<!-- Issues -->
|
|
243
|
+
<section>
|
|
244
|
+
<h2>⚠ Issues</h2>
|
|
245
|
+
<ul class="issues-list">${issueItems}</ul>
|
|
246
|
+
</section>` : ""}
|
|
247
|
+
|
|
248
|
+
<!-- Capabilities -->
|
|
249
|
+
<section>
|
|
250
|
+
<h2>Capabilities (${capCount})</h2>
|
|
251
|
+
${capCount > 0 ? `
|
|
252
|
+
<table>
|
|
253
|
+
<thead><tr><th>ID</th><th>Title</th><th>Since</th></tr></thead>
|
|
254
|
+
<tbody>${capRows}</tbody>
|
|
255
|
+
</table>` : `<div class="empty">No capabilities found in inferno/capabilities.json</div>`}
|
|
256
|
+
</section>
|
|
257
|
+
|
|
258
|
+
<!-- Agents -->
|
|
259
|
+
<section>
|
|
260
|
+
<h2>Synthesized Agents (${agentCount})</h2>
|
|
261
|
+
${agentCount > 0 ? `
|
|
262
|
+
<table>
|
|
263
|
+
<thead><tr><th>Name</th><th>Description</th><th>Steps</th><th>Confidence</th></tr></thead>
|
|
264
|
+
<tbody>${agentRows}</tbody>
|
|
265
|
+
</table>` : `<div class="empty">No agents yet — run <code>infernoflow synthesize</code> to generate them</div>`}
|
|
266
|
+
</section>
|
|
267
|
+
|
|
268
|
+
${candidates.length > 0 ? `
|
|
269
|
+
<!-- Candidates -->
|
|
270
|
+
<section>
|
|
271
|
+
<h2>Workflow Candidates (${candidates.length})</h2>
|
|
272
|
+
<ul class="candidates-list">${candidateItems}</ul>
|
|
273
|
+
</section>` : ""}
|
|
274
|
+
|
|
275
|
+
<!-- Session timeline -->
|
|
276
|
+
<section>
|
|
277
|
+
<h2>Recent Sessions</h2>
|
|
278
|
+
${sessions.length > 0 ? `<div>${sessionItems}</div>`
|
|
279
|
+
: `<div class="empty">No session data yet — sessions are logged automatically as you use infernoflow</div>`}
|
|
280
|
+
</section>
|
|
281
|
+
|
|
282
|
+
</main>
|
|
283
|
+
<footer>infernoflow dashboard · auto-refreshes every 10s · <a href="/" style="color:var(--muted)">refresh now</a></footer>
|
|
284
|
+
<script>
|
|
285
|
+
// SSE live reload
|
|
286
|
+
const es = new EventSource('/events');
|
|
287
|
+
es.onmessage = () => window.location.reload();
|
|
288
|
+
es.onerror = () => {};
|
|
289
|
+
</script>
|
|
290
|
+
</body>
|
|
291
|
+
</html>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function esc(str) {
|
|
295
|
+
return String(str || "")
|
|
296
|
+
.replace(/&/g, "&")
|
|
297
|
+
.replace(/</g, "<")
|
|
298
|
+
.replace(/>/g, ">")
|
|
299
|
+
.replace(/"/g, """);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── HTTP server ───────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
function startServer(infernoDir, port) {
|
|
305
|
+
const cwd = path.dirname(infernoDir);
|
|
306
|
+
const projectName = path.basename(cwd);
|
|
307
|
+
const sseClients = new Set();
|
|
308
|
+
|
|
309
|
+
// Watch inferno/ for changes → notify SSE clients
|
|
310
|
+
let watchTimer = null;
|
|
311
|
+
try {
|
|
312
|
+
fs.watch(infernoDir, { recursive: true }, () => {
|
|
313
|
+
clearTimeout(watchTimer);
|
|
314
|
+
watchTimer = setTimeout(() => {
|
|
315
|
+
for (const res of sseClients) {
|
|
316
|
+
try { res.write("data: reload\n\n"); } catch {}
|
|
317
|
+
}
|
|
318
|
+
}, 500);
|
|
319
|
+
});
|
|
320
|
+
} catch {}
|
|
321
|
+
|
|
322
|
+
const server = http.createServer((req, res) => {
|
|
323
|
+
// SSE endpoint
|
|
324
|
+
if (req.url === "/events") {
|
|
325
|
+
res.writeHead(200, {
|
|
326
|
+
"Content-Type": "text/event-stream",
|
|
327
|
+
"Cache-Control": "no-cache",
|
|
328
|
+
"Connection": "keep-alive",
|
|
329
|
+
});
|
|
330
|
+
sseClients.add(res);
|
|
331
|
+
req.on("close", () => sseClients.delete(res));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// JSON API
|
|
336
|
+
if (req.url === "/api/data") {
|
|
337
|
+
const data = gatherData(infernoDir);
|
|
338
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
339
|
+
res.end(JSON.stringify(data, null, 2));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Dashboard HTML
|
|
344
|
+
try {
|
|
345
|
+
const data = gatherData(infernoDir);
|
|
346
|
+
const html = buildHtml(data, projectName);
|
|
347
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
348
|
+
res.end(html);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
351
|
+
res.end(`Error: ${err.message}`);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
server.listen(port, "127.0.0.1", () => {});
|
|
356
|
+
return server;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function openBrowser(url) {
|
|
360
|
+
const platform = os.platform();
|
|
361
|
+
try {
|
|
362
|
+
if (platform === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
|
|
363
|
+
else if (platform === "win32") execSync(`start "" "${url}"`, { stdio: "ignore", shell: true });
|
|
364
|
+
else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
365
|
+
} catch {}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── main ──────────────────────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
export async function dashboardCommand(rawArgs) {
|
|
371
|
+
const args = rawArgs.slice(1);
|
|
372
|
+
const noOpen = args.includes("--no-open");
|
|
373
|
+
const portIdx = args.indexOf("--port");
|
|
374
|
+
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 7337;
|
|
375
|
+
|
|
376
|
+
const cwd = process.cwd();
|
|
377
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
378
|
+
|
|
379
|
+
header("infernoflow dashboard");
|
|
380
|
+
|
|
381
|
+
if (!fs.existsSync(infernoDir)) {
|
|
382
|
+
warn("inferno/ not found — run: infernoflow init");
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const url = `http://localhost:${port}`;
|
|
387
|
+
|
|
388
|
+
startServer(infernoDir, port);
|
|
389
|
+
|
|
390
|
+
ok(`Dashboard running → ${cyan(url)}`);
|
|
391
|
+
info("Auto-refreshes when inferno/ files change");
|
|
392
|
+
info("Press Ctrl+C to stop");
|
|
393
|
+
console.log();
|
|
394
|
+
|
|
395
|
+
if (!noOpen) openBrowser(url);
|
|
396
|
+
|
|
397
|
+
// Keep alive
|
|
398
|
+
await new Promise(() => {});
|
|
399
|
+
}
|