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 +1 -1
- package/dist/bin/infernoflow.mjs +47 -28
- package/dist/lib/adopters/angular.mjs +1 -0
- package/dist/lib/adopters/css.mjs +1 -0
- package/dist/lib/adopters/react.mjs +1 -0
- package/dist/lib/commands/adopt.mjs +11 -11
- package/dist/lib/commands/context.mjs +25 -18
- package/dist/lib/commands/generateSkills.mjs +114 -0
- package/dist/lib/commands/publish.mjs +21 -0
- package/dist/lib/commands/setup.mjs +4 -0
- package/dist/lib/commands/suggest.mjs +13 -13
- package/dist/lib/git/detect-drift.mjs +4 -0
- package/dist/lib/learning/adapt.mjs +9 -0
- package/dist/lib/learning/observe.mjs +1 -0
- package/dist/lib/learning/profile.mjs +2 -0
- package/dist/templates/cursor/inferno-mcp-server.mjs +378 -0
- package/package.json +48 -48
package/CHANGELOG.md
CHANGED
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync as
|
|
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
|
-
${
|
|
5
|
-
${
|
|
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
|
-
${
|
|
7
|
+
${t("Usage:")}
|
|
8
8
|
infernoflow <command> [options]
|
|
9
9
|
|
|
10
|
-
${
|
|
11
|
-
${
|
|
10
|
+
${t("Commands:")}
|
|
11
|
+
${y()}
|
|
12
12
|
|
|
13
|
-
${
|
|
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
|
-
${
|
|
39
|
+
${t("install-cursor-hooks options:")}
|
|
27
40
|
--force, -f Overwrite .cursor/hooks.json and hook scripts if they exist
|
|
28
41
|
|
|
29
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
52
|
-
${
|
|
53
|
-
${
|
|
54
|
-
${
|
|
55
|
-
${
|
|
56
|
-
${
|
|
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
|
-
${
|
|
59
|
-
${
|
|
60
|
-
${
|
|
61
|
-
${
|
|
62
|
-
${
|
|
63
|
-
${
|
|
64
|
-
${
|
|
65
|
-
|
|
66
|
-
Unknown command: ${n}`)),console.error(
|
|
67
|
-
`)),process.exit(1));const
|
|
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
|
|
2
|
-
`),
|
|
3
|
-
`)}function
|
|
4
|
-
`);for(const
|
|
5
|
-
`)};return["Project Structure Signals","=".repeat(56),
|
|
6
|
-
`)}function
|
|
7
|
-
`);const
|
|
8
|
-
`);const
|
|
9
|
-
`),
|
|
10
|
-
`)}const
|
|
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
|
-
`;
|
|
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
|
|
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
|
|
3
|
-
`+
|
|
4
|
-
`),l.existsSync(
|
|
5
|
-
`)),process.exit(1));const
|
|
6
|
-
`)),process.exit(1));let
|
|
7
|
-
`))),s&&(
|
|
8
|
-
`),
|
|
9
|
-
`+e.items.map(
|
|
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_"
|
|
13
|
-
`):"_No decisions recorded_",
|
|
14
|
-
`);if(
|
|
15
|
-
\u2714 Context written \u2192 `+
|
|
16
|
-
`+
|
|
17
|
-
`),console.log(" "+
|
|
18
|
-
`),
|
|
19
|
-
`)):(console.log(" "+
|
|
20
|
-
`))}
|
|
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
|
|
2
|
-
`),s=
|
|
3
|
-
`);return` 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
|
-
${
|
|
19
|
+
${d||" (none)"}
|
|
20
20
|
|
|
21
21
|
## Current scenarios
|
|
22
22
|
${s||" (none)"}
|
|
23
23
|
|
|
24
24
|
## Developer's description of what changed
|
|
25
|
-
"${
|
|
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
|
|
56
|
-
`),
|
|
57
|
-
`),
|
|
58
|
-
`),
|
|
59
|
-
`),
|
|
60
|
-
${
|
|
61
|
-
`),
|
|
62
|
-
`)}),
|
|
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.
|
|
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
|
+
}
|