infernoflow 0.42.7 → 0.43.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog — infernoflow
2
2
 
3
+ ## 0.43.0 — 2026-05-06
4
+
5
+ ### Added
6
+ - **`infernoflow contract graph` auto-runs scan first** if `inferno/scan.json` is missing or older than 5 minutes. No more two-step "scan then graph" — one command does both.
7
+ - **UI layer in scan + graph** — `infernoflow scan` now also walks JSX/TSX/Vue/Svelte files for interactive elements: `<button onClick={...}>`, `<input onChange={...}>`, `<form onSubmit={...}>`, `<a onClick={...}>`, `<select onChange={...}>`. Each element's handler is mapped back to the capability that owns it. The graph shows a separate UI tier so you can see "click 'Add Task' button → CreateTask capability → API call" as a visual flow.
8
+ - In Mermaid output: UI nodes appear as round-cornered nodes with a tag emoji (🔘 button, ⌨️ input, 📝 form, 🔗 link, ▾ select).
9
+ - In HTML output: UI nodes are smaller green dashed circles, distinct from the capability circles. Tooltip shows the element type.
10
+ - **Combined workflow** — for the "show me how the app works" view, run:
11
+ ```
12
+ infernoflow contract graph --html
13
+ ```
14
+ Auto-scans, builds the dep tree, attaches UI elements, generates the interactive D3 page. One command.
15
+
16
+ ## 0.42.9 — 2026-05-06
17
+
18
+ ### Added
19
+ - **Visual graph output** for `infernoflow contract graph` — two new flags:
20
+ - `--mermaid` prints Mermaid syntax to stdout. Color-coded by stability (frozen=red, stable=yellow, experimental=blue). Renders directly in GitHub markdown, VS Code preview (with the Mermaid extension), or paste into https://mermaid.live for instant browser rendering. Pipe to a file: `infernoflow contract graph --mermaid > graph.md`.
21
+ - `--html` generates a self-contained `inferno/graph.html` with an interactive D3 force-directed graph: drag nodes, scroll to zoom, hover for details. Open it in any browser. No external runtime needed beyond a one-time D3 fetch from cdnjs.
22
+
23
+ ### Notes
24
+ - The default ASCII output is unchanged. `--mermaid` and `--html` are opt-in alternatives for when you want a real diagram.
25
+
26
+ ## 0.42.8 — 2026-05-06
27
+
28
+ ### Fixed
29
+ - **`infernoflow doctor` no longer reports a false-positive "CLI not found on PATH" on Windows**. `npm install -g` creates an `infernoflow.cmd` shim on Windows, not an `.exe`, and `spawnSync` won't resolve `.cmd` files without `shell: true`. The PATH-check now passes the right flag on Windows. Belt-and-suspenders: if doctor is running, the CLI is by definition reachable, so the check now defaults to pass even if the spawn probe fails on exotic shells.
30
+
3
31
  ## 0.42.7 — 2026-05-06
4
32
 
5
33
  ### Fixed
@@ -95,6 +123,11 @@ Same content as 0.42.6 — that version got registered on npm during a flaky pub
95
123
  - extension v0.7.2 + CLI hotfixes: auto-capture, CodeLens, bulk + orphan delete, MCP setup tools fix, graph crash guard
96
124
  - VS Code Marketplace badge + extension install section
97
125
 
126
+ - infernoflow CLI v0.42.7: graph crash fix + MCP setup/error-handling hotfixes; README v0.7.2 extension features
127
+ - infernoflow CLI v0.42.6: graph crash fix + MCP setup/error-handling hotfixes
128
+ - extension v0.7.2 + CLI hotfixes: auto-capture, CodeLens, bulk + orphan delete, MCP setup tools fix, graph crash guard
129
+ - VS Code Marketplace badge + extension install section
130
+
98
131
  ## 0.42.1 — 2026-05-03
99
132
 
100
133
  ### Added
