infernoflow 0.37.0 → 0.37.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/dist/bin/infernoflow.mjs +29 -277
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/ask.mjs +4 -299
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +30 -135
- package/dist/lib/commands/cloud.mjs +10 -773
- package/dist/lib/commands/context.mjs +34 -346
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/feedback.mjs +12 -216
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +11 -378
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +45 -631
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/log.mjs +18 -248
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/recap.mjs +6 -380
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +11 -1118
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/stats.mjs +5 -402
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/switch.mjs +13 -517
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/theme.mjs +18 -195
- package/dist/lib/commands/uninstall.mjs +13 -406
- package/dist/lib/commands/upgrade.mjs +20 -153
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/telemetry.mjs +19 -269
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/theme/scanner.mjs +4 -343
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -95
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/package.json +2 -4
- package/scripts/postinstall.js +2 -2
|
@@ -1,152 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
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>
|
|
1
|
+
import*as u from"node:fs";import*as b from"node:path";import{execSync as S}from"node:child_process";import{done as D,warn as z,info as C,cyan as k,gray as O}from"../ui/output.mjs";function w(n,e){try{return S(n,{cwd:e,encoding:"utf8",timeout:15e3}).trim()}catch{return""}}function A(n){return n?/^\d+d$/.test(n)?new Date(Date.now()-parseInt(n)*24*60*60*1e3):/^\d+w$/.test(n)?new Date(Date.now()-parseInt(n)*7*24*60*60*1e3):/^\d{4}-\d{2}-\d{2}$/.test(n)?new Date(n):new Date(Date.now()-720*60*60*1e3):new Date(Date.now()-720*60*60*1e3)}function N(n,e){const i=e.toISOString(),t=w(`git log --after="${i}" --format="%H|%s|%an|%ad" --date=short`,n);return t?t.split(`
|
|
2
|
+
`).filter(Boolean).map(a=>{const[g,m,l,c]=a.split("|");return{hash:g?.slice(0,8),subject:m,author:l,date:c}}):[]}function I(n,e,i){const t=w(`git log --after="${i.toISOString()}" --format="%H|%ad" --date=short -- inferno/`,e);return t?t.split(`
|
|
3
|
+
`).filter(Boolean).map(a=>{const[g,m]=a.split("|");return{hash:g?.slice(0,8),date:m}}):[]}function R(n,e){const i=w("git tag --sort=-version:refname",n);return i?i.split(`
|
|
4
|
+
`).filter(t=>t.startsWith("v")).slice(0,10).map(t=>{const a=w(`git log -1 --format=%ad --date=short ${t}`,n);return{tag:t,date:a}}).filter(t=>!e||new Date(t.date)>=e):[]}function M(n,e,i){const t=(()=>{for(const s of["contract.json","capabilities.json"]){const r=b.join(e,s);if(u.existsSync(r))try{return JSON.parse(u.readFileSync(r,"utf8"))}catch{}}return{}})(),a=t.capabilities||[],g=a.filter(s=>typeof s=="object"&&s.covered).length,m=N(n,i),l=I(e,n,i),c=R(n,i),h=(()=>{const s=b.join(e,"agents");return u.existsSync(s)?u.readdirSync(s).filter(r=>r.endsWith(".json")).map(r=>{try{return JSON.parse(u.readFileSync(b.join(s,r),"utf8"))}catch{return null}}).filter(Boolean):[]})(),d=(()=>{const s=b.join(n,"CHANGELOG.md");if(!u.existsSync(s))return null;try{const r=u.readFileSync(s,"utf8").split(`
|
|
5
|
+
`),f=r.findIndex(v=>v.startsWith("## "));if(f===-1)return null;const o=r.findIndex((v,y)=>y>f&&v.startsWith("## "));return r.slice(f,o===-1?f+20:o).join(`
|
|
6
|
+
`)}catch{return null}})();return{project:t.policyId||b.basename(n),version:t.policyVersion||"?",caps:a,covered:g,commits:m,capCommits:l,versionTags:c,agents:h,changelog:d,since:i,generatedAt:new Date().toLocaleString()}}function L(n){const{project:e,version:i,caps:t,covered:a,commits:g,capCommits:m,versionTags:l,agents:c,changelog:h,since:d,generatedAt:s}=n,r=d.toLocaleDateString(),f=t.length?Math.round(a/t.length*100):0,o=t.slice(0,30).map(p=>{const j=typeof p=="string"?p:p.id,$=typeof p=="object"?p.covered:void 0;return`<tr><td style="padding:4px 10px">${$===!0?'<span style="color:#4ade80">\u2714</span>':$===!1?'<span style="color:#f87171">\u2717</span>':'<span style="color:#475569">\xB7</span>'}</td><td style="padding:4px 10px;font-weight:600">${j}</td></tr>`}).join(""),v=l.map(p=>`<tr><td style="padding:4px 10px;font-weight:600">${p.tag}</td><td style="padding:4px 10px;color:#94a3b8">${p.date}</td></tr>`).join("")||'<tr><td colspan="2" style="padding:4px 10px;color:#475569">No version tags in this period</td></tr>',y=c.map(p=>{const j=(p.steps||[]).map(x=>typeof x=="string"?x:x.command).join(" \u2192 "),$=p.confidence?Math.round(p.confidence*100)+"%":"\u2014";return`<tr><td style="padding:4px 10px;font-weight:600">${p.name}</td><td style="padding:4px 10px;color:#94a3b8">${$}</td><td style="padding:4px 10px;color:#64748b;font-size:0.8em">${j}</td></tr>`}).join("")||'<tr><td colspan="3" style="padding:4px 10px;color:#475569">No agents synthesized yet</td></tr>';return`<!doctype html>
|
|
145
7
|
<html lang="en">
|
|
146
8
|
<head>
|
|
147
9
|
<meta charset="UTF-8">
|
|
148
10
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
149
|
-
<title>infernoflow report
|
|
11
|
+
<title>infernoflow report \u2014 ${e}</title>
|
|
150
12
|
<style>
|
|
151
13
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
152
14
|
body{background:#0f0f1a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;padding:0 0 3rem}
|
|
@@ -172,149 +34,43 @@ function buildHtmlReport(data) {
|
|
|
172
34
|
<body>
|
|
173
35
|
<header>
|
|
174
36
|
<div>
|
|
175
|
-
<h1
|
|
176
|
-
<div class="meta">v${
|
|
37
|
+
<h1>\u{1F525} infernoflow report \u2014 ${e}</h1>
|
|
38
|
+
<div class="meta">v${i} \xB7 ${r} \u2192 ${s}</div>
|
|
177
39
|
</div>
|
|
178
|
-
<div class="meta" style="text-align:right">${
|
|
40
|
+
<div class="meta" style="text-align:right">${g.length} commits \xB7 ${m.length} contract updates</div>
|
|
179
41
|
</header>
|
|
180
42
|
<main>
|
|
181
43
|
<div class="grid">
|
|
182
|
-
<div class="card"><div class="n">${
|
|
183
|
-
<div class="card"><div class="n">${
|
|
184
|
-
<div class="card"><div class="n">${
|
|
185
|
-
<div class="card"><div class="n">${
|
|
186
|
-
<div class="card"><div class="n">${
|
|
187
|
-
<div class="card"><div class="n">${
|
|
44
|
+
<div class="card"><div class="n">${t.length}</div><div class="l">capabilities</div></div>
|
|
45
|
+
<div class="card"><div class="n">${f}%</div><div class="l">coverage</div><div class="prog"><div class="prog-bar" style="width:${f}%"></div></div></div>
|
|
46
|
+
<div class="card"><div class="n">${g.length}</div><div class="l">commits</div></div>
|
|
47
|
+
<div class="card"><div class="n">${m.length}</div><div class="l">contract updates</div></div>
|
|
48
|
+
<div class="card"><div class="n">${l.length}</div><div class="l">releases</div></div>
|
|
49
|
+
<div class="card"><div class="n">${c.length}</div><div class="l">agents</div></div>
|
|
188
50
|
</div>
|
|
189
51
|
|
|
190
52
|
<div class="section">
|
|
191
|
-
<h2>Capabilities (${
|
|
192
|
-
<table><tbody>${
|
|
53
|
+
<h2>Capabilities (${t.length})</h2>
|
|
54
|
+
<table><tbody>${o}${t.length>30?`<tr><td colspan="2" style="padding:6px 10px;color:#475569">\u2026 and ${t.length-30} more</td></tr>`:""}</tbody></table>
|
|
193
55
|
</div>
|
|
194
56
|
|
|
195
57
|
<div class="section">
|
|
196
58
|
<h2>Version history</h2>
|
|
197
|
-
<table><tbody>${
|
|
59
|
+
<table><tbody>${v}</tbody></table>
|
|
198
60
|
</div>
|
|
199
61
|
|
|
200
62
|
<div class="section">
|
|
201
|
-
<h2>Agents (${
|
|
202
|
-
<table><tbody>${
|
|
63
|
+
<h2>Agents (${c.length})</h2>
|
|
64
|
+
<table><tbody>${y}</tbody></table>
|
|
203
65
|
</div>
|
|
204
66
|
|
|
205
|
-
${
|
|
67
|
+
${h?`<div class="section"><h2>Latest changelog</h2><pre>${h.replace(/</g,"<")}</pre></div>`:""}
|
|
206
68
|
</main>
|
|
207
|
-
<footer>Generated by <a href="https://github.com/ronmiz/infernoflow">infernoflow</a> on ${
|
|
69
|
+
<footer>Generated by <a href="https://github.com/ronmiz/infernoflow">infernoflow</a> on ${s}</footer>
|
|
208
70
|
</body>
|
|
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
|
-
}
|
|
71
|
+
</html>`}function V(n){const{project:e,version:i,caps:t,covered:a,commits:g,capCommits:m,versionTags:l,agents:c,changelog:h,since:d,generatedAt:s}=n,r=t.length?Math.round(a/t.length*100):0;return[`# \u{1F525} infernoflow report \u2014 ${e}`,"",`**Version:** v${i} \xB7 **Period:** ${d.toLocaleDateString()} \u2192 ${s}`,"","## Summary","","| Metric | Value |","|---|---|",`| Capabilities | ${t.length} |`,`| Coverage | ${r}% |`,`| Commits | ${g.length} |`,`| Contract updates | ${m.length} |`,`| Releases | ${l.length} |`,`| Agents | ${c.length} |`,"","## Capabilities","",...t.slice(0,30).map(o=>{const v=typeof o=="string"?o:o.id,y=typeof o=="object"?o.covered:void 0;return`- ${y===!0?"\u2705":y===!1?"\u274C":"\xB7"} **${v}**`}),t.length>30?`- *\u2026 and ${t.length-30} more*`:"","","## Version history","",...l.length?l.map(o=>`- **${o.tag}** \u2014 ${o.date}`):["- *No releases in this period*"],"","## Agents","",...c.length?c.map(o=>`- **${o.name}** (${o.confidence?Math.round(o.confidence*100)+"%":"\u2014"}) \u2014 ${o.description||""}`):["- *No agents synthesized yet*"],"",h?`## Latest changelog
|
|
315
72
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
console.log();
|
|
320
|
-
}
|
|
73
|
+
\`\`\`
|
|
74
|
+
${h}
|
|
75
|
+
\`\`\``:"","","---",`*Generated by [infernoflow](https://github.com/ronmiz/infernoflow) on ${s}*`].filter(o=>o!==void 0).join(`
|
|
76
|
+
`)}async function B(n){const e=n.slice(1),i=e.includes("--json"),t=e.includes("--open"),a=e.includes("--format")?e[e.indexOf("--format")+1]:"html",g=e.includes("--since")?e[e.indexOf("--since")+1]:null,m=e.includes("--out")?e[e.indexOf("--out")+1]:null,l=process.cwd(),c=b.join(l,"inferno");if(!u.existsSync(c)){const o="inferno/ not found. Run: infernoflow init";i?console.log(JSON.stringify({ok:!1,error:o})):z(o),process.exit(1)}i||C(`Generating ${a} report\u2026`);const h=A(g),d=M(l,c,h),s=a==="md"?"md":"html",r=m||b.join(c,`report.${s}`),f=a==="md"?V(d):L(d);if(u.writeFileSync(r,f,"utf8"),t&&a==="html")try{const o=process.platform==="win32"?`start "" "${r}"`:process.platform==="darwin"?`open "${r}"`:`xdg-open "${r}"`;S(o,{stdio:"ignore"})}catch{}if(i){console.log(JSON.stringify({ok:!0,file:r,format:a,project:d.project,version:d.version,capabilities:d.caps.length,commits:d.commits.length,agents:d.agents.length}));return}D("Report generated"),console.log(` ${k(r)}`),console.log(` ${O(d.caps.length+" capabilities \xB7 "+d.commits.length+" commits \xB7 "+d.agents.length+" agents")}`),console.log()}export{B as reportCommand};
|
|
@@ -1,97 +1,16 @@
|
|
|
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
|
-
*/
|
|
1
|
+
import*as A from"node:fs";import*as x from"node:path";import{execSync as C}from"node:child_process";import{bold as y,cyan as S,gray as c,green as I,yellow as p,red as N}from"../ui/output.mjs";function j(e,o){try{return C(e,{cwd:o,encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}}function k(e){try{return JSON.parse(A.readFileSync(e,"utf8"))}catch{return null}}function w(e){return e.replace(/([a-z])([A-Z])/g,"$1 $2").toLowerCase().split(/[\s_\-/.]+/).filter(o=>o.length>2)}function D(e,o){const t=e.toLowerCase(),s=new Set;for(const i of o){const n=[...w(i.id||""),...w(i.name||""),...(i.tags||[]).flatMap(w)];if(t.includes((i.id||"").toLowerCase())){s.add(i.id);continue}n.filter(g=>g.length>3&&t.includes(g)).length>=2&&s.add(i.id)}return[...s]}function L(e,o=8e3){if(e.length<=o)return e;const t=Math.floor(o/2);return e.slice(0,t)+`
|
|
15
2
|
|
|
16
|
-
|
|
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";
|
|
3
|
+
[\u2026 diff truncated \u2026]
|
|
20
4
|
|
|
21
|
-
|
|
5
|
+
`+e.slice(-t)}function P(e,o,t){const s=t.filter(n=>o.includes(n.id)).map(n=>` \u2022 ${n.id}: ${n.name}${n.description?" \u2014 "+n.description:""}`).join(`
|
|
6
|
+
`);return`You are a senior software architect reviewing a code change for capability drift.
|
|
22
7
|
|
|
23
|
-
|
|
24
|
-
|
|
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}
|
|
8
|
+
${o.length>0?`Affected capabilities detected:
|
|
9
|
+
${s}`:"No specific capabilities were matched \u2014 review the entire contract."}
|
|
91
10
|
|
|
92
11
|
Git diff:
|
|
93
12
|
\`\`\`diff
|
|
94
|
-
${
|
|
13
|
+
${L(e)}
|
|
95
14
|
\`\`\`
|
|
96
15
|
|
|
97
16
|
Write a concise capability impact summary covering:
|
|
@@ -101,138 +20,5 @@ Write a concise capability impact summary covering:
|
|
|
101
20
|
4. Recommended follow-up actions (one sentence each)
|
|
102
21
|
|
|
103
22
|
Keep the tone professional and brief. Use bullet points only where genuinely helpful.
|
|
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
|
-
}
|
|
23
|
+
Do NOT repeat the diff back.`}function R(e,o,t,s){if(console.log(),console.log(y(S(" \u2726 Capability Impact Review"))),console.log(c(` Source: ${s}`)),console.log(),e.length===0)console.log(p(" No capabilities directly matched \u2014 reviewing full diff."));else{console.log(y(" Affected capabilities:"));for(const i of e){const n=t.find(r=>r.id===i);console.log(` ${I("\u25B8")} ${i}${n?c(" \u2014 "+n.name):""}`)}}console.log(),console.log(y(" AI Impact Summary")),console.log(c(" \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 i of o.split(`
|
|
24
|
+
`))console.log(" "+i);console.log()}async function O(e){const o=e||[],t=o.includes("--dry-run"),s=o.includes("--json"),i=o.includes("--unstaged"),n=o.includes("--last"),r=process.cwd(),g=x.join(r,"inferno"),b=x.join(g,"capabilities.json");A.existsSync(b)||(console.error(N("\u2717 inferno/capabilities.json not found \u2014 run `infernoflow init` first.")),process.exit(1));const f=k(b);(!Array.isArray(f)||f.length===0)&&(console.log(p("No capabilities found \u2014 nothing to review.")),process.exit(0));let u,a;n?(u="git diff HEAD~1",a="last commit (HEAD~1)"):i?(u="git diff",a="unstaged changes"):(u="git diff --staged",a="staged changes");let d=j(u,r);!d&&!n&&!i&&(d=j("git diff",r),a="unstaged changes (no staged changes found)"),d||(console.log(p("No changes found to review.")),console.log(c(" Tip: stage some files first (`git add -p`) or use --last / --unstaged")),process.exit(0));const m=D(d,f),v=P(d,m,f);t&&(console.log(c("\u2500\u2500 Prompt (--dry-run) \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(v),process.exit(0)),s||process.stdout.write(c(" Calling AI provider\u2026"));let l=null;try{const{callAI:h}=await import("../ai/providerRouter.mjs");l=await h(v,{cwd:r,maxTokens:600})}catch{}if(s||process.stdout.write("\r"+" ".repeat(30)+"\r"),!l){console.log(),console.log(p(" \u26A0 No AI provider configured \u2014 skipping narrative review.")),console.log(),console.log(y(" Affected capabilities:"));for(const h of m)console.log(` \u25B8 ${h}`);console.log(),console.log(` ${p("\u{1F4A1}")} ${c("For AI-powered impact summaries:")} ${S("infernoflow ai setup")}`),console.log(),process.exit(0)}const $=l.text||"(empty response)";s?console.log(JSON.stringify({source:a,provider:l.provider,model:l.model,affectedCapabilities:m,summary:$},null,2)):(R(m,$,f,a),console.log(c(` Provider: ${l.provider} Model: ${l.model}`)),console.log())}export{O as reviewCommand};
|