infernoflow 0.42.8 → 0.43.1
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 +33 -0
- package/dist/lib/commands/graph.mjs +111 -3
- package/dist/lib/commands/scan.mjs +5 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog — infernoflow
|
|
2
2
|
|
|
3
|
+
## 0.43.1 — 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **UI element regex broke on multi-line JSX attributes**. `<button\n onClick={handler}\n>` was being missed because `[^>]*` doesn't span newlines. Switched to `[\s\S]*?` so multi-line attributes work. UI elements should now actually appear in the graph.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **Component layer in scan + graph**. `infernoflow scan` now also detects React/Vue/Svelte function components by Capitalized-name pattern (`function ComponentName`, `export default function`, `const Component = (...) =>`, etc.). Each component becomes a hexagon-shaped node in Mermaid output and an orange circle in HTML output, sitting **between** UI elements and capabilities. Three-tier visual: UI → Component → Capability.
|
|
10
|
+
- **Component-aware UI wiring**. UI elements now prefer wiring through their containing component's hexagon (so you see "Add Task button → TaskComposer component → CreateTask capability") instead of jumping directly to capabilities.
|
|
11
|
+
- **Better legend** in HTML output covering all 4 node kinds (capability / component / UI / frozen).
|
|
12
|
+
|
|
13
|
+
## 0.43.0 — 2026-05-06
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **`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.
|
|
17
|
+
- **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.
|
|
18
|
+
- In Mermaid output: UI nodes appear as round-cornered nodes with a tag emoji (🔘 button, ⌨️ input, 📝 form, 🔗 link, ▾ select).
|
|
19
|
+
- In HTML output: UI nodes are smaller green dashed circles, distinct from the capability circles. Tooltip shows the element type.
|
|
20
|
+
- **Combined workflow** — for the "show me how the app works" view, run:
|
|
21
|
+
```
|
|
22
|
+
infernoflow contract graph --html
|
|
23
|
+
```
|
|
24
|
+
Auto-scans, builds the dep tree, attaches UI elements, generates the interactive D3 page. One command.
|
|
25
|
+
|
|
26
|
+
## 0.42.9 — 2026-05-06
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **Visual graph output** for `infernoflow contract graph` — two new flags:
|
|
30
|
+
- `--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`.
|
|
31
|
+
- `--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.
|
|
32
|
+
|
|
33
|
+
### Notes
|
|
34
|
+
- The default ASCII output is unchanged. `--mermaid` and `--html` are opt-in alternatives for when you want a real diagram.
|
|
35
|
+
|
|
3
36
|
## 0.42.8 — 2026-05-06
|
|
4
37
|
|
|
5
38
|
### Fixed
|
|
@@ -1,3 +1,111 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import*as O from"node:fs";import*as v from"node:path";import{bold as k,cyan as G,gray as g,green as w,yellow as M,red as j}from"../ui/output.mjs";function D(a){try{return JSON.parse(O.readFileSync(a,"utf8"))}catch{return null}}function ne(a){return a?.stability||"experimental"}const C={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},N={frozen:j,stable:M,experimental:w};function V(a,o){const l={},r={},s={},e={};for(const t of a){const p=o.find(c=>c.id===t.id)||{};l[t.id]={id:t.id,name:t.name||p.name||p.title||t.id,stability:p.stability||"experimental",functions:t.codeAnalysis?.functions||[],calls:t.codeAnalysis?.calls||[],services:t.codeAnalysis?.services||[],dbCalls:t.codeAnalysis?.dbCalls||[],httpCalls:t.codeAnalysis?.httpCalls||[]},r[t.id]=new Set,s[t.id]=new Set;for(const c of t.codeAnalysis?.functions||[]){const f=c.replace(/\(\)$/,"");e[f]=t.id,e[f.toLowerCase()]=t.id}}for(const[t,p]of Object.entries(l))for(const c of p.calls){const f=c.replace(/\(\)$/,""),h=e[f]||e[f.toLowerCase()];h&&h!==t&&r[t]&&s[h]&&(r[t].add(h),s[h].add(t))}const u={},m={};for(const t of Object.keys(l))u[t]=[...r[t]],m[t]=[...s[t]];return{nodes:l,edges:u,reverse:m}}function Y(a){const{nodes:o,edges:l,reverse:r}=a,s=Object.keys(o).sort();console.log(),console.log(k(" Capability Dependency Graph")),console.log(g(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 m of s){const t=o[m],p=l[m]||[],c=r[m]||[],f=C[t.stability]||"\u{1F30A}",h=N[t.stability]||w;if(!(p.length===0&&c.length===0)){if(e=!0,console.log(` ${f} ${k(h(m))}`),p.length>0){console.log(g(" calls \u2192"));for(const x of p){const y=o[x],R=C[y?.stability]||"\u{1F30A}";console.log(g(` ${R} ${x}`))}}if(c.length>0){console.log(g(" called by \u2190"));for(const x of c){const y=C[o[x]?.stability]||"\u{1F30A}";console.log(g(` ${y} ${x}`))}}console.log()}}e||(console.log(g(" No inter-capability dependencies detected.")),console.log(g(" Run `infernoflow scan` first to populate call data.")),console.log());const u=Object.values(a.edges).reduce((m,t)=>m+t.length,0);console.log(g(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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(g(` ${s.length} capabilities \xB7 ${u} dependency edge(s)`)),console.log()}function X(a,o){const{nodes:l,edges:r,reverse:s}=o,e=l[a];e||(console.error(j(`\u2717 Capability "${a}" not found in graph.`)),process.exit(1));const u=C[e.stability]||"\u{1F30A}",m=N[e.stability]||w;console.log(),console.log(k(` ${u} ${m(a)}`)+g(` (${e.stability})`)),e.services?.length&&console.log(g(" external: ")+G(e.services.join(", "))),console.log();const t=r[a]||[],p=s[a]||[];if(t.length>0){console.log(k(" Calls (downstream dependencies):"));for(const c of t){const f=l[c],h=N[f?.stability]||w,x=C[f?.stability]||"\u{1F30A}";console.log(` ${x} ${h(c)}`+g(f?.services?.length?` [${f.services.join(", ")}]`:""))}console.log()}else console.log(g(" No downstream dependencies.")),console.log();if(p.length>0){console.log(k(" Called by (upstream dependents):"));for(const c of p){const f=l[c],h=N[f?.stability]||w,x=C[f?.stability]||"\u{1F30A}";console.log(` ${x} ${h(c)}`)}console.log()}else console.log(g(" No capabilities call this one.")),console.log();if((e.stability==="frozen"||e.stability==="stable")&&p.length>0){const c=e.stability==="frozen"?j:M;console.log(c(` \u26A0 This capability is ${e.stability}. Changing it may break:`));for(const f of p)console.log(c(` \u2022 ${f}`));console.log()}}function Z(a,o){const l=[];if(!a||!o)return l;for(const[r,s]of Object.entries(o.nodes)){if(s.stability==="experimental")continue;const e=new Set(a.reverse?.[r]||[]),m=[...new Set(o.reverse[r]||[])].filter(t=>!e.has(t));if(m.length>0&&l.push({type:"new-dependents",capId:r,stability:s.stability,detail:`${m.join(", ")} now depend on this`}),s.stability==="frozen"){const t=new Set(a.edges?.[r]||[]),p=new Set(o.edges[r]||[]),c=[...p].filter(h=>!t.has(h)),f=[...t].filter(h=>!p.has(h));(c.length>0||f.length>0)&&l.push({type:"frozen-internals-changed",capId:r,stability:s.stability,detail:[c.length?`added calls: ${c.join(", ")}`:"",f.length?`removed calls: ${f.join(", ")}`:""].filter(Boolean).join("; ")})}}return l}async function te(a){const o=(a||[]).slice(1),l=o.includes("--json"),r=o.includes("--check"),s=o.includes("--mermaid"),e=o.includes("--html"),u=o.indexOf("--cap"),m=u!==-1?o[u+1]:null,t=process.cwd(),p=v.join(t,"inferno"),c=v.join(p,"scan.json"),f=v.join(p,"graph.json"),h=v.join(p,"capabilities.json"),x=300*1e3;let y=D(c);if(!y||!Array.isArray(y.capabilities)||y.capabilities.length===0||O.existsSync(c)&&Date.now()-O.statSync(c).mtimeMs>x){console.log(g(" \u27F3 Running infernoflow scan first (scan.json missing or stale)\u2026"));try{const{scanCommand:n}=await import("./scan.mjs");await n(["scan"]),y=D(c)}catch(n){console.error(j(`\u2717 Could not run scan automatically: ${n.message}`)),console.error(g(" Run `infernoflow scan` manually and try again.")),process.exit(1)}}(!y||!Array.isArray(y.capabilities)||y.capabilities.length===0)&&(console.error(j("\u2717 inferno/scan.json still empty after scan.")),console.error(g(" Make sure your contract has at least one capability and your code matches.")),process.exit(1));let H=[];const I=D(h);I&&(H=Array.isArray(I)?I:I.capabilities||[]);const F=y.capabilities||[],i=V(F,H),L=Array.isArray(y.components)?y.components:[],S={};for(const n of F){const d=(n.codeAnalysis?.files||[]).map($=>$.replace(/\\/g,"/"));for(const $ of d)S[$]||(S[$]=new Set),S[$].add(n.id)}let _=0;for(const n of L){const d=`comp:${n.name}`;i.nodes[d]={id:d,name:n.name,stability:"component",kind:"component",file:n.file,functions:[],calls:[]},i.edges[d]=i.edges[d]||new Set,i.reverse[d]=i.reverse[d]||new Set;const $=S[n.file]?[...S[n.file]]:[];for(const b of $)i.edges[d].add(b),i.reverse[b]||(i.reverse[b]=new Set),i.reverse[b].add(d),_++}const P=Array.isArray(y.uiElements)?y.uiElements:[],T={};for(const n of L)T[n.file]||(T[n.file]=n.name);const E={};for(const n of F){const d=n.codeAnalysis?.functions||[];for(const $ of d){const b=$.replace(/\(\)$/,"");E[b]=n.id,E[b.toLowerCase()]=n.id}}let A=0;for(const n of P){const d=`ui:${n.tag}:${n.handler}:${n.file.replace(/[^a-z0-9]/gi,"_")}`;i.nodes[d]={id:d,name:n.label||n.handler,stability:"ui",kind:"ui",tag:n.tag,handler:n.handler,file:n.file,functions:[],calls:[]},i.edges[d]=i.edges[d]||new Set,i.reverse[d]=i.reverse[d]||new Set;const $=T[n.file];if($){const z=`comp:${$}`;if(i.nodes[z]){i.edges[d].add(z),i.reverse[z]||(i.reverse[z]=new Set),i.reverse[z].add(d),A++;continue}}const b=E[n.handler]||E[n.handler?.toLowerCase()];b&&(i.edges[d].add(b),i.reverse[b]||(i.reverse[b]=new Set),i.reverse[b].add(d),A++)}!l&&!s&&!e&&(_>0&&console.log(g(` \u{1F9E9} Wired ${L.length} component${L.length===1?"":"s"} to capabilities.`)),A>0&&console.log(g(` \u26A1 Wired ${A} UI element${A===1?"":"s"}.`)));const U=D(f),B=Z(U,i),J={builtAt:new Date().toISOString(),capabilities:Object.keys(i.nodes).length,edges:Object.values(i.edges).reduce((n,d)=>n+d.length,0),nodes:i.nodes,deps:i.edges,dependents:i.reverse};if(l||O.writeFileSync(f,JSON.stringify(J,null,2)),l){console.log(JSON.stringify(J,null,2));return}if(s){console.log(q(i));return}if(e){const n=v.join(p,"graph.html");O.writeFileSync(n,Q(i)),console.log(w("\u2714 Interactive graph saved \u2192 inferno/graph.html")),console.log(g(` Open it: file://${n.replace(/\\/g,"/")}`));return}if(m?X(m,i):Y(i),B.length>0){console.log(M(" \u26A0 Dependency changes detected:"));for(const n of B){const d=n.stability==="frozen"?j("\u{1F9CA}"):M("\u3030\uFE0F ");console.log(` ${d} ${k(n.capId)} \u2014 ${n.detail}`)}console.log(),r&&process.exit(1)}l||console.log(g(" Graph saved \u2192 inferno/graph.json"))}function q(a){const o=[];o.push("```mermaid"),o.push("graph LR"),o.push(" classDef frozen fill:#fee,stroke:#c44,color:#900;"),o.push(" classDef stable fill:#fffbe6,stroke:#cc9,color:#840;"),o.push(" classDef experimental fill:#eef,stroke:#88c,color:#226;"),o.push(" classDef component fill:#fff3e0,stroke:#ff9800,color:#bf6d00;"),o.push(" classDef ui fill:#e8f5e9,stroke:#4caf50,color:#2e7d32,stroke-dasharray:4 2;");for(const l of Object.keys(a.nodes)){const r=W(l),s=a.nodes[l];if(s.kind==="ui"){const u=`${K(s.tag)} ${s.name||s.handler}<br/><small><${s.tag}></small>`;o.push(` ${r}(["${u}"]):::ui`)}else if(s.kind==="component")o.push(` ${r}{{"\u{1F9E9} ${s.name}"}}:::component`);else{const e=s.functions?.length||0,u=`${s.name||l}<br/><small>${e} fn${e===1?"":"s"}</small>`;o.push(` ${r}["${u}"]:::${s.stability||"experimental"}`)}}for(const[l,r]of Object.entries(a.edges)){const s=r instanceof Set?[...r]:Array.isArray(r)?r:[];for(const e of s)o.push(` ${W(l)} --> ${W(e)}`)}return o.push("```"),o.join(`
|
|
2
|
+
`)}function W(a){return String(a).replace(/[^a-zA-Z0-9_]/g,"_")}function K(a){switch(a){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 Q(a){const o=Object.keys(a.nodes).map(s=>{const e=a.nodes[s];return{id:s,name:e.name||s,stability:e.stability||"experimental",kind:e.kind||"capability",tag:e.tag||null,handler:e.handler||null,file:e.file||null,functions:e.functions?.length||0}}),l=[];for(const[s,e]of Object.entries(a.edges)){const u=e instanceof Set?[...e]:Array.isArray(e)?e:[];for(const m of u)l.push({source:s,target:m})}const r=JSON.stringify({nodes:o,links:l});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.component circle { fill: #ff9800; }
|
|
24
|
+
.node.component text { fill: #ffd180; }
|
|
25
|
+
.node.ui circle { fill: #4caf50; stroke-dasharray: 3 2; }
|
|
26
|
+
.node.ui text { fill: #aef; font-weight: 400; font-style: italic; }
|
|
27
|
+
.node:hover circle { stroke: #fff; stroke-width: 3px; }
|
|
28
|
+
.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; }
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<header>
|
|
33
|
+
<h1>\u{1F525} infernoflow \u2014 capability graph</h1>
|
|
34
|
+
<div class="meta">
|
|
35
|
+
<span>Generated: ${new Date().toLocaleString()}</span>
|
|
36
|
+
<span>${o.length} capabilities \xB7 ${l.length} edges</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="legend">
|
|
39
|
+
<span><span class="swatch" style="background:#5bc0de"></span>capability</span>
|
|
40
|
+
<span><span class="swatch" style="background:#ff9800"></span>component (React/Vue)</span>
|
|
41
|
+
<span><span class="swatch" style="background:#4caf50"></span>UI element (button/input/form)</span>
|
|
42
|
+
<span><span class="swatch" style="background:#d43f3a"></span>frozen (high-risk to change)</span>
|
|
43
|
+
<span style="color:#666; margin-left:16px;">drag \xB7 scroll to zoom \xB7 hover for details</span>
|
|
44
|
+
</div>
|
|
45
|
+
</header>
|
|
46
|
+
<svg></svg>
|
|
47
|
+
<div class="tooltip" id="tt"></div>
|
|
48
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
|
|
49
|
+
<script>
|
|
50
|
+
const data = ${r};
|
|
51
|
+
const svg = d3.select("svg");
|
|
52
|
+
const W = window.innerWidth, H = window.innerHeight - 88;
|
|
53
|
+
const g = svg.append("g");
|
|
54
|
+
|
|
55
|
+
const zoom = d3.zoom().scaleExtent([0.3, 4]).on("zoom", e => g.attr("transform", e.transform));
|
|
56
|
+
svg.call(zoom);
|
|
57
|
+
|
|
58
|
+
const sim = d3.forceSimulation(data.nodes)
|
|
59
|
+
.force("link", d3.forceLink(data.links).id(d => d.id).distance(120))
|
|
60
|
+
.force("charge", d3.forceManyBody().strength(-400))
|
|
61
|
+
.force("center", d3.forceCenter(W/2, H/2))
|
|
62
|
+
.force("collide", d3.forceCollide(40));
|
|
63
|
+
|
|
64
|
+
const link = g.append("g").selectAll("line")
|
|
65
|
+
.data(data.links).enter().append("line")
|
|
66
|
+
.attr("class", "link").attr("marker-end", "url(#arrow)");
|
|
67
|
+
|
|
68
|
+
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");
|
|
69
|
+
|
|
70
|
+
const node = g.append("g").selectAll(".node")
|
|
71
|
+
.data(data.nodes).enter().append("g")
|
|
72
|
+
.attr("class", d => "node " + d.stability)
|
|
73
|
+
.call(d3.drag()
|
|
74
|
+
.on("start", (e,d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx=d.x; d.fy=d.y; })
|
|
75
|
+
.on("drag", (e,d) => { d.fx=e.x; d.fy=e.y; })
|
|
76
|
+
.on("end", (e,d) => { if (!e.active) sim.alphaTarget(0); d.fx=null; d.fy=null; }));
|
|
77
|
+
|
|
78
|
+
node.append("circle").attr("r", d => {
|
|
79
|
+
if (d.kind === "ui") return 7;
|
|
80
|
+
if (d.kind === "component") return 11;
|
|
81
|
+
return 12 + Math.min(d.functions, 8);
|
|
82
|
+
});
|
|
83
|
+
node.append("text").attr("dx", 18).attr("dy", 4).text(d => {
|
|
84
|
+
if (d.kind === "ui") {
|
|
85
|
+
const emoji = { button: "\u{1F518}", input: "\u2328\uFE0F", form: "\u{1F4DD}", link: "\u{1F517}", select: "\u25BE" }[d.tag] || "\u{1F9E9}";
|
|
86
|
+
return emoji + " " + d.name;
|
|
87
|
+
}
|
|
88
|
+
if (d.kind === "component") return "\u{1F9E9} " + d.name;
|
|
89
|
+
return d.name;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const tt = d3.select("#tt");
|
|
93
|
+
node.on("mouseover", (e,d) => {
|
|
94
|
+
let html;
|
|
95
|
+
if (d.kind === "ui") {
|
|
96
|
+
html = \`<strong>\${d.name}</strong><br/>UI element: <\${d.tag}><br/>Handler: \${d.handler || "\u2014"}\`;
|
|
97
|
+
} else if (d.kind === "component") {
|
|
98
|
+
html = \`<strong>\u{1F9E9} \${d.name}</strong><br/>Component<br/>\${d.file || ""}\`;
|
|
99
|
+
} else {
|
|
100
|
+
html = \`<strong>\${d.name}</strong><br/>Capability \xB7 \${d.stability}<br/>Functions: \${d.functions}\`;
|
|
101
|
+
}
|
|
102
|
+
tt.html(html).style("left", (e.pageX+12)+"px").style("top", (e.pageY+12)+"px").style("opacity", 1);
|
|
103
|
+
}).on("mouseout", () => tt.style("opacity", 0));
|
|
104
|
+
|
|
105
|
+
sim.on("tick", () => {
|
|
106
|
+
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);
|
|
107
|
+
node.attr("transform", d => \`translate(\${d.x},\${d.y})\`);
|
|
108
|
+
});
|
|
109
|
+
</script>
|
|
110
|
+
</body>
|
|
111
|
+
</html>`}function oe(a){return D(v.join(a,"graph.json"))}export{te as graphCommand,oe as loadGraph};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as S from"node:fs";import*as f from"node:path";import{createRequire as V}from"node:module";import{execSync as W}from"node:child_process";import{bold as P,cyan as G,gray as u,green as k,yellow as A,red as T}from"../ui/output.mjs";const z=V(import.meta.url),K=["/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 Q(){for(const e of K)try{return z(f.join(e,"lib/typescript.js"))}catch{}try{return z("typescript")}catch{}return null}const a=Q(),X=[{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 X)s.some(o=>e.includes(o))&&t.add(n);return[...t]}const Y=[/\.(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 Y){const s=new RegExp(n.source,"g");let o;for(;(o=s.exec(e))!==null;)t.add(o[0].replace(/\s*\($/,"()"))}return[...t].slice(0,10)}const ee=[/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 E(e){const t=new Set;for(const n of ee){const s=new RegExp(n.source,"g");let o;for(;(o=s.exec(e))!==null;)t.add(o[0].replace(/\s*\($/,"()"))}return[...t].slice(0,8)}const se=[{tag:"button",re:/<button[\s\S]*?on(?:Click|Press|Submit)\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[\s\S]*?>([\s\S]*?)<\/button>/gi},{tag:"input",re:/<input[\s\S]*?on(?:Change|Input|Blur)\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[\s\S]*?\/?>/gi},{tag:"form",re:/<form[\s\S]*?onSubmit\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[\s\S]*?>/gi},{tag:"link",re:/<a[\s\S]*?onClick\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[\s\S]*?>([\s\S]*?)<\/a>/gi},{tag:"select",re:/<select[\s\S]*?onChange\s*=\s*\{?([A-Za-z_$][\w$]*)\}?[\s\S]*?>/gi}];function te(e,t){if(!/\.(jsx|tsx|vue|svelte)$/i.test(t))return[];const n=[];for(const{tag:s,re:o}of se){const i=new RegExp(o.source,o.flags);let l;for(;(l=i.exec(e))!==null;){const c=l[1],y=(l[2]||"").replace(/\{[^}]*\}/g,"").replace(/\s+/g," ").trim().slice(0,40);if(n.push({tag:s,handler:c,label:y||c,file:t}),n.length>=50)return n}}return n}const ne=[/export\s+default\s+function\s+([A-Z][\w$]*)/g,/export\s+function\s+([A-Z][\w$]*)/g,/^function\s+([A-Z][\w$]*)\s*\([\s\S]*?\)\s*\{[\s\S]*?return\s*\(/gm,/(?:export\s+)?const\s+([A-Z][\w$]*)\s*=\s*(?:\([\s\S]*?\)|[\w$]+)\s*=>\s*[({<]/g];function oe(e,t){if(!/\.(jsx|tsx|vue|svelte)$/i.test(t))return[];const n=new Set;for(const s of ne){const o=new RegExp(s.source,s.flags);let i;for(;(i=o.exec(e))!==null;)i[1]&&n.add(i[1])}return[...n].map(s=>({name:s,file:t}))}function O(e){return a&&e.name&&a.isIdentifier(e.name)?e.name.text:null}function ie(e){const t=[],n=[];function s(o){if(a.isCallExpression(o)){const i=o.expression;a.isIdentifier(i)?t.push({pos:o.pos,end:o.end,name:i.text+"()"}):a.isPropertyAccessExpression(i)&&t.push({pos:o.pos,end:o.end,name:i.name.text+"()"})}a.isThrowStatement(o)&&o.expression&&a.isNewExpression(o.expression)&&a.isIdentifier(o.expression.expression)&&n.push({pos:o.pos,end:o.end,name:o.expression.expression.text}),o.forEachChild?.(s)}return s(e),{calls:t,throws:n}}function re(e,t,n){return[...new Set(e.filter(s=>s.pos>=t&&s.end<=n).map(s=>s.name))].slice(0,20)}function ce(e,t,n){return[...new Set(e.filter(s=>s.pos>=t&&s.end<=n).map(s=>s.name))]}function le(e){return a?a.isFunctionDeclaration(e)||a.isFunctionExpression(e)||a.isArrowFunction(e)||a.isMethodDeclaration(e):!1}function ae(e){return a&&(e.parent&&a.isVariableDeclaration(e.parent)||e.parent&&a.isPropertyAssignment(e.parent))?O(e.parent):null}function pe(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:o}=ie(n),i=[];function l(c){if(le(c)){const y=O(c)||ae(c)||"<anonymous>",d=t.slice(c.pos,c.end),m=re(s,c.pos,c.end),x=ce(o,c.pos,c.end);i.push({name:y,calls:m,throws:x,services:_(d),dbCalls:F(d),httpCalls:E(d),loc:n.getLineAndCharacterOfPosition(c.pos).line+1})}c.forEachChild?.(l)}return l(n),i}const ue=`
|
|
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
|
|
41
|
-
`),s||process.stdout.write(u(" Analyzing\u2026"));const
|
|
42
|
-
`);const
|
|
39
|
+
`;function fe(e){try{const t=W(`python3 -c ${JSON.stringify(ue)} ${JSON.stringify(e)}`,{timeout:8e3,encoding:"utf8",stdio:["pipe","pipe","pipe"]}),n=JSON.parse(t.trim()||"[]"),s=S.readFileSync(e,"utf8");return n.map(o=>({...o,services:_(s),dbCalls:F(s),httpCalls:E(s)}))}catch{return null}}const de=[{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=f.extname(e).slice(1),s=de.find(c=>c.lang===n);if(!s)return null;const o=[],i=new RegExp(s.re.source,"gm");let l;for(;(l=i.exec(t))!==null;){const c=l.index,y=Math.min(c+2e3,t.length),d=t.slice(c,y);o.push({name:l[1],calls:[],throws:[],services:_(d),dbCalls:F(d),httpCalls:E(d),loc:t.slice(0,c).split(`
|
|
40
|
+
`).length})}return o.length>0?o:null}const ge=new Set(["node_modules",".git","dist","build","out",".next",".nuxt","coverage","__pycache__",".pytest_cache","vendor","tmp",".turbo","target",".gradle","public","static","assets"]),me=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".py",".go",".rb",".java"]),he=/\.(test|spec)\.[jt]sx?$|_test\.(go|py|rb)|spec\.(rb|js|ts)$/;function*D(e){let t;try{t=S.readdirSync(e,{withFileTypes:!0})}catch{return}for(const n of t)if(n.isDirectory())ge.has(n.name)||(yield*D(f.join(e,n.name)));else if(n.isFile()){const s=f.extname(n.name);me.has(s)&&!he.test(n.name)&&(yield f.join(e,n.name))}}function ye(e){let t;try{t=S.readFileSync(e,"utf8")}catch{return[]}const n=f.extname(e);return[".ts",".tsx",".js",".jsx",".mjs",".cjs"].includes(n)?pe(e,t)||N(e,t)||[]:n===".py"?fe(e)||N(e,t)||[]:N(e,t)||[]}function R(e){return e.replace(/([a-z])([A-Z])/g,"$1 $2").toLowerCase().split(/[\s_\-/.]+/).filter(t=>t.length>1)}function q(e,t){const n=new Set(e),s=new Set(t);let o=0;for(const l of n)s.has(l)&&o++;const i=n.size+s.size-o;return i===0?0:o/i}function we(e,t){const n=R(e.name);let s=null,o=0;for(const i of t){const l=Math.max(q(n,R(i.id||"")),q(n,R(i.name||i.title||"")));l>o&&(o=l,s=i)}return o>=.2?{cap:s,score:o}:null}function Se(e={},t,n,s){const o=f.relative(s,n),i=(l=[],c=[])=>[...new Set([...l,...c])];return{functions:i(e.functions,[t.name]),sourceFiles:i(e.sourceFiles,[o]),calls:i(e.calls,t.calls),throws:i(e.throws,t.throws),services:i(e.services,t.services),dbCalls:i(e.dbCalls,t.dbCalls),httpCalls:i(e.httpCalls,t.httpCalls),scannedAt:new Date().toISOString()}}function xe(e){console.log(),console.log(P(" 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(` ${k("\u25CF")} ${P(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: ")+G(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: ")+A(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 Ae(e){const t=e||[],n=t.includes("--dry-run"),s=t.includes("--json"),o=t.indexOf("--dir"),i=o!==-1?[t[o+1]]:[],l=(()=>{const r=t.indexOf("--capability");return r!==-1?t[r+1]:null})(),c=process.cwd(),y=f.join(c,"inferno"),d=f.join(y,"capabilities.json");S.existsSync(d)||(console.error(T("\u2717 inferno/capabilities.json not found \u2014 run `infernoflow init` first.")),process.exit(1));let m;try{m=JSON.parse(S.readFileSync(d,"utf8"))}catch(r){console.error(T("\u2717 Failed to parse capabilities.json: "+r.message)),process.exit(1)}Array.isArray(m)||(m.capabilities?m=m.capabilities:(console.error(T("\u2717 Unexpected capabilities.json format.")),process.exit(1)));const x=l?m.filter(r=>r.id===l||(r.name||"").toLowerCase()===l.toLowerCase()):m;x.length===0&&(console.log(A(l?`No capability matched: ${l}`:"No capabilities found.")),process.exit(0));const M=[c,...i];s||process.stdout.write(u(" Walking source files\u2026"));const w=[];for(const r of M)for(const p of D(r))w.push(p);s||process.stdout.write(`\r Found ${w.length} source files.
|
|
41
|
+
`),s||process.stdout.write(u(" Analyzing\u2026"));const b=[];let C=0;for(const r of w){const p=ye(r);for(const g of p)b.push({fn:g,filePath:r});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 ${b.length} functions.
|
|
42
|
+
`);const j=[],v=[];for(const r of w)if(/\.(jsx|tsx|vue|svelte)$/i.test(r))try{const p=S.readFileSync(r,"utf8"),g=f.relative(c,r).replace(/\\/g,"/");j.push(...te(p,g)),v.push(...oe(p,g))}catch{}s||(v.length>0&&console.log(` Found ${v.length} components (React/Vue/Svelte).`),j.length>0&&console.log(` Found ${j.length} UI elements (buttons, inputs, forms, links).`));const h={};for(const r of x)h[r.id]={...r,codeAnalysis:null};for(const{fn:r,filePath:p}of b){const g=we(r,x);if(!g)continue;const{cap:I}=g,H=h[I.id]?.codeAnalysis||{};h[I.id].codeAnalysis=Se(H,r,p,c)}const Z=Object.keys(h).length,J=Object.values(h).filter(r=>r.codeAnalysis).length;if(s){const r={scannedAt:new Date().toISOString(),files:w.length,functions:b.length,capabilities:Object.entries(h).map(([p,g])=>({id:p,name:g.name||g.title,codeAnalysis:g.codeAnalysis}))};console.log(JSON.stringify(r,null,2));return}if(xe(h),console.log(` ${k("\u2714")} Matched ${J}/${Z} capabilities to source functions`),console.log(),n){console.log(A(" --dry-run: no files written."));return}const L={scannedAt:new Date().toISOString(),files:w.length,functions:b.length,capabilities:Object.entries(h).map(([r,p])=>({id:r,name:p.name||p.title,codeAnalysis:p.codeAnalysis})),uiElements:j,components:v},U=f.join(y,"scan.json");S.writeFileSync(U,JSON.stringify(L,null,2)),console.log(u(" Saved \u2192 inferno/scan.json"));let $=0;const B=m.map(r=>{const p=h[r.id]?.codeAnalysis;return p?($++,{...r,codeAnalysis:p}):r});$>0&&(S.writeFileSync(d,JSON.stringify(B,null,2)),console.log(u(` Updated ${$} capability entries in capabilities.json`))),console.log(),a||(console.log(A(" \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{Ae as scanCommand};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.1",
|
|
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": {
|