@@ -1,4 +1,4 @@
1
- import*as s from"node:fs";import*as l from"node:path";import*as h from"node:os";import*as O from"node:http";import{execSync as b,spawnSync as k}from"node:child_process";import{fileURLToPath as v}from"node:url";import{bold as C,cyan as N,gray as w,green as y,yellow as x,red as j}from"../ui/output.mjs";import{detectAvailableProviders as A}from"../ai/providerRouter.mjs";function c(n,o){try{const e=o();return{label:n,...e}}catch(e){return{label:n,status:"error",message:e.message,fix:null}}}function a(n,o){return{status:"pass",message:n,detail:o||null,fix:null}}function f(n,o){return{status:"warn",message:n,detail:null,fix:o||null}}function m(n,o){return{status:"fail",message:n,detail:null,fix:o||null}}function _(){const n=process.version,o=parseInt(n.slice(1).split(".")[0],10);return o>=20?a(`Node.js ${n}`,"Node 20+ recommended"):o>=18?a(`Node.js ${n}`):m(`Node.js ${n} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function P(){try{const n=k("infernoflow",["--version"],{encoding:"utf8",timeout:5e3});return n.status===0?a(`infernoflow v${n.stdout.trim()} installed`):m("infernoflow CLI not found on PATH","npm install -g infernoflow")}catch{return m("infernoflow CLI not found on PATH","npm install -g infernoflow")}}function E(n){try{return b("git rev-parse --git-dir",{cwd:n,stdio:"ignore"}),a("Git repository detected")}catch{return m("Not a git repository","git init && git add . && git commit -m 'init'")}}function I(n){const o=l.join(n,"inferno");return s.existsSync(o)?a("inferno/ directory exists"):m("inferno/ not found","infernoflow init")}function T(n){const o=l.join(n,"inferno");if((()=>{try{return JSON.parse(s.readFileSync(l.join(o,"config.json"),"utf8"))}catch{return{}}})().mode==="memory"){const i=l.join(o,"sessions.jsonl");if(!s.existsSync(i))return a("Memory mode \u2014 sessions.jsonl will be created on first log");let t=0;try{t=s.readFileSync(i,"utf8").split(`
2
- `).filter(Boolean).length}catch{}return a(`Memory mode \u2014 ${t} session entr${t===1?"y":"ies"}`)}for(const i of["contract.json","capabilities.json"]){const t=l.join(o,i);if(s.existsSync(t))try{const u=(JSON.parse(s.readFileSync(t,"utf8")).capabilities||[]).length;return a(`${i} valid \u2014 ${u} capabilities`)}catch{return m(`${i} contains invalid JSON`,`Fix the JSON syntax in inferno/${i}`)}}return m("No contract.json/capabilities.json (and not in memory mode)","infernoflow init or infernoflow init --mode full")}function S(n){try{return JSON.parse(s.readFileSync(l.join(n,"inferno","config.json"),"utf8")).mode==="memory"}catch{return!1}}function G(n){if(S(n))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const o=l.join(n,"inferno","scenarios");if(!s.existsSync(o))return f("No scenarios/ directory","infernoflow init");const e=s.readdirSync(o).filter(i=>i.endsWith(".json"));return e.length?a(`${e.length} scenario file${e.length!==1?"s":""} found`):f("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function F(n){if(S(n))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const o=l.join(n,"inferno","CHANGELOG.md");return s.existsSync(o)?a("inferno/CHANGELOG.md exists"):f("No inferno/CHANGELOG.md","infernoflow init")}function M(n){if(S(n))return{status:"info",message:"n/a in memory mode (CLAUDE.md is auto-maintained)",detail:null,fix:null};const o=l.join(n,"inferno","CONTEXT.md");if(!s.existsSync(o))return f("No CONTEXT.md generated","infernoflow context");const e=(Date.now()-s.statSync(o).mtimeMs)/(1e3*60*60*24);return e>7?f(`CONTEXT.md is ${Math.round(e)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(e)}d old)`)}function D(n){const o=l.join(n,".git","hooks"),e=l.join(o,"post-commit"),i=l.join(o,"pre-push"),t=s.existsSync(e)&&s.readFileSync(e,"utf8").includes("infernoflow"),r=s.existsSync(i)&&s.readFileSync(i,"utf8").includes("infernoflow");return t&&r?a("Git hooks installed (post-commit + pre-push)"):f(t||r?"Partial git hooks installed":"Git hooks not installed","infernoflow setup --yes")}function L(n){const o=[l.join(n,".cursor","mcp.json"),l.join(n,".mcp.json"),l.join(h.homedir(),".cursor","mcp.json"),l.join(h.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),l.join(h.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const e of o)if(s.existsSync(e))try{const i=JSON.parse(s.readFileSync(e,"utf8")),t=i.mcpServers||i.mcp_servers||{};if(Object.keys(t).some(r=>r.toLowerCase().includes("inferno")))return a(`MCP server configured in ${l.basename(e)}`)}catch{}return f("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude config)")}function R(n){const o=A(n),e=Object.entries(o).filter(([,i])=>i).map(([i])=>i);return e.length?a(`AI provider${e.length!==1?"s":""}: ${e.join(", ")}`):f("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
1
+ import*as s from"node:fs";import*as l from"node:path";import*as h from"node:os";import*as N from"node:http";import{execSync as O,spawnSync as k}from"node:child_process";import{fileURLToPath as v}from"node:url";import{bold as b,cyan as C,gray as w,green as y,yellow as x,red as j}from"../ui/output.mjs";import{detectAvailableProviders as A}from"../ai/providerRouter.mjs";function c(n,o){try{const e=o();return{label:n,...e}}catch(e){return{label:n,status:"error",message:e.message,fix:null}}}function a(n,o){return{status:"pass",message:n,detail:o||null,fix:null}}function f(n,o){return{status:"warn",message:n,detail:null,fix:o||null}}function g(n,o){return{status:"fail",message:n,detail:null,fix:o||null}}function _(){const n=process.version,o=parseInt(n.slice(1).split(".")[0],10);return o>=20?a(`Node.js ${n}`,"Node 20+ recommended"):o>=18?a(`Node.js ${n}`):g(`Node.js ${n} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function P(){try{const n=k("infernoflow",["--version"],{encoding:"utf8",timeout:5e3,shell:process.platform==="win32"});return n.status===0?a(`infernoflow v${n.stdout.trim()} installed`):a("infernoflow CLI on PATH (version probe failed but doctor itself ran)")}catch{return a("infernoflow CLI on PATH (version probe threw but doctor itself ran)")}}function E(n){try{return O("git rev-parse --git-dir",{cwd:n,stdio:"ignore"}),a("Git repository detected")}catch{return g("Not a git repository","git init && git add . && git commit -m 'init'")}}function I(n){const o=l.join(n,"inferno");return s.existsSync(o)?a("inferno/ directory exists"):g("inferno/ not found","infernoflow init")}function T(n){const o=l.join(n,"inferno");if((()=>{try{return JSON.parse(s.readFileSync(l.join(o,"config.json"),"utf8"))}catch{return{}}})().mode==="memory"){const i=l.join(o,"sessions.jsonl");if(!s.existsSync(i))return a("Memory mode \u2014 sessions.jsonl will be created on first log");let t=0;try{t=s.readFileSync(i,"utf8").split(`
2
+ `).filter(Boolean).length}catch{}return a(`Memory mode \u2014 ${t} session entr${t===1?"y":"ies"}`)}for(const i of["contract.json","capabilities.json"]){const t=l.join(o,i);if(s.existsSync(t))try{const u=(JSON.parse(s.readFileSync(t,"utf8")).capabilities||[]).length;return a(`${i} valid \u2014 ${u} capabilities`)}catch{return g(`${i} contains invalid JSON`,`Fix the JSON syntax in inferno/${i}`)}}return g("No contract.json/capabilities.json (and not in memory mode)","infernoflow init or infernoflow init --mode full")}function S(n){try{return JSON.parse(s.readFileSync(l.join(n,"inferno","config.json"),"utf8")).mode==="memory"}catch{return!1}}function G(n){if(S(n))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const o=l.join(n,"inferno","scenarios");if(!s.existsSync(o))return f("No scenarios/ directory","infernoflow init");const e=s.readdirSync(o).filter(i=>i.endsWith(".json"));return e.length?a(`${e.length} scenario file${e.length!==1?"s":""} found`):f("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function F(n){if(S(n))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const o=l.join(n,"inferno","CHANGELOG.md");return s.existsSync(o)?a("inferno/CHANGELOG.md exists"):f("No inferno/CHANGELOG.md","infernoflow init")}function M(n){if(S(n))return{status:"info",message:"n/a in memory mode (CLAUDE.md is auto-maintained)",detail:null,fix:null};const o=l.join(n,"inferno","CONTEXT.md");if(!s.existsSync(o))return f("No CONTEXT.md generated","infernoflow context");const e=(Date.now()-s.statSync(o).mtimeMs)/(1e3*60*60*24);return e>7?f(`CONTEXT.md is ${Math.round(e)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(e)}d old)`)}function D(n){const o=l.join(n,".git","hooks"),e=l.join(o,"post-commit"),i=l.join(o,"pre-push"),t=s.existsSync(e)&&s.readFileSync(e,"utf8").includes("infernoflow"),r=s.existsSync(i)&&s.readFileSync(i,"utf8").includes("infernoflow");return t&&r?a("Git hooks installed (post-commit + pre-push)"):f(t||r?"Partial git hooks installed":"Git hooks not installed","infernoflow setup --yes")}function L(n){const o=[l.join(n,".cursor","mcp.json"),l.join(n,".mcp.json"),l.join(h.homedir(),".cursor","mcp.json"),l.join(h.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),l.join(h.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const e of o)if(s.existsSync(e))try{const i=JSON.parse(s.readFileSync(e,"utf8")),t=i.mcpServers||i.mcp_servers||{};if(Object.keys(t).some(r=>r.toLowerCase().includes("inferno")))return a(`MCP server configured in ${l.basename(e)}`)}catch{}return f("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude config)")}function R(n){const o=A(n),e=Object.entries(o).filter(([,i])=>i).map(([i])=>i);return e.length?a(`AI provider${e.length!==1?"s":""}: ${e.join(", ")}`):f("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
3
3
  Or install Ollama (ollama.com) for free local AI
4
- Or use VS Code with GitHub Copilot (zero config)`)}async function J(){return new Promise(n=>{const o=O.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},e=>{n(a("Ollama running on localhost:11434"))});o.on("error",()=>n({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),o.on("timeout",()=>{o.destroy(),n({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function H(){const n=l.join(h.homedir(),".infernoflow","credentials.json");if(!s.existsSync(n))return{status:"info",message:"Not logged in to cloud (optional)",fix:"infernoflow login",detail:null};try{const o=JSON.parse(s.readFileSync(n,"utf8")),e=o.user?.login||o.user?.name||o.user?.email||"unknown";if(o.mode==="supabase"&&o.access_token){if(o.expires_at){const i=new Date(o.expires_at).getTime();if(Date.now()>i)return f(`JWT expired for ${e} \u2014 refresh on next log will retry`,"infernoflow login")}return a(`Authenticated as ${e} (Supabase JWT \u2014 auth.uid() writes)`)}return o.mode==="device-flow"&&o.github_access_token?{status:"info",message:`Identity-only as ${e} (device flow \u2014 anon-mode writes)`,fix:"infernoflow login (without --device-flow, for full auth)",detail:null}:o.access_token?f(`Legacy login for ${e} \u2014 re-run for authenticated cloud writes`,"infernoflow logout && infernoflow login"):{status:"info",message:"Credentials file present but no recognised token",fix:"infernoflow logout && infernoflow login",detail:null}}catch{return f("Credentials file unreadable","infernoflow logout && infernoflow login")}}function X(){try{const n=v(import.meta.url),o=l.resolve(l.dirname(n),"..","..","bin","infernoflow.mjs");if(!s.existsSync(o))return{status:"info",message:"bin/infernoflow.mjs not found from doctor location",fix:null,detail:null};const i=[...s.readFileSync(o,"utf8").matchAll(/import\("\.\.\/lib\/(commands\/[^"]+|telemetry\.mjs)"\)/g)],t=[],r=l.resolve(l.dirname(o),"..");for(const u of i){const g=u[1],$=l.join(r,"lib",g);s.existsSync($)||t.push(g)}return t.length?m(`${t.length} routed command(s) missing module files: ${t.slice(0,3).join(", ")}${t.length>3?"\u2026":""}`,"Restore the missing files or remove their entries from bin/infernoflow.mjs"):a(`All ${i.length} routed commands resolve to real files`)}catch(n){return{status:"info",message:`Router integrity check skipped: ${n.message}`,fix:null,detail:null}}}function K(n){const o=l.join(n,".gitignore");if(!s.existsSync(o))return{status:"info",message:".gitignore not found",fix:null,detail:null};const e=s.readFileSync(o,"utf8");return/^(?:\*\*\/)?node_modules\/?$/m.test(e)?a(".gitignore excludes node_modules"):f(".gitignore does not exclude node_modules","Add 'node_modules/' (and '**/node_modules/') to .gitignore")}function W(n,o){const e=n.filter(t=>t.status==="warn"&&t.fix),i=[];for(const t of e){const r=t.fix;if(r.startsWith("infernoflow ")){const u=r.slice(12).split(" ");k("infernoflow",u,{cwd:o,encoding:"utf8",timeout:3e4}).status===0&&i.push(t.label)}}return i}function Y(n){return n==="pass"?y("\u2714"):n==="warn"?x("\u26A0"):n==="fail"?j("\u2717"):w("\xB7")}function U(n,o){const e={pass:0,warn:0,fail:0,info:0,error:0};for(const r of n)e[r.status]=(e[r.status]||0)+1;console.log(),console.log(` ${C("\u{1F525} infernoflow doctor")}`),console.log();const i=Math.max(...n.map(r=>r.label.length))+2;for(const r of n)console.log(` ${Y(r.status)} ${C(r.label.padEnd(i))} ${r.message}`),r.detail&&console.log(` ${" ".repeat(i)} ${w(r.detail)}`),r.fix&&(r.status==="warn"||r.status==="fail")&&console.log(` ${" ".repeat(i)} ${N("fix:")} ${w(r.fix)}`);console.log();const t=e.fail>0?j("issues found"):e.warn>0?x("warnings"):y("all good");console.log(` ${t} \u2014 ${y(String(e.pass))} pass \xB7 ${x(String(e.warn))} warn \xB7 ${j(String(e.fail))} fail (${o}ms)`),console.log(),(e.warn>0||e.fail>0)&&(console.log(` Run ${N("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function q(n){const o=n.slice(1),e=o.includes("--json"),i=o.includes("--fix"),t=process.cwd(),r=Date.now(),u=[c("Node.js version",()=>_()),c("infernoflow CLI",()=>P()),c("Git repository",()=>E(t)),c("inferno/ directory",()=>I(t)),c("Contract / mode",()=>T(t)),c("Scenarios",()=>G(t)),c("Changelog",()=>F(t)),c("CONTEXT.md",()=>M(t)),c("Git hooks",()=>D(t)),c("MCP server",()=>L(t)),c("AI providers",()=>R(t)),c("Cloud sync",()=>H()),c(".gitignore",()=>K(t)),c("Router integrity",()=>X()),await J().then(d=>({label:"Ollama (local AI)",...d}))],g=Date.now()-r;if(i){const d=W(u,t);if(d.length)return e||(console.log(),d.forEach(p=>console.log(` ${y("\u2714")} Fixed: ${p}`)),console.log()),q(["doctor","--json"])}if(e){const d={pass:0,warn:0,fail:0,info:0};u.forEach(p=>d[p.status]=(d[p.status]||0)+1),console.log(JSON.stringify({ok:d.fail===0,counts:d,results:u,elapsed:g}));return}U(u,g),u.some(d=>d.status==="fail")&&process.exit(1)}export{q as doctorCommand};
4
+ Or use VS Code with GitHub Copilot (zero config)`)}async function J(){return new Promise(n=>{const o=N.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},e=>{n(a("Ollama running on localhost:11434"))});o.on("error",()=>n({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),o.on("timeout",()=>{o.destroy(),n({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function H(){const n=l.join(h.homedir(),".infernoflow","credentials.json");if(!s.existsSync(n))return{status:"info",message:"Not logged in to cloud (optional)",fix:"infernoflow login",detail:null};try{const o=JSON.parse(s.readFileSync(n,"utf8")),e=o.user?.login||o.user?.name||o.user?.email||"unknown";if(o.mode==="supabase"&&o.access_token){if(o.expires_at){const i=new Date(o.expires_at).getTime();if(Date.now()>i)return f(`JWT expired for ${e} \u2014 refresh on next log will retry`,"infernoflow login")}return a(`Authenticated as ${e} (Supabase JWT \u2014 auth.uid() writes)`)}return o.mode==="device-flow"&&o.github_access_token?{status:"info",message:`Identity-only as ${e} (device flow \u2014 anon-mode writes)`,fix:"infernoflow login (without --device-flow, for full auth)",detail:null}:o.access_token?f(`Legacy login for ${e} \u2014 re-run for authenticated cloud writes`,"infernoflow logout && infernoflow login"):{status:"info",message:"Credentials file present but no recognised token",fix:"infernoflow logout && infernoflow login",detail:null}}catch{return f("Credentials file unreadable","infernoflow logout && infernoflow login")}}function X(){try{const n=v(import.meta.url),o=l.resolve(l.dirname(n),"..","..","bin","infernoflow.mjs");if(!s.existsSync(o))return{status:"info",message:"bin/infernoflow.mjs not found from doctor location",fix:null,detail:null};const i=[...s.readFileSync(o,"utf8").matchAll(/import\("\.\.\/lib\/(commands\/[^"]+|telemetry\.mjs)"\)/g)],t=[],r=l.resolve(l.dirname(o),"..");for(const u of i){const m=u[1],$=l.join(r,"lib",m);s.existsSync($)||t.push(m)}return t.length?g(`${t.length} routed command(s) missing module files: ${t.slice(0,3).join(", ")}${t.length>3?"\u2026":""}`,"Restore the missing files or remove their entries from bin/infernoflow.mjs"):a(`All ${i.length} routed commands resolve to real files`)}catch(n){return{status:"info",message:`Router integrity check skipped: ${n.message}`,fix:null,detail:null}}}function K(n){const o=l.join(n,".gitignore");if(!s.existsSync(o))return{status:"info",message:".gitignore not found",fix:null,detail:null};const e=s.readFileSync(o,"utf8");return/^(?:\*\*\/)?node_modules\/?$/m.test(e)?a(".gitignore excludes node_modules"):f(".gitignore does not exclude node_modules","Add 'node_modules/' (and '**/node_modules/') to .gitignore")}function W(n,o){const e=n.filter(t=>t.status==="warn"&&t.fix),i=[];for(const t of e){const r=t.fix;if(r.startsWith("infernoflow ")){const u=r.slice(12).split(" ");k("infernoflow",u,{cwd:o,encoding:"utf8",timeout:3e4}).status===0&&i.push(t.label)}}return i}function Y(n){return n==="pass"?y("\u2714"):n==="warn"?x("\u26A0"):n==="fail"?j("\u2717"):w("\xB7")}function U(n,o){const e={pass:0,warn:0,fail:0,info:0,error:0};for(const r of n)e[r.status]=(e[r.status]||0)+1;console.log(),console.log(` ${b("\u{1F525} infernoflow doctor")}`),console.log();const i=Math.max(...n.map(r=>r.label.length))+2;for(const r of n)console.log(` ${Y(r.status)} ${b(r.label.padEnd(i))} ${r.message}`),r.detail&&console.log(` ${" ".repeat(i)} ${w(r.detail)}`),r.fix&&(r.status==="warn"||r.status==="fail")&&console.log(` ${" ".repeat(i)} ${C("fix:")} ${w(r.fix)}`);console.log();const t=e.fail>0?j("issues found"):e.warn>0?x("warnings"):y("all good");console.log(` ${t} \u2014 ${y(String(e.pass))} pass \xB7 ${x(String(e.warn))} warn \xB7 ${j(String(e.fail))} fail (${o}ms)`),console.log(),(e.warn>0||e.fail>0)&&(console.log(` Run ${C("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function q(n){const o=n.slice(1),e=o.includes("--json"),i=o.includes("--fix"),t=process.cwd(),r=Date.now(),u=[c("Node.js version",()=>_()),c("infernoflow CLI",()=>P()),c("Git repository",()=>E(t)),c("inferno/ directory",()=>I(t)),c("Contract / mode",()=>T(t)),c("Scenarios",()=>G(t)),c("Changelog",()=>F(t)),c("CONTEXT.md",()=>M(t)),c("Git hooks",()=>D(t)),c("MCP server",()=>L(t)),c("AI providers",()=>R(t)),c("Cloud sync",()=>H()),c(".gitignore",()=>K(t)),c("Router integrity",()=>X()),await J().then(d=>({label:"Ollama (local AI)",...d}))],m=Date.now()-r;if(i){const d=W(u,t);if(d.length)return e||(console.log(),d.forEach(p=>console.log(` ${y("\u2714")} Fixed: ${p}`)),console.log()),q(["doctor","--json"])}if(e){const d={pass:0,warn:0,fail:0,info:0};u.forEach(p=>d[p.status]=(d[p.status]||0)+1),console.log(JSON.stringify({ok:d.fail===0,counts:d,results:u,elapsed:m}));return}U(u,m),u.some(d=>d.status==="fail")&&process.exit(1)}export{q as doctorCommand};
@@ -1,3 +1,99 @@
1
- import*as k from"node:fs";import*as m from"node:path";import{bold as h,cyan as L,gray as f,green as w,yellow as v,red as u}from"../ui/output.mjs";function x(t){try{return JSON.parse(k.readFileSync(t,"utf8"))}catch{return null}}function J(t){return t?.stability||"experimental"}const j={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},O={frozen:u,stable:v,experimental:w};function z(t,i){const c={},a={},g={},s={};for(const e of t){const l=i.find(n=>n.id===e.id)||{};c[e.id]={id:e.id,name:e.name||l.name||l.title||e.id,stability:l.stability||"experimental",functions:e.codeAnalysis?.functions||[],calls:e.codeAnalysis?.calls||[],services:e.codeAnalysis?.services||[],dbCalls:e.codeAnalysis?.dbCalls||[],httpCalls:e.codeAnalysis?.httpCalls||[]},a[e.id]=new Set,g[e.id]=new Set;for(const n of e.codeAnalysis?.functions||[]){const o=n.replace(/\(\)$/,"");s[o]=e.id,s[o.toLowerCase()]=e.id}}for(const[e,l]of Object.entries(c))for(const n of l.calls){const o=n.replace(/\(\)$/,""),d=s[o]||s[o.toLowerCase()];d&&d!==e&&a[e]&&g[d]&&(a[e].add(d),g[d].add(e))}const y={},r={};for(const e of Object.keys(c))y[e]=[...a[e]],r[e]=[...g[e]];return{nodes:c,edges:y,reverse:r}}function I(t){const{nodes:i,edges:c,reverse:a}=t,g=Object.keys(i).sort();console.log(),console.log(h(" Capability Dependency Graph")),console.log(f(" \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")),console.log();let s=!1;for(const r of g){const e=i[r],l=c[r]||[],n=a[r]||[],o=j[e.stability]||"\u{1F30A}",d=O[e.stability]||w;if(!(l.length===0&&n.length===0)){if(s=!0,console.log(` ${o} ${h(d(r))}`),l.length>0){console.log(f(" calls \u2192"));for(const p of l){const $=i[p],b=j[$?.stability]||"\u{1F30A}";console.log(f(` ${b} ${p}`))}}if(n.length>0){console.log(f(" called by \u2190"));for(const p of n){const $=j[i[p]?.stability]||"\u{1F30A}";console.log(f(` ${$} ${p}`))}}console.log()}}s||(console.log(f(" No inter-capability dependencies detected.")),console.log(f(" Run `infernoflow scan` first to populate call data.")),console.log());const y=Object.values(t.edges).reduce((r,e)=>r+e.length,0);console.log(f(" \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")),console.log(f(` ${g.length} capabilities \xB7 ${y} dependency edge(s)`)),console.log()}function E(t,i){const{nodes:c,edges:a,reverse:g}=i,s=c[t];s||(console.error(u(`\u2717 Capability "${t}" not found in graph.`)),process.exit(1));const y=j[s.stability]||"\u{1F30A}",r=O[s.stability]||w;console.log(),console.log(h(` ${y} ${r(t)}`)+f(` (${s.stability})`)),s.services?.length&&console.log(f(" external: ")+L(s.services.join(", "))),console.log();const e=a[t]||[],l=g[t]||[];if(e.length>0){console.log(h(" Calls (downstream dependencies):"));for(const n of e){const o=c[n],d=O[o?.stability]||w,p=j[o?.stability]||"\u{1F30A}";console.log(` ${p} ${d(n)}`+f(o?.services?.length?` [${o.services.join(", ")}]`:""))}console.log()}else console.log(f(" No downstream dependencies.")),console.log();if(l.length>0){console.log(h(" Called by (upstream dependents):"));for(const n of l){const o=c[n],d=O[o?.stability]||w,p=j[o?.stability]||"\u{1F30A}";console.log(` ${p} ${d(n)}`)}console.log()}else console.log(f(" No capabilities call this one.")),console.log();if((s.stability==="frozen"||s.stability==="stable")&&l.length>0){const n=s.stability==="frozen"?u:v;console.log(n(` \u26A0 This capability is ${s.stability}. Changing it may break:`));for(const o of l)console.log(n(` \u2022 ${o}`));console.log()}}function F(t,i){const c=[];if(!t||!i)return c;for(const[a,g]of Object.entries(i.nodes)){if(g.stability==="experimental")continue;const s=new Set(t.reverse?.[a]||[]),r=[...new Set(i.reverse[a]||[])].filter(e=>!s.has(e));if(r.length>0&&c.push({type:"new-dependents",capId:a,stability:g.stability,detail:`${r.join(", ")} now depend on this`}),g.stability==="frozen"){const e=new Set(t.edges?.[a]||[]),l=new Set(i.edges[a]||[]),n=[...l].filter(d=>!e.has(d)),o=[...e].filter(d=>!l.has(d));(n.length>0||o.length>0)&&c.push({type:"frozen-internals-changed",capId:a,stability:g.stability,detail:[n.length?`added calls: ${n.join(", ")}`:"",o.length?`removed calls: ${o.join(", ")}`:""].filter(Boolean).join("; ")})}}return c}async function P(t){const i=(t||[]).slice(1),c=i.includes("--json"),a=i.includes("--check"),g=i.indexOf("--cap"),s=g!==-1?i[g+1]:null,y=process.cwd(),r=m.join(y,"inferno"),e=m.join(r,"scan.json"),l=m.join(r,"graph.json"),n=m.join(r,"capabilities.json"),o=x(e);o||(console.error(u("\u2717 inferno/scan.json not found.")),console.error(f(" The graph is built from a deep AST scan of your codebase.")),console.error(f(" Run this first to generate it:")),console.error(L(`
2
- infernoflow scan
3
- `)),process.exit(1)),(!Array.isArray(o.capabilities)||o.capabilities.length===0)&&(console.error(u("\u2717 inferno/scan.json has no capabilities.")),console.error(f(" Re-run `infernoflow scan` to refresh the data.")),process.exit(1));let d=[];const p=x(n);p&&(d=Array.isArray(p)?p:p.capabilities||[]);const $=o.capabilities||[],b=z($,d),N=x(l),A=F(N,b),D={builtAt:new Date().toISOString(),capabilities:Object.keys(b.nodes).length,edges:Object.values(b.edges).reduce((C,S)=>C+S.length,0),nodes:b.nodes,deps:b.edges,dependents:b.reverse};if(c||k.writeFileSync(l,JSON.stringify(D,null,2)),c){console.log(JSON.stringify(D,null,2));return}if(s?E(s,b):I(b),A.length>0){console.log(v(" \u26A0 Dependency changes detected:"));for(const C of A){const S=C.stability==="frozen"?u("\u{1F9CA}"):v("\u3030\uFE0F ");console.log(` ${S} ${h(C.capId)} \u2014 ${C.detail}`)}console.log(),a&&process.exit(1)}c||console.log(f(" Graph saved \u2192 inferno/graph.json"))}function T(t){return x(m.join(t,"graph.json"))}export{P as graphCommand,T as loadGraph};
1
+ import*as A from"node:fs";import*as k from"node:path";import{bold as v,cyan as B,gray as d,green as j,yellow as D,red as C}from"../ui/output.mjs";function O(o){try{return JSON.parse(A.readFileSync(o,"utf8"))}catch{return null}}function Z(o){return o?.stability||"experimental"}const S={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},L={frozen:C,stable:D,experimental:j};function J(o,n){const a={},l={},s={},e={};for(const t of o){const c=n.find(i=>i.id===t.id)||{};a[t.id]={id:t.id,name:t.name||c.name||c.title||t.id,stability:c.stability||"experimental",functions:t.codeAnalysis?.functions||[],calls:t.codeAnalysis?.calls||[],services:t.codeAnalysis?.services||[],dbCalls:t.codeAnalysis?.dbCalls||[],httpCalls:t.codeAnalysis?.httpCalls||[]},l[t.id]=new Set,s[t.id]=new Set;for(const i of t.codeAnalysis?.functions||[]){const r=i.replace(/\(\)$/,"");e[r]=t.id,e[r.toLowerCase()]=t.id}}for(const[t,c]of Object.entries(a))for(const i of c.calls){const r=i.replace(/\(\)$/,""),p=e[r]||e[r.toLowerCase()];p&&p!==t&&l[t]&&s[p]&&(l[t].add(p),s[p].add(t))}const h={},f={};for(const t of Object.keys(a))h[t]=[...l[t]],f[t]=[...s[t]];return{nodes:a,edges:h,reverse:f}}function P(o){const{nodes:n,edges:a,reverse:l}=o,s=Object.keys(n).sort();console.log(),console.log(v(" Capability Dependency Graph")),console.log(d(" \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")),console.log();let e=!1;for(const f of s){const t=n[f],c=a[f]||[],i=l[f]||[],r=S[t.stability]||"\u{1F30A}",p=L[t.stability]||j;if(!(c.length===0&&i.length===0)){if(e=!0,console.log(` ${r} ${v(p(f))}`),c.length>0){console.log(d(" calls \u2192"));for(const b of c){const y=n[b],E=S[y?.stability]||"\u{1F30A}";console.log(d(` ${E} ${b}`))}}if(i.length>0){console.log(d(" called by \u2190"));for(const b of i){const y=S[n[b]?.stability]||"\u{1F30A}";console.log(d(` ${y} ${b}`))}}console.log()}}e||(console.log(d(" No inter-capability dependencies detected.")),console.log(d(" Run `infernoflow scan` first to populate call data.")),console.log());const h=Object.values(o.edges).reduce((f,t)=>f+t.length,0);console.log(d(" \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")),console.log(d(` ${s.length} capabilities \xB7 ${h} dependency edge(s)`)),console.log()}function U(o,n){const{nodes:a,edges:l,reverse:s}=n,e=a[o];e||(console.error(C(`\u2717 Capability "${o}" not found in graph.`)),process.exit(1));const h=S[e.stability]||"\u{1F30A}",f=L[e.stability]||j;console.log(),console.log(v(` ${h} ${f(o)}`)+d(` (${e.stability})`)),e.services?.length&&console.log(d(" external: ")+B(e.services.join(", "))),console.log();const t=l[o]||[],c=s[o]||[];if(t.length>0){console.log(v(" Calls (downstream dependencies):"));for(const i of t){const r=a[i],p=L[r?.stability]||j,b=S[r?.stability]||"\u{1F30A}";console.log(` ${b} ${p(i)}`+d(r?.services?.length?` [${r.services.join(", ")}]`:""))}console.log()}else console.log(d(" No downstream dependencies.")),console.log();if(c.length>0){console.log(v(" Called by (upstream dependents):"));for(const i of c){const r=a[i],p=L[r?.stability]||j,b=S[r?.stability]||"\u{1F30A}";console.log(` ${b} ${p(i)}`)}console.log()}else console.log(d(" No capabilities call this one.")),console.log();if((e.stability==="frozen"||e.stability==="stable")&&c.length>0){const i=e.stability==="frozen"?C:D;console.log(i(` \u26A0 This capability is ${e.stability}. Changing it may break:`));for(const r of c)console.log(i(` \u2022 ${r}`));console.log()}}function _(o,n){const a=[];if(!o||!n)return a;for(const[l,s]of Object.entries(n.nodes)){if(s.stability==="experimental")continue;const e=new Set(o.reverse?.[l]||[]),f=[...new Set(n.reverse[l]||[])].filter(t=>!e.has(t));if(f.length>0&&a.push({type:"new-dependents",capId:l,stability:s.stability,detail:`${f.join(", ")} now depend on this`}),s.stability==="frozen"){const t=new Set(o.edges?.[l]||[]),c=new Set(n.edges[l]||[]),i=[...c].filter(p=>!t.has(p)),r=[...t].filter(p=>!c.has(p));(i.length>0||r.length>0)&&a.push({type:"frozen-internals-changed",capId:l,stability:s.stability,detail:[i.length?`added calls: ${i.join(", ")}`:"",r.length?`removed calls: ${r.join(", ")}`:""].filter(Boolean).join("; ")})}}return a}async function q(o){const n=(o||[]).slice(1),a=n.includes("--json"),l=n.includes("--check"),s=n.includes("--mermaid"),e=n.includes("--html"),h=n.indexOf("--cap"),f=h!==-1?n[h+1]:null,t=process.cwd(),c=k.join(t,"inferno"),i=k.join(c,"scan.json"),r=k.join(c,"graph.json"),p=k.join(c,"capabilities.json"),b=300*1e3;let y=O(i);if(!y||!Array.isArray(y.capabilities)||y.capabilities.length===0||A.existsSync(i)&&Date.now()-A.statSync(i).mtimeMs>b){console.log(d(" \u27F3 Running infernoflow scan first (scan.json missing or stale)\u2026"));try{const{scanCommand:g}=await import("./scan.mjs");await g(["scan"]),y=O(i)}catch(g){console.error(C(`\u2717 Could not run scan automatically: ${g.message}`)),console.error(d(" Run `infernoflow scan` manually and try again.")),process.exit(1)}}(!y||!Array.isArray(y.capabilities)||y.capabilities.length===0)&&(console.error(C("\u2717 inferno/scan.json still empty after scan.")),console.error(d(" Make sure your contract has at least one capability and your code matches.")),process.exit(1));let M=[];const z=O(p);z&&(M=Array.isArray(z)?z:z.capabilities||[]);const F=y.capabilities||[],u=J(F,M),N=Array.isArray(y.uiElements)?y.uiElements:[];if(N.length>0){const g={};for(const m of F){const w=m.codeAnalysis?.functions||[];for(const $ of w){const R=$.replace(/\(\)$/,"");g[R]=m.id,g[R.toLowerCase()]=m.id}}let x=0;for(const m of N){const w=g[m.handler]||g[m.handler?.toLowerCase()];if(!w)continue;const $=`ui:${m.tag}:${m.handler}`;u.nodes[$]={id:$,name:m.label||m.handler,stability:"ui",kind:"ui",tag:m.tag,handler:m.handler,file:m.file,functions:[],calls:[]},u.edges[$]=new Set([w]),u.reverse[$]=new Set,u.reverse[w]||(u.reverse[w]=new Set),u.reverse[w].add($),x++}x>0&&!a&&!s&&!e&&console.log(d(` \u26A1 Wired ${x} UI element${x===1?"":"s"} to capabilities.`))}const W=O(r),T=_(W,u),H={builtAt:new Date().toISOString(),capabilities:Object.keys(u.nodes).length,edges:Object.values(u.edges).reduce((g,x)=>g+x.length,0),nodes:u.nodes,deps:u.edges,dependents:u.reverse};if(a||A.writeFileSync(r,JSON.stringify(H,null,2)),a){console.log(JSON.stringify(H,null,2));return}if(s){console.log(G(u));return}if(e){const g=k.join(c,"graph.html");A.writeFileSync(g,V(u)),console.log(j("\u2714 Interactive graph saved \u2192 inferno/graph.html")),console.log(d(` Open it: file://${g.replace(/\\/g,"/")}`));return}if(f?U(f,u):P(u),T.length>0){console.log(D(" \u26A0 Dependency changes detected:"));for(const g of T){const x=g.stability==="frozen"?C("\u{1F9CA}"):D("\u3030\uFE0F ");console.log(` ${x} ${v(g.capId)} \u2014 ${g.detail}`)}console.log(),l&&process.exit(1)}a||console.log(d(" Graph saved \u2192 inferno/graph.json"))}function G(o){const n=[];n.push("```mermaid"),n.push("graph LR"),n.push(" classDef frozen fill:#fee,stroke:#c44,color:#900;"),n.push(" classDef stable fill:#fffbe6,stroke:#cc9,color:#840;"),n.push(" classDef experimental fill:#eef,stroke:#88c,color:#226;"),n.push(" classDef ui fill:#e8f5e9,stroke:#4caf50,color:#2e7d32,stroke-dasharray:4 2;");for(const a of Object.keys(o.nodes)){const l=I(a),s=o.nodes[a];if(s.kind==="ui"){const h=`${Y(s.tag)} ${s.name||s.handler}<br/><small>&lt;${s.tag}&gt;</small>`;n.push(` ${l}(["${h}"]):::ui`)}else{const e=s.functions?.length||0,h=`${s.name||a}<br/><small>${e} fn${e===1?"":"s"}</small>`;n.push(` ${l}["${h}"]:::${s.stability||"experimental"}`)}}for(const[a,l]of Object.entries(o.edges)){const s=l instanceof Set?[...l]:Array.isArray(l)?l:[];for(const e of s)n.push(` ${I(a)} --> ${I(e)}`)}return n.push("```"),n.join(`
2
+ `)}function I(o){return String(o).replace(/[^a-zA-Z0-9_]/g,"_")}function Y(o){switch(o){case"button":return"\u{1F518}";case"input":return"\u2328\uFE0F ";case"form":return"\u{1F4DD}";case"link":return"\u{1F517}";case"select":return"\u25BE";default:return"\u{1F9E9}"}}function V(o){const n=Object.keys(o.nodes).map(s=>{const e=o.nodes[s];return{id:s,name:e.name||s,stability:e.stability||"experimental",kind:e.kind||"capability",tag:e.tag||null,functions:e.functions?.length||0}}),a=[];for(const[s,e]of Object.entries(o.edges)){const h=e instanceof Set?[...e]:Array.isArray(e)?e:[];for(const f of h)a.push({source:s,target:f})}const l=JSON.stringify({nodes:n,links:a});return`<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <title>infernoflow \u2014 capability graph</title>
7
+ <style>
8
+ body { margin: 0; padding: 0; background: #1e1e1e; color: #ccc; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; overflow: hidden; }
9
+ header { padding: 12px 24px; background: #2a2a2a; border-bottom: 1px solid #3a3a3a; }
10
+ header h1 { margin: 0; font-size: 16px; font-weight: 600; }
11
+ header .meta { font-size: 12px; color: #999; margin-top: 4px; }
12
+ header .meta span { margin-right: 16px; }
13
+ header .legend { margin-top: 8px; font-size: 11px; }
14
+ header .legend .swatch { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
15
+ svg { width: 100vw; height: calc(100vh - 88px); cursor: grab; }
16
+ svg:active { cursor: grabbing; }
17
+ .link { stroke: #555; stroke-opacity: 0.7; }
18
+ .node circle { stroke: #1e1e1e; stroke-width: 2px; cursor: pointer; }
19
+ .node text { fill: #ddd; font-size: 11px; font-weight: 500; pointer-events: none; }
20
+ .node.frozen circle { fill: #d43f3a; }
21
+ .node.stable circle { fill: #f0ad4e; }
22
+ .node.experimental circle { fill: #5bc0de; }
23
+ .node.ui circle { fill: #4caf50; stroke-dasharray: 3 2; }
24
+ .node.ui text { fill: #aef; font-weight: 400; font-style: italic; }
25
+ .node:hover circle { stroke: #fff; stroke-width: 3px; }
26
+ .tooltip { position: fixed; background: #2a2a2a; border: 1px solid #555; padding: 8px 12px; border-radius: 4px; font-size: 12px; pointer-events: none; opacity: 0; transition: opacity 0.15s; max-width: 300px; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <header>
31
+ <h1>\u{1F525} infernoflow \u2014 capability graph</h1>
32
+ <div class="meta">
33
+ <span>Generated: ${new Date().toLocaleString()}</span>
34
+ <span>${n.length} capabilities \xB7 ${a.length} edges</span>
35
+ </div>
36
+ <div class="legend">
37
+ <span><span class="swatch" style="background:#d43f3a"></span>frozen (high-risk to change)</span>
38
+ <span><span class="swatch" style="background:#f0ad4e"></span>stable</span>
39
+ <span><span class="swatch" style="background:#5bc0de"></span>experimental</span>
40
+ <span><span class="swatch" style="background:#4caf50"></span>UI element (button/input/form)</span>
41
+ <span style="color:#666; margin-left:16px;">drag nodes \xB7 scroll to zoom \xB7 hover for details</span>
42
+ </div>
43
+ </header>
44
+ <svg></svg>
45
+ <div class="tooltip" id="tt"></div>
46
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
47
+ <script>
48
+ const data = ${l};
49
+ const svg = d3.select("svg");
50
+ const W = window.innerWidth, H = window.innerHeight - 88;
51
+ const g = svg.append("g");
52
+
53
+ const zoom = d3.zoom().scaleExtent([0.3, 4]).on("zoom", e => g.attr("transform", e.transform));
54
+ svg.call(zoom);
55
+
56
+ const sim = d3.forceSimulation(data.nodes)
57
+ .force("link", d3.forceLink(data.links).id(d => d.id).distance(120))
58
+ .force("charge", d3.forceManyBody().strength(-400))
59
+ .force("center", d3.forceCenter(W/2, H/2))
60
+ .force("collide", d3.forceCollide(40));
61
+
62
+ const link = g.append("g").selectAll("line")
63
+ .data(data.links).enter().append("line")
64
+ .attr("class", "link").attr("marker-end", "url(#arrow)");
65
+
66
+ svg.append("defs").append("marker").attr("id","arrow").attr("viewBox","0 -5 10 10").attr("refX",18).attr("refY",0).attr("markerWidth",6).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M0,-5L10,0L0,5").attr("fill","#888");
67
+
68
+ const node = g.append("g").selectAll(".node")
69
+ .data(data.nodes).enter().append("g")
70
+ .attr("class", d => "node " + d.stability)
71
+ .call(d3.drag()
72
+ .on("start", (e,d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx=d.x; d.fy=d.y; })
73
+ .on("drag", (e,d) => { d.fx=e.x; d.fy=e.y; })
74
+ .on("end", (e,d) => { if (!e.active) sim.alphaTarget(0); d.fx=null; d.fy=null; }));
75
+
76
+ node.append("circle").attr("r", d => d.kind === "ui" ? 8 : 12 + Math.min(d.functions, 8));
77
+ node.append("text").attr("dx", 18).attr("dy", 4).text(d => {
78
+ if (d.kind === "ui") {
79
+ const emoji = { button: "\u{1F518}", input: "\u2328\uFE0F", form: "\u{1F4DD}", link: "\u{1F517}", select: "\u25BE" }[d.tag] || "\u{1F9E9}";
80
+ return emoji + " " + d.name;
81
+ }
82
+ return d.name;
83
+ });
84
+
85
+ const tt = d3.select("#tt");
86
+ node.on("mouseover", (e,d) => {
87
+ const html = d.kind === "ui"
88
+ ? \`<strong>\${d.name}</strong><br/>UI element: &lt;\${d.tag}&gt;<br/>Handler wires to a capability\`
89
+ : \`<strong>\${d.name}</strong><br/>Stability: \${d.stability}<br/>Functions: \${d.functions}\`;
90
+ tt.html(html).style("left", (e.pageX+12)+"px").style("top", (e.pageY+12)+"px").style("opacity", 1);
91
+ }).on("mouseout", () => tt.style("opacity", 0));
92
+
93
+ sim.on("tick", () => {
94
+ link.attr("x1", d=>d.source.x).attr("y1", d=>d.source.y).attr("x2", d=>d.target.x).attr("y2", d=>d.target.y);
95
+ node.attr("transform", d => \`translate(\${d.x},\${d.y})\`);
96
+ });
97
+ </script>
98
+ </body>
99
+ </html>`}function K(o){return O(k.join(o,"graph.json"))}export{q as graphCommand,K as loadGraph};
@@ -1,4 +1,4 @@
1
- import*as w from"node:fs";import*as d from"node:path";import{createRequire as U}from"node:module";import{execSync as V}from"node:child_process";import{bold as R,cyan as W,gray as u,green as I,yellow as j,red as A}from"../ui/output.mjs";const D=U(import.meta.url),G=["/usr/local/lib/node_modules_global/lib/node_modules/typescript","/usr/lib/node_modules/typescript",d.join(process.env.HOME||"",".npm-global/lib/node_modules/typescript")];function K(){for(const e of G)try{return D(d.join(e,"lib/typescript.js"))}catch{}try{return D("typescript")}catch{}return null}const l=K(),Q=[{service:"stripe",patterns:["stripe","Stripe","createPaymentIntent","charges.create"]},{service:"sendgrid",patterns:["sendgrid","@sendgrid","sgMail","sendgrid.send"]},{service:"ses",patterns:["SES","ses.sendEmail","aws-sdk/ses","nodemailer"]},{service:"s3",patterns:["S3","s3.upload","s3.getObject","PutObjectCommand","@aws-sdk/s3"]},{service:"redis",patterns:["redis","Redis","ioredis","createClient"]},{service:"jwt",patterns:["jwt","jsonwebtoken","sign(","verify(","decode("]},{service:"bcrypt",patterns:["bcrypt","argon2","scrypt","hashSync","compare("]},{service:"prisma",patterns:["prisma.","PrismaClient","@prisma/client"]},{service:"mongoose",patterns:["mongoose",".save()",".findOne(",".aggregate("]},{service:"postgres",patterns:["pg","Pool(","Client(","query(","postgres("]},{service:"mysql",patterns:["mysql","mysql2","createConnection"]},{service:"graphql",patterns:["graphql","gql`","ApolloServer","GraphQLSchema"]},{service:"firebase",patterns:["firebase","firestore","initializeApp"]},{service:"twilio",patterns:["twilio","Twilio(","messages.create"]},{service:"openai",patterns:["openai","OpenAI(","createCompletion","chat.completions"]}];function F(e){const t=new Set;for(const{service:n,patterns:s}of Q)s.some(i=>e.includes(i))&&t.add(n);return[...t]}const X=[/\.(find|findOne|findMany|findById|findAll)\s*\(/g,/\.(create|insert|insertOne|insertMany|save)\s*\(/g,/\.(update|updateOne|updateMany|updateById|upsert)\s*\(/g,/\.(delete|deleteOne|deleteMany|remove|destroy)\s*\(/g,/\.(query|execute|raw)\s*\(/g,/\.(aggregate|groupBy|count|sum)\s*\(/g,/db\.\w+\s*\(/g,/prisma\.\w+\.\w+\s*\(/g];function O(e){const t=new Set;for(const n of X){const s=new RegExp(n.source,"g");let i;for(;(i=s.exec(e))!==null;)t.add(i[0].replace(/\s*\($/,"()"))}return[...t].slice(0,10)}const Y=[/fetch\s*\(/g,/axios\.(get|post|put|patch|delete)\s*\(/g,/http\.(get|post|request)\s*\(/g,/got\.(get|post|put|delete)\s*\(/g,/request\.(get|post|put|delete)\s*\(/g,/\$http\.(get|post|put|delete)\s*\(/g];function T(e){const t=new Set;for(const n of Y){const s=new RegExp(n.source,"g");let i;for(;(i=s.exec(e))!==null;)t.add(i[0].replace(/\s*\($/,"()"))}return[...t].slice(0,8)}function _(e){return l&&e.name&&l.isIdentifier(e.name)?e.name.text:null}function Z(e){const t=[],n=[];function s(i){if(l.isCallExpression(i)){const r=i.expression;l.isIdentifier(r)?t.push({pos:i.pos,end:i.end,name:r.text+"()"}):l.isPropertyAccessExpression(r)&&t.push({pos:i.pos,end:i.end,name:r.name.text+"()"})}l.isThrowStatement(i)&&i.expression&&l.isNewExpression(i.expression)&&l.isIdentifier(i.expression.expression)&&n.push({pos:i.pos,end:i.end,name:i.expression.expression.text}),i.forEachChild?.(s)}return s(e),{calls:t,throws:n}}function ee(e,t,n){return[...new Set(e.filter(s=>s.pos>=t&&s.end<=n).map(s=>s.name))].slice(0,20)}function se(e,t,n){return[...new Set(e.filter(s=>s.pos>=t&&s.end<=n).map(s=>s.name))]}function te(e){return l?l.isFunctionDeclaration(e)||l.isFunctionExpression(e)||l.isArrowFunction(e)||l.isMethodDeclaration(e):!1}function ne(e){return l&&(e.parent&&l.isVariableDeclaration(e.parent)||e.parent&&l.isPropertyAssignment(e.parent))?_(e.parent):null}function ie(e,t){if(!l)return null;let n;try{n=l.createSourceFile(e,t,l.ScriptTarget.Latest,!0)}catch{return null}const{calls:s,throws:i}=Z(n),r=[];function a(c){if(te(c)){const b=_(c)||ne(c)||"<anonymous>",f=t.slice(c.pos,c.end),g=ee(s,c.pos,c.end),S=se(i,c.pos,c.end);r.push({name:b,calls:g,throws:S,services:F(f),dbCalls:O(f),httpCalls:T(f),loc:n.getLineAndCharacterOfPosition(c.pos).line+1})}c.forEachChild?.(a)}return a(n),r}const oe=`
1
+ import*as b from"node:fs";import*as f from"node:path";import{createRequire as H}from"node:module";import{execSync as V}from"node:child_process";import{bold as R,cyan as W,gray as u,green as P,yellow as v,red as $}from"../ui/output.mjs";const k=H(import.meta.url),G=["/usr/local/lib/node_modules_global/lib/node_modules/typescript","/usr/lib/node_modules/typescript",f.join(process.env.HOME||"",".npm-global/lib/node_modules/typescript")];function K(){for(const e of G)try{return k(f.join(e,"lib/typescript.js"))}catch{}try{return k("typescript")}catch{}return null}const a=K(),Q=[{service:"stripe",patterns:["stripe","Stripe","createPaymentIntent","charges.create"]},{service:"sendgrid",patterns:["sendgrid","@sendgrid","sgMail","sendgrid.send"]},{service:"ses",patterns:["SES","ses.sendEmail","aws-sdk/ses","nodemailer"]},{service:"s3",patterns:["S3","s3.upload","s3.getObject","PutObjectCommand","@aws-sdk/s3"]},{service:"redis",patterns:["redis","Redis","ioredis","createClient"]},{service:"jwt",patterns:["jwt","jsonwebtoken","sign(","verify(","decode("]},{service:"bcrypt",patterns:["bcrypt","argon2","scrypt","hashSync","compare("]},{service:"prisma",patterns:["prisma.","PrismaClient","@prisma/client"]},{service:"mongoose",patterns:["mongoose",".save()",".findOne(",".aggregate("]},{service:"postgres",patterns:["pg","Pool(","Client(","query(","postgres("]},{service:"mysql",patterns:["mysql","mysql2","createConnection"]},{service:"graphql",patterns:["graphql","gql`","ApolloServer","GraphQLSchema"]},{service:"firebase",patterns:["firebase","firestore","initializeApp"]},{service:"twilio",patterns:["twilio","Twilio(","messages.create"]},{service:"openai",patterns:["openai","OpenAI(","createCompletion","chat.completions"]}];function _(e){const t=new Set;for(const{service:n,patterns:s}of Q)s.some(i=>e.includes(i))&&t.add(n);return[...t]}const X=[/\.(find|findOne|findMany|findById|findAll)\s*\(/g,/\.(create|insert|insertOne|insertMany|save)\s*\(/g,/\.(update|updateOne|updateMany|updateById|upsert)\s*\(/g,/\.(delete|deleteOne|deleteMany|remove|destroy)\s*\(/g,/\.(query|execute|raw)\s*\(/g,/\.(aggregate|groupBy|count|sum)\s*\(/g,/db\.\w+\s*\(/g,/prisma\.\w+\.\w+\s*\(/g];function F(e){const t=new Set;for(const n of X){const s=new RegExp(n.source,"g");let i;for(;(i=s.exec(e))!==null;)t.add(i[0].replace(/\s*\($/,"()"))}return[...t].slice(0,10)}const Y=[/fetch\s*\(/g,/axios\.(get|post|put|patch|delete)\s*\(/g,/http\.(get|post|request)\s*\(/g,/got\.(get|post|put|delete)\s*\(/g,/request\.(get|post|put|delete)\s*\(/g,/\$http\.(get|post|put|delete)\s*\(/g];function T(e){const t=new Set;for(const n of Y){const s=new RegExp(n.source,"g");let i;for(;(i=s.exec(e))!==null;)t.add(i[0].replace(/\s*\($/,"()"))}return[...t].slice(0,8)}const ee=[{tag:"button",re:/<button\s+[^>]*on(?:Click|Press|Submit)\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[^>]*>\s*([^<{]*)/gi},{tag:"input",re:/<input\s+[^>]*on(?:Change|Input|Blur)\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[^>]*(?:placeholder=["']([^"']*)["'])?/gi},{tag:"form",re:/<form\s+[^>]*onSubmit\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[^>]*>/gi},{tag:"link",re:/<a\s+[^>]*onClick\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[^>]*>\s*([^<{]*)/gi},{tag:"select",re:/<select\s+[^>]*onChange\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[^>]*>/gi}];function se(e,t){if(!/\.(jsx|tsx|vue|svelte)$/i.test(t))return[];const n=[];for(const{tag:s,re:i}of ee){const r=new RegExp(i.source,i.flags);let l;for(;(l=r.exec(e))!==null;){const c=l[1],y=(l[2]||"").trim().slice(0,40);if(n.push({tag:s,handler:c,label:y||c,file:t}),n.length>=50)return n}}return n}function O(e){return a&&e.name&&a.isIdentifier(e.name)?e.name.text:null}function te(e){const t=[],n=[];function s(i){if(a.isCallExpression(i)){const r=i.expression;a.isIdentifier(r)?t.push({pos:i.pos,end:i.end,name:r.text+"()"}):a.isPropertyAccessExpression(r)&&t.push({pos:i.pos,end:i.end,name:r.name.text+"()"})}a.isThrowStatement(i)&&i.expression&&a.isNewExpression(i.expression)&&a.isIdentifier(i.expression.expression)&&n.push({pos:i.pos,end:i.end,name:i.expression.expression.text}),i.forEachChild?.(s)}return s(e),{calls:t,throws:n}}function ne(e,t,n){return[...new Set(e.filter(s=>s.pos>=t&&s.end<=n).map(s=>s.name))].slice(0,20)}function ie(e,t,n){return[...new Set(e.filter(s=>s.pos>=t&&s.end<=n).map(s=>s.name))]}function oe(e){return a?a.isFunctionDeclaration(e)||a.isFunctionExpression(e)||a.isArrowFunction(e)||a.isMethodDeclaration(e):!1}function re(e){return a&&(e.parent&&a.isVariableDeclaration(e.parent)||e.parent&&a.isPropertyAssignment(e.parent))?O(e.parent):null}function ce(e,t){if(!a)return null;let n;try{n=a.createSourceFile(e,t,a.ScriptTarget.Latest,!0)}catch{return null}const{calls:s,throws:i}=te(n),r=[];function l(c){if(oe(c)){const y=O(c)||re(c)||"<anonymous>",d=t.slice(c.pos,c.end),g=ne(s,c.pos,c.end),S=ie(i,c.pos,c.end);r.push({name:y,calls:g,throws:S,services:_(d),dbCalls:F(d),httpCalls:T(d),loc:n.getLineAndCharacterOfPosition(c.pos).line+1})}c.forEachChild?.(l)}return l(n),r}const le=`
2
2
  import ast, json, sys
3
3
 
4
4
  def get_calls(node):
@@ -36,7 +36,7 @@ try:
36
36
  print(json.dumps(functions))
37
37
  except Exception as e:
38
38
  print(json.dumps([]))
39
- `;function re(e){try{const t=V(`python3 -c ${JSON.stringify(oe)} ${JSON.stringify(e)}`,{timeout:8e3,encoding:"utf8",stdio:["pipe","pipe","pipe"]}),n=JSON.parse(t.trim()||"[]"),s=w.readFileSync(e,"utf8");return n.map(i=>({...i,services:F(s),dbCalls:O(s),httpCalls:T(s)}))}catch{return null}}const ce=[{re:/^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/gm,lang:"go"},{re:/^\s*(?:def|async def)\s+(\w+)\s*\(/gm,lang:"py"},{re:/^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)?(\w+)\s*\(/gm,lang:"java"},{re:/^\s*def\s+(\w+)\s*[\(\|]/gm,lang:"rb"}];function N(e,t){const n=d.extname(e).slice(1),s=ce.find(c=>c.lang===n);if(!s)return null;const i=[],r=new RegExp(s.re.source,"gm");let a;for(;(a=r.exec(t))!==null;){const c=a.index,b=Math.min(c+2e3,t.length),f=t.slice(c,b);i.push({name:a[1],calls:[],throws:[],services:F(f),dbCalls:O(f),httpCalls:T(f),loc:t.slice(0,c).split(`
40
- `).length})}return i.length>0?i:null}const le=new Set(["node_modules",".git","dist","build","out",".next",".nuxt","coverage","__pycache__",".pytest_cache","vendor","tmp",".turbo","target",".gradle","public","static","assets"]),ae=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".py",".go",".rb",".java"]),pe=/\.(test|spec)\.[jt]sx?$|_test\.(go|py|rb)|spec\.(rb|js|ts)$/;function*P(e){let t;try{t=w.readdirSync(e,{withFileTypes:!0})}catch{return}for(const n of t)if(n.isDirectory())le.has(n.name)||(yield*P(d.join(e,n.name)));else if(n.isFile()){const s=d.extname(n.name);ae.has(s)&&!pe.test(n.name)&&(yield d.join(e,n.name))}}function ue(e){let t;try{t=w.readFileSync(e,"utf8")}catch{return[]}const n=d.extname(e);return[".ts",".tsx",".js",".jsx",".mjs",".cjs"].includes(n)?ie(e,t)||N(e,t)||[]:n===".py"?re(e)||N(e,t)||[]:N(e,t)||[]}function E(e){return e.replace(/([a-z])([A-Z])/g,"$1 $2").toLowerCase().split(/[\s_\-/.]+/).filter(t=>t.length>1)}function k(e,t){const n=new Set(e),s=new Set(t);let i=0;for(const a of n)s.has(a)&&i++;const r=n.size+s.size-i;return r===0?0:i/r}function fe(e,t){const n=E(e.name);let s=null,i=0;for(const r of t){const a=Math.max(k(n,E(r.id||"")),k(n,E(r.name||r.title||"")));a>i&&(i=a,s=r)}return i>=.2?{cap:s,score:i}:null}function de(e={},t,n,s){const i=d.relative(s,n),r=(a=[],c=[])=>[...new Set([...a,...c])];return{functions:r(e.functions,[t.name]),sourceFiles:r(e.sourceFiles,[i]),calls:r(e.calls,t.calls),throws:r(e.throws,t.throws),services:r(e.services,t.services),dbCalls:r(e.dbCalls,t.dbCalls),httpCalls:r(e.httpCalls,t.httpCalls),scannedAt:new Date().toISOString()}}function ge(e){console.log(),console.log(R(" Scan Results")),console.log(u(" \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\u2500\u2500\u2500\u2500"));for(const[t,n]of Object.entries(e)){const{codeAnalysis:s}=n;s&&(console.log(),console.log(` ${I("\u25CF")} ${R(t)}`),s.sourceFiles?.length&&console.log(u(" files: ")+s.sourceFiles.join(", ")),s.functions?.length&&console.log(u(" funcs: ")+s.functions.join(", ")),s.services?.length&&console.log(u(" services: ")+W(s.services.join(", "))),s.dbCalls?.length&&console.log(u(" db: ")+s.dbCalls.slice(0,4).join(", ")),s.httpCalls?.length&&console.log(u(" http: ")+s.httpCalls.slice(0,4).join(", ")),s.throws?.length&&console.log(u(" throws: ")+j(s.throws.join(", "))))}console.log(),console.log(u(" \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\u2500\u2500\u2500\u2500"))}async function we(e){const t=e||[],n=t.includes("--dry-run"),s=t.includes("--json"),i=t.indexOf("--dir"),r=i!==-1?[t[i+1]]:[],a=(()=>{const o=t.indexOf("--capability");return o!==-1?t[o+1]:null})(),c=process.cwd(),b=d.join(c,"inferno"),f=d.join(b,"capabilities.json");w.existsSync(f)||(console.error(A("\u2717 inferno/capabilities.json not found \u2014 run `infernoflow init` first.")),process.exit(1));let g;try{g=JSON.parse(w.readFileSync(f,"utf8"))}catch(o){console.error(A("\u2717 Failed to parse capabilities.json: "+o.message)),process.exit(1)}Array.isArray(g)||(g.capabilities?g=g.capabilities:(console.error(A("\u2717 Unexpected capabilities.json format.")),process.exit(1)));const S=a?g.filter(o=>o.id===a||(o.name||"").toLowerCase()===a.toLowerCase()):g;S.length===0&&(console.log(j(a?`No capability matched: ${a}`:"No capabilities found.")),process.exit(0));const z=[c,...r];s||process.stdout.write(u(" Walking source files\u2026"));const h=[];for(const o of z)for(const p of P(o))h.push(p);s||process.stdout.write(`\r Found ${h.length} source files.
41
- `),s||process.stdout.write(u(" Analyzing\u2026"));const x=[];let v=0;for(const o of h){const p=ue(o);for(const y of p)x.push({fn:y,filePath:o});v++,!s&&v%20===0&&process.stdout.write(`\r Analyzed ${v}/${h.length} files\u2026`)}s||process.stdout.write(`\r Analyzed ${h.length} files, found ${x.length} functions.
42
- `);const m={};for(const o of S)m[o.id]={...o,codeAnalysis:null};for(const{fn:o,filePath:p}of x){const y=fe(o,S);if(!y)continue;const{cap:$}=y,B=m[$.id]?.codeAnalysis||{};m[$.id].codeAnalysis=de(B,o,p,c)}const q=Object.keys(m).length,M=Object.values(m).filter(o=>o.codeAnalysis).length;if(s){const o={scannedAt:new Date().toISOString(),files:h.length,functions:x.length,capabilities:Object.entries(m).map(([p,y])=>({id:p,name:y.name||y.title,codeAnalysis:y.codeAnalysis}))};console.log(JSON.stringify(o,null,2));return}if(ge(m),console.log(` ${I("\u2714")} Matched ${M}/${q} capabilities to source functions`),console.log(),n){console.log(j(" --dry-run: no files written."));return}const J={scannedAt:new Date().toISOString(),files:h.length,functions:x.length,capabilities:Object.entries(m).map(([o,p])=>({id:o,name:p.name||p.title,codeAnalysis:p.codeAnalysis}))},L=d.join(b,"scan.json");w.writeFileSync(L,JSON.stringify(J,null,2)),console.log(u(" Saved \u2192 inferno/scan.json"));let C=0;const H=g.map(o=>{const p=m[o.id]?.codeAnalysis;return p?(C++,{...o,codeAnalysis:p}):o});C>0&&(w.writeFileSync(f,JSON.stringify(H,null,2)),console.log(u(` Updated ${C} capability entries in capabilities.json`))),console.log(),l||(console.log(j(" \u26A0 TypeScript compiler not found \u2014 JS/TS analyzed with regex fallback.")),console.log(u(" For deeper analysis: npm install -g typescript")),console.log())}export{we as scanCommand};
39
+ `;function ae(e){try{const t=V(`python3 -c ${JSON.stringify(le)} ${JSON.stringify(e)}`,{timeout:8e3,encoding:"utf8",stdio:["pipe","pipe","pipe"]}),n=JSON.parse(t.trim()||"[]"),s=b.readFileSync(e,"utf8");return n.map(i=>({...i,services:_(s),dbCalls:F(s),httpCalls:T(s)}))}catch{return null}}const pe=[{re:/^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/gm,lang:"go"},{re:/^\s*(?:def|async def)\s+(\w+)\s*\(/gm,lang:"py"},{re:/^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)?(\w+)\s*\(/gm,lang:"java"},{re:/^\s*def\s+(\w+)\s*[\(\|]/gm,lang:"rb"}];function E(e,t){const n=f.extname(e).slice(1),s=pe.find(c=>c.lang===n);if(!s)return null;const i=[],r=new RegExp(s.re.source,"gm");let l;for(;(l=r.exec(t))!==null;){const c=l.index,y=Math.min(c+2e3,t.length),d=t.slice(c,y);i.push({name:l[1],calls:[],throws:[],services:_(d),dbCalls:F(d),httpCalls:T(d),loc:t.slice(0,c).split(`
40
+ `).length})}return i.length>0?i:null}const ue=new Set(["node_modules",".git","dist","build","out",".next",".nuxt","coverage","__pycache__",".pytest_cache","vendor","tmp",".turbo","target",".gradle","public","static","assets"]),fe=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".py",".go",".rb",".java"]),de=/\.(test|spec)\.[jt]sx?$|_test\.(go|py|rb)|spec\.(rb|js|ts)$/;function*z(e){let t;try{t=b.readdirSync(e,{withFileTypes:!0})}catch{return}for(const n of t)if(n.isDirectory())ue.has(n.name)||(yield*z(f.join(e,n.name)));else if(n.isFile()){const s=f.extname(n.name);fe.has(s)&&!de.test(n.name)&&(yield f.join(e,n.name))}}function ge(e){let t;try{t=b.readFileSync(e,"utf8")}catch{return[]}const n=f.extname(e);return[".ts",".tsx",".js",".jsx",".mjs",".cjs"].includes(n)?ce(e,t)||E(e,t)||[]:n===".py"?ae(e)||E(e,t)||[]:E(e,t)||[]}function N(e){return e.replace(/([a-z])([A-Z])/g,"$1 $2").toLowerCase().split(/[\s_\-/.]+/).filter(t=>t.length>1)}function D(e,t){const n=new Set(e),s=new Set(t);let i=0;for(const l of n)s.has(l)&&i++;const r=n.size+s.size-i;return r===0?0:i/r}function me(e,t){const n=N(e.name);let s=null,i=0;for(const r of t){const l=Math.max(D(n,N(r.id||"")),D(n,N(r.name||r.title||"")));l>i&&(i=l,s=r)}return i>=.2?{cap:s,score:i}:null}function he(e={},t,n,s){const i=f.relative(s,n),r=(l=[],c=[])=>[...new Set([...l,...c])];return{functions:r(e.functions,[t.name]),sourceFiles:r(e.sourceFiles,[i]),calls:r(e.calls,t.calls),throws:r(e.throws,t.throws),services:r(e.services,t.services),dbCalls:r(e.dbCalls,t.dbCalls),httpCalls:r(e.httpCalls,t.httpCalls),scannedAt:new Date().toISOString()}}function ye(e){console.log(),console.log(R(" Scan Results")),console.log(u(" \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\u2500\u2500\u2500\u2500"));for(const[t,n]of Object.entries(e)){const{codeAnalysis:s}=n;s&&(console.log(),console.log(` ${P("\u25CF")} ${R(t)}`),s.sourceFiles?.length&&console.log(u(" files: ")+s.sourceFiles.join(", ")),s.functions?.length&&console.log(u(" funcs: ")+s.functions.join(", ")),s.services?.length&&console.log(u(" services: ")+W(s.services.join(", "))),s.dbCalls?.length&&console.log(u(" db: ")+s.dbCalls.slice(0,4).join(", ")),s.httpCalls?.length&&console.log(u(" http: ")+s.httpCalls.slice(0,4).join(", ")),s.throws?.length&&console.log(u(" throws: ")+v(s.throws.join(", "))))}console.log(),console.log(u(" \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\u2500\u2500\u2500\u2500"))}async function xe(e){const t=e||[],n=t.includes("--dry-run"),s=t.includes("--json"),i=t.indexOf("--dir"),r=i!==-1?[t[i+1]]:[],l=(()=>{const o=t.indexOf("--capability");return o!==-1?t[o+1]:null})(),c=process.cwd(),y=f.join(c,"inferno"),d=f.join(y,"capabilities.json");b.existsSync(d)||(console.error($("\u2717 inferno/capabilities.json not found \u2014 run `infernoflow init` first.")),process.exit(1));let g;try{g=JSON.parse(b.readFileSync(d,"utf8"))}catch(o){console.error($("\u2717 Failed to parse capabilities.json: "+o.message)),process.exit(1)}Array.isArray(g)||(g.capabilities?g=g.capabilities:(console.error($("\u2717 Unexpected capabilities.json format.")),process.exit(1)));const S=l?g.filter(o=>o.id===l||(o.name||"").toLowerCase()===l.toLowerCase()):g;S.length===0&&(console.log(v(l?`No capability matched: ${l}`:"No capabilities found.")),process.exit(0));const q=[c,...r];s||process.stdout.write(u(" Walking source files\u2026"));const w=[];for(const o of q)for(const p of z(o))w.push(p);s||process.stdout.write(`\r Found ${w.length} source files.
41
+ `),s||process.stdout.write(u(" Analyzing\u2026"));const x=[];let C=0;for(const o of w){const p=ge(o);for(const m of p)x.push({fn:m,filePath:o});C++,!s&&C%20===0&&process.stdout.write(`\r Analyzed ${C}/${w.length} files\u2026`)}s||process.stdout.write(`\r Analyzed ${w.length} files, found ${x.length} functions.
42
+ `);const j=[];for(const o of w)if(/\.(jsx|tsx|vue|svelte)$/i.test(o))try{const p=b.readFileSync(o,"utf8"),m=se(p,f.relative(c,o).replace(/\\/g,"/"));j.push(...m)}catch{}!s&&j.length>0&&console.log(` Found ${j.length} UI elements (buttons, inputs, forms, links).`);const h={};for(const o of S)h[o.id]={...o,codeAnalysis:null};for(const{fn:o,filePath:p}of x){const m=me(o,S);if(!m)continue;const{cap:I}=m,B=h[I.id]?.codeAnalysis||{};h[I.id].codeAnalysis=he(B,o,p,c)}const M=Object.keys(h).length,J=Object.values(h).filter(o=>o.codeAnalysis).length;if(s){const o={scannedAt:new Date().toISOString(),files:w.length,functions:x.length,capabilities:Object.entries(h).map(([p,m])=>({id:p,name:m.name||m.title,codeAnalysis:m.codeAnalysis}))};console.log(JSON.stringify(o,null,2));return}if(ye(h),console.log(` ${P("\u2714")} Matched ${J}/${M} capabilities to source functions`),console.log(),n){console.log(v(" --dry-run: no files written."));return}const L={scannedAt:new Date().toISOString(),files:w.length,functions:x.length,capabilities:Object.entries(h).map(([o,p])=>({id:o,name:p.name||p.title,codeAnalysis:p.codeAnalysis})),uiElements:j},U=f.join(y,"scan.json");b.writeFileSync(U,JSON.stringify(L,null,2)),console.log(u(" Saved \u2192 inferno/scan.json"));let A=0;const Z=g.map(o=>{const p=h[o.id]?.codeAnalysis;return p?(A++,{...o,codeAnalysis:p}):o});A>0&&(b.writeFileSync(d,JSON.stringify(Z,null,2)),console.log(u(` Updated ${A} capability entries in capabilities.json`))),console.log(),a||(console.log(v(" \u26A0 TypeScript compiler not found \u2014 JS/TS analyzed with regex fallback.")),console.log(u(" For deeper analysis: npm install -g typescript")),console.log())}export{xe as scanCommand};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.42.7",
3
+ "version": "0.43.0",
4
4
  "description": "Persistent memory for AI coding sessions \u2014 captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
5
5
  "type": "module",
6
6
  "bin": {