infernoflow 0.10.23 → 0.10.26

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +13 -6
  2. package/dist/bin/infernoflow.mjs +135 -41
  3. package/dist/lib/adopters/angular.mjs +128 -1
  4. package/dist/lib/adopters/css.mjs +111 -1
  5. package/dist/lib/adopters/react.mjs +104 -1
  6. package/dist/lib/ai/ideDetection.mjs +31 -1
  7. package/dist/lib/ai/localProvider.mjs +88 -1
  8. package/dist/lib/ai/providerRouter.mjs +73 -1
  9. package/dist/lib/commands/adopt.mjs +869 -20
  10. package/dist/lib/commands/changelog.mjs +343 -21
  11. package/dist/lib/commands/check.mjs +179 -3
  12. package/dist/lib/commands/context.mjs +287 -31
  13. package/dist/lib/commands/diff.mjs +274 -5
  14. package/dist/lib/commands/docGate.mjs +81 -2
  15. package/dist/lib/commands/generateSkills.mjs +163 -38
  16. package/dist/lib/commands/implement.mjs +103 -7
  17. package/dist/lib/commands/init.mjs +394 -10
  18. package/dist/lib/commands/installCursorHooks.mjs +36 -1
  19. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
  20. package/dist/lib/commands/prImpact.mjs +157 -2
  21. package/dist/lib/commands/publish.mjs +293 -15
  22. package/dist/lib/commands/run.mjs +336 -8
  23. package/dist/lib/commands/setup.mjs +234 -4
  24. package/dist/lib/commands/status.mjs +172 -4
  25. package/dist/lib/commands/suggest.mjs +563 -21
  26. package/dist/lib/commands/syncAuto.mjs +96 -1
  27. package/dist/lib/cursorHooksInstall.mjs +60 -1
  28. package/dist/lib/draftToolingInstall.mjs +68 -7
  29. package/dist/lib/git/detect-drift.mjs +208 -4
  30. package/dist/lib/learning/adapt.mjs +101 -6
  31. package/dist/lib/learning/observe.mjs +114 -1
  32. package/dist/lib/learning/profile.mjs +212 -2
  33. package/dist/lib/ui/output.mjs +72 -6
  34. package/dist/lib/ui/prompts.mjs +147 -6
  35. package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
  36. package/package.json +47 -47
