infernoflow 0.14.0 → 0.17.0

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.
@@ -0,0 +1,203 @@
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
+ */
21
+
22
+ import * as fs from "node:fs";
23
+ import * as path from "node:path";
24
+ import { execSync, spawnSync } from "node:child_process";
25
+ import { ok, warn, info, bold, cyan, gray, green, yellow } from "../ui/output.mjs";
26
+
27
+ // ── Source file detection ─────────────────────────────────────────────────────
28
+
29
+ const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".cs", ".rb", ".swift"]);
30
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".angular", "vendor", "coverage", "__pycache__"]);
31
+
32
+ function defaultWatchDirs(cwd) {
33
+ const candidates = ["src", "lib", "app", "pages", "components", "server", "api"];
34
+ const found = candidates.filter(d => fs.existsSync(path.join(cwd, d)));
35
+ return found.length ? found.map(d => path.join(cwd, d)) : [cwd];
36
+ }
37
+
38
+ function isSourceFile(filePath) {
39
+ return SOURCE_EXTS.has(path.extname(filePath).toLowerCase());
40
+ }
41
+
42
+ // ── Capability relevance check ────────────────────────────────────────────────
43
+
44
+ function capabilityRelevance(changedFiles, infernoDir) {
45
+ const mapPath = path.join(infernoDir, "capability-map.json");
46
+ if (!fs.existsSync(mapPath)) return { relevant: true, reason: "no cap-map — suggesting broadly" };
47
+
48
+ let capMap;
49
+ try { capMap = JSON.parse(fs.readFileSync(mapPath, "utf8")); } catch { return { relevant: true, reason: "cap-map unreadable" }; }
50
+
51
+ const hits = [];
52
+ for (const file of changedFiles) {
53
+ const rel = path.relative(process.cwd(), file).replace(/\\/g, "/");
54
+ for (const [prefix, capIds] of Object.entries(capMap)) {
55
+ if (rel.startsWith(prefix.replace(/\\/g, "/"))) {
56
+ hits.push(...capIds);
57
+ }
58
+ }
59
+ }
60
+
61
+ if (hits.length > 0) return { relevant: true, reason: `touches: ${[...new Set(hits)].slice(0,3).join(", ")}` };
62
+ return { relevant: false, reason: "no mapped capabilities affected" };
63
+ }
64
+
65
+ // ── Run suggest + check ───────────────────────────────────────────────────────
66
+
67
+ function runSuggest(changedFiles, cwd, infernoDir, dryRun, silent) {
68
+ const names = changedFiles.map(f => path.basename(f, path.extname(f))).slice(0, 3).join(", ");
69
+ const desc = `code changes in ${names}`;
70
+
71
+ if (!silent) {
72
+ process.stdout.write(` ${yellow("⟳")} suggesting from ${bold(String(changedFiles.length))} changed file${changedFiles.length !== 1 ? "s" : ""}… `);
73
+ }
74
+
75
+ if (dryRun) {
76
+ if (!silent) console.log(gray("(dry run)"));
77
+ return;
78
+ }
79
+
80
+ try {
81
+ spawnSync(process.execPath, [
82
+ path.join(path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))), "bin", "infernoflow.mjs"),
83
+ "suggest", desc, "--json"
84
+ ], { cwd, encoding: "utf8", timeout: 30_000, stdio: "ignore" });
85
+
86
+ if (!silent) console.log(green("done"));
87
+ } catch {
88
+ if (!silent) console.log(gray("skipped (no changes)"));
89
+ }
90
+
91
+ // Silent check — write issues to WATCH.log
92
+ try {
93
+ const result = spawnSync(process.execPath, [
94
+ path.join(path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))), "bin", "infernoflow.mjs"),
95
+ "check", "--json"
96
+ ], { cwd, encoding: "utf8", timeout: 15_000 });
97
+
98
+ const out = result.stdout?.trim();
99
+ if (out) {
100
+ const data = JSON.parse(out);
101
+ if (data.status === "error" || data.status === "warning") {
102
+ fs.writeFileSync(path.join(infernoDir, "WATCH.log"), out + "\n");
103
+ if (!silent) warn(`Contract issues detected — see inferno/WATCH.log`);
104
+ } else {
105
+ const logPath = path.join(infernoDir, "WATCH.log");
106
+ if (fs.existsSync(logPath)) fs.unlinkSync(logPath);
107
+ }
108
+ }
109
+ } catch {}
110
+ }
111
+
112
+ // ── Watcher ───────────────────────────────────────────────────────────────────
113
+
114
+ export async function watchCommand(rawArgs) {
115
+ const args = rawArgs.slice(1);
116
+ const dryRun = args.includes("--dry-run");
117
+ const silent = args.includes("--silent");
118
+ const intervalIdx = args.indexOf("--interval");
119
+ const debounceMs = ((intervalIdx !== -1 ? parseFloat(args[intervalIdx + 1]) : 3) || 3) * 1000;
120
+ const cwd = process.cwd();
121
+ const infernoDir = path.join(cwd, "inferno");
122
+
123
+ if (!fs.existsSync(infernoDir)) {
124
+ warn("inferno/ not found. Run: infernoflow init");
125
+ process.exit(1);
126
+ }
127
+
128
+ // Collect directories to watch
129
+ const dirArgs = args.filter(a => !a.startsWith("-") && a !== String(args[intervalIdx + 1]));
130
+ const watchDirs = dirArgs.length
131
+ ? dirArgs.map(d => path.resolve(cwd, d))
132
+ : defaultWatchDirs(cwd);
133
+
134
+ const validDirs = watchDirs.filter(d => fs.existsSync(d));
135
+ if (!validDirs.length) {
136
+ warn("No valid directories to watch.");
137
+ process.exit(1);
138
+ }
139
+
140
+ if (!silent) {
141
+ console.log();
142
+ console.log(` ${bold("🔥 infernoflow watch")} ${gray("(Ctrl+C to stop)")}`);
143
+ console.log();
144
+ validDirs.forEach(d => console.log(` ${cyan("watching")} ${gray(path.relative(cwd, d) || ".")}`));
145
+ console.log(` ${gray(`debounce: ${debounceMs / 1000}s`)}`);
146
+ console.log();
147
+ }
148
+
149
+ let debounceTimer = null;
150
+ const pendingFiles = new Set();
151
+
152
+ const handleChange = (filePath) => {
153
+ if (!isSourceFile(filePath)) return;
154
+ pendingFiles.add(filePath);
155
+
156
+ if (debounceTimer) clearTimeout(debounceTimer);
157
+ debounceTimer = setTimeout(() => {
158
+ const changed = Array.from(pendingFiles);
159
+ pendingFiles.clear();
160
+
161
+ if (!silent) {
162
+ const names = changed.map(f => path.relative(cwd, f)).slice(0, 3).join(", ");
163
+ process.stdout.write(`\n ${gray(new Date().toLocaleTimeString())} ${bold(names)}${changed.length > 3 ? ` +${changed.length - 3} more` : ""} `);
164
+ }
165
+
166
+ const { relevant, reason } = capabilityRelevance(changed, infernoDir);
167
+ if (!relevant) {
168
+ if (!silent) console.log(gray(`skip (${reason})`));
169
+ return;
170
+ }
171
+
172
+ runSuggest(changed, cwd, infernoDir, dryRun, silent);
173
+ }, debounceMs);
174
+ };
175
+
176
+ // Start watchers on each directory
177
+ const watchers = [];
178
+ for (const dir of validDirs) {
179
+ try {
180
+ const watcher = fs.watch(dir, { recursive: true }, (event, filename) => {
181
+ if (filename) handleChange(path.join(dir, filename));
182
+ });
183
+ watchers.push(watcher);
184
+ } catch (err) {
185
+ if (!silent) warn(`Cannot watch ${dir}: ${err.message}`);
186
+ }
187
+ }
188
+
189
+ if (!watchers.length) {
190
+ warn("No directories could be watched.");
191
+ process.exit(1);
192
+ }
193
+
194
+ // Keep alive
195
+ process.on("SIGINT", () => {
196
+ watchers.forEach(w => w.close());
197
+ if (!silent) { console.log("\n\n Stopped."); console.log(); }
198
+ process.exit(0);
199
+ });
200
+
201
+ // Block forever
202
+ await new Promise(() => {});
203
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.14.0",
3
+ "version": "0.17.0",
4
4
  "description": "The forge for liquid code - keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {