@laphilosophia/steady-watch 2.0.3 → 2.1.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.
- package/README.md +76 -2
- package/bin/steady-watch +1 -1
- package/dist/index.d.mts +107 -7
- package/dist/index.d.ts +107 -7
- package/dist/index.js +598 -119
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +594 -119
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -5
- package/scripts/smoke-tests.mjs +277 -0
package/dist/index.js
CHANGED
|
@@ -30,8 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
CLI_VERSION: () => CLI_VERSION,
|
|
33
34
|
SteadyWatcher: () => SteadyWatcher,
|
|
34
35
|
getTheme: () => getTheme,
|
|
36
|
+
loadConfig: () => loadConfig,
|
|
37
|
+
mergeOptions: () => mergeOptions,
|
|
38
|
+
parseCliArgs: () => parseCliArgs,
|
|
35
39
|
runCli: () => runCli,
|
|
36
40
|
steadyWatch: () => steadyWatch,
|
|
37
41
|
themes: () => themes
|
|
@@ -87,13 +91,19 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
87
91
|
constructor(options) {
|
|
88
92
|
super();
|
|
89
93
|
this.watcher = null;
|
|
94
|
+
this.watcherReady = false;
|
|
90
95
|
this.fileHashes = /* @__PURE__ */ new Map();
|
|
96
|
+
this.gitChangedFiles = /* @__PURE__ */ new Set();
|
|
91
97
|
this.timeout = null;
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
94
|
-
this.
|
|
98
|
+
this.cycleRunning = false;
|
|
99
|
+
this.pendingCycle = false;
|
|
100
|
+
this.lifecycleProcess = null;
|
|
101
|
+
this.lifecycleKillTimer = null;
|
|
102
|
+
this.startedProcess = null;
|
|
103
|
+
this.stoppingStartedProcess = null;
|
|
95
104
|
this.retryCount = 0;
|
|
96
105
|
this.disposed = false;
|
|
106
|
+
this.closePromise = null;
|
|
97
107
|
this.options = this.normalizeOptions(options);
|
|
98
108
|
this.t = getTheme(this.options.theme);
|
|
99
109
|
}
|
|
@@ -104,15 +114,34 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
104
114
|
..._SteadyWatcher.DEFAULT_IGNORE,
|
|
105
115
|
...this.normalizeIgnorePatterns(options.ignore || [])
|
|
106
116
|
];
|
|
117
|
+
const start = options.start || "";
|
|
118
|
+
const hooks = {
|
|
119
|
+
...options.hooks || {},
|
|
120
|
+
beforeCommand: options.beforeCommand || options.hooks?.beforeCommand,
|
|
121
|
+
afterCommand: options.afterCommand || options.hooks?.afterCommand,
|
|
122
|
+
beforeStop: options.beforeStop || options.hooks?.beforeStop,
|
|
123
|
+
afterStop: options.afterStop || options.hooks?.afterStop,
|
|
124
|
+
beforeStart: options.beforeStart || options.hooks?.beforeStart,
|
|
125
|
+
afterStart: options.afterStart || options.hooks?.afterStart,
|
|
126
|
+
onSkip: options.onSkip || options.hooks?.onSkip
|
|
127
|
+
};
|
|
107
128
|
return {
|
|
108
129
|
pattern: options.pattern,
|
|
109
|
-
cmd: options.cmd,
|
|
130
|
+
cmd: options.cmd || "",
|
|
131
|
+
start,
|
|
132
|
+
restartOnSuccess: options.restartOnSuccess ?? false,
|
|
133
|
+
restartOnChange: options.restartOnChange ?? false,
|
|
134
|
+
initialRun: options.initialRun ?? Boolean(start),
|
|
135
|
+
hooks,
|
|
110
136
|
delay: Math.max(0, options.delay ?? 300),
|
|
111
137
|
verbose: options.verbose ?? false,
|
|
112
138
|
quiet: options.quiet ?? false,
|
|
113
139
|
ignore: mergedIgnore,
|
|
114
140
|
ext: options.ext || [],
|
|
115
|
-
|
|
141
|
+
gitChanged: options.gitChanged ?? false,
|
|
142
|
+
gitBase: options.gitBase || "HEAD",
|
|
143
|
+
killTimeout: Math.max(0, options.killTimeout ?? (start ? 5e3 : 0)),
|
|
144
|
+
restartDelay: Math.max(0, options.restartDelay ?? 0),
|
|
116
145
|
retry: Math.max(0, options.retry ?? 0),
|
|
117
146
|
hash,
|
|
118
147
|
mtimeOnly: options.mtimeOnly ?? false,
|
|
@@ -136,8 +165,23 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
136
165
|
if (!this.options.pattern) {
|
|
137
166
|
errors.push("Pattern is required");
|
|
138
167
|
}
|
|
139
|
-
if (!this.options.cmd) {
|
|
140
|
-
errors.push("Command is required");
|
|
168
|
+
if (!this.options.cmd && !this.options.start) {
|
|
169
|
+
errors.push("Command is required. Use --cmd, or use --start with --restart-on-change");
|
|
170
|
+
}
|
|
171
|
+
if (this.options.start && !this.options.restartOnSuccess && !this.options.restartOnChange) {
|
|
172
|
+
errors.push("A start command requires --restart-on-success or --restart-on-change");
|
|
173
|
+
}
|
|
174
|
+
if (this.options.restartOnSuccess && (!this.options.cmd || !this.options.start)) {
|
|
175
|
+
errors.push("--restart-on-success requires both --cmd and --start");
|
|
176
|
+
}
|
|
177
|
+
if (this.options.restartOnChange && !this.options.start) {
|
|
178
|
+
errors.push("--restart-on-change requires --start");
|
|
179
|
+
}
|
|
180
|
+
if (this.options.restartOnChange && this.options.cmd) {
|
|
181
|
+
errors.push("--restart-on-change cannot be combined with --cmd; use --restart-on-success");
|
|
182
|
+
}
|
|
183
|
+
if (!this.options.cmd && this.options.start && !this.options.restartOnChange) {
|
|
184
|
+
errors.push("--restart-on-change is required when --start is used without --cmd");
|
|
141
185
|
}
|
|
142
186
|
if (this.options.delay < 0) {
|
|
143
187
|
errors.push("Delay must be a non-negative number");
|
|
@@ -145,6 +189,12 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
145
189
|
if (this.options.killTimeout < 0) {
|
|
146
190
|
errors.push("Kill timeout must be a non-negative number");
|
|
147
191
|
}
|
|
192
|
+
if (this.options.restartDelay < 0) {
|
|
193
|
+
errors.push("Restart delay must be a non-negative number");
|
|
194
|
+
}
|
|
195
|
+
if (this.options.gitChanged && !this.options.gitBase) {
|
|
196
|
+
errors.push("Git base must be set when git-changed mode is enabled");
|
|
197
|
+
}
|
|
148
198
|
if (this.options.retry < 0) {
|
|
149
199
|
errors.push("Retry must be a non-negative number");
|
|
150
200
|
}
|
|
@@ -171,6 +221,36 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
171
221
|
return null;
|
|
172
222
|
}
|
|
173
223
|
}
|
|
224
|
+
normalizeGitPath(filePath) {
|
|
225
|
+
return import_path.default.relative(process.cwd(), import_path.default.resolve(filePath)).replace(/\\/g, "/");
|
|
226
|
+
}
|
|
227
|
+
runGit(args) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
(0, import_child_process.execFile)("git", args, { cwd: process.cwd(), maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
230
|
+
if (error) {
|
|
231
|
+
const message = stderr.trim() || error.message;
|
|
232
|
+
reject(new Error(`git ${args.join(" ")} failed: ${message}`));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
resolve(stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async refreshGitChangedFiles() {
|
|
240
|
+
if (!this.options.gitChanged) return;
|
|
241
|
+
const [unstaged, staged, untracked] = await Promise.all([
|
|
242
|
+
this.runGit(["diff", "--name-only", "--relative", this.options.gitBase, "--"]),
|
|
243
|
+
this.runGit(["diff", "--name-only", "--cached", "--relative", this.options.gitBase, "--"]),
|
|
244
|
+
this.runGit(["ls-files", "--others", "--exclude-standard"])
|
|
245
|
+
]);
|
|
246
|
+
this.gitChangedFiles = new Set([...unstaged, ...staged, ...untracked].map((file) => file.replace(/\\/g, "/")));
|
|
247
|
+
}
|
|
248
|
+
async isGitChangedFile(filePath) {
|
|
249
|
+
if (!this.options.gitChanged) return true;
|
|
250
|
+
await this.refreshGitChangedFiles();
|
|
251
|
+
const relativePath = this.normalizeGitPath(filePath);
|
|
252
|
+
return this.gitChangedFiles.has(relativePath);
|
|
253
|
+
}
|
|
174
254
|
log(...args) {
|
|
175
255
|
if (!this.options.quiet && !this.disposed) console.log(...args);
|
|
176
256
|
}
|
|
@@ -187,6 +267,33 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
187
267
|
timestamp() {
|
|
188
268
|
return this.t.gray(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}]`);
|
|
189
269
|
}
|
|
270
|
+
async runHook(hook, context, mode) {
|
|
271
|
+
const callback = this.options.hooks[hook];
|
|
272
|
+
if (!callback) return true;
|
|
273
|
+
const hookContext = {
|
|
274
|
+
phase: hook,
|
|
275
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
276
|
+
...context
|
|
277
|
+
};
|
|
278
|
+
try {
|
|
279
|
+
await callback(Object.freeze(hookContext));
|
|
280
|
+
return true;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
283
|
+
this.log(this.t.red(`Hook ${hook} failed: ${err.message}`));
|
|
284
|
+
this.logJson("hook_error", { hook, error: err.message });
|
|
285
|
+
this.emit("hookError", hook, err);
|
|
286
|
+
if (mode === "notify") return true;
|
|
287
|
+
this.emit("fail", null);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
notifyHook(hook, context) {
|
|
292
|
+
void this.runHook(hook, context, "notify");
|
|
293
|
+
}
|
|
294
|
+
sleep(ms) {
|
|
295
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
296
|
+
}
|
|
190
297
|
parseCommand(cmdString) {
|
|
191
298
|
const tokens = [];
|
|
192
299
|
let current = "";
|
|
@@ -195,7 +302,6 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
195
302
|
let escaped = false;
|
|
196
303
|
for (let i = 0; i < cmdString.length; i++) {
|
|
197
304
|
const char = cmdString[i];
|
|
198
|
-
const prevChar = i > 0 ? cmdString[i - 1] : "";
|
|
199
305
|
if (escaped) {
|
|
200
306
|
current += char;
|
|
201
307
|
escaped = false;
|
|
@@ -225,92 +331,380 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
225
331
|
}
|
|
226
332
|
return { cmd: tokens[0] || "", args: tokens.slice(1) };
|
|
227
333
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
334
|
+
commandNeedsShell(command) {
|
|
335
|
+
return /(?:&&|\|\||[|<>])/.test(command);
|
|
336
|
+
}
|
|
337
|
+
resolveWindowsCommand(command) {
|
|
338
|
+
if (process.platform !== "win32") return null;
|
|
339
|
+
if (import_path.default.isAbsolute(command) || command.includes("\\") || command.includes("/")) {
|
|
340
|
+
return import_fs.default.existsSync(command) ? command : null;
|
|
234
341
|
}
|
|
235
|
-
|
|
236
|
-
|
|
342
|
+
const pathEntries = (process.env.PATH || "").split(import_path.default.delimiter).filter(Boolean);
|
|
343
|
+
const pathExts = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
344
|
+
const hasExt = Boolean(import_path.default.extname(command));
|
|
345
|
+
const candidates = hasExt ? [command] : pathExts.map((ext) => `${command}${ext.toLowerCase()}`);
|
|
346
|
+
for (const entry of pathEntries) {
|
|
347
|
+
for (const candidate of candidates) {
|
|
348
|
+
const fullPath = import_path.default.join(entry, candidate);
|
|
349
|
+
if (import_fs.default.existsSync(fullPath)) return fullPath;
|
|
350
|
+
}
|
|
237
351
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
shouldUseShell(command, preferShell) {
|
|
355
|
+
if (preferShell || this.commandNeedsShell(command)) return true;
|
|
356
|
+
const parsed = this.parseCommand(command);
|
|
357
|
+
if (!parsed.cmd) return true;
|
|
358
|
+
if (process.platform !== "win32") return false;
|
|
359
|
+
const resolved = this.resolveWindowsCommand(parsed.cmd);
|
|
360
|
+
if (!resolved) return true;
|
|
361
|
+
return /\.(cmd|bat)$/i.test(resolved);
|
|
362
|
+
}
|
|
363
|
+
spawnCommand(command, preferShell) {
|
|
364
|
+
const useShell = this.shouldUseShell(command, preferShell);
|
|
365
|
+
const parsed = this.parseCommand(command);
|
|
366
|
+
const child = useShell ? (0, import_child_process.spawn)(command, {
|
|
246
367
|
stdio: "inherit",
|
|
247
368
|
shell: true,
|
|
248
|
-
env: { ...process.env }
|
|
369
|
+
env: { ...process.env },
|
|
370
|
+
detached: process.platform !== "win32",
|
|
371
|
+
windowsHide: false
|
|
372
|
+
}) : (0, import_child_process.spawn)(parsed.cmd, parsed.args, {
|
|
373
|
+
stdio: "inherit",
|
|
374
|
+
env: { ...process.env },
|
|
375
|
+
detached: process.platform !== "win32",
|
|
376
|
+
windowsHide: false
|
|
249
377
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
378
|
+
return Object.assign(child, { steadyWatchUsesShell: useShell });
|
|
379
|
+
}
|
|
380
|
+
runTaskkill(pid, force) {
|
|
381
|
+
return new Promise((resolve) => {
|
|
382
|
+
const args = ["/pid", String(pid), "/T"];
|
|
383
|
+
if (force) args.push("/F");
|
|
384
|
+
const killer = (0, import_child_process.spawn)("taskkill", args, {
|
|
385
|
+
stdio: "ignore",
|
|
386
|
+
windowsHide: true
|
|
387
|
+
});
|
|
388
|
+
let settled = false;
|
|
389
|
+
const finish = (ok) => {
|
|
390
|
+
if (settled) return;
|
|
391
|
+
settled = true;
|
|
392
|
+
resolve(ok);
|
|
393
|
+
};
|
|
394
|
+
killer.on("close", (code) => finish(code === 0));
|
|
395
|
+
killer.on("error", () => finish(false));
|
|
261
396
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (code === 0) {
|
|
272
|
-
this.log(`${this.timestamp()} ${this.t.green("\u2714 Done")} in ${duration}s`);
|
|
273
|
-
this.logJson("done", { duration: parseFloat(duration) });
|
|
274
|
-
this.emit("done", parseFloat(duration));
|
|
275
|
-
this.retryCount = 0;
|
|
397
|
+
}
|
|
398
|
+
async forceKillProcess(child, label) {
|
|
399
|
+
if (process.platform === "win32" && child.pid) {
|
|
400
|
+
const killedTree = await this.runTaskkill(child.pid, true);
|
|
401
|
+
if (killedTree) return;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
if (process.platform !== "win32" && child.pid) {
|
|
405
|
+
process.kill(-child.pid, "SIGKILL");
|
|
276
406
|
} else {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
407
|
+
child.kill("SIGKILL");
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
this.logVerbose(this.t.gray(`Process already exited before force kill: ${label}`));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
terminateProcess(child, command, label) {
|
|
414
|
+
return new Promise((resolve) => {
|
|
415
|
+
let settled = false;
|
|
416
|
+
const timeoutMs = this.options.killTimeout > 0 ? this.options.killTimeout : 5e3;
|
|
417
|
+
let forceTimer = null;
|
|
418
|
+
const finish = () => {
|
|
419
|
+
if (settled) return;
|
|
420
|
+
settled = true;
|
|
421
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
422
|
+
resolve();
|
|
423
|
+
};
|
|
424
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
425
|
+
finish();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
child.once("close", finish);
|
|
429
|
+
this.emit("stop", command, "SIGTERM");
|
|
430
|
+
const requestGracefulStop = async () => {
|
|
431
|
+
try {
|
|
432
|
+
if (process.platform === "win32" && child.pid) {
|
|
433
|
+
const killedTree = await this.runTaskkill(child.pid, false);
|
|
434
|
+
if (killedTree) {
|
|
435
|
+
finish();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (process.platform !== "win32" && child.pid) {
|
|
440
|
+
process.kill(-child.pid, "SIGTERM");
|
|
441
|
+
} else {
|
|
442
|
+
child.kill("SIGTERM");
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
finish();
|
|
285
446
|
return;
|
|
286
447
|
}
|
|
448
|
+
};
|
|
449
|
+
void requestGracefulStop();
|
|
450
|
+
forceTimer = setTimeout(() => {
|
|
451
|
+
if (!settled) {
|
|
452
|
+
this.log(this.t.yellow(`Process did not exit after ${timeoutMs}ms, force killing ${label}...`));
|
|
453
|
+
void (async () => {
|
|
454
|
+
await this.forceKillProcess(child, label);
|
|
455
|
+
finish();
|
|
456
|
+
})();
|
|
457
|
+
}
|
|
458
|
+
}, timeoutMs);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
runSingleCommand(attempt) {
|
|
462
|
+
if (this.disposed) {
|
|
463
|
+
return Promise.resolve({ code: null, signal: null, duration: 0 });
|
|
464
|
+
}
|
|
465
|
+
this.retryCount = attempt;
|
|
466
|
+
const retryInfo = attempt > 0 ? ` (Retry ${attempt}/${this.options.retry})` : "";
|
|
467
|
+
this.log(`${this.timestamp()} ${this.t.cyan("Triggering:")} ${this.t.bold(this.options.cmd)}${retryInfo}`);
|
|
468
|
+
this.logJson("trigger", { command: this.options.cmd, retry: attempt });
|
|
469
|
+
this.emit("trigger", this.options.cmd);
|
|
470
|
+
const startTime = Date.now();
|
|
471
|
+
const child = this.spawnCommand(this.options.cmd, false);
|
|
472
|
+
this.lifecycleProcess = child;
|
|
473
|
+
const commandTimeout = this.options.killTimeout;
|
|
474
|
+
if (commandTimeout > 0) {
|
|
475
|
+
this.lifecycleKillTimer = setTimeout(() => {
|
|
476
|
+
if (this.lifecycleProcess === child && !this.disposed) {
|
|
477
|
+
this.log(this.t.yellow(`Process timeout (${commandTimeout}ms), force killing...`));
|
|
478
|
+
void this.forceKillProcess(child, this.options.cmd);
|
|
479
|
+
}
|
|
480
|
+
}, commandTimeout);
|
|
481
|
+
}
|
|
482
|
+
return new Promise((resolve) => {
|
|
483
|
+
let settled = false;
|
|
484
|
+
const finish = (code, signal) => {
|
|
485
|
+
if (settled) return;
|
|
486
|
+
settled = true;
|
|
487
|
+
if (this.lifecycleKillTimer) {
|
|
488
|
+
clearTimeout(this.lifecycleKillTimer);
|
|
489
|
+
this.lifecycleKillTimer = null;
|
|
490
|
+
}
|
|
491
|
+
if (this.lifecycleProcess === child) {
|
|
492
|
+
this.lifecycleProcess = null;
|
|
493
|
+
}
|
|
494
|
+
const duration = (Date.now() - startTime) / 1e3;
|
|
495
|
+
if (!this.disposed) {
|
|
496
|
+
if (code === 0) {
|
|
497
|
+
this.log(`${this.timestamp()} ${this.t.green("Done")} in ${duration.toFixed(2)}s`);
|
|
498
|
+
this.logJson("done", { duration: parseFloat(duration.toFixed(2)) });
|
|
499
|
+
this.emit("done", parseFloat(duration.toFixed(2)));
|
|
500
|
+
} else {
|
|
501
|
+
const exitMessage = signal ? ` (Signal: ${signal})` : ` (Exit code: ${code})`;
|
|
502
|
+
this.log(`${this.timestamp()} ${this.t.red("Failed")}${exitMessage}`);
|
|
503
|
+
this.logJson("failed", { exitCode: code, signal });
|
|
504
|
+
this.emit("fail", code, signal ?? void 0);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
resolve({ code, signal, duration });
|
|
508
|
+
};
|
|
509
|
+
child.on("error", (err) => {
|
|
510
|
+
if (!this.disposed) {
|
|
511
|
+
this.log(this.t.red(`Process error: ${err.message}`));
|
|
512
|
+
}
|
|
513
|
+
finish(null, null);
|
|
514
|
+
});
|
|
515
|
+
child.on("close", finish);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
async runCommandWithRetry() {
|
|
519
|
+
let attempt = 0;
|
|
520
|
+
while (!this.disposed) {
|
|
521
|
+
const result = await this.runSingleCommand(attempt);
|
|
522
|
+
if (result.code === 0) {
|
|
287
523
|
this.retryCount = 0;
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
if (this.options.retry > 0 && attempt < this.options.retry) {
|
|
527
|
+
attempt++;
|
|
528
|
+
this.log(this.t.yellow(`Retrying in 1s... (${attempt}/${this.options.retry})`));
|
|
529
|
+
await this.sleep(1e3);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
this.retryCount = 0;
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
return { code: null, signal: null, duration: 0 };
|
|
536
|
+
}
|
|
537
|
+
async stopStartedProcess() {
|
|
538
|
+
if (!this.startedProcess) return;
|
|
539
|
+
const child = this.startedProcess;
|
|
540
|
+
const pid = child.pid;
|
|
541
|
+
this.stoppingStartedProcess = child;
|
|
542
|
+
await this.terminateProcess(child, this.options.start, this.options.start);
|
|
543
|
+
if (this.startedProcess === child) {
|
|
544
|
+
this.startedProcess = null;
|
|
545
|
+
}
|
|
546
|
+
if (this.stoppingStartedProcess === child) {
|
|
547
|
+
this.stoppingStartedProcess = null;
|
|
548
|
+
}
|
|
549
|
+
this.notifyHook("afterStop", { command: this.options.start, startCommand: this.options.start, pid });
|
|
550
|
+
}
|
|
551
|
+
startLongRunningProcess() {
|
|
552
|
+
if (this.disposed || !this.options.start) return;
|
|
553
|
+
this.log(`${this.timestamp()} ${this.t.cyan("Starting:")} ${this.t.bold(this.options.start)}`);
|
|
554
|
+
this.logJson("start", { command: this.options.start });
|
|
555
|
+
const child = this.spawnCommand(this.options.start, false);
|
|
556
|
+
this.startedProcess = child;
|
|
557
|
+
this.emit("start", this.options.start, child.pid);
|
|
558
|
+
setImmediate(() => {
|
|
559
|
+
if (this.startedProcess === child && !this.disposed && child.exitCode === null && child.signalCode === null) {
|
|
560
|
+
this.notifyHook("afterStart", { command: this.options.start, startCommand: this.options.start, pid: child.pid });
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
child.on("error", (err) => {
|
|
564
|
+
if (!this.disposed) {
|
|
565
|
+
this.log(this.t.red(`Start process error: ${err.message}`));
|
|
566
|
+
this.logJson("start_error", { command: this.options.start, error: err.message });
|
|
567
|
+
this.emit("fail", null);
|
|
568
|
+
}
|
|
569
|
+
if (this.startedProcess === child) {
|
|
570
|
+
this.startedProcess = null;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
child.on("close", (code, signal) => {
|
|
574
|
+
const expectedStop = this.disposed || this.stoppingStartedProcess === child;
|
|
575
|
+
if (this.startedProcess === child) {
|
|
576
|
+
this.startedProcess = null;
|
|
577
|
+
}
|
|
578
|
+
if (this.stoppingStartedProcess === child) {
|
|
579
|
+
this.stoppingStartedProcess = null;
|
|
580
|
+
}
|
|
581
|
+
if (!expectedStop && !this.disposed) {
|
|
582
|
+
const exitMessage = signal ? ` (Signal: ${signal})` : ` (Exit code: ${code})`;
|
|
583
|
+
this.log(`${this.timestamp()} ${this.t.yellow("Start process exited")}${exitMessage}`);
|
|
584
|
+
this.logJson("start_exit", { exitCode: code, signal, expected: false });
|
|
585
|
+
}
|
|
586
|
+
if (!this.disposed) {
|
|
587
|
+
this.emit("startExit", code, signal ?? void 0, expectedStop);
|
|
288
588
|
}
|
|
289
|
-
this.log(this.t.dim("\u2500".repeat(40)));
|
|
290
589
|
});
|
|
291
590
|
}
|
|
292
|
-
|
|
591
|
+
async restartStartedProcess() {
|
|
592
|
+
if (!this.options.start) return;
|
|
593
|
+
this.emit("restart", this.options.start);
|
|
594
|
+
const canStart = await this.runHook("beforeStart", {
|
|
595
|
+
command: this.options.start,
|
|
596
|
+
startCommand: this.options.start
|
|
597
|
+
}, "gate");
|
|
598
|
+
if (!canStart) return;
|
|
599
|
+
if (this.startedProcess) {
|
|
600
|
+
const canStop = await this.runHook("beforeStop", {
|
|
601
|
+
command: this.options.start,
|
|
602
|
+
startCommand: this.options.start,
|
|
603
|
+
pid: this.startedProcess.pid
|
|
604
|
+
}, "gate");
|
|
605
|
+
if (!canStop) return;
|
|
606
|
+
await this.stopStartedProcess();
|
|
607
|
+
}
|
|
608
|
+
if (this.options.restartDelay > 0 && !this.disposed) {
|
|
609
|
+
await this.sleep(this.options.restartDelay);
|
|
610
|
+
}
|
|
611
|
+
this.startLongRunningProcess();
|
|
612
|
+
}
|
|
613
|
+
async runCycle(reason) {
|
|
293
614
|
if (this.disposed) return;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.
|
|
298
|
-
this.emit("skip", "content unchanged", filePath);
|
|
615
|
+
if (this.cycleRunning) {
|
|
616
|
+
this.pendingCycle = true;
|
|
617
|
+
this.logVerbose(this.t.yellow(`Lifecycle already running, coalescing ${reason} change...`));
|
|
618
|
+
this.skip("lifecycle already running");
|
|
299
619
|
return;
|
|
300
620
|
}
|
|
301
|
-
|
|
302
|
-
|
|
621
|
+
this.cycleRunning = true;
|
|
622
|
+
try {
|
|
623
|
+
if (this.options.clearScreen) {
|
|
624
|
+
console.clear();
|
|
625
|
+
}
|
|
626
|
+
if (this.options.cmd) {
|
|
627
|
+
const canRunCommand = await this.runHook("beforeCommand", {
|
|
628
|
+
reason,
|
|
629
|
+
command: this.options.cmd,
|
|
630
|
+
startCommand: this.options.start || void 0
|
|
631
|
+
}, "gate");
|
|
632
|
+
if (!canRunCommand) return;
|
|
633
|
+
const result = await this.runCommandWithRetry();
|
|
634
|
+
const canContinue = await this.runHook("afterCommand", {
|
|
635
|
+
reason,
|
|
636
|
+
command: this.options.cmd,
|
|
637
|
+
startCommand: this.options.start || void 0,
|
|
638
|
+
exitCode: result.code,
|
|
639
|
+
signal: result.signal,
|
|
640
|
+
duration: result.duration
|
|
641
|
+
}, "gate");
|
|
642
|
+
if (!canContinue) return;
|
|
643
|
+
if (result.code === 0 && this.options.start && this.options.restartOnSuccess) {
|
|
644
|
+
await this.restartStartedProcess();
|
|
645
|
+
}
|
|
646
|
+
} else if (this.options.start && this.options.restartOnChange) {
|
|
647
|
+
await this.restartStartedProcess();
|
|
648
|
+
}
|
|
649
|
+
} finally {
|
|
650
|
+
this.cycleRunning = false;
|
|
651
|
+
if (!this.disposed) {
|
|
652
|
+
this.log(this.t.dim("-".repeat(40)));
|
|
653
|
+
}
|
|
654
|
+
if (this.pendingCycle && !this.disposed) {
|
|
655
|
+
this.pendingCycle = false;
|
|
656
|
+
setImmediate(() => {
|
|
657
|
+
void this.runCycle("coalesced");
|
|
658
|
+
});
|
|
659
|
+
}
|
|
303
660
|
}
|
|
661
|
+
}
|
|
662
|
+
requestCycle(reason) {
|
|
663
|
+
void this.runCycle(reason);
|
|
664
|
+
}
|
|
665
|
+
skip(reason, filePath) {
|
|
666
|
+
this.emit("skip", reason, filePath);
|
|
667
|
+
this.notifyHook("onSkip", { skipReason: reason, file: filePath });
|
|
668
|
+
}
|
|
669
|
+
scheduleFileCycle(filePath, reason) {
|
|
304
670
|
this.emit("change", filePath);
|
|
305
671
|
if (this.timeout) clearTimeout(this.timeout);
|
|
306
672
|
this.timeout = setTimeout(() => {
|
|
307
673
|
if (!this.disposed) {
|
|
308
|
-
this.log(`${this.timestamp()} ${this.t.yellow("
|
|
309
|
-
this.logJson("change", { file: import_path.default.basename(filePath) });
|
|
310
|
-
this.
|
|
674
|
+
this.log(`${this.timestamp()} ${this.t.yellow("Change detected:")} ${import_path.default.basename(filePath)}`);
|
|
675
|
+
this.logJson("change", { file: import_path.default.basename(filePath), reason });
|
|
676
|
+
this.requestCycle(reason);
|
|
311
677
|
}
|
|
312
678
|
}, this.options.delay);
|
|
313
679
|
}
|
|
680
|
+
async handleFileChange(filePath) {
|
|
681
|
+
if (this.disposed) return;
|
|
682
|
+
const currentHash = this.getHash(filePath);
|
|
683
|
+
const lastHash = this.fileHashes.get(filePath);
|
|
684
|
+
if (currentHash === lastHash) {
|
|
685
|
+
this.logVerbose(this.t.gray(`Skipping ghost change: ${import_path.default.basename(filePath)}`));
|
|
686
|
+
this.skip("content unchanged", filePath);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (currentHash) {
|
|
690
|
+
this.fileHashes.set(filePath, currentHash);
|
|
691
|
+
}
|
|
692
|
+
let isGitChanged = true;
|
|
693
|
+
try {
|
|
694
|
+
isGitChanged = await this.isGitChangedFile(filePath);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
697
|
+
this.log(this.t.red(`Git changed-file check failed: ${err.message}`));
|
|
698
|
+
this.emit("error", err);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (!isGitChanged) {
|
|
702
|
+
this.logVerbose(this.t.gray(`Skipping unchanged git file: ${import_path.default.basename(filePath)}`));
|
|
703
|
+
this.skip("not changed from git base", filePath);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
this.scheduleFileCycle(filePath, "file");
|
|
707
|
+
}
|
|
314
708
|
async start() {
|
|
315
709
|
const validation = this.validate();
|
|
316
710
|
if (!validation.valid) {
|
|
@@ -320,17 +714,23 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
320
714
|
throw new Error(errorMsg);
|
|
321
715
|
}
|
|
322
716
|
const effectivePattern = this.getEffectivePattern();
|
|
323
|
-
this.log(this.t.bold(
|
|
324
|
-
\u{1F52D} Steady Watch Initialized`));
|
|
717
|
+
this.log(this.t.bold("\nSteady Watch Initialized"));
|
|
325
718
|
this.log(` ${this.t.dim("Pattern:")} ${effectivePattern}`);
|
|
326
|
-
this.log(` ${this.t.dim("Command:")} ${this.options.cmd}`);
|
|
719
|
+
if (this.options.cmd) this.log(` ${this.t.dim("Command:")} ${this.options.cmd}`);
|
|
720
|
+
if (this.options.start) this.log(` ${this.t.dim("Start:")} ${this.options.start}`);
|
|
327
721
|
this.log(` ${this.t.dim("Delay:")} ${this.options.delay}ms`);
|
|
328
722
|
if (this.options.quiet) this.log(` ${this.t.dim("Mode:")} quiet`);
|
|
329
723
|
if (this.options.killTimeout > 0) this.log(` ${this.t.dim("Kill:")} ${this.options.killTimeout}ms`);
|
|
724
|
+
if (this.options.restartDelay > 0) this.log(` ${this.t.dim("Restart delay:")} ${this.options.restartDelay}ms`);
|
|
330
725
|
if (this.options.retry > 0) this.log(` ${this.t.dim("Retry:")} ${this.options.retry}x`);
|
|
726
|
+
if (this.options.gitChanged) this.log(` ${this.t.dim("Git:")} changed from ${this.options.gitBase}`);
|
|
727
|
+
if (this.options.initialRun) this.log(` ${this.t.dim("Initial:")} enabled`);
|
|
331
728
|
if (this.options.mtimeOnly) this.log(` ${this.t.dim("Hash:")} mtime-only (fastest)`);
|
|
332
729
|
else this.log(` ${this.t.dim("Hash:")} ${this.options.hash}`);
|
|
333
730
|
this.log("");
|
|
731
|
+
if (this.options.gitChanged) {
|
|
732
|
+
await this.refreshGitChangedFiles();
|
|
733
|
+
}
|
|
334
734
|
this.watcher = import_chokidar.default.watch(effectivePattern, {
|
|
335
735
|
ignored: this.options.ignore,
|
|
336
736
|
ignoreInitial: false,
|
|
@@ -340,55 +740,103 @@ var _SteadyWatcher = class _SteadyWatcher extends import_events.EventEmitter {
|
|
|
340
740
|
}
|
|
341
741
|
});
|
|
342
742
|
this.watcher.on("ready", () => {
|
|
343
|
-
this.
|
|
743
|
+
this.watcherReady = true;
|
|
744
|
+
this.log(this.t.green("Watcher ready. Monitoring for changes..."));
|
|
344
745
|
this.logVerbose(this.t.dim(` Tracking ${this.fileHashes.size} file(s)`));
|
|
345
746
|
this.emit("ready");
|
|
747
|
+
if (this.options.initialRun) {
|
|
748
|
+
this.requestCycle("initial");
|
|
749
|
+
}
|
|
346
750
|
});
|
|
347
751
|
this.watcher.on("error", (error) => {
|
|
348
752
|
this.log(this.t.red(`Watcher error: ${error.message}`));
|
|
349
753
|
this.emit("error", error);
|
|
350
754
|
});
|
|
351
755
|
this.watcher.on("add", (filePath) => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
this.
|
|
756
|
+
void this.handleFileAdd(filePath);
|
|
757
|
+
});
|
|
758
|
+
this.watcher.on("change", (filePath) => {
|
|
759
|
+
void this.handleFileChange(filePath);
|
|
356
760
|
});
|
|
357
|
-
this.watcher.on("change", (filePath) => this.handleFileChange(filePath));
|
|
358
761
|
this.watcher.on("unlink", (filePath) => {
|
|
359
|
-
|
|
360
|
-
this.fileHashes.delete(filePath);
|
|
361
|
-
this.logVerbose(this.t.dim(`Removed: ${import_path.default.basename(filePath)}`));
|
|
762
|
+
void this.handleFileUnlink(filePath);
|
|
362
763
|
});
|
|
363
764
|
}
|
|
765
|
+
async handleFileAdd(filePath) {
|
|
766
|
+
if (this.disposed) return;
|
|
767
|
+
const hash = this.getHash(filePath);
|
|
768
|
+
if (hash) this.fileHashes.set(filePath, hash);
|
|
769
|
+
this.logVerbose(this.t.dim(`Indexed: ${import_path.default.basename(filePath)}`));
|
|
770
|
+
if (!this.watcherReady || !this.options.gitChanged) return;
|
|
771
|
+
try {
|
|
772
|
+
if (await this.isGitChangedFile(filePath)) {
|
|
773
|
+
this.scheduleFileCycle(filePath, "file added");
|
|
774
|
+
}
|
|
775
|
+
} catch (error) {
|
|
776
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
777
|
+
this.log(this.t.red(`Git changed-file check failed: ${err.message}`));
|
|
778
|
+
this.emit("error", err);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async handleFileUnlink(filePath) {
|
|
782
|
+
if (this.disposed) return;
|
|
783
|
+
this.fileHashes.delete(filePath);
|
|
784
|
+
this.logVerbose(this.t.dim(`Removed: ${import_path.default.basename(filePath)}`));
|
|
785
|
+
if (!this.watcherReady || !this.options.gitChanged) return;
|
|
786
|
+
try {
|
|
787
|
+
if (await this.isGitChangedFile(filePath)) {
|
|
788
|
+
this.scheduleFileCycle(filePath, "file removed");
|
|
789
|
+
}
|
|
790
|
+
} catch (error) {
|
|
791
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
792
|
+
this.log(this.t.red(`Git changed-file check failed: ${err.message}`));
|
|
793
|
+
this.emit("error", err);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
364
796
|
async close() {
|
|
797
|
+
if (this.closePromise) {
|
|
798
|
+
return this.closePromise;
|
|
799
|
+
}
|
|
800
|
+
this.closePromise = this.closeInternal();
|
|
801
|
+
return this.closePromise;
|
|
802
|
+
}
|
|
803
|
+
async closeInternal() {
|
|
365
804
|
if (this.disposed) return;
|
|
366
|
-
this.
|
|
367
|
-
this.log(this.t.yellow("\n\u{1F6D1} Shutting down..."));
|
|
805
|
+
this.log(this.t.yellow("\nShutting down..."));
|
|
368
806
|
this.logJson("shutdown", {});
|
|
807
|
+
this.disposed = true;
|
|
369
808
|
if (this.timeout) {
|
|
370
809
|
clearTimeout(this.timeout);
|
|
371
810
|
this.timeout = null;
|
|
372
811
|
}
|
|
373
|
-
if (this.
|
|
374
|
-
clearTimeout(this.
|
|
375
|
-
this.
|
|
376
|
-
}
|
|
377
|
-
if (this.activeProcess) {
|
|
378
|
-
this.activeProcess.kill("SIGTERM");
|
|
379
|
-
this.activeProcess = null;
|
|
812
|
+
if (this.lifecycleKillTimer) {
|
|
813
|
+
clearTimeout(this.lifecycleKillTimer);
|
|
814
|
+
this.lifecycleKillTimer = null;
|
|
380
815
|
}
|
|
816
|
+
const lifecycleProcess = this.lifecycleProcess;
|
|
817
|
+
const startedProcess = this.startedProcess;
|
|
818
|
+
await Promise.all([
|
|
819
|
+
lifecycleProcess ? this.terminateProcess(lifecycleProcess, this.options.cmd, this.options.cmd) : Promise.resolve(),
|
|
820
|
+
startedProcess ? this.terminateProcess(startedProcess, this.options.start, this.options.start) : Promise.resolve()
|
|
821
|
+
]);
|
|
822
|
+
this.lifecycleProcess = null;
|
|
823
|
+
this.startedProcess = null;
|
|
824
|
+
this.stoppingStartedProcess = null;
|
|
381
825
|
if (this.watcher) {
|
|
382
826
|
await this.watcher.close();
|
|
383
827
|
this.watcher = null;
|
|
384
828
|
}
|
|
829
|
+
this.watcherReady = false;
|
|
385
830
|
this.removeAllListeners();
|
|
386
831
|
}
|
|
387
832
|
getTrackedFiles() {
|
|
388
833
|
return Array.from(this.fileHashes.keys());
|
|
389
834
|
}
|
|
390
835
|
isCurrentlyRunning() {
|
|
391
|
-
return this.
|
|
836
|
+
return this.cycleRunning || Boolean(this.lifecycleProcess);
|
|
837
|
+
}
|
|
838
|
+
isStartedProcessRunning() {
|
|
839
|
+
return Boolean(this.startedProcess);
|
|
392
840
|
}
|
|
393
841
|
isDisposed() {
|
|
394
842
|
return this.disposed;
|
|
@@ -406,6 +854,7 @@ function steadyWatch(options) {
|
|
|
406
854
|
var import_fs2 = __toESM(require("fs"));
|
|
407
855
|
var import_path2 = __toESM(require("path"));
|
|
408
856
|
var import_commander = require("commander");
|
|
857
|
+
var CLI_VERSION = "2.1.0";
|
|
409
858
|
function loadConfig(configPath) {
|
|
410
859
|
const searchPaths = [
|
|
411
860
|
configPath,
|
|
@@ -447,28 +896,54 @@ function parseNumber(value, defaultValue) {
|
|
|
447
896
|
}
|
|
448
897
|
return defaultValue;
|
|
449
898
|
}
|
|
899
|
+
function unsetDefaultedCliOptions(program, opts) {
|
|
900
|
+
const getSource = program.getOptionValueSource.bind(program);
|
|
901
|
+
const optionKeys = [
|
|
902
|
+
"delay",
|
|
903
|
+
"verbose",
|
|
904
|
+
"quiet",
|
|
905
|
+
"gitBase",
|
|
906
|
+
"retry",
|
|
907
|
+
"hash",
|
|
908
|
+
"theme"
|
|
909
|
+
];
|
|
910
|
+
for (const key of optionKeys) {
|
|
911
|
+
if (getSource(key) === "default") {
|
|
912
|
+
delete opts[key];
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
450
916
|
function mergeOptions(cliArgs, cliOpts, config) {
|
|
451
917
|
return {
|
|
452
918
|
pattern: cliArgs.pattern || config.pattern || "",
|
|
453
919
|
cmd: cliOpts.cmd || config.cmd || "",
|
|
920
|
+
start: cliOpts.start || config.start || "",
|
|
921
|
+
restartOnSuccess: parseBoolean(cliOpts.restartOnSuccess ?? config.restartOnSuccess),
|
|
922
|
+
restartOnChange: parseBoolean(cliOpts.restartOnChange ?? config.restartOnChange),
|
|
923
|
+
initialRun: cliOpts.initialRun ?? config.initialRun,
|
|
454
924
|
delay: parseNumber(cliOpts.delay ?? config.delay, 300),
|
|
455
925
|
verbose: parseBoolean(cliOpts.verbose ?? config.verbose),
|
|
456
926
|
quiet: parseBoolean(cliOpts.quiet ?? config.quiet),
|
|
457
927
|
ignore: [...parseIgnorePatterns(cliOpts.ignore), ...config.ignore || []],
|
|
458
928
|
ext: cliOpts.ext ? parseExtFilter(cliOpts.ext) : config.ext || [],
|
|
459
|
-
|
|
929
|
+
gitChanged: parseBoolean(cliOpts.gitChanged ?? config.gitChanged),
|
|
930
|
+
gitBase: cliOpts.gitBase || config.gitBase || "HEAD",
|
|
931
|
+
killTimeout: cliOpts.killTimeout !== void 0 || config.killTimeout !== void 0 ? parseNumber(cliOpts.killTimeout ?? config.killTimeout, 0) : void 0,
|
|
932
|
+
restartDelay: parseNumber(cliOpts.restartDelay ?? config.restartDelay, 0),
|
|
460
933
|
retry: parseNumber(cliOpts.retry ?? config.retry, 0),
|
|
461
|
-
hash: cliOpts.hash || config.hash || "md5",
|
|
462
|
-
mtimeOnly: cliOpts.
|
|
934
|
+
hash: (typeof cliOpts.hash === "string" ? cliOpts.hash : void 0) || config.hash || "md5",
|
|
935
|
+
mtimeOnly: cliOpts.hash === false || config.mtimeOnly || false,
|
|
463
936
|
clearScreen: parseBoolean(cliOpts.clear ?? config.clearScreen),
|
|
464
937
|
json: parseBoolean(cliOpts.json ?? config.json),
|
|
465
938
|
theme: cliOpts.theme || config.theme || "default"
|
|
466
939
|
};
|
|
467
940
|
}
|
|
468
|
-
function parseCliArgs() {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
941
|
+
function parseCliArgs(argv = process.argv) {
|
|
942
|
+
const program = new import_commander.Command();
|
|
943
|
+
program.name("steady-watch").description("Intelligent file watcher with debouncing and content hashing.").argument("[files]", 'Glob pattern to watch (e.g., "src/**/*.ts")').option("-c, --cmd <command>", "Command(s) to execute on change (supports quotes)").option("--start <command>", "Long-running command to start or restart after a successful lifecycle command").option("--restart-on-success", "Restart --start only when --cmd exits with code 0").option("--restart-on-change", "Restart --start directly on change when no --cmd is configured").option("--initial-run", "Run the initial lifecycle when the watcher starts").option("--no-initial-run", "Wait for the first file change before running the lifecycle").option("-d, --delay <ms>", "Debounce delay in milliseconds", "300").option("-v, --verbose", "Show hash calculations", false).option("-q, --quiet", "Minimize output", false).option("--ignore <patterns>", "Additional ignore patterns (comma-separated)").option("--ext <extensions>", "Filter by file extensions (e.g., .ts,.tsx)").option("--git-changed", "Only trigger for files changed from the git base ref").option("--git-base <ref>", "Git base ref for --git-changed", "HEAD").option("--config <path>", "Path to config file").option("--kill-timeout <ms>", "Force kill lifecycle commands and process shutdown after timeout").option("--restart-delay <ms>", "Delay between stopping and starting --start").option("--retry <count>", "Retry failed command (0 = disabled)", "0").option("--hash <algorithm>", "Hash algorithm (md5, sha1, sha256)", "md5").option("--no-hash", "Use mtime only instead of content hash (fastest)").option("--clear", "Clear screen on each trigger").option("--json", "Output in JSON format").option("--theme <theme>", "Color theme (default, minimal, none)", "default").version(CLI_VERSION).parse(argv);
|
|
944
|
+
const cliOpts = program.opts();
|
|
945
|
+
unsetDefaultedCliOptions(program, cliOpts);
|
|
946
|
+
const cliArgs = { pattern: program.args[0] || "" };
|
|
472
947
|
return { args: cliArgs, opts: cliOpts };
|
|
473
948
|
}
|
|
474
949
|
|
|
@@ -476,44 +951,48 @@ function parseCliArgs() {
|
|
|
476
951
|
function runCli() {
|
|
477
952
|
const { args, opts } = parseCliArgs();
|
|
478
953
|
const config = loadConfig(opts.config);
|
|
479
|
-
if (opts.config && config.cmd) {
|
|
480
|
-
opts.cmd = config.cmd;
|
|
481
|
-
}
|
|
482
954
|
const options = mergeOptions(args, opts, config);
|
|
483
|
-
if (!options.cmd) {
|
|
484
|
-
console.error("Error: Command is required. Use -c option or config file.");
|
|
485
|
-
process.exit(1);
|
|
486
|
-
}
|
|
487
955
|
const watcher = new SteadyWatcher(options);
|
|
956
|
+
let exiting = false;
|
|
957
|
+
const shutdown = async (exitCode, message) => {
|
|
958
|
+
if (exiting) return;
|
|
959
|
+
exiting = true;
|
|
960
|
+
if (message) console.error(message);
|
|
961
|
+
try {
|
|
962
|
+
await watcher.close();
|
|
963
|
+
} finally {
|
|
964
|
+
process.exit(exitCode);
|
|
965
|
+
}
|
|
966
|
+
};
|
|
488
967
|
watcher.on("error", (err) => {
|
|
489
|
-
|
|
490
|
-
process.exit(1);
|
|
968
|
+
void shutdown(1, `Error: ${err.message}`);
|
|
491
969
|
});
|
|
492
970
|
watcher.start().catch((err) => {
|
|
493
|
-
|
|
494
|
-
process.exit(1);
|
|
971
|
+
void shutdown(1, `Failed to start: ${err.message}`);
|
|
495
972
|
});
|
|
496
|
-
const
|
|
973
|
+
const shutdownSignal = async (signal) => {
|
|
974
|
+
if (exiting) return;
|
|
497
975
|
console.log(`
|
|
498
976
|
Received ${signal}, shutting down...`);
|
|
499
|
-
await
|
|
500
|
-
process.exit(0);
|
|
977
|
+
await shutdown(0);
|
|
501
978
|
};
|
|
502
|
-
process.on("SIGINT", () =>
|
|
503
|
-
process.on("SIGTERM", () =>
|
|
979
|
+
process.on("SIGINT", () => void shutdownSignal("SIGINT"));
|
|
980
|
+
process.on("SIGTERM", () => void shutdownSignal("SIGTERM"));
|
|
504
981
|
process.on("uncaughtException", (err) => {
|
|
505
|
-
|
|
506
|
-
process.exit(1);
|
|
982
|
+
void shutdown(1, `Uncaught exception: ${err instanceof Error ? err.stack || err.message : String(err)}`);
|
|
507
983
|
});
|
|
508
984
|
}
|
|
509
|
-
|
|
510
|
-
if (isMain) {
|
|
985
|
+
if (require.main === module) {
|
|
511
986
|
runCli();
|
|
512
987
|
}
|
|
513
988
|
// Annotate the CommonJS export names for ESM import in node:
|
|
514
989
|
0 && (module.exports = {
|
|
990
|
+
CLI_VERSION,
|
|
515
991
|
SteadyWatcher,
|
|
516
992
|
getTheme,
|
|
993
|
+
loadConfig,
|
|
994
|
+
mergeOptions,
|
|
995
|
+
parseCliArgs,
|
|
517
996
|
runCli,
|
|
518
997
|
steadyWatch,
|
|
519
998
|
themes
|