infernoflow 0.10.13 → 0.10.15
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 +68 -0
- package/dist/lib/ai/ideDetection.mjs +1 -0
- package/dist/lib/ai/localProvider.mjs +1 -0
- package/dist/lib/ai/providerRouter.mjs +1 -0
- package/dist/lib/commands/adopt.mjs +20 -0
- package/dist/lib/commands/check.mjs +3 -0
- package/dist/lib/commands/context.mjs +20 -0
- package/dist/lib/commands/docGate.mjs +2 -0
- package/dist/lib/commands/implement.mjs +7 -0
- package/dist/lib/commands/init.mjs +17 -0
- package/dist/lib/commands/installCursorHooks.mjs +1 -0
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -0
- package/dist/lib/commands/prImpact.mjs +2 -0
- package/dist/lib/commands/run.mjs +10 -0
- package/dist/lib/commands/status.mjs +4 -0
- package/dist/lib/commands/suggest.mjs +62 -0
- package/dist/lib/commands/syncAuto.mjs +1 -0
- package/dist/lib/cursorHooksInstall.mjs +1 -0
- package/dist/lib/draftToolingInstall.mjs +8 -0
- package/dist/lib/ui/output.mjs +6 -0
- package/dist/lib/ui/prompts.mjs +6 -0
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -0
- package/{templates → dist/templates}/scripts/inferno-vscode-copilot-hook.mjs +23 -3
- package/package.json +48 -44
- package/bin/infernoflow.mjs +0 -138
- package/lib/ai/ideDetection.mjs +0 -31
- package/lib/ai/localProvider.mjs +0 -88
- package/lib/ai/providerRouter.mjs +0 -73
- package/lib/commands/adopt.mjs +0 -768
- package/lib/commands/check.mjs +0 -179
- package/lib/commands/context.mjs +0 -164
- package/lib/commands/docGate.mjs +0 -81
- package/lib/commands/implement.mjs +0 -103
- package/lib/commands/init.mjs +0 -401
- package/lib/commands/installCursorHooks.mjs +0 -36
- package/lib/commands/installVsCodeCopilotHooks.mjs +0 -37
- package/lib/commands/prImpact.mjs +0 -157
- package/lib/commands/run.mjs +0 -338
- package/lib/commands/status.mjs +0 -172
- package/lib/commands/suggest.mjs +0 -501
- package/lib/commands/syncAuto.mjs +0 -96
- package/lib/cursorHooksInstall.mjs +0 -39
- package/lib/draftToolingInstall.mjs +0 -69
- package/lib/ui/output.mjs +0 -72
- package/lib/ui/prompts.mjs +0 -147
- package/lib/vsCodeCopilotHooksInstall.mjs +0 -42
- /package/{templates → dist/templates}/ci/github-inferno-check.yml +0 -0
- /package/{templates → dist/templates}/cursor/hooks/inferno-session-draft.mjs +0 -0
- /package/{templates → dist/templates}/cursor/hooks.json +0 -0
- /package/{templates → dist/templates}/github-hooks/infernoflow-drafts.json +0 -0
- /package/{templates → dist/templates}/inferno/CHANGELOG.md +0 -0
- /package/{templates → dist/templates}/inferno/capabilities.json +0 -0
- /package/{templates → dist/templates}/inferno/contract.json +0 -0
- /package/{templates → dist/templates}/inferno/scenarios/happy_path.json +0 -0
- /package/{templates → dist/templates}/scripts/inferno-doc-gate.mjs +0 -0
- /package/{templates → dist/templates}/scripts/inferno-install-hooks.mjs +0 -0
- /package/{templates → dist/templates}/scripts/inferno-promote-draft.mjs +0 -0
package/lib/commands/run.mjs
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { generateWithLocalModel } from "../ai/localProvider.mjs";
|
|
6
|
-
import { resolveProvider } from "../ai/providerRouter.mjs";
|
|
7
|
-
import {
|
|
8
|
-
buildPrompt,
|
|
9
|
-
loadSuggestContext,
|
|
10
|
-
parseSuggestionJson,
|
|
11
|
-
validateSuggestion,
|
|
12
|
-
detectSuggestionConflicts,
|
|
13
|
-
applyChanges,
|
|
14
|
-
} from "./suggest.mjs";
|
|
15
|
-
import { header, section, ok, warn, fail, info, gray } from "../ui/output.mjs";
|
|
16
|
-
|
|
17
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
-
const __dirname = path.dirname(__filename);
|
|
19
|
-
const binPath = path.resolve(__dirname, "..", "..", "bin", "infernoflow.mjs");
|
|
20
|
-
|
|
21
|
-
function runCliJson(args) {
|
|
22
|
-
try {
|
|
23
|
-
const out = execFileSync(process.execPath, [binPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
24
|
-
return { ok: true, data: JSON.parse(out) };
|
|
25
|
-
} catch (err) {
|
|
26
|
-
const stdout = err?.stdout?.toString?.() || "";
|
|
27
|
-
try {
|
|
28
|
-
return { ok: false, data: JSON.parse(stdout) };
|
|
29
|
-
} catch {
|
|
30
|
-
return { ok: false, data: { ok: false, errors: ["command_failed"] } };
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stageEvent(asJson, events, stage, status, details = {}) {
|
|
36
|
-
const ev = { ts: new Date().toISOString(), stage, status, ...details };
|
|
37
|
-
events.push(ev);
|
|
38
|
-
if (asJson) return;
|
|
39
|
-
const text = `${stage}: ${status}`;
|
|
40
|
-
if (status === "ok") ok(text);
|
|
41
|
-
else if (status === "warn") warn(text);
|
|
42
|
-
else if (status === "fail") fail(text);
|
|
43
|
-
else info(text);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function snapshotInferno(cwd) {
|
|
47
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
48
|
-
const targets = [];
|
|
49
|
-
const walk = (dir) => {
|
|
50
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
-
const p = path.join(dir, entry.name);
|
|
52
|
-
if (entry.isDirectory()) walk(p);
|
|
53
|
-
else targets.push(p);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
if (fs.existsSync(infernoDir)) walk(infernoDir);
|
|
57
|
-
const snapshot = new Map();
|
|
58
|
-
targets.forEach((filePath) => snapshot.set(filePath, fs.readFileSync(filePath, "utf8")));
|
|
59
|
-
return snapshot;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function restoreSnapshot(cwd, snapshot) {
|
|
63
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
64
|
-
if (fs.existsSync(infernoDir)) {
|
|
65
|
-
const existing = [];
|
|
66
|
-
const walk = (dir) => {
|
|
67
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
-
const p = path.join(dir, entry.name);
|
|
69
|
-
if (entry.isDirectory()) walk(p);
|
|
70
|
-
else existing.push(p);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
walk(infernoDir);
|
|
74
|
-
existing.forEach((filePath) => {
|
|
75
|
-
if (!snapshot.has(filePath)) fs.unlinkSync(filePath);
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
for (const [filePath, content] of snapshot.entries()) {
|
|
79
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
80
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function writeRunArtifact(cwd, artifact) {
|
|
85
|
-
const runsDir = path.join(cwd, "inferno", "runs");
|
|
86
|
-
fs.mkdirSync(runsDir, { recursive: true });
|
|
87
|
-
const filePath = path.join(runsDir, `${Date.now()}.json`);
|
|
88
|
-
fs.writeFileSync(filePath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
89
|
-
return path.relative(cwd, filePath);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getOptionValue(args, flag, fallback = null) {
|
|
93
|
-
const idx = args.indexOf(flag);
|
|
94
|
-
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
|
|
95
|
-
return fallback;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function extractTask(args) {
|
|
99
|
-
const takesValue = new Set(["--provider", "--ide"]);
|
|
100
|
-
const out = [];
|
|
101
|
-
for (let i = 1; i < args.length; i++) {
|
|
102
|
-
const token = args[i];
|
|
103
|
-
if (token.startsWith("-")) {
|
|
104
|
-
if (takesValue.has(token)) i += 1;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
out.push(token);
|
|
108
|
-
}
|
|
109
|
-
return out.join(" ").trim();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function buildPromptFallbackSuggestion(task, contract) {
|
|
113
|
-
return {
|
|
114
|
-
summary: `Prompt fallback only: ${task}`,
|
|
115
|
-
newCapabilities: [],
|
|
116
|
-
removedCapabilities: [],
|
|
117
|
-
updatedScenarios: [],
|
|
118
|
-
changelogEntry: `- Prompt fallback mode for task: ${task} (no automatic contract mutation).`,
|
|
119
|
-
_meta: {
|
|
120
|
-
actionRequired: true,
|
|
121
|
-
nextStep: "Run infernoflow suggest or provide an agent bridge for automatic apply.",
|
|
122
|
-
capabilitiesCount: (contract?.capabilities || []).length,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function generateWithIdeAgent(prompt) {
|
|
128
|
-
if (process.env.INFERNO_AGENT_MOCK_RESPONSE) return process.env.INFERNO_AGENT_MOCK_RESPONSE;
|
|
129
|
-
const responseFile = process.env.INFERNO_AGENT_RESPONSE_FILE
|
|
130
|
-
? process.env.INFERNO_AGENT_RESPONSE_FILE
|
|
131
|
-
: path.join(process.cwd(), "inferno", "agent-response.json");
|
|
132
|
-
if (fs.existsSync(responseFile)) {
|
|
133
|
-
const data = fs.readFileSync(responseFile, "utf8");
|
|
134
|
-
fs.unlinkSync(responseFile);
|
|
135
|
-
return data;
|
|
136
|
-
}
|
|
137
|
-
const infernoDir = path.join(process.cwd(), "inferno");
|
|
138
|
-
const promptFile = path.join(infernoDir, "agent-prompt.md");
|
|
139
|
-
if (fs.existsSync(promptFile)) fs.unlinkSync(promptFile);
|
|
140
|
-
fs.writeFileSync(promptFile, prompt, "utf8");
|
|
141
|
-
process.stderr.write("\n \u2139 Prompt written to inferno/agent-prompt.md\n");
|
|
142
|
-
process.stderr.write(" \u2192 Open it, paste into Cursor or Claude\n");
|
|
143
|
-
process.stderr.write(" \u2192 Save the JSON reply to: inferno/agent-response.json\n");
|
|
144
|
-
process.stderr.write(" Waiting up to 5 minutes...\n\n");
|
|
145
|
-
const deadline = Date.now() + 300_000;
|
|
146
|
-
while (Date.now() < deadline) {
|
|
147
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
148
|
-
if (fs.existsSync(responseFile)) {
|
|
149
|
-
const data = fs.readFileSync(responseFile, "utf8");
|
|
150
|
-
fs.unlinkSync(responseFile);
|
|
151
|
-
process.stderr.write(" \u2714 Response received\n\n");
|
|
152
|
-
return data;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
throw new Error("ide_agent_bridge_timeout");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function runCommand(args = []) {
|
|
159
|
-
const asJson = args.includes("--json");
|
|
160
|
-
const dryRun = args.includes("--dry-run");
|
|
161
|
-
const noRollback = args.includes("--no-rollback");
|
|
162
|
-
const providerRequested = (getOptionValue(args, "--provider", "auto") || "auto").toLowerCase();
|
|
163
|
-
const ideRequested = (getOptionValue(args, "--ide", "auto") || "auto").toLowerCase();
|
|
164
|
-
const task = extractTask(args) || "sync check";
|
|
165
|
-
const cwd = process.cwd();
|
|
166
|
-
const events = [];
|
|
167
|
-
const reasonCodes = [];
|
|
168
|
-
|
|
169
|
-
if (!asJson) header("run");
|
|
170
|
-
stageEvent(asJson, events, "init", "info", { task, dryRun, noRollback });
|
|
171
|
-
|
|
172
|
-
// detect
|
|
173
|
-
const impact = runCliJson(["pr-impact", "--json"]);
|
|
174
|
-
stageEvent(asJson, events, "detect", impact.data?.ok ? "ok" : "warn", { confidence: impact.data?.confidence || "low" });
|
|
175
|
-
|
|
176
|
-
const routed = await resolveProvider(providerRequested, ideRequested);
|
|
177
|
-
reasonCodes.push(...(routed.reasonCodes || []));
|
|
178
|
-
if (routed.error === "agent_unavailable") {
|
|
179
|
-
const payload = {
|
|
180
|
-
ok: false,
|
|
181
|
-
error: "agent_unavailable",
|
|
182
|
-
providerRequested,
|
|
183
|
-
providerResolved: routed.providerResolved,
|
|
184
|
-
ideDetected: routed.ideDetected,
|
|
185
|
-
agentAvailable: routed.agentAvailable,
|
|
186
|
-
reasonCodes,
|
|
187
|
-
events,
|
|
188
|
-
};
|
|
189
|
-
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
190
|
-
else fail("provider agent unavailable", "Use --provider auto|local|prompt");
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
stageEvent(asJson, events, "route", "ok", {
|
|
194
|
-
providerRequested,
|
|
195
|
-
providerResolved: routed.providerResolved,
|
|
196
|
-
ideDetected: routed.ideDetected,
|
|
197
|
-
agentAvailable: routed.agentAvailable,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const ctx = loadSuggestContext(cwd);
|
|
201
|
-
if (!ctx?.contract) {
|
|
202
|
-
const payload = { ok: false, error: "inferno_missing", events };
|
|
203
|
-
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
204
|
-
else fail("inferno/ missing or invalid");
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// propose
|
|
209
|
-
const prompt = buildPrompt({
|
|
210
|
-
description: task,
|
|
211
|
-
contract: ctx.contract,
|
|
212
|
-
capabilities: ctx.capabilities,
|
|
213
|
-
scenarios: ctx.scenarios,
|
|
214
|
-
});
|
|
215
|
-
let suggestion;
|
|
216
|
-
try {
|
|
217
|
-
if (routed.providerResolved === "local") {
|
|
218
|
-
const raw = await generateWithLocalModel(prompt);
|
|
219
|
-
suggestion = parseSuggestionJson(raw);
|
|
220
|
-
} else if (routed.providerResolved === "agent") {
|
|
221
|
-
const raw = await generateWithIdeAgent(prompt);
|
|
222
|
-
suggestion = parseSuggestionJson(raw);
|
|
223
|
-
} else {
|
|
224
|
-
suggestion = buildPromptFallbackSuggestion(task, ctx.contract);
|
|
225
|
-
}
|
|
226
|
-
} catch (err) {
|
|
227
|
-
const payload = { ok: false, error: "proposal_failed", reason: String(err.message || err), reasonCodes, events };
|
|
228
|
-
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
229
|
-
else fail("proposal generation failed", err.message);
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
stageEvent(asJson, events, "propose", "ok", {
|
|
233
|
-
newCapabilities: (suggestion.newCapabilities || []).length,
|
|
234
|
-
removedCapabilities: (suggestion.removedCapabilities || []).length,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const schemaErrors = routed.providerResolved === "prompt" ? [] : validateSuggestion(suggestion);
|
|
238
|
-
const conflictErrors = routed.providerResolved === "prompt" ? [] : detectSuggestionConflicts(ctx.contract, suggestion);
|
|
239
|
-
if (schemaErrors.length || conflictErrors.length) {
|
|
240
|
-
const payload = {
|
|
241
|
-
ok: false,
|
|
242
|
-
error: "invalid_suggestion",
|
|
243
|
-
issues: [...schemaErrors, ...conflictErrors],
|
|
244
|
-
events,
|
|
245
|
-
};
|
|
246
|
-
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
247
|
-
else fail("suggestion invalid", payload.issues[0]);
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const snapshot = snapshotInferno(cwd);
|
|
252
|
-
let rolledBack = false;
|
|
253
|
-
let applyChanged = false;
|
|
254
|
-
let validationPassed = false;
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
if (dryRun) {
|
|
258
|
-
stageEvent(asJson, events, "apply", "info", { dryRun: true });
|
|
259
|
-
} else if (routed.providerResolved === "prompt") {
|
|
260
|
-
stageEvent(asJson, events, "apply", "warn", {
|
|
261
|
-
skipped: true,
|
|
262
|
-
reason: "prompt_fallback_requires_manual_step",
|
|
263
|
-
});
|
|
264
|
-
} else {
|
|
265
|
-
applyChanged = applyChanges({
|
|
266
|
-
cwd,
|
|
267
|
-
contract: ctx.contract,
|
|
268
|
-
capabilities: ctx.capabilities,
|
|
269
|
-
suggestion,
|
|
270
|
-
version: ctx.version,
|
|
271
|
-
quiet: asJson,
|
|
272
|
-
});
|
|
273
|
-
stageEvent(asJson, events, "apply", "ok", { changed: applyChanged });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
let check = runCliJson(["check", "--json"]);
|
|
277
|
-
if (process.env.INFERNO_TEST_FORCE_VALIDATE_FAIL === "1") {
|
|
278
|
-
check = { ok: false, data: { ok: false, errors: ["forced_validation_failure"] } };
|
|
279
|
-
}
|
|
280
|
-
if (!check.ok || !check.data?.ok) {
|
|
281
|
-
throw new Error(`validation_failed:${(check.data?.errors || []).join(",")}`);
|
|
282
|
-
}
|
|
283
|
-
validationPassed = true;
|
|
284
|
-
stageEvent(asJson, events, "validate", "ok");
|
|
285
|
-
} catch (err) {
|
|
286
|
-
stageEvent(asJson, events, "validate", "fail", { reason: String(err.message || err) });
|
|
287
|
-
if (!dryRun && !noRollback) {
|
|
288
|
-
restoreSnapshot(cwd, snapshot);
|
|
289
|
-
rolledBack = true;
|
|
290
|
-
stageEvent(asJson, events, "rollback", "ok");
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const artifact = {
|
|
295
|
-
task,
|
|
296
|
-
dryRun,
|
|
297
|
-
noRollback,
|
|
298
|
-
rolledBack,
|
|
299
|
-
applyChanged,
|
|
300
|
-
suggestionSummary: suggestion.summary || "",
|
|
301
|
-
touchedCapabilities: [
|
|
302
|
-
...(suggestion.newCapabilities || []).map((c) => c.id),
|
|
303
|
-
...(suggestion.removedCapabilities || []),
|
|
304
|
-
],
|
|
305
|
-
events,
|
|
306
|
-
};
|
|
307
|
-
const artifactPath = writeRunArtifact(cwd, artifact);
|
|
308
|
-
|
|
309
|
-
const payload = {
|
|
310
|
-
ok: validationPassed,
|
|
311
|
-
mode: "run",
|
|
312
|
-
task,
|
|
313
|
-
dryRun,
|
|
314
|
-
providerRequested,
|
|
315
|
-
providerResolved: routed.providerResolved,
|
|
316
|
-
ideDetected: routed.ideDetected,
|
|
317
|
-
agentAvailable: routed.agentAvailable,
|
|
318
|
-
reasonCodes: Array.from(new Set(reasonCodes)),
|
|
319
|
-
rolledBack,
|
|
320
|
-
applyChanged,
|
|
321
|
-
artifactPath,
|
|
322
|
-
events,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
if (asJson) {
|
|
326
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
327
|
-
process.exit(payload.ok ? 0 : 1);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
section("Result");
|
|
331
|
-
info(`task: ${gray(task)}`);
|
|
332
|
-
info(`artifact: ${gray(artifactPath)}`);
|
|
333
|
-
if (payload.ok) ok("run completed");
|
|
334
|
-
else warn("run rolled back after failed validation");
|
|
335
|
-
console.log();
|
|
336
|
-
process.exit(payload.ok ? 0 : 1);
|
|
337
|
-
}
|
|
338
|
-
|
package/lib/commands/status.mjs
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { header, ok, fail, warn, section, bold, cyan, yellow, gray, green, red, white } from "../ui/output.mjs";
|
|
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
|
-
}
|