infernoflow 0.32.7 → 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.
Files changed (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -320
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -1,152 +1,14 @@
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>
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 ${project}</title>
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>🔥 infernoflow report ${project}</h1>
176
- <div class="meta">v${version} · ${sinceStr} ${generatedAt}</div>
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">${commits.length} commits · ${capCommits.length} contract updates</div>
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">${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>
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 (${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>
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>${versionRows}</tbody></table>
59
+ <table><tbody>${v}</tbody></table>
198
60
  </div>
199
61
 
200
62
  <div class="section">
201
- <h2>Agents (${agents.length})</h2>
202
- <table><tbody>${agentRows}</tbody></table>
63
+ <h2>Agents (${c.length})</h2>
64
+ <table><tbody>${y}</tbody></table>
203
65
  </div>
204
66
 
205
- ${changelog ? `<div class="section"><h2>Latest changelog</h2><pre>${changelog.replace(/</g, "&lt;")}</pre></div>` : ""}
67
+ ${h?`<div class="section"><h2>Latest changelog</h2><pre>${h.replace(/</g,"&lt;")}</pre></div>`:""}
206
68
  </main>
207
- <footer>Generated by <a href="https://github.com/ronmiz/infernoflow">infernoflow</a> on ${generatedAt}</footer>
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
- 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
- }
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
- 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";
3
+ [\u2026 diff truncated \u2026]
20
4
 
21
- // ─── helpers ─────────────────────────────────────────────────────────────────
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
- 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}
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
- ${trimDiff(diff)}
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};