@@ -1,31 +1,287 @@
1
- import g from"node:fs";import b from"node:path";import{execSync as h}from"node:child_process";import{bold as I,gray as d,cyan as l,red as q,green as a,yellow as F}from"../ui/output.mjs";import{buildCursorImplementPrompt as wt,buildGenericImplementPrompt as yt}from"../ui/prompts.mjs";import{detectDrift as kt}from"../git/detect-drift.mjs";function Ct(e){try{const n=process.platform;if(n==="win32")h("clip",{input:e});else if(n==="darwin")h("pbcopy",{input:e});else try{h("xclip -selection clipboard",{input:e})}catch{h("xsel --clipboard --input",{input:e})}return!0}catch{return!1}}const w="inferno",P=b.join(w,"CONTEXT.md"),z=b.join(w,"context-state.json");function Q(e){try{return JSON.parse(g.readFileSync(e,"utf8"))}catch{return null}}function W(e){try{return g.readFileSync(e,"utf8")}catch{return null}}function Y(){const e=W(z);if(!e)return{};try{return JSON.parse(e)}catch{return{}}}function Z(e){g.writeFileSync(z,JSON.stringify(e,null,2),"utf8")}function E(e){return e?new Date(e).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}):"unknown"}function St(e,n){if(!e)return[];const i=[];let c=null;for(const r of e.split(`
2
- `))if(r.startsWith("## ")){if(c&&i.length<n&&i.push(c),i.length>=n)break;c={title:r.replace("## ","").trim(),items:[]}}else c&&r.startsWith("- ")&&c.items.push(r.replace("- ","").trim());return c&&i.length<n&&i.push(c),i.filter(r=>r.items.length>0)}async function bt(e){const n=o=>e.includes(o),i=o=>{const u=e.indexOf(o);return u!==-1&&e[u+1]?e[u+1]:null},c=i("--intent")||i("-i"),r=i("--working")||i("-w"),$=i("--decision")||i("-d"),tt=n("--show")||n("-s"),G=n("--copy")||n("-c"),et=n("--cursor"),nt=n("--copilot"),ot=n("--reset"),it=n("--watch"),N=n("--auto-commit")||n("--auto-push"),v=n("--auto-push"),R=parseInt(i("--interval")||"30",10)*1e3;console.log(`
3
- `+I("\uFFFD\uFFFD\uFFFD infernoflow \u2014 context")),console.log(" "+"\u2500".repeat(50)+`
4
- `),g.existsSync(w)||(console.error(q(" \u2718 inferno/ not found")),console.error(d(` \u2192 Run: infernoflow init
5
- `)),process.exit(1));const f=Q(b.join(w,"contract.json")),D=Q(b.join(w,"capabilities.json")),st=W(b.join(w,"CHANGELOG.md"));(!f||!D)&&(console.error(q(` \u2718 Missing contract.json or capabilities.json
6
- `)),process.exit(1));let t=Y();ot&&(t={},console.log(F(` \u26A0 State reset
7
- `))),c&&(t.intent=c,t.intentUpdated=new Date().toISOString(),console.log(a(' \u2714 Intent saved: "'+c+'"'))),r&&(t.working=r,t.workingUpdated=new Date().toISOString(),console.log(a(' \u2714 Working on: "'+r+'"'))),$&&(t.decisions||(t.decisions=[]),t.decisions.push({text:$,date:new Date().toISOString()}),console.log(a(' \u2714 Decision recorded: "'+$+'"'))),(c||r||$)&&Z(t);const j=D.capabilities||[],A=j.length===(f.capabilities||[]).length,U=St(st,3),T=String(f.policyVersion).replace(/^v/i,""),ct=new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}),B=A?"\u2713 validated":"\u26A0 out of sync",L=t.intent||"describe the exact task to implement",J={task:L,contract:f,caps:D,scenarios:[],state:t},rt=wt(J),lt=yt(J),at=j.map(o=>"- **"+o.id+"** \u2014 "+o.title).join(`
8
- `),dt=U.length>0?U.map(o=>"### "+o.title+`
9
- `+o.items.map(u=>" - "+u).join(`
10
- `)).join(`
11
-
12
- `):"_No recent changes_",pt=t.intent?t.intent+" _("+E(t.intentUpdated)+")_":'_Not set \u2014 run: infernoflow context --intent "..."_',gt=t.working?t.working+" _("+E(t.workingUpdated)+")_":'_Not set \u2014 run: infernoflow context --working "..."_',ut=t.decisions&&t.decisions.length>0?t.decisions.slice(-5).map(o=>"- "+o.text+" _("+E(o.date)+")_").join(`
13
- `):"_No decisions recorded_",x=["# Project Context \u2014 "+f.policyId+" v"+T,"> Generated by infernoflow | "+ct+" | "+B,"","---","","## What this system does","",at,"","---","","## Recent changes","",dt,"","---","","## Current state","","- **Capabilities:** "+j.length,"- **Version:** v"+T,"- **Sync:** "+B,"","---","","## What I am working on right now","",gt,"","---","","## Intent \u2014 what I want to build next","",pt,"","---","","## Decisions & notes","",ut,"","---","","## Implementation Prompt Seed","","Use this to start coding immediately with an agent:","","```bash",`infernoflow implement "${L}" --mode both`,"```","","### Cursor Agent Prompt","","```text",rt,"```","","### Generic Agent Prompt","","```text",lt,"```","","---","_Paste this block at the start of any new AI session._"].join(`
14
- `);if(tt||(g.writeFileSync(P,x,"utf8"),console.log(a(`
15
- \u2714 Context written \u2192 `+P))),G){const o=Ct(x);console.log(o?a(" \u2714 Copied to clipboard \u2014 paste with Ctrl+V"):F(" \u26A0 Clipboard copy failed \u2014 open inferno/CONTEXT.md manually"))}if(et&&(g.writeFileSync(".cursorrules",x,"utf8"),console.log(a(" \u2714 Written to .cursorrules \u2014 Cursor loads this automatically"))),nt&&(g.existsSync(".github")||g.mkdirSync(".github"),g.writeFileSync(".github/copilot-instructions.md",x,"utf8"),console.log(a(" \u2714 Written to .github/copilot-instructions.md \u2014 Copilot loads this automatically"))),console.log(`
16
- `+I("Context Summary")),console.log(" "+"\u2500".repeat(50)),console.log(" Project "+f.policyId+" \u2014 v"+T),console.log(" Capabilities "+j.length+" registered"),console.log(" Sync "+(A?a("\u2713 in sync"):F("\u26A0 check needed"))),console.log(" Working on "+(t.working?l(t.working):d("not set"))),console.log(" Intent "+(t.intent?l(t.intent):d("not set"))),console.log(" Decisions "+(t.decisions?t.decisions.length:0)+` recorded
17
- `),console.log(" "+I("Implementation Prompt")),console.log(" "+l("\u2192")+" Run "+l(`infernoflow implement "${L}" --mode both`)+`
18
- `),G?(console.log(" "+I("Ready to use:")),console.log(" "+l("\u2192")+" Paste into Claude / Cursor / Copilot with "+l("Ctrl+V")+`
19
- `)):(console.log(" "+I("Ready to use:")),console.log(" "+l("1.")+" Open "+l("inferno/CONTEXT.md")),console.log(" "+l("2.")+" Copy everything"),console.log(" "+l("3.")+" Paste at the start of your next AI session"),console.log(" "+d(" tip: use --copy to skip steps 1-2 automatically")+`
20
- `)),it){let _=function(p){try{return h(p,{cwd:process.cwd(),encoding:"utf8",stdio:["ignore","pipe","pipe"]}),!0}catch{return!1}},X=function(p){try{return h(`git status --porcelain "${p}"`,{cwd:process.cwd(),encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()===""}catch{return!0}},H=function(p,s,O){const k=`chore: update context [${s.length>0?s.slice(0,3).join(", "):`${O} files`}]`;return _(`git add "${p}"`)?X(p)?{ok:!1,reason:"nothing to commit"}:_(`git commit -m "${k}"`)?{ok:!0,msg:k}:{ok:!1,reason:"git commit failed (lock?)"}:{ok:!1,reason:"git add failed"}},K=function(){return _("git push")};var It=_,Ft=X,$t=H,jt=K;const o=v?"auto-push":N?"auto-commit":"watch";console.log(" "+l("\u{1F441} Watch mode active")+d(` \u2014 polling every ${R/1e3}s`+(v?" \xB7 will commit + push on change":N?" \xB7 will commit on change":""))),console.log(" "+d(`Press Ctrl+C to stop
21
- `));let u="",V=null;const M=async()=>{try{const p=process.cwd(),s=kt(p,{sinceCommits:1}),O=s.changedFiles.sort().join("|");if(O===u||(u=O,s.changedFiles.length===0))return;const y=s.affectedCapabilities.map(S=>S.id),k=y.length>0?`Working on: ${y.join(", ")} (${s.changedFiles.length} files changed)`:`${s.changedFiles.length} files changed \u2014 no capability match yet`,C=Y();if(C.working!==k){C.working=k,C.workingUpdated=new Date().toISOString(),Z(C),await bt(e.filter(m=>m!=="--watch"&&m!=="--auto-commit"&&m!=="--auto-push"));const S=W(P),ft=new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",second:"2-digit"});if(process.stderr.write(`
22
- ${a("\u2714")} [${ft}] Context updated \u2014 ${y.length} capabilities affected
23
- ${d(s.changedFiles.slice(0,3).join(", ")+(s.changedFiles.length>3?` +${s.changedFiles.length-3} more`:""))}
24
- `),N&&S!==V){V=S;const m=H(P,y,s.changedFiles.length);if(m.ok){if(process.stderr.write(` ${a("\u2714")} Committed: ${d(m.msg)}
25
- `),v){const ht=K();process.stderr.write(ht?` ${a("\u2714")} Pushed to origin
26
- `:` ${F("\u26A0")} Push failed \u2014 will retry next change
27
- `)}}else process.stderr.write(` ${F("\u26A0")} Commit skipped: ${d(m.reason)}
28
- `)}}}catch{}};await M();const mt=setInterval(M,R);process.on("SIGINT",()=>{clearInterval(mt),process.stderr.write(`
29
- `+d(`Watch stopped.
30
-
31
- `)),process.exit(0)}),await new Promise(()=>{})}}export{bt as contextCommand};
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { bold, gray, cyan, red, green, yellow } from "../ui/output.mjs";
5
+ import { buildCursorImplementPrompt, buildGenericImplementPrompt } from "../ui/prompts.mjs";
6
+ import { detectDrift } from "../git/detect-drift.mjs";
7
+
8
+ function copyToClipboard(text) {
9
+ try {
10
+ const p = process.platform;
11
+ if (p === "win32") execSync("clip", { input: text });
12
+ else if (p === "darwin") execSync("pbcopy", { input: text });
13
+ else { try { execSync("xclip -selection clipboard", { input: text }); } catch { execSync("xsel --clipboard --input", { input: text }); } }
14
+ return true;
15
+ } catch { return false; }
16
+ }
17
+
18
+ const INFERNO_DIR = "inferno";
19
+ const CONTEXT_FILE = path.join(INFERNO_DIR, "CONTEXT.md");
20
+ const STATE_FILE = path.join(INFERNO_DIR, "context-state.json");
21
+
22
+ function readJSON(f) { try { return JSON.parse(fs.readFileSync(f,"utf8")); } catch { return null; } }
23
+ function readFile(f) { try { return fs.readFileSync(f,"utf8"); } catch { return null; } }
24
+ function loadState() { const r=readFile(STATE_FILE); if(!r) return {}; try { return JSON.parse(r); } catch { return {}; } }
25
+ function saveState(s) { fs.writeFileSync(STATE_FILE,JSON.stringify(s,null,2),"utf8"); }
26
+ function fmtDate(iso) { if(!iso) return "unknown"; return new Date(iso).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}); }
27
+ function parseChangelog(txt,max) {
28
+ if(!txt) return [];
29
+ const entries=[]; let cur=null;
30
+ for(const line of txt.split("\n")) {
31
+ if(line.startsWith("## ")) { if(cur&&entries.length<max) entries.push(cur); if(entries.length>=max) break; cur={title:line.replace("## ","").trim(),items:[]}; }
32
+ else if(cur&&line.startsWith("- ")) cur.items.push(line.replace("- ","").trim());
33
+ }
34
+ if(cur&&entries.length<max) entries.push(cur);
35
+ return entries.filter(e=>e.items.length>0);
36
+ }
37
+
38
+ export async function contextCommand(args) {
39
+ const has = (f) => args.includes(f);
40
+ const flag = (f) => { const i=args.indexOf(f); return i!==-1&&args[i+1]?args[i+1]:null; };
41
+
42
+ const intent = flag("--intent") || flag("-i");
43
+ const working = flag("--working") || flag("-w");
44
+ const decision = flag("--decision") || flag("-d");
45
+ const showOnly = has("--show") || has("-s");
46
+ const copyFlag = has("--copy") || has("-c");
47
+ const cursorFlag = has("--cursor");
48
+ const copilotFlag = has("--copilot");
49
+ const resetFlag= has("--reset");
50
+ const watchFlag = has("--watch");
51
+ const autoCommit = has("--auto-commit") || has("--auto-push");
52
+ const autoPush = has("--auto-push");
53
+ const watchInterval = parseInt(flag("--interval") || "30", 10) * 1000;
54
+
55
+ console.log("\n "+bold("��� infernoflow — context"));
56
+ console.log(" "+"─".repeat(50)+"\n");
57
+
58
+ if(!fs.existsSync(INFERNO_DIR)){
59
+ console.error(red(" ✘ inferno/ not found"));
60
+ console.error(gray(" → Run: infernoflow init\n"));
61
+ process.exit(1);
62
+ }
63
+
64
+ const contract = readJSON(path.join(INFERNO_DIR,"contract.json"));
65
+ const capabilities = readJSON(path.join(INFERNO_DIR,"capabilities.json"));
66
+ const changelog = readFile(path.join(INFERNO_DIR,"CHANGELOG.md"));
67
+
68
+ if(!contract||!capabilities){
69
+ console.error(red(" ✘ Missing contract.json or capabilities.json\n"));
70
+ process.exit(1);
71
+ }
72
+
73
+ let state = loadState();
74
+ if(resetFlag){ state={}; console.log(yellow(" ⚠ State reset\n")); }
75
+ if(intent) { state.intent=intent; state.intentUpdated=new Date().toISOString(); console.log(green(' ✔ Intent saved: "'+intent+'"')); }
76
+ if(working) { state.working=working; state.workingUpdated=new Date().toISOString(); console.log(green(' ✔ Working on: "'+working+'"')); }
77
+ if(decision) { if(!state.decisions) state.decisions=[]; state.decisions.push({text:decision,date:new Date().toISOString()}); console.log(green(' ✔ Decision recorded: "'+decision+'"')); }
78
+ if(intent||working||decision) saveState(state);
79
+
80
+ const capList = capabilities.capabilities||[];
81
+ const allInSync = capList.length===(contract.capabilities||[]).length;
82
+ const recent = parseChangelog(changelog,3);
83
+ const version = String(contract.policyVersion).replace(/^v/i,"");
84
+ const now = new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"});
85
+ const syncBadge = allInSync?"✓ validated":"⚠ out of sync";
86
+ const implementTask = state.intent || "describe the exact task to implement";
87
+ const implementInput = { task: implementTask, contract, caps: capabilities, scenarios: [], state };
88
+ const cursorPrompt = buildCursorImplementPrompt(implementInput);
89
+ const genericPrompt = buildGenericImplementPrompt(implementInput);
90
+
91
+ const capLines = capList.map(c=>"- **"+c.id+"** — "+c.title).join("\n");
92
+ const chgLines = recent.length>0 ? recent.map(e=>"### "+e.title+"\n"+e.items.map(i=>" - "+i).join("\n")).join("\n\n") : "_No recent changes_";
93
+ const intentLine = state.intent ? state.intent+" _("+fmtDate(state.intentUpdated)+")_" : "_Not set — run: infernoflow context --intent \"...\"_";
94
+ const workingLine = state.working ? state.working+" _("+fmtDate(state.workingUpdated)+")_" : "_Not set — run: infernoflow context --working \"...\"_";
95
+ const decLines = state.decisions&&state.decisions.length>0 ? state.decisions.slice(-5).map(d=>"- "+d.text+" _("+fmtDate(d.date)+")_").join("\n") : "_No decisions recorded_";
96
+
97
+ const md = [
98
+ "# Project Context — "+contract.policyId+" v"+version,
99
+ "> Generated by infernoflow | "+now+" | "+syncBadge,
100
+ "","---","",
101
+ "## What this system does","",capLines,"","---","",
102
+ "## Recent changes","",chgLines,"","---","",
103
+ "## Current state","",
104
+ "- **Capabilities:** "+capList.length,
105
+ "- **Version:** v"+version,
106
+ "- **Sync:** "+syncBadge,
107
+ "","---","",
108
+ "## What I am working on right now","",workingLine,"","---","",
109
+ "## Intent — what I want to build next","",intentLine,"","---","",
110
+ "## Decisions & notes","",decLines,"","---",
111
+ "",
112
+ "## Implementation Prompt Seed","",
113
+ "Use this to start coding immediately with an agent:","",
114
+ "```bash",
115
+ `infernoflow implement "${implementTask}" --mode both`,
116
+ "```",
117
+ "",
118
+ "### Cursor Agent Prompt","",
119
+ "```text",
120
+ cursorPrompt,
121
+ "```",
122
+ "",
123
+ "### Generic Agent Prompt","",
124
+ "```text",
125
+ genericPrompt,
126
+ "```",
127
+ "",
128
+ "---",
129
+ "_Paste this block at the start of any new AI session._"
130
+ ].join("\n");
131
+
132
+ if(!showOnly){ fs.writeFileSync(CONTEXT_FILE,md,"utf8"); console.log(green("\n ✔ Context written → "+CONTEXT_FILE)); }
133
+
134
+ if(copyFlag){
135
+ const ok=copyToClipboard(md);
136
+ console.log(ok ? green(" ✔ Copied to clipboard — paste with Ctrl+V") : yellow(" ⚠ Clipboard copy failed — open inferno/CONTEXT.md manually"));
137
+ }
138
+
139
+ if (cursorFlag) {
140
+ fs.writeFileSync(".cursorrules", md, "utf8");
141
+ console.log(green(" ✔ Written to .cursorrules — Cursor loads this automatically"));
142
+ }
143
+ if (copilotFlag) {
144
+ if (!fs.existsSync(".github")) fs.mkdirSync(".github");
145
+ fs.writeFileSync(".github/copilot-instructions.md", md, "utf8");
146
+ console.log(green(" ✔ Written to .github/copilot-instructions.md — Copilot loads this automatically"));
147
+ }
148
+ console.log("\n "+bold("Context Summary"));
149
+ console.log(" "+"─".repeat(50));
150
+ console.log(" Project "+contract.policyId+" — v"+version);
151
+ console.log(" Capabilities "+capList.length+" registered");
152
+ console.log(" Sync "+(allInSync?green("✓ in sync"):yellow("⚠ check needed")));
153
+ console.log(" Working on "+(state.working?cyan(state.working):gray("not set")));
154
+ console.log(" Intent "+(state.intent ?cyan(state.intent) :gray("not set")));
155
+ console.log(" Decisions "+(state.decisions?state.decisions.length:0)+" recorded\n");
156
+ console.log(" "+bold("Implementation Prompt"));
157
+ console.log(" "+cyan("→")+" Run "+cyan(`infernoflow implement "${implementTask}" --mode both`)+"\n");
158
+
159
+ if(copyFlag){
160
+ console.log(" "+bold("Ready to use:"));
161
+ console.log(" "+cyan("→")+" Paste into Claude / Cursor / Copilot with "+cyan("Ctrl+V")+"\n");
162
+ } else {
163
+ console.log(" "+bold("Ready to use:"));
164
+ console.log(" "+cyan("1.")+" Open "+cyan("inferno/CONTEXT.md"));
165
+ console.log(" "+cyan("2.")+" Copy everything");
166
+ console.log(" "+cyan("3.")+" Paste at the start of your next AI session");
167
+ console.log(" "+gray(" tip: use --copy to skip steps 1-2 automatically")+"\n");
168
+ }
169
+
170
+ // ── Watch mode ────────────────────────────────────────────────────────────
171
+ if (watchFlag) {
172
+ const modeLabel = autoPush ? "auto-push" : autoCommit ? "auto-commit" : "watch";
173
+ console.log(" " + cyan("👁 Watch mode active") + gray(
174
+ ` — polling every ${watchInterval / 1000}s` +
175
+ (autoPush ? " · will commit + push on change" : autoCommit ? " · will commit on change" : "")
176
+ ));
177
+ console.log(" " + gray("Press Ctrl+C to stop\n"));
178
+
179
+ let lastChangedFiles = "";
180
+ let lastCommittedContent = null;
181
+
182
+ // ── git helpers ──────────────────────────────────────────────────────
183
+ function gitRun(cmd) {
184
+ try {
185
+ execSync(cmd, { cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
186
+ return true;
187
+ } catch { return false; }
188
+ }
189
+
190
+ function gitIsCleanFor(filePath) {
191
+ // Returns true if the file has no staged/unstaged changes (nothing to commit)
192
+ try {
193
+ const out = execSync(`git status --porcelain "${filePath}"`, {
194
+ cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
195
+ }).trim();
196
+ return out === "";
197
+ } catch { return true; }
198
+ }
199
+
200
+ function commitContext(contextPath, affectedCaps, changedCount) {
201
+ const caps = affectedCaps.length > 0 ? affectedCaps.slice(0, 3).join(", ") : `${changedCount} files`;
202
+ const msg = `chore: update context [${caps}]`;
203
+ const staged = gitRun(`git add "${contextPath}"`);
204
+ if (!staged) return { ok: false, reason: "git add failed" };
205
+ if (gitIsCleanFor(contextPath)) return { ok: false, reason: "nothing to commit" };
206
+ const committed = gitRun(`git commit -m "${msg}"`);
207
+ if (!committed) return { ok: false, reason: "git commit failed (lock?)" };
208
+ return { ok: true, msg };
209
+ }
210
+
211
+ function pushContext() {
212
+ const ok = gitRun("git push");
213
+ return ok;
214
+ }
215
+
216
+ // ── poll loop ────────────────────────────────────────────────────────
217
+ const poll = async () => {
218
+ try {
219
+ const cwd = process.cwd();
220
+ const drift = detectDrift(cwd, { sinceCommits: 1 });
221
+ const changedKey = drift.changedFiles.sort().join("|");
222
+
223
+ if (changedKey === lastChangedFiles) return;
224
+ lastChangedFiles = changedKey;
225
+ if (drift.changedFiles.length === 0) return;
226
+
227
+ // Update "working" field
228
+ const affected = drift.affectedCapabilities.map(c => c.id);
229
+ const newWorking = affected.length > 0
230
+ ? `Working on: ${affected.join(", ")} (${drift.changedFiles.length} files changed)`
231
+ : `${drift.changedFiles.length} files changed — no capability match yet`;
232
+
233
+ const currentState = loadState();
234
+ if (currentState.working !== newWorking) {
235
+ currentState.working = newWorking;
236
+ currentState.workingUpdated = new Date().toISOString();
237
+ saveState(currentState);
238
+
239
+ // Regenerate CONTEXT.md silently
240
+ await contextCommand(args.filter(a => a !== "--watch" && a !== "--auto-commit" && a !== "--auto-push"));
241
+
242
+ const newContent = readFile(CONTEXT_FILE);
243
+ const ts = new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
244
+
245
+ process.stderr.write(
246
+ `\n ${green("✔")} [${ts}] Context updated — ${affected.length} capabilities affected\n` +
247
+ ` ${gray(drift.changedFiles.slice(0, 3).join(", ") + (drift.changedFiles.length > 3 ? ` +${drift.changedFiles.length - 3} more` : ""))}\n`
248
+ );
249
+
250
+ // Auto-commit if enabled and content actually changed
251
+ if (autoCommit && newContent !== lastCommittedContent) {
252
+ lastCommittedContent = newContent;
253
+ const result = commitContext(CONTEXT_FILE, affected, drift.changedFiles.length);
254
+ if (result.ok) {
255
+ process.stderr.write(` ${green("✔")} Committed: ${gray(result.msg)}\n`);
256
+ if (autoPush) {
257
+ const pushed = pushContext();
258
+ process.stderr.write(
259
+ pushed
260
+ ? ` ${green("✔")} Pushed to origin\n`
261
+ : ` ${yellow("⚠")} Push failed — will retry next change\n`
262
+ );
263
+ }
264
+ } else {
265
+ process.stderr.write(` ${yellow("⚠")} Commit skipped: ${gray(result.reason)}\n`);
266
+ }
267
+ }
268
+ }
269
+ } catch {
270
+ // Silent — watch mode never crashes
271
+ }
272
+ };
273
+
274
+ // Poll immediately then on interval
275
+ await poll();
276
+ const timer = setInterval(poll, watchInterval);
277
+
278
+ process.on("SIGINT", () => {
279
+ clearInterval(timer);
280
+ process.stderr.write("\n " + gray("Watch stopped.\n\n"));
281
+ process.exit(0);
282
+ });
283
+
284
+ // Prevent Node from exiting
285
+ await new Promise(() => {});
286
+ }
287
+ }
@@ -1,5 +1,274 @@
1
- import*as u from"node:fs";import*as v from"node:path";import{execSync as C}from"node:child_process";import{header as N,ok as J,fail as m,bold as d,cyan as k,gray as c,green as $,red as y,yellow as x}from"../ui/output.mjs";function h(n,e){try{return C(n,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}catch{return null}}function D(n){const e=h("git describe --tags --abbrev=0",n);return e||null}function j(n,e,o){return h(`git show "${n}:${e}"`,o)}function z(n){return h("git rev-parse --abbrev-ref HEAD",n)||"HEAD"}function O(n,e){const o=h(`git log -1 --format=%ci "${e}"`,n),t=o?o.slice(0,10):null;return t?`${e} ${c("("+t+")")}`:e}function b(n){if(!n)return null;try{return(JSON.parse(n).capabilities||[]).map(t=>typeof t=="string"?{id:t,title:t}:{id:t.id||t,title:t.title||t.id||String(t),since:t.since,status:t.status})}catch{return null}}function _(n,e,o){const t=j(n,`${e}/capabilities.json`,o);if(t)return b(t);const g=j(n,`${e}/contract.json`,o);return b(g)}function E(n){const e=v.join(n,"capabilities.json"),o=v.join(n,"contract.json");return u.existsSync(e)?b(u.readFileSync(e,"utf8")):u.existsSync(o)?b(u.readFileSync(o,"utf8")):null}function R(n,e){const o=new Map(n.map(i=>[i.id,i])),t=new Map(e.map(i=>[i.id,i])),g=e.filter(i=>!o.has(i.id)),s=n.filter(i=>!t.has(i.id)),l=[];for(const i of e){const f=o.get(i.id);if(!f)continue;const a=[];f.title!==i.title&&a.push({field:"title",from:f.title,to:i.title}),f.status!==i.status&&(f.status||i.status)&&a.push({field:"status",from:f.status||"\u2014",to:i.status||"\u2014"}),a.length&&l.push({id:i.id,changes:a})}return{added:g,removed:s,changed:l}}function H(n){if(n.length){console.log(`
2
- ${d($("+ Added"))} ${c("("+n.length+")")}`);for(const e of n){const o=e.since?c(" since "+e.since):"";console.log(` ${$("+")} ${d(e.id)} ${c(e.title)}${o}`)}}}function M(n){if(n.length){console.log(`
3
- ${d(y("- Removed"))} ${c("("+n.length+")")}`);for(const e of n)console.log(` ${y("-")} ${d(e.id)} ${c(e.title)}`)}}function F(n){if(n.length){console.log(`
4
- ${d(x("~ Changed"))} ${c("("+n.length+")")}`);for(const e of n){console.log(` ${x("~")} ${d(e.id)}`);for(const o of e.changes)console.log(` ${c(o.field+":")} ${y(o.from)} \u2192 ${$(o.to)}`)}}}function U(n){n!==0&&console.log(`
5
- ${c(" Unchanged "+n)}`)}function w(n,e){const o=[];n.added.length&&o.push($("+"+n.added.length+" added")),n.removed.length&&o.push(y("-"+n.removed.length+" removed")),n.changed.length&&o.push(x("~"+n.changed.length+" changed")),o.length||o.push(c("no changes")),console.log(` ${o.join(" ")} ${c("vs "+e)}`)}async function G(n){const e=n.slice(1),o=e.includes("--json"),t=e.includes("--summary"),g=e.indexOf("--ref");let s=g!==-1?e[g+1]:null;const l=process.cwd(),i=v.join(l,"inferno"),f="inferno";o||N("diff"),u.existsSync(i)||(o&&(console.log(JSON.stringify({ok:!1,error:"inferno_not_found"})),process.exit(1)),m("inferno/ not found","Run: infernoflow init"),process.exit(1)),s||(s=D(l),s||(h("git rev-parse HEAD~1",l)?s="HEAD~1":(o&&(console.log(JSON.stringify({ok:!1,error:"no_ref",hint:"No git tags found and no parent commit. Use --ref <commit>"})),process.exit(1)),m("No git tags found","Create a tag first: git tag v0.1.0 or use --ref <commit>"),process.exit(1))));const a=E(i);a||(o&&(console.log(JSON.stringify({ok:!1,error:"no_capabilities_found"})),process.exit(1)),m("No capabilities.json or contract.json found in inferno/"),process.exit(1));const p=_(s,f,l);p||(o&&(console.log(JSON.stringify({ok:!1,error:"ref_not_found",ref:s,hint:"Does inferno/capabilities.json exist at that ref?"})),process.exit(1)),m(`Could not read capabilities at ${s}`,"The inferno/ directory may not exist at that ref"),process.exit(1));const r=R(p,a),S=a.length-r.added.length-r.changed.length;if(o){console.log(JSON.stringify({ok:!0,ref:s,current:a.length,previous:p.length,added:r.added,removed:r.removed,changed:r.changed,unchanged:S},null,2));return}const A=O(l,s);if(console.log(),console.log(` Comparing ${d(k("current"))} vs ${d(A)}`),console.log(` ${c(a.length+" capabilities now / "+p.length+" before")}`),t){w(r,s),console.log();return}if(!(r.added.length||r.removed.length||r.changed.length)){console.log(),J("No capability changes since "+s),console.log();return}H(r.added),M(r.removed),F(r.changed),U(S),console.log(),w(r,s),console.log()}export{G as diffCommand};
1
+ /**
2
+ * infernoflow diff
3
+ *
4
+ * Compare capabilities between the current working tree and a git ref.
5
+ * Defaults to the most recent git tag; falls back to HEAD~1 if no tags exist.
6
+ *
7
+ * Usage:
8
+ * infernoflow diff # vs last tag
9
+ * infernoflow diff --ref v0.10.18 # vs specific tag / commit
10
+ * infernoflow diff --ref HEAD~5 # vs 5 commits ago
11
+ * infernoflow diff --json # machine-readable output
12
+ * infernoflow diff --summary # one-liner count only
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { execSync } from "node:child_process";
18
+ import { header, ok, fail, warn, info, bold, cyan, gray, green, red, yellow } from "../ui/output.mjs";
19
+
20
+ // ── git helpers ──────────────────────────────────────────────────────────────
21
+
22
+ function capture(cmd, cwd) {
23
+ try {
24
+ return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function lastTag(cwd) {
31
+ // Try to find the most recent reachable tag
32
+ const tag = capture("git describe --tags --abbrev=0", cwd);
33
+ if (tag) return tag;
34
+ return null;
35
+ }
36
+
37
+ function fileAtRef(ref, relPath, cwd) {
38
+ // Returns file content at a git ref, or null if not found
39
+ const content = capture(`git show "${ref}:${relPath}"`, cwd);
40
+ return content;
41
+ }
42
+
43
+ function currentBranch(cwd) {
44
+ return capture("git rev-parse --abbrev-ref HEAD", cwd) || "HEAD";
45
+ }
46
+
47
+ function refDescription(cwd, ref) {
48
+ // Human label: "v0.10.18 (2026-04-10)" or "HEAD~1"
49
+ const date = capture(`git log -1 --format=%ci "${ref}"`, cwd);
50
+ const short = date ? date.slice(0, 10) : null;
51
+ return short ? `${ref} ${gray("(" + short + ")")}` : ref;
52
+ }
53
+
54
+ // ── capability helpers ───────────────────────────────────────────────────────
55
+
56
+ function parseCaps(jsonText) {
57
+ // Accepts capabilities.json OR contract.json — both have a "capabilities" array
58
+ if (!jsonText) return null;
59
+ try {
60
+ const obj = JSON.parse(jsonText);
61
+ const raw = obj.capabilities || [];
62
+ // capabilities.json: array of objects { id, title, ... }
63
+ // contract.json: array of id strings
64
+ return raw.map(c => {
65
+ if (typeof c === "string") return { id: c, title: c };
66
+ return { id: c.id || c, title: c.title || c.id || String(c), since: c.since, status: c.status };
67
+ });
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function loadCapsAtRef(ref, infernoRelDir, cwd) {
74
+ // Try capabilities.json first, fall back to contract.json
75
+ const capsJson = fileAtRef(ref, `${infernoRelDir}/capabilities.json`, cwd);
76
+ if (capsJson) return parseCaps(capsJson);
77
+ const contractJson = fileAtRef(ref, `${infernoRelDir}/contract.json`, cwd);
78
+ return parseCaps(contractJson);
79
+ }
80
+
81
+ function loadCapsFromDisk(infernoDir) {
82
+ const capsPath = path.join(infernoDir, "capabilities.json");
83
+ const contractPath = path.join(infernoDir, "contract.json");
84
+ if (fs.existsSync(capsPath)) {
85
+ return parseCaps(fs.readFileSync(capsPath, "utf8"));
86
+ }
87
+ if (fs.existsSync(contractPath)) {
88
+ return parseCaps(fs.readFileSync(contractPath, "utf8"));
89
+ }
90
+ return null;
91
+ }
92
+
93
+ // ── diff logic ───────────────────────────────────────────────────────────────
94
+
95
+ function diffCaps(before, after) {
96
+ const beforeMap = new Map(before.map(c => [c.id, c]));
97
+ const afterMap = new Map(after.map(c => [c.id, c]));
98
+
99
+ const added = after.filter(c => !beforeMap.has(c.id));
100
+ const removed = before.filter(c => !afterMap.has(c.id));
101
+
102
+ const changed = [];
103
+ for (const c of after) {
104
+ const old = beforeMap.get(c.id);
105
+ if (!old) continue;
106
+ const changes = [];
107
+ if (old.title !== c.title) changes.push({ field: "title", from: old.title, to: c.title });
108
+ if (old.status !== c.status && (old.status || c.status)) changes.push({ field: "status", from: old.status || "—", to: c.status || "—" });
109
+ if (changes.length) changed.push({ id: c.id, changes });
110
+ }
111
+
112
+ return { added, removed, changed };
113
+ }
114
+
115
+ // ── rendering ────────────────────────────────────────────────────────────────
116
+
117
+ function renderAdded(caps) {
118
+ if (!caps.length) return;
119
+ console.log(`\n ${bold(green("+ Added"))} ${gray("(" + caps.length + ")")}`);
120
+ for (const c of caps) {
121
+ const since = c.since ? gray(" since " + c.since) : "";
122
+ console.log(` ${green("+")} ${bold(c.id)} ${gray(c.title)}${since}`);
123
+ }
124
+ }
125
+
126
+ function renderRemoved(caps) {
127
+ if (!caps.length) return;
128
+ console.log(`\n ${bold(red("- Removed"))} ${gray("(" + caps.length + ")")}`);
129
+ for (const c of caps) {
130
+ console.log(` ${red("-")} ${bold(c.id)} ${gray(c.title)}`);
131
+ }
132
+ }
133
+
134
+ function renderChanged(items) {
135
+ if (!items.length) return;
136
+ console.log(`\n ${bold(yellow("~ Changed"))} ${gray("(" + items.length + ")")}`);
137
+ for (const item of items) {
138
+ console.log(` ${yellow("~")} ${bold(item.id)}`);
139
+ for (const ch of item.changes) {
140
+ console.log(` ${gray(ch.field + ":")} ${red(ch.from)} → ${green(ch.to)}`);
141
+ }
142
+ }
143
+ }
144
+
145
+ function renderUnchanged(count) {
146
+ if (count === 0) return;
147
+ console.log(`\n ${gray(" Unchanged " + count)}`);
148
+ }
149
+
150
+ function renderSummaryLine(result, refLabel) {
151
+ const parts = [];
152
+ if (result.added.length) parts.push(green("+" + result.added.length + " added"));
153
+ if (result.removed.length) parts.push(red("-" + result.removed.length + " removed"));
154
+ if (result.changed.length) parts.push(yellow("~" + result.changed.length + " changed"));
155
+ if (!parts.length) parts.push(gray("no changes"));
156
+ console.log(` ${parts.join(" ")} ${gray("vs " + refLabel)}`);
157
+ }
158
+
159
+ // ── main ─────────────────────────────────────────────────────────────────────
160
+
161
+ export async function diffCommand(rawArgs) {
162
+ const args = rawArgs.slice(1);
163
+
164
+ const asJson = args.includes("--json");
165
+ const summary = args.includes("--summary");
166
+
167
+ const refIdx = args.indexOf("--ref");
168
+ let ref = refIdx !== -1 ? args[refIdx + 1] : null;
169
+
170
+ const cwd = process.cwd();
171
+ const infernoDir = path.join(cwd, "inferno");
172
+ const infernoRelDir = "inferno"; // relative for git show
173
+
174
+ if (!asJson) header("diff");
175
+
176
+ // ── Validate inferno/ exists ─────────────────────────────────────────────
177
+ if (!fs.existsSync(infernoDir)) {
178
+ if (asJson) {
179
+ console.log(JSON.stringify({ ok: false, error: "inferno_not_found" }));
180
+ process.exit(1);
181
+ }
182
+ fail("inferno/ not found", "Run: infernoflow init");
183
+ process.exit(1);
184
+ }
185
+
186
+ // ── Resolve ref ──────────────────────────────────────────────────────────
187
+ if (!ref) {
188
+ ref = lastTag(cwd);
189
+ if (!ref) {
190
+ // No tags — fall back to HEAD~1
191
+ const parentExists = capture("git rev-parse HEAD~1", cwd);
192
+ if (parentExists) {
193
+ ref = "HEAD~1";
194
+ } else {
195
+ if (asJson) {
196
+ console.log(JSON.stringify({ ok: false, error: "no_ref", hint: "No git tags found and no parent commit. Use --ref <commit>" }));
197
+ process.exit(1);
198
+ }
199
+ fail("No git tags found", "Create a tag first: git tag v0.1.0 or use --ref <commit>");
200
+ process.exit(1);
201
+ }
202
+ }
203
+ }
204
+
205
+ // ── Load capabilities ────────────────────────────────────────────────────
206
+ const current = loadCapsFromDisk(infernoDir);
207
+ if (!current) {
208
+ if (asJson) {
209
+ console.log(JSON.stringify({ ok: false, error: "no_capabilities_found" }));
210
+ process.exit(1);
211
+ }
212
+ fail("No capabilities.json or contract.json found in inferno/");
213
+ process.exit(1);
214
+ }
215
+
216
+ const previous = loadCapsAtRef(ref, infernoRelDir, cwd);
217
+ if (!previous) {
218
+ if (asJson) {
219
+ console.log(JSON.stringify({ ok: false, error: "ref_not_found", ref, hint: "Does inferno/capabilities.json exist at that ref?" }));
220
+ process.exit(1);
221
+ }
222
+ fail(`Could not read capabilities at ${ref}`, "The inferno/ directory may not exist at that ref");
223
+ process.exit(1);
224
+ }
225
+
226
+ // ── Compute diff ─────────────────────────────────────────────────────────
227
+ const result = diffCaps(previous, current);
228
+ const unchanged = current.length - result.added.length - result.changed.length;
229
+
230
+ // ── JSON output ──────────────────────────────────────────────────────────
231
+ if (asJson) {
232
+ console.log(JSON.stringify({
233
+ ok: true,
234
+ ref,
235
+ current: current.length,
236
+ previous: previous.length,
237
+ added: result.added,
238
+ removed: result.removed,
239
+ changed: result.changed,
240
+ unchanged,
241
+ }, null, 2));
242
+ return;
243
+ }
244
+
245
+ // ── Human output ─────────────────────────────────────────────────────────
246
+ const refLabel = refDescription(cwd, ref);
247
+ console.log();
248
+ console.log(` Comparing ${bold(cyan("current"))} vs ${bold(refLabel)}`);
249
+ console.log(` ${gray(current.length + " capabilities now / " + previous.length + " before")}`);
250
+
251
+ if (summary) {
252
+ renderSummaryLine(result, ref);
253
+ console.log();
254
+ return;
255
+ }
256
+
257
+ const hasAny = result.added.length || result.removed.length || result.changed.length;
258
+
259
+ if (!hasAny) {
260
+ console.log();
261
+ ok("No capability changes since " + ref);
262
+ console.log();
263
+ return;
264
+ }
265
+
266
+ renderAdded(result.added);
267
+ renderRemoved(result.removed);
268
+ renderChanged(result.changed);
269
+ renderUnchanged(unchanged);
270
+
271
+ console.log();
272
+ renderSummaryLine(result, ref);
273
+ console.log();
274
+ }