infernoflow 0.37.1 → 0.37.4

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 (88) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -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};