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,3 +1,321 @@
1
- import*as l from"node:fs";import*as c from"node:path";import*as $ from"node:os";import*as k from"node:http";import{execSync as O,spawnSync as N}from"node:child_process";import{bold as j,cyan as y,gray as x,green as g,yellow as C,red as S}from"../ui/output.mjs";import{detectAvailableProviders as b}from"../ai/providerRouter.mjs";function f(o,e){try{const t=e();return{label:o,...t}}catch(t){return{label:o,status:"error",message:t.message,fix:null}}}function a(o,e){return{status:"pass",message:o,detail:e||null,fix:null}}function u(o,e){return{status:"warn",message:o,detail:null,fix:e||null}}function m(o,e){return{status:"fail",message:o,detail:null,fix:e||null}}function v(){const o=process.version,e=parseInt(o.slice(1).split(".")[0],10);return e>=20?a(`Node.js ${o}`,"Node 20+ recommended"):e>=18?a(`Node.js ${o}`):m(`Node.js ${o} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function A(){try{const o=N("infernoflow",["--version"],{encoding:"utf8",timeout:5e3,shell:!0});return o.status===0&&o.stdout.trim()?a(`infernoflow v${o.stdout.trim()} on PATH`):u("infernoflow not resolvable in subprocesses","Ensure npm global bin folder is in your PATH")}catch{return u("infernoflow not resolvable in subprocesses","Ensure npm global bin folder is in your PATH")}}function E(o){try{return O("git rev-parse --git-dir",{cwd:o,stdio:"ignore"}),a("Git repository detected")}catch{return m("Not a git repository","git init && git add . && git commit -m 'init'")}}function P(o){const e=c.join(o,"inferno");return l.existsSync(e)?a("inferno/ directory exists"):m("inferno/ not found","infernoflow init")}function I(o){const e=c.join(o,"inferno");for(const t of["contract.json","capabilities.json"]){const s=c.join(e,t);if(l.existsSync(s))try{const r=(JSON.parse(l.readFileSync(s,"utf8")).capabilities||[]).length;return a(`${t} valid \u2014 ${r} capabilities`)}catch{return m(`${t} contains invalid JSON`,`Fix the JSON syntax in inferno/${t}`)}}return m("No contract.json or capabilities.json","infernoflow init")}function G(o){const e=c.join(o,"inferno","scenarios");if(!l.existsSync(e))return u("No scenarios/ directory","infernoflow init");const t=l.readdirSync(e).filter(s=>s.endsWith(".json"));return t.length?a(`${t.length} scenario file${t.length!==1?"s":""} found`):u("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function T(o){const e=c.join(o,"inferno","CHANGELOG.md");return l.existsSync(e)?a("inferno/CHANGELOG.md exists"):u("No inferno/CHANGELOG.md","infernoflow init")}function _(o){const e=c.join(o,"inferno","CONTEXT.md");if(!l.existsSync(e))return u("No CONTEXT.md generated","infernoflow context");const t=(Date.now()-l.statSync(e).mtimeMs)/(1e3*60*60*24);return t>7?u(`CONTEXT.md is ${Math.round(t)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(t)}d old)`)}function F(o){const e=c.join(o,".git","hooks"),t=c.join(e,"post-commit"),s=c.join(e,"pre-push"),i=l.existsSync(t)&&l.readFileSync(t,"utf8").includes("infernoflow"),r=l.existsSync(s)&&l.readFileSync(s,"utf8").includes("infernoflow");return i&&r?a("Git hooks installed (post-commit + pre-push)"):u(i||r?"Partial git hooks installed":"Git hooks not installed","infernoflow setup --yes")}function M(o){const e=[c.join(o,".cursor","mcp.json"),c.join(o,".mcp.json"),c.join($.homedir(),".cursor","mcp.json"),c.join($.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),c.join($.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const t of e)if(l.existsSync(t))try{const s=JSON.parse(l.readFileSync(t,"utf8")),i=s.mcpServers||s.mcp_servers||{};if(Object.keys(i).some(r=>r.toLowerCase().includes("inferno")))return a(`MCP server configured in ${c.basename(t)}`)}catch{}return u("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude config)")}function D(o){const e=b(o),t=Object.entries(e).filter(([,s])=>s).map(([s])=>s);return t.length?a(`AI provider${t.length!==1?"s":""}: ${t.join(", ")}`):u("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
2
- Or install Ollama (ollama.com) for free local AI
3
- Or use VS Code with GitHub Copilot (zero config)`)}async function H(){return new Promise(o=>{const e=k.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},t=>{o(a("Ollama running on localhost:11434"))});e.on("error",()=>o({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),e.on("timeout",()=>{e.destroy(),o({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function L(o){const e=c.join(o,"inferno","integrations.json");if(!l.existsSync(e))return{status:"info",message:"Cloud sync not configured (optional)",fix:"infernoflow cloud init",detail:null};try{return JSON.parse(l.readFileSync(e,"utf8")).cloud?.token?a("Cloud sync configured"):{status:"info",message:"Cloud sync not configured (optional)",fix:"infernoflow cloud init",detail:null}}catch{return{status:"info",message:"Cloud sync not configured (optional)",fix:null,detail:null}}}function R(o,e){const t=o.filter(i=>i.status==="warn"&&i.fix),s=[];for(const i of t){const r=i.fix;if(r.startsWith("infernoflow ")){const d=r.slice(12).split(" ");N("infernoflow",d,{cwd:e,encoding:"utf8",timeout:3e4}).status===0&&s.push(i.label)}}return s}function J(o){return o==="pass"?g("\u2714"):o==="warn"?C("\u26A0"):o==="fail"?S("\u2717"):x("\xB7")}function X(o,e){const t={pass:0,warn:0,fail:0,info:0,error:0};for(const n of o)t[n.status]=(t[n.status]||0)+1;console.log(),console.log(` ${j("\u{1F525} infernoflow doctor")}`),console.log();const s=Math.max(...o.map(n=>n.label.length))+2;for(const n of o)console.log(` ${J(n.status)} ${j(n.label.padEnd(s))} ${n.message}`),n.detail&&console.log(` ${" ".repeat(s)} ${x(n.detail)}`),n.fix&&(n.status==="warn"||n.status==="fail")&&console.log(` ${" ".repeat(s)} ${y("fix:")} ${x(n.fix)}`);console.log();const i=t.fail>0?S("issues found"):t.warn>0?C("warnings"):g("all good");console.log(` ${i} \u2014 ${g(String(t.pass))} pass \xB7 ${C(String(t.warn))} warn \xB7 ${S(String(t.fail))} fail (${e}ms)`),console.log();const r=[],d=o.filter(n=>n.status==="fail");for(const n of d)n.fix?r.push({priority:"\u{1F534}",text:n.fix}):r.push({priority:"\u{1F534}",text:`Fix: ${n.label} \u2014 ${n.message}`});const p=o.find(n=>n.label==="AI providers");p&&p.status!=="pass"&&r.push({priority:"\u{1F4A1}",text:`Connect an AI provider: ${y("infernoflow ai setup")} (unlocks explain, why, review, changelog)`});const w=o.filter(n=>n.status==="warn"&&n.label!=="AI providers");for(const n of w)n.fix&&r.push({priority:"\u26A0\uFE0F ",text:n.fix});if(r.length>0){console.log(` ${j("Next steps:")}`);for(const n of r)console.log(` ${n.priority} ${n.text}`);console.log(),w.length>0&&console.log(` ${x("Auto-fix warnings:")} ${y("infernoflow doctor --fix")}`),console.log()}else console.log(` ${g("\u2713")} You're all set. Run ${y("infernoflow demo")} to see the full capability chain.`),console.log()}async function Y(o){const e=o.slice(1),t=e.includes("--json"),s=e.includes("--fix"),i=process.cwd(),r=Date.now(),d=[f("Node.js version",()=>v()),f("infernoflow CLI",()=>A()),f("Git repository",()=>E(i)),f("inferno/ directory",()=>P(i)),f("Contract file",()=>I(i)),f("Scenarios",()=>G(i)),f("Changelog",()=>T(i)),f("CONTEXT.md",()=>_(i)),f("Git hooks",()=>F(i)),f("MCP server",()=>M(i)),f("AI providers",()=>D(i)),f("Cloud sync",()=>L(i)),await H().then(n=>({label:"Ollama (local AI)",...n}))],p=Date.now()-r;if(s){const n=R(d,i);if(n.length)return t||(console.log(),n.forEach(h=>console.log(` ${g("\u2714")} Fixed: ${h}`)),console.log()),Y(["doctor","--json"])}if(t){const n={pass:0,warn:0,fail:0,info:0};d.forEach(h=>n[h.status]=(n[h.status]||0)+1),console.log(JSON.stringify({ok:n.fail===0,counts:n,results:d,elapsed:p}));return}X(d,p),d.some(n=>n.status==="fail")&&process.exit(1)}export{Y as doctorCommand};
1
+ /**
2
+ * infernoflow doctor
3
+ *
4
+ * Comprehensive setup diagnostic — like `brew doctor`.
5
+ * Checks every component of the infernoflow setup and tells you
6
+ * exactly what's wrong and how to fix it.
7
+ *
8
+ * Usage:
9
+ * infernoflow doctor Print full diagnostic report
10
+ * infernoflow doctor --fix Auto-fix common issues
11
+ * infernoflow doctor --json Machine-readable output
12
+ */
13
+
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import * as os from "node:os";
17
+ import * as http from "node:http";
18
+ import { execSync, spawnSync } from "node:child_process";
19
+ import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
20
+ import { detectAvailableProviders } from "../ai/providerRouter.mjs";
21
+
22
+ // ── Check runners ─────────────────────────────────────────────────────────────
23
+
24
+ function check(label, fn) {
25
+ try {
26
+ const result = fn();
27
+ return { label, ...result };
28
+ } catch (err) {
29
+ return { label, status: "error", message: err.message, fix: null };
30
+ }
31
+ }
32
+
33
+ function pass(message, detail) { return { status: "pass", message, detail: detail || null, fix: null }; }
34
+ function warn(message, fix) { return { status: "warn", message, detail: null, fix: fix || null }; }
35
+ function fail(message, fix) { return { status: "fail", message, detail: null, fix: fix || null }; }
36
+
37
+ // ── Individual checks ─────────────────────────────────────────────────────────
38
+
39
+ function checkNodeVersion() {
40
+ const v = process.version;
41
+ const major = parseInt(v.slice(1).split(".")[0], 10);
42
+ if (major >= 20) return pass(`Node.js ${v}`, "Node 20+ recommended");
43
+ if (major >= 18) return pass(`Node.js ${v}`);
44
+ return fail(`Node.js ${v} — infernoflow requires Node 18+`, "Install Node 20 from nodejs.org");
45
+ }
46
+
47
+ function checkCli() {
48
+ // Use shell: true so Windows .cmd wrappers resolve correctly.
49
+ // If the spawn fails entirely, we're still clearly running — downgrade to warn.
50
+ try {
51
+ const r = spawnSync("infernoflow", ["--version"], { encoding: "utf8", timeout: 5000, shell: true });
52
+ if (r.status === 0 && r.stdout.trim()) {
53
+ return pass(`infernoflow v${r.stdout.trim()} on PATH`);
54
+ }
55
+ // Running but PATH lookup failed for subprocesses (common on Windows)
56
+ return warn("infernoflow not resolvable in subprocesses", "Ensure npm global bin folder is in your PATH");
57
+ } catch {
58
+ return warn("infernoflow not resolvable in subprocesses", "Ensure npm global bin folder is in your PATH");
59
+ }
60
+ }
61
+
62
+ function checkGitRepo(cwd) {
63
+ try {
64
+ execSync("git rev-parse --git-dir", { cwd, stdio: "ignore" });
65
+ return pass("Git repository detected");
66
+ } catch {
67
+ return fail("Not a git repository", "git init && git add . && git commit -m 'init'");
68
+ }
69
+ }
70
+
71
+ function checkInfernoDir(cwd) {
72
+ const infernoDir = path.join(cwd, "inferno");
73
+ if (!fs.existsSync(infernoDir)) return fail("inferno/ not found", "infernoflow init");
74
+ return pass("inferno/ directory exists");
75
+ }
76
+
77
+ function checkContract(cwd) {
78
+ const infernoDir = path.join(cwd, "inferno");
79
+ for (const f of ["contract.json", "capabilities.json"]) {
80
+ const p = path.join(infernoDir, f);
81
+ if (!fs.existsSync(p)) continue;
82
+ try {
83
+ const data = JSON.parse(fs.readFileSync(p, "utf8"));
84
+ const caps = (data.capabilities || []).length;
85
+ return pass(`${f} valid — ${caps} capabilities`);
86
+ } catch {
87
+ return fail(`${f} contains invalid JSON`, `Fix the JSON syntax in inferno/${f}`);
88
+ }
89
+ }
90
+ return fail("No contract.json or capabilities.json", "infernoflow init");
91
+ }
92
+
93
+ function checkScenarios(cwd) {
94
+ const scenDir = path.join(cwd, "inferno", "scenarios");
95
+ if (!fs.existsSync(scenDir)) return warn("No scenarios/ directory", "infernoflow init");
96
+ const files = fs.readdirSync(scenDir).filter(f => f.endsWith(".json"));
97
+ if (!files.length) return warn("scenarios/ is empty", "Add scenario files or run infernoflow suggest");
98
+ return pass(`${files.length} scenario file${files.length !== 1 ? "s" : ""} found`);
99
+ }
100
+
101
+ function checkChangelog(cwd) {
102
+ const p = path.join(cwd, "inferno", "CHANGELOG.md");
103
+ if (!fs.existsSync(p)) return warn("No inferno/CHANGELOG.md", "infernoflow init");
104
+ return pass("inferno/CHANGELOG.md exists");
105
+ }
106
+
107
+ function checkContextMd(cwd) {
108
+ const p = path.join(cwd, "inferno", "CONTEXT.md");
109
+ if (!fs.existsSync(p)) return warn("No CONTEXT.md generated", "infernoflow context");
110
+ const age = (Date.now() - fs.statSync(p).mtimeMs) / (1000 * 60 * 60 * 24);
111
+ if (age > 7) return warn(`CONTEXT.md is ${Math.round(age)} days old — may be stale`, "infernoflow context");
112
+ return pass(`CONTEXT.md present (${Math.round(age)}d old)`);
113
+ }
114
+
115
+ function checkGitHooks(cwd) {
116
+ const hooksDir = path.join(cwd, ".git", "hooks");
117
+ const postCommit = path.join(hooksDir, "post-commit");
118
+ const prePush = path.join(hooksDir, "pre-push");
119
+ const hasPost = fs.existsSync(postCommit) && fs.readFileSync(postCommit, "utf8").includes("infernoflow");
120
+ const hasPre = fs.existsSync(prePush) && fs.readFileSync(prePush, "utf8").includes("infernoflow");
121
+ if (hasPost && hasPre) return pass("Git hooks installed (post-commit + pre-push)");
122
+ if (hasPost || hasPre) return warn("Partial git hooks installed", "infernoflow setup --yes");
123
+ return warn("Git hooks not installed", "infernoflow setup --yes");
124
+ }
125
+
126
+ function checkMcp(cwd) {
127
+ // Check for MCP server in cursor config or .mcp.json
128
+ const checks = [
129
+ path.join(cwd, ".cursor", "mcp.json"),
130
+ path.join(cwd, ".mcp.json"),
131
+ path.join(os.homedir(), ".cursor", "mcp.json"),
132
+ path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
133
+ path.join(os.homedir(), "AppData", "Roaming", "Claude", "claude_desktop_config.json"),
134
+ ];
135
+ for (const p of checks) {
136
+ if (!fs.existsSync(p)) continue;
137
+ try {
138
+ const data = JSON.parse(fs.readFileSync(p, "utf8"));
139
+ const servers = data.mcpServers || data.mcp_servers || {};
140
+ if (Object.keys(servers).some(k => k.toLowerCase().includes("inferno"))) {
141
+ return pass(`MCP server configured in ${path.basename(p)}`);
142
+ }
143
+ } catch {}
144
+ }
145
+ return warn("MCP server not configured", "infernoflow setup --yes (adds to Cursor/Claude config)");
146
+ }
147
+
148
+ function checkAiProviders(cwd) {
149
+ const providers = detectAvailableProviders(cwd);
150
+ const available = Object.entries(providers).filter(([, v]) => v).map(([k]) => k);
151
+
152
+ if (available.length) return pass(`AI provider${available.length !== 1 ? "s" : ""}: ${available.join(", ")}`);
153
+
154
+ return warn(
155
+ "No AI provider configured",
156
+ "Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY\n" +
157
+ " Or install Ollama (ollama.com) for free local AI\n" +
158
+ " Or use VS Code with GitHub Copilot (zero config)"
159
+ );
160
+ }
161
+
162
+ async function checkOllama() {
163
+ return new Promise(resolve => {
164
+ const req = http.get({ hostname: "localhost", port: 11434, path: "/api/tags", timeout: 1500 }, res => {
165
+ resolve(pass("Ollama running on localhost:11434"));
166
+ });
167
+ req.on("error", () => resolve({ status: "info", message: "Ollama not running (optional)", fix: "ollama serve", detail: null }));
168
+ req.on("timeout", () => { req.destroy(); resolve({ status: "info", message: "Ollama not running (optional)", fix: null, detail: null }); });
169
+ });
170
+ }
171
+
172
+ function checkCloudToken(cwd) {
173
+ const p = path.join(cwd, "inferno", "integrations.json");
174
+ if (!fs.existsSync(p)) return { status: "info", message: "Cloud sync not configured (optional)", fix: "infernoflow cloud init", detail: null };
175
+ try {
176
+ const data = JSON.parse(fs.readFileSync(p, "utf8"));
177
+ if (data.cloud?.token) return pass("Cloud sync configured");
178
+ return { status: "info", message: "Cloud sync not configured (optional)", fix: "infernoflow cloud init", detail: null };
179
+ } catch {
180
+ return { status: "info", message: "Cloud sync not configured (optional)", fix: null, detail: null };
181
+ }
182
+ }
183
+
184
+ // ── Auto-fix ──────────────────────────────────────────────────────────────────
185
+
186
+ function autoFix(results, cwd) {
187
+ const fixable = results.filter(r => r.status === "warn" && r.fix);
188
+ const fixed = [];
189
+
190
+ for (const r of fixable) {
191
+ const fix = r.fix;
192
+ if (fix.startsWith("infernoflow ")) {
193
+ const args = fix.slice("infernoflow ".length).split(" ");
194
+ const res = spawnSync("infernoflow", args, { cwd, encoding: "utf8", timeout: 30_000 });
195
+ if (res.status === 0) fixed.push(r.label);
196
+ }
197
+ }
198
+ return fixed;
199
+ }
200
+
201
+ // ── Renderer ──────────────────────────────────────────────────────────────────
202
+
203
+ function icon(status) {
204
+ if (status === "pass") return green("✔");
205
+ if (status === "warn") return yellow("⚠");
206
+ if (status === "fail") return red("✗");
207
+ return gray("·");
208
+ }
209
+
210
+ function printReport(results, elapsed) {
211
+ const counts = { pass: 0, warn: 0, fail: 0, info: 0, error: 0 };
212
+ for (const r of results) counts[r.status] = (counts[r.status] || 0) + 1;
213
+
214
+ console.log();
215
+ console.log(` ${bold("🔥 infernoflow doctor")}`);
216
+ console.log();
217
+
218
+ const w = Math.max(...results.map(r => r.label.length)) + 2;
219
+ for (const r of results) {
220
+ console.log(` ${icon(r.status)} ${bold(r.label.padEnd(w))} ${r.message}`);
221
+ if (r.detail) console.log(` ${" ".repeat(w)} ${gray(r.detail)}`);
222
+ if (r.fix && (r.status === "warn" || r.status === "fail")) {
223
+ console.log(` ${" ".repeat(w)} ${cyan("fix:")} ${gray(r.fix)}`);
224
+ }
225
+ }
226
+
227
+ console.log();
228
+ const overall = counts.fail > 0 ? red("issues found") : counts.warn > 0 ? yellow("warnings") : green("all good");
229
+ console.log(` ${overall} — ${green(String(counts.pass))} pass · ${yellow(String(counts.warn))} warn · ${red(String(counts.fail))} fail (${elapsed}ms)`);
230
+ console.log();
231
+
232
+ // Prioritized action list — show concrete next steps, not just status flags
233
+ const actions = [];
234
+
235
+ // Failures first
236
+ const fails = results.filter(r => r.status === "fail");
237
+ for (const f of fails) {
238
+ if (f.fix) actions.push({ priority: "🔴", text: f.fix });
239
+ else actions.push({ priority: "🔴", text: `Fix: ${f.label} — ${f.message}` });
240
+ }
241
+
242
+ // AI provider — very common gap, elevate it
243
+ const aiCheck = results.find(r => r.label === "AI providers");
244
+ if (aiCheck && aiCheck.status !== "pass") {
245
+ actions.push({ priority: "💡", text: `Connect an AI provider: ${cyan("infernoflow ai setup")} (unlocks explain, why, review, changelog)` });
246
+ }
247
+
248
+ // Warnings
249
+ const warns = results.filter(r => r.status === "warn" && r.label !== "AI providers");
250
+ for (const w of warns) {
251
+ if (w.fix) actions.push({ priority: "⚠️ ", text: w.fix });
252
+ }
253
+
254
+ if (actions.length > 0) {
255
+ console.log(` ${bold("Next steps:")}`);
256
+ for (const a of actions) {
257
+ console.log(` ${a.priority} ${a.text}`);
258
+ }
259
+ console.log();
260
+ if (warns.length > 0) {
261
+ console.log(` ${gray("Auto-fix warnings:")} ${cyan("infernoflow doctor --fix")}`);
262
+ }
263
+ console.log();
264
+ } else {
265
+ console.log(` ${green("✓")} You're all set. Run ${cyan("infernoflow demo")} to see the full capability chain.`);
266
+ console.log();
267
+ }
268
+ }
269
+
270
+ // ── Entry ─────────────────────────────────────────────────────────────────────
271
+
272
+ export async function doctorCommand(rawArgs) {
273
+ const args = rawArgs.slice(1);
274
+ const jsonMode = args.includes("--json");
275
+ const fixMode = args.includes("--fix");
276
+ const cwd = process.cwd();
277
+ const start = Date.now();
278
+
279
+ const results = [
280
+ check("Node.js version", () => checkNodeVersion()),
281
+ check("infernoflow CLI", () => checkCli()),
282
+ check("Git repository", () => checkGitRepo(cwd)),
283
+ check("inferno/ directory",() => checkInfernoDir(cwd)),
284
+ check("Contract file", () => checkContract(cwd)),
285
+ check("Scenarios", () => checkScenarios(cwd)),
286
+ check("Changelog", () => checkChangelog(cwd)),
287
+ check("CONTEXT.md", () => checkContextMd(cwd)),
288
+ check("Git hooks", () => checkGitHooks(cwd)),
289
+ check("MCP server", () => checkMcp(cwd)),
290
+ check("AI providers", () => checkAiProviders(cwd)),
291
+ check("Cloud sync", () => checkCloudToken(cwd)),
292
+ await checkOllama().then(r => ({ label: "Ollama (local AI)", ...r })),
293
+ ];
294
+
295
+ const elapsed = Date.now() - start;
296
+
297
+ if (fixMode) {
298
+ const fixed = autoFix(results, cwd);
299
+ if (fixed.length) {
300
+ if (!jsonMode) {
301
+ console.log();
302
+ fixed.forEach(f => console.log(` ${green("✔")} Fixed: ${f}`));
303
+ console.log();
304
+ }
305
+ // Re-run checks after fixing
306
+ return doctorCommand(["doctor", "--json"]);
307
+ }
308
+ }
309
+
310
+ if (jsonMode) {
311
+ const counts = { pass: 0, warn: 0, fail: 0, info: 0 };
312
+ results.forEach(r => counts[r.status] = (counts[r.status] || 0) + 1);
313
+ console.log(JSON.stringify({ ok: counts.fail === 0, counts, results, elapsed }));
314
+ return;
315
+ }
316
+
317
+ printReport(results, elapsed);
318
+
319
+ const hasFail = results.some(r => r.status === "fail");
320
+ if (hasFail) process.exit(1);
321
+ }