infernoflow 0.37.1 β†’ 0.37.4

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 (88) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,282 +1,2 @@
1
- /**
2
- * infernoflow version
3
- *
4
- * Smart semver bump recommendation based on capability changes since the last
5
- * git tag (or a custom ref).
6
- *
7
- * Classification rules:
8
- * MAJOR β€” any capability was REMOVED (breaking: callers lose functionality)
9
- * MINOR β€” capabilities were ADDED (non-breaking: new surface area)
10
- * PATCH β€” only metadata changed (title / description / status edits)
11
- * NONE β€” no capability changes at all
12
- *
13
- * Usage:
14
- * infernoflow version # recommend bump type + show next version
15
- * infernoflow version --apply # apply recommended bump to package.json
16
- * infernoflow version --ref v1.2.3 # compare against a specific ref
17
- * infernoflow version --json # machine-readable output
18
- */
19
-
20
- import * as fs from "node:fs";
21
- import * as path from "node:path";
22
- import { execSync } from "node:child_process";
23
- import { header, ok, warn, info, bold, cyan, gray, green, red, yellow, done } from "../ui/output.mjs";
24
-
25
- // ── git helpers (shared with diff.mjs) ───────────────────────────────────────
26
-
27
- function capture(cmd, cwd) {
28
- try {
29
- return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- function lastTag(cwd) {
36
- return capture("git describe --tags --abbrev=0", cwd) || null;
37
- }
38
-
39
- function fileAtRef(ref, relPath, cwd) {
40
- return capture(`git show "${ref}:${relPath}"`, cwd);
41
- }
42
-
43
- // ── capability helpers ────────────────────────────────────────────────────────
44
-
45
- function parseCaps(jsonText) {
46
- if (!jsonText) return null;
47
- try {
48
- const obj = JSON.parse(jsonText);
49
- const raw = obj.capabilities || [];
50
- return raw.map(c => {
51
- if (typeof c === "string") return { id: c, title: c };
52
- return { id: c.id || c, title: c.title || c.id || String(c), status: c.status };
53
- });
54
- } catch {
55
- return null;
56
- }
57
- }
58
-
59
- function loadCapsFromDisk(infernoDir) {
60
- const capsPath = path.join(infernoDir, "capabilities.json");
61
- const contractPath = path.join(infernoDir, "contract.json");
62
- if (fs.existsSync(capsPath)) return parseCaps(fs.readFileSync(capsPath, "utf8"));
63
- if (fs.existsSync(contractPath)) return parseCaps(fs.readFileSync(contractPath, "utf8"));
64
- return null;
65
- }
66
-
67
- function loadCapsAtRef(ref, infernoRelDir, cwd) {
68
- const capsJson = fileAtRef(ref, `${infernoRelDir}/capabilities.json`, cwd);
69
- if (capsJson) return parseCaps(capsJson);
70
- const contractJson = fileAtRef(ref, `${infernoRelDir}/contract.json`, cwd);
71
- return parseCaps(contractJson);
72
- }
73
-
74
- function diffCaps(before, after) {
75
- const beforeMap = new Map(before.map(c => [c.id, c]));
76
- const afterMap = new Map(after.map(c => [c.id, c]));
77
-
78
- const added = after.filter(c => !beforeMap.has(c.id));
79
- const removed = before.filter(c => !afterMap.has(c.id));
80
-
81
- const changed = [];
82
- for (const c of after) {
83
- const old = beforeMap.get(c.id);
84
- if (!old) continue;
85
- const changes = [];
86
- if (old.title !== c.title) changes.push({ field: "title", from: old.title, to: c.title });
87
- if ((old.status || "") !== (c.status || "")) changes.push({ field: "status", from: old.status || "β€”", to: c.status || "β€”" });
88
- if (changes.length) changed.push({ id: c.id, changes });
89
- }
90
-
91
- return { added, removed, changed };
92
- }
93
-
94
- // ── semver helpers ────────────────────────────────────────────────────────────
95
-
96
- function classifyBump(diff) {
97
- if (diff.removed.length > 0) return "major";
98
- if (diff.added.length > 0) return "minor";
99
- if (diff.changed.length > 0) return "patch";
100
- return "none";
101
- }
102
-
103
- function applyBump(version, type) {
104
- const parts = (version || "0.0.0").split(".").map(Number);
105
- if (type === "major") { parts[0]++; parts[1] = 0; parts[2] = 0; }
106
- else if (type === "minor") { parts[1]++; parts[2] = 0; }
107
- else if (type === "patch") { parts[2]++; }
108
- return parts.join(".");
109
- }
110
-
111
- function readPackageVersion(cwd) {
112
- const pkgPath = path.join(cwd, "package.json");
113
- if (!fs.existsSync(pkgPath)) return null;
114
- try {
115
- return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version || null;
116
- } catch { return null; }
117
- }
118
-
119
- function writePackageVersion(cwd, newVersion) {
120
- const pkgPath = path.join(cwd, "package.json");
121
- const raw = fs.readFileSync(pkgPath, "utf8");
122
- const data = JSON.parse(raw);
123
- data.version = newVersion;
124
- fs.writeFileSync(pkgPath, JSON.stringify(data, null, 2) + "\n", "utf8");
125
- }
126
-
127
- // ── reason builder ────────────────────────────────────────────────────────────
128
-
129
- function buildReason(type, diff, ref) {
130
- const lines = [];
131
- if (type === "major") {
132
- lines.push(`${diff.removed.length} capability removed β€” breaking change`);
133
- for (const c of diff.removed.slice(0, 3)) lines.push(` - ${c.id}: ${c.title}`);
134
- if (diff.removed.length > 3) lines.push(` … and ${diff.removed.length - 3} more`);
135
- } else if (type === "minor") {
136
- lines.push(`${diff.added.length} new capability added`);
137
- for (const c of diff.added.slice(0, 3)) lines.push(` + ${c.id}: ${c.title}`);
138
- if (diff.added.length > 3) lines.push(` … and ${diff.added.length - 3} more`);
139
- } else if (type === "patch") {
140
- lines.push(`${diff.changed.length} capability metadata updated`);
141
- } else {
142
- lines.push(`No capability changes since ${ref}`);
143
- }
144
- return lines;
145
- }
146
-
147
- // ── MCP-compatible JSON output ────────────────────────────────────────────────
148
-
149
- function emitJson(payload) {
150
- console.log(JSON.stringify(payload, null, 2));
151
- }
152
-
153
- // ── main command ──────────────────────────────────────────────────────────────
154
-
155
- export async function versionCommand(rawArgs) {
156
- const args = rawArgs.slice(1);
157
- const asJson = args.includes("--json");
158
- const apply = args.includes("--apply");
159
-
160
- const refIdx = args.indexOf("--ref");
161
- let ref = refIdx !== -1 ? args[refIdx + 1] : null;
162
-
163
- const cwd = process.cwd();
164
- const infernoDir = path.join(cwd, "inferno");
165
-
166
- if (!asJson) header("infernoflow version");
167
-
168
- // ── Validate ───────────────────────────────────────────────────────────────
169
- if (!fs.existsSync(infernoDir)) {
170
- if (asJson) { emitJson({ ok: false, error: "inferno_not_found" }); process.exit(1); }
171
- warn("inferno/ not found β€” run: infernoflow init");
172
- process.exit(1);
173
- }
174
-
175
- // ── Resolve ref ────────────────────────────────────────────────────────────
176
- if (!ref) {
177
- ref = lastTag(cwd);
178
- if (!ref) {
179
- const parentExists = capture("git rev-parse HEAD~1", cwd);
180
- ref = parentExists ? "HEAD~1" : null;
181
- }
182
- }
183
-
184
- if (!ref) {
185
- const currentVersion = readPackageVersion(cwd) || "0.0.0";
186
- if (asJson) {
187
- emitJson({ ok: true, bump: "minor", current: currentVersion, next: applyBump(currentVersion, "minor"), reason: ["No git history β€” defaulting to minor for first release"], ref: null });
188
- } else {
189
- info("No git history found β€” defaulting to minor for first release");
190
- ok(`Recommended: ${bold(cyan("minor"))} β†’ ${bold(applyBump(currentVersion, "minor"))}`);
191
- }
192
- return;
193
- }
194
-
195
- // ── Load capabilities ──────────────────────────────────────────────────────
196
- const current = loadCapsFromDisk(infernoDir);
197
- const previous = loadCapsAtRef(ref, "inferno", cwd);
198
-
199
- if (!current) {
200
- if (asJson) { emitJson({ ok: false, error: "no_capabilities" }); process.exit(1); }
201
- warn("No capabilities.json or contract.json found");
202
- process.exit(1);
203
- }
204
-
205
- // If no previous snapshot, treat all current caps as new β†’ minor
206
- const prevCaps = previous || [];
207
- const diff = diffCaps(prevCaps, current);
208
- const bump = classifyBump(diff);
209
-
210
- const currentVersion = readPackageVersion(cwd) || "0.0.0";
211
- const nextVersion = bump === "none" ? currentVersion : applyBump(currentVersion, bump);
212
- const reason = buildReason(bump, diff, ref);
213
-
214
- // ── JSON output ────────────────────────────────────────────────────────────
215
- if (asJson) {
216
- emitJson({
217
- ok: true,
218
- bump,
219
- current: currentVersion,
220
- next: nextVersion,
221
- ref,
222
- reason,
223
- diff: {
224
- added: diff.added.length,
225
- removed: diff.removed.length,
226
- changed: diff.changed.length,
227
- },
228
- });
229
- return;
230
- }
231
-
232
- // ── Human output ──────────────────────────────────────────────────────────
233
- const bumpColor = bump === "major" ? red
234
- : bump === "minor" ? green
235
- : bump === "patch" ? yellow
236
- : gray;
237
-
238
- console.log();
239
- console.log(` Current version ${bold(currentVersion)} ${gray("(" + ref + ")")}`);
240
- console.log();
241
-
242
- if (bump === "none") {
243
- ok(`No capability changes β€” version stays at ${bold(currentVersion)}`);
244
- } else {
245
- console.log(` ${bold("Recommended bump:")} ${bumpColor(bold(bump.toUpperCase()))}`);
246
- console.log();
247
- for (const line of reason) {
248
- const prefix = line.startsWith(" +") ? green(" +")
249
- : line.startsWith(" -") ? red(" -")
250
- : line.startsWith(" …") ? gray(" …")
251
- : " ";
252
- const text = line.replace(/^\s+[+\-…]\s?/, "");
253
- if (line.startsWith(" ") && !line.startsWith(" …")) {
254
- console.log(` ${line.trim()}`);
255
- } else {
256
- console.log(` ${gray(line)}`);
257
- }
258
- }
259
- console.log();
260
- console.log(` ${bold(currentVersion)} β†’ ${bumpColor(bold(nextVersion))}`);
261
- }
262
-
263
- // ── Apply ──────────────────────────────────────────────────────────────────
264
- if (apply && bump !== "none") {
265
- const pkgPath = path.join(cwd, "package.json");
266
- if (!fs.existsSync(pkgPath)) {
267
- warn("No package.json found β€” skipping --apply");
268
- } else {
269
- writePackageVersion(cwd, nextVersion);
270
- console.log();
271
- done(`package.json updated β†’ ${bold(nextVersion)}`);
272
- }
273
- } else if (apply && bump === "none") {
274
- console.log();
275
- info("No changes to apply β€” version unchanged");
276
- } else if (bump !== "none") {
277
- console.log();
278
- info(`Run ${cyan("infernoflow version --apply")} to write ${nextVersion} to package.json`);
279
- }
280
-
281
- console.log();
282
- }
1
+ import*as u from"node:fs";import*as h from"node:path";import{execSync as V}from"node:child_process";import{header as W,ok as C,warn as v,info as k,bold as p,cyan as J,gray as $,green as P,red as F,yellow as A,done as E}from"../ui/output.mjs";function x(e,n){try{return V(e,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}catch{return null}}function M(e){return x("git describe --tags --abbrev=0",e)||null}function O(e,n,t){return x(`git show "${e}:${n}"`,t)}function j(e){if(!e)return null;try{return(JSON.parse(e).capabilities||[]).map(o=>typeof o=="string"?{id:o,title:o}:{id:o.id||o,title:o.title||o.id||String(o),status:o.status})}catch{return null}}function _(e){const n=h.join(e,"capabilities.json"),t=h.join(e,"contract.json");return u.existsSync(n)?j(u.readFileSync(n,"utf8")):u.existsSync(t)?j(u.readFileSync(t,"utf8")):null}function B(e,n,t){const o=O(e,`${n}/capabilities.json`,t);if(o)return j(o);const r=O(e,`${n}/contract.json`,t);return j(r)}function D(e,n){const t=new Map(e.map(s=>[s.id,s])),o=new Map(n.map(s=>[s.id,s])),r=n.filter(s=>!t.has(s.id)),c=e.filter(s=>!o.has(s.id)),l=[];for(const s of n){const f=t.get(s.id);if(!f)continue;const d=[];f.title!==s.title&&d.push({field:"title",from:f.title,to:s.title}),(f.status||"")!==(s.status||"")&&d.push({field:"status",from:f.status||"\u2014",to:s.status||"\u2014"}),d.length&&l.push({id:s.id,changes:d})}return{added:r,removed:c,changed:l}}function H(e){return e.removed.length>0?"major":e.added.length>0?"minor":e.changed.length>0?"patch":"none"}function S(e,n){const t=(e||"0.0.0").split(".").map(Number);return n==="major"?(t[0]++,t[1]=0,t[2]=0):n==="minor"?(t[1]++,t[2]=0):n==="patch"&&t[2]++,t.join(".")}function R(e){const n=h.join(e,"package.json");if(!u.existsSync(n))return null;try{return JSON.parse(u.readFileSync(n,"utf8")).version||null}catch{return null}}function I(e,n){const t=h.join(e,"package.json"),o=u.readFileSync(t,"utf8"),r=JSON.parse(o);r.version=n,u.writeFileSync(t,JSON.stringify(r,null,2)+`
2
+ `,"utf8")}function U(e,n,t){const o=[];if(e==="major"){o.push(`${n.removed.length} capability removed \u2014 breaking change`);for(const r of n.removed.slice(0,3))o.push(` - ${r.id}: ${r.title}`);n.removed.length>3&&o.push(` \u2026 and ${n.removed.length-3} more`)}else if(e==="minor"){o.push(`${n.added.length} new capability added`);for(const r of n.added.slice(0,3))o.push(` + ${r.id}: ${r.title}`);n.added.length>3&&o.push(` \u2026 and ${n.added.length-3} more`)}else e==="patch"?o.push(`${n.changed.length} capability metadata updated`):o.push(`No capability changes since ${t}`);return o}function b(e){console.log(JSON.stringify(e,null,2))}async function Q(e){const n=e.slice(1),t=n.includes("--json"),o=n.includes("--apply"),r=n.indexOf("--ref");let c=r!==-1?n[r+1]:null;const l=process.cwd(),s=h.join(l,"inferno");if(t||W("infernoflow version"),u.existsSync(s)||(t&&(b({ok:!1,error:"inferno_not_found"}),process.exit(1)),v("inferno/ not found \u2014 run: infernoflow init"),process.exit(1)),c||(c=M(l),c||(c=x("git rev-parse HEAD~1",l)?"HEAD~1":null)),!c){const i=R(l)||"0.0.0";t?b({ok:!0,bump:"minor",current:i,next:S(i,"minor"),reason:["No git history \u2014 defaulting to minor for first release"],ref:null}):(k("No git history found \u2014 defaulting to minor for first release"),C(`Recommended: ${p(J("minor"))} \u2192 ${p(S(i,"minor"))}`));return}const f=_(s),d=B(c,"inferno",l);f||(t&&(b({ok:!1,error:"no_capabilities"}),process.exit(1)),v("No capabilities.json or contract.json found"),process.exit(1));const m=D(d||[],f),a=H(m),g=R(l)||"0.0.0",y=a==="none"?g:S(g,a),w=U(a,m,c);if(t){b({ok:!0,bump:a,current:g,next:y,ref:c,reason:w,diff:{added:m.added.length,removed:m.removed.length,changed:m.changed.length}});return}const N=a==="major"?F:a==="minor"?P:a==="patch"?A:$;if(console.log(),console.log(` Current version ${p(g)} ${$("("+c+")")}`),console.log(),a==="none")C(`No capability changes \u2014 version stays at ${p(g)}`);else{console.log(` ${p("Recommended bump:")} ${N(p(a.toUpperCase()))}`),console.log();for(const i of w){const z=i.startsWith(" +")?P(" +"):i.startsWith(" -")?F(" -"):i.startsWith(" \u2026")?$(" \u2026"):" ",G=i.replace(/^\s+[+\-…]\s?/,"");i.startsWith(" ")&&!i.startsWith(" \u2026")?console.log(` ${i.trim()}`):console.log(` ${$(i)}`)}console.log(),console.log(` ${p(g)} \u2192 ${N(p(y))}`)}if(o&&a!=="none"){const i=h.join(l,"package.json");u.existsSync(i)?(I(l,y),console.log(),E(`package.json updated \u2192 ${p(y)}`)):v("No package.json found \u2014 skipping --apply")}else o&&a==="none"?(console.log(),k("No changes to apply \u2014 version unchanged")):a!=="none"&&(console.log(),k(`Run ${J("infernoflow version --apply")} to write ${y} to package.json`));console.log()}export{Q as versionCommand};
@@ -1,365 +1,15 @@
1
- /**
2
- * infernoflow vibe
3
- *
4
- * Persistent background process for vibe coding sessions.
5
- * Completely invisible to the developer β€” just leave it running in a terminal.
6
- *
7
- * What it does every cycle:
8
- * 1. Watches source files for saves
9
- * 2. On save: runs infernoflow suggest to sync the contract
10
- * 3. Regenerates CONTEXT.md so the next AI prompt is always fresh
11
- * 4. Watches inferno/ for contract changes β†’ updates CONTEXT.md automatically
12
- * 5. Prints a one-line status ticker β€” nothing noisy
13
- *
14
- * Also provides a session summary on exit (Ctrl+C):
15
- * - Files changed
16
- * - Capabilities added / updated
17
- * - Health score delta
18
- *
19
- * Usage:
20
- * infernoflow vibe Start vibe mode (default dirs)
21
- * infernoflow vibe --dir src,api Watch specific directories
22
- * infernoflow vibe --no-suggest Watch + context only, skip suggest
23
- * infernoflow vibe --no-context Skip CONTEXT.md regeneration
24
- * infernoflow vibe --interval 5 Debounce seconds (default 4)
25
- * infernoflow vibe --silent Suppress all output (pure background)
26
- * infernoflow vibe --port 7337 Also start dashboard on this port
27
- */
28
-
29
- import * as fs from "node:fs";
30
- import * as path from "node:path";
31
- import * as http from "node:http";
32
- import * as os from "node:os";
33
- import { execSync, spawnSync } from "node:child_process";
34
- import { bold, cyan, gray, green, yellow, red, info, warn } from "../ui/output.mjs";
35
-
36
- // ── Helpers ───────────────────────────────────────────────────────────────────
37
-
38
- const SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".py", ".rb", ".go", ".rs"]);
39
- const IGNORE_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "__pycache__", "vendor", "coverage", "inferno"]);
40
-
41
- function runCli(args, cwd, silent = false) {
42
- const result = spawnSync("infernoflow", args, {
43
- cwd,
44
- encoding: "utf8",
45
- env: { ...process.env, NO_COLOR: "1" },
46
- timeout: 30_000,
47
- stdio: ["ignore", "pipe", "pipe"],
48
- });
49
- if (!silent && result.error) return { ok: false, out: "", err: result.error.message };
50
- return { ok: result.status === 0, out: result.stdout || "", err: result.stderr || "" };
51
- }
52
-
53
- function timestamp() {
54
- return new Date().toLocaleTimeString("en-US", { hour12: false });
55
- }
56
-
57
- function readContractCapCount(infernoDir) {
58
- for (const f of ["contract.json", "capabilities.json"]) {
59
- const p = path.join(infernoDir, f);
60
- if (!fs.existsSync(p)) continue;
61
- try {
62
- const data = JSON.parse(fs.readFileSync(p, "utf8"));
63
- return (data.capabilities || []).length;
64
- } catch {}
65
- }
66
- return 0;
67
- }
68
-
69
- function readHealthScore(cwd) {
70
- const result = spawnSync("infernoflow", ["health", "--json"], {
71
- cwd, encoding: "utf8", timeout: 20_000, stdio: ["ignore", "pipe", "pipe"],
72
- env: { ...process.env, NO_COLOR: "1" },
73
- });
74
- try { return JSON.parse(result.stdout || "{}").score ?? null; } catch { return null; }
75
- }
76
-
77
- // ── Session state ─────────────────────────────────────────────────────────────
78
-
79
- class VibeSession {
80
- constructor() {
81
- this.startedAt = new Date();
82
- this.filesChanged = new Set();
83
- this.suggestRuns = 0;
84
- this.contextUpdates = 0;
85
- this.capsAtStart = 0;
86
- this.capsNow = 0;
87
- this.scoreAtStart = null;
88
- this.scoreNow = null;
89
- }
90
-
91
- recordChange(filePath) { this.filesChanged.add(filePath); }
92
- recordSuggest() { this.suggestRuns++; }
93
- recordContext() { this.contextUpdates++; }
94
-
95
- elapsed() {
96
- const ms = Date.now() - this.startedAt.getTime();
97
- const mins = Math.floor(ms / 60000);
98
- const secs = Math.floor((ms % 60000) / 1000);
99
- return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
100
- }
101
-
102
- printSummary() {
103
- const capDelta = this.capsNow - this.capsAtStart;
104
- const scoreDelta = this.scoreNow !== null && this.scoreAtStart !== null
105
- ? this.scoreNow - this.scoreAtStart : null;
106
-
107
- console.log();
108
- console.log(` ${bold("πŸ”₯ infernoflow vibe β€” session summary")}`);
109
- console.log();
110
- console.log(` ${bold("Duration:")} ${this.elapsed()}`);
111
- console.log(` ${bold("Files watched:")} ${this.filesChanged.size}`);
112
- console.log(` ${bold("Syncs run:")} ${this.suggestRuns}`);
113
- console.log(` ${bold("Context refreshes:")} ${this.contextUpdates}`);
114
-
115
- if (capDelta !== 0) {
116
- const col = capDelta > 0 ? green : yellow;
117
- console.log(` ${bold("Capabilities:")} ${col((capDelta > 0 ? "+" : "") + capDelta)} (${this.capsAtStart} β†’ ${this.capsNow})`);
118
- } else {
119
- console.log(` ${bold("Capabilities:")} ${this.capsNow} (no change)`);
120
- }
121
-
122
- if (scoreDelta !== null) {
123
- const col = scoreDelta > 0 ? green : scoreDelta < 0 ? yellow : gray;
124
- console.log(` ${bold("Health score:")} ${col((scoreDelta > 0 ? "+" : "") + scoreDelta)} (${this.scoreAtStart} β†’ ${this.scoreNow})`);
125
- }
126
-
127
- console.log();
128
- }
129
- }
130
-
131
- // ── Status ticker ─────────────────────────────────────────────────────────────
132
-
133
- const SPINNERS = ["β ‹", "β ™", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ‡", "⠏"];
134
- let spinIdx = 0;
135
-
136
- function tick(msg, col = gray) {
137
- const spinner = SPINNERS[spinIdx++ % SPINNERS.length];
138
- process.stdout.write(`\r ${col(spinner)} ${msg} `);
139
- }
140
-
141
- function tickDone(msg) {
142
- process.stdout.write(`\r ${green("βœ”")} ${msg} \n`);
143
- }
144
-
145
- function tickWarn(msg) {
146
- process.stdout.write(`\r ${yellow("⚠")} ${msg} \n`);
147
- }
148
-
149
- // ── Core cycle ────────────────────────────────────────────────────────────────
150
-
151
- async function runCycle(changedFile, cwd, infernoDir, opts, session) {
152
- const rel = path.relative(cwd, changedFile);
153
- session.recordChange(changedFile);
154
-
155
- if (!opts.silent) tick(`${cyan(rel)} changed β€” syncing…`, cyan);
156
-
157
- // 1. Suggest (sync contract)
158
- if (!opts.noSuggest) {
159
- const desc = `updated ${path.basename(changedFile)}`;
160
- const r = runCli(["suggest", desc, "--json"], cwd, true);
161
- session.recordSuggest();
162
-
163
- if (!opts.silent) {
164
- if (r.ok) {
165
- tickDone(`Synced: ${cyan(rel)}`);
166
- } else {
167
- tickWarn(`Suggest skipped (${gray("no AI provider")})`);
168
- }
169
- }
170
- }
171
-
172
- // 2. Regenerate CONTEXT.md
173
- if (!opts.noContext) {
174
- const r = runCli(["context"], cwd, true);
175
- session.recordContext();
176
- if (!opts.silent && r.ok) {
177
- tick(`Context refreshed`, gray);
178
- }
179
- }
180
-
181
- // 3. Update cap count
182
- session.capsNow = readContractCapCount(infernoDir);
183
- }
184
-
185
- // ── File watcher ──────────────────────────────────────────────────────────────
186
-
187
- function watchDirs(dirs, cwd, infernoDir, opts, session) {
188
- const watchers = [];
189
- const debounceMap = new Map();
190
-
191
- const onFileChange = (eventType, filePath) => {
192
- if (!SOURCE_EXTENSIONS.has(path.extname(filePath))) return;
193
- if ([...IGNORE_DIRS].some(d => filePath.includes(`${path.sep}${d}${path.sep}`))) return;
194
-
195
- // Debounce per file
196
- if (debounceMap.has(filePath)) clearTimeout(debounceMap.get(filePath));
197
- debounceMap.set(filePath, setTimeout(async () => {
198
- debounceMap.delete(filePath);
199
- await runCycle(filePath, cwd, infernoDir, opts, session);
200
- }, opts.interval * 1000));
201
- };
202
-
203
- for (const dir of dirs) {
204
- if (!fs.existsSync(dir)) continue;
205
- try {
206
- const w = fs.watch(dir, { recursive: true }, (eventType, filename) => {
207
- if (!filename) return;
208
- onFileChange(eventType, path.join(dir, filename));
209
- });
210
- watchers.push(w);
211
- } catch {}
212
- }
213
-
214
- // Also watch inferno/ for contract changes β†’ regenerate CONTEXT.md
215
- try {
216
- const iw = fs.watch(infernoDir, { recursive: true }, (eventType, filename) => {
217
- if (!filename) return;
218
- if (!filename.endsWith(".json") && !filename.endsWith(".md")) return;
219
- if (filename.includes("CONTEXT")) return; // avoid loop
220
-
221
- if (debounceMap.has("__inferno__")) clearTimeout(debounceMap.get("__inferno__"));
222
- debounceMap.set("__inferno__", setTimeout(async () => {
223
- debounceMap.delete("__inferno__");
224
- if (!opts.noContext) {
225
- runCli(["context"], cwd, true);
226
- session.recordContext();
227
- if (!opts.silent) tick(`Contract updated β€” context refreshed`, gray);
228
- }
229
- session.capsNow = readContractCapCount(infernoDir);
230
- }, 1000));
231
- });
232
- watchers.push(iw);
233
- } catch {}
234
-
235
- return watchers;
236
- }
237
-
238
- // ── Dashboard mini-server ─────────────────────────────────────────────────────
239
-
240
- function startMiniDashboard(port, cwd, infernoDir) {
241
- const server = http.createServer((req, res) => {
242
- if (req.url === "/status") {
243
- const caps = readContractCapCount(infernoDir);
244
- const health = readHealthScore(cwd);
245
- res.writeHead(200, { "Content-Type": "application/json" });
246
- res.end(JSON.stringify({ ok: true, capabilities: caps, health, ts: new Date().toISOString() }));
247
- return;
248
- }
249
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
250
- res.end(`<!DOCTYPE html><html><head><meta charset="UTF-8">
1
+ import*as m from"node:fs";import*as p from"node:path";import*as R from"node:http";import"node:os";import{spawnSync as j}from"node:child_process";import{bold as d,cyan as w,gray as h,green as v,yellow as C,info as k,warn as A}from"../ui/output.mjs";const E=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".py",".rb",".go",".rs"]),M=new Set(["node_modules",".git","dist","build",".next","__pycache__","vendor","coverage","inferno"]);function N(s,t,o=!1){const e=j("infernoflow",s,{cwd:t,encoding:"utf8",env:{...process.env,NO_COLOR:"1"},timeout:3e4,stdio:["ignore","pipe","pipe"]});return!o&&e.error?{ok:!1,out:"",err:e.error.message}:{ok:e.status===0,out:e.stdout||"",err:e.stderr||""}}function B(){return new Date().toLocaleTimeString("en-US",{hour12:!1})}function y(s){for(const t of["contract.json","capabilities.json"]){const o=p.join(s,t);if(m.existsSync(o))try{return(JSON.parse(m.readFileSync(o,"utf8")).capabilities||[]).length}catch{}}return 0}function _(s){const t=j("infernoflow",["health","--json"],{cwd:s,encoding:"utf8",timeout:2e4,stdio:["ignore","pipe","pipe"],env:{...process.env,NO_COLOR:"1"}});try{return JSON.parse(t.stdout||"{}").score??null}catch{return null}}class H{constructor(){this.startedAt=new Date,this.filesChanged=new Set,this.suggestRuns=0,this.contextUpdates=0,this.capsAtStart=0,this.capsNow=0,this.scoreAtStart=null,this.scoreNow=null}recordChange(t){this.filesChanged.add(t)}recordSuggest(){this.suggestRuns++}recordContext(){this.contextUpdates++}elapsed(){const t=Date.now()-this.startedAt.getTime(),o=Math.floor(t/6e4),e=Math.floor(t%6e4/1e3);return o>0?`${o}m ${e}s`:`${e}s`}printSummary(){const t=this.capsNow-this.capsAtStart,o=this.scoreNow!==null&&this.scoreAtStart!==null?this.scoreNow-this.scoreAtStart:null;if(console.log(),console.log(` ${d("\u{1F525} infernoflow vibe \u2014 session summary")}`),console.log(),console.log(` ${d("Duration:")} ${this.elapsed()}`),console.log(` ${d("Files watched:")} ${this.filesChanged.size}`),console.log(` ${d("Syncs run:")} ${this.suggestRuns}`),console.log(` ${d("Context refreshes:")} ${this.contextUpdates}`),t!==0){const e=t>0?v:C;console.log(` ${d("Capabilities:")} ${e((t>0?"+":"")+t)} (${this.capsAtStart} \u2192 ${this.capsNow})`)}else console.log(` ${d("Capabilities:")} ${this.capsNow} (no change)`);if(o!==null){const e=o>0?v:o<0?C:h;console.log(` ${d("Health score:")} ${e((o>0?"+":"")+o)} (${this.scoreAtStart} \u2192 ${this.scoreNow})`)}console.log()}}const I=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];let U=0;function x(s,t=h){const o=I[U++%I.length];process.stdout.write(`\r ${t(o)} ${s} `)}function W(s){process.stdout.write(`\r ${v("\u2714")} ${s}
2
+ `)}function L(s){process.stdout.write(`\r ${C("\u26A0")} ${s}
3
+ `)}async function z(s,t,o,e,i){const n=p.relative(t,s);if(i.recordChange(s),e.silent||x(`${w(n)} changed \u2014 syncing\u2026`,w),!e.noSuggest){const r=`updated ${p.basename(s)}`,g=N(["suggest",r,"--json"],t,!0);i.recordSuggest(),e.silent||(g.ok?W(`Synced: ${w(n)}`):L(`Suggest skipped (${h("no AI provider")})`))}if(!e.noContext){const r=N(["context"],t,!0);i.recordContext(),!e.silent&&r.ok&&x("Context refreshed",h)}i.capsNow=y(o)}function G(s,t,o,e,i){const n=[],r=new Map,g=(f,c)=>{E.has(p.extname(c))&&([...M].some(a=>c.includes(`${p.sep}${a}${p.sep}`))||(r.has(c)&&clearTimeout(r.get(c)),r.set(c,setTimeout(async()=>{r.delete(c),await z(c,t,o,e,i)},e.interval*1e3))))};for(const f of s)if(m.existsSync(f))try{const c=m.watch(f,{recursive:!0},(a,S)=>{S&&g(a,p.join(f,S))});n.push(c)}catch{}try{const f=m.watch(o,{recursive:!0},(c,a)=>{a&&(!a.endsWith(".json")&&!a.endsWith(".md")||a.includes("CONTEXT")||(r.has("__inferno__")&&clearTimeout(r.get("__inferno__")),r.set("__inferno__",setTimeout(async()=>{r.delete("__inferno__"),e.noContext||(N(["context"],t,!0),i.recordContext(),e.silent||x("Contract updated \u2014 context refreshed",h)),i.capsNow=y(o)},1e3))))});n.push(f)}catch{}return n}function J(s,t,o){const e=R.createServer((i,n)=>{if(i.url==="/status"){const r=y(o),g=_(t);n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({ok:!0,capabilities:r,health:g,ts:new Date().toISOString()}));return}n.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),n.end(`<!DOCTYPE html><html><head><meta charset="UTF-8">
251
4
  <meta http-equiv="refresh" content="10">
252
- <title>πŸ”₯ vibe</title>
5
+ <title>\u{1F525} vibe</title>
253
6
  <style>body{font-family:system-ui;background:#0f1117;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;flex-direction:column;gap:16px}
254
7
  h1{color:#f97316;margin:0}.sub{color:#64748b;font-size:14px}</style></head>
255
- <body><h1>πŸ”₯ infernoflow vibe</h1><div class="sub">Auto-refreshes every 10s</div>
8
+ <body><h1>\u{1F525} infernoflow vibe</h1><div class="sub">Auto-refreshes every 10s</div>
256
9
  <script>
257
10
  fetch('/status').then(r=>r.json()).then(d=>{
258
- document.body.innerHTML='<h1>πŸ”₯ vibe</h1><p style="color:#22c55e">'+d.capabilities+' capabilities</p>'
11
+ document.body.innerHTML='<h1>\u{1F525} vibe</h1><p style="color:#22c55e">'+d.capabilities+' capabilities</p>'
259
12
  +(d.health!==null?'<p>Health: '+d.health+'/100</p>':'')
260
13
  +'<p style="color:#64748b;font-size:12px">'+new Date(d.ts).toLocaleTimeString()+'</p>';
261
14
  });
262
- </script></body></html>`);
263
- });
264
- server.listen(port, "127.0.0.1", () => {});
265
- return server;
266
- }
267
-
268
- // ── Entry ─────────────────────────────────────────────────────────────────────
269
-
270
- export async function vibeCommand(rawArgs) {
271
- const args = rawArgs.slice(1);
272
- const silent = args.includes("--silent");
273
- const noSuggest = args.includes("--no-suggest");
274
- const noContext = args.includes("--no-context");
275
- const cwd = process.cwd();
276
- const infernoDir = path.join(cwd, "inferno");
277
-
278
- if (!fs.existsSync(infernoDir)) {
279
- warn("inferno/ not found. Run: infernoflow init");
280
- process.exit(1);
281
- }
282
-
283
- // Parse flags
284
- const dirIdx = args.indexOf("--dir");
285
- const intervalIdx = args.indexOf("--interval");
286
- const portIdx = args.indexOf("--port");
287
-
288
- const interval = intervalIdx !== -1 ? parseInt(args[intervalIdx + 1], 10) : 4;
289
- const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : null;
290
-
291
- const scanDirs = dirIdx !== -1
292
- ? args[dirIdx + 1].split(",").map(d => path.resolve(cwd, d.trim()))
293
- : ["src", "lib", "app", "api", "pages", "routes", "controllers", "handlers", "services"]
294
- .map(d => path.join(cwd, d))
295
- .filter(d => fs.existsSync(d));
296
-
297
- if (!scanDirs.length) {
298
- // Watch the whole project root as fallback
299
- scanDirs.push(cwd);
300
- }
301
-
302
- const opts = { silent, noSuggest, noContext, interval };
303
- const session = new VibeSession();
304
-
305
- // Snapshot initial state
306
- session.capsAtStart = readContractCapCount(infernoDir);
307
- session.capsNow = session.capsAtStart;
308
- session.scoreAtStart = readHealthScore(cwd);
309
-
310
- if (!silent) {
311
- console.log();
312
- console.log(` ${bold("πŸ”₯ infernoflow vibe")} ${gray("β€” vibe coding mode active")}`);
313
- console.log();
314
- console.log(` ${bold("Watching:")} ${scanDirs.map(d => path.relative(cwd, d) || ".").join(", ")}`);
315
- console.log(` ${bold("Debounce:")} ${interval}s`);
316
- if (noSuggest) console.log(` ${gray("--no-suggest: contract sync disabled")}`);
317
- if (noContext) console.log(` ${gray("--no-context: CONTEXT.md refresh disabled")}`);
318
- console.log();
319
- console.log(` ${cyan(String(session.capsAtStart))} capabilities in contract`);
320
- if (session.scoreAtStart !== null) console.log(` Health score: ${cyan(String(session.scoreAtStart))}/100`);
321
- console.log();
322
- console.log(` ${gray("Watching for file saves… Press Ctrl+C to stop and see session summary.")}`);
323
- console.log();
324
- }
325
-
326
- // Optional mini dashboard
327
- if (port) {
328
- startMiniDashboard(port, cwd, infernoDir);
329
- if (!silent) info(`Mini dashboard β†’ http://localhost:${port}`);
330
- }
331
-
332
- // Start watching
333
- const watchers = watchDirs(scanDirs, cwd, infernoDir, opts, session);
334
-
335
- if (!watchers.length) {
336
- warn("No directories to watch. Use --dir src,lib,api");
337
- process.exit(1);
338
- }
339
-
340
- // Spinner to show we're alive
341
- let spinTimer = null;
342
- if (!silent) {
343
- let idleMsg = "Watching…";
344
- spinTimer = setInterval(() => {
345
- tick(idleMsg, gray);
346
- }, 150);
347
- }
348
-
349
- // Graceful shutdown
350
- const shutdown = async () => {
351
- if (spinTimer) clearInterval(spinTimer);
352
- process.stdout.write("\r \r");
353
- watchers.forEach(w => { try { w.close(); } catch {} });
354
- session.scoreNow = readHealthScore(cwd);
355
- session.capsNow = readContractCapCount(infernoDir);
356
- if (!silent) session.printSummary();
357
- process.exit(0);
358
- };
359
-
360
- process.on("SIGINT", shutdown);
361
- process.on("SIGTERM", shutdown);
362
-
363
- // Keep alive
364
- await new Promise(() => {});
365
- }
15
+ </script></body></html>`)});return e.listen(s,"127.0.0.1",()=>{}),e}async function K(s){const t=s.slice(1),o=t.includes("--silent"),e=t.includes("--no-suggest"),i=t.includes("--no-context"),n=process.cwd(),r=p.join(n,"inferno");m.existsSync(r)||(A("inferno/ not found. Run: infernoflow init"),process.exit(1));const g=t.indexOf("--dir"),f=t.indexOf("--interval"),c=t.indexOf("--port"),a=f!==-1?parseInt(t[f+1],10):4,S=c!==-1?parseInt(t[c+1],10):null,$=g!==-1?t[g+1].split(",").map(u=>p.resolve(n,u.trim())):["src","lib","app","api","pages","routes","controllers","handlers","services"].map(u=>p.join(n,u)).filter(u=>m.existsSync(u));$.length||$.push(n);const D={silent:o,noSuggest:e,noContext:i,interval:a},l=new H;l.capsAtStart=y(r),l.capsNow=l.capsAtStart,l.scoreAtStart=_(n),o||(console.log(),console.log(` ${d("\u{1F525} infernoflow vibe")} ${h("\u2014 vibe coding mode active")}`),console.log(),console.log(` ${d("Watching:")} ${$.map(u=>p.relative(n,u)||".").join(", ")}`),console.log(` ${d("Debounce:")} ${a}s`),e&&console.log(` ${h("--no-suggest: contract sync disabled")}`),i&&console.log(` ${h("--no-context: CONTEXT.md refresh disabled")}`),console.log(),console.log(` ${w(String(l.capsAtStart))} capabilities in contract`),l.scoreAtStart!==null&&console.log(` Health score: ${w(String(l.scoreAtStart))}/100`),console.log(),console.log(` ${h("Watching for file saves\u2026 Press Ctrl+C to stop and see session summary.")}`),console.log()),S&&(J(S,n,r),o||k(`Mini dashboard \u2192 http://localhost:${S}`));const T=G($,n,r,D,l);T.length||(A("No directories to watch. Use --dir src,lib,api"),process.exit(1));let b=null;if(!o){let u="Watching\u2026";b=setInterval(()=>{x(u,h)},150)}const O=async()=>{b&&clearInterval(b),process.stdout.write("\r \r"),T.forEach(u=>{try{u.close()}catch{}}),l.scoreNow=_(n),l.capsNow=y(r),o||l.printSummary(),process.exit(0)};process.on("SIGINT",O),process.on("SIGTERM",O),await new Promise(()=>{})}export{K as vibeCommand};