infernoflow 0.10.17 → 0.10.19

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,6 +1,6 @@
1
1
  # Changelog — infernoflow
2
2
 
3
- ## Unreleased
3
+ ## 0.10.19 — 2026-04-21
4
4
 
5
5
  ## 0.10.12 — 2026-04-12
6
6
 
@@ -1,16 +1,29 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as d}from"node:fs";import{dirname as m,join as f}from"node:path";import{fileURLToPath as u}from"node:url";import{bold as e,gray as t,red as r}from"../lib/ui/output.mjs";const h=m(u(import.meta.url)),y=JSON.parse(d(f(h,"..","package.json"),"utf8")),i=y.version||"0.0.0",s={init:"Scaffold inferno/ in your project (or adopt existing project)","install-cursor-hooks":"Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md","install-vscode-copilot-hooks":"Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",check:"Validate contract, capabilities, scenarios, changelog",status:"Show contract health at a glance","pr-impact":"Summarize PR impact on capabilities and docs",sync:"Run deterministic inferno sync flow",run:"One-command detect/propose/apply/validate flow","doc-gate":"Fail if code changed but docs were not updated",suggest:"Generate AI prompt + apply capability updates",implement:"Generate code-agent implementation prompt(s)",context:"Generate AI-ready context for new sessions"},c={init:async o=>(await import("../lib/commands/init.mjs")).initCommand(o),"install-cursor-hooks":async o=>(await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(o),"install-vscode-copilot-hooks":async o=>(await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(o),check:async o=>(await import("../lib/commands/check.mjs")).checkCommand(o),status:async o=>(await import("../lib/commands/status.mjs")).statusCommand(o),"pr-impact":async o=>(await import("../lib/commands/prImpact.mjs")).prImpactCommand(o),sync:async o=>(await import("../lib/commands/syncAuto.mjs")).syncCommand(o),run:async o=>(await import("../lib/commands/run.mjs")).runCommand(o),suggest:async o=>(await import("../lib/commands/suggest.mjs")).suggestCommand(o),implement:async o=>(await import("../lib/commands/implement.mjs")).implementCommand(o),context:async o=>(await import("../lib/commands/context.mjs")).contextCommand(o),"doc-gate":async o=>(await import("../lib/commands/docGate.mjs")).docGateCommand(o)};function g(){const o=Object.keys(s),l=Math.max(...o.map(a=>a.length),8)+1;return Object.entries(s).map(([a,p])=>` ${a.padEnd(l," ")}${p}`).join(`
2
+ import{readFileSync as m}from"node:fs";import{dirname as d,join as u}from"node:path";import{fileURLToPath as f}from"node:url";import{bold as t,gray as e,red as a}from"../lib/ui/output.mjs";const h=d(f(import.meta.url)),g=JSON.parse(m(u(h,"..","package.json"),"utf8")),r=g.version||"0.0.0",c={publish:"Bump version, update changelog, build, npm publish, git commit + push in one shot",setup:"One command to get fully operational \u2014 detects IDE, inits, installs hooks + MCP",init:"Scaffold inferno/ in your project (or adopt existing project)","install-cursor-hooks":"Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md","install-vscode-copilot-hooks":"Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",check:"Validate contract, capabilities, scenarios, changelog",status:"Show contract health at a glance","pr-impact":"Summarize PR impact on capabilities and docs",sync:"Run deterministic inferno sync flow",run:"One-command detect/propose/apply/validate flow","doc-gate":"Fail if code changed but docs were not updated",suggest:"Generate AI prompt + apply capability updates",implement:"Generate code-agent implementation prompt(s)",context:"Generate AI-ready context for new sessions","generate-skills":"Generate personalised Cursor rules + skill files from your developer profile"},l={publish:async o=>(await import("../lib/commands/publish.mjs")).publishCommand(o),setup:async o=>(await import("../lib/commands/setup.mjs")).setupCommand(o),init:async o=>(await import("../lib/commands/init.mjs")).initCommand(o),"install-cursor-hooks":async o=>(await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(o),"install-vscode-copilot-hooks":async o=>(await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(o),check:async o=>(await import("../lib/commands/check.mjs")).checkCommand(o),status:async o=>(await import("../lib/commands/status.mjs")).statusCommand(o),"pr-impact":async o=>(await import("../lib/commands/prImpact.mjs")).prImpactCommand(o),sync:async o=>(await import("../lib/commands/syncAuto.mjs")).syncCommand(o),run:async o=>(await import("../lib/commands/run.mjs")).runCommand(o),suggest:async o=>(await import("../lib/commands/suggest.mjs")).suggestCommand(o),implement:async o=>(await import("../lib/commands/implement.mjs")).implementCommand(o),context:async o=>(await import("../lib/commands/context.mjs")).contextCommand(o),"doc-gate":async o=>(await import("../lib/commands/docGate.mjs")).docGateCommand(o),"generate-skills":async o=>(await import("../lib/commands/generateSkills.mjs")).generateSkillsCommand(o)};function y(){const o=Object.keys(c),s=Math.max(...o.map(i=>i.length),8)+1;return Object.entries(c).map(([i,p])=>` ${i.padEnd(s," ")}${p}`).join(`
3
3
  `)}const w=`
4
- ${e("\u{1F525} infernoflow")} ${t("v"+i)}
5
- ${t("The forge for liquid code \u2014 keep every AI session in sync")}
4
+ ${t("\u{1F525} infernoflow")} ${e("v"+r)}
5
+ ${e("The forge for liquid code \u2014 keep every AI session in sync")}
6
6
 
7
- ${e("Usage:")}
7
+ ${t("Usage:")}
8
8
  infernoflow <command> [options]
9
9
 
10
- ${e("Commands:")}
11
- ${g()}
10
+ ${t("Commands:")}
11
+ ${y()}
12
12
 
13
- ${e("init options:")}
13
+ ${t("publish options:")}
14
+ --bump patch|minor|major Version bump type (default: patch)
15
+ --skip-build Skip the build step
16
+ --skip-tests Skip smoke tests
17
+ --skip-push Commit but don't git push
18
+ --tag Also create a git tag vX.Y.Z
19
+ --dry-run Print all steps without executing
20
+ --yes, -y Non-interactive (skip confirmation prompt)
21
+
22
+ ${t("setup options:")}
23
+ --yes, -y Skip prompts (non-interactive)
24
+ --force, -f Overwrite existing hook files
25
+
26
+ ${t("init options:")}
14
27
  --cursor-hooks Also install Cursor hooks (draft \u2192 inferno/CONTEXT.draft.md)
15
28
  --vscode-copilot-hooks Also install VS Code + Copilot hooks (.github/hooks \u2014 Preview)
16
29
  --adopt Infer capabilities from an existing codebase
@@ -23,46 +36,52 @@ ${g()}
23
36
  --yes, -y Skip prompts and accept inferred/default values
24
37
  --force, -f Overwrite existing inferno/ files
25
38
 
26
- ${e("install-cursor-hooks options:")}
39
+ ${t("install-cursor-hooks options:")}
27
40
  --force, -f Overwrite .cursor/hooks.json and hook scripts if they exist
28
41
 
29
- ${e("install-vscode-copilot-hooks options:")}
42
+ ${t("install-vscode-copilot-hooks options:")}
30
43
  --force, -f Overwrite .github/hooks/infernoflow-drafts.json and scripts if they exist
31
44
 
32
- ${e("context options:")}
45
+ ${t("context options:")}
33
46
  --intent "..." What you plan to build next
34
47
  --working "..." What you are building right now
35
48
  --decision "..." Record a decision or note
36
49
  --show Print context without writing file
37
50
  --copy, -c Copy context to clipboard instantly
38
51
  --reset Clear all stored state
52
+ --watch Poll git diff every 30s and auto-update CONTEXT.md (living context)
53
+ --interval <secs> Watch poll interval in seconds (default: 30)
54
+
55
+ ${t("generate-skills options:")}
56
+ --cursor Also install rules to .cursor/rules/infernoflow.md
57
+ --force, -f Overwrite existing generated skill files
39
58
 
40
- ${e("implement options:")}
59
+ ${t("implement options:")}
41
60
  --mode <type> cursor | generic | both (default: both)
42
61
  --copy, -c Copy generated prompt(s) to clipboard
43
62
 
44
- ${e("run options:")}
63
+ ${t("run options:")}
45
64
  --dry-run Execute full flow without writing files
46
65
  --json Emit machine-readable events and result payload
47
66
  --no-rollback Keep changes even if validation fails
48
67
  --provider <type> auto | agent | local | prompt (default: auto)
49
68
  --ide <name> auto | cursor | vscode | windsurf (default: auto)
50
69
 
51
- ${e("Typical workflow:")}
52
- ${t('1. infernoflow context --intent "what I want to build"')}
53
- ${t("2. [paste inferno/CONTEXT.md into Claude / Cursor / Copilot]")}
54
- ${t("3. [build the feature]")}
55
- ${t('4. infernoflow suggest "what I built"')}
56
- ${t("5. infernoflow check")}
70
+ ${t("Typical workflow:")}
71
+ ${e('1. infernoflow context --intent "what I want to build"')}
72
+ ${e("2. [paste inferno/CONTEXT.md into Claude / Cursor / Copilot]")}
73
+ ${e("3. [build the feature]")}
74
+ ${e('4. infernoflow suggest "what I built"')}
75
+ ${e("5. infernoflow check")}
57
76
 
58
- ${e("Machine output:")}
59
- ${t("status --json")}
60
- ${t("check --json")}
61
- ${t("doc-gate --json")}
62
- ${t("pr-impact --json")}
63
- ${t("sync --auto --json")}
64
- ${t('run "task" --json')}
65
- `,[,,n,...k]=process.argv;(!n||n==="--help"||n==="-h")&&(console.log(w),process.exit(0)),(n==="--version"||n==="-v")&&(console.log(i),process.exit(0));const C=Object.keys(c);C.includes(n)||(console.error(r(`
66
- Unknown command: ${n}`)),console.error(t(`Run: infernoflow --help
67
- `)),process.exit(1));const b=[n,...k];c[n](b).catch(o=>{console.error(r(`
77
+ ${t("Machine output:")}
78
+ ${e("status --json")}
79
+ ${e("check --json")}
80
+ ${e("doc-gate --json")}
81
+ ${e("pr-impact --json")}
82
+ ${e("sync --auto --json")}
83
+ ${e('run "task" --json')}
84
+ `;import*as k from"node:fs";import*as C from"node:path";try{const o=C.join(process.cwd(),"inferno");if(k.existsSync(o)){const{observeCommandStart:s}=await import("../lib/learning/observe.mjs"),i=process.argv[2];i&&!i.startsWith("-")&&s(o,i)}}catch{}const[,,n,...b]=process.argv;(!n||n==="--help"||n==="-h")&&(console.log(w),process.exit(0)),(n==="--version"||n==="-v")&&(console.log(r),process.exit(0));const v=Object.keys(l);v.includes(n)||(console.error(a(`
85
+ Unknown command: ${n}`)),console.error(e(`Run: infernoflow --help
86
+ `)),process.exit(1));const $=[n,...b];l[n]($).catch(o=>{console.error(a(`
68
87
  Error: `)+o.message),process.exit(1)});
@@ -0,0 +1 @@
1
+ import*as z from"node:fs";import*as w from"node:path";function S(m){try{return z.readFileSync(m,"utf8")}catch{return""}}function v(m,y){const d=new Set,p=new Set,h=new Set,u=new Set,A=new Set,f=new Map,c=(t,n,o,a)=>{f.has(t)||f.set(t,{id:t,title:n,reason:o,sourceFiles:new Set}),f.get(t).sourceFiles.add(w.relative(m,a))};for(const t of y){const n=w.relative(m,t).replace(/\\/g,"/"),o=S(t);if(o){if(/\.(ts)$/.test(t)){const a=o.matchAll(/@Component\s*\([^)]*\)[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Component)/g);for(const e of a){const s=e[1].replace(/Component$/,"");d.add(s);const i=s.endsWith("Page")||s.endsWith("View")?`View${s.replace(/(Page|View)$/,"")}`:`View${s}`;c(i,`View ${s.replace(/([A-Z])/g," $1").trim()}`,`@Component class detected: ${e[1]}`,t)}const l=o.matchAll(/@Injectable[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Service)/g);for(const e of l)h.add(e[1]);if([...o.matchAll(/FormBuilder|FormGroup|FormControl/g)].length>0){const e=o.matchAll(/['"]([a-zA-Z][a-zA-Z0-9_]*)['"]:\s*(?:this\.\w+\.control|new FormControl|\[)/g);for(const s of e)A.add(s[1])}}if(n.includes("routing")||n.includes("routes")||n.endsWith("app.routes.ts")){const a=o.matchAll(/\bpath\s*:\s*['"`]([^'"`]+)['"`]/g);for(const r of a){const e=r[1].trim();if(e&&e!=="**"&&!e.startsWith(":")){p.add(e);const s=e.split("/").filter(Boolean);if(s.length>=1){const i=s[s.length-1],C="View"+i.charAt(0).toUpperCase()+i.slice(1).replace(/-([a-z])/g,(g,F)=>F.toUpperCase()),$="View "+i.replace(/-/g," ").replace(/\b\w/g,g=>g.toUpperCase());c(C,$,`Route detected: /${e}`,t)}}}const l=o.matchAll(/loadChildren\s*:\s*\(\s*\)\s*=>\s*import\s*\(['"`]([^'"`]+)['"`]\)/g);for(const r of l)u.add(r[1])}if(/\.html$/.test(t)){const a=o.matchAll(/routerLink\s*=\s*['"`]([^'"`]+)['"`]/g);for(const r of a)p.add(r[1].replace(/^\//,""));const l=o.matchAll(/\(click\)\s*=\s*["']([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g);for(const r of l){const e=r[1];/delete|remove/i.test(e)&&c("DeleteItem","Delete Item",`(click) handler: ${e}`,t),/create|add|new/i.test(e)&&c("CreateItem","Create Item",`(click) handler: ${e}`,t),/submit|save/i.test(e)&&c("UpdateItem","Update Item",`(click) handler: ${e}`,t)}}}}return{components:Array.from(d).sort(),routes:Array.from(p).sort(),services:Array.from(h).sort(),lazyModules:Array.from(u).sort(),formFields:Array.from(A).sort(),capabilities:Array.from(f.values()).map(t=>({...t,sourceFiles:Array.from(t.sourceFiles)}))}}export{v as scanAngular};
@@ -0,0 +1 @@
1
+ import*as h from"node:fs";import"node:path";function g(c){try{return h.readFileSync(c,"utf8")}catch{return""}}function w(c,l){const t=new Set,a=new Set,s=new Set,d=new Set,f=new Set,m=l.filter(o=>/\.(css|scss|sass|less|styl)$/.test(o)||/\.(ts|tsx|js|jsx)$/.test(o)&&!o.includes("node_modules"));for(const o of m){const i=g(o);if(!i)continue;const u=i.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);for(const e of u){const r=`--${e[1]}`;t.add(r),/color|colour|bg|background|text|border|shadow|fill|stroke/i.test(e[1])?a.add(r):/space|spacing|gap|padding|margin|size|radius|width|height/i.test(e[1])?s.add(r):/theme|primary|secondary|accent|brand|dark|light/i.test(e[1])&&f.add(r)}if(/\.(css|scss|sass|less)$/.test(o)){const e=i.matchAll(/^\s*\.([a-zA-Z][a-zA-Z0-9_-]*)[\s{,]/gm);for(const r of e){const n=r[1];n.length<4||/^(flex|grid|block|hidden|text|font|bg|border|p-|m-|w-|h-)/.test(n)||/^(active|disabled|hover|focus|error|success|warning)$/.test(n)||d.add(n)}}if(/\.(ts|tsx|js|jsx)$/.test(o)){const e=i.matchAll(/(?:styled|css)`[^`]*--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);for(const n of e)t.add(`--${n[1]}`);const r=i.matchAll(/\[--([a-zA-Z][a-zA-Z0-9_-]*)\]/g);for(const n of r)t.add(`--${n[1]}`)}}return{designTokens:Array.from(t).sort().slice(0,40),colorTokens:Array.from(a).sort().slice(0,20),spacingTokens:Array.from(s).sort().slice(0,15),componentClasses:Array.from(d).sort().slice(0,30),themeVars:Array.from(f).sort().slice(0,15)}}function y(c,l=[]){const t=s=>l.includes(s),a=s=>s.test(c);return t("tailwindcss")||a(/\b(?:flex|grid|px-\d|py-\d|text-\w+|bg-\w+|rounded)/)?"tailwind":t("bootstrap")||a(/\b(?:container|row|col-|btn btn-|navbar|card)/)?"bootstrap":l.some(s=>s.startsWith("@angular/material"))?"angular-material":t("antd")||a(/\bant-/)?"ant-design":t("@mui/material")||t("@material-ui/core")?"mui":t("styled-components")?"styled-components":t("@emotion/react")||t("@emotion/styled")?"emotion":t("@chakra-ui/react")?"chakra-ui":t("@radix-ui/react-primitive")?"radix-ui":"unknown"}export{y as detectCSSFramework,w as scanCSS};
@@ -0,0 +1 @@
1
+ import*as w from"node:fs";import*as C from"node:path";function k(c){try{return w.readFileSync(c,"utf8")}catch{return""}}function I(c,d){const n=new Set,l=new Set,i=new Set,r=new Map,a=(t,s,m,f)=>{r.has(t)||r.set(t,{id:t,title:s,reason:m,sourceFiles:new Set}),r.get(t).sourceFiles.add(C.relative(c,f))};for(const t of d){if(!/\.(tsx?|jsx?)$/.test(t))continue;const s=k(t);if(!s)continue;const m=s.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);for(const o of m)if(n.add(o[1]),/Page|View|Screen|Dashboard|Panel|Modal|Dialog/i.test(o[1])){const e="View"+o[1].replace(/(Page|View|Screen|Dashboard|Panel|Modal|Dialog)$/,"");a(e,`View ${o[1].replace(/([A-Z])/g," $1").trim()}`,`React component: ${o[1]}`,t)}const f=s.matchAll(/(?:export\s+)?const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:React\.memo\()?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/g);for(const o of f)n.add(o[1]);const p=s.matchAll(/export\s+(?:default\s+)?function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/g);for(const o of p)l.add(o[1]);const h=s.matchAll(/(?:export\s+)?const\s+(use[A-Z][A-Za-z0-9_]*)\s*=/g);for(const o of h)l.add(o[1]);const u=s.matchAll(/<Route[^>]+path\s*=\s*["'`]([^"'`]+)["'`]/g);for(const o of u){const e=o[1].replace(/^\//,"").replace(/:[\w]+/g,"{id}");e&&i.add(e)}const g=s.matchAll(/path\s*:\s*["'`]([^"'`]+)["'`]/g);for(const o of g){const e=o[1].replace(/^\//,"").replace(/:[\w]+/g,"{id}");e&&e!=="*"&&e.length<60&&i.add(e)}const A=s.matchAll(/onClick\s*=\s*\{(?:[^}]*\b(delete|remove|create|add|submit|save|search|filter|toggle|update|edit)\b[^}]*)\}/gi);for(const o of A){const e=o[1].toLowerCase();(e==="delete"||e==="remove")&&a("DeleteItem","Delete Item",`onClick handler contains "${e}"`,t),(e==="create"||e==="add")&&a("CreateItem","Create Item",`onClick handler contains "${e}"`,t),(e==="submit"||e==="save"||e==="update"||e==="edit")&&a("UpdateItem","Update Item",`onClick handler contains "${e}"`,t),e==="search"&&a("SearchItems","Search Items",'onClick handler contains "search"',t),e==="filter"&&a("FilterItems","Filter Items",'onClick handler contains "filter"',t),e==="toggle"&&a("ToggleComplete","Toggle Complete",'onClick handler contains "toggle"',t)}}return{components:Array.from(n).sort(),customHooks:Array.from(l).sort(),routes:Array.from(i).sort(),capabilities:Array.from(r.values()).map(t=>({...t,sourceFiles:Array.from(t.sourceFiles)}))}}export{I as scanReact};
@@ -1,13 +1,13 @@
1
- import*as C from"node:fs";import*as b from"node:path";import*as D from"node:readline";function M(t){return t.replace(/[^a-zA-Z0-9]+/g," ").trim().split(/\s+/).filter(Boolean).map(l=>l[0].toUpperCase()+l.slice(1)).join("")}function z(t){return t.replace(/([A-Z])/g," $1").trim()}function S(t){try{return C.readFileSync(t,"utf8")}catch{return""}}const R=[{id:"CreateItem",title:"Create Item",regex:/\b(post|create|add)\b/i},{id:"ReadItems",title:"Read Items",regex:/\b(get|read|list|fetch)\b/i},{id:"UpdateItem",title:"Update Item",regex:/\b(put|patch|update|edit)\b/i},{id:"DeleteItem",title:"Delete Item",regex:/\b(delete|remove)\b/i},{id:"SearchItems",title:"Search Items",regex:/\bsearch\b/i},{id:"FilterItems",title:"Filter Items",regex:/\bfilter\b/i},{id:"SetDueDate",title:"Set Due Date",regex:/\bdueDate|deadline|due\b/i},{id:"SetPriority",title:"Set Priority",regex:/\bpriority\b/i},{id:"ToggleComplete",title:"Toggle Complete",regex:/\bcomplete|completed|toggle\b/i},{id:"ClearCompleted",title:"Clear Completed",regex:/\bclearCompleted|clear completed\b/i}];function K(t){return J(t).capabilities}function U(t){const l=[],r=["src","server","app","backend","frontend","api","Controllers"];for(const i of r){const n=b.join(t,i);if(!C.existsSync(n))continue;const e=[n];for(;e.length;){const c=e.pop();for(const s of C.readdirSync(c,{withFileTypes:!0})){const o=b.join(c,s.name);if(s.isDirectory()){if(["node_modules",".git","dist","build"].includes(s.name))continue;e.push(o)}else/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm|cs|csproj)$/.test(s.name)&&l.push(o)}}}for(const i of C.readdirSync(t,{withFileTypes:!0}))i.isFile()&&/^(Program\.cs|.+\.csproj)$/i.test(i.name)&&l.push(b.join(t,i.name));return l}function O(t,l){const r=new Set;for(const i of t){const n=b.relative(l,i),e=S(i),c=e.matchAll(/\bclass\s+([A-Z][A-Za-z0-9_]*?(?:Component|Page|View|Widget|Card))\b/g);for(const A of c)r.add(A[1]);const s=e.matchAll(/\bselector\s*:\s*["']([^"']+)["']/g);for(const A of s)r.add(A[1]);const o=e.matchAll(/\bfunction\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);for(const A of o)/component|page|view|card|chart|dashboard/i.test(A[1])&&r.add(A[1]);const p=n.match(/([^/\\]+)\.(component|page|view|widget|card)\.(ts|tsx|js|jsx)$/i);p&&r.add(p[1])}return Array.from(r).sort()}function H(t){const l=new Set,r=new Set,i=new Set(["if","for","while","const","let","var","return","function","class","import","export","null","undefined","true","false","string","number","boolean","any","unknown","never","selector","templateUrl","styleUrl","standalone","imports","providers","providedIn","options","scales","responsive","display","title","type","label","component","service","routes","appConfig","ApplicationConfig"]),n=e=>{e&&/^[A-Za-z_][A-Za-z0-9_]*$/.test(e)&&(e.length<=1||i.has(e)||/^[A-Z0-9_]+$/.test(e)||l.add(e))};for(const e of t){const c=S(e);if(/\.(html|htm)$/i.test(e)){const s=c.matchAll(/\{\{\s*(?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);for(const f of s)n(f[1]);const o=c.matchAll(/\[\(ngModel\)\]\s*=\s*["']([a-zA-Z_][a-zA-Z0-9_]*)["']/g);for(const f of o)n(f[1]);const p=c.matchAll(/\[[a-zA-Z0-9_-]+\]\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)["']/g);for(const f of p)n(f[1]);const A=c.matchAll(/\*ngIf\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);for(const f of A)n(f[1])}if(/\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(e)){const s=c.matchAll(/(?:^|\n)\s*(?:public|private|protected)?\s*(?:async\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*\{/g);for(const d of s)r.add(d[1]);const o=c.matchAll(/\bthis\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g);for(const d of o)n(d[1]);const p=c.matchAll(/(?:^|\n)\s*(?:public|private|protected)?\s*(?:readonly\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*(?::|=)/g);for(const d of p)n(d[1]);const A=c.matchAll(/@Input\([^)]*\)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*[:=]/g);for(const d of A)n(d[1]);const f=c.matchAll(/forEach\(\((\w+)\)\s*=>/g);for(const d of f){const w=d[1],j=new RegExp(`\\b${w}\\.([a-zA-Z_][a-zA-Z0-9_]*)\\b`,"g");for(const g of c.matchAll(j))n(g[1])}}}return Array.from(l).filter(e=>!r.has(e)).sort().slice(0,80)}function W(t){const l=new Set,r=b.join(t,"package.json");if(!C.existsSync(r))return[];try{const i=JSON.parse(S(r)||"{}"),n={...i.dependencies||{},...i.devDependencies||{}};for(const e of Object.keys(n))l.add(e)}catch{}return Array.from(l).sort()}function G(t,l,r){const i=l.filter(s=>/\.(css|scss|sass|less|styl)$/i.test(s)).map(s=>b.relative(t,s)).sort(),n=[],e=s=>r.includes(s);e("tailwindcss")&&n.push("Tailwind CSS"),e("bootstrap")&&n.push("Bootstrap"),r.some(s=>s.startsWith("@angular/material"))&&n.push("Angular Material"),e("antd")&&n.push("Ant Design"),e("styled-components")&&n.push("styled-components"),(e("emotion")||e("@emotion/react"))&&n.push("Emotion");const c=new Set;for(const s of l){if(!/\.(css|scss|sass|less|styl|html|htm|ts|tsx|js|jsx|mjs|cjs)$/i.test(s))continue;const o=S(s);for(const p of o.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)/g))c.add(`--${p[1]}`)}return{cssFrameworks:n,styleFileCount:i.length,styleFilesSample:i.slice(0,12),designTokens:Array.from(c).sort().slice(0,24)}}function N(t){let l=!1,r=!1;const i=new Set;for(const e of t){if(!/\.(html|htm|tsx|jsx|ts|js|mjs|cjs)$/i.test(e))continue;const c=S(e);/\bgrid\b|grid-template|grid-cols-|display:\s*grid/i.test(c)&&(l=!0),/\bflex\b|display:\s*flex|flex-row|flex-col|justify-|items-/i.test(c)&&(r=!0);for(const s of c.matchAll(/<(main|header|footer|section|aside|nav)\b/gi))i.add(s[1].toLowerCase());for(const s of c.matchAll(/class(?:Name)?\s*=\s*["'`][^"'`]*(dashboard|chart|card|sidebar|content|toolbar|filter|panel|table)[^"'`]*["'`]/gi)){const o=s[1].toLowerCase();i.add(o==="filter"?"filters":o)}}return{layoutType:l&&r?"mixed":l?"grid":r?"flex":"unknown",usesGrid:l,usesFlex:r,sections:Array.from(i).sort()}}function B(t,l,r,i={}){const n={ts:0,js:0,py:0,java:0,go:0,rb:0,rs:0,cs:0,php:0};for(const g of l){const y=b.extname(g).toLowerCase();(y===".ts"||y===".tsx")&&(n.ts+=1),(y===".js"||y===".jsx"||y===".mjs"||y===".cjs")&&(n.js+=1),y===".py"&&(n.py+=1),y===".java"&&(n.java+=1),y===".go"&&(n.go+=1),y===".rb"&&(n.rb+=1),y===".rs"&&(n.rs+=1),y===".cs"&&(n.cs+=1),y===".php"&&(n.php+=1)}const e=Object.entries(n).sort((g,y)=>y[1]-g[1]),c=e[0]?.[1]>0?e[0][0]:"unknown";let s="unknown",o=!1,p=!1,A=!1;for(const g of l){const y=b.basename(g).toLowerCase();if(y.endsWith(".csproj")){const k=S(g);/Microsoft\.NET\.Sdk\.Web/i.test(k)&&(o=!0),(/Blazor/i.test(k)||/Microsoft\.AspNetCore\.Components/i.test(k))&&(A=!0)}if(y==="program.cs"){const k=S(g);/app\.Map(Get|Post|Put|Delete|Patch)\s*\(/i.test(k)&&(p=!0)}}const f=g=>r.includes(g);r.some(g=>g.startsWith("@angular/"))?s="angular":f("react")?s="react":f("vue")?s="vue":f("svelte")?s="svelte":f("next")?s="nextjs":f("nuxt")?s="nuxt":f("express")?s="express":f("@nestjs/core")?s="nestjs":f("fastify")?s="fastify":f("flask")?s="flask":f("django")?s="django":f("spring-boot")?s="spring":A?s="blazor":p?s="minimalapi":(o||n.cs>0)&&(s="aspnet");let d="fullstack";const w=["src","frontend","app"].some(g=>C.existsSync(b.join(t,g))),j=["server","backend","api"].some(g=>C.existsSync(b.join(t,g)));return["react","angular","vue","svelte","nextjs","nuxt"].includes(s)&&(d="frontend"),["express","nestjs","fastify","flask","django","spring","aspnet","minimalapi"].includes(s)&&(d="backend"),w&&j&&(d="fullstack"),!w&&!j&&(d="library"),s==="blazor"&&(d="frontend"),{language:i.language||c,framework:i.framework||s,projectType:i.projectType||d,detected:{language:c,framework:s,projectType:d}}}function q(t,l){const r=[],i=new Set,n=s=>{let o=String(s||"").trim();return o?(o=o.replace(/https?:\/\/[^/]+/gi,""),o=o.replace(/\$\{[^}]+\}/g,"{var}"),o=o.replace(/\{[A-Za-z_][A-Za-z0-9_]*\}/g,"{var}"),o=o.replace(/:[A-Za-z_][A-Za-z0-9_]*/g,"{var}"),o=o.replace(/\/\d+(?=\/|$)/g,"/{id}"),o=o.replace(/=[^&\s]+/g,"={value}"),o=o.replace(/\/+/g,"/"),o):""},e=s=>{const o=n(s.endpointPattern);if(!o)return;const p=`${s.method}|${o}|${s.sourceFile}|${s.style}`;i.has(p)||(i.add(p),r.push({...s,endpointPattern:o}))};for(const s of l){if(!/\.(ts|tsx|js|jsx|mjs|cjs|cs)$/i.test(s))continue;const o=b.relative(t,s),p=S(s);if(!(/service|api|client|controller|program\.cs/i.test(o)||/HttpClient|fetch\(|app\.Map(Get|Post|Put|Delete|Patch)\(/i.test(p)))continue;const f=p.replace(/\r\n/g,`
2
- `),d={},w=u=>{let a=String(u||"").trim();if(!a)return"";for(a=a.replace(/;+$/,"").trim(),a=a.replace(/\(\s*$/,"");a.startsWith("'")&&a.endsWith("'")||a.startsWith('"')&&a.endsWith('"')||a.startsWith("`")&&a.endsWith("`");)a=a.slice(1,-1).trim();return a},j=u=>{const a=String(u||"").trim();return a?!!(/^https?:\/\//i.test(a)||a.startsWith("/")||/\bapi\b/i.test(a)||/\$\{[^}]+\}/.test(a)||/\?[^=\s]+=?/.test(a)||/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_./${}-]+$/.test(a)):!1},g=(u,a)=>{!u||!a||(d[u]=w(a))},y=/(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;for(const u of f.matchAll(y))g(u[1],u[2]);const k=/(?:public|private|protected)?\s*(?:readonly\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;for(const u of f.matchAll(k))g(u[1],u[2]);const F=u=>{const a=w(u);if(!a)return"";if(/^['"`][\s\S]*['"`]$/.test(a))return a.replace(/^['"`]|['"`]$/g,"");if(d[a]&&j(d[a]))return d[a];const m=a.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);if(m&&d[m[1]]&&j(d[m[1]]))return d[m[1]];const $=a.split("+").map(h=>h.trim()).filter(Boolean);if($.length>1){const h=$.map(x=>{if(/^['"`][\s\S]*['"`]$/.test(x))return x.replace(/^['"`]|['"`]$/g,"");if(d[x]&&j(d[x]))return d[x];const v=x.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);return v&&d[v[1]]&&j(d[v[1]])?d[v[1]]:`{${x}}`}).join("");if(h)return h}const P=a.match(/^(.+?)\?(.+?):(.+)$/s);if(P){const h=F(P[2]),x=F(P[3]);if(h||x)return`${h||"{optionA}"} | ${x||"{optionB}"}`}return a},Z=/\.\s*(get|post|put|patch|delete)\s*(?:<[\s\S]*?>)?\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of f.matchAll(Z)){const a=u[1].toUpperCase(),m=F(u[2]);!m||!j(m)||e({method:a,endpointPattern:m,style:"httpClient",sourceFile:o})}const _=/\bfetch\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of f.matchAll(_)){const a=F(u[1]);if(!a||!j(a))continue;const m=u.index||0,$=f.slice(m,m+260),h=(/method\s*:\s*["'](GET|POST|PUT|PATCH|DELETE)["']/i.exec($)?.[1]||"GET").toUpperCase();e({method:h,endpointPattern:a,style:"fetch",sourceFile:o})}const I=/\baxios\.(get|post|put|patch|delete)\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of f.matchAll(I)){const a=u[1].toUpperCase(),m=F(u[2]);!m||!j(m)||e({method:a,endpointPattern:m,style:"axios",sourceFile:o})}const T=/\baxios\s*\(\s*\{([\s\S]*?)\}\s*\)/gi;for(const u of f.matchAll(T)){const a=u[1],m=/\bmethod\s*:\s*["']?(get|post|put|patch|delete)["']?/i.exec(a),$=/\burl\s*:\s*([^,\n]+)/i.exec(a),P=(m?.[1]||"get").toUpperCase(),h=F($?.[1]||"");!h||!j(h)||e({method:P,endpointPattern:h,style:"axios-config",sourceFile:o})}const E=/\.\s*request\s*\(\s*["'](GET|POST|PUT|PATCH|DELETE)["']\s*,\s*([\s\S]*?)(?:,|\))/gi;for(const u of f.matchAll(E)){const a=u[1].toUpperCase(),m=F(u[2]);!m||!j(m)||e({method:a,endpointPattern:m,style:"request",sourceFile:o})}if(/\.cs$/i.test(s)){const u=/\bapp\.Map(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]+)"/gi;for(const h of f.matchAll(u))e({method:h[1].toUpperCase(),endpointPattern:h[2],style:"csharp-map",sourceFile:o});const a=/\[Route\("([^"]+)"\)\][\s\S]*?class\s+\w+/i.exec(f),m=a?a[1]:"",$=/\[(HttpGet|HttpPost|HttpPut|HttpDelete|HttpPatch)(?:\("([^"]*)"\))?\]/gi;for(const h of f.matchAll($)){const x=h[1].replace("Http","").toUpperCase(),v=h[2]||"",L=[m,v].filter(Boolean).join("/").replace(/\/+/g,"/").replace(/\[controller\]/gi,"{controller}");e({method:x,endpointPattern:L||m||"{controller-route}",style:"csharp-controller",sourceFile:o})}const P=/\b(GetAsync|PostAsync|PutAsync|DeleteAsync|SendAsync)\s*\(\s*"([^"]+)"/gi;for(const h of f.matchAll(P)){const x=h[1].replace("Async","").replace("Send","SEND").toUpperCase();e({method:x,endpointPattern:h[2],style:"csharp-httpclient",sourceFile:o})}}}const c=r.reduce((s,o)=>(s[o.method]=(s[o.method]||0)+1,s),{});return{totalCalls:r.length,byMethod:c,calls:r.slice(0,80)}}function J(t,l={}){const r=U(t),i=new Map,n=(o,p)=>{i.has(o.id)||i.set(o.id,{id:o.id,title:o.title,reason:"Detected from code signals",sourceFiles:new Set}),i.get(o.id).sourceFiles.add(b.relative(t,p))};for(const o of r){const p=S(o);for(const A of R)A.regex.test(p)&&n(A,o)}const e=b.join(t,"package.json");if(C.existsSync(e)){const o=JSON.parse(S(e)||"{}"),p=typeof o.name=="string"?o.name:b.basename(t);M(p)&&!i.size&&(i.set("ReadItems",{id:"ReadItems",title:"Read Items",reason:`Fallback default for ${p}`,sourceFiles:new Set}),i.set("CreateItem",{id:"CreateItem",title:"Create Item",reason:`Fallback default for ${p}`,sourceFiles:new Set}))}i.size||(i.set("CreateItem",{id:"CreateItem",title:"Create Item",reason:"Fallback default",sourceFiles:new Set}),i.set("ReadItems",{id:"ReadItems",title:"Read Items",reason:"Fallback default",sourceFiles:new Set}));const c=Array.from(i.values()).map(o=>({...o,sourceFiles:Array.from(o.sourceFiles||[])})),s=W(t);return{capabilities:c,components:O(r,t),displayFields:H(r),externalLibraries:s,uiLayout:N(r),styling:G(t,r,s),developmentProfile:B(t,r,s,l),apiCalls:q(t,r)}}async function Q(t,l=!1){if(l)return t;const r=D.createInterface({input:process.stdin,output:process.stdout}),i=t.map(c=>c.id).join(", "),n=await new Promise(c=>r.question(` Inferred capabilities (${i}). Press Enter to accept or type comma list: `,c));r.close();const e=String(n).trim();return e?e.split(",").map(c=>c.trim()).filter(Boolean).map(c=>({id:c,title:z(c),reason:"User provided during adopt review"})):t}function X(t){if(!t.length)return"No capabilities inferred.";const l=V(t),r=l.reduce((e,c)=>e+c.signalCount,0),i={high:l.filter(e=>e.confidence==="high").length,medium:l.filter(e=>e.confidence==="medium").length,low:l.filter(e=>e.confidence==="low").length},n=[];n.push("Adoption Analysis"),n.push("=".repeat(56)),n.push(`Capabilities detected : ${l.length}`),n.push(`Signal hits total : ${r}`),n.push(`Confidence mix : high=${i.high}, medium=${i.medium}, low=${i.low}`),n.push("-".repeat(56)),n.push("Capability Breakdown"),n.push("-".repeat(56)),n.push("Confidence Signals Capability"),n.push("-".repeat(56));for(const e of l){const c=e.confidence.toUpperCase().padEnd(10," "),s=String(e.signalCount).padEnd(7," ");if(n.push(`${c} ${s} ${e.id} (${e.title})`),e.signalCount>0){const o=e.sourceFiles.slice(0,3).join(", ");n.push(` sources: ${o}`),e.sourceFiles.length>3&&n.push(` more : +${e.sourceFiles.length-3} additional files`)}else n.push(" sources: inferred fallback (no strong code signal)")}return n.push("=".repeat(56)),n.join(`
3
- `)}function Y(t){const l=(r,i,n=10)=>{const e=[`${r} (${i.length})`];if(e.push("-".repeat(56)),!i.length)return e.push(" - none"),e.join(`
4
- `);for(const c of i.slice(0,n))e.push(` - ${c}`);return i.length>n&&e.push(` - ... +${i.length-n} more`),e.join(`
5
- `)};return["Project Structure Signals","=".repeat(56),l("Components",t.components||[]),l("Display fields",t.displayFields||[]),l("External libraries",t.externalLibraries||[]),"UI layout","-".repeat(56),` - layout type: ${t.uiLayout?.layoutType||"unknown"}`,` - uses grid : ${t.uiLayout?.usesGrid?"yes":"no"}`,` - uses flex : ${t.uiLayout?.usesFlex?"yes":"no"}`,` - sections : ${(t.uiLayout?.sections||[]).slice(0,10).join(", ")||"none"}`,"Styling","-".repeat(56),` - frameworks : ${(t.styling?.cssFrameworks||[]).join(", ")||"none detected"}`,` - style files: ${t.styling?.styleFileCount??0}`,` - tokens : ${(t.styling?.designTokens||[]).slice(0,8).join(", ")||"none detected"}`,"Development profile","-".repeat(56),` - language : ${t.developmentProfile?.language||"unknown"} (auto: ${t.developmentProfile?.detected?.language||"unknown"})`,` - framework : ${t.developmentProfile?.framework||"unknown"} (auto: ${t.developmentProfile?.detected?.framework||"unknown"})`,` - project type: ${t.developmentProfile?.projectType||"unknown"} (auto: ${t.developmentProfile?.detected?.projectType||"unknown"})`,"API calls","-".repeat(56),` - total calls : ${t.apiCalls?.totalCalls??0}`,` - by method : ${Object.entries(t.apiCalls?.byMethod||{}).map(([r,i])=>`${r}:${i}`).join(", ")||"none"}`,...(t.apiCalls?.calls||[]).slice(0,6).map(r=>` - ${r.method} ${r.endpointPattern} [${r.style}] (${r.sourceFile})`),...(t.apiCalls?.calls||[]).length>6?[` - ... +${(t.apiCalls?.calls||[]).length-6} more`]:[],"=".repeat(56)].join(`
6
- `)}function V(t){return t.map(l=>{const r=l.sourceFiles?.length||0,i=r>=3?"high":r>=1?"medium":"low";return{id:l.id,title:l.title,reason:l.reason,confidence:i,sourceFiles:l.sourceFiles||[],signalCount:r}})}function ee(t,l,r,i=null){const n=r.map(p=>p.id),e={policyId:l,policyVersion:1,capabilities:n,rules:{docsRequiredOnCapabilityChange:!0,requireScenarioForEachCapability:!0,requireChangelogOnCapabilityChange:!0}};C.mkdirSync(b.join(t,"scenarios"),{recursive:!0}),C.writeFileSync(b.join(t,"contract.json"),JSON.stringify(e,null,2)+`
7
- `);const c={schemaVersion:1,capabilities:r.map(p=>({id:p.id,title:p.title||z(p.id),since:"0.1.0"}))};C.writeFileSync(b.join(t,"capabilities.json"),JSON.stringify(c,null,2)+`
8
- `);const s={scenarioId:"adoption_baseline",description:"Baseline inferred from existing codebase during adoption",capabilitiesCovered:n,steps:n.map(p=>({action:p,expect:`${p} behavior exists in the current project`}))};if(C.writeFileSync(b.join(t,"scenarios","adoption_baseline.json"),JSON.stringify(s,null,2)+`
9
- `),i){const p={profileId:"adoption_profile",generatedAt:new Date().toISOString(),components:i.components||[],displayFields:i.displayFields||[],externalLibraries:i.externalLibraries||[],uiLayout:i.uiLayout||{layoutType:"unknown",usesGrid:!1,usesFlex:!1,sections:[]},styling:i.styling||{cssFrameworks:[],styleFileCount:0,styleFilesSample:[],designTokens:[]},developmentProfile:i.developmentProfile||{language:"unknown",framework:"unknown",projectType:"unknown",detected:{language:"unknown",framework:"unknown",projectType:"unknown"}},apiCalls:i.apiCalls||{totalCalls:0,byMethod:{},calls:[]}};C.writeFileSync(b.join(t,"adoption_profile.json"),JSON.stringify(p,null,2)+`
10
- `)}const o=`# Changelog \u2014 ${l}
1
+ import*as j from"node:fs";import*as A from"node:path";import*as D from"node:readline";import{seedProfileFromAdoption as R}from"../learning/profile.mjs";import{scanAngular as U}from"../adopters/angular.mjs";import{scanReact as M}from"../adopters/react.mjs";import{scanCSS as O}from"../adopters/css.mjs";function H(e){return e.replace(/[^a-zA-Z0-9]+/g," ").trim().split(/\s+/).filter(Boolean).map(r=>r[0].toUpperCase()+r.slice(1)).join("")}function _(e){return e.replace(/([A-Z])/g," $1").trim()}function k(e){try{return j.readFileSync(e,"utf8")}catch{return""}}const W=[{id:"CreateItem",title:"Create Item",regex:/\b(post|create|add)\b/i},{id:"ReadItems",title:"Read Items",regex:/\b(get|read|list|fetch)\b/i},{id:"UpdateItem",title:"Update Item",regex:/\b(put|patch|update|edit)\b/i},{id:"DeleteItem",title:"Delete Item",regex:/\b(delete|remove)\b/i},{id:"SearchItems",title:"Search Items",regex:/\bsearch\b/i},{id:"FilterItems",title:"Filter Items",regex:/\bfilter\b/i},{id:"SetDueDate",title:"Set Due Date",regex:/\bdueDate|deadline|due\b/i},{id:"SetPriority",title:"Set Priority",regex:/\bpriority\b/i},{id:"ToggleComplete",title:"Toggle Complete",regex:/\bcomplete|completed|toggle\b/i},{id:"ClearCompleted",title:"Clear Completed",regex:/\bclearCompleted|clear completed\b/i}];function ie(e){return X(e).capabilities}function G(e){const r=[],i=["src","server","app","backend","frontend","api","Controllers"];for(const o of i){const n=A.join(e,o);if(!j.existsSync(n))continue;const t=[n];for(;t.length;){const p=t.pop();for(const s of j.readdirSync(p,{withFileTypes:!0})){const a=A.join(p,s.name);if(s.isDirectory()){if(new Set(["node_modules",".git","dist","build","out","www","tmp",".tmp","vendor","assets","public","static","coverage",".nyc_output",".angular",".vite",".cache",".parcel-cache",".next",".nuxt","__pycache__","e2e","test","tests","spec","__tests__","fixtures","mocks",".turbo","storybook-static"]).has(s.name))continue;t.push(a)}else if(/\.(js|jsx|ts|tsx|mjs|cjs|json|md|html|htm|cs|csproj)$/.test(s.name)){if(/\.(min|bundle)\.(js|css)$/.test(s.name)||/\.map$/.test(s.name)||/\.(spec|test)\.(ts|js|tsx|jsx)$/.test(s.name))continue;r.push(a)}}}}for(const o of j.readdirSync(e,{withFileTypes:!0}))o.isFile()&&/^(Program\.cs|.+\.csproj)$/i.test(o.name)&&r.push(A.join(e,o.name));return r}function N(e,r){const i=new Set;for(const o of e){const n=A.relative(r,o),t=k(o),p=t.matchAll(/\bclass\s+([A-Z][A-Za-z0-9_]*?(?:Component|Page|View|Widget|Card))\b/g);for(const m of p)i.add(m[1]);const s=t.matchAll(/\bselector\s*:\s*["']([^"']+)["']/g);for(const m of s)i.add(m[1]);const a=t.matchAll(/\bfunction\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);for(const m of a)/component|page|view|card|chart|dashboard/i.test(m[1])&&i.add(m[1]);const d=n.match(/([^/\\]+)\.(component|page|view|widget|card)\.(ts|tsx|js|jsx)$/i);d&&i.add(d[1])}return Array.from(i).sort()}function B(e){const r=new Set,i=new Set,o=new Set(["if","for","while","const","let","var","return","function","class","import","export","null","undefined","true","false","string","number","boolean","any","unknown","never","selector","templateUrl","styleUrl","standalone","imports","providers","providedIn","options","scales","responsive","display","title","type","label","component","service","routes","appConfig","ApplicationConfig"]),n=t=>{t&&/^[A-Za-z_][A-Za-z0-9_]*$/.test(t)&&(t.length<=1||o.has(t)||/^[A-Z0-9_]+$/.test(t)||r.add(t))};for(const t of e){const p=k(t);if(/\.(html|htm)$/i.test(t)){const s=p.matchAll(/\{\{\s*(?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);for(const c of s)n(c[1]);const a=p.matchAll(/\[\(ngModel\)\]\s*=\s*["']([a-zA-Z_][a-zA-Z0-9_]*)["']/g);for(const c of a)n(c[1]);const d=p.matchAll(/\[[a-zA-Z0-9_-]+\]\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)["']/g);for(const c of d)n(c[1]);const m=p.matchAll(/\*ngIf\s*=\s*["'](?:this\.)?([a-zA-Z_][a-zA-Z0-9_]*)/g);for(const c of m)n(c[1])}if(/\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(t)){const s=p.matchAll(/(?:^|\n)\s*(?:public|private|protected)?\s*(?:async\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*\{/g);for(const f of s)i.add(f[1]);const a=p.matchAll(/\bthis\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g);for(const f of a)n(f[1]);const d=p.matchAll(/(?:^|\n)\s*(?:public|private|protected)?\s*(?:readonly\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*(?::|=)/g);for(const f of d)n(f[1]);const m=p.matchAll(/@Input\([^)]*\)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*[:=]/g);for(const f of m)n(f[1]);const c=p.matchAll(/forEach\(\((\w+)\)\s*=>/g);for(const f of c){const C=f[1],S=new RegExp(`\\b${C}\\.([a-zA-Z_][a-zA-Z0-9_]*)\\b`,"g");for(const y of p.matchAll(S))n(y[1])}}}return Array.from(r).filter(t=>!i.has(t)).sort().slice(0,80)}function V(e){const r=new Set,i=A.join(e,"package.json");if(!j.existsSync(i))return[];try{const o=JSON.parse(k(i)||"{}"),n={...o.dependencies||{},...o.devDependencies||{}};for(const t of Object.keys(n))r.add(t)}catch{}return Array.from(r).sort()}function q(e,r,i){const o=r.filter(s=>/\.(css|scss|sass|less|styl)$/i.test(s)).map(s=>A.relative(e,s)).sort(),n=[],t=s=>i.includes(s);t("tailwindcss")&&n.push("Tailwind CSS"),t("bootstrap")&&n.push("Bootstrap"),i.some(s=>s.startsWith("@angular/material"))&&n.push("Angular Material"),t("antd")&&n.push("Ant Design"),t("styled-components")&&n.push("styled-components"),(t("emotion")||t("@emotion/react"))&&n.push("Emotion");const p=new Set;for(const s of r){if(!/\.(css|scss|sass|less|styl|html|htm|ts|tsx|js|jsx|mjs|cjs)$/i.test(s))continue;const a=k(s);for(const d of a.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)/g))p.add(`--${d[1]}`)}return{cssFrameworks:n,styleFileCount:o.length,styleFilesSample:o.slice(0,12),designTokens:Array.from(p).sort().slice(0,24)}}function J(e){let r=!1,i=!1;const o=new Set;for(const t of e){if(!/\.(html|htm|tsx|jsx|ts|js|mjs|cjs)$/i.test(t))continue;const p=k(t);/\bgrid\b|grid-template|grid-cols-|display:\s*grid/i.test(p)&&(r=!0),/\bflex\b|display:\s*flex|flex-row|flex-col|justify-|items-/i.test(p)&&(i=!0);for(const s of p.matchAll(/<(main|header|footer|section|aside|nav)\b/gi))o.add(s[1].toLowerCase());for(const s of p.matchAll(/class(?:Name)?\s*=\s*["'`][^"'`]*(dashboard|chart|card|sidebar|content|toolbar|filter|panel|table)[^"'`]*["'`]/gi)){const a=s[1].toLowerCase();o.add(a==="filter"?"filters":a)}}return{layoutType:r&&i?"mixed":r?"grid":i?"flex":"unknown",usesGrid:r,usesFlex:i,sections:Array.from(o).sort()}}function K(e,r,i,o={}){const n={ts:0,js:0,py:0,java:0,go:0,rb:0,rs:0,cs:0,php:0};for(const y of r){const b=A.extname(y).toLowerCase();(b===".ts"||b===".tsx")&&(n.ts+=1),(b===".js"||b===".jsx"||b===".mjs"||b===".cjs")&&(n.js+=1),b===".py"&&(n.py+=1),b===".java"&&(n.java+=1),b===".go"&&(n.go+=1),b===".rb"&&(n.rb+=1),b===".rs"&&(n.rs+=1),b===".cs"&&(n.cs+=1),b===".php"&&(n.php+=1)}const t=Object.entries(n).sort((y,b)=>b[1]-y[1]),p=t[0]?.[1]>0?t[0][0]:"unknown";let s="unknown",a=!1,d=!1,m=!1;for(const y of r){const b=A.basename(y).toLowerCase();if(b.endsWith(".csproj")){const P=k(y);/Microsoft\.NET\.Sdk\.Web/i.test(P)&&(a=!0),(/Blazor/i.test(P)||/Microsoft\.AspNetCore\.Components/i.test(P))&&(m=!0)}if(b==="program.cs"){const P=k(y);/app\.Map(Get|Post|Put|Delete|Patch)\s*\(/i.test(P)&&(d=!0)}}const c=y=>i.includes(y);i.some(y=>y.startsWith("@angular/"))?s="angular":c("react")?s="react":c("vue")?s="vue":c("svelte")?s="svelte":c("next")?s="nextjs":c("nuxt")?s="nuxt":c("express")?s="express":c("@nestjs/core")?s="nestjs":c("fastify")?s="fastify":c("flask")?s="flask":c("django")?s="django":c("spring-boot")?s="spring":m?s="blazor":d?s="minimalapi":(a||n.cs>0)&&(s="aspnet");let f="fullstack";const C=["src","frontend","app"].some(y=>j.existsSync(A.join(e,y))),S=["server","backend","api"].some(y=>j.existsSync(A.join(e,y)));return["react","angular","vue","svelte","nextjs","nuxt"].includes(s)&&(f="frontend"),["express","nestjs","fastify","flask","django","spring","aspnet","minimalapi"].includes(s)&&(f="backend"),C&&S&&(f="fullstack"),!C&&!S&&(f="library"),s==="blazor"&&(f="frontend"),{language:o.language||p,framework:o.framework||s,projectType:o.projectType||f,detected:{language:p,framework:s,projectType:f}}}function Q(e,r){const i=[],o=new Set,n=s=>{let a=String(s||"").trim();return a?(a=a.replace(/https?:\/\/[^/]+/gi,""),a=a.replace(/\$\{[^}]+\}/g,"{var}"),a=a.replace(/\{[A-Za-z_][A-Za-z0-9_]*\}/g,"{var}"),a=a.replace(/:[A-Za-z_][A-Za-z0-9_]*/g,"{var}"),a=a.replace(/\/\d+(?=\/|$)/g,"/{id}"),a=a.replace(/=[^&\s]+/g,"={value}"),a=a.replace(/\/+/g,"/"),a):""},t=s=>{const a=n(s.endpointPattern);if(!a)return;const d=`${s.method}|${a}|${s.sourceFile}|${s.style}`;o.has(d)||(o.add(d),i.push({...s,endpointPattern:a}))};for(const s of r){if(!/\.(ts|tsx|js|jsx|mjs|cjs|cs)$/i.test(s))continue;const a=A.relative(e,s),d=k(s);if(!(/service|api|client|controller|program\.cs/i.test(a)||/HttpClient|fetch\(|app\.Map(Get|Post|Put|Delete|Patch)\(/i.test(d)))continue;const c=d.replace(/\r\n/g,`
2
+ `),f={},C=u=>{let l=String(u||"").trim();if(!l)return"";for(l=l.replace(/;+$/,"").trim(),l=l.replace(/\(\s*$/,"");l.startsWith("'")&&l.endsWith("'")||l.startsWith('"')&&l.endsWith('"')||l.startsWith("`")&&l.endsWith("`");)l=l.slice(1,-1).trim();return l},S=u=>{const l=String(u||"").trim();return l?!!(/^https?:\/\//i.test(l)||l.startsWith("/")||/\bapi\b/i.test(l)||/\$\{[^}]+\}/.test(l)||/\?[^=\s]+=?/.test(l)||/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_./${}-]+$/.test(l)):!1},y=(u,l)=>{!u||!l||(f[u]=C(l))},b=/(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;for(const u of c.matchAll(b))y(u[1],u[2]);const P=/(?:public|private|protected)?\s*(?:readonly\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*?);/g;for(const u of c.matchAll(P))y(u[1],u[2]);const F=u=>{const l=C(u);if(!l)return"";if(/^['"`][\s\S]*['"`]$/.test(l))return l.replace(/^['"`]|['"`]$/g,"");if(f[l]&&S(f[l]))return f[l];const h=l.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);if(h&&f[h[1]]&&S(f[h[1]]))return f[h[1]];const w=l.split("+").map(g=>g.trim()).filter(Boolean);if(w.length>1){const g=w.map(x=>{if(/^['"`][\s\S]*['"`]$/.test(x))return x.replace(/^['"`]|['"`]$/g,"");if(f[x]&&S(f[x]))return f[x];const v=x.match(/^this\.([A-Za-z_][A-Za-z0-9_]*)$/);return v&&f[v[1]]&&S(f[v[1]])?f[v[1]]:`{${x}}`}).join("");if(g)return g}const $=l.match(/^(.+?)\?(.+?):(.+)$/s);if($){const g=F($[2]),x=F($[3]);if(g||x)return`${g||"{optionA}"} | ${x||"{optionB}"}`}return l},T=/\.\s*(get|post|put|patch|delete)\s*(?:<[\s\S]*?>)?\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of c.matchAll(T)){const l=u[1].toUpperCase(),h=F(u[2]);!h||!S(h)||t({method:l,endpointPattern:h,style:"httpClient",sourceFile:a})}const z=/\bfetch\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of c.matchAll(z)){const l=F(u[1]);if(!l||!S(l))continue;const h=u.index||0,w=c.slice(h,h+260),g=(/method\s*:\s*["'](GET|POST|PUT|PATCH|DELETE)["']/i.exec(w)?.[1]||"GET").toUpperCase();t({method:g,endpointPattern:l,style:"fetch",sourceFile:a})}const I=/\baxios\.(get|post|put|patch|delete)\s*\(\s*([\s\S]*?)(?:,|\))/gi;for(const u of c.matchAll(I)){const l=u[1].toUpperCase(),h=F(u[2]);!h||!S(h)||t({method:l,endpointPattern:h,style:"axios",sourceFile:a})}const Z=/\baxios\s*\(\s*\{([\s\S]*?)\}\s*\)/gi;for(const u of c.matchAll(Z)){const l=u[1],h=/\bmethod\s*:\s*["']?(get|post|put|patch|delete)["']?/i.exec(l),w=/\burl\s*:\s*([^,\n]+)/i.exec(l),$=(h?.[1]||"get").toUpperCase(),g=F(w?.[1]||"");!g||!S(g)||t({method:$,endpointPattern:g,style:"axios-config",sourceFile:a})}const E=/\.\s*request\s*\(\s*["'](GET|POST|PUT|PATCH|DELETE)["']\s*,\s*([\s\S]*?)(?:,|\))/gi;for(const u of c.matchAll(E)){const l=u[1].toUpperCase(),h=F(u[2]);!h||!S(h)||t({method:l,endpointPattern:h,style:"request",sourceFile:a})}if(/\.cs$/i.test(s)){const u=/\bapp\.Map(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]+)"/gi;for(const g of c.matchAll(u))t({method:g[1].toUpperCase(),endpointPattern:g[2],style:"csharp-map",sourceFile:a});const l=/\[Route\("([^"]+)"\)\][\s\S]*?class\s+\w+/i.exec(c),h=l?l[1]:"",w=/\[(HttpGet|HttpPost|HttpPut|HttpDelete|HttpPatch)(?:\("([^"]*)"\))?\]/gi;for(const g of c.matchAll(w)){const x=g[1].replace("Http","").toUpperCase(),v=g[2]||"",L=[h,v].filter(Boolean).join("/").replace(/\/+/g,"/").replace(/\[controller\]/gi,"{controller}");t({method:x,endpointPattern:L||h||"{controller-route}",style:"csharp-controller",sourceFile:a})}const $=/\b(GetAsync|PostAsync|PutAsync|DeleteAsync|SendAsync)\s*\(\s*"([^"]+)"/gi;for(const g of c.matchAll($)){const x=g[1].replace("Async","").replace("Send","SEND").toUpperCase();t({method:x,endpointPattern:g[2],style:"csharp-httpclient",sourceFile:a})}}}const p=i.reduce((s,a)=>(s[a.method]=(s[a.method]||0)+1,s),{});return{totalCalls:i.length,byMethod:p,calls:i.slice(0,80)}}function X(e,r={}){const i=G(e),o=new Map,n=(c,f)=>{o.has(c.id)||o.set(c.id,{id:c.id,title:c.title,reason:"Detected from code signals",sourceFiles:new Set}),o.get(c.id).sourceFiles.add(A.relative(e,f))};for(const c of i){const f=k(c);for(const C of W)C.regex.test(f)&&n(C,c)}const t=A.join(e,"package.json");if(j.existsSync(t)){const c=JSON.parse(k(t)||"{}"),f=typeof c.name=="string"?c.name:A.basename(e);H(f)&&!o.size&&(o.set("ReadItems",{id:"ReadItems",title:"Read Items",reason:`Fallback default for ${f}`,sourceFiles:new Set}),o.set("CreateItem",{id:"CreateItem",title:"Create Item",reason:`Fallback default for ${f}`,sourceFiles:new Set}))}o.size||(o.set("CreateItem",{id:"CreateItem",title:"Create Item",reason:"Fallback default",sourceFiles:new Set}),o.set("ReadItems",{id:"ReadItems",title:"Read Items",reason:"Fallback default",sourceFiles:new Set}));const p=V(e),s=K(e,i,p,r);let a=[];try{if(s.framework==="angular"){const c=U(e,i);for(const f of c.capabilities)o.has(f.id)||o.set(f.id,{...f,sourceFiles:new Set(f.sourceFiles)})}else if(s.framework==="react"||s.framework==="nextjs"){const c=M(e,i);for(const f of c.capabilities)o.has(f.id)||o.set(f.id,{...f,sourceFiles:new Set(f.sourceFiles)})}}catch{}let d={designTokens:[],colorTokens:[],spacingTokens:[],componentClasses:[],themeVars:[]};try{d=O(e,i)}catch{}return{capabilities:Array.from(o.values()).map(c=>({...c,sourceFiles:Array.from(c.sourceFiles||[])})),components:N(i,e),displayFields:B(i),externalLibraries:p,uiLayout:J(i),styling:{...q(e,i,p),designTokens:d.designTokens.length>0?d.designTokens:void 0,colorTokens:d.colorTokens,spacingTokens:d.spacingTokens,componentClasses:d.componentClasses,themeVars:d.themeVars},developmentProfile:s,apiCalls:Q(e,i)}}async function re(e,r=!1){if(r)return e;const i=D.createInterface({input:process.stdin,output:process.stdout}),o=e.map(p=>p.id).join(", "),n=await new Promise(p=>i.question(` Inferred capabilities (${o}). Press Enter to accept or type comma list: `,p));i.close();const t=String(n).trim();return t?t.split(",").map(p=>p.trim()).filter(Boolean).map(p=>({id:p,title:_(p),reason:"User provided during adopt review"})):e}function ae(e){if(!e.length)return"No capabilities inferred.";const r=Y(e),i=r.reduce((t,p)=>t+p.signalCount,0),o={high:r.filter(t=>t.confidence==="high").length,medium:r.filter(t=>t.confidence==="medium").length,low:r.filter(t=>t.confidence==="low").length},n=[];n.push("Adoption Analysis"),n.push("=".repeat(56)),n.push(`Capabilities detected : ${r.length}`),n.push(`Signal hits total : ${i}`),n.push(`Confidence mix : high=${o.high}, medium=${o.medium}, low=${o.low}`),n.push("-".repeat(56)),n.push("Capability Breakdown"),n.push("-".repeat(56)),n.push("Confidence Signals Capability"),n.push("-".repeat(56));for(const t of r){const p=t.confidence.toUpperCase().padEnd(10," "),s=String(t.signalCount).padEnd(7," ");if(n.push(`${p} ${s} ${t.id} (${t.title})`),t.signalCount>0){const a=t.sourceFiles.slice(0,3).join(", ");n.push(` sources: ${a}`),t.sourceFiles.length>3&&n.push(` more : +${t.sourceFiles.length-3} additional files`)}else n.push(" sources: inferred fallback (no strong code signal)")}return n.push("=".repeat(56)),n.join(`
3
+ `)}function ce(e){const r=(i,o,n=10)=>{const t=[`${i} (${o.length})`];if(t.push("-".repeat(56)),!o.length)return t.push(" - none"),t.join(`
4
+ `);for(const p of o.slice(0,n))t.push(` - ${p}`);return o.length>n&&t.push(` - ... +${o.length-n} more`),t.join(`
5
+ `)};return["Project Structure Signals","=".repeat(56),r("Components",e.components||[]),r("Display fields",e.displayFields||[]),r("External libraries",e.externalLibraries||[]),"UI layout","-".repeat(56),` - layout type: ${e.uiLayout?.layoutType||"unknown"}`,` - uses grid : ${e.uiLayout?.usesGrid?"yes":"no"}`,` - uses flex : ${e.uiLayout?.usesFlex?"yes":"no"}`,` - sections : ${(e.uiLayout?.sections||[]).slice(0,10).join(", ")||"none"}`,"Styling","-".repeat(56),` - frameworks : ${(e.styling?.cssFrameworks||[]).join(", ")||"none detected"}`,` - style files: ${e.styling?.styleFileCount??0}`,` - tokens : ${(e.styling?.designTokens||[]).slice(0,8).join(", ")||"none detected"}`,"Development profile","-".repeat(56),` - language : ${e.developmentProfile?.language||"unknown"} (auto: ${e.developmentProfile?.detected?.language||"unknown"})`,` - framework : ${e.developmentProfile?.framework||"unknown"} (auto: ${e.developmentProfile?.detected?.framework||"unknown"})`,` - project type: ${e.developmentProfile?.projectType||"unknown"} (auto: ${e.developmentProfile?.detected?.projectType||"unknown"})`,"API calls","-".repeat(56),` - total calls : ${e.apiCalls?.totalCalls??0}`,` - by method : ${Object.entries(e.apiCalls?.byMethod||{}).map(([i,o])=>`${i}:${o}`).join(", ")||"none"}`,...(e.apiCalls?.calls||[]).slice(0,6).map(i=>` - ${i.method} ${i.endpointPattern} [${i.style}] (${i.sourceFile})`),...(e.apiCalls?.calls||[]).length>6?[` - ... +${(e.apiCalls?.calls||[]).length-6} more`]:[],"=".repeat(56)].join(`
6
+ `)}function Y(e){return e.map(r=>{const i=r.sourceFiles?.length||0,o=i>=3?"high":i>=1?"medium":"low";return{id:r.id,title:r.title,reason:r.reason,confidence:o,sourceFiles:r.sourceFiles||[],signalCount:i}})}function ee(e){if(!e)return null;const r=[...e.components||[]].filter(Boolean).slice(0,40),i=(e.styling?.designTokens||[]).slice(0,20),o=e.uiLayout?.layoutType||"unknown",n=(e.uiLayout?.sections||[]).slice(0,12),t=e.styling?.cssFrameworks||[],p=(e.styling?.colorTokens||[]).slice(0,10),s=(e.styling?.themeVars||[]).slice(0,10);return{components:r,designTokens:i,colorTokens:p,themeVars:s,cssFrameworks:t,layout:o,sections:n,lastScanned:new Date().toISOString()}}function le(e,r){return r?e?{...r,components:[...new Set([...r.components||[],...e.components||[]])].slice(0,50),designTokens:[...new Set([...r.designTokens||[],...e.designTokens||[]])].slice(0,30)}:r:e}function pe(e,r,i,o=null){const n=i.map(m=>m.id),t=ee(o),p={policyId:r,policyVersion:1,capabilities:n,rules:{docsRequiredOnCapabilityChange:!0,requireScenarioForEachCapability:!0,requireChangelogOnCapabilityChange:!0},...t?{ui:t}:{}};j.mkdirSync(A.join(e,"scenarios"),{recursive:!0}),j.writeFileSync(A.join(e,"contract.json"),JSON.stringify(p,null,2)+`
7
+ `);const s={schemaVersion:1,capabilities:i.map(m=>({id:m.id,title:m.title||_(m.id),since:"0.1.0"}))};j.writeFileSync(A.join(e,"capabilities.json"),JSON.stringify(s,null,2)+`
8
+ `);const a={scenarioId:"adoption_baseline",description:"Baseline inferred from existing codebase during adoption",capabilitiesCovered:n,steps:n.map(m=>({action:m,expect:`${m} behavior exists in the current project`}))};if(j.writeFileSync(A.join(e,"scenarios","adoption_baseline.json"),JSON.stringify(a,null,2)+`
9
+ `),o){const m={profileId:"adoption_profile",generatedAt:new Date().toISOString(),components:o.components||[],displayFields:o.displayFields||[],externalLibraries:o.externalLibraries||[],uiLayout:o.uiLayout||{layoutType:"unknown",usesGrid:!1,usesFlex:!1,sections:[]},styling:o.styling||{cssFrameworks:[],styleFileCount:0,styleFilesSample:[],designTokens:[]},developmentProfile:o.developmentProfile||{language:"unknown",framework:"unknown",projectType:"unknown",detected:{language:"unknown",framework:"unknown",projectType:"unknown"}},apiCalls:o.apiCalls||{totalCalls:0,byMethod:{},calls:[]}};j.writeFileSync(A.join(e,"adoption_profile.json"),JSON.stringify(m,null,2)+`
10
+ `);try{R(e,o,i)}catch{}}const d=`# Changelog \u2014 ${r}
11
11
 
12
12
  ## Unreleased
13
13
 
@@ -17,4 +17,4 @@ import*as C from"node:fs";import*as b from"node:path";import*as D from"node:read
17
17
  ## 0.1.0 \u2014 Adoption baseline
18
18
 
19
19
  - Initial baseline generated by infernoflow init --adopt
20
- `;C.writeFileSync(b.join(t,"CHANGELOG.md"),o,"utf8")}export{X as buildAdoptionReport,Y as buildSignalsReport,K as discoverCapabilities,J as discoverProjectSignals,Q as reviewCapabilitiesInteractive,V as summarizeCapabilities,ee as writeAdoptionBaseline};
20
+ `;j.writeFileSync(A.join(e,"CHANGELOG.md"),d,"utf8")}export{ae as buildAdoptionReport,ce as buildSignalsReport,ee as buildUiContractSection,ie as discoverCapabilities,X as discoverProjectSignals,le as mergeUiSection,re as reviewCapabilitiesInteractive,Y as summarizeCapabilities,pe as writeAdoptionBaseline};
@@ -1,20 +1,27 @@
1
- import l from"node:fs";import d from"node:path";import{execSync as w}from"node:child_process";import{bold as f,gray as S,cyan as r,red as P,green as a,yellow as I}from"../ui/output.mjs";import{buildCursorImplementPrompt as q,buildGenericImplementPrompt as z}from"../ui/prompts.mjs";function K(t){try{const o=process.platform;if(o==="win32")w("clip",{input:t});else if(o==="darwin")w("pbcopy",{input:t});else try{w("xclip -selection clipboard",{input:t})}catch{w("xsel --clipboard --input",{input:t})}return!0}catch{return!1}}const g="inferno",D=d.join(g,"CONTEXT.md"),T=d.join(g,"context-state.json");function L(t){try{return JSON.parse(l.readFileSync(t,"utf8"))}catch{return null}}function v(t){try{return l.readFileSync(t,"utf8")}catch{return null}}function Q(){const t=v(T);if(!t)return{};try{return JSON.parse(t)}catch{return{}}}function Y(t){l.writeFileSync(T,JSON.stringify(t,null,2),"utf8")}function _(t){return t?new Date(t).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}):"unknown"}function Z(t,o){if(!t)return[];const i=[];let s=null;for(const c of t.split(`
2
- `))if(c.startsWith("## ")){if(s&&i.length<o&&i.push(s),i.length>=o)break;s={title:c.replace("## ","").trim(),items:[]}}else s&&c.startsWith("- ")&&s.items.push(c.replace("- ","").trim());return s&&i.length<o&&i.push(s),i.filter(c=>c.items.length>0)}async function cn(t){const o=e=>t.includes(e),i=e=>{const u=t.indexOf(e);return u!==-1&&t[u+1]?t[u+1]:null},s=i("--intent")||i("-i"),c=i("--working")||i("-w"),m=i("--decision")||i("-d"),E=o("--show")||o("-s"),x=o("--copy")||o("-c"),W=o("--cursor"),G=o("--copilot"),R=o("--reset");console.log(`
3
- `+f("\uFFFD\uFFFD\uFFFD infernoflow \u2014 context")),console.log(" "+"\u2500".repeat(50)+`
4
- `),l.existsSync(g)||(console.error(P(" \u2718 inferno/ not found")),console.error(S(` \u2192 Run: infernoflow init
5
- `)),process.exit(1));const p=L(d.join(g,"contract.json")),b=L(d.join(g,"capabilities.json")),A=v(d.join(g,"CHANGELOG.md"));(!p||!b)&&(console.error(P(` \u2718 Missing contract.json or capabilities.json
6
- `)),process.exit(1));let n=Q();R&&(n={},console.log(I(` \u26A0 State reset
7
- `))),s&&(n.intent=s,n.intentUpdated=new Date().toISOString(),console.log(a(' \u2714 Intent saved: "'+s+'"'))),c&&(n.working=c,n.workingUpdated=new Date().toISOString(),console.log(a(' \u2714 Working on: "'+c+'"'))),m&&(n.decisions||(n.decisions=[]),n.decisions.push({text:m,date:new Date().toISOString()}),console.log(a(' \u2714 Decision recorded: "'+m+'"'))),(s||c||m)&&Y(n);const h=b.capabilities||[],j=h.length===(p.capabilities||[]).length,O=Z(A,3),C=String(p.policyVersion).replace(/^v/i,""),U=new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}),N=j?"\u2713 validated":"\u26A0 out of sync",k=n.intent||"describe the exact task to implement",F={task:k,contract:p,caps:b,scenarios:[],state:n},J=q(F),V=z(F),X=h.map(e=>"- **"+e.id+"** \u2014 "+e.title).join(`
8
- `),B=O.length>0?O.map(e=>"### "+e.title+`
9
- `+e.items.map(u=>" - "+u).join(`
1
+ import l from"node:fs";import h from"node:path";import{execSync as b}from"node:child_process";import{bold as m,gray as d,cyan as r,red as G,green as a,yellow as _}from"../ui/output.mjs";import{buildCursorImplementPrompt as ct,buildGenericImplementPrompt as rt}from"../ui/prompts.mjs";import{detectDrift as lt}from"../git/detect-drift.mjs";function at(n){try{const o=process.platform;if(o==="win32")b("clip",{input:n});else if(o==="darwin")b("pbcopy",{input:n});else try{b("xclip -selection clipboard",{input:n})}catch{b("xsel --clipboard --input",{input:n})}return!0}catch{return!1}}const u="inferno",R=h.join(u,"CONTEXT.md"),A=h.join(u,"context-state.json");function U(n){try{return JSON.parse(l.readFileSync(n,"utf8"))}catch{return null}}function $(n){try{return l.readFileSync(n,"utf8")}catch{return null}}function J(){const n=$(A);if(!n)return{};try{return JSON.parse(n)}catch{return{}}}function V(n){l.writeFileSync(A,JSON.stringify(n,null,2),"utf8")}function O(n){return n?new Date(n).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}):"unknown"}function pt(n,o){if(!n)return[];const i=[];let s=null;for(const c of n.split(`
2
+ `))if(c.startsWith("## ")){if(s&&i.length<o&&i.push(s),i.length>=o)break;s={title:c.replace("## ","").trim(),items:[]}}else s&&c.startsWith("- ")&&s.items.push(c.replace("- ","").trim());return s&&i.length<o&&i.push(s),i.filter(c=>c.items.length>0)}async function gt(n){const o=e=>n.includes(e),i=e=>{const p=n.indexOf(e);return p!==-1&&n[p+1]?n[p+1]:null},s=i("--intent")||i("-i"),c=i("--working")||i("-w"),w=i("--decision")||i("-d"),X=o("--show")||o("-s"),N=o("--copy")||o("-c"),B=o("--cursor"),H=o("--copilot"),K=o("--reset"),M=o("--watch"),v=parseInt(i("--interval")||"30",10)*1e3;console.log(`
3
+ `+m("\uFFFD\uFFFD\uFFFD infernoflow \u2014 context")),console.log(" "+"\u2500".repeat(50)+`
4
+ `),l.existsSync(u)||(console.error(G(" \u2718 inferno/ not found")),console.error(d(` \u2192 Run: infernoflow init
5
+ `)),process.exit(1));const f=U(h.join(u,"contract.json")),k=U(h.join(u,"capabilities.json")),q=$(h.join(u,"CHANGELOG.md"));(!f||!k)&&(console.error(G(` \u2718 Missing contract.json or capabilities.json
6
+ `)),process.exit(1));let t=J();K&&(t={},console.log(_(` \u26A0 State reset
7
+ `))),s&&(t.intent=s,t.intentUpdated=new Date().toISOString(),console.log(a(' \u2714 Intent saved: "'+s+'"'))),c&&(t.working=c,t.workingUpdated=new Date().toISOString(),console.log(a(' \u2714 Working on: "'+c+'"'))),w&&(t.decisions||(t.decisions=[]),t.decisions.push({text:w,date:new Date().toISOString()}),console.log(a(' \u2714 Decision recorded: "'+w+'"'))),(s||c||w)&&V(t);const y=k.capabilities||[],P=y.length===(f.capabilities||[]).length,D=pt(q,3),I=String(f.policyVersion).replace(/^v/i,""),z=new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}),T=P?"\u2713 validated":"\u26A0 out of sync",F=t.intent||"describe the exact task to implement",W={task:F,contract:f,caps:k,scenarios:[],state:t},Q=ct(W),Y=rt(W),Z=y.map(e=>"- **"+e.id+"** \u2014 "+e.title).join(`
8
+ `),tt=D.length>0?D.map(e=>"### "+e.title+`
9
+ `+e.items.map(p=>" - "+p).join(`
10
10
  `)).join(`
11
11
 
12
- `):"_No recent changes_",$=n.intent?n.intent+" _("+_(n.intentUpdated)+")_":'_Not set \u2014 run: infernoflow context --intent "..."_',H=n.working?n.working+" _("+_(n.workingUpdated)+")_":'_Not set \u2014 run: infernoflow context --working "..."_',M=n.decisions&&n.decisions.length>0?n.decisions.slice(-5).map(e=>"- "+e.text+" _("+_(e.date)+")_").join(`
13
- `):"_No decisions recorded_",y=["# Project Context \u2014 "+p.policyId+" v"+C,"> Generated by infernoflow | "+U+" | "+N,"","---","","## What this system does","",X,"","---","","## Recent changes","",B,"","---","","## Current state","","- **Capabilities:** "+h.length,"- **Version:** v"+C,"- **Sync:** "+N,"","---","","## What I am working on right now","",H,"","---","","## Intent \u2014 what I want to build next","",$,"","---","","## Decisions & notes","",M,"","---","","## Implementation Prompt Seed","","Use this to start coding immediately with an agent:","","```bash",`infernoflow implement "${k}" --mode both`,"```","","### Cursor Agent Prompt","","```text",J,"```","","### Generic Agent Prompt","","```text",V,"```","","---","_Paste this block at the start of any new AI session._"].join(`
14
- `);if(E||(l.writeFileSync(D,y,"utf8"),console.log(a(`
15
- \u2714 Context written \u2192 `+D))),x){const e=K(y);console.log(e?a(" \u2714 Copied to clipboard \u2014 paste with Ctrl+V"):I(" \u26A0 Clipboard copy failed \u2014 open inferno/CONTEXT.md manually"))}W&&(l.writeFileSync(".cursorrules",y,"utf8"),console.log(a(" \u2714 Written to .cursorrules \u2014 Cursor loads this automatically"))),G&&(l.existsSync(".github")||l.mkdirSync(".github"),l.writeFileSync(".github/copilot-instructions.md",y,"utf8"),console.log(a(" \u2714 Written to .github/copilot-instructions.md \u2014 Copilot loads this automatically"))),console.log(`
16
- `+f("Context Summary")),console.log(" "+"\u2500".repeat(50)),console.log(" Project "+p.policyId+" \u2014 v"+C),console.log(" Capabilities "+h.length+" registered"),console.log(" Sync "+(j?a("\u2713 in sync"):I("\u26A0 check needed"))),console.log(" Working on "+(n.working?r(n.working):S("not set"))),console.log(" Intent "+(n.intent?r(n.intent):S("not set"))),console.log(" Decisions "+(n.decisions?n.decisions.length:0)+` recorded
17
- `),console.log(" "+f("Implementation Prompt")),console.log(" "+r("\u2192")+" Run "+r(`infernoflow implement "${k}" --mode both`)+`
18
- `),x?(console.log(" "+f("Ready to use:")),console.log(" "+r("\u2192")+" Paste into Claude / Cursor / Copilot with "+r("Ctrl+V")+`
19
- `)):(console.log(" "+f("Ready to use:")),console.log(" "+r("1.")+" Open "+r("inferno/CONTEXT.md")),console.log(" "+r("2.")+" Copy everything"),console.log(" "+r("3.")+" Paste at the start of your next AI session"),console.log(" "+S(" tip: use --copy to skip steps 1-2 automatically")+`
20
- `))}export{cn as contextCommand};
12
+ `):"_No recent changes_",nt=t.intent?t.intent+" _("+O(t.intentUpdated)+")_":'_Not set \u2014 run: infernoflow context --intent "..."_',et=t.working?t.working+" _("+O(t.workingUpdated)+")_":'_Not set \u2014 run: infernoflow context --working "..."_',ot=t.decisions&&t.decisions.length>0?t.decisions.slice(-5).map(e=>"- "+e.text+" _("+O(e.date)+")_").join(`
13
+ `):"_No decisions recorded_",S=["# Project Context \u2014 "+f.policyId+" v"+I,"> Generated by infernoflow | "+z+" | "+T,"","---","","## What this system does","",Z,"","---","","## Recent changes","",tt,"","---","","## Current state","","- **Capabilities:** "+y.length,"- **Version:** v"+I,"- **Sync:** "+T,"","---","","## What I am working on right now","",et,"","---","","## Intent \u2014 what I want to build next","",nt,"","---","","## Decisions & notes","",ot,"","---","","## Implementation Prompt Seed","","Use this to start coding immediately with an agent:","","```bash",`infernoflow implement "${F}" --mode both`,"```","","### Cursor Agent Prompt","","```text",Q,"```","","### Generic Agent Prompt","","```text",Y,"```","","---","_Paste this block at the start of any new AI session._"].join(`
14
+ `);if(X||(l.writeFileSync(R,S,"utf8"),console.log(a(`
15
+ \u2714 Context written \u2192 `+R))),N){const e=at(S);console.log(e?a(" \u2714 Copied to clipboard \u2014 paste with Ctrl+V"):_(" \u26A0 Clipboard copy failed \u2014 open inferno/CONTEXT.md manually"))}if(B&&(l.writeFileSync(".cursorrules",S,"utf8"),console.log(a(" \u2714 Written to .cursorrules \u2014 Cursor loads this automatically"))),H&&(l.existsSync(".github")||l.mkdirSync(".github"),l.writeFileSync(".github/copilot-instructions.md",S,"utf8"),console.log(a(" \u2714 Written to .github/copilot-instructions.md \u2014 Copilot loads this automatically"))),console.log(`
16
+ `+m("Context Summary")),console.log(" "+"\u2500".repeat(50)),console.log(" Project "+f.policyId+" \u2014 v"+I),console.log(" Capabilities "+y.length+" registered"),console.log(" Sync "+(P?a("\u2713 in sync"):_("\u26A0 check needed"))),console.log(" Working on "+(t.working?r(t.working):d("not set"))),console.log(" Intent "+(t.intent?r(t.intent):d("not set"))),console.log(" Decisions "+(t.decisions?t.decisions.length:0)+` recorded
17
+ `),console.log(" "+m("Implementation Prompt")),console.log(" "+r("\u2192")+" Run "+r(`infernoflow implement "${F}" --mode both`)+`
18
+ `),N?(console.log(" "+m("Ready to use:")),console.log(" "+r("\u2192")+" Paste into Claude / Cursor / Copilot with "+r("Ctrl+V")+`
19
+ `)):(console.log(" "+m("Ready to use:")),console.log(" "+r("1.")+" Open "+r("inferno/CONTEXT.md")),console.log(" "+r("2.")+" Copy everything"),console.log(" "+r("3.")+" Paste at the start of your next AI session"),console.log(" "+d(" tip: use --copy to skip steps 1-2 automatically")+`
20
+ `)),M){console.log(" "+r("\u{1F441} Watch mode active")+d(" \u2014 polling git diff every "+v/1e3+"s")),console.log(" "+d(`Press Ctrl+C to stop
21
+ `));let e="";const p=async()=>{try{const st=process.cwd(),g=lt(st,{sinceCommits:1}),L=g.changedFiles.sort().join("|");if(L===e||(e=L,g.changedFiles.length===0))return;const j=g.affectedCapabilities.map(x=>x.id),E=j.length>0?`Working on: ${j.join(", ")} (${g.changedFiles.length} files changed)`:`${g.changedFiles.length} files changed \u2014 no capability match yet`,C=J();C.working!==E&&(C.working=E,C.workingUpdated=new Date().toISOString(),V(C),await gt(n.filter(x=>x!=="--watch")),process.stderr.write(`
22
+ `+a("\u2714")+" Context updated \u2014 "+j.length+` capabilities affected
23
+ `+d(g.changedFiles.slice(0,3).join(", ")+(g.changedFiles.length>3?` +${g.changedFiles.length-3} more`:""))+`
24
+ `))}catch{}};await p();const it=setInterval(p,v);process.on("SIGINT",()=>{clearInterval(it),process.stderr.write(`
25
+ `+d(`Watch stopped.
26
+
27
+ `)),process.exit(0)}),await new Promise(()=>{})}}export{gt as contextCommand};
@@ -0,0 +1,114 @@
1
+ import*as l from"node:fs";import*as s from"node:path";import{readProfile as k}from"../learning/profile.mjs";import{header as y,ok as w,warn as b,done as $,nextSteps as C,cyan as m,yellow as j}from"../ui/output.mjs";const p=s.join("inferno","generated-skills");function f(e,n,i){return l.existsSync(e)&&!i?(b(`Already exists (use --force to overwrite): ${s.relative(process.cwd(),e)}`),!1):(l.mkdirSync(s.dirname(e),{recursive:!0}),l.writeFileSync(e,n,"utf8"),w(`Generated: ${s.relative(process.cwd(),e)}`),!0)}function S(e,n){const i=e.namingStyle!=="unknown"?e.namingStyle:"PascalCase",r=e.preferredVerbs.length>0?e.preferredVerbs.slice(0,5).join(", "):"Add, Update, Remove",t=e.stack?.framework||"unknown",o=e.featureClusters.slice(0,3),a=e.sessionCount>=20?"experienced":e.sessionCount>=5?"intermediate":"new",c=o.map(u=>`- When touching [${u.slice(0,3).join(", ")}], check whether related capabilities also need updating`).join(`
2
+ `);return`# infernoflow \u2014 Cursor Rules (auto-generated)
3
+ # Project: ${n?.policyId||"unknown"} | Stack: ${t}
4
+ # Regenerate with: infernoflow generate-skills --cursor
5
+
6
+ ## Contract awareness
7
+ - This project uses infernoflow to track capability contracts
8
+ - After implementing any feature, always call \`infernoflow_run\` then \`infernoflow_apply\`
9
+ - Run \`infernoflow_check\` before every commit
10
+ - Current capabilities: [${(n?.capabilities||[]).join(", ")}]
11
+
12
+ ## Naming conventions (detected from this developer's history)
13
+ - Capability IDs use **${i}** (e.g. ${r.split(", ").slice(0,2).map(u=>u+"Item").join(", ")})
14
+ - Preferred action verbs: ${r}
15
+ - Match this style when suggesting new capability names
16
+
17
+ ## Feature clusters (capabilities this developer adds together)
18
+ ${c||"- No clusters detected yet \u2014 build more features to train this"}
19
+
20
+ ## Session style (${a})
21
+ ${a==="experienced"?`- Skip basic explanations \u2014 this developer knows the codebase well
22
+ - Be direct and minimal in responses`:`- Include brief context for non-obvious decisions
23
+ - Explain infernoflow commands when used`}
24
+
25
+ ## Workflow reminders
26
+ - Start sessions with: \`infernoflow context --show\`
27
+ - Use \`infernoflow_git_drift\` to check what's changed before starting work
28
+ - Use \`infernoflow_implement\` to get a structured coding prompt before writing code
29
+ - Changelog entries should be ${e.changelogVerbosity==="detailed"?"detailed (include context and impact)":"brief (one line, action-focused)"}
30
+ `}function R(e,n){const r=e.stack?.framework||"unknown",t=(n?.capabilities||[]).slice(0,6);return`# Quick Restore \u2014 ${n?.policyId||"this project"}
31
+ # Paste this at the start of any new AI session to restore context instantly.
32
+ # Regenerate with: infernoflow generate-skills
33
+
34
+ ## Project snapshot
35
+ - **Project:** ${n?.policyId||"unknown"}
36
+ - **Stack:** ${r} / ${e.stack?.language||"unknown"} (${e.stack?.projectType||"unknown"})
37
+ - **Capabilities:** ${t.join(", ")}${(n?.capabilities||[]).length>6?` +${(n?.capabilities||[]).length-6} more`:""}
38
+
39
+ ## How to start a session
40
+ 1. Run: \`infernoflow context --show\`
41
+ 2. Check \`inferno/CONTEXT.md\` for current intent
42
+ 3. Run: \`infernoflow_git_drift\` to see what changed since last session
43
+ 4. Pick up where you left off
44
+
45
+ ## infernoflow tools available (in Cursor / VS Code Agent mode)
46
+ - \`infernoflow_run\` \u2014 generate a contract update prompt
47
+ - \`infernoflow_apply\` \u2014 apply a JSON response
48
+ - \`infernoflow_implement\` \u2014 get a structured coding prompt
49
+ - \`infernoflow_git_drift\` \u2014 see what capabilities may have drifted
50
+ - \`infernoflow_check\` \u2014 validate contract is in sync
51
+ - \`infernoflow_status\` \u2014 quick health check
52
+
53
+ ## Definition of done (every feature branch)
54
+ - [ ] Code works as intended
55
+ - [ ] \`infernoflow_run\` \u2192 \`infernoflow_apply\` completed
56
+ - [ ] \`infernoflow_check\` passes
57
+ - [ ] Commit message references the capability changed
58
+ `}function _(e){const n=e.namingStyle!=="unknown"?e.namingStyle:"PascalCase",i=e.preferredVerbs.length>0?e.preferredVerbs:["Create","Read","Update","Delete","Search"],r=i.slice(0,5).map(t=>n==="PascalCase"?` - ${t}Item, ${t}Task, ${t}User`:n==="camelCase"?` - ${t.toLowerCase()}Item, ${t.toLowerCase()}Task`:` - ${t.toLowerCase()}-item, ${t.toLowerCase()}-task`).join(`
59
+ `);return`# Naming Guide \u2014 auto-generated from your capability history
60
+
61
+ ## Detected style: ${n}
62
+
63
+ ### Your preferred action verbs
64
+ ${i.map(t=>`- **${t}**`).join(`
65
+ `)}
66
+
67
+ ### Examples matching your style
68
+ ${r}
69
+
70
+ ### Rules
71
+ - All capability IDs in \`inferno/contract.json\` must follow this style
72
+ - New capabilities suggested by AI should match \u2014 reject any that don't
73
+ - If you rename a capability, update contract.json + capabilities.json + any scenarios
74
+
75
+ ### When naming a new capability, ask:
76
+ 1. Does it describe a single, discrete behavior? (If not, split it)
77
+ 2. Does it start with one of your preferred verbs?
78
+ 3. Is it in ${n}?
79
+ 4. Is it unique \u2014 not already in contract.json?
80
+ `}function I(e,n){const r=e.featureClusters.slice(0,2)[0]||[],t=e.stack?.framework||"unknown",o=r.slice(0,4).map(a=>`- [ ] Does **${a}** need updating? (check inferno/capabilities.json)`).join(`
81
+ `);return`# Feature Scaffold \u2014 ${t} project
82
+ # Use this checklist whenever starting a new feature.
83
+ # Regenerate with: infernoflow generate-skills
84
+
85
+ ## Before you start
86
+ - [ ] Run \`infernoflow context --show\` to load current state
87
+ - [ ] Run \`infernoflow_git_drift\` to see any pending drift
88
+ - [ ] Set intent: \`infernoflow context --intent "what I'm building"\`
89
+
90
+ ## Implementation checklist
91
+ - [ ] Create feature branch
92
+ - [ ] Implement the feature
93
+ - [ ] Write / update tests
94
+ - [ ] Verify it works end-to-end
95
+
96
+ ## Capability cluster check
97
+ ${o||"- [ ] Review existing capabilities \u2014 do any need updating?"}
98
+
99
+ ## Contract update (required before merge)
100
+ - [ ] Run \`infernoflow_run\` with a description of what changed
101
+ - [ ] Review the suggested JSON
102
+ - [ ] Run \`infernoflow_apply\` with the JSON
103
+ - [ ] Run \`infernoflow_check\` \u2014 must pass
104
+
105
+ ## Commit message
106
+ - Reference the capability: "feat: add SearchItems endpoint (#42)"
107
+ - Update CHANGELOG.md if not auto-updated
108
+
109
+ ## Done when
110
+ - [ ] Feature works
111
+ - [ ] \`infernoflow_check\` passes
112
+ - [ ] PR description mentions which capabilities changed
113
+ `}async function A(e){const n=process.cwd(),i=e.includes("--force")||e.includes("-f"),r=e.includes("--cursor");y("generate-skills");const t=s.join(n,"inferno");l.existsSync(t)||(console.error(` \u2718 inferno/ not found \u2014 run: infernoflow init
114
+ `),process.exit(1));const o=k(t);let a=null;try{a=JSON.parse(l.readFileSync(s.join(t,"contract.json"),"utf8"))}catch{}const c=s.join(n,p);if(f(s.join(c,"cursor-rules.md"),S(o,a),i),f(s.join(c,"quick-restore.md"),R(o,a),i),f(s.join(c,"naming-guide.md"),_(o),i),f(s.join(c,"feature-scaffold.md"),I(o,a),i),r){const d=s.join(n,".cursor","rules");l.mkdirSync(d,{recursive:!0});const h=s.join(c,"cursor-rules.md"),g=s.join(d,"infernoflow.md");l.copyFileSync(h,g),w("Installed to: .cursor/rules/infernoflow.md")}const u=[o.namingStyle!=="unknown"?`naming: ${o.namingStyle}`:null,o.stack?.framework!=="unknown"?`stack: ${o.stack.framework}`:null,o.sessionCount>0?`sessions: ${o.sessionCount}`:null].filter(Boolean).join(" \xB7 ");$(`Skills generated${u?` (${u})`:""}`),C([`Review files in ${j(p+"/")}`,`Copy to Cursor: ${m("infernoflow generate-skills --cursor")}`,`Re-run any time to refresh after more sessions: ${m("infernoflow generate-skills --force")}`,o.sessionCount<5?`Run more commands to improve personalisation (${o.sessionCount} sessions so far)`:`Profile has ${o.sessionCount} sessions \u2014 personalisation is well-trained`])}export{A as generateSkillsCommand};
@@ -0,0 +1,21 @@
1
+ import*as c from"node:fs";import*as f from"node:path";import{execSync as b}from"node:child_process";import{fileURLToPath as F}from"node:url";import{header as L,ok as o,fail as k,warn as l,info as u,done as O,bold as S,cyan as C,gray as n,green as T}from"../ui/output.mjs";const U=f.dirname(F(import.meta.url)),g=f.resolve(U,"../..");function a(r,i={}){return b(r,{cwd:g,encoding:"utf8",stdio:i.silent?["ignore","pipe","pipe"]:["inherit","inherit","inherit"],...i})}function x(r){return b(r,{cwd:g,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}function E(r,i){const e=r.split(".").map(Number);return i==="major"?(e[0]++,e[1]=0,e[2]=0):i==="minor"?(e[1]++,e[2]=0):e[2]++,e.join(".")}function h(){return new Date().toISOString().slice(0,10)}function H(r,i){if(!c.existsSync(r))return c.writeFileSync(r,`# Changelog \u2014 infernoflow
2
+
3
+ ## ${i} \u2014 ${h()}
4
+
5
+ ### Added
6
+ - Release ${i}
7
+ `),!0;let e=c.readFileSync(r,"utf8");if(/^## Unreleased/im.test(e))return e=e.replace(/^## Unreleased.*$/im,`## ${i} \u2014 ${h()}`),c.writeFileSync(r,e),!0;const p=/^# .+$/im;return p.test(e)?(e=e.replace(p,y=>`${y}
8
+
9
+ ## ${i} \u2014 ${h()}
10
+
11
+ ### Added
12
+ - Release ${i}
13
+ `),c.writeFileSync(r,e),!0):(c.writeFileSync(r,`## ${i} \u2014 ${h()}
14
+
15
+ ### Added
16
+ - Release ${i}
17
+
18
+ ${e}`),!0)}function _(){try{return x("git status --porcelain").length>0}catch{return!1}}function J(){try{return x("git config user.email"),!0}catch{return!1}}async function M(r){const i=r.slice(1),e=i.includes("--dry-run"),p=i.includes("--skip-build"),y=i.includes("--skip-tests"),A=i.includes("--skip-push"),j=i.includes("--tag"),N=i.includes("--yes")||i.includes("-y"),G=i.indexOf("--bump"),m=G!==-1&&i[G+1]||"patch";["patch","minor","major"].includes(m)||(console.error(` Invalid --bump value: ${m}. Must be patch, minor, or major.`),process.exit(1)),L("infernoflow publish"),e&&l("DRY RUN \u2014 no files will be written, no commands executed");const v=f.join(g,"package.json"),$=JSON.parse(c.readFileSync(v,"utf8")),w=$.version,t=E(w,m);if(console.log(),console.log(` ${n("current")} ${S(w)}`),console.log(` ${n("new ")} ${S(T(t))} ${n("("+m+" bump)")}`),console.log(),!N&&!e){process.stdout.write(` Publish ${S(C("infernoflow@"+t))} to npm? [y/N] `);let s=!1;try{const d=b("bash -c 'read -r ans </dev/tty; echo $ans'",{encoding:"utf8",stdio:["inherit","pipe","inherit"]}).trim().toLowerCase();s=d==="y"||d==="yes"}catch{s=!1}console.log(),s||(console.log(n(` Aborted.
19
+ `)),process.exit(0))}u(`Bumping package.json ${n(w+" \u2192 "+t)}`),e?o(n("[dry] would write package.json")):($.version=t,c.writeFileSync(v,JSON.stringify($,null,4)+`
20
+ `),o("package.json updated"));const R=f.join(g,"CHANGELOG.md");if(u("Updating CHANGELOG.md"),e?o(n("[dry] would update CHANGELOG.md")):(H(R,t),o("CHANGELOG.md updated")),p)l("Skipping build (--skip-build)");else if(u("Running build "+n("node build.mjs")),e)o(n("[dry] would run: node build.mjs"));else try{a("node build.mjs",{silent:!1}),o("Build succeeded")}catch(s){k("Build failed",s.message),process.exit(1)}if(y)l("Skipping tests (--skip-tests)");else if(u("Running smoke tests"),e)o(n("[dry] would run: npm test"));else try{a("npm test",{silent:!1}),o("All smoke tests passed")}catch{k("Smoke tests failed","Fix tests or re-run with --skip-tests"),process.exit(1)}if(u(`Publishing to npm ${n("infernoflow@"+t)}`),e)o(n("[dry] would run: npm publish"));else try{a("npm publish",{silent:!1}),o(`Published infernoflow@${t}`)}catch(s){k("npm publish failed",s.message||"Check npm credentials"),l("Continuing to git commit despite publish failure")}if(u("Committing version bump"),e)o(n(`[dry] would commit: chore: release ${t}`));else try{a(`git add ${["package.json","CHANGELOG.md"].join(" ")}`,{silent:!1});const d=`chore: release ${t}`;a(`git commit -m "${d}"`,{silent:!1}),o(`Committed: ${n(d)}`)}catch(s){l(`Git commit failed: ${s.message}`),l('You can commit manually: git add package.json CHANGELOG.md && git commit -m "chore: release '+t+'"')}if(j)if(u(`Creating git tag ${n("v"+t)}`),e)o(n(`[dry] would tag: v${t}`));else try{a(`git tag v${t}`,{silent:!1}),o(`Tagged v${t}`)}catch(s){l(`Git tag failed: ${s.message}`)}if(A)l("Skipping push (--skip-push)");else if(u("Pushing to origin"),e)o(n("[dry] would run: git push"));else try{const s=j?`git push && git push origin v${t}`:"git push";a(s,{silent:!1}),o("Pushed to origin")}catch(s){l(`Git push failed: ${s.message}`),l("Push manually: git push")}console.log(),e?O(`Dry run complete \u2014 would have published infernoflow@${t}`):(O(`infernoflow@${t} published, committed, and pushed`),console.log(` ${C("npm:")} https://www.npmjs.com/package/infernoflow`),console.log(` ${C("git:")} ${n("chore: release "+t)}
21
+ `))}export{M as publishCommand};
@@ -0,0 +1,4 @@
1
+ import*as g from"node:fs";import*as s from"node:path";import{fileURLToPath as y}from"node:url";import{execSync as $}from"node:child_process";import{detectIdeContext as k}from"../ai/ideDetection.mjs";import{header as v,ok as i,warn as S,done as R,cyan as l,yellow as r,bold as x}from"../ui/output.mjs";import{installCursorHooksArtifacts as C}from"../cursorHooksInstall.mjs";import{installVsCodeCopilotHooksArtifacts as P}from"../vsCodeCopilotHooksInstall.mjs";const j=s.dirname(y(import.meta.url));function b(){return s.resolve(j,"../../templates")}function A(n){try{return $(`npx infernoflow ${n}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(o){return o.stdout||o.stderr||o.message}}async function T(n){const o=process.cwd(),d=n.includes("--force")||n.includes("-f"),w=n.includes("--yes")||n.includes("-y"),c=b();v("infernoflow setup");const{ideDetected:e}=k("auto"),a=e==="cursor"?"Cursor":e==="vscode"?"VS Code + Copilot":e==="windsurf"?"Windsurf":"unknown";console.log(` IDE detected: ${x(a)}`);const h=s.join(o,"inferno"),u=s.join(h,"contract.json");if(g.existsSync(u))i("inferno/contract.json already exists \u2014 skipping init");else{console.log(`
2
+ ${r("inferno/")} not found \u2014 running init --adopt ...
3
+ `);const t=["--adopt",w?"--yes":""].filter(Boolean).join(" ");A(`init ${t}`)}const p=t=>i(t),m=t=>S(t);(e==="cursor"||e==="unknown")&&(C({cwd:o,templatesRoot:c,force:d,silent:!1,logOk:p,logWarn:m}),i("Cursor hooks + MCP server installed"),console.log(` \u2192 Restart Cursor, then go to Settings \u2192 MCP and verify ${r("infernoflow")} shows 4 tools`)),e==="vscode"&&(P({cwd:o,templatesRoot:c,force:d,silent:!1,logOk:p,logWarn:m}),i("VS Code Copilot hooks installed"),console.log(` \u2192 Restart VS Code, then open GitHub Copilot Chat in ${r("Agent")} mode`),C({cwd:o,templatesRoot:c,force:!1,silent:!0,logOk:()=>{},logWarn:()=>{}}));let f=0;try{f=(JSON.parse(g.readFileSync(u,"utf8")).capabilities||[]).length}catch{}console.log(),R(f>0?`infernoflow ready \u2014 ${f} capabilities tracked, MCP server installed for ${a}`:`infernoflow ready \u2014 MCP server installed for ${a}`),console.log(`
4
+ ${l("Next steps:")}`),e==="cursor"?(console.log(" 1. Restart Cursor"),console.log(` 2. In Cursor chat, try: ${l('Use infernoflow_run with task "add a new feature"')}`)):e==="vscode"?(console.log(" 1. Restart VS Code"),console.log(` 2. Switch Copilot Chat to ${r("Agent")} mode`),console.log(` 3. Try: ${l('Use infernoflow_run with task "add a new feature"')}`)):(console.log(` 1. Open your IDE and install the MCP server from ${r("inferno-mcp-server.mjs")}`),console.log(` 2. Run: ${l("infernoflow status")} to verify everything is working`)),console.log()}export{T as setupCommand};
@@ -1,6 +1,6 @@
1
- import*as c from"node:fs";import*as l from"node:path";import*as J from"node:readline";import{header as V,ok as E,warn as F,info as U,done as R,section as D,nextSteps as W,bold as H,cyan as C,gray as A,yellow as Y,green as L,red as G,errorAndExit as I}from"../ui/output.mjs";function v(n){try{return JSON.parse(c.readFileSync(n,"utf8"))}catch{return null}}function T(n,e){return new Promise(i=>{n.question(e,d=>i(d.trim()))})}function q(n){return n.replace(/[-_]+/g," ").split(" ").map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}function z({description:n,contract:e,capabilities:i,scenarios:d}){const g=e.capabilities||[],f=(i?.capabilities||[]).map(u=>` - ${u.id}: ${u.title||u.id}`).join(`
2
- `),s=d.map(u=>{const w=(u.capabilitiesCovered||[]).join(", "),y=(u.steps||[]).map(h=>` {action: "${h.action}", expect: "${h.expect}"}`).join(`
3
- `);return` File: ${u._file}
1
+ import*as c from"node:fs";import*as l from"node:path";import*as J from"node:readline";import{header as U,ok as N,warn as G,info as R,done as W,section as D,nextSteps as H,bold as Y,cyan as C,gray as A,yellow as z,green as L,red as T,errorAndExit as E}from"../ui/output.mjs";import{personalisePrompt as M}from"../learning/adapt.mjs";function $(o){try{return JSON.parse(c.readFileSync(o,"utf8"))}catch{return null}}function V(o,e){return new Promise(i=>{o.question(e,p=>i(p.trim()))})}function ie(o){return o.replace(/[-_]+/g," ").split(" ").map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}function Z({description:o,contract:e,capabilities:i,scenarios:p}){const g=e.capabilities||[],d=(i?.capabilities||[]).map(f=>` - ${f.id}: ${f.title||f.id}`).join(`
2
+ `),s=p.map(f=>{const w=(f.capabilitiesCovered||[]).join(", "),y=(f.steps||[]).map(h=>` {action: "${h.action}", expect: "${h.expect}"}`).join(`
3
+ `);return` File: ${f._file}
4
4
  capabilitiesCovered: [${w}]
5
5
  steps:
6
6
  ${y}`}).join(`
@@ -16,13 +16,13 @@ policyVersion: ${e.policyVersion}
16
16
  capabilities: [${g.join(", ")}]
17
17
 
18
18
  ## Current capabilities registry
19
- ${f||" (none)"}
19
+ ${d||" (none)"}
20
20
 
21
21
  ## Current scenarios
22
22
  ${s||" (none)"}
23
23
 
24
24
  ## Developer's description of what changed
25
- "${n}"
25
+ "${o}"
26
26
 
27
27
  ## Your task
28
28
 
@@ -52,11 +52,11 @@ Rules:
52
52
  - Capability IDs must be PascalCase (e.g. SendEmail, not send_email)
53
53
  - If nothing changed capability-wise, return empty arrays
54
54
  - changelogEntry should start with "- "
55
- - Keep it minimal and accurate`}function M(n){const e=[];if(!n||typeof n!="object")return["AI response must be a JSON object."];n.summary!=null&&typeof n.summary!="string"&&e.push('"summary" must be a string.'),Array.isArray(n.newCapabilities)||e.push('"newCapabilities" must be an array.'),Array.isArray(n.removedCapabilities)||e.push('"removedCapabilities" must be an array.'),Array.isArray(n.updatedScenarios)||e.push('"updatedScenarios" must be an array.'),n.changelogEntry!=null&&typeof n.changelogEntry!="string"&&e.push('"changelogEntry" must be a string.');for(const i of n.newCapabilities||[]){if(!i||typeof i!="object"){e.push('Each item in "newCapabilities" must be an object.');continue}(typeof i.id!="string"||!/^[A-Z][A-Za-z0-9]*$/.test(i.id))&&e.push("newCapabilities[].id must be PascalCase (example: SendEmail)."),(typeof i.title!="string"||!i.title.trim())&&e.push("newCapabilities[].title must be a non-empty string.")}for(const i of n.removedCapabilities||[])(typeof i!="string"||!i.trim())&&e.push("removedCapabilities[] must contain non-empty strings.");for(const i of n.updatedScenarios||[]){if(!i||typeof i!="object"){e.push('Each item in "updatedScenarios" must be an object.');continue}(typeof i.file!="string"||!i.file.endsWith(".json"))&&e.push("updatedScenarios[].file must be a .json filename."),typeof i.isNew!="boolean"&&e.push("updatedScenarios[].isNew must be boolean."),(!Array.isArray(i.capabilitiesCovered)||!Array.isArray(i.stepsToAdd))&&e.push("updatedScenarios[].capabilitiesCovered and stepsToAdd must be arrays.")}return e}function Z(n,e){const i=[],d=new Set(n.capabilities||[]),g=new Set((e.newCapabilities||[]).map(s=>s.id)),f=new Set(e.removedCapabilities||[]);for(const s of g)f.has(s)&&i.push(`Capability "${s}" appears in both newCapabilities and removedCapabilities.`),d.has(s)&&i.push(`Capability "${s}" already exists in contract capabilities.`);for(const s of f)d.has(s)||i.push(`Capability "${s}" cannot be removed because it does not exist in contract.`);return i}function K({cwd:n,contract:e,capabilities:i,suggestion:d,version:g,quiet:f=!1}){const s=l.join(n,"inferno"),u=l.join(s,"contract.json"),w=l.join(s,"capabilities.json"),y=l.join(s,"CHANGELOG.md"),h=l.join(s,"scenarios"),m=d.newCapabilities||[],S=d.removedCapabilities||[],k=d.updatedScenarios||[],P=d.changelogEntry||"";let $=!1;const N=[],p=(t,o)=>N.push({filePath:t,content:o});if(m.length>0||S.length>0){const t=[...e.capabilities.filter(b=>!S.includes(b)),...m.map(b=>b.id)],o=Number(e.policyVersion||1)+1,r={...e,capabilities:t,policyVersion:o};p(u,JSON.stringify(r,null,2)+`
56
- `),f||E(`contract.json updated \u2192 policyVersion: v${o}`),$=!0}if(m.length>0||S.length>0){const t=i?{...i}:{schemaVersion:1,capabilities:[]};t.capabilities=(t.capabilities||[]).filter(o=>!S.includes(o.id));for(const o of m)t.capabilities.find(r=>r.id===o.id)||t.capabilities.push({id:o.id,title:o.title,since:g});p(w,JSON.stringify(t,null,2)+`
57
- `),f||E("capabilities.json updated")}for(const t of k){const o=l.join(h,t.file);let r;if(t.isNew||!c.existsSync(o))r={scenarioId:t.file.replace(".json",""),description:d.summary||"",capabilitiesCovered:t.capabilitiesCovered||[],steps:t.stepsToAdd||[]},p(o,JSON.stringify(r,null,2)+`
58
- `),f||E(`Created scenario: ${C(t.file)}`);else{r=v(o);const b=new Set(r.capabilitiesCovered||[]);(t.capabilitiesCovered||[]).forEach(O=>b.add(O)),r.capabilitiesCovered=[...b],r.steps=[...r.steps||[],...t.stepsToAdd||[]],p(o,JSON.stringify(r,null,2)+`
59
- `),f||E(`Updated scenario: ${C(t.file)}`)}$=!0}if(P&&c.existsSync(y)){let t=c.readFileSync(y,"utf8");/##\s+Unreleased/i.test(t)&&(t=t.replace(/(##\s+Unreleased[^\n]*\n)/i,`$1
60
- ${P}
61
- `),p(y,t),f||E("CHANGELOG.md updated"),$=!0)}const x=new Map;try{for(const t of N){c.existsSync(t.filePath)?x.set(t.filePath,c.readFileSync(t.filePath,"utf8")):x.set(t.filePath,null);const o=`${t.filePath}.tmp`;c.writeFileSync(o,t.content),c.renameSync(o,t.filePath)}}catch(t){for(const[o,r]of x.entries())r===null?c.existsSync(o)&&c.unlinkSync(o):c.writeFileSync(o,r);throw new Error(`Failed applying changes. Rolled back. Details: ${t.message}`)}return $}function B(n){const e=String(n||"").trim().replace(/^```json?\n?/,"").replace(/\n?```$/,"");return JSON.parse(e)}function ee(n){const e=l.join(n,"inferno"),i=l.join(e,"contract.json"),d=l.join(e,"capabilities.json"),g=l.join(e,"scenarios"),f=v(i),s=v(d),u=[];if(c.existsSync(g))for(const h of c.readdirSync(g).filter(m=>m.endsWith(".json"))){const m=v(l.join(g,h));m&&u.push({...m,_file:h})}let w="0.1.0";const y=l.join(n,"package.json");if(c.existsSync(y)){const h=v(y);h?.version&&(w=h.version)}return{contract:f,capabilities:s,scenarios:u,version:w}}async function te(n){const e=process.cwd(),i=l.join(e,"inferno");V("suggest"),c.existsSync(i)||I("inferno/ not found","Run: infernoflow init");const d=l.join(i,"contract.json"),g=l.join(i,"capabilities.json"),f=l.join(i,"scenarios"),s=v(d);s||I("contract.json not found or invalid");const u=v(g),w=[];if(c.existsSync(f))for(const a of c.readdirSync(f).filter(j=>j.endsWith(".json"))){const j=v(l.join(f,a));j&&w.push({...j,_file:a})}let y="0.1.0";const h=l.join(e,"package.json");if(c.existsSync(h)){const a=v(h);a?.version&&(y=a.version)}let S=n.filter(a=>!a.startsWith("-")).slice(1).join(" ");if(!S){const a=J.createInterface({input:process.stdin,output:process.stdout});console.log(A(" Describe what changed in your code (e.g. 'added email notifications'):")),S=await T(a,` ${C(">")} `),a.close(),console.log()}S||I("No description provided",'Usage: infernoflow suggest "what changed"');const k=z({description:S,contract:s,capabilities:u,scenarios:w});D("Generated Prompt"),console.log(),console.log(A("\u2500".repeat(50))),console.log(k),console.log(A("\u2500".repeat(50))),console.log(),U("Copy the prompt above and paste it into:"),console.log(` ${C("\u2022")} Claude \u2192 https://claude.ai`),console.log(` ${C("\u2022")} ChatGPT \u2192 https://chatgpt.com`),console.log(` ${C("\u2022")} Copilot, Cursor, or any AI you use`),console.log(),F("The AI will respond with a JSON object."),console.log();const P=J.createInterface({input:process.stdin,output:process.stdout});console.log(A(" Paste the AI's JSON response below, then press Enter twice:")),console.log();let $="",N=0;await new Promise(a=>{P.on("line",j=>{j.trim()===""?(N++,N>=2&&$.trim()&&a()):(N=0,$+=j+`
62
- `)}),P.on("close",a)}),P.close();let p;try{p=B($)}catch{I("Could not parse the AI response as JSON","Make sure you copied the full JSON response from the AI")}const x=M(p);x.length>0&&I("AI response schema is invalid",x[0]+(x.length>1?` (+${x.length-1} more)`:""));const t=Z(s,p);t.length>0&&I("AI response contains conflicting capability operations",t[0]+(t.length>1?` (+${t.length-1} more)`:"")),D("Proposed Changes"),console.log(),p.summary&&(console.log(` ${H("Summary:")} ${p.summary}`),console.log());const o=p.newCapabilities||[],r=p.removedCapabilities||[],b=p.updatedScenarios||[];o.length===0&&r.length===0&&b.length===0&&(E("No capability changes detected \u2014 nothing to apply."),console.log(),process.exit(0)),o.length>0&&(console.log(` ${L("+")} New capabilities:`),o.forEach(a=>console.log(` ${L(a.id)} \u2014 ${A(a.title)}`)),console.log()),r.length>0&&(console.log(` ${G("-")} Removed capabilities:`),r.forEach(a=>console.log(` ${G(a)}`)),console.log()),b.length>0&&(console.log(` ${C("~")} Scenario updates:`),b.forEach(a=>{const j=a.isNew?L("[new]"):C("[update]");console.log(` ${j} ${a.file}`)}),console.log()),p.changelogEntry&&(console.log(` ${Y("\u{1F4DD}")} Changelog: ${A(p.changelogEntry)}`),console.log());const O=J.createInterface({input:process.stdin,output:process.stdout}),_=await T(O,` Apply these changes? ${A("(y/n)")} `);O.close(),console.log(),_.toLowerCase()!=="y"&&_.toLowerCase()!=="yes"&&(F("Cancelled \u2014 no changes made."),console.log(),process.exit(0)),D("Applying Changes"),console.log(),K({cwd:e,contract:s,capabilities:u,suggestion:p,version:y}),R("suggest complete!"),W([C("infernoflow status")+" \u2014 verify the updated contract",C("infernoflow check")+" \u2014 validate everything"])}export{K as applyChanges,z as buildPrompt,Z as detectSuggestionConflicts,ee as loadSuggestContext,B as parseSuggestionJson,v as readJson,te as suggestCommand,M as validateSuggestion};
55
+ - Keep it minimal and accurate`}function K(o){const e=[];if(!o||typeof o!="object")return["AI response must be a JSON object."];o.summary!=null&&typeof o.summary!="string"&&e.push('"summary" must be a string.'),Array.isArray(o.newCapabilities)||e.push('"newCapabilities" must be an array.'),Array.isArray(o.removedCapabilities)||e.push('"removedCapabilities" must be an array.'),Array.isArray(o.updatedScenarios)||e.push('"updatedScenarios" must be an array.'),o.changelogEntry!=null&&typeof o.changelogEntry!="string"&&e.push('"changelogEntry" must be a string.');for(const i of o.newCapabilities||[]){if(!i||typeof i!="object"){e.push('Each item in "newCapabilities" must be an object.');continue}(typeof i.id!="string"||!/^[A-Z][A-Za-z0-9]*$/.test(i.id))&&e.push("newCapabilities[].id must be PascalCase (example: SendEmail)."),(typeof i.title!="string"||!i.title.trim())&&e.push("newCapabilities[].title must be a non-empty string.")}for(const i of o.removedCapabilities||[])(typeof i!="string"||!i.trim())&&e.push("removedCapabilities[] must contain non-empty strings.");for(const i of o.updatedScenarios||[]){if(!i||typeof i!="object"){e.push('Each item in "updatedScenarios" must be an object.');continue}(typeof i.file!="string"||!i.file.endsWith(".json"))&&e.push("updatedScenarios[].file must be a .json filename."),typeof i.isNew!="boolean"&&e.push("updatedScenarios[].isNew must be boolean."),(!Array.isArray(i.capabilitiesCovered)||!Array.isArray(i.stepsToAdd))&&e.push("updatedScenarios[].capabilitiesCovered and stepsToAdd must be arrays.")}return e}function B(o,e){const i=[],p=new Set(o.capabilities||[]),g=new Set((e.newCapabilities||[]).map(s=>s.id)),d=new Set(e.removedCapabilities||[]);for(const s of g)d.has(s)&&i.push(`Capability "${s}" appears in both newCapabilities and removedCapabilities.`),p.has(s)&&i.push(`Capability "${s}" already exists in contract capabilities.`);for(const s of d)p.has(s)||i.push(`Capability "${s}" cannot be removed because it does not exist in contract.`);return i}function Q({cwd:o,contract:e,capabilities:i,suggestion:p,version:g,quiet:d=!1}){const s=l.join(o,"inferno"),f=l.join(s,"contract.json"),w=l.join(s,"capabilities.json"),y=l.join(s,"CHANGELOG.md"),h=l.join(s,"scenarios"),m=p.newCapabilities||[],S=p.removedCapabilities||[],k=p.updatedScenarios||[],O=p.changelogEntry||"";let x=!1;const P=[],j=(t,n)=>P.push({filePath:t,content:n});if(m.length>0||S.length>0){const t=[...e.capabilities.filter(b=>!S.includes(b)),...m.map(b=>b.id)],n=Number(e.policyVersion||1)+1,r={...e,capabilities:t,policyVersion:n};j(f,JSON.stringify(r,null,2)+`
56
+ `),d||N(`contract.json updated \u2192 policyVersion: v${n}`),x=!0}if(m.length>0||S.length>0){const t=i?{...i}:{schemaVersion:1,capabilities:[]};t.capabilities=(t.capabilities||[]).filter(n=>!S.includes(n.id));for(const n of m)t.capabilities.find(r=>r.id===n.id)||t.capabilities.push({id:n.id,title:n.title,since:g});j(w,JSON.stringify(t,null,2)+`
57
+ `),d||N("capabilities.json updated")}for(const t of k){const n=l.join(h,t.file);let r;if(t.isNew||!c.existsSync(n))r={scenarioId:t.file.replace(".json",""),description:p.summary||"",capabilitiesCovered:t.capabilitiesCovered||[],steps:t.stepsToAdd||[]},j(n,JSON.stringify(r,null,2)+`
58
+ `),d||N(`Created scenario: ${C(t.file)}`);else{r=$(n);const b=new Set(r.capabilitiesCovered||[]);(t.capabilitiesCovered||[]).forEach(I=>b.add(I)),r.capabilitiesCovered=[...b],r.steps=[...r.steps||[],...t.stepsToAdd||[]],j(n,JSON.stringify(r,null,2)+`
59
+ `),d||N(`Updated scenario: ${C(t.file)}`)}x=!0}if(O&&c.existsSync(y)){let t=c.readFileSync(y,"utf8");/##\s+Unreleased/i.test(t)&&(t=t.replace(/(##\s+Unreleased[^\n]*\n)/i,`$1
60
+ ${O}
61
+ `),j(y,t),d||N("CHANGELOG.md updated"),x=!0)}const u=new Map;try{for(const t of P){c.existsSync(t.filePath)?u.set(t.filePath,c.readFileSync(t.filePath,"utf8")):u.set(t.filePath,null);const n=`${t.filePath}.tmp`;c.writeFileSync(n,t.content),c.renameSync(n,t.filePath)}}catch(t){for(const[n,r]of u.entries())r===null?c.existsSync(n)&&c.unlinkSync(n):c.writeFileSync(n,r);throw new Error(`Failed applying changes. Rolled back. Details: ${t.message}`)}return x}function X(o){const e=String(o||"").trim().replace(/^```json?\n?/,"").replace(/\n?```$/,"");return JSON.parse(e)}function oe(o){const e=l.join(o,"inferno"),i=l.join(e,"contract.json"),p=l.join(e,"capabilities.json"),g=l.join(e,"scenarios"),d=$(i),s=$(p),f=[];if(c.existsSync(g))for(const h of c.readdirSync(g).filter(m=>m.endsWith(".json"))){const m=$(l.join(g,h));m&&f.push({...m,_file:h})}let w="0.1.0";const y=l.join(o,"package.json");if(c.existsSync(y)){const h=$(y);h?.version&&(w=h.version)}return{contract:d,capabilities:s,scenarios:f,version:w}}async function ne(o){const e=process.cwd(),i=l.join(e,"inferno");U("suggest"),c.existsSync(i)||E("inferno/ not found","Run: infernoflow init");const p=l.join(i,"contract.json"),g=l.join(i,"capabilities.json"),d=l.join(i,"scenarios"),s=$(p);s||E("contract.json not found or invalid");const f=$(g),w=[];if(c.existsSync(d))for(const a of c.readdirSync(d).filter(v=>v.endsWith(".json"))){const v=$(l.join(d,a));v&&w.push({...v,_file:a})}let y="0.1.0";const h=l.join(e,"package.json");if(c.existsSync(h)){const a=$(h);a?.version&&(y=a.version)}let S=o.filter(a=>!a.startsWith("-")).slice(1).join(" ");if(!S){const a=J.createInterface({input:process.stdin,output:process.stdout});console.log(A(" Describe what changed in your code (e.g. 'added email notifications'):")),S=await V(a,` ${C(">")} `),a.close(),console.log()}S||E("No description provided",'Usage: infernoflow suggest "what changed"');const k=Z({description:S,contract:s,capabilities:f,scenarios:w}),O=M(k,i);D("Generated Prompt"),console.log(),console.log(A("\u2500".repeat(50))),console.log(O),console.log(A("\u2500".repeat(50))),console.log(),R("Copy the prompt above and paste it into:"),console.log(` ${C("\u2022")} Claude \u2192 https://claude.ai`),console.log(` ${C("\u2022")} ChatGPT \u2192 https://chatgpt.com`),console.log(` ${C("\u2022")} Copilot, Cursor, or any AI you use`),console.log(),G("The AI will respond with a JSON object."),console.log();const x=J.createInterface({input:process.stdin,output:process.stdout});console.log(A(" Paste the AI's JSON response below, then press Enter twice:")),console.log();let P="",j=0;await new Promise(a=>{x.on("line",v=>{v.trim()===""?(j++,j>=2&&P.trim()&&a()):(j=0,P+=v+`
62
+ `)}),x.on("close",a)}),x.close();let u;try{u=X(P)}catch{E("Could not parse the AI response as JSON","Make sure you copied the full JSON response from the AI")}const t=K(u);t.length>0&&E("AI response schema is invalid",t[0]+(t.length>1?` (+${t.length-1} more)`:""));const n=B(s,u);n.length>0&&E("AI response contains conflicting capability operations",n[0]+(n.length>1?` (+${n.length-1} more)`:"")),D("Proposed Changes"),console.log(),u.summary&&(console.log(` ${Y("Summary:")} ${u.summary}`),console.log());const r=u.newCapabilities||[],b=u.removedCapabilities||[],I=u.updatedScenarios||[];r.length===0&&b.length===0&&I.length===0&&(N("No capability changes detected \u2014 nothing to apply."),console.log(),process.exit(0)),r.length>0&&(console.log(` ${L("+")} New capabilities:`),r.forEach(a=>console.log(` ${L(a.id)} \u2014 ${A(a.title)}`)),console.log()),b.length>0&&(console.log(` ${T("-")} Removed capabilities:`),b.forEach(a=>console.log(` ${T(a)}`)),console.log()),I.length>0&&(console.log(` ${C("~")} Scenario updates:`),I.forEach(a=>{const v=a.isNew?L("[new]"):C("[update]");console.log(` ${v} ${a.file}`)}),console.log()),u.changelogEntry&&(console.log(` ${z("\u{1F4DD}")} Changelog: ${A(u.changelogEntry)}`),console.log());const _=J.createInterface({input:process.stdin,output:process.stdout}),F=await V(_,` Apply these changes? ${A("(y/n)")} `);_.close(),console.log(),F.toLowerCase()!=="y"&&F.toLowerCase()!=="yes"&&(G("Cancelled \u2014 no changes made."),console.log(),process.exit(0)),D("Applying Changes"),console.log(),Q({cwd:e,contract:s,capabilities:f,suggestion:u,version:y}),W("suggest complete!"),H([C("infernoflow status")+" \u2014 verify the updated contract",C("infernoflow check")+" \u2014 validate everything"])}export{Q as applyChanges,Z as buildPrompt,B as detectSuggestionConflicts,oe as loadSuggestContext,X as parseSuggestionJson,$ as readJson,ne as suggestCommand,K as validateSuggestion};
@@ -0,0 +1,4 @@
1
+ import*as p from"node:fs";import*as m from"node:path";import{execSync as F}from"node:child_process";function k(i,o={}){const{sinceCommits:d=1,includeStagedOnly:l=!1}=o,r=new Set;try{const f=F("git diff --name-only HEAD",{cwd:i,encoding:"utf8",timeout:1e4});for(const c of f.split(`
2
+ `).map(n=>n.trim()).filter(Boolean))r.add(c)}catch{}try{const f=F(`git diff --name-only HEAD~${d} HEAD`,{cwd:i,encoding:"utf8",timeout:1e4});for(const c of f.split(`
3
+ `).map(n=>n.trim()).filter(Boolean))r.add(c)}catch{}try{const f=F("git ls-files --others --exclude-standard",{cwd:i,encoding:"utf8",timeout:1e4});for(const c of f.split(`
4
+ `).map(n=>n.trim()).filter(Boolean))r.add(c)}catch{}return Array.from(r).sort()}function C(i){const o=m.join(i,"capability-map.json");if(!p.existsSync(o))return null;try{return JSON.parse(p.readFileSync(o,"utf8"))}catch{return null}}function x(i){const o=m.join(i,"adoption_profile.json");if(!p.existsSync(o))return null;try{return JSON.parse(p.readFileSync(o,"utf8"))}catch{return null}}function j(i){const o=m.join(i,"capabilities.json");if(!p.existsSync(o))return[];try{return JSON.parse(p.readFileSync(o,"utf8")).capabilities||[]}catch{return[]}}function A(i,o={}){const d=m.join(i,"inferno"),l=k(i,o);if(!l.length)return{changedFiles:[],affectedCapabilities:[],unmappedFiles:[],hasCapabilityMap:!1};const r=C(d),f=x(d),c=j(d),n=new Map,y=(e,s)=>{n.has(e.id)||n.set(e.id,{id:e.id,title:e.title||e.id,matchedFiles:new Set}),n.get(e.id).matchedFiles.add(s)},h=new Set;if(r){for(const e of l)for(const[s,t]of Object.entries(r))if(e.startsWith(s.replace(/\\/g,"/")))for(const a of t){const u=c.find(b=>b.id===a)||{id:a,title:a};y(u,e),h.add(e)}}const g=[];if(f){const e=m.join(d,"capabilities.json");try{const s=JSON.parse(p.readFileSync(e,"utf8"));for(const t of s.capabilities||[])t.sourceFiles&&t.sourceFiles.length>0&&g.push(t)}catch{}}if(g.length>0)for(const e of g)for(const s of e.sourceFiles||[]){const t=s.replace(/\\/g,"/");for(const a of l){const u=a.replace(/\\/g,"/");(u===t||u.startsWith(m.dirname(t)+"/"))&&(y(e,a),h.add(a))}}const I=[{keywords:["search"],capId:"SearchItems"},{keywords:["filter"],capId:"FilterItems"},{keywords:["auth","login","logout","signin","signup"],capId:"Authentication"},{keywords:["create","add","new"],capId:"CreateItem"},{keywords:["update","edit"],capId:"UpdateItem"},{keywords:["delete","remove"],capId:"DeleteItem"},{keywords:["read","list","view"],capId:"ReadItems"},{keywords:["due","deadline","date"],capId:"SetDueDate"},{keywords:["priority"],capId:"SetPriority"},{keywords:["complete","done","toggle"],capId:"ToggleComplete"}];for(const e of l){if(h.has(e))continue;const s=e.toLowerCase();for(const t of I)if(t.keywords.some(a=>s.includes(a))){const a=c.find(u=>u.id===t.capId)||{id:t.capId,title:t.capId};y(a,e),h.add(e);break}}const S=l.filter(e=>!h.has(e)),w=Array.from(n.values()).map(e=>({id:e.id,title:e.title,matchedFiles:Array.from(e.matchedFiles),confidence:e.matchedFiles.size>=3?"high":e.matchedFiles.size>=1?"medium":"low"}));return{changedFiles:l,affectedCapabilities:w,unmappedFiles:S,hasCapabilityMap:!!r}}export{A as detectDrift,k as getChangedFiles,x as loadAdoptionProfile,j as loadCapabilities,C as loadCapabilityMap};
@@ -0,0 +1,9 @@
1
+ import{readProfile as r,summarizeProfile as i}from"./profile.mjs";function o(t){let e;try{e=r(t)}catch{return""}const n=[];if(!(e.namingStyle!=="unknown"||e.preferredVerbs.length>0||e.stack.framework!=="unknown"))return"";if(n.push("## Developer profile (personalise your response to match these preferences)"),e.namingStyle!=="unknown"&&n.push(`- Capability naming style: **${e.namingStyle}** \u2014 use this for all new capability IDs`),e.preferredVerbs.length>0&&n.push(`- Preferred action verbs: ${e.preferredVerbs.slice(0,5).join(", ")} \u2014 prefer these when naming new capabilities`),e.stack.framework!=="unknown"&&n.push(`- Stack: ${e.stack.framework} / ${e.stack.language} (${e.stack.projectType})`),e.changelogVerbosity!=="unknown"){const s=e.changelogVerbosity==="brief"?"Keep changelog entries short (one line, action-focused)":"Write detailed changelog entries (include context and impact)";n.push(`- Changelog style: ${s}`)}if(e.featureClusters.length>0){const s=e.featureClusters[0];s.length>=2&&n.push(`- Common capability cluster: [${s.slice(0,4).join(", ")}] \u2014 if the task touches one of these, consider whether others need updating too`)}return e.sessionCount>=10&&n.push(`- Experienced user (${e.sessionCount} sessions) \u2014 skip basic explanations, be direct`),n.join(`
2
+ `)}function c(t,e){const n=o(e);return n?t.includes("## Instructions")?t.replace("## Instructions",n+`
3
+
4
+ ## Instructions`):t.includes("Respond with ONLY")?t.replace("Respond with ONLY",n+`
5
+
6
+ ---
7
+ Respond with ONLY`):t+`
8
+
9
+ `+n:t}function u(t){try{const e=r(t);return i(e)||null}catch{return null}}export{o as buildPersonalisationBlock,c as personalisePrompt,u as profileStatusLine};
@@ -0,0 +1 @@
1
+ import"node:path";import{readProfile as s,writeProfile as c,recordCommandUse as l,detectNamingStyle as f,detectPreferredVerbs as a,recordCapabilityCluster as d}from"./profile.mjs";const m=1800*1e3;function h(o,t){try{const e=s(o);l(e,t);const r=Date.now(),n=e._lastCommandTs||0;r-n>m&&(e.sessionCount=(e.sessionCount||0)+1),e._lastCommandTs=r,c(o,e)}catch{}}function S(o,t){if(!(!t||t.length===0))try{const e=s(o),r=f(t);r!=="unknown"&&(e.sessionCount>=3||e.namingStyle==="unknown")&&(e.namingStyle=r);const n=a(t);if(n.length>0){const i=[...new Set([...e.preferredVerbs,...n])];e.preferredVerbs=i.slice(0,8)}t.length>=2&&d(e,t),c(o,e)}catch{}}function b(o,t){if(t)try{const e=s(o),n=String(t).trim().split(/\s+/).length>=15?"detailed":"brief";(e.changelogVerbosity==="unknown"||e.sessionCount>=5)&&(e.changelogVerbosity=n),c(o,e)}catch{}}export{S as observeCapabilitiesAdded,b as observeChangelogEntry,h as observeCommandStart};
@@ -0,0 +1,2 @@
1
+ import*as a from"node:fs";import*as f from"node:path";const i=1;function m(e){return f.join(e,"developer-profile.json")}function c(){const e=new Date().toISOString();return{schemaVersion:i,createdAt:e,updatedAt:e,sessionCount:0,namingStyle:"unknown",preferredVerbs:[],commandUsage:{},featureClusters:[],avgSessionLength:0,commitFrequency:"unknown",changelogVerbosity:"unknown",stack:{language:"unknown",framework:"unknown",projectType:"unknown"}}}function l(e){const n=m(e);if(!a.existsSync(n))return c();try{const o=JSON.parse(a.readFileSync(n,"utf8"));return{...c(),...o}}catch{return c()}}function d(e,n){n.updatedAt=new Date().toISOString(),n.schemaVersion=i,a.mkdirSync(e,{recursive:!0}),a.writeFileSync(m(e),JSON.stringify(n,null,2)+`
2
+ `,"utf8")}function S(e,n){return e.commandUsage||(e.commandUsage={}),e.commandUsage[n]=(e.commandUsage[n]||0)+1,e}function g(e){if(!e||e.length===0)return"unknown";let n=0,o=0,t=0;for(const s of e)/^[A-Z][a-z]/.test(s)?n++:/^[a-z].*[A-Z]/.test(s)?o++:s.includes("-")&&t++;const r=Math.max(n,o,t);return r===0?"unknown":n===r?"PascalCase":o===r?"camelCase":"kebab-case"}function w(e){const n={},o=/^(Create|Add|Update|Edit|Delete|Remove|Get|Read|List|Fetch|Search|Filter|Toggle|Set|Clear|Send|Upload|Download|Export|Import|Generate|Sync|Validate|Check|Run|Start|Stop|Enable|Disable|Show|Hide)/;for(const t of e||[]){const r=t.match(o);r&&(n[r[1]]=(n[r[1]]||0)+1)}return Object.entries(n).sort((t,r)=>r[1]-t[1]).slice(0,5).map(([t])=>t)}function h(e,n,o){const t=l(e);n?.developmentProfile&&(t.stack={language:n.developmentProfile.language||"unknown",framework:n.developmentProfile.framework||"unknown",projectType:n.developmentProfile.projectType||"unknown"});const r=(o||[]).map(s=>s.id);return r.length>0&&(t.namingStyle=g(r),t.preferredVerbs=w(r)),r.length>1&&(t.featureClusters=[r]),d(e,t),t}function p(e,n){if(!n||n.length<2)return e;e.featureClusters||(e.featureClusters=[]);for(let o=0;o<e.featureClusters.length;o++){const t=new Set(e.featureClusters[o]),r=new Set(n);if([...r].filter(u=>t.has(u)).length/Math.min(t.size,r.size)>.5){const u=Array.from(new Set([...t,...r]));return e.featureClusters[o]=u,e}}return e.featureClusters.push([...n]),e}function C(e){if(!e||e.sessionCount===0&&e.namingStyle==="unknown")return null;const n=[];return e.namingStyle!=="unknown"&&n.push(`naming: ${e.namingStyle}`),e.preferredVerbs.length>0&&n.push(`verbs: ${e.preferredVerbs.slice(0,3).join(", ")}`),e.stack.framework!=="unknown"&&n.push(`stack: ${e.stack.framework} (${e.stack.language})`),e.sessionCount>0&&n.push(`sessions: ${e.sessionCount}`),n.join(" \xB7 ")}export{i as PROFILE_SCHEMA_VERSION,c as blankProfile,g as detectNamingStyle,w as detectPreferredVerbs,m as profilePath,l as readProfile,p as recordCapabilityCluster,S as recordCommandUse,h as seedProfileFromAdoption,C as summarizeProfile,d as writeProfile};
@@ -18,8 +18,378 @@ const TOOLS = [
18
18
  { name: "infernoflow_check", description: "Validate infernoflow contract and capabilities", inputSchema: { type: "object", properties: {} } },
19
19
  { name: "infernoflow_status", description: "Show contract health at a glance", inputSchema: { type: "object", properties: {} } },
20
20
  { name: "infernoflow_context", description: "Generate AI-ready context", inputSchema: { type: "object", properties: { intent: { type: "string" }, working: { type: "string" } } } },
21
+ { name: "infernoflow_git_drift", description: "Detect which capabilities may be affected by recent code changes. Compares git-changed files to the capability registry and returns suggestions for contract updates.", inputSchema: { type: "object", properties: { sinceCommits: { type: "number", description: "How many commits back to check (default: 1)" } } } },
22
+ { name: "infernoflow_implement", description: "Generate a structured code implementation prompt for a task. Uses the contract and stack context to produce step-by-step coding instructions for the agent.", inputSchema: { type: "object", properties: { task: { type: "string", description: "What to implement" }, mode: { type: "string", enum: ["cursor", "generic", "both"], description: "Prompt style (default: both)" } }, required: ["task"] } },
23
+ { name: "infernoflow_scan_ui", description: "Scan components and styles for UI changes vs the stored contract. Returns new/changed components, design token changes, and suggested contract updates.", inputSchema: { type: "object", properties: {} } },
24
+ { name: "infernoflow_review", description: "Pre-merge capability drift check. Compares all changed files in the current branch against the capability contract and reports drift risk before you merge.", inputSchema: { type: "object", properties: { branch: { type: "string", description: "Branch to compare against (default: main)" } } } },
21
25
  ];
22
26
 
27
+ // ── git drift detection (inline — no external imports in this template file) ─
28
+ function detectGitDrift(sinceCommits) {
29
+ const cwd = process.cwd();
30
+ const infernoDir = path.join(cwd, "inferno");
31
+
32
+ const runGit = (cmd) => {
33
+ try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 10_000 }); }
34
+ catch { return ""; }
35
+ };
36
+
37
+ const changedSet = new Set();
38
+ const addLines = (out) => out.split("\n").map(l => l.trim()).filter(Boolean).forEach(f => changedSet.add(f));
39
+
40
+ addLines(runGit("git diff --name-only HEAD"));
41
+ addLines(runGit(`git diff --name-only HEAD~${sinceCommits} HEAD`));
42
+ addLines(runGit("git ls-files --others --exclude-standard"));
43
+
44
+ const changedFiles = Array.from(changedSet).sort();
45
+ if (!changedFiles.length) return "No changed files detected since last commit.";
46
+
47
+ // Load capabilities registry
48
+ let capabilities = [];
49
+ try {
50
+ const capsPath = path.join(infernoDir, "capabilities.json");
51
+ if (fs.existsSync(capsPath)) capabilities = JSON.parse(fs.readFileSync(capsPath, "utf8")).capabilities || [];
52
+ } catch {}
53
+
54
+ // Load capability-map if present
55
+ let capMap = null;
56
+ try {
57
+ const mapPath = path.join(infernoDir, "capability-map.json");
58
+ if (fs.existsSync(mapPath)) capMap = JSON.parse(fs.readFileSync(mapPath, "utf8"));
59
+ } catch {}
60
+
61
+ const capHits = new Map();
62
+ const mappedFiles = new Set();
63
+
64
+ const addHit = (capId, capTitle, file) => {
65
+ if (!capHits.has(capId)) capHits.set(capId, { id: capId, title: capTitle || capId, files: new Set() });
66
+ capHits.get(capId).files.add(file);
67
+ mappedFiles.add(file);
68
+ };
69
+
70
+ // Strategy 1: capability-map.json
71
+ if (capMap) {
72
+ for (const file of changedFiles) {
73
+ for (const [prefix, capIds] of Object.entries(capMap)) {
74
+ if (file.startsWith(prefix.replace(/\\/g, "/"))) {
75
+ for (const capId of capIds) {
76
+ const cap = capabilities.find(c => c.id === capId);
77
+ addHit(capId, cap?.title, file);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ // Strategy 2: heuristic keyword matching on filename
85
+ const RULES = [
86
+ { kw: ["search"], id: "SearchItems" }, { kw: ["filter"], id: "FilterItems" },
87
+ { kw: ["auth", "login", "logout"], id: "Authentication" },
88
+ { kw: ["create", "add", "new"], id: "CreateItem" },
89
+ { kw: ["update", "edit"], id: "UpdateItem" },
90
+ { kw: ["delete", "remove"], id: "DeleteItem" },
91
+ { kw: ["list", "read", "view"], id: "ReadItems" },
92
+ { kw: ["due", "deadline"], id: "SetDueDate" },
93
+ { kw: ["priority"], id: "SetPriority" },
94
+ { kw: ["complete", "toggle"], id: "ToggleComplete" },
95
+ ];
96
+ for (const file of changedFiles) {
97
+ if (mappedFiles.has(file)) continue;
98
+ const lower = file.toLowerCase();
99
+ for (const rule of RULES) {
100
+ if (rule.kw.some(k => lower.includes(k))) {
101
+ const cap = capabilities.find(c => c.id === rule.id);
102
+ addHit(rule.id, cap?.title, file);
103
+ break;
104
+ }
105
+ }
106
+ }
107
+
108
+ const unmapped = changedFiles.filter(f => !mappedFiles.has(f));
109
+ const affected = Array.from(capHits.values());
110
+
111
+ // Format output
112
+ const lines = [
113
+ `## infernoflow git drift report`,
114
+ `Changed files: ${changedFiles.length}`,
115
+ `Affected capabilities: ${affected.length}`,
116
+ "",
117
+ ];
118
+
119
+ if (affected.length) {
120
+ lines.push("### Capabilities likely needing contract review:");
121
+ for (const cap of affected) {
122
+ lines.push(`\n**${cap.id}** — ${cap.title}`);
123
+ for (const f of cap.files) lines.push(` - ${f}`);
124
+ }
125
+ lines.push("");
126
+ lines.push("### Suggested action:");
127
+ lines.push(`Call infernoflow_run with task "review changes to ${affected.map(c => c.id).join(", ")}" to update the contract.`);
128
+ } else {
129
+ lines.push("No capability matches found for changed files.");
130
+ lines.push("Consider updating inferno/capability-map.json to map your source paths to capabilities.");
131
+ }
132
+
133
+ if (unmapped.length) {
134
+ lines.push(`\n### Unmapped changed files (${unmapped.length}):`);
135
+ for (const f of unmapped.slice(0, 10)) lines.push(` - ${f}`);
136
+ if (unmapped.length > 10) lines.push(` ... +${unmapped.length - 10} more`);
137
+ }
138
+
139
+ return lines.join("\n");
140
+ }
141
+
142
+ // ── infernoflow_scan_ui ────────────────────────────────────────────────────
143
+ function scanUi() {
144
+ const cwd = process.cwd();
145
+ const infernoDir = path.join(cwd, "inferno");
146
+ const contractPath = path.join(infernoDir, "contract.json");
147
+ if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
148
+
149
+ const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
150
+ const storedUi = contract.ui || {};
151
+
152
+ // Collect style + component files
153
+ const styleExts = /\.(css|scss|sass|less|ts|tsx|js|jsx|html)$/;
154
+ const SKIP = new Set(["node_modules", ".git", "dist", "build", ".angular", ".next", "vendor", "coverage"]);
155
+ const files = [];
156
+ const walk = (dir) => {
157
+ try {
158
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
159
+ const full = path.join(dir, entry.name);
160
+ if (entry.isDirectory()) { if (!SKIP.has(entry.name)) walk(full); }
161
+ else if (styleExts.test(entry.name) && !entry.name.includes(".min.") && !entry.name.endsWith(".map")) files.push(full);
162
+ }
163
+ } catch {}
164
+ };
165
+ for (const root of ["src", "app", "frontend", "components", "styles"]) {
166
+ const p = path.join(cwd, root);
167
+ if (fs.existsSync(p)) walk(p);
168
+ }
169
+
170
+ // Extract current components from TS/TSX files
171
+ const currentComponents = new Set();
172
+ const currentTokens = new Set();
173
+
174
+ for (const f of files) {
175
+ const text = fs.existsSync(f) ? fs.readFileSync(f, "utf8") : "";
176
+ // Components
177
+ for (const m of text.matchAll(/@Component[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Component)/g)) currentComponents.add(m[1].replace(/Component$/, ""));
178
+ for (const m of text.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)/g)) currentComponents.add(m[1]);
179
+ // Design tokens
180
+ for (const m of text.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g)) currentTokens.add(`--${m[1]}`);
181
+ }
182
+
183
+ const storedComponents = new Set(storedUi.components || []);
184
+ const storedTokens = new Set(storedUi.designTokens || []);
185
+
186
+ const newComponents = [...currentComponents].filter(c => !storedComponents.has(c));
187
+ const removedComponents = [...storedComponents].filter(c => !currentComponents.has(c));
188
+ const newTokens = [...currentTokens].filter(t => !storedTokens.has(t));
189
+ const removedTokens = [...storedTokens].filter(t => !currentTokens.has(t));
190
+
191
+ const lines = ["## infernoflow UI scan report", ""];
192
+
193
+ if (!newComponents.length && !removedComponents.length && !newTokens.length && !removedTokens.length) {
194
+ lines.push("✔ No UI changes detected since last scan.");
195
+ return lines.join("\n");
196
+ }
197
+
198
+ if (newComponents.length) {
199
+ lines.push(`### New components (${newComponents.length})`);
200
+ newComponents.slice(0, 15).forEach(c => lines.push(` + ${c}`));
201
+ lines.push("");
202
+ }
203
+ if (removedComponents.length) {
204
+ lines.push(`### Removed components (${removedComponents.length})`);
205
+ removedComponents.slice(0, 10).forEach(c => lines.push(` - ${c}`));
206
+ lines.push("");
207
+ }
208
+ if (newTokens.length) {
209
+ lines.push(`### New design tokens (${newTokens.length})`);
210
+ newTokens.slice(0, 10).forEach(t => lines.push(` + ${t}`));
211
+ lines.push("");
212
+ }
213
+ if (removedTokens.length) {
214
+ lines.push(`### Removed design tokens (${removedTokens.length})`);
215
+ removedTokens.slice(0, 10).forEach(t => lines.push(` - ${t}`));
216
+ lines.push("");
217
+ }
218
+
219
+ lines.push("### Suggested action");
220
+ if (newComponents.length) {
221
+ const newCaps = newComponents.slice(0, 5).map(c => `View${c}`).join(", ");
222
+ lines.push(`Consider adding these capabilities: ${newCaps}`);
223
+ lines.push(`Call infernoflow_run with task "add UI capabilities for new components: ${newComponents.slice(0,3).join(", ")}" to update the contract.`);
224
+ }
225
+
226
+ return lines.join("\n");
227
+ }
228
+
229
+ // ── infernoflow_review ─────────────────────────────────────────────────────
230
+ function reviewDrift(baseBranch) {
231
+ const cwd = process.cwd();
232
+ const infernoDir = path.join(cwd, "inferno");
233
+ const contractPath = path.join(infernoDir, "contract.json");
234
+ if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
235
+
236
+ const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
237
+
238
+ // Get changed files vs base branch
239
+ const runGit = (cmd) => { try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 15_000 }); } catch { return ""; } };
240
+
241
+ const diffOutput = runGit(`git diff --name-only ${baseBranch}...HEAD`);
242
+ const changedFiles = diffOutput.split("\n").map(l => l.trim()).filter(Boolean);
243
+
244
+ if (!changedFiles.length) return `No changes detected vs ${baseBranch}. Safe to merge.`;
245
+
246
+ // Categorise changed files
247
+ const infraFiles = changedFiles.filter(f => /\.(json|yaml|yml|env|config|lock)$/.test(f) || f.includes("inferno/"));
248
+ const sourceFiles = changedFiles.filter(f => /\.(ts|tsx|js|jsx|mjs|cs|py|go|java)$/.test(f));
249
+ const styleFiles = changedFiles.filter(f => /\.(css|scss|sass|less)$/.test(f));
250
+ const contractChanged = changedFiles.some(f => f.startsWith("inferno/"));
251
+
252
+ // Keyword-based drift detection on changed source files
253
+ const HEURISTICS = [
254
+ { kw: ["search"], id: "SearchItems" }, { kw: ["filter"], id: "FilterItems" },
255
+ { kw: ["auth", "login", "logout"], id: "Authentication" },
256
+ { kw: ["create", "add", "new"], id: "CreateItem" },
257
+ { kw: ["update", "edit", "patch"], id: "UpdateItem" },
258
+ { kw: ["delete", "remove"], id: "DeleteItem" },
259
+ { kw: ["list", "read", "fetch", "get"], id: "ReadItems" },
260
+ { kw: ["due", "deadline"], id: "SetDueDate" },
261
+ { kw: ["priority"], id: "SetPriority" },
262
+ { kw: ["complete", "toggle"], id: "ToggleComplete" },
263
+ { kw: ["export", "download"], id: "ExportData" },
264
+ { kw: ["import", "upload"], id: "ImportData" },
265
+ { kw: ["notify", "notification", "email"], id: "SendNotification" },
266
+ { kw: ["payment", "checkout", "stripe"], id: "ProcessPayment" },
267
+ ];
268
+
269
+ const capHits = new Map();
270
+ const registeredCaps = new Set(contract.capabilities || []);
271
+
272
+ for (const file of sourceFiles) {
273
+ const lower = file.toLowerCase();
274
+ for (const rule of HEURISTICS) {
275
+ if (rule.kw.some(k => lower.includes(k))) {
276
+ if (!capHits.has(rule.id)) capHits.set(rule.id, []);
277
+ capHits.get(rule.id).push(file);
278
+ }
279
+ }
280
+ }
281
+
282
+ const newCapSignals = [...capHits.entries()].filter(([id]) => !registeredCaps.has(id));
283
+ const existingCapSignals = [...capHits.entries()].filter(([id]) => registeredCaps.has(id));
284
+
285
+ const lines = [
286
+ `## infernoflow PR review — drift check vs \`${baseBranch}\``,
287
+ `Changed files: ${changedFiles.length} | Source: ${sourceFiles.length} | Styles: ${styleFiles.length} | Infra: ${infraFiles.length}`,
288
+ "",
289
+ ];
290
+
291
+ // Risk assessment
292
+ let riskLevel = "LOW";
293
+ if (newCapSignals.length > 0) riskLevel = "MEDIUM";
294
+ if (newCapSignals.length >= 3 || (newCapSignals.length >= 1 && !contractChanged)) riskLevel = "HIGH";
295
+
296
+ const riskEmoji = riskLevel === "HIGH" ? "🔴" : riskLevel === "MEDIUM" ? "🟡" : "🟢";
297
+ lines.push(`### ${riskEmoji} Drift risk: ${riskLevel}`);
298
+ lines.push("");
299
+
300
+ if (contractChanged) {
301
+ lines.push("✔ inferno/ contract files were updated in this PR — good practice.");
302
+ lines.push("");
303
+ } else if (sourceFiles.length > 0) {
304
+ lines.push("⚠ Source files changed but inferno/ contract was NOT updated.");
305
+ lines.push(" Consider running: infernoflow_run to check if capabilities need updating.");
306
+ lines.push("");
307
+ }
308
+
309
+ if (newCapSignals.length > 0) {
310
+ lines.push(`### Possible new capabilities (not in contract):`);
311
+ for (const [id, files] of newCapSignals.slice(0, 6)) {
312
+ lines.push(` - **${id}** — suggested by: ${files.slice(0,2).join(", ")}`);
313
+ }
314
+ lines.push("");
315
+ lines.push(`Suggested action: call infernoflow_run with task "review new capabilities: ${newCapSignals.slice(0,3).map(([id])=>id).join(', ')}"`);
316
+ lines.push("");
317
+ }
318
+
319
+ if (existingCapSignals.length > 0) {
320
+ lines.push(`### Existing capabilities touched:`);
321
+ for (const [id, files] of existingCapSignals.slice(0, 6)) {
322
+ lines.push(` - **${id}** — ${files.slice(0,2).join(", ")}`);
323
+ }
324
+ lines.push("");
325
+ }
326
+
327
+ if (styleFiles.length > 0) {
328
+ lines.push(`### Style changes (${styleFiles.length} files) — run infernoflow_scan_ui to check UI contract`);
329
+ styleFiles.slice(0, 5).forEach(f => lines.push(` - ${f}`));
330
+ lines.push("");
331
+ }
332
+
333
+ if (riskLevel === "LOW" && !newCapSignals.length) {
334
+ lines.push("✔ No new capability signals detected. Safe to merge (run infernoflow_check as final gate).");
335
+ }
336
+
337
+ return lines.join("\n");
338
+ }
339
+
340
+ function buildImplementPrompt(task, mode) {
341
+ const cwd = process.cwd();
342
+ const infernoDir = path.join(cwd, "inferno");
343
+ const contractPath = path.join(infernoDir, "contract.json");
344
+ const capsPath = path.join(infernoDir, "capabilities.json");
345
+ const profilePath = path.join(infernoDir, "developer-profile.json");
346
+
347
+ if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
348
+
349
+ const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
350
+ const caps = fs.existsSync(capsPath) ? JSON.parse(fs.readFileSync(capsPath, "utf8")) : {};
351
+ const profile = fs.existsSync(profilePath) ? JSON.parse(fs.readFileSync(profilePath, "utf8")) : {};
352
+
353
+ const capList = (caps.capabilities || []).map(c => ` - ${c.id}: ${c.title || c.id}`).join("\n");
354
+ const stack = profile.stack || {};
355
+ const stackLine = [stack.framework, stack.language, stack.projectType].filter(Boolean).join(" / ") || "unknown";
356
+ const namingStyle = profile.namingStyle || "PascalCase";
357
+
358
+ const cursorPrompt = `## Cursor Agent Implementation Prompt
359
+ Task: "${task}"
360
+ Project: ${contract.policyId} (${stackLine})
361
+ Naming convention: ${namingStyle}
362
+
363
+ ### Current capabilities
364
+ ${capList || " (none registered)"}
365
+
366
+ ### Implementation instructions
367
+ 1. Implement "${task}" following the existing code patterns in this project
368
+ 2. Use ${namingStyle} for any new identifiers, matching the existing capability naming
369
+ 3. Keep changes minimal — only touch files relevant to this task
370
+ 4. After implementing, call \`infernoflow_run\` with task "${task}" to update the contract
371
+ 5. Then call \`infernoflow_check\` to validate everything is in sync
372
+
373
+ ### Definition of done
374
+ - Feature works as described
375
+ - Contract updated via infernoflow_run → infernoflow_apply
376
+ - infernoflow_check passes`;
377
+
378
+ const genericPrompt = `## Implementation Prompt
379
+ Task: "${task}"
380
+ Project: ${contract.policyId}
381
+ Stack: ${stackLine}
382
+ Capabilities already in contract: ${(contract.capabilities || []).join(", ")}
383
+
384
+ Implement the task above. When done, run:
385
+ infernoflow suggest "${task}"
386
+ infernoflow check`;
387
+
388
+ if (mode === "cursor") return cursorPrompt;
389
+ if (mode === "generic") return genericPrompt;
390
+ return cursorPrompt + "\n\n---\n\n" + genericPrompt;
391
+ }
392
+
23
393
  function buildPrompt(task) {
24
394
  const infernoDir = path.join(process.cwd(), "inferno");
25
395
  const contractPath = path.join(infernoDir, "contract.json");
@@ -76,6 +446,14 @@ function handleTool(id, name, input) {
76
446
  if (input.intent) parts.push(`--intent "${input.intent}"`);
77
447
  if (input.working) parts.push(`--working "${input.working}"`);
78
448
  text = runCmd("context " + parts.join(" "));
449
+ } else if (name === "infernoflow_git_drift") {
450
+ text = detectGitDrift(input.sinceCommits || 1);
451
+ } else if (name === "infernoflow_implement") {
452
+ text = buildImplementPrompt(input.task, input.mode || "both");
453
+ } else if (name === "infernoflow_scan_ui") {
454
+ text = scanUi();
455
+ } else if (name === "infernoflow_review") {
456
+ text = reviewDrift(input.branch || "main");
79
457
  } else { return sendError(id, -32601, `Unknown tool: ${name}`); }
80
458
  sendResult(id, { content: [{ type: "text", text: text || "(no output)" }] });
81
459
  } catch (err) { sendError(id, -32000, err.message); }
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "infernoflow",
3
- "version": "0.10.17",
4
- "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
- "type": "module",
6
- "bin": {
7
- "infernoflow": "dist/bin/infernoflow.mjs"
8
- },
9
- "engines": {
10
- "node": ">=18"
11
- },
12
- "files": [
13
- "dist/bin",
14
- "dist/lib",
15
- "dist/templates",
16
- "README.md",
17
- "CHANGELOG.md"
18
- ],
19
- "scripts": {
20
- "test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs && node scripts/adopt-smoke.mjs && node scripts/pr-impact-smoke.mjs && node scripts/sync-smoke.mjs && node scripts/run-smoke.mjs",
21
- "test:help": "node bin/infernoflow.mjs --help",
22
- "build": "node build.mjs",
23
- "prepublishOnly": "node build.mjs"
24
- },
25
- "keywords": [
26
- "cli",
27
- "capabilities",
28
- "contract",
29
- "documentation",
30
- "ai",
31
- "liquid-code",
32
- "dx",
33
- "developer-tools"
34
- ],
35
- "author": "infernoflow",
36
- "license": "MIT",
37
- "repository": {
38
- "type": "git",
39
- "url": "git+https://github.com/ronmiz/infernoflow.git"
40
- },
41
- "homepage": "https://github.com/ronmiz/infernoflow#readme",
42
- "bugs": {
43
- "url": "https://github.com/ronmiz/infernoflow/issues"
44
- },
45
- "devDependencies": {
46
- "esbuild": "^0.28.0"
47
- }
48
- }
1
+ {
2
+ "name": "infernoflow",
3
+ "version": "0.10.19",
4
+ "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
+ "type": "module",
6
+ "bin": {
7
+ "infernoflow": "dist/bin/infernoflow.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "dist/bin",
14
+ "dist/lib",
15
+ "dist/templates",
16
+ "README.md",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "scripts": {
20
+ "test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs && node scripts/adopt-smoke.mjs && node scripts/pr-impact-smoke.mjs && node scripts/sync-smoke.mjs && node scripts/run-smoke.mjs",
21
+ "test:help": "node bin/infernoflow.mjs --help",
22
+ "build": "node build.mjs",
23
+ "prepublishOnly": "node build.mjs"
24
+ },
25
+ "keywords": [
26
+ "cli",
27
+ "capabilities",
28
+ "contract",
29
+ "documentation",
30
+ "ai",
31
+ "liquid-code",
32
+ "dx",
33
+ "developer-tools"
34
+ ],
35
+ "author": "infernoflow",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/ronmiz/infernoflow.git"
40
+ },
41
+ "homepage": "https://github.com/ronmiz/infernoflow#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/ronmiz/infernoflow/issues"
44
+ },
45
+ "devDependencies": {
46
+ "esbuild": "^0.28.0"
47
+ }
48
+ }