infernoflow 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -120
- package/dist/bin/infernoflow.mjs +271 -85
- package/dist/lib/adopters/angular.mjs +128 -1
- package/dist/lib/adopters/css.mjs +111 -1
- package/dist/lib/adopters/react.mjs +104 -1
- package/dist/lib/ai/ideDetection.mjs +31 -1
- package/dist/lib/ai/localProvider.mjs +88 -1
- package/dist/lib/ai/providerRouter.mjs +295 -2
- package/dist/lib/commands/adopt.mjs +869 -20
- package/dist/lib/commands/adoptWizard.mjs +320 -9
- package/dist/lib/commands/agent.mjs +191 -5
- package/dist/lib/commands/ai.mjs +407 -2
- package/dist/lib/commands/ask.mjs +299 -0
- package/dist/lib/commands/audit.mjs +300 -13
- package/dist/lib/commands/changelog.mjs +594 -26
- package/dist/lib/commands/check.mjs +184 -3
- package/dist/lib/commands/ci.mjs +208 -3
- package/dist/lib/commands/claudeMd.mjs +139 -28
- package/dist/lib/commands/cloud.mjs +521 -5
- package/dist/lib/commands/context.mjs +346 -34
- package/dist/lib/commands/coverage.mjs +282 -2
- package/dist/lib/commands/dashboard.mjs +635 -123
- package/dist/lib/commands/demo.mjs +465 -8
- package/dist/lib/commands/diff.mjs +274 -5
- package/dist/lib/commands/docGate.mjs +81 -2
- package/dist/lib/commands/doctor.mjs +321 -3
- package/dist/lib/commands/explain.mjs +438 -8
- package/dist/lib/commands/export.mjs +239 -10
- package/dist/lib/commands/generateSkills.mjs +163 -38
- package/dist/lib/commands/graph.mjs +378 -11
- package/dist/lib/commands/health.mjs +309 -2
- package/dist/lib/commands/impact.mjs +325 -2
- package/dist/lib/commands/implement.mjs +103 -7
- package/dist/lib/commands/init.mjs +545 -23
- package/dist/lib/commands/installCursorHooks.mjs +36 -1
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
- package/dist/lib/commands/link.mjs +342 -2
- package/dist/lib/commands/log.mjs +164 -16
- package/dist/lib/commands/monorepo.mjs +428 -4
- package/dist/lib/commands/notify.mjs +258 -4
- package/dist/lib/commands/onboard.mjs +296 -4
- package/dist/lib/commands/prComment.mjs +361 -2
- package/dist/lib/commands/prImpact.mjs +157 -2
- package/dist/lib/commands/publish.mjs +316 -15
- package/dist/lib/commands/recap.mjs +359 -0
- package/dist/lib/commands/report.mjs +272 -28
- package/dist/lib/commands/review.mjs +223 -9
- package/dist/lib/commands/run.mjs +336 -8
- package/dist/lib/commands/scaffold.mjs +419 -54
- package/dist/lib/commands/scan.mjs +1118 -5
- package/dist/lib/commands/scout.mjs +291 -2
- package/dist/lib/commands/setup.mjs +310 -5
- package/dist/lib/commands/share.mjs +196 -13
- package/dist/lib/commands/snapshot.mjs +383 -3
- package/dist/lib/commands/stability.mjs +293 -2
- package/dist/lib/commands/stats.mjs +402 -0
- package/dist/lib/commands/status.mjs +172 -4
- package/dist/lib/commands/suggest.mjs +563 -21
- package/dist/lib/commands/switch.mjs +310 -0
- package/dist/lib/commands/syncAuto.mjs +96 -1
- package/dist/lib/commands/synthesize.mjs +228 -10
- package/dist/lib/commands/teamSync.mjs +388 -2
- package/dist/lib/commands/test.mjs +363 -6
- package/dist/lib/commands/theme.mjs +195 -18
- package/dist/lib/commands/upgrade.mjs +153 -0
- package/dist/lib/commands/version.mjs +282 -2
- package/dist/lib/commands/vibe.mjs +357 -7
- package/dist/lib/commands/watch.mjs +203 -4
- package/dist/lib/commands/why.mjs +358 -4
- package/dist/lib/cursorHooksInstall.mjs +60 -1
- package/dist/lib/draftToolingInstall.mjs +68 -7
- package/dist/lib/git/detect-drift.mjs +208 -4
- package/dist/lib/learning/adapt.mjs +101 -6
- package/dist/lib/learning/observe.mjs +119 -1
- package/dist/lib/learning/patternDetector.mjs +298 -1
- package/dist/lib/learning/profile.mjs +279 -2
- package/dist/lib/learning/skillSynthesizer.mjs +145 -24
- package/dist/lib/templates/index.mjs +131 -1
- package/dist/lib/theme/scanner.mjs +343 -4
- package/dist/lib/ui/errors.mjs +142 -1
- package/dist/lib/ui/output.mjs +72 -6
- package/dist/lib/ui/prompts.mjs +147 -6
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
- package/package.json +1 -1
|
@@ -1,14 +1,152 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow report
|
|
3
|
+
*
|
|
4
|
+
* Generate a weekly/monthly HTML or Markdown report of capability changes,
|
|
5
|
+
* version history, drift events, and agent usage. Can be committed to the
|
|
6
|
+
* repo on a schedule or emailed to the team.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* infernoflow report Generate HTML report (last 30 days)
|
|
10
|
+
* infernoflow report --format md Markdown instead of HTML
|
|
11
|
+
* infernoflow report --since 7d Last 7 days
|
|
12
|
+
* infernoflow report --since 2024-01-01 Since a specific date
|
|
13
|
+
* infernoflow report --out report.html Custom output path
|
|
14
|
+
* infernoflow report --open Open in browser after generating
|
|
15
|
+
* infernoflow report --json Machine-readable summary
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
21
|
+
import { done, warn, info, bold, cyan, gray } from "../ui/output.mjs";
|
|
22
|
+
|
|
23
|
+
// ── Git helpers ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function git(cmd, cwd) {
|
|
26
|
+
try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 15_000 }).trim(); }
|
|
27
|
+
catch { return ""; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseSinceDuration(since) {
|
|
31
|
+
if (!since) return new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
32
|
+
if (/^\d+d$/.test(since)) return new Date(Date.now() - parseInt(since) * 24 * 60 * 60 * 1000);
|
|
33
|
+
if (/^\d+w$/.test(since)) return new Date(Date.now() - parseInt(since) * 7 * 24 * 60 * 60 * 1000);
|
|
34
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(since)) return new Date(since);
|
|
35
|
+
return new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getCommitsSince(cwd, since) {
|
|
39
|
+
const iso = since.toISOString();
|
|
40
|
+
const out = git(`git log --after="${iso}" --format="%H|%s|%an|%ad" --date=short`, cwd);
|
|
41
|
+
if (!out) return [];
|
|
42
|
+
return out.split("\n").filter(Boolean).map(line => {
|
|
43
|
+
const [hash, subject, author, date] = line.split("|");
|
|
44
|
+
return { hash: hash?.slice(0, 8), subject, author, date };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getCapabilityHistory(infernoDir, cwd, since) {
|
|
49
|
+
const out = git(`git log --after="${since.toISOString()}" --format="%H|%ad" --date=short -- inferno/`, cwd);
|
|
50
|
+
if (!out) return [];
|
|
51
|
+
return out.split("\n").filter(Boolean).map(line => {
|
|
52
|
+
const [hash, date] = line.split("|");
|
|
53
|
+
return { hash: hash?.slice(0, 8), date };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getVersionTags(cwd, since) {
|
|
58
|
+
const out = git(`git tag --sort=-version:refname`, cwd);
|
|
59
|
+
if (!out) return [];
|
|
60
|
+
return out.split("\n").filter(t => t.startsWith("v")).slice(0, 10).map(tag => {
|
|
61
|
+
const date = git(`git log -1 --format=%ad --date=short ${tag}`, cwd);
|
|
62
|
+
return { tag, date };
|
|
63
|
+
}).filter(t => !since || new Date(t.date) >= since);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Data gathering ────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function gatherData(cwd, infernoDir, since) {
|
|
69
|
+
const contract = (() => {
|
|
70
|
+
for (const f of ["contract.json", "capabilities.json"]) {
|
|
71
|
+
const p = path.join(infernoDir, f);
|
|
72
|
+
if (fs.existsSync(p)) { try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {} }
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
const caps = contract.capabilities || [];
|
|
78
|
+
const covered = caps.filter(c => typeof c === "object" && c.covered).length;
|
|
79
|
+
|
|
80
|
+
const commits = getCommitsSince(cwd, since);
|
|
81
|
+
const capCommits = getCapabilityHistory(infernoDir, cwd, since);
|
|
82
|
+
const versionTags = getVersionTags(cwd, since);
|
|
83
|
+
|
|
84
|
+
const agents = (() => {
|
|
85
|
+
const dir = path.join(infernoDir, "agents");
|
|
86
|
+
if (!fs.existsSync(dir)) return [];
|
|
87
|
+
return fs.readdirSync(dir).filter(f => f.endsWith(".json")).map(f => {
|
|
88
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); }
|
|
89
|
+
catch { return null; }
|
|
90
|
+
}).filter(Boolean);
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
const changelog = (() => {
|
|
94
|
+
const p = path.join(cwd, "CHANGELOG.md");
|
|
95
|
+
if (!fs.existsSync(p)) return null;
|
|
96
|
+
try {
|
|
97
|
+
const lines = fs.readFileSync(p, "utf8").split("\n");
|
|
98
|
+
const start = lines.findIndex(l => l.startsWith("## "));
|
|
99
|
+
if (start === -1) return null;
|
|
100
|
+
const end = lines.findIndex((l, i) => i > start && l.startsWith("## "));
|
|
101
|
+
return lines.slice(start, end === -1 ? start + 20 : end).join("\n");
|
|
102
|
+
} catch { return null; }
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
project: contract.policyId || path.basename(cwd),
|
|
107
|
+
version: contract.policyVersion || "?",
|
|
108
|
+
caps,
|
|
109
|
+
covered,
|
|
110
|
+
commits,
|
|
111
|
+
capCommits,
|
|
112
|
+
versionTags,
|
|
113
|
+
agents,
|
|
114
|
+
changelog,
|
|
115
|
+
since,
|
|
116
|
+
generatedAt: new Date().toLocaleString(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── HTML report ───────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function buildHtmlReport(data) {
|
|
123
|
+
const { project, version, caps, covered, commits, capCommits, versionTags, agents, changelog, since, generatedAt } = data;
|
|
124
|
+
const sinceStr = since.toLocaleDateString();
|
|
125
|
+
const coverage = caps.length ? Math.round((covered / caps.length) * 100) : 0;
|
|
126
|
+
|
|
127
|
+
const capRows = caps.slice(0, 30).map(c => {
|
|
128
|
+
const id = typeof c === "string" ? c : c.id;
|
|
129
|
+
const cov = typeof c === "object" ? c.covered : undefined;
|
|
130
|
+
const badge = cov === true ? `<span style="color:#4ade80">✔</span>` : cov === false ? `<span style="color:#f87171">✗</span>` : `<span style="color:#475569">·</span>`;
|
|
131
|
+
return `<tr><td style="padding:4px 10px">${badge}</td><td style="padding:4px 10px;font-weight:600">${id}</td></tr>`;
|
|
132
|
+
}).join("");
|
|
133
|
+
|
|
134
|
+
const versionRows = versionTags.map(t =>
|
|
135
|
+
`<tr><td style="padding:4px 10px;font-weight:600">${t.tag}</td><td style="padding:4px 10px;color:#94a3b8">${t.date}</td></tr>`
|
|
136
|
+
).join("") || `<tr><td colspan="2" style="padding:4px 10px;color:#475569">No version tags in this period</td></tr>`;
|
|
137
|
+
|
|
138
|
+
const agentRows = agents.map(a => {
|
|
139
|
+
const steps = (a.steps || []).map(s => typeof s === "string" ? s : s.command).join(" → ");
|
|
140
|
+
const conf = a.confidence ? Math.round(a.confidence * 100) + "%" : "—";
|
|
141
|
+
return `<tr><td style="padding:4px 10px;font-weight:600">${a.name}</td><td style="padding:4px 10px;color:#94a3b8">${conf}</td><td style="padding:4px 10px;color:#64748b;font-size:0.8em">${steps}</td></tr>`;
|
|
142
|
+
}).join("") || `<tr><td colspan="3" style="padding:4px 10px;color:#475569">No agents synthesized yet</td></tr>`;
|
|
143
|
+
|
|
144
|
+
return `<!doctype html>
|
|
7
145
|
<html lang="en">
|
|
8
146
|
<head>
|
|
9
147
|
<meta charset="UTF-8">
|
|
10
148
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
11
|
-
<title>infernoflow report
|
|
149
|
+
<title>infernoflow report — ${project}</title>
|
|
12
150
|
<style>
|
|
13
151
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
14
152
|
body{background:#0f0f1a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;padding:0 0 3rem}
|
|
@@ -34,43 +172,149 @@ import*as u from"node:fs";import*as b from"node:path";import{execSync as S}from"
|
|
|
34
172
|
<body>
|
|
35
173
|
<header>
|
|
36
174
|
<div>
|
|
37
|
-
<h1
|
|
38
|
-
<div class="meta">v${
|
|
175
|
+
<h1>🔥 infernoflow report — ${project}</h1>
|
|
176
|
+
<div class="meta">v${version} · ${sinceStr} → ${generatedAt}</div>
|
|
39
177
|
</div>
|
|
40
|
-
<div class="meta" style="text-align:right">${
|
|
178
|
+
<div class="meta" style="text-align:right">${commits.length} commits · ${capCommits.length} contract updates</div>
|
|
41
179
|
</header>
|
|
42
180
|
<main>
|
|
43
181
|
<div class="grid">
|
|
44
|
-
<div class="card"><div class="n">${
|
|
45
|
-
<div class="card"><div class="n">${
|
|
46
|
-
<div class="card"><div class="n">${
|
|
47
|
-
<div class="card"><div class="n">${
|
|
48
|
-
<div class="card"><div class="n">${
|
|
49
|
-
<div class="card"><div class="n">${
|
|
182
|
+
<div class="card"><div class="n">${caps.length}</div><div class="l">capabilities</div></div>
|
|
183
|
+
<div class="card"><div class="n">${coverage}%</div><div class="l">coverage</div><div class="prog"><div class="prog-bar" style="width:${coverage}%"></div></div></div>
|
|
184
|
+
<div class="card"><div class="n">${commits.length}</div><div class="l">commits</div></div>
|
|
185
|
+
<div class="card"><div class="n">${capCommits.length}</div><div class="l">contract updates</div></div>
|
|
186
|
+
<div class="card"><div class="n">${versionTags.length}</div><div class="l">releases</div></div>
|
|
187
|
+
<div class="card"><div class="n">${agents.length}</div><div class="l">agents</div></div>
|
|
50
188
|
</div>
|
|
51
189
|
|
|
52
190
|
<div class="section">
|
|
53
|
-
<h2>Capabilities (${
|
|
54
|
-
<table><tbody>${
|
|
191
|
+
<h2>Capabilities (${caps.length})</h2>
|
|
192
|
+
<table><tbody>${capRows}${caps.length > 30 ? `<tr><td colspan="2" style="padding:6px 10px;color:#475569">… and ${caps.length - 30} more</td></tr>` : ""}</tbody></table>
|
|
55
193
|
</div>
|
|
56
194
|
|
|
57
195
|
<div class="section">
|
|
58
196
|
<h2>Version history</h2>
|
|
59
|
-
<table><tbody>${
|
|
197
|
+
<table><tbody>${versionRows}</tbody></table>
|
|
60
198
|
</div>
|
|
61
199
|
|
|
62
200
|
<div class="section">
|
|
63
|
-
<h2>Agents (${
|
|
64
|
-
<table><tbody>${
|
|
201
|
+
<h2>Agents (${agents.length})</h2>
|
|
202
|
+
<table><tbody>${agentRows}</tbody></table>
|
|
65
203
|
</div>
|
|
66
204
|
|
|
67
|
-
${
|
|
205
|
+
${changelog ? `<div class="section"><h2>Latest changelog</h2><pre>${changelog.replace(/</g, "<")}</pre></div>` : ""}
|
|
68
206
|
</main>
|
|
69
|
-
<footer>Generated by <a href="https://github.com/ronmiz/infernoflow">infernoflow</a> on ${
|
|
207
|
+
<footer>Generated by <a href="https://github.com/ronmiz/infernoflow">infernoflow</a> on ${generatedAt}</footer>
|
|
70
208
|
</body>
|
|
71
|
-
</html
|
|
209
|
+
</html>`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Markdown report ───────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
function buildMarkdownReport(data) {
|
|
215
|
+
const { project, version, caps, covered, commits, capCommits, versionTags, agents, changelog, since, generatedAt } = data;
|
|
216
|
+
const coverage = caps.length ? Math.round((covered / caps.length) * 100) : 0;
|
|
217
|
+
|
|
218
|
+
const lines = [
|
|
219
|
+
`# 🔥 infernoflow report — ${project}`,
|
|
220
|
+
``,
|
|
221
|
+
`**Version:** v${version} · **Period:** ${since.toLocaleDateString()} → ${generatedAt}`,
|
|
222
|
+
``,
|
|
223
|
+
`## Summary`,
|
|
224
|
+
``,
|
|
225
|
+
`| Metric | Value |`,
|
|
226
|
+
`|---|---|`,
|
|
227
|
+
`| Capabilities | ${caps.length} |`,
|
|
228
|
+
`| Coverage | ${coverage}% |`,
|
|
229
|
+
`| Commits | ${commits.length} |`,
|
|
230
|
+
`| Contract updates | ${capCommits.length} |`,
|
|
231
|
+
`| Releases | ${versionTags.length} |`,
|
|
232
|
+
`| Agents | ${agents.length} |`,
|
|
233
|
+
``,
|
|
234
|
+
`## Capabilities`,
|
|
235
|
+
``,
|
|
236
|
+
...caps.slice(0, 30).map(c => {
|
|
237
|
+
const id = typeof c === "string" ? c : c.id;
|
|
238
|
+
const cov = typeof c === "object" ? c.covered : undefined;
|
|
239
|
+
return `- ${cov === true ? "✅" : cov === false ? "❌" : "·"} **${id}**`;
|
|
240
|
+
}),
|
|
241
|
+
caps.length > 30 ? `- *… and ${caps.length - 30} more*` : "",
|
|
242
|
+
``,
|
|
243
|
+
`## Version history`,
|
|
244
|
+
``,
|
|
245
|
+
...(versionTags.length
|
|
246
|
+
? versionTags.map(t => `- **${t.tag}** — ${t.date}`)
|
|
247
|
+
: ["- *No releases in this period*"]),
|
|
248
|
+
``,
|
|
249
|
+
`## Agents`,
|
|
250
|
+
``,
|
|
251
|
+
...(agents.length
|
|
252
|
+
? agents.map(a => `- **${a.name}** (${a.confidence ? Math.round(a.confidence * 100) + "%" : "—"}) — ${a.description || ""}`)
|
|
253
|
+
: ["- *No agents synthesized yet*"]),
|
|
254
|
+
``,
|
|
255
|
+
changelog ? `## Latest changelog\n\n\`\`\`\n${changelog}\n\`\`\`` : "",
|
|
256
|
+
``,
|
|
257
|
+
`---`,
|
|
258
|
+
`*Generated by [infernoflow](https://github.com/ronmiz/infernoflow) on ${generatedAt}*`,
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
return lines.filter(l => l !== undefined).join("\n");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
export async function reportCommand(rawArgs) {
|
|
267
|
+
const args = rawArgs.slice(1);
|
|
268
|
+
const jsonMode = args.includes("--json");
|
|
269
|
+
const openBrowser = args.includes("--open");
|
|
270
|
+
const format = args.includes("--format") ? args[args.indexOf("--format") + 1] : "html";
|
|
271
|
+
const sinceArg = args.includes("--since") ? args[args.indexOf("--since") + 1] : null;
|
|
272
|
+
const outArg = args.includes("--out") ? args[args.indexOf("--out") + 1] : null;
|
|
273
|
+
const cwd = process.cwd();
|
|
274
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
275
|
+
|
|
276
|
+
if (!fs.existsSync(infernoDir)) {
|
|
277
|
+
const msg = "inferno/ not found. Run: infernoflow init";
|
|
278
|
+
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!jsonMode) info(`Generating ${format} report…`);
|
|
283
|
+
|
|
284
|
+
const since = parseSinceDuration(sinceArg);
|
|
285
|
+
const data = gatherData(cwd, infernoDir, since);
|
|
286
|
+
|
|
287
|
+
const ext = format === "md" ? "md" : "html";
|
|
288
|
+
const outPath = outArg || path.join(infernoDir, `report.${ext}`);
|
|
289
|
+
const content = format === "md" ? buildMarkdownReport(data) : buildHtmlReport(data);
|
|
290
|
+
|
|
291
|
+
fs.writeFileSync(outPath, content, "utf8");
|
|
292
|
+
|
|
293
|
+
if (openBrowser && format === "html") {
|
|
294
|
+
try {
|
|
295
|
+
const cmd = process.platform === "win32" ? `start "" "${outPath}"`
|
|
296
|
+
: process.platform === "darwin" ? `open "${outPath}"`
|
|
297
|
+
: `xdg-open "${outPath}"`;
|
|
298
|
+
execSync(cmd, { stdio: "ignore" });
|
|
299
|
+
} catch {}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (jsonMode) {
|
|
303
|
+
console.log(JSON.stringify({
|
|
304
|
+
ok: true,
|
|
305
|
+
file: outPath,
|
|
306
|
+
format,
|
|
307
|
+
project: data.project,
|
|
308
|
+
version: data.version,
|
|
309
|
+
capabilities: data.caps.length,
|
|
310
|
+
commits: data.commits.length,
|
|
311
|
+
agents: data.agents.length,
|
|
312
|
+
}));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
72
315
|
|
|
73
|
-
|
|
74
|
-
${
|
|
75
|
-
|
|
76
|
-
|
|
316
|
+
done(`Report generated`);
|
|
317
|
+
console.log(` ${cyan(outPath)}`);
|
|
318
|
+
console.log(` ${gray(data.caps.length + " capabilities · " + data.commits.length + " commits · " + data.agents.length + " agents")}`);
|
|
319
|
+
console.log();
|
|
320
|
+
}
|
|
@@ -1,16 +1,97 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow review
|
|
3
|
+
*
|
|
4
|
+
* AI-powered capability impact review for staged (or recent) git changes.
|
|
5
|
+
* Reads git diff, identifies which capabilities are affected, then asks
|
|
6
|
+
* your configured AI provider to write a capability impact summary.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* infernoflow review Review staged changes (git diff --staged)
|
|
10
|
+
* infernoflow review --unstaged Review all working-tree changes
|
|
11
|
+
* infernoflow review --last Review last commit (git diff HEAD~1)
|
|
12
|
+
* infernoflow review --dry-run Print the AI prompt only — no API call
|
|
13
|
+
* infernoflow review --json Machine-readable output
|
|
14
|
+
*/
|
|
2
15
|
|
|
3
|
-
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
4
20
|
|
|
5
|
-
|
|
6
|
-
`);return`You are a senior software architect reviewing a code change for capability drift.
|
|
21
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
7
22
|
|
|
8
|
-
|
|
9
|
-
|
|
23
|
+
function runGit(cmd, cwd) {
|
|
24
|
+
try {
|
|
25
|
+
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
26
|
+
} catch {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadJson(filePath) {
|
|
32
|
+
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
|
33
|
+
catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Tokenise a string into lowercase words */
|
|
37
|
+
function tokenise(str) {
|
|
38
|
+
return str
|
|
39
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.split(/[\s_\-/.]+/)
|
|
42
|
+
.filter(t => t.length > 2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Return capability IDs mentioned in or matched by the diff text */
|
|
46
|
+
function findAffectedCaps(diff, capabilities) {
|
|
47
|
+
const diffLower = diff.toLowerCase();
|
|
48
|
+
const affected = new Set();
|
|
49
|
+
|
|
50
|
+
for (const cap of capabilities) {
|
|
51
|
+
const tokens = [
|
|
52
|
+
...tokenise(cap.id || ""),
|
|
53
|
+
...tokenise(cap.name || ""),
|
|
54
|
+
...(cap.tags || []).flatMap(tokenise),
|
|
55
|
+
];
|
|
56
|
+
// Direct ID mention (e.g. "auth-login" appears in diff)
|
|
57
|
+
if (diffLower.includes((cap.id || "").toLowerCase())) {
|
|
58
|
+
affected.add(cap.id);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Token overlap — need ≥2 matching tokens to avoid false positives
|
|
62
|
+
const matches = tokens.filter(t => t.length > 3 && diffLower.includes(t));
|
|
63
|
+
if (matches.length >= 2) affected.add(cap.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [...affected];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Trim diff to a reasonable size for the prompt */
|
|
70
|
+
function trimDiff(diff, maxChars = 8000) {
|
|
71
|
+
if (diff.length <= maxChars) return diff;
|
|
72
|
+
const half = Math.floor(maxChars / 2);
|
|
73
|
+
return diff.slice(0, half) + "\n\n[… diff truncated …]\n\n" + diff.slice(-half);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── prompt builder ───────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function buildPrompt(diff, affectedCaps, capabilities) {
|
|
79
|
+
const capDetails = capabilities
|
|
80
|
+
.filter(c => affectedCaps.includes(c.id))
|
|
81
|
+
.map(c => ` • ${c.id}: ${c.name}${c.description ? " — " + c.description : ""}`)
|
|
82
|
+
.join("\n");
|
|
83
|
+
|
|
84
|
+
const capList = affectedCaps.length > 0
|
|
85
|
+
? `Affected capabilities detected:\n${capDetails}`
|
|
86
|
+
: "No specific capabilities were matched — review the entire contract.";
|
|
87
|
+
|
|
88
|
+
return `You are a senior software architect reviewing a code change for capability drift.
|
|
89
|
+
|
|
90
|
+
${capList}
|
|
10
91
|
|
|
11
92
|
Git diff:
|
|
12
93
|
\`\`\`diff
|
|
13
|
-
${
|
|
94
|
+
${trimDiff(diff)}
|
|
14
95
|
\`\`\`
|
|
15
96
|
|
|
16
97
|
Write a concise capability impact summary covering:
|
|
@@ -20,5 +101,138 @@ Write a concise capability impact summary covering:
|
|
|
20
101
|
4. Recommended follow-up actions (one sentence each)
|
|
21
102
|
|
|
22
103
|
Keep the tone professional and brief. Use bullet points only where genuinely helpful.
|
|
23
|
-
Do NOT repeat the diff back
|
|
24
|
-
|
|
104
|
+
Do NOT repeat the diff back.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── reporters ────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function printReport(affectedCaps, summary, capabilities, source) {
|
|
110
|
+
console.log();
|
|
111
|
+
console.log(bold(cyan(" ✦ Capability Impact Review")));
|
|
112
|
+
console.log(gray(` Source: ${source}`));
|
|
113
|
+
console.log();
|
|
114
|
+
|
|
115
|
+
if (affectedCaps.length === 0) {
|
|
116
|
+
console.log(yellow(" No capabilities directly matched — reviewing full diff."));
|
|
117
|
+
} else {
|
|
118
|
+
console.log(bold(" Affected capabilities:"));
|
|
119
|
+
for (const id of affectedCaps) {
|
|
120
|
+
const cap = capabilities.find(c => c.id === id);
|
|
121
|
+
console.log(` ${green("▸")} ${id}${cap ? gray(" — " + cap.name) : ""}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(bold(" AI Impact Summary"));
|
|
127
|
+
console.log(gray(" ─────────────────────────────────────────────────────────────"));
|
|
128
|
+
// Indent each line
|
|
129
|
+
for (const line of summary.split("\n")) {
|
|
130
|
+
console.log(" " + line);
|
|
131
|
+
}
|
|
132
|
+
console.log();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── entry point ─────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export async function reviewCommand(rawArgs) {
|
|
138
|
+
const args = rawArgs || [];
|
|
139
|
+
const dryRun = args.includes("--dry-run");
|
|
140
|
+
const jsonMode = args.includes("--json");
|
|
141
|
+
const unstaged = args.includes("--unstaged");
|
|
142
|
+
const lastCommit = args.includes("--last");
|
|
143
|
+
|
|
144
|
+
const cwd = process.cwd();
|
|
145
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
146
|
+
|
|
147
|
+
// Load capabilities
|
|
148
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
149
|
+
if (!fs.existsSync(capsPath)) {
|
|
150
|
+
console.error(red("✗ inferno/capabilities.json not found — run `infernoflow init` first."));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const capabilities = loadJson(capsPath);
|
|
154
|
+
if (!Array.isArray(capabilities) || capabilities.length === 0) {
|
|
155
|
+
console.log(yellow("No capabilities found — nothing to review."));
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Get diff
|
|
160
|
+
let diffCmd, diffSource;
|
|
161
|
+
if (lastCommit) {
|
|
162
|
+
diffCmd = "git diff HEAD~1";
|
|
163
|
+
diffSource = "last commit (HEAD~1)";
|
|
164
|
+
} else if (unstaged) {
|
|
165
|
+
diffCmd = "git diff";
|
|
166
|
+
diffSource = "unstaged changes";
|
|
167
|
+
} else {
|
|
168
|
+
diffCmd = "git diff --staged";
|
|
169
|
+
diffSource = "staged changes";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let diff = runGit(diffCmd, cwd);
|
|
173
|
+
|
|
174
|
+
// Fallback: if staged is empty, try unstaged
|
|
175
|
+
if (!diff && !lastCommit && !unstaged) {
|
|
176
|
+
diff = runGit("git diff", cwd);
|
|
177
|
+
diffSource = "unstaged changes (no staged changes found)";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!diff) {
|
|
181
|
+
console.log(yellow("No changes found to review."));
|
|
182
|
+
console.log(gray(" Tip: stage some files first (`git add -p`) or use --last / --unstaged"));
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Identify affected capabilities
|
|
187
|
+
const affectedCaps = findAffectedCaps(diff, capabilities);
|
|
188
|
+
|
|
189
|
+
// Build prompt
|
|
190
|
+
const prompt = buildPrompt(diff, affectedCaps, capabilities);
|
|
191
|
+
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
console.log(gray("── Prompt (--dry-run) ────────────────────────────────────────────────"));
|
|
194
|
+
console.log(prompt);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Call AI
|
|
199
|
+
if (!jsonMode) process.stdout.write(gray(" Calling AI provider…"));
|
|
200
|
+
|
|
201
|
+
let aiResult = null;
|
|
202
|
+
try {
|
|
203
|
+
const { callAI } = await import("../ai/providerRouter.mjs");
|
|
204
|
+
aiResult = await callAI(prompt, { cwd, maxTokens: 600 });
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// provider router import failure is non-fatal — we degrade gracefully
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!jsonMode) process.stdout.write("\r" + " ".repeat(30) + "\r");
|
|
210
|
+
|
|
211
|
+
if (!aiResult) {
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(yellow(" ⚠ No AI provider configured — skipping narrative review."));
|
|
214
|
+
console.log();
|
|
215
|
+
console.log(bold(" Affected capabilities:"));
|
|
216
|
+
for (const id of affectedCaps) console.log(` ▸ ${id}`);
|
|
217
|
+
console.log();
|
|
218
|
+
console.log(` ${yellow("💡")} ${gray("For AI-powered impact summaries:")} ${cyan("infernoflow ai setup")}`);
|
|
219
|
+
console.log();
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const summary = aiResult.text || "(empty response)";
|
|
224
|
+
|
|
225
|
+
if (jsonMode) {
|
|
226
|
+
console.log(JSON.stringify({
|
|
227
|
+
source: diffSource,
|
|
228
|
+
provider: aiResult.provider,
|
|
229
|
+
model: aiResult.model,
|
|
230
|
+
affectedCapabilities: affectedCaps,
|
|
231
|
+
summary,
|
|
232
|
+
}, null, 2));
|
|
233
|
+
} else {
|
|
234
|
+
printReport(affectedCaps, summary, capabilities, diffSource);
|
|
235
|
+
console.log(gray(` Provider: ${aiResult.provider} Model: ${aiResult.model}`));
|
|
236
|
+
console.log();
|
|
237
|
+
}
|
|
238
|
+
}
|