infernoflow 0.33.0 → 0.34.0

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