infernoflow 0.37.1 → 0.37.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/dist/bin/infernoflow.mjs +29 -277
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/ask.mjs +4 -299
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +30 -135
- package/dist/lib/commands/cloud.mjs +10 -773
- package/dist/lib/commands/context.mjs +34 -346
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/feedback.mjs +12 -216
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +11 -378
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +45 -631
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/log.mjs +18 -248
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/recap.mjs +6 -380
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +11 -1118
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/stats.mjs +5 -402
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/switch.mjs +13 -520
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/theme.mjs +18 -195
- package/dist/lib/commands/uninstall.mjs +13 -406
- package/dist/lib/commands/upgrade.mjs +20 -153
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/telemetry.mjs +19 -269
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/theme/scanner.mjs +4 -343
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -95
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/package.json +2 -4
- package/scripts/postinstall.js +2 -2
|
@@ -1,402 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
* infernoflow stats
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Shows:
|
|
7
|
-
* • Session memory — total entries + breakdown by type
|
|
8
|
-
* • Context size — estimated tokens injected per session start
|
|
9
|
-
* • Coverage — % of capabilities that have code analysis
|
|
10
|
-
* • HTTP chains — resolved end-to-end call chains
|
|
11
|
-
* • Design system — what's captured in theme.json
|
|
12
|
-
* • Savings estimate— tokens saved by not re-discovering recorded entries
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* infernoflow stats Interactive dashboard
|
|
16
|
-
* infernoflow stats --json Machine-readable output
|
|
17
|
-
* infernoflow stats --brief One-line summary (for CI / scripts)
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import * as fs from "node:fs";
|
|
21
|
-
import * as path from "node:path";
|
|
22
|
-
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
23
|
-
|
|
24
|
-
const INFERNO_DIR = "inferno";
|
|
25
|
-
const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
|
|
26
|
-
const CONTEXT_FILE = path.join(INFERNO_DIR, "CONTEXT.md");
|
|
27
|
-
const THEME_FILE = path.join(INFERNO_DIR, "theme.json");
|
|
28
|
-
const SCAN_FILE = path.join(INFERNO_DIR, "scan.json");
|
|
29
|
-
const CONTRACT_FILE = path.join(INFERNO_DIR, "contract.json");
|
|
30
|
-
const CAPS_FILE = path.join(INFERNO_DIR, "capabilities.json");
|
|
31
|
-
|
|
32
|
-
function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Rough token estimator: ~1 token per 4 characters (GPT-style).
|
|
36
|
-
* Conservative — actual savings are often higher with tool/system prompt overhead.
|
|
37
|
-
*/
|
|
38
|
-
function estimateTokens(text) {
|
|
39
|
-
return Math.ceil((text || "").length / 4);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Per-entry-type token savings heuristic.
|
|
44
|
-
* Each captured entry avoids N tokens of back-and-forth rediscovery.
|
|
45
|
-
*
|
|
46
|
-
* Gotcha: ~400 tokens (agent tries wrong thing, gets error, explains, retries)
|
|
47
|
-
* Decision: ~200 tokens (agent asks why, developer explains)
|
|
48
|
-
* Attempt: ~250 tokens (agent attempts failed approach, error, pivot)
|
|
49
|
-
* Preference:~150 tokens (agent does it wrong, developer corrects)
|
|
50
|
-
* Note: ~100 tokens (minor context the agent would have to infer)
|
|
51
|
-
* Theme: ~300 tokens (agent uses wrong colors/fonts, developer corrects)
|
|
52
|
-
*/
|
|
53
|
-
const SAVINGS_PER_TYPE = {
|
|
54
|
-
gotcha: 400,
|
|
55
|
-
decision: 200,
|
|
56
|
-
attempt: 250,
|
|
57
|
-
preference: 150,
|
|
58
|
-
note: 100,
|
|
59
|
-
theme: 300,
|
|
60
|
-
handoff: 500, // full handoff = avoided a "catch me up" conversation
|
|
61
|
-
error: 200,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
function collectStats(cwd) {
|
|
65
|
-
const stats = {
|
|
66
|
-
ok: false,
|
|
67
|
-
memory: {
|
|
68
|
-
total: 0,
|
|
69
|
-
byType: {},
|
|
70
|
-
oldestEntry: null,
|
|
71
|
-
newestEntry: null,
|
|
72
|
-
sessionsTracked: 0,
|
|
73
|
-
},
|
|
74
|
-
context: {
|
|
75
|
-
sizeBytes: 0,
|
|
76
|
-
estimatedTokens: 0,
|
|
77
|
-
hasIntent: false,
|
|
78
|
-
hasWorking: false,
|
|
79
|
-
},
|
|
80
|
-
theme: {
|
|
81
|
-
captured: false,
|
|
82
|
-
fonts: 0,
|
|
83
|
-
colors: 0,
|
|
84
|
-
cssVars: 0,
|
|
85
|
-
framework: null,
|
|
86
|
-
},
|
|
87
|
-
coverage: {
|
|
88
|
-
total: 0,
|
|
89
|
-
withAnalysis: 0,
|
|
90
|
-
pct: 0,
|
|
91
|
-
},
|
|
92
|
-
chains: {
|
|
93
|
-
total: 0,
|
|
94
|
-
resolved: 0,
|
|
95
|
-
},
|
|
96
|
-
contract: {
|
|
97
|
-
policyId: null,
|
|
98
|
-
capabilities: 0,
|
|
99
|
-
isLite: false,
|
|
100
|
-
},
|
|
101
|
-
savings: {
|
|
102
|
-
estimatedTokens: 0,
|
|
103
|
-
breakdown: {},
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// ── contract ──────────────────────────────────────────────────────────────
|
|
108
|
-
const contract = readJSON(path.join(cwd, CONTRACT_FILE));
|
|
109
|
-
if (contract) {
|
|
110
|
-
stats.contract.policyId = contract.policyId;
|
|
111
|
-
stats.contract.capabilities = (contract.capabilities || []).length;
|
|
112
|
-
stats.contract.isLite = !!contract.lite;
|
|
113
|
-
stats.ok = true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── capabilities coverage ─────────────────────────────────────────────────
|
|
117
|
-
const caps = readJSON(path.join(cwd, CAPS_FILE));
|
|
118
|
-
if (caps) {
|
|
119
|
-
const list = Array.isArray(caps) ? caps : (caps.capabilities || []);
|
|
120
|
-
stats.coverage.total = list.length;
|
|
121
|
-
stats.coverage.withAnalysis = list.filter(c => c.codeAnalysis).length;
|
|
122
|
-
stats.coverage.pct = stats.coverage.total
|
|
123
|
-
? Math.round((stats.coverage.withAnalysis / stats.coverage.total) * 100)
|
|
124
|
-
: 0;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ── session memory ────────────────────────────────────────────────────────
|
|
128
|
-
const sessPath = path.join(cwd, SESSIONS_FILE);
|
|
129
|
-
if (fs.existsSync(sessPath)) {
|
|
130
|
-
const lines = fs.readFileSync(sessPath, "utf8")
|
|
131
|
-
.split("\n").filter(Boolean);
|
|
132
|
-
const entries = lines
|
|
133
|
-
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
134
|
-
.filter(Boolean);
|
|
135
|
-
|
|
136
|
-
stats.memory.total = entries.length;
|
|
137
|
-
|
|
138
|
-
for (const e of entries) {
|
|
139
|
-
const t = e.type || "note";
|
|
140
|
-
stats.memory.byType[t] = (stats.memory.byType[t] || 0) + 1;
|
|
141
|
-
|
|
142
|
-
// Savings
|
|
143
|
-
const saved = SAVINGS_PER_TYPE[t] || 100;
|
|
144
|
-
stats.savings.estimatedTokens += saved;
|
|
145
|
-
stats.savings.breakdown[t] = (stats.savings.breakdown[t] || 0) + saved;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (entries.length) {
|
|
149
|
-
stats.memory.oldestEntry = entries[0].ts;
|
|
150
|
-
stats.memory.newestEntry = entries[entries.length - 1].ts;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Count unique sessions (group by day)
|
|
154
|
-
const days = new Set(entries.map(e => (e.ts || "").slice(0, 10)));
|
|
155
|
-
stats.memory.sessionsTracked = days.size;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ── context size ──────────────────────────────────────────────────────────
|
|
159
|
-
const ctxPath = path.join(cwd, CONTEXT_FILE);
|
|
160
|
-
if (fs.existsSync(ctxPath)) {
|
|
161
|
-
const ctxText = fs.readFileSync(ctxPath, "utf8");
|
|
162
|
-
stats.context.sizeBytes = Buffer.byteLength(ctxText, "utf8");
|
|
163
|
-
stats.context.estimatedTokens = estimateTokens(ctxText);
|
|
164
|
-
stats.context.hasIntent = ctxText.includes("## Intent");
|
|
165
|
-
stats.context.hasWorking = ctxText.includes("## Working on");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── theme / design system ─────────────────────────────────────────────────
|
|
169
|
-
const theme = readJSON(path.join(cwd, THEME_FILE));
|
|
170
|
-
if (theme) {
|
|
171
|
-
stats.theme.captured = true;
|
|
172
|
-
stats.theme.fonts = Object.keys(theme.fonts || {}).filter(k => theme.fonts[k]).length;
|
|
173
|
-
stats.theme.colors = Object.keys(theme.colors?.palette || {}).length;
|
|
174
|
-
stats.theme.cssVars = Object.keys(theme.cssVars || {}).length;
|
|
175
|
-
stats.theme.framework = theme.framework || null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ── HTTP chains (from scan.json) ──────────────────────────────────────────
|
|
179
|
-
const scan = readJSON(path.join(cwd, SCAN_FILE));
|
|
180
|
-
if (scan?.httpChains) {
|
|
181
|
-
const allSteps = Object.values(scan.httpChains).flat();
|
|
182
|
-
stats.chains.total = allSteps.length;
|
|
183
|
-
stats.chains.resolved = allSteps.filter(s => s.resolved).length;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return stats;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ── bar chart helper ──────────────────────────────────────────────────────────
|
|
190
|
-
|
|
191
|
-
function bar(value, max, width = 20) {
|
|
192
|
-
const filled = max > 0 ? Math.round((value / max) * width) : 0;
|
|
193
|
-
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function pctColor(pct) {
|
|
197
|
-
if (pct >= 80) return green;
|
|
198
|
-
if (pct >= 40) return yellow;
|
|
199
|
-
return red;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ── formatter ─────────────────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
function fmtRelDate(iso) {
|
|
205
|
-
if (!iso) return "never";
|
|
206
|
-
const d = new Date(iso);
|
|
207
|
-
const now = Date.now();
|
|
208
|
-
const diff = now - d.getTime();
|
|
209
|
-
const days = Math.floor(diff / 86400000);
|
|
210
|
-
if (days === 0) return "today";
|
|
211
|
-
if (days === 1) return "yesterday";
|
|
212
|
-
if (days < 7) return `${days}d ago`;
|
|
213
|
-
if (days < 30) return `${Math.floor(days / 7)}w ago`;
|
|
214
|
-
return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short" });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function fmtTokens(n) {
|
|
218
|
-
if (n >= 1000) return `~${Math.round(n / 100) / 10}k`;
|
|
219
|
-
return `~${n}`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ── print dashboard ───────────────────────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
function printDashboard(stats) {
|
|
225
|
-
const SEP = gray(" " + "─".repeat(52));
|
|
226
|
-
|
|
227
|
-
console.log();
|
|
228
|
-
console.log(" " + bold("🔥 infernoflow stats"));
|
|
229
|
-
if (stats.contract.policyId) {
|
|
230
|
-
console.log(gray(` Project: ${stats.contract.policyId}${stats.contract.isLite ? " (lite)" : ""}`));
|
|
231
|
-
}
|
|
232
|
-
console.log(SEP);
|
|
233
|
-
|
|
234
|
-
// ── Session memory ──────────────────────────────────────────────────────
|
|
235
|
-
console.log();
|
|
236
|
-
console.log(" " + bold("Session memory") + gray(" (inferno/sessions.jsonl)"));
|
|
237
|
-
console.log();
|
|
238
|
-
|
|
239
|
-
const total = stats.memory.total;
|
|
240
|
-
if (total === 0) {
|
|
241
|
-
console.log(gray(" No entries yet — run: infernoflow log \"<what happened>\" --type gotcha"));
|
|
242
|
-
} else {
|
|
243
|
-
const typeOrder = ["gotcha", "decision", "attempt", "preference", "theme", "note", "handoff", "error"];
|
|
244
|
-
const maxCount = Math.max(...Object.values(stats.memory.byType));
|
|
245
|
-
|
|
246
|
-
for (const type of typeOrder) {
|
|
247
|
-
const count = stats.memory.byType[type] || 0;
|
|
248
|
-
if (count === 0) continue;
|
|
249
|
-
const b = bar(count, maxCount, 16);
|
|
250
|
-
const label = type.padEnd(12);
|
|
251
|
-
console.log(` ${gray(label)} ${cyan(b)} ${count}`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
console.log();
|
|
255
|
-
console.log(gray(` Total entries: `) + bold(total));
|
|
256
|
-
console.log(gray(` Sessions tracked: `) + stats.memory.sessionsTracked);
|
|
257
|
-
if (stats.memory.newestEntry) {
|
|
258
|
-
console.log(gray(` Last entry: `) + fmtRelDate(stats.memory.newestEntry));
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ── Context injection ───────────────────────────────────────────────────
|
|
263
|
-
console.log();
|
|
264
|
-
console.log(SEP);
|
|
265
|
-
console.log();
|
|
266
|
-
console.log(" " + bold("Context injection") + gray(" (per session start)"));
|
|
267
|
-
console.log();
|
|
268
|
-
|
|
269
|
-
if (stats.context.sizeBytes === 0) {
|
|
270
|
-
console.log(gray(" No CONTEXT.md yet — run: infernoflow context"));
|
|
271
|
-
} else {
|
|
272
|
-
console.log(gray(` Size: `) + `${Math.round(stats.context.sizeBytes / 1024 * 10) / 10} KB`);
|
|
273
|
-
console.log(gray(` Tokens: `) + bold(fmtTokens(stats.context.estimatedTokens)) + gray(" injected into every session"));
|
|
274
|
-
if (stats.context.hasIntent) console.log(gray(` `) + green("✔") + gray(" Intent captured"));
|
|
275
|
-
if (stats.context.hasWorking) console.log(gray(` `) + green("✔") + gray(" Working state captured"));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── Capability coverage ─────────────────────────────────────────────────
|
|
279
|
-
console.log();
|
|
280
|
-
console.log(SEP);
|
|
281
|
-
console.log();
|
|
282
|
-
console.log(" " + bold("Capability coverage") + gray(" (code analysis via infernoflow scan)"));
|
|
283
|
-
console.log();
|
|
284
|
-
|
|
285
|
-
if (stats.coverage.total === 0) {
|
|
286
|
-
console.log(gray(" No capabilities yet — run: infernoflow init"));
|
|
287
|
-
} else {
|
|
288
|
-
const colorFn = pctColor(stats.coverage.pct);
|
|
289
|
-
const b = bar(stats.coverage.withAnalysis, stats.coverage.total, 24);
|
|
290
|
-
console.log(` ${colorFn(b)} ${bold(stats.coverage.pct + "%")} (${stats.coverage.withAnalysis}/${stats.coverage.total})`);
|
|
291
|
-
|
|
292
|
-
if (stats.coverage.pct < 100) {
|
|
293
|
-
const uncovered = stats.coverage.total - stats.coverage.withAnalysis;
|
|
294
|
-
console.log(gray(`\n ${uncovered} capabilities without code analysis`));
|
|
295
|
-
console.log(gray(` Run: infernoflow scan to enrich them`));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ── HTTP chains ─────────────────────────────────────────────────────────
|
|
300
|
-
if (stats.chains.total > 0) {
|
|
301
|
-
console.log();
|
|
302
|
-
console.log(SEP);
|
|
303
|
-
console.log();
|
|
304
|
-
console.log(" " + bold("HTTP call chains") + gray(" (end-to-end resolution)"));
|
|
305
|
-
console.log();
|
|
306
|
-
const resPct = Math.round((stats.chains.resolved / stats.chains.total) * 100);
|
|
307
|
-
const colorFn = pctColor(resPct);
|
|
308
|
-
const b = bar(stats.chains.resolved, stats.chains.total, 20);
|
|
309
|
-
console.log(` ${colorFn(b)} ${bold(resPct + "%")} resolved (${stats.chains.resolved}/${stats.chains.total} call chains)`);
|
|
310
|
-
if (stats.chains.resolved < stats.chains.total) {
|
|
311
|
-
console.log(gray(`\n Unresolved calls may be to external services or missing route files`));
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ── Design system ───────────────────────────────────────────────────────
|
|
316
|
-
console.log();
|
|
317
|
-
console.log(SEP);
|
|
318
|
-
console.log();
|
|
319
|
-
console.log(" " + bold("Design system") + gray(" (inferno/theme.json)"));
|
|
320
|
-
console.log();
|
|
321
|
-
|
|
322
|
-
if (!stats.theme.captured) {
|
|
323
|
-
console.log(gray(" Not captured yet — run: infernoflow theme"));
|
|
324
|
-
} else {
|
|
325
|
-
const checks = [];
|
|
326
|
-
if (stats.theme.fonts) checks.push(`${stats.theme.fonts} font${stats.theme.fonts !== 1 ? "s" : ""}`);
|
|
327
|
-
if (stats.theme.colors) checks.push(`${stats.theme.colors} colors`);
|
|
328
|
-
if (stats.theme.cssVars) checks.push(`${stats.theme.cssVars} CSS vars`);
|
|
329
|
-
if (stats.theme.framework) checks.push(`${stats.theme.framework}`);
|
|
330
|
-
console.log(gray(" ") + green("✔") + " " + checks.join(" · "));
|
|
331
|
-
console.log(gray(" AI agents always use the correct fonts and colors for this project"));
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── Estimated savings ───────────────────────────────────────────────────
|
|
335
|
-
console.log();
|
|
336
|
-
console.log(SEP);
|
|
337
|
-
console.log();
|
|
338
|
-
console.log(" " + bold("Estimated token savings") + gray(" (vs re-discovering from scratch)"));
|
|
339
|
-
console.log();
|
|
340
|
-
|
|
341
|
-
const saved = stats.savings.estimatedTokens;
|
|
342
|
-
if (saved === 0) {
|
|
343
|
-
console.log(gray(" No session entries yet — start logging to track savings"));
|
|
344
|
-
} else {
|
|
345
|
-
const sessions = Math.max(stats.memory.sessionsTracked, 1);
|
|
346
|
-
const perSession = Math.round(saved / sessions);
|
|
347
|
-
|
|
348
|
-
console.log(` Total saved: ` + bold(green(fmtTokens(saved) + " tokens")));
|
|
349
|
-
console.log(` Per session: ` + bold(fmtTokens(perSession) + " tokens"));
|
|
350
|
-
console.log();
|
|
351
|
-
console.log(gray(" Breakdown:"));
|
|
352
|
-
|
|
353
|
-
const typeOrder = ["gotcha", "handoff", "attempt", "decision", "theme", "preference", "note", "error"];
|
|
354
|
-
for (const type of typeOrder) {
|
|
355
|
-
const tokens = stats.savings.breakdown[type];
|
|
356
|
-
if (!tokens) continue;
|
|
357
|
-
const count = stats.memory.byType[type] || 0;
|
|
358
|
-
console.log(gray(` ${type.padEnd(12)} ${count}× × ${SAVINGS_PER_TYPE[type] || 100} = `) + cyan(fmtTokens(tokens)));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
console.log();
|
|
362
|
-
console.log(gray(" * Estimates based on typical back-and-forth cost per entry type."));
|
|
363
|
-
console.log(gray(" Actual savings vary with model, project complexity, and session length."));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
console.log();
|
|
367
|
-
console.log(SEP);
|
|
368
|
-
console.log();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ── entry point ───────────────────────────────────────────────────────────────
|
|
372
|
-
|
|
373
|
-
export async function statsCommand(args = []) {
|
|
374
|
-
const jsonMode = args.includes("--json");
|
|
375
|
-
const briefMode = args.includes("--brief");
|
|
376
|
-
|
|
377
|
-
const cwd = process.cwd();
|
|
378
|
-
|
|
379
|
-
if (!fs.existsSync(path.join(cwd, INFERNO_DIR))) {
|
|
380
|
-
console.error(red(" ✘ inferno/ not found — run: infernoflow init\n"));
|
|
381
|
-
process.exit(1);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const stats = collectStats(cwd);
|
|
385
|
-
|
|
386
|
-
if (jsonMode) {
|
|
387
|
-
console.log(JSON.stringify(stats, null, 2));
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (briefMode) {
|
|
392
|
-
const parts = [];
|
|
393
|
-
if (stats.memory.total) parts.push(`${stats.memory.total} memory entries`);
|
|
394
|
-
if (stats.context.estimatedTokens) parts.push(`${fmtTokens(stats.context.estimatedTokens)} tokens/session`);
|
|
395
|
-
if (stats.coverage.total) parts.push(`${stats.coverage.pct}% capability coverage`);
|
|
396
|
-
if (stats.savings.estimatedTokens) parts.push(`${fmtTokens(stats.savings.estimatedTokens)} tokens saved`);
|
|
397
|
-
console.log(parts.join(" · ") || "No data yet — run infernoflow init + infernoflow log");
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
printDashboard(stats);
|
|
402
|
-
}
|
|
1
|
+
import*as y from"node:fs";import*as f from"node:path";import{bold as g,cyan as w,gray as t,green as v,yellow as E,red as j}from"../ui/output.mjs";const m="inferno",I=f.join(m,"sessions.jsonl"),N=f.join(m,"CONTEXT.md"),O=f.join(m,"theme.json"),A=f.join(m,"scan.json"),C=f.join(m,"contract.json"),M=f.join(m,"capabilities.json");function k(o){try{return JSON.parse(y.readFileSync(o,"utf8"))}catch{return null}}function F(o){return Math.ceil((o||"").length/4)}const $={gotcha:400,decision:200,attempt:250,preference:150,note:100,theme:300,handoff:500,error:200};function L(o){const e={ok:!1,memory:{total:0,byType:{},oldestEntry:null,newestEntry:null,sessionsTracked:0},context:{sizeBytes:0,estimatedTokens:0,hasIntent:!1,hasWorking:!1},theme:{captured:!1,fonts:0,colors:0,cssVars:0,framework:null},coverage:{total:0,withAnalysis:0,pct:0},chains:{total:0,resolved:0},contract:{policyId:null,capabilities:0,isLite:!1},savings:{estimatedTokens:0,breakdown:{}}},a=k(f.join(o,C));a&&(e.contract.policyId=a.policyId,e.contract.capabilities=(a.capabilities||[]).length,e.contract.isLite=!!a.lite,e.ok=!0);const c=k(f.join(o,M));if(c){const s=Array.isArray(c)?c:c.capabilities||[];e.coverage.total=s.length,e.coverage.withAnalysis=s.filter(i=>i.codeAnalysis).length,e.coverage.pct=e.coverage.total?Math.round(e.coverage.withAnalysis/e.coverage.total*100):0}const n=f.join(o,I);if(y.existsSync(n)){const i=y.readFileSync(n,"utf8").split(`
|
|
2
|
+
`).filter(Boolean).map(u=>{try{return JSON.parse(u)}catch{return null}}).filter(Boolean);e.memory.total=i.length;for(const u of i){const p=u.type||"note";e.memory.byType[p]=(e.memory.byType[p]||0)+1;const b=$[p]||100;e.savings.estimatedTokens+=b,e.savings.breakdown[p]=(e.savings.breakdown[p]||0)+b}i.length&&(e.memory.oldestEntry=i[0].ts,e.memory.newestEntry=i[i.length-1].ts);const x=new Set(i.map(u=>(u.ts||"").slice(0,10)));e.memory.sessionsTracked=x.size}const l=f.join(o,N);if(y.existsSync(l)){const s=y.readFileSync(l,"utf8");e.context.sizeBytes=Buffer.byteLength(s,"utf8"),e.context.estimatedTokens=F(s),e.context.hasIntent=s.includes("## Intent"),e.context.hasWorking=s.includes("## Working on")}const r=k(f.join(o,O));r&&(e.theme.captured=!0,e.theme.fonts=Object.keys(r.fonts||{}).filter(s=>r.fonts[s]).length,e.theme.colors=Object.keys(r.colors?.palette||{}).length,e.theme.cssVars=Object.keys(r.cssVars||{}).length,e.theme.framework=r.framework||null);const h=k(f.join(o,A));if(h?.httpChains){const s=Object.values(h.httpChains).flat();e.chains.total=s.length,e.chains.resolved=s.filter(i=>i.resolved).length}return e}function T(o,e,a=20){const c=e>0?Math.round(o/e*a):0;return"\u2588".repeat(c)+"\u2591".repeat(a-c)}function S(o){return o>=80?v:o>=40?E:j}function B(o){if(!o)return"never";const e=new Date(o),c=Date.now()-e.getTime(),n=Math.floor(c/864e5);return n===0?"today":n===1?"yesterday":n<7?`${n}d ago`:n<30?`${Math.floor(n/7)}w ago`:e.toLocaleDateString("en-GB",{day:"2-digit",month:"short"})}function d(o){return o>=1e3?`~${Math.round(o/100)/10}k`:`~${o}`}function P(o){const e=t(" "+"\u2500".repeat(52));console.log(),console.log(" "+g("\u{1F525} infernoflow stats")),o.contract.policyId&&console.log(t(` Project: ${o.contract.policyId}${o.contract.isLite?" (lite)":""}`)),console.log(e),console.log(),console.log(" "+g("Session memory")+t(" (inferno/sessions.jsonl)")),console.log();const a=o.memory.total;if(a===0)console.log(t(' No entries yet \u2014 run: infernoflow log "<what happened>" --type gotcha'));else{const n=["gotcha","decision","attempt","preference","theme","note","handoff","error"],l=Math.max(...Object.values(o.memory.byType));for(const r of n){const h=o.memory.byType[r]||0;if(h===0)continue;const s=T(h,l,16),i=r.padEnd(12);console.log(` ${t(i)} ${w(s)} ${h}`)}console.log(),console.log(t(" Total entries: ")+g(a)),console.log(t(" Sessions tracked: ")+o.memory.sessionsTracked),o.memory.newestEntry&&console.log(t(" Last entry: ")+B(o.memory.newestEntry))}if(console.log(),console.log(e),console.log(),console.log(" "+g("Context injection")+t(" (per session start)")),console.log(),o.context.sizeBytes===0?console.log(t(" No CONTEXT.md yet \u2014 run: infernoflow context")):(console.log(t(" Size: ")+`${Math.round(o.context.sizeBytes/1024*10)/10} KB`),console.log(t(" Tokens: ")+g(d(o.context.estimatedTokens))+t(" injected into every session")),o.context.hasIntent&&console.log(t(" ")+v("\u2714")+t(" Intent captured")),o.context.hasWorking&&console.log(t(" ")+v("\u2714")+t(" Working state captured"))),console.log(),console.log(e),console.log(),console.log(" "+g("Capability coverage")+t(" (code analysis via infernoflow scan)")),console.log(),o.coverage.total===0)console.log(t(" No capabilities yet \u2014 run: infernoflow init"));else{const n=S(o.coverage.pct),l=T(o.coverage.withAnalysis,o.coverage.total,24);if(console.log(` ${n(l)} ${g(o.coverage.pct+"%")} (${o.coverage.withAnalysis}/${o.coverage.total})`),o.coverage.pct<100){const r=o.coverage.total-o.coverage.withAnalysis;console.log(t(`
|
|
3
|
+
${r} capabilities without code analysis`)),console.log(t(" Run: infernoflow scan to enrich them"))}}if(o.chains.total>0){console.log(),console.log(e),console.log(),console.log(" "+g("HTTP call chains")+t(" (end-to-end resolution)")),console.log();const n=Math.round(o.chains.resolved/o.chains.total*100),l=S(n),r=T(o.chains.resolved,o.chains.total,20);console.log(` ${l(r)} ${g(n+"%")} resolved (${o.chains.resolved}/${o.chains.total} call chains)`),o.chains.resolved<o.chains.total&&console.log(t(`
|
|
4
|
+
Unresolved calls may be to external services or missing route files`))}if(console.log(),console.log(e),console.log(),console.log(" "+g("Design system")+t(" (inferno/theme.json)")),console.log(),!o.theme.captured)console.log(t(" Not captured yet \u2014 run: infernoflow theme"));else{const n=[];o.theme.fonts&&n.push(`${o.theme.fonts} font${o.theme.fonts!==1?"s":""}`),o.theme.colors&&n.push(`${o.theme.colors} colors`),o.theme.cssVars&&n.push(`${o.theme.cssVars} CSS vars`),o.theme.framework&&n.push(`${o.theme.framework}`),console.log(t(" ")+v("\u2714")+" "+n.join(" \xB7 ")),console.log(t(" AI agents always use the correct fonts and colors for this project"))}console.log(),console.log(e),console.log(),console.log(" "+g("Estimated token savings")+t(" (vs re-discovering from scratch)")),console.log();const c=o.savings.estimatedTokens;if(c===0)console.log(t(" No session entries yet \u2014 start logging to track savings"));else{const n=Math.max(o.memory.sessionsTracked,1),l=Math.round(c/n);console.log(" Total saved: "+g(v(d(c)+" tokens"))),console.log(" Per session: "+g(d(l)+" tokens")),console.log(),console.log(t(" Breakdown:"));const r=["gotcha","handoff","attempt","decision","theme","preference","note","error"];for(const h of r){const s=o.savings.breakdown[h];if(!s)continue;const i=o.memory.byType[h]||0;console.log(t(` ${h.padEnd(12)} ${i}\xD7 \xD7 ${$[h]||100} = `)+w(d(s)))}console.log(),console.log(t(" * Estimates based on typical back-and-forth cost per entry type.")),console.log(t(" Actual savings vary with model, project complexity, and session length."))}console.log(),console.log(e),console.log()}async function D(o=[]){const e=o.includes("--json"),a=o.includes("--brief"),c=process.cwd();y.existsSync(f.join(c,m))||(console.error(j(` \u2718 inferno/ not found \u2014 run: infernoflow init
|
|
5
|
+
`)),process.exit(1));const n=L(c);if(e){console.log(JSON.stringify(n,null,2));return}if(a){const l=[];n.memory.total&&l.push(`${n.memory.total} memory entries`),n.context.estimatedTokens&&l.push(`${d(n.context.estimatedTokens)} tokens/session`),n.coverage.total&&l.push(`${n.coverage.pct}% capability coverage`),n.savings.estimatedTokens&&l.push(`${d(n.savings.estimatedTokens)} tokens saved`),console.log(l.join(" \xB7 ")||"No data yet \u2014 run infernoflow init + infernoflow log");return}P(n)}export{D as statsCommand};
|
|
@@ -1,172 +1,4 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function timeAgo(ms) {
|
|
6
|
-
const s = Math.floor((Date.now() - ms) / 1000);
|
|
7
|
-
if (s < 60) return "just now";
|
|
8
|
-
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
9
|
-
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
10
|
-
return `${Math.floor(s / 86400)}d ago`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function getCoverage(scenariosDir, caps) {
|
|
14
|
-
const covered = new Set();
|
|
15
|
-
if (fs.existsSync(scenariosDir)) {
|
|
16
|
-
for (const f of fs.readdirSync(scenariosDir).filter(f => f.endsWith(".json"))) {
|
|
17
|
-
try {
|
|
18
|
-
const s = JSON.parse(fs.readFileSync(path.join(scenariosDir, f), "utf8"));
|
|
19
|
-
(s.capabilitiesCovered || []).forEach(c => covered.add(c));
|
|
20
|
-
} catch {}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return { covered: caps.filter(c => covered.has(c)), uncovered: caps.filter(c => !covered.has(c)) };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function statusCommand(args = []) {
|
|
27
|
-
const asJson = args.includes("--json");
|
|
28
|
-
const cwd = process.cwd();
|
|
29
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
30
|
-
if (!asJson) {
|
|
31
|
-
header("status");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!fs.existsSync(infernoDir)) {
|
|
35
|
-
if (asJson) {
|
|
36
|
-
console.log(JSON.stringify({ ok: false, error: "inferno_not_found", hint: "Run: infernoflow init" }, null, 2));
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
fail("inferno/ not found", `Run: infernoflow init`);
|
|
40
|
-
console.log();
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
45
|
-
if (!fs.existsSync(contractPath)) {
|
|
46
|
-
if (asJson) {
|
|
47
|
-
console.log(JSON.stringify({ ok: false, error: "contract_not_found" }, null, 2));
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
fail("contract.json not found");
|
|
51
|
-
console.log();
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
56
|
-
const caps = contract.capabilities || [];
|
|
57
|
-
const stat = fs.statSync(contractPath);
|
|
58
|
-
const scenariosDir = path.join(infernoDir, "scenarios");
|
|
59
|
-
const changelogPath = path.join(infernoDir, "CHANGELOG.md");
|
|
60
|
-
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
61
|
-
const { covered, uncovered } = getCoverage(scenariosDir, caps);
|
|
62
|
-
|
|
63
|
-
const hasChangelog = fs.existsSync(changelogPath) && /##\s+Unreleased/i.test(fs.readFileSync(changelogPath, "utf8"));
|
|
64
|
-
const driftReasons = [];
|
|
65
|
-
if (uncovered.length > 0) driftReasons.push(`${uncovered.length} capabilities without scenario coverage`);
|
|
66
|
-
if (!hasChangelog) driftReasons.push("CHANGELOG missing ## Unreleased section");
|
|
67
|
-
const allGood = driftReasons.length === 0;
|
|
68
|
-
|
|
69
|
-
if (asJson) {
|
|
70
|
-
const payload = {
|
|
71
|
-
ok: allGood,
|
|
72
|
-
driftReasons,
|
|
73
|
-
project: {
|
|
74
|
-
policyId: contract.policyId || null,
|
|
75
|
-
policyVersion: contract.policyVersion || null,
|
|
76
|
-
lastChange: timeAgo(stat.mtimeMs),
|
|
77
|
-
},
|
|
78
|
-
capabilities: {
|
|
79
|
-
total: caps.length,
|
|
80
|
-
uncovered,
|
|
81
|
-
},
|
|
82
|
-
changelog: {
|
|
83
|
-
hasUnreleased: hasChangelog,
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
87
|
-
process.exit(allGood ? 0 : 1);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!allGood) {
|
|
91
|
-
section("Drift");
|
|
92
|
-
driftReasons.forEach((reason) => console.log(` ${yellow("⚠")} ${reason}`));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ── Project ─────────────────────────────────────────────────────
|
|
96
|
-
section("Project");
|
|
97
|
-
console.log(` ${gray("policy")} ${bold(contract.policyId || "—")}`);
|
|
98
|
-
console.log(` ${gray("version")} ${bold("v" + (contract.policyVersion || "?"))}`);
|
|
99
|
-
console.log(` ${gray("last change")} ${gray(timeAgo(stat.mtimeMs))}`);
|
|
100
|
-
|
|
101
|
-
// ── Capabilities ─────────────────────────────────────────────────
|
|
102
|
-
section(`Capabilities ${gray("(" + caps.length + ")")}`);
|
|
103
|
-
|
|
104
|
-
let capsRegistry = {};
|
|
105
|
-
if (fs.existsSync(capsPath)) {
|
|
106
|
-
try {
|
|
107
|
-
const reg = JSON.parse(fs.readFileSync(capsPath, "utf8"));
|
|
108
|
-
(reg.capabilities || []).forEach(c => { capsRegistry[c.id] = c; });
|
|
109
|
-
} catch {}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
caps.forEach(cap => {
|
|
113
|
-
const reg = capsRegistry[cap];
|
|
114
|
-
const hasCoverage = covered.includes(cap);
|
|
115
|
-
const icon = hasCoverage ? green("✔") : red("✘");
|
|
116
|
-
const title = reg?.title ? gray(` — ${reg.title}`) : "";
|
|
117
|
-
const since = reg?.since ? gray(` [${reg.since}]`) : "";
|
|
118
|
-
console.log(` ${icon} ${white(cap)}${title}${since}`);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
if (uncovered.length > 0) {
|
|
122
|
-
console.log(`\n ${yellow("⚠")} ${uncovered.length} capability(ies) lack scenario coverage`);
|
|
123
|
-
} else {
|
|
124
|
-
console.log(`\n ${green("✔")} All capabilities have scenario coverage`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ── Scenarios ─────────────────────────────────────────────────────
|
|
128
|
-
section("Scenarios");
|
|
129
|
-
if (fs.existsSync(scenariosDir)) {
|
|
130
|
-
const files = fs.readdirSync(scenariosDir).filter(f => f.endsWith(".json"));
|
|
131
|
-
if (files.length === 0) {
|
|
132
|
-
warn("No scenario files — add .json files to inferno/scenarios/");
|
|
133
|
-
} else {
|
|
134
|
-
files.forEach(f => {
|
|
135
|
-
try {
|
|
136
|
-
const s = JSON.parse(fs.readFileSync(path.join(scenariosDir, f), "utf8"));
|
|
137
|
-
const steps = s.steps?.length || 0;
|
|
138
|
-
const capCount = (s.capabilitiesCovered || []).length;
|
|
139
|
-
console.log(` ${green("✔")} ${cyan(f)} ${gray(`— ${steps} steps, ${capCount} caps covered`)}`);
|
|
140
|
-
} catch {
|
|
141
|
-
console.log(` ${red("✘")} ${cyan(f)} ${gray("— invalid JSON")}`);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
warn("scenarios/ directory not found");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ── Changelog ─────────────────────────────────────────────────────
|
|
150
|
-
section("Changelog");
|
|
151
|
-
if (fs.existsSync(changelogPath)) {
|
|
152
|
-
const txt = fs.readFileSync(changelogPath, "utf8");
|
|
153
|
-
if (/##\s+Unreleased/i.test(txt)) {
|
|
154
|
-
ok("Has ## Unreleased section");
|
|
155
|
-
} else {
|
|
156
|
-
fail("Missing ## Unreleased section");
|
|
157
|
-
}
|
|
158
|
-
const sections = txt.split("\n").filter(l => /^##\s/.test(l)).slice(0, 3);
|
|
159
|
-
sections.forEach(l => console.log(` ${gray(l)}`));
|
|
160
|
-
} else {
|
|
161
|
-
fail("inferno/CHANGELOG.md not found");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ── Health ────────────────────────────────────────────────────────
|
|
165
|
-
console.log();
|
|
166
|
-
if (allGood) {
|
|
167
|
-
console.log(` ${green("●")} ${bold(green("ready"))} ${gray("— run infernoflow check for full validation")}`);
|
|
168
|
-
} else {
|
|
169
|
-
console.log(` ${yellow("●")} ${bold(yellow("needs attention"))} ${gray("— run infernoflow check for details")}`);
|
|
170
|
-
}
|
|
171
|
-
console.log();
|
|
172
|
-
}
|
|
1
|
+
import*as o from"node:fs";import*as a from"node:path";import{header as A,ok as H,fail as j,warn as J,section as p,bold as m,cyan as k,yellow as x,gray as t,green as u,red as G,white as P}from"../ui/output.mjs";function M(c){const e=Math.floor((Date.now()-c)/1e3);return e<60?"just now":e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`}function R(c,e){const g=new Set;if(o.existsSync(c))for(const i of o.readdirSync(c).filter(f=>f.endsWith(".json")))try{(JSON.parse(o.readFileSync(a.join(c,i),"utf8")).capabilitiesCovered||[]).forEach(l=>g.add(l))}catch{}return{covered:e.filter(i=>g.has(i)),uncovered:e.filter(i=>!g.has(i))}}async function I(c=[]){const e=c.includes("--json"),g=process.cwd(),i=a.join(g,"inferno");e||A("status"),o.existsSync(i)||(e&&(console.log(JSON.stringify({ok:!1,error:"inferno_not_found",hint:"Run: infernoflow init"},null,2)),process.exit(1)),j("inferno/ not found","Run: infernoflow init"),console.log(),process.exit(1));const f=a.join(i,"contract.json");o.existsSync(f)||(e&&(console.log(JSON.stringify({ok:!1,error:"contract_not_found"},null,2)),process.exit(1)),j("contract.json not found"),console.log(),process.exit(1));const l=JSON.parse(o.readFileSync(f,"utf8")),y=l.capabilities||[],N=o.statSync(f),$=a.join(i,"scenarios"),S=a.join(i,"CHANGELOG.md"),b=a.join(i,"capabilities.json"),{covered:F,uncovered:d}=R($,y),O=o.existsSync(S)&&/##\s+Unreleased/i.test(o.readFileSync(S,"utf8")),h=[];d.length>0&&h.push(`${d.length} capabilities without scenario coverage`),O||h.push("CHANGELOG missing ## Unreleased section");const v=h.length===0;if(e){const n={ok:v,driftReasons:h,project:{policyId:l.policyId||null,policyVersion:l.policyVersion||null,lastChange:M(N.mtimeMs)},capabilities:{total:y.length,uncovered:d},changelog:{hasUnreleased:O}};console.log(JSON.stringify(n,null,2)),process.exit(v?0:1)}v||(p("Drift"),h.forEach(n=>console.log(` ${x("\u26A0")} ${n}`))),p("Project"),console.log(` ${t("policy")} ${m(l.policyId||"\u2014")}`),console.log(` ${t("version")} ${m("v"+(l.policyVersion||"?"))}`),console.log(` ${t("last change")} ${t(M(N.mtimeMs))}`),p(`Capabilities ${t("("+y.length+")")}`);let E={};if(o.existsSync(b))try{(JSON.parse(o.readFileSync(b,"utf8")).capabilities||[]).forEach(s=>{E[s.id]=s})}catch{}if(y.forEach(n=>{const s=E[n],C=F.includes(n)?u("\u2714"):G("\u2718"),w=s?.title?t(` \u2014 ${s.title}`):"",U=s?.since?t(` [${s.since}]`):"";console.log(` ${C} ${P(n)}${w}${U}`)}),d.length>0?console.log(`
|
|
2
|
+
${x("\u26A0")} ${d.length} capability(ies) lack scenario coverage`):console.log(`
|
|
3
|
+
${u("\u2714")} All capabilities have scenario coverage`),p("Scenarios"),o.existsSync($)){const n=o.readdirSync($).filter(s=>s.endsWith(".json"));n.length===0?J("No scenario files \u2014 add .json files to inferno/scenarios/"):n.forEach(s=>{try{const r=JSON.parse(o.readFileSync(a.join($,s),"utf8")),C=r.steps?.length||0,w=(r.capabilitiesCovered||[]).length;console.log(` ${u("\u2714")} ${k(s)} ${t(`\u2014 ${C} steps, ${w} caps covered`)}`)}catch{console.log(` ${G("\u2718")} ${k(s)} ${t("\u2014 invalid JSON")}`)}})}else J("scenarios/ directory not found");if(p("Changelog"),o.existsSync(S)){const n=o.readFileSync(S,"utf8");/##\s+Unreleased/i.test(n)?H("Has ## Unreleased section"):j("Missing ## Unreleased section"),n.split(`
|
|
4
|
+
`).filter(r=>/^##\s/.test(r)).slice(0,3).forEach(r=>console.log(` ${t(r)}`))}else j("inferno/CHANGELOG.md not found");console.log(),console.log(v?` ${u("\u25CF")} ${m(u("ready"))} ${t("\u2014 run infernoflow check for full validation")}`:` ${x("\u25CF")} ${m(x("needs attention"))} ${t("\u2014 run infernoflow check for details")}`),console.log()}export{I as statusCommand};
|