infernoflow 0.42.8 → 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 +23 -0
- package/dist/lib/commands/graph.mjs +99 -3
- package/dist/lib/commands/scan.mjs +5 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
3
26
|
## 0.42.8 — 2026-05-06
|
|
4
27
|
|
|
5
28
|
### Fixed
|
|
@@ -1,3 +1,99 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
|
|
3
|
-
|
|
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><${s.tag}></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: <\${d.tag}><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
|
|
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
|
|
40
|
-
`).length})}return i.length>0?i:null}const
|
|
41
|
-
`),s||process.stdout.write(u(" Analyzing\u2026"));const x=[];let
|
|
42
|
-
`);const m={};for(const o of S)
|
|
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.
|
|
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": {
|