infernoflow 0.32.8 → 0.32.9
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 +84 -255
- 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/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 +25 -130
- package/dist/lib/commands/cloud.mjs +5 -521
- package/dist/lib/commands/context.mjs +31 -287
- 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/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +203 -321
- 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 +23 -475
- 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/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/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 +5 -558
- 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/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- 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/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/templates/index.mjs +1 -131
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -72
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
- package/dist/templates/github-app/GITHUB_APP.md +67 -0
- package/dist/templates/github-app/app-manifest.json +20 -0
- package/package.json +1 -1
|
@@ -1,287 +1,31 @@
|
|
|
1
|
-
import fs from "node:
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
else if(cur&&line.startsWith("- ")) cur.items.push(line.replace("- ","").trim());
|
|
33
|
-
}
|
|
34
|
-
if(cur&&entries.length<max) entries.push(cur);
|
|
35
|
-
return entries.filter(e=>e.items.length>0);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function contextCommand(args) {
|
|
39
|
-
const has = (f) => args.includes(f);
|
|
40
|
-
const flag = (f) => { const i=args.indexOf(f); return i!==-1&&args[i+1]?args[i+1]:null; };
|
|
41
|
-
|
|
42
|
-
const intent = flag("--intent") || flag("-i");
|
|
43
|
-
const working = flag("--working") || flag("-w");
|
|
44
|
-
const decision = flag("--decision") || flag("-d");
|
|
45
|
-
const showOnly = has("--show") || has("-s");
|
|
46
|
-
const copyFlag = has("--copy") || has("-c");
|
|
47
|
-
const cursorFlag = has("--cursor");
|
|
48
|
-
const copilotFlag = has("--copilot");
|
|
49
|
-
const resetFlag= has("--reset");
|
|
50
|
-
const watchFlag = has("--watch");
|
|
51
|
-
const autoCommit = has("--auto-commit") || has("--auto-push");
|
|
52
|
-
const autoPush = has("--auto-push");
|
|
53
|
-
const watchInterval = parseInt(flag("--interval") || "30", 10) * 1000;
|
|
54
|
-
|
|
55
|
-
console.log("\n "+bold("��� infernoflow — context"));
|
|
56
|
-
console.log(" "+"─".repeat(50)+"\n");
|
|
57
|
-
|
|
58
|
-
if(!fs.existsSync(INFERNO_DIR)){
|
|
59
|
-
console.error(red(" ✘ inferno/ not found"));
|
|
60
|
-
console.error(gray(" → Run: infernoflow init\n"));
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const contract = readJSON(path.join(INFERNO_DIR,"contract.json"));
|
|
65
|
-
const capabilities = readJSON(path.join(INFERNO_DIR,"capabilities.json"));
|
|
66
|
-
const changelog = readFile(path.join(INFERNO_DIR,"CHANGELOG.md"));
|
|
67
|
-
|
|
68
|
-
if(!contract||!capabilities){
|
|
69
|
-
console.error(red(" ✘ Missing contract.json or capabilities.json\n"));
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let state = loadState();
|
|
74
|
-
if(resetFlag){ state={}; console.log(yellow(" ⚠ State reset\n")); }
|
|
75
|
-
if(intent) { state.intent=intent; state.intentUpdated=new Date().toISOString(); console.log(green(' ✔ Intent saved: "'+intent+'"')); }
|
|
76
|
-
if(working) { state.working=working; state.workingUpdated=new Date().toISOString(); console.log(green(' ✔ Working on: "'+working+'"')); }
|
|
77
|
-
if(decision) { if(!state.decisions) state.decisions=[]; state.decisions.push({text:decision,date:new Date().toISOString()}); console.log(green(' ✔ Decision recorded: "'+decision+'"')); }
|
|
78
|
-
if(intent||working||decision) saveState(state);
|
|
79
|
-
|
|
80
|
-
const capList = capabilities.capabilities||[];
|
|
81
|
-
const allInSync = capList.length===(contract.capabilities||[]).length;
|
|
82
|
-
const recent = parseChangelog(changelog,3);
|
|
83
|
-
const version = String(contract.policyVersion).replace(/^v/i,"");
|
|
84
|
-
const now = new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"});
|
|
85
|
-
const syncBadge = allInSync?"✓ validated":"⚠ out of sync";
|
|
86
|
-
const implementTask = state.intent || "describe the exact task to implement";
|
|
87
|
-
const implementInput = { task: implementTask, contract, caps: capabilities, scenarios: [], state };
|
|
88
|
-
const cursorPrompt = buildCursorImplementPrompt(implementInput);
|
|
89
|
-
const genericPrompt = buildGenericImplementPrompt(implementInput);
|
|
90
|
-
|
|
91
|
-
const capLines = capList.map(c=>"- **"+c.id+"** — "+c.title).join("\n");
|
|
92
|
-
const chgLines = recent.length>0 ? recent.map(e=>"### "+e.title+"\n"+e.items.map(i=>" - "+i).join("\n")).join("\n\n") : "_No recent changes_";
|
|
93
|
-
const intentLine = state.intent ? state.intent+" _("+fmtDate(state.intentUpdated)+")_" : "_Not set — run: infernoflow context --intent \"...\"_";
|
|
94
|
-
const workingLine = state.working ? state.working+" _("+fmtDate(state.workingUpdated)+")_" : "_Not set — run: infernoflow context --working \"...\"_";
|
|
95
|
-
const decLines = state.decisions&&state.decisions.length>0 ? state.decisions.slice(-5).map(d=>"- "+d.text+" _("+fmtDate(d.date)+")_").join("\n") : "_No decisions recorded_";
|
|
96
|
-
|
|
97
|
-
const md = [
|
|
98
|
-
"# Project Context — "+contract.policyId+" v"+version,
|
|
99
|
-
"> Generated by infernoflow | "+now+" | "+syncBadge,
|
|
100
|
-
"","---","",
|
|
101
|
-
"## What this system does","",capLines,"","---","",
|
|
102
|
-
"## Recent changes","",chgLines,"","---","",
|
|
103
|
-
"## Current state","",
|
|
104
|
-
"- **Capabilities:** "+capList.length,
|
|
105
|
-
"- **Version:** v"+version,
|
|
106
|
-
"- **Sync:** "+syncBadge,
|
|
107
|
-
"","---","",
|
|
108
|
-
"## What I am working on right now","",workingLine,"","---","",
|
|
109
|
-
"## Intent — what I want to build next","",intentLine,"","---","",
|
|
110
|
-
"## Decisions & notes","",decLines,"","---",
|
|
111
|
-
"",
|
|
112
|
-
"## Implementation Prompt Seed","",
|
|
113
|
-
"Use this to start coding immediately with an agent:","",
|
|
114
|
-
"```bash",
|
|
115
|
-
`infernoflow implement "${implementTask}" --mode both`,
|
|
116
|
-
"```",
|
|
117
|
-
"",
|
|
118
|
-
"### Cursor Agent Prompt","",
|
|
119
|
-
"```text",
|
|
120
|
-
cursorPrompt,
|
|
121
|
-
"```",
|
|
122
|
-
"",
|
|
123
|
-
"### Generic Agent Prompt","",
|
|
124
|
-
"```text",
|
|
125
|
-
genericPrompt,
|
|
126
|
-
"```",
|
|
127
|
-
"",
|
|
128
|
-
"---",
|
|
129
|
-
"_Paste this block at the start of any new AI session._"
|
|
130
|
-
].join("\n");
|
|
131
|
-
|
|
132
|
-
if(!showOnly){ fs.writeFileSync(CONTEXT_FILE,md,"utf8"); console.log(green("\n ✔ Context written → "+CONTEXT_FILE)); }
|
|
133
|
-
|
|
134
|
-
if(copyFlag){
|
|
135
|
-
const ok=copyToClipboard(md);
|
|
136
|
-
console.log(ok ? green(" ✔ Copied to clipboard — paste with Ctrl+V") : yellow(" ⚠ Clipboard copy failed — open inferno/CONTEXT.md manually"));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (cursorFlag) {
|
|
140
|
-
fs.writeFileSync(".cursorrules", md, "utf8");
|
|
141
|
-
console.log(green(" ✔ Written to .cursorrules — Cursor loads this automatically"));
|
|
142
|
-
}
|
|
143
|
-
if (copilotFlag) {
|
|
144
|
-
if (!fs.existsSync(".github")) fs.mkdirSync(".github");
|
|
145
|
-
fs.writeFileSync(".github/copilot-instructions.md", md, "utf8");
|
|
146
|
-
console.log(green(" ✔ Written to .github/copilot-instructions.md — Copilot loads this automatically"));
|
|
147
|
-
}
|
|
148
|
-
console.log("\n "+bold("Context Summary"));
|
|
149
|
-
console.log(" "+"─".repeat(50));
|
|
150
|
-
console.log(" Project "+contract.policyId+" — v"+version);
|
|
151
|
-
console.log(" Capabilities "+capList.length+" registered");
|
|
152
|
-
console.log(" Sync "+(allInSync?green("✓ in sync"):yellow("⚠ check needed")));
|
|
153
|
-
console.log(" Working on "+(state.working?cyan(state.working):gray("not set")));
|
|
154
|
-
console.log(" Intent "+(state.intent ?cyan(state.intent) :gray("not set")));
|
|
155
|
-
console.log(" Decisions "+(state.decisions?state.decisions.length:0)+" recorded\n");
|
|
156
|
-
console.log(" "+bold("Implementation Prompt"));
|
|
157
|
-
console.log(" "+cyan("→")+" Run "+cyan(`infernoflow implement "${implementTask}" --mode both`)+"\n");
|
|
158
|
-
|
|
159
|
-
if(copyFlag){
|
|
160
|
-
console.log(" "+bold("Ready to use:"));
|
|
161
|
-
console.log(" "+cyan("→")+" Paste into Claude / Cursor / Copilot with "+cyan("Ctrl+V")+"\n");
|
|
162
|
-
} else {
|
|
163
|
-
console.log(" "+bold("Ready to use:"));
|
|
164
|
-
console.log(" "+cyan("1.")+" Open "+cyan("inferno/CONTEXT.md"));
|
|
165
|
-
console.log(" "+cyan("2.")+" Copy everything");
|
|
166
|
-
console.log(" "+cyan("3.")+" Paste at the start of your next AI session");
|
|
167
|
-
console.log(" "+gray(" tip: use --copy to skip steps 1-2 automatically")+"\n");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ── Watch mode ────────────────────────────────────────────────────────────
|
|
171
|
-
if (watchFlag) {
|
|
172
|
-
const modeLabel = autoPush ? "auto-push" : autoCommit ? "auto-commit" : "watch";
|
|
173
|
-
console.log(" " + cyan("👁 Watch mode active") + gray(
|
|
174
|
-
` — polling every ${watchInterval / 1000}s` +
|
|
175
|
-
(autoPush ? " · will commit + push on change" : autoCommit ? " · will commit on change" : "")
|
|
176
|
-
));
|
|
177
|
-
console.log(" " + gray("Press Ctrl+C to stop\n"));
|
|
178
|
-
|
|
179
|
-
let lastChangedFiles = "";
|
|
180
|
-
let lastCommittedContent = null;
|
|
181
|
-
|
|
182
|
-
// ── git helpers ──────────────────────────────────────────────────────
|
|
183
|
-
function gitRun(cmd) {
|
|
184
|
-
try {
|
|
185
|
-
execSync(cmd, { cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
186
|
-
return true;
|
|
187
|
-
} catch { return false; }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function gitIsCleanFor(filePath) {
|
|
191
|
-
// Returns true if the file has no staged/unstaged changes (nothing to commit)
|
|
192
|
-
try {
|
|
193
|
-
const out = execSync(`git status --porcelain "${filePath}"`, {
|
|
194
|
-
cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
|
|
195
|
-
}).trim();
|
|
196
|
-
return out === "";
|
|
197
|
-
} catch { return true; }
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function commitContext(contextPath, affectedCaps, changedCount) {
|
|
201
|
-
const caps = affectedCaps.length > 0 ? affectedCaps.slice(0, 3).join(", ") : `${changedCount} files`;
|
|
202
|
-
const msg = `chore: update context [${caps}]`;
|
|
203
|
-
const staged = gitRun(`git add "${contextPath}"`);
|
|
204
|
-
if (!staged) return { ok: false, reason: "git add failed" };
|
|
205
|
-
if (gitIsCleanFor(contextPath)) return { ok: false, reason: "nothing to commit" };
|
|
206
|
-
const committed = gitRun(`git commit -m "${msg}"`);
|
|
207
|
-
if (!committed) return { ok: false, reason: "git commit failed (lock?)" };
|
|
208
|
-
return { ok: true, msg };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function pushContext() {
|
|
212
|
-
const ok = gitRun("git push");
|
|
213
|
-
return ok;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ── poll loop ────────────────────────────────────────────────────────
|
|
217
|
-
const poll = async () => {
|
|
218
|
-
try {
|
|
219
|
-
const cwd = process.cwd();
|
|
220
|
-
const drift = detectDrift(cwd, { sinceCommits: 1 });
|
|
221
|
-
const changedKey = drift.changedFiles.sort().join("|");
|
|
222
|
-
|
|
223
|
-
if (changedKey === lastChangedFiles) return;
|
|
224
|
-
lastChangedFiles = changedKey;
|
|
225
|
-
if (drift.changedFiles.length === 0) return;
|
|
226
|
-
|
|
227
|
-
// Update "working" field
|
|
228
|
-
const affected = drift.affectedCapabilities.map(c => c.id);
|
|
229
|
-
const newWorking = affected.length > 0
|
|
230
|
-
? `Working on: ${affected.join(", ")} (${drift.changedFiles.length} files changed)`
|
|
231
|
-
: `${drift.changedFiles.length} files changed — no capability match yet`;
|
|
232
|
-
|
|
233
|
-
const currentState = loadState();
|
|
234
|
-
if (currentState.working !== newWorking) {
|
|
235
|
-
currentState.working = newWorking;
|
|
236
|
-
currentState.workingUpdated = new Date().toISOString();
|
|
237
|
-
saveState(currentState);
|
|
238
|
-
|
|
239
|
-
// Regenerate CONTEXT.md silently
|
|
240
|
-
await contextCommand(args.filter(a => a !== "--watch" && a !== "--auto-commit" && a !== "--auto-push"));
|
|
241
|
-
|
|
242
|
-
const newContent = readFile(CONTEXT_FILE);
|
|
243
|
-
const ts = new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
244
|
-
|
|
245
|
-
process.stderr.write(
|
|
246
|
-
`\n ${green("✔")} [${ts}] Context updated — ${affected.length} capabilities affected\n` +
|
|
247
|
-
` ${gray(drift.changedFiles.slice(0, 3).join(", ") + (drift.changedFiles.length > 3 ? ` +${drift.changedFiles.length - 3} more` : ""))}\n`
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
// Auto-commit if enabled and content actually changed
|
|
251
|
-
if (autoCommit && newContent !== lastCommittedContent) {
|
|
252
|
-
lastCommittedContent = newContent;
|
|
253
|
-
const result = commitContext(CONTEXT_FILE, affected, drift.changedFiles.length);
|
|
254
|
-
if (result.ok) {
|
|
255
|
-
process.stderr.write(` ${green("✔")} Committed: ${gray(result.msg)}\n`);
|
|
256
|
-
if (autoPush) {
|
|
257
|
-
const pushed = pushContext();
|
|
258
|
-
process.stderr.write(
|
|
259
|
-
pushed
|
|
260
|
-
? ` ${green("✔")} Pushed to origin\n`
|
|
261
|
-
: ` ${yellow("⚠")} Push failed — will retry next change\n`
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
process.stderr.write(` ${yellow("⚠")} Commit skipped: ${gray(result.reason)}\n`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
} catch {
|
|
270
|
-
// Silent — watch mode never crashes
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// Poll immediately then on interval
|
|
275
|
-
await poll();
|
|
276
|
-
const timer = setInterval(poll, watchInterval);
|
|
277
|
-
|
|
278
|
-
process.on("SIGINT", () => {
|
|
279
|
-
clearInterval(timer);
|
|
280
|
-
process.stderr.write("\n " + gray("Watch stopped.\n\n"));
|
|
281
|
-
process.exit(0);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Prevent Node from exiting
|
|
285
|
-
await new Promise(() => {});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
1
|
+
import g from"node:fs";import b from"node:path";import{execSync as h}from"node:child_process";import{bold as I,gray as d,cyan as l,red as q,green as a,yellow as F}from"../ui/output.mjs";import{buildCursorImplementPrompt as wt,buildGenericImplementPrompt as yt}from"../ui/prompts.mjs";import{detectDrift as kt}from"../git/detect-drift.mjs";function Ct(e){try{const n=process.platform;if(n==="win32")h("clip",{input:e});else if(n==="darwin")h("pbcopy",{input:e});else try{h("xclip -selection clipboard",{input:e})}catch{h("xsel --clipboard --input",{input:e})}return!0}catch{return!1}}const w="inferno",P=b.join(w,"CONTEXT.md"),z=b.join(w,"context-state.json");function Q(e){try{return JSON.parse(g.readFileSync(e,"utf8"))}catch{return null}}function W(e){try{return g.readFileSync(e,"utf8")}catch{return null}}function Y(){const e=W(z);if(!e)return{};try{return JSON.parse(e)}catch{return{}}}function Z(e){g.writeFileSync(z,JSON.stringify(e,null,2),"utf8")}function E(e){return e?new Date(e).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}):"unknown"}function St(e,n){if(!e)return[];const i=[];let c=null;for(const r of e.split(`
|
|
2
|
+
`))if(r.startsWith("## ")){if(c&&i.length<n&&i.push(c),i.length>=n)break;c={title:r.replace("## ","").trim(),items:[]}}else c&&r.startsWith("- ")&&c.items.push(r.replace("- ","").trim());return c&&i.length<n&&i.push(c),i.filter(r=>r.items.length>0)}async function bt(e){const n=o=>e.includes(o),i=o=>{const u=e.indexOf(o);return u!==-1&&e[u+1]?e[u+1]:null},c=i("--intent")||i("-i"),r=i("--working")||i("-w"),$=i("--decision")||i("-d"),tt=n("--show")||n("-s"),G=n("--copy")||n("-c"),et=n("--cursor"),nt=n("--copilot"),ot=n("--reset"),it=n("--watch"),N=n("--auto-commit")||n("--auto-push"),v=n("--auto-push"),R=parseInt(i("--interval")||"30",10)*1e3;console.log(`
|
|
3
|
+
`+I("\uFFFD\uFFFD\uFFFD infernoflow \u2014 context")),console.log(" "+"\u2500".repeat(50)+`
|
|
4
|
+
`),g.existsSync(w)||(console.error(q(" \u2718 inferno/ not found")),console.error(d(` \u2192 Run: infernoflow init
|
|
5
|
+
`)),process.exit(1));const f=Q(b.join(w,"contract.json")),D=Q(b.join(w,"capabilities.json")),st=W(b.join(w,"CHANGELOG.md"));(!f||!D)&&(console.error(q(` \u2718 Missing contract.json or capabilities.json
|
|
6
|
+
`)),process.exit(1));let t=Y();ot&&(t={},console.log(F(` \u26A0 State reset
|
|
7
|
+
`))),c&&(t.intent=c,t.intentUpdated=new Date().toISOString(),console.log(a(' \u2714 Intent saved: "'+c+'"'))),r&&(t.working=r,t.workingUpdated=new Date().toISOString(),console.log(a(' \u2714 Working on: "'+r+'"'))),$&&(t.decisions||(t.decisions=[]),t.decisions.push({text:$,date:new Date().toISOString()}),console.log(a(' \u2714 Decision recorded: "'+$+'"'))),(c||r||$)&&Z(t);const j=D.capabilities||[],A=j.length===(f.capabilities||[]).length,U=St(st,3),T=String(f.policyVersion).replace(/^v/i,""),ct=new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}),B=A?"\u2713 validated":"\u26A0 out of sync",L=t.intent||"describe the exact task to implement",J={task:L,contract:f,caps:D,scenarios:[],state:t},rt=wt(J),lt=yt(J),at=j.map(o=>"- **"+o.id+"** \u2014 "+o.title).join(`
|
|
8
|
+
`),dt=U.length>0?U.map(o=>"### "+o.title+`
|
|
9
|
+
`+o.items.map(u=>" - "+u).join(`
|
|
10
|
+
`)).join(`
|
|
11
|
+
|
|
12
|
+
`):"_No recent changes_",pt=t.intent?t.intent+" _("+E(t.intentUpdated)+")_":'_Not set \u2014 run: infernoflow context --intent "..."_',gt=t.working?t.working+" _("+E(t.workingUpdated)+")_":'_Not set \u2014 run: infernoflow context --working "..."_',ut=t.decisions&&t.decisions.length>0?t.decisions.slice(-5).map(o=>"- "+o.text+" _("+E(o.date)+")_").join(`
|
|
13
|
+
`):"_No decisions recorded_",x=["# Project Context \u2014 "+f.policyId+" v"+T,"> Generated by infernoflow | "+ct+" | "+B,"","---","","## What this system does","",at,"","---","","## Recent changes","",dt,"","---","","## Current state","","- **Capabilities:** "+j.length,"- **Version:** v"+T,"- **Sync:** "+B,"","---","","## What I am working on right now","",gt,"","---","","## Intent \u2014 what I want to build next","",pt,"","---","","## Decisions & notes","",ut,"","---","","## Implementation Prompt Seed","","Use this to start coding immediately with an agent:","","```bash",`infernoflow implement "${L}" --mode both`,"```","","### Cursor Agent Prompt","","```text",rt,"```","","### Generic Agent Prompt","","```text",lt,"```","","---","_Paste this block at the start of any new AI session._"].join(`
|
|
14
|
+
`);if(tt||(g.writeFileSync(P,x,"utf8"),console.log(a(`
|
|
15
|
+
\u2714 Context written \u2192 `+P))),G){const o=Ct(x);console.log(o?a(" \u2714 Copied to clipboard \u2014 paste with Ctrl+V"):F(" \u26A0 Clipboard copy failed \u2014 open inferno/CONTEXT.md manually"))}if(et&&(g.writeFileSync(".cursorrules",x,"utf8"),console.log(a(" \u2714 Written to .cursorrules \u2014 Cursor loads this automatically"))),nt&&(g.existsSync(".github")||g.mkdirSync(".github"),g.writeFileSync(".github/copilot-instructions.md",x,"utf8"),console.log(a(" \u2714 Written to .github/copilot-instructions.md \u2014 Copilot loads this automatically"))),console.log(`
|
|
16
|
+
`+I("Context Summary")),console.log(" "+"\u2500".repeat(50)),console.log(" Project "+f.policyId+" \u2014 v"+T),console.log(" Capabilities "+j.length+" registered"),console.log(" Sync "+(A?a("\u2713 in sync"):F("\u26A0 check needed"))),console.log(" Working on "+(t.working?l(t.working):d("not set"))),console.log(" Intent "+(t.intent?l(t.intent):d("not set"))),console.log(" Decisions "+(t.decisions?t.decisions.length:0)+` recorded
|
|
17
|
+
`),console.log(" "+I("Implementation Prompt")),console.log(" "+l("\u2192")+" Run "+l(`infernoflow implement "${L}" --mode both`)+`
|
|
18
|
+
`),G?(console.log(" "+I("Ready to use:")),console.log(" "+l("\u2192")+" Paste into Claude / Cursor / Copilot with "+l("Ctrl+V")+`
|
|
19
|
+
`)):(console.log(" "+I("Ready to use:")),console.log(" "+l("1.")+" Open "+l("inferno/CONTEXT.md")),console.log(" "+l("2.")+" Copy everything"),console.log(" "+l("3.")+" Paste at the start of your next AI session"),console.log(" "+d(" tip: use --copy to skip steps 1-2 automatically")+`
|
|
20
|
+
`)),it){let _=function(p){try{return h(p,{cwd:process.cwd(),encoding:"utf8",stdio:["ignore","pipe","pipe"]}),!0}catch{return!1}},X=function(p){try{return h(`git status --porcelain "${p}"`,{cwd:process.cwd(),encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()===""}catch{return!0}},H=function(p,s,O){const k=`chore: update context [${s.length>0?s.slice(0,3).join(", "):`${O} files`}]`;return _(`git add "${p}"`)?X(p)?{ok:!1,reason:"nothing to commit"}:_(`git commit -m "${k}"`)?{ok:!0,msg:k}:{ok:!1,reason:"git commit failed (lock?)"}:{ok:!1,reason:"git add failed"}},K=function(){return _("git push")};var It=_,Ft=X,$t=H,jt=K;const o=v?"auto-push":N?"auto-commit":"watch";console.log(" "+l("\u{1F441} Watch mode active")+d(` \u2014 polling every ${R/1e3}s`+(v?" \xB7 will commit + push on change":N?" \xB7 will commit on change":""))),console.log(" "+d(`Press Ctrl+C to stop
|
|
21
|
+
`));let u="",V=null;const M=async()=>{try{const p=process.cwd(),s=kt(p,{sinceCommits:1}),O=s.changedFiles.sort().join("|");if(O===u||(u=O,s.changedFiles.length===0))return;const y=s.affectedCapabilities.map(S=>S.id),k=y.length>0?`Working on: ${y.join(", ")} (${s.changedFiles.length} files changed)`:`${s.changedFiles.length} files changed \u2014 no capability match yet`,C=Y();if(C.working!==k){C.working=k,C.workingUpdated=new Date().toISOString(),Z(C),await bt(e.filter(m=>m!=="--watch"&&m!=="--auto-commit"&&m!=="--auto-push"));const S=W(P),ft=new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",second:"2-digit"});if(process.stderr.write(`
|
|
22
|
+
${a("\u2714")} [${ft}] Context updated \u2014 ${y.length} capabilities affected
|
|
23
|
+
${d(s.changedFiles.slice(0,3).join(", ")+(s.changedFiles.length>3?` +${s.changedFiles.length-3} more`:""))}
|
|
24
|
+
`),N&&S!==V){V=S;const m=H(P,y,s.changedFiles.length);if(m.ok){if(process.stderr.write(` ${a("\u2714")} Committed: ${d(m.msg)}
|
|
25
|
+
`),v){const ht=K();process.stderr.write(ht?` ${a("\u2714")} Pushed to origin
|
|
26
|
+
`:` ${F("\u26A0")} Push failed \u2014 will retry next change
|
|
27
|
+
`)}}else process.stderr.write(` ${F("\u26A0")} Commit skipped: ${d(m.reason)}
|
|
28
|
+
`)}}}catch{}};await M();const mt=setInterval(M,R);process.on("SIGINT",()=>{clearInterval(mt),process.stderr.write(`
|
|
29
|
+
`+d(`Watch stopped.
|
|
30
|
+
|
|
31
|
+
`)),process.exit(0)}),await new Promise(()=>{})}}export{bt as contextCommand};
|
|
@@ -1,282 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Maps test files to capabilities via fuzzy name matching.
|
|
5
|
-
* Shows which capabilities have test coverage and which don't.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* infernoflow coverage Print coverage table
|
|
9
|
-
* infernoflow coverage --json Machine-readable output
|
|
10
|
-
* infernoflow coverage --dir src/ Extra dirs to scan (default: project root)
|
|
11
|
-
* infernoflow coverage --fail-below 50 Exit 1 if coverage < N%
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import * as fs from "node:fs";
|
|
15
|
-
import * as path from "node:path";
|
|
16
|
-
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
17
|
-
|
|
18
|
-
// ─── test pattern extractors ─────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const TEST_PATTERNS = [
|
|
21
|
-
// Jest / Vitest — it("...", …) test("...", …) describe("...", …)
|
|
22
|
-
{ regex: /(?:it|test|describe)\s*\(\s*["'`]([^"'`]+)["'`]/g, lang: "js" },
|
|
23
|
-
// Pytest — def test_something
|
|
24
|
-
{ regex: /def\s+(test_[\w_]+)\s*\(/g, lang: "py" },
|
|
25
|
-
// RSpec — describe/it "..."
|
|
26
|
-
{ regex: /(?:describe|it)\s+["']([^"']+)["']/g, lang: "rb" },
|
|
27
|
-
// Go — func TestXxx(
|
|
28
|
-
{ regex: /func\s+(Test\w+)\s*\(/g, lang: "go" },
|
|
29
|
-
// Rust — #[test] fn xxx
|
|
30
|
-
{ regex: /#\[test\]\s*\n\s*(?:async\s+)?fn\s+(\w+)/g, lang: "rs" },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const TEST_FILE_GLOBS = [
|
|
34
|
-
/\.(test|spec)\.[jt]sx?$/, // foo.test.ts
|
|
35
|
-
/__tests__/, // __tests__/foo.js
|
|
36
|
-
/\.test\.py$/, // test_foo.py
|
|
37
|
-
/^test_.*\.py$/, // test_foo.py (basename)
|
|
38
|
-
/_spec\.rb$/, // foo_spec.rb
|
|
39
|
-
/\/spec\//, // spec/ directory
|
|
40
|
-
/_test\.go$/, // foo_test.go
|
|
41
|
-
/_test\.rs$/, // foo_test.rs
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const SKIP_DIRS = new Set([
|
|
45
|
-
"node_modules", ".git", "dist", "build", "out", ".next",
|
|
46
|
-
"coverage", ".nyc_output", "__pycache__", ".pytest_cache",
|
|
47
|
-
"vendor", "tmp", ".turbo",
|
|
48
|
-
]);
|
|
49
|
-
|
|
50
|
-
// ─── file walker ─────────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
function* walkFiles(dir) {
|
|
53
|
-
let entries;
|
|
54
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
55
|
-
catch { return; }
|
|
56
|
-
|
|
57
|
-
for (const e of entries) {
|
|
58
|
-
if (e.isDirectory()) {
|
|
59
|
-
if (!SKIP_DIRS.has(e.name)) yield* walkFiles(path.join(dir, e.name));
|
|
60
|
-
} else if (e.isFile()) {
|
|
61
|
-
yield path.join(dir, e.name);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function isTestFile(filePath) {
|
|
67
|
-
const basename = path.basename(filePath);
|
|
68
|
-
return TEST_FILE_GLOBS.some(re => re.test(filePath) || re.test(basename));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ─── test name extractor ─────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
function extractTestNames(filePath) {
|
|
74
|
-
let src;
|
|
75
|
-
try { src = fs.readFileSync(filePath, "utf8"); }
|
|
76
|
-
catch { return []; }
|
|
77
|
-
|
|
78
|
-
const names = new Set();
|
|
79
|
-
for (const { regex } of TEST_PATTERNS) {
|
|
80
|
-
const r = new RegExp(regex.source, regex.flags);
|
|
81
|
-
let m;
|
|
82
|
-
while ((m = r.exec(src)) !== null) {
|
|
83
|
-
names.add(m[1].trim());
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return [...names];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─── fuzzy matcher ───────────────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Tokenise a string: split on spaces, hyphens, underscores, camelCase.
|
|
93
|
-
* Returns an array of lowercase tokens.
|
|
94
|
-
*/
|
|
95
|
-
function tokenise(str) {
|
|
96
|
-
return str
|
|
97
|
-
.replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase split
|
|
98
|
-
.toLowerCase()
|
|
99
|
-
.split(/[\s_\-/]+/)
|
|
100
|
-
.filter(Boolean);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Jaccard-like overlap score between two token sets.
|
|
105
|
-
* Returns a value in [0, 1].
|
|
106
|
-
*/
|
|
107
|
-
function overlapScore(a, b) {
|
|
108
|
-
const setA = new Set(a);
|
|
109
|
-
const setB = new Set(b);
|
|
110
|
-
let common = 0;
|
|
111
|
-
for (const t of setA) if (setB.has(t)) common++;
|
|
112
|
-
const union = setA.size + setB.size - common;
|
|
113
|
-
return union === 0 ? 0 : common / union;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Best match score between a test name and a capability (id + name).
|
|
118
|
-
*/
|
|
119
|
-
function matchScore(testName, cap) {
|
|
120
|
-
const testTokens = tokenise(testName);
|
|
121
|
-
const idTokens = tokenise(cap.id || "");
|
|
122
|
-
const nameTokens = tokenise(cap.name || "");
|
|
123
|
-
return Math.max(
|
|
124
|
-
overlapScore(testTokens, idTokens),
|
|
125
|
-
overlapScore(testTokens, nameTokens),
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ─── main scanner ─────────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
function scanTestFiles(dirs) {
|
|
132
|
-
const testFiles = [];
|
|
133
|
-
for (const dir of dirs) {
|
|
134
|
-
for (const f of walkFiles(dir)) {
|
|
135
|
-
if (isTestFile(f)) testFiles.push(f);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return testFiles;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function buildTestIndex(testFiles) {
|
|
142
|
-
// Returns: Map<testName, filePath>
|
|
143
|
-
const index = new Map();
|
|
144
|
-
for (const f of testFiles) {
|
|
145
|
-
for (const name of extractTestNames(f)) {
|
|
146
|
-
if (!index.has(name)) index.set(name, f);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return index;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Returns: Map<capId, { matched: [{testName, file, score}], score: number }> */
|
|
153
|
-
function mapTestsToCaps(capabilities, testIndex, threshold = 0.25) {
|
|
154
|
-
const result = new Map();
|
|
155
|
-
|
|
156
|
-
for (const cap of capabilities) {
|
|
157
|
-
const hits = [];
|
|
158
|
-
for (const [testName, file] of testIndex) {
|
|
159
|
-
const score = matchScore(testName, cap);
|
|
160
|
-
if (score >= threshold) {
|
|
161
|
-
hits.push({ testName, file: path.relative(process.cwd(), file), score });
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
hits.sort((a, b) => b.score - a.score);
|
|
165
|
-
result.set(cap.id, { cap, hits });
|
|
166
|
-
}
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ─── reporters ────────────────────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
function bar(pct, width = 20) {
|
|
173
|
-
const filled = Math.round((pct / 100) * width);
|
|
174
|
-
const colour = pct >= 75 ? green : pct >= 40 ? yellow : red;
|
|
175
|
-
return colour("█".repeat(filled)) + gray("░".repeat(width - filled));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function printTable(coverageMap) {
|
|
179
|
-
const covered = [...coverageMap.values()].filter(v => v.hits.length > 0).length;
|
|
180
|
-
const total = coverageMap.size;
|
|
181
|
-
const pct = total === 0 ? 0 : Math.round((covered / total) * 100);
|
|
182
|
-
|
|
183
|
-
console.log();
|
|
184
|
-
console.log(bold(" Capability Coverage"));
|
|
185
|
-
console.log(gray(" ─────────────────────────────────────────────────────────────"));
|
|
186
|
-
console.log(
|
|
187
|
-
gray(" ") +
|
|
188
|
-
bold(cyan("Capability".padEnd(32))) +
|
|
189
|
-
bold(cyan("Tests".padEnd(8))) +
|
|
190
|
-
bold(cyan("Top match"))
|
|
191
|
-
);
|
|
192
|
-
console.log(gray(" ─────────────────────────────────────────────────────────────"));
|
|
193
|
-
|
|
194
|
-
for (const [, { cap, hits }] of coverageMap) {
|
|
195
|
-
const status = hits.length > 0 ? green("✔") : red("✗");
|
|
196
|
-
const topName = hits[0] ? gray(` ${hits[0].testName.slice(0, 42)}`) : "";
|
|
197
|
-
const count = hits.length === 0 ? red("0") : green(String(hits.length));
|
|
198
|
-
console.log(
|
|
199
|
-
` ${status} ${cap.id.padEnd(30)} ${count.padEnd(6)} ${topName}`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
console.log(gray(" ─────────────────────────────────────────────────────────────"));
|
|
204
|
-
console.log();
|
|
205
|
-
console.log(` ${bar(pct)} ${bold(pct + "%")} (${covered}/${total} capabilities covered)`);
|
|
206
|
-
console.log();
|
|
207
|
-
|
|
208
|
-
if (total > 0 && covered < total) {
|
|
209
|
-
const uncovered = [...coverageMap.values()]
|
|
210
|
-
.filter(v => v.hits.length === 0)
|
|
211
|
-
.map(v => v.cap.id);
|
|
212
|
-
console.log(yellow(` ⚠ Uncovered: ${uncovered.join(", ")}`));
|
|
213
|
-
console.log();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ─── entry point ─────────────────────────────────────────────────────────────
|
|
218
|
-
|
|
219
|
-
export async function coverageCommand(rawArgs) {
|
|
220
|
-
const args = rawArgs || [];
|
|
221
|
-
const jsonMode = args.includes("--json");
|
|
222
|
-
const dirIdx = args.indexOf("--dir");
|
|
223
|
-
const extraDirs = dirIdx !== -1 ? [args[dirIdx + 1]] : [];
|
|
224
|
-
const failIdx = args.indexOf("--fail-below");
|
|
225
|
-
const failBelow = failIdx !== -1 ? Number(args[failIdx + 1]) : null;
|
|
226
|
-
const threshold = (() => {
|
|
227
|
-
const i = args.indexOf("--threshold");
|
|
228
|
-
return i !== -1 ? Number(args[i + 1]) : 0.25;
|
|
229
|
-
})();
|
|
230
|
-
|
|
231
|
-
const cwd = process.cwd();
|
|
232
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
233
|
-
|
|
234
|
-
// Load capabilities
|
|
235
|
-
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
236
|
-
if (!fs.existsSync(capsPath)) {
|
|
237
|
-
console.error(red("✗ inferno/capabilities.json not found — run `infernoflow init` first."));
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
240
|
-
let capabilities;
|
|
241
|
-
try { capabilities = JSON.parse(fs.readFileSync(capsPath, "utf8")); }
|
|
242
|
-
catch (e) { console.error(red("✗ Failed to parse capabilities.json: " + e.message)); process.exit(1); }
|
|
243
|
-
|
|
244
|
-
if (!Array.isArray(capabilities) || capabilities.length === 0) {
|
|
245
|
-
console.log(yellow("No capabilities found."));
|
|
246
|
-
process.exit(0);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Scan test files
|
|
250
|
-
const scanDirs = [cwd, ...extraDirs];
|
|
251
|
-
if (!jsonMode) process.stdout.write(gray(" Scanning test files…"));
|
|
252
|
-
const testFiles = scanTestFiles(scanDirs);
|
|
253
|
-
if (!jsonMode) process.stdout.write(`\r Found ${testFiles.length} test file(s). \n`);
|
|
254
|
-
|
|
255
|
-
const testIndex = buildTestIndex(testFiles);
|
|
256
|
-
const coverageMap = mapTestsToCaps(capabilities, testIndex, threshold);
|
|
257
|
-
|
|
258
|
-
const covered = [...coverageMap.values()].filter(v => v.hits.length > 0).length;
|
|
259
|
-
const total = coverageMap.size;
|
|
260
|
-
const pct = total === 0 ? 0 : Math.round((covered / total) * 100);
|
|
261
|
-
|
|
262
|
-
if (jsonMode) {
|
|
263
|
-
const out = {
|
|
264
|
-
summary: { covered, total, pct, testFiles: testFiles.length },
|
|
265
|
-
capabilities: [...coverageMap.entries()].map(([id, { cap, hits }]) => ({
|
|
266
|
-
id,
|
|
267
|
-
name: cap.name,
|
|
268
|
-
covered: hits.length > 0,
|
|
269
|
-
testCount: hits.length,
|
|
270
|
-
topTests: hits.slice(0, 3).map(h => ({ name: h.testName, file: h.file, score: +h.score.toFixed(3) })),
|
|
271
|
-
})),
|
|
272
|
-
};
|
|
273
|
-
console.log(JSON.stringify(out, null, 2));
|
|
274
|
-
} else {
|
|
275
|
-
printTable(coverageMap);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (failBelow !== null && pct < failBelow) {
|
|
279
|
-
if (!jsonMode) console.error(red(`✗ Coverage ${pct}% is below threshold ${failBelow}%`));
|
|
280
|
-
process.exit(1);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
1
|
+
import*as h from"node:fs";import*as u from"node:path";import{bold as p,cyan as T,gray as f,green as $,yellow as S,red as d}from"../ui/output.mjs";const A=[{regex:/(?:it|test|describe)\s*\(\s*["'`]([^"'`]+)["'`]/g,lang:"js"},{regex:/def\s+(test_[\w_]+)\s*\(/g,lang:"py"},{regex:/(?:describe|it)\s+["']([^"']+)["']/g,lang:"rb"},{regex:/func\s+(Test\w+)\s*\(/g,lang:"go"},{regex:/#\[test\]\s*\n\s*(?:async\s+)?fn\s+(\w+)/g,lang:"rs"}],D=[/\.(test|spec)\.[jt]sx?$/,/__tests__/,/\.test\.py$/,/^test_.*\.py$/,/_spec\.rb$/,/\/spec\//,/_test\.go$/,/_test\.rs$/],M=new Set(["node_modules",".git","dist","build","out",".next","coverage",".nyc_output","__pycache__",".pytest_cache","vendor","tmp",".turbo"]);function*N(s){let t;try{t=h.readdirSync(s,{withFileTypes:!0})}catch{return}for(const e of t)e.isDirectory()?M.has(e.name)||(yield*N(u.join(s,e.name))):e.isFile()&&(yield u.join(s,e.name))}function B(s){const t=u.basename(s);return D.some(e=>e.test(s)||e.test(t))}function L(s){let t;try{t=h.readFileSync(s,"utf8")}catch{return[]}const e=new Set;for(const{regex:o}of A){const c=new RegExp(o.source,o.flags);let n;for(;(n=c.exec(t))!==null;)e.add(n[1].trim())}return[...e]}function v(s){return s.replace(/([a-z])([A-Z])/g,"$1 $2").toLowerCase().split(/[\s_\-/]+/).filter(Boolean)}function E(s,t){const e=new Set(s),o=new Set(t);let c=0;for(const r of e)o.has(r)&&c++;const n=e.size+o.size-c;return n===0?0:c/n}function R(s,t){const e=v(s),o=v(t.id||""),c=v(t.name||"");return Math.max(E(e,o),E(e,c))}function J(s){const t=[];for(const e of s)for(const o of N(e))B(o)&&t.push(o);return t}function P(s){const t=new Map;for(const e of s)for(const o of L(e))t.has(o)||t.set(o,e);return t}function G(s,t,e=.25){const o=new Map;for(const c of s){const n=[];for(const[r,l]of t){const a=R(r,c);a>=e&&n.push({testName:r,file:u.relative(process.cwd(),l),score:a})}n.sort((r,l)=>l.score-r.score),o.set(c.id,{cap:c,hits:n})}return o}function K(s,t=20){const e=Math.round(s/100*t);return(s>=75?$:s>=40?S:d)("\u2588".repeat(e))+f("\u2591".repeat(t-e))}function U(s){const t=[...s.values()].filter(c=>c.hits.length>0).length,e=s.size,o=e===0?0:Math.round(t/e*100);console.log(),console.log(p(" Capability Coverage")),console.log(f(" \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\u2500\u2500\u2500")),console.log(f(" ")+p(T("Capability".padEnd(32)))+p(T("Tests".padEnd(8)))+p(T("Top match"))),console.log(f(" \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\u2500\u2500\u2500"));for(const[,{cap:c,hits:n}]of s){const r=n.length>0?$("\u2714"):d("\u2717"),l=n[0]?f(` ${n[0].testName.slice(0,42)}`):"",a=n.length===0?d("0"):$(String(n.length));console.log(` ${r} ${c.id.padEnd(30)} ${a.padEnd(6)} ${l}`)}if(console.log(f(" \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\u2500\u2500\u2500")),console.log(),console.log(` ${K(o)} ${p(o+"%")} (${t}/${e} capabilities covered)`),console.log(),e>0&&t<e){const c=[...s.values()].filter(n=>n.hits.length===0).map(n=>n.cap.id);console.log(S(` \u26A0 Uncovered: ${c.join(", ")}`)),console.log()}}async function q(s){const t=s||[],e=t.includes("--json"),o=t.indexOf("--dir"),c=o!==-1?[t[o+1]]:[],n=t.indexOf("--fail-below"),r=n!==-1?Number(t[n+1]):null,l=(()=>{const i=t.indexOf("--threshold");return i!==-1?Number(t[i+1]):.25})(),a=process.cwd(),C=u.join(a,"inferno"),F=u.join(C,"capabilities.json");h.existsSync(F)||(console.error(d("\u2717 inferno/capabilities.json not found \u2014 run `infernoflow init` first.")),process.exit(1));let g;try{g=JSON.parse(h.readFileSync(F,"utf8"))}catch(i){console.error(d("\u2717 Failed to parse capabilities.json: "+i.message)),process.exit(1)}(!Array.isArray(g)||g.length===0)&&(console.log(S("No capabilities found.")),process.exit(0));const I=[a,...c];e||process.stdout.write(f(" Scanning test files\u2026"));const x=J(I);e||process.stdout.write(`\r Found ${x.length} test file(s).
|
|
2
|
+
`);const O=P(x),m=G(g,O,l),j=[...m.values()].filter(i=>i.hits.length>0).length,y=m.size,b=y===0?0:Math.round(j/y*100);if(e){const i={summary:{covered:j,total:y,pct:b,testFiles:x.length},capabilities:[...m.entries()].map(([k,{cap:z,hits:w}])=>({id:k,name:z.name,covered:w.length>0,testCount:w.length,topTests:w.slice(0,3).map(_=>({name:_.testName,file:_.file,score:+_.score.toFixed(3)}))}))};console.log(JSON.stringify(i,null,2))}else U(m);r!==null&&b<r&&(e||console.error(d(`\u2717 Coverage ${b}% is below threshold ${r}%`)),process.exit(1))}export{q as coverageCommand};
|