infernoflow 0.32.8 → 0.32.9
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/dist/bin/infernoflow.mjs +84 -255
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +25 -130
- package/dist/lib/commands/cloud.mjs +5 -521
- package/dist/lib/commands/context.mjs +31 -287
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +203 -321
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +23 -475
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +5 -558
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -72
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
- package/dist/templates/github-app/GITHUB_APP.md +67 -0
- package/dist/templates/github-app/app-manifest.json +20 -0
- package/package.json +1 -1
|
@@ -1,365 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
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
|
|
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
|
|
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};
|
|
@@ -1,204 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
* infernoflow watch
|
|
3
|
-
|
|
4
|
-
* File-system watcher that runs `infernoflow suggest` automatically whenever
|
|
5
|
-
* source files are saved. Zero manual steps — just code, save, and the
|
|
6
|
-
* contract stays in sync.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* infernoflow watch Watch src/ (or auto-detected root)
|
|
10
|
-
* infernoflow watch src lib Watch specific directories
|
|
11
|
-
* infernoflow watch --interval 5 Debounce interval in seconds (default: 3)
|
|
12
|
-
* infernoflow watch --dry-run Print what would run, don't actually run
|
|
13
|
-
* infernoflow watch --silent No output (git-hook friendly)
|
|
14
|
-
*
|
|
15
|
-
* What it does on each save:
|
|
16
|
-
* 1. Debounce (3 s default) — batches rapid multi-file saves
|
|
17
|
-
* 2. Diff changed files against capability-map.json
|
|
18
|
-
* 3. If relevant capabilities may be affected → run suggest
|
|
19
|
-
* 4. Run check silently — log issues to inferno/WATCH.log
|
|
20
|
-
*/
|
|
1
|
+
import*as f from"node:fs";import*as e from"node:path";import{fileURLToPath as $}from"node:url";import{spawnSync as b}from"node:child_process";import{warn as m,bold as x,cyan as D,gray as d,green as I,yellow as P}from"../ui/output.mjs";const R=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".py",".go",".java",".cs",".rb",".swift"]),M=new Set(["node_modules",".git","dist","build","out",".next",".angular","vendor","coverage","__pycache__"]);function W(t){const i=["src","lib","app","pages","components","server","api"].filter(o=>f.existsSync(e.join(t,o)));return i.length?i.map(o=>e.join(t,o)):[t]}function k(t){return R.has(e.extname(t).toLowerCase())}function A(t,r){const i=e.join(r,"capability-map.json");if(!f.existsSync(i))return{relevant:!0,reason:"no cap-map \u2014 suggesting broadly"};let o;try{o=JSON.parse(f.readFileSync(i,"utf8"))}catch{return{relevant:!0,reason:"cap-map unreadable"}}const s=[];for(const g of t){const c=e.relative(process.cwd(),g).replace(/\\/g,"/");for(const[a,u]of Object.entries(o))c.startsWith(a.replace(/\\/g,"/"))&&s.push(...u)}return s.length>0?{relevant:!0,reason:`touches: ${[...new Set(s)].slice(0,3).join(", ")}`}:{relevant:!1,reason:"no mapped capabilities affected"}}function N(t,r,i,o,s){const c=`code changes in ${t.map(a=>e.basename(a,e.extname(a))).slice(0,3).join(", ")}`;if(s||process.stdout.write(` ${P("\u27F3")} suggesting from ${x(String(t.length))} changed file${t.length!==1?"s":""}\u2026 `),o){s||console.log(d("(dry run)"));return}try{b(process.execPath,[e.join(e.dirname(e.dirname(e.dirname($(import.meta.url)))),"bin","infernoflow.mjs"),"suggest",c,"--json"],{cwd:r,encoding:"utf8",timeout:3e4,stdio:"ignore"}),s||console.log(I("done"))}catch{s||console.log(d("skipped (no changes)"))}try{const u=b(process.execPath,[e.join(e.dirname(e.dirname(e.dirname($(import.meta.url)))),"bin","infernoflow.mjs"),"check","--json"],{cwd:r,encoding:"utf8",timeout:15e3}).stdout?.trim();if(u){const w=JSON.parse(u);if(w.status==="error"||w.status==="warning")f.writeFileSync(e.join(i,"WATCH.log"),u+`
|
|
2
|
+
`),s||m("Contract issues detected \u2014 see inferno/WATCH.log");else{const p=e.join(i,"WATCH.log");f.existsSync(p)&&f.unlinkSync(p)}}}catch{}}async function U(t){const r=t.slice(1),i=r.includes("--dry-run"),o=r.includes("--silent"),s=r.indexOf("--interval"),g=((s!==-1?parseFloat(r[s+1]):3)||3)*1e3,c=process.cwd(),a=e.join(c,"inferno");f.existsSync(a)||(m("inferno/ not found. Run: infernoflow init"),process.exit(1));const u=r.filter(n=>!n.startsWith("-")&&n!==String(r[s+1])),p=(u.length?u.map(n=>e.resolve(c,n)):W(c)).filter(n=>f.existsSync(n));p.length||(m("No valid directories to watch."),process.exit(1)),o||(console.log(),console.log(` ${x("\u{1F525} infernoflow watch")} ${d("(Ctrl+C to stop)")}`),console.log(),p.forEach(n=>console.log(` ${D("watching")} ${d(e.relative(c,n)||".")}`)),console.log(` ${d(`debounce: ${g/1e3}s`)}`),console.log());let y=null;const S=new Set,C=n=>{k(n)&&(S.add(n),y&&clearTimeout(y),y=setTimeout(()=>{const l=Array.from(S);if(S.clear(),!o){const T=l.map(_=>e.relative(c,_)).slice(0,3).join(", ");process.stdout.write(`
|
|
3
|
+
${d(new Date().toLocaleTimeString())} ${x(T)}${l.length>3?` +${l.length-3} more`:""} `)}const{relevant:v,reason:h}=A(l,a);if(!v){o||console.log(d(`skip (${h})`));return}N(l,c,a,i,o)},g))},j=[];for(const n of p)try{const l=f.watch(n,{recursive:!0},(v,h)=>{h&&C(e.join(n,h))});j.push(l)}catch(l){o||m(`Cannot watch ${n}: ${l.message}`)}j.length||(m("No directories could be watched."),process.exit(1)),process.on("SIGINT",()=>{j.forEach(n=>n.close()),o||(console.log(`
|
|
21
4
|
|
|
22
|
-
|
|
23
|
-
import * as path from "node:path";
|
|
24
|
-
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
26
|
-
import { ok, warn, info, bold, cyan, gray, green, yellow } from "../ui/output.mjs";
|
|
27
|
-
|
|
28
|
-
// ── Source file detection ─────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".cs", ".rb", ".swift"]);
|
|
31
|
-
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".angular", "vendor", "coverage", "__pycache__"]);
|
|
32
|
-
|
|
33
|
-
function defaultWatchDirs(cwd) {
|
|
34
|
-
const candidates = ["src", "lib", "app", "pages", "components", "server", "api"];
|
|
35
|
-
const found = candidates.filter(d => fs.existsSync(path.join(cwd, d)));
|
|
36
|
-
return found.length ? found.map(d => path.join(cwd, d)) : [cwd];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isSourceFile(filePath) {
|
|
40
|
-
return SOURCE_EXTS.has(path.extname(filePath).toLowerCase());
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ── Capability relevance check ────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
function capabilityRelevance(changedFiles, infernoDir) {
|
|
46
|
-
const mapPath = path.join(infernoDir, "capability-map.json");
|
|
47
|
-
if (!fs.existsSync(mapPath)) return { relevant: true, reason: "no cap-map — suggesting broadly" };
|
|
48
|
-
|
|
49
|
-
let capMap;
|
|
50
|
-
try { capMap = JSON.parse(fs.readFileSync(mapPath, "utf8")); } catch { return { relevant: true, reason: "cap-map unreadable" }; }
|
|
51
|
-
|
|
52
|
-
const hits = [];
|
|
53
|
-
for (const file of changedFiles) {
|
|
54
|
-
const rel = path.relative(process.cwd(), file).replace(/\\/g, "/");
|
|
55
|
-
for (const [prefix, capIds] of Object.entries(capMap)) {
|
|
56
|
-
if (rel.startsWith(prefix.replace(/\\/g, "/"))) {
|
|
57
|
-
hits.push(...capIds);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (hits.length > 0) return { relevant: true, reason: `touches: ${[...new Set(hits)].slice(0,3).join(", ")}` };
|
|
63
|
-
return { relevant: false, reason: "no mapped capabilities affected" };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Run suggest + check ───────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
function runSuggest(changedFiles, cwd, infernoDir, dryRun, silent) {
|
|
69
|
-
const names = changedFiles.map(f => path.basename(f, path.extname(f))).slice(0, 3).join(", ");
|
|
70
|
-
const desc = `code changes in ${names}`;
|
|
71
|
-
|
|
72
|
-
if (!silent) {
|
|
73
|
-
process.stdout.write(` ${yellow("⟳")} suggesting from ${bold(String(changedFiles.length))} changed file${changedFiles.length !== 1 ? "s" : ""}… `);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (dryRun) {
|
|
77
|
-
if (!silent) console.log(gray("(dry run)"));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
spawnSync(process.execPath, [
|
|
83
|
-
path.join(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), "bin", "infernoflow.mjs"),
|
|
84
|
-
"suggest", desc, "--json"
|
|
85
|
-
], { cwd, encoding: "utf8", timeout: 30_000, stdio: "ignore" });
|
|
86
|
-
|
|
87
|
-
if (!silent) console.log(green("done"));
|
|
88
|
-
} catch {
|
|
89
|
-
if (!silent) console.log(gray("skipped (no changes)"));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Silent check — write issues to WATCH.log
|
|
93
|
-
try {
|
|
94
|
-
const result = spawnSync(process.execPath, [
|
|
95
|
-
path.join(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), "bin", "infernoflow.mjs"),
|
|
96
|
-
"check", "--json"
|
|
97
|
-
], { cwd, encoding: "utf8", timeout: 15_000 });
|
|
98
|
-
|
|
99
|
-
const out = result.stdout?.trim();
|
|
100
|
-
if (out) {
|
|
101
|
-
const data = JSON.parse(out);
|
|
102
|
-
if (data.status === "error" || data.status === "warning") {
|
|
103
|
-
fs.writeFileSync(path.join(infernoDir, "WATCH.log"), out + "\n");
|
|
104
|
-
if (!silent) warn(`Contract issues detected — see inferno/WATCH.log`);
|
|
105
|
-
} else {
|
|
106
|
-
const logPath = path.join(infernoDir, "WATCH.log");
|
|
107
|
-
if (fs.existsSync(logPath)) fs.unlinkSync(logPath);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} catch {}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Watcher ───────────────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
export async function watchCommand(rawArgs) {
|
|
116
|
-
const args = rawArgs.slice(1);
|
|
117
|
-
const dryRun = args.includes("--dry-run");
|
|
118
|
-
const silent = args.includes("--silent");
|
|
119
|
-
const intervalIdx = args.indexOf("--interval");
|
|
120
|
-
const debounceMs = ((intervalIdx !== -1 ? parseFloat(args[intervalIdx + 1]) : 3) || 3) * 1000;
|
|
121
|
-
const cwd = process.cwd();
|
|
122
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
123
|
-
|
|
124
|
-
if (!fs.existsSync(infernoDir)) {
|
|
125
|
-
warn("inferno/ not found. Run: infernoflow init");
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Collect directories to watch
|
|
130
|
-
const dirArgs = args.filter(a => !a.startsWith("-") && a !== String(args[intervalIdx + 1]));
|
|
131
|
-
const watchDirs = dirArgs.length
|
|
132
|
-
? dirArgs.map(d => path.resolve(cwd, d))
|
|
133
|
-
: defaultWatchDirs(cwd);
|
|
134
|
-
|
|
135
|
-
const validDirs = watchDirs.filter(d => fs.existsSync(d));
|
|
136
|
-
if (!validDirs.length) {
|
|
137
|
-
warn("No valid directories to watch.");
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (!silent) {
|
|
142
|
-
console.log();
|
|
143
|
-
console.log(` ${bold("🔥 infernoflow watch")} ${gray("(Ctrl+C to stop)")}`);
|
|
144
|
-
console.log();
|
|
145
|
-
validDirs.forEach(d => console.log(` ${cyan("watching")} ${gray(path.relative(cwd, d) || ".")}`));
|
|
146
|
-
console.log(` ${gray(`debounce: ${debounceMs / 1000}s`)}`);
|
|
147
|
-
console.log();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
let debounceTimer = null;
|
|
151
|
-
const pendingFiles = new Set();
|
|
152
|
-
|
|
153
|
-
const handleChange = (filePath) => {
|
|
154
|
-
if (!isSourceFile(filePath)) return;
|
|
155
|
-
pendingFiles.add(filePath);
|
|
156
|
-
|
|
157
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
158
|
-
debounceTimer = setTimeout(() => {
|
|
159
|
-
const changed = Array.from(pendingFiles);
|
|
160
|
-
pendingFiles.clear();
|
|
161
|
-
|
|
162
|
-
if (!silent) {
|
|
163
|
-
const names = changed.map(f => path.relative(cwd, f)).slice(0, 3).join(", ");
|
|
164
|
-
process.stdout.write(`\n ${gray(new Date().toLocaleTimeString())} ${bold(names)}${changed.length > 3 ? ` +${changed.length - 3} more` : ""} `);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const { relevant, reason } = capabilityRelevance(changed, infernoDir);
|
|
168
|
-
if (!relevant) {
|
|
169
|
-
if (!silent) console.log(gray(`skip (${reason})`));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
runSuggest(changed, cwd, infernoDir, dryRun, silent);
|
|
174
|
-
}, debounceMs);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// Start watchers on each directory
|
|
178
|
-
const watchers = [];
|
|
179
|
-
for (const dir of validDirs) {
|
|
180
|
-
try {
|
|
181
|
-
const watcher = fs.watch(dir, { recursive: true }, (event, filename) => {
|
|
182
|
-
if (filename) handleChange(path.join(dir, filename));
|
|
183
|
-
});
|
|
184
|
-
watchers.push(watcher);
|
|
185
|
-
} catch (err) {
|
|
186
|
-
if (!silent) warn(`Cannot watch ${dir}: ${err.message}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (!watchers.length) {
|
|
191
|
-
warn("No directories could be watched.");
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Keep alive
|
|
196
|
-
process.on("SIGINT", () => {
|
|
197
|
-
watchers.forEach(w => w.close());
|
|
198
|
-
if (!silent) { console.log("\n\n Stopped."); console.log(); }
|
|
199
|
-
process.exit(0);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// Block forever
|
|
203
|
-
await new Promise(() => {});
|
|
204
|
-
}
|
|
5
|
+
Stopped.`),console.log()),process.exit(0)}),await new Promise(()=>{})}export{U as watchCommand};
|