git-watchtower 1.12.0 → 1.12.2
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/bin/git-watchtower.js +51 -19
- package/package.json +1 -1
- package/src/cli/args.js +9 -1
- package/src/server/process.js +29 -4
package/bin/git-watchtower.js
CHANGED
|
@@ -389,6 +389,20 @@ let AUTO_PULL = true;
|
|
|
389
389
|
const MAX_LOG_ENTRIES = 10;
|
|
390
390
|
const MAX_SERVER_LOG_LINES = 500;
|
|
391
391
|
|
|
392
|
+
// Timing constants (ms)
|
|
393
|
+
/** Grace period before SIGKILLing a process after SIGTERM. */
|
|
394
|
+
const FORCE_KILL_GRACE_MS = 3000;
|
|
395
|
+
/** Additional grace period added to a command's timeout before SIGKILL. */
|
|
396
|
+
const SIGKILL_GRACE_AFTER_TIMEOUT_MS = 5000;
|
|
397
|
+
/** Delay between stopping and restarting the dev server. */
|
|
398
|
+
const SERVER_RESTART_DELAY_MS = 500;
|
|
399
|
+
/** How long a transient flash message stays on screen. */
|
|
400
|
+
const FLASH_MESSAGE_DURATION_MS = 3000;
|
|
401
|
+
/** Debounce window for file watcher events before notifying clients. */
|
|
402
|
+
const FILE_WATCHER_DEBOUNCE_MS = 100;
|
|
403
|
+
/** Max time to wait for the static HTTP server to close on shutdown. */
|
|
404
|
+
const SERVER_CLOSE_TIMEOUT_MS = 2000;
|
|
405
|
+
|
|
392
406
|
// Telemetry session tracking
|
|
393
407
|
let branchSwitchCount = 0;
|
|
394
408
|
let sessionStartTime = null;
|
|
@@ -405,6 +419,10 @@ let worker = null;
|
|
|
405
419
|
let projectId = null;
|
|
406
420
|
let webStateInterval = null;
|
|
407
421
|
|
|
422
|
+
// Periodic update check controller — hoisted to module scope so the exit
|
|
423
|
+
// handler can clean it up regardless of where in start() we are.
|
|
424
|
+
let periodicUpdateCheck = null;
|
|
425
|
+
|
|
408
426
|
function applyConfig(config) {
|
|
409
427
|
// Server settings
|
|
410
428
|
SERVER_MODE = config.server?.mode || 'static';
|
|
@@ -467,6 +485,17 @@ function openInBrowser(url) {
|
|
|
467
485
|
});
|
|
468
486
|
}
|
|
469
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Build a localhost URL for the given port.
|
|
490
|
+
* Centralizes the `http://localhost:${port}` pattern so it's easy to adjust
|
|
491
|
+
* (e.g. switch protocol or host) in one place.
|
|
492
|
+
* @param {number} port
|
|
493
|
+
* @returns {string}
|
|
494
|
+
*/
|
|
495
|
+
function localhostUrl(port) {
|
|
496
|
+
return `http://localhost:${port}`;
|
|
497
|
+
}
|
|
498
|
+
|
|
470
499
|
// parseRemoteUrl, buildBranchUrl, detectPlatform, buildWebUrl, extractSessionUrl
|
|
471
500
|
// imported from src/git/remote.js
|
|
472
501
|
|
|
@@ -742,7 +771,7 @@ function stopServerProcess() {
|
|
|
742
771
|
} catch (e) {
|
|
743
772
|
// Process group may already be dead
|
|
744
773
|
}
|
|
745
|
-
},
|
|
774
|
+
}, FORCE_KILL_GRACE_MS);
|
|
746
775
|
|
|
747
776
|
// Clear the force-kill timer if the process exits cleanly
|
|
748
777
|
proc.once('close', () => {
|
|
@@ -760,7 +789,7 @@ function restartServerProcess() {
|
|
|
760
789
|
setTimeout(() => {
|
|
761
790
|
startServerProcess();
|
|
762
791
|
render();
|
|
763
|
-
},
|
|
792
|
+
}, SERVER_RESTART_DELAY_MS);
|
|
764
793
|
}
|
|
765
794
|
|
|
766
795
|
// Network and polling state
|
|
@@ -857,7 +886,7 @@ function execCli(cmd, args = [], options = {}) {
|
|
|
857
886
|
if (timeout > 0) {
|
|
858
887
|
const killTimer = setTimeout(() => {
|
|
859
888
|
try { child.kill('SIGKILL'); } catch (e) { /* already dead */ }
|
|
860
|
-
}, timeout +
|
|
889
|
+
}, timeout + SIGKILL_GRACE_AFTER_TIMEOUT_MS);
|
|
861
890
|
child.on('close', () => clearTimeout(killTimer));
|
|
862
891
|
}
|
|
863
892
|
});
|
|
@@ -1336,7 +1365,7 @@ function showFlash(message) {
|
|
|
1336
1365
|
flashTimeout = setTimeout(() => {
|
|
1337
1366
|
store.setState({ flashMessage: null });
|
|
1338
1367
|
render();
|
|
1339
|
-
},
|
|
1368
|
+
}, FLASH_MESSAGE_DURATION_MS);
|
|
1340
1369
|
}
|
|
1341
1370
|
|
|
1342
1371
|
function hideFlash() {
|
|
@@ -2109,7 +2138,7 @@ function createStaticServer() {
|
|
|
2109
2138
|
return;
|
|
2110
2139
|
}
|
|
2111
2140
|
|
|
2112
|
-
const url = new URL(req.url,
|
|
2141
|
+
const url = new URL(req.url, localhostUrl(PORT));
|
|
2113
2142
|
let pathname = url.pathname;
|
|
2114
2143
|
const logPath = pathname; // Keep original for logging
|
|
2115
2144
|
|
|
@@ -2194,7 +2223,7 @@ function setupFileWatcher() {
|
|
|
2194
2223
|
addLog(`File changed: ${filename}`, 'info');
|
|
2195
2224
|
notifyClients();
|
|
2196
2225
|
render();
|
|
2197
|
-
},
|
|
2226
|
+
}, FILE_WATCHER_DEBOUNCE_MS);
|
|
2198
2227
|
});
|
|
2199
2228
|
|
|
2200
2229
|
fileWatcher.on('error', (err) => {
|
|
@@ -2759,7 +2788,7 @@ function setupKeyboardInput() {
|
|
|
2759
2788
|
|
|
2760
2789
|
case 'o': // Open live server in browser
|
|
2761
2790
|
if (!NO_SERVER) {
|
|
2762
|
-
const serverUrl =
|
|
2791
|
+
const serverUrl = localhostUrl(PORT);
|
|
2763
2792
|
addLog(`Opening ${serverUrl} in browser...`, 'info');
|
|
2764
2793
|
openInBrowser(serverUrl);
|
|
2765
2794
|
render();
|
|
@@ -3002,7 +3031,7 @@ async function handleWebAction(action, payload) {
|
|
|
3002
3031
|
break;
|
|
3003
3032
|
case 'openBrowser':
|
|
3004
3033
|
if (!NO_SERVER) {
|
|
3005
|
-
openInBrowser(
|
|
3034
|
+
openInBrowser(localhostUrl(PORT));
|
|
3006
3035
|
sendResult(true, 'Opened in browser');
|
|
3007
3036
|
}
|
|
3008
3037
|
break;
|
|
@@ -3089,7 +3118,7 @@ async function startWebDashboard(openBrowser) {
|
|
|
3089
3118
|
});
|
|
3090
3119
|
worker.onCommand = (action, payload) => handleWebAction(action, payload);
|
|
3091
3120
|
await worker.connect();
|
|
3092
|
-
addLog(`Joined web dashboard at
|
|
3121
|
+
addLog(`Joined web dashboard at ${localhostUrl(existing.port)} (tab)`, 'success');
|
|
3093
3122
|
|
|
3094
3123
|
// Push state periodically
|
|
3095
3124
|
webStateInterval = setInterval(() => {
|
|
@@ -3140,8 +3169,8 @@ async function startWebDashboard(openBrowser) {
|
|
|
3140
3169
|
WEB_PORT = port;
|
|
3141
3170
|
writeLock(process.pid, port, coordinator.socketPath);
|
|
3142
3171
|
|
|
3143
|
-
addLog(`Web dashboard:
|
|
3144
|
-
if (openBrowser) openInBrowser(
|
|
3172
|
+
addLog(`Web dashboard: ${localhostUrl(port)}`, 'success');
|
|
3173
|
+
if (openBrowser) openInBrowser(localhostUrl(port));
|
|
3145
3174
|
render();
|
|
3146
3175
|
} catch (err) {
|
|
3147
3176
|
addLog(`Web dashboard failed: ${err.message}`, 'error');
|
|
@@ -3250,7 +3279,7 @@ async function shutdown() {
|
|
|
3250
3279
|
clients.clear();
|
|
3251
3280
|
|
|
3252
3281
|
const serverClosePromise = new Promise(resolve => server.close(resolve));
|
|
3253
|
-
const timeoutPromise = new Promise(resolve => setTimeout(resolve,
|
|
3282
|
+
const timeoutPromise = new Promise(resolve => setTimeout(resolve, SERVER_CLOSE_TIMEOUT_MS));
|
|
3254
3283
|
await Promise.race([serverClosePromise, timeoutPromise]);
|
|
3255
3284
|
}
|
|
3256
3285
|
|
|
@@ -3271,6 +3300,11 @@ async function shutdown() {
|
|
|
3271
3300
|
|
|
3272
3301
|
process.on('SIGINT', shutdown);
|
|
3273
3302
|
process.on('SIGTERM', shutdown);
|
|
3303
|
+
// Clean up long-lived timers on exit, regardless of which code path got us
|
|
3304
|
+
// here (normal exit, uncaught exception, early failure in start()).
|
|
3305
|
+
process.on('exit', () => {
|
|
3306
|
+
if (periodicUpdateCheck) periodicUpdateCheck.stop();
|
|
3307
|
+
});
|
|
3274
3308
|
process.on('uncaughtException', async (err) => {
|
|
3275
3309
|
telemetry.captureError(err);
|
|
3276
3310
|
write(ansi.showCursor);
|
|
@@ -3399,11 +3433,11 @@ async function start() {
|
|
|
3399
3433
|
// Static mode
|
|
3400
3434
|
server = createStaticServer();
|
|
3401
3435
|
server.listen(PORT, '127.0.0.1', () => {
|
|
3402
|
-
addLog(`Server started on
|
|
3436
|
+
addLog(`Server started on ${localhostUrl(PORT)}`, 'success');
|
|
3403
3437
|
addLog(`Serving ${STATIC_DIR.replace(PROJECT_ROOT, '.')}`, 'info');
|
|
3404
3438
|
addLog(`Current branch: ${store.get('currentBranch')}`, 'info');
|
|
3405
3439
|
// Add server log entries for static server
|
|
3406
|
-
addServerLog(`Static server started on
|
|
3440
|
+
addServerLog(`Static server started on ${localhostUrl(PORT)}`);
|
|
3407
3441
|
addServerLog(`Serving files from: ${STATIC_DIR.replace(PROJECT_ROOT, '.')}`);
|
|
3408
3442
|
addServerLog(`Live reload enabled - waiting for browser connections...`);
|
|
3409
3443
|
render();
|
|
@@ -3454,8 +3488,9 @@ async function start() {
|
|
|
3454
3488
|
}
|
|
3455
3489
|
}).catch(() => {});
|
|
3456
3490
|
|
|
3457
|
-
// Re-check for updates periodically (every 4 hours) while running
|
|
3458
|
-
|
|
3491
|
+
// Re-check for updates periodically (every 4 hours) while running.
|
|
3492
|
+
// Assigned to module scope so the top-level exit handler can stop it.
|
|
3493
|
+
periodicUpdateCheck = startPeriodicUpdateCheck((latestVersion) => {
|
|
3459
3494
|
const alreadyKnown = store.get('updateAvailable');
|
|
3460
3495
|
store.setState({ updateAvailable: latestVersion });
|
|
3461
3496
|
if (!alreadyKnown) {
|
|
@@ -3465,9 +3500,6 @@ async function start() {
|
|
|
3465
3500
|
}
|
|
3466
3501
|
render();
|
|
3467
3502
|
});
|
|
3468
|
-
|
|
3469
|
-
// Clean up periodic check on exit
|
|
3470
|
-
process.on('exit', () => periodicCheck.stop());
|
|
3471
3503
|
}
|
|
3472
3504
|
|
|
3473
3505
|
start().catch(err => {
|
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -175,7 +175,15 @@ function parseArgs(argv, options = {}) {
|
|
|
175
175
|
* @returns {object} Merged config
|
|
176
176
|
*/
|
|
177
177
|
function applyCliArgsToConfig(config, cliArgs) {
|
|
178
|
-
|
|
178
|
+
// Shallow clone with nested object spreading — the config is at most two
|
|
179
|
+
// levels deep and all values are primitives, so this is equivalent to a deep
|
|
180
|
+
// clone but avoids the JSON round-trip (which silently drops `undefined`,
|
|
181
|
+
// functions, and throws on circular refs).
|
|
182
|
+
const merged = {
|
|
183
|
+
...config,
|
|
184
|
+
server: { ...config.server },
|
|
185
|
+
web: { ...config.web },
|
|
186
|
+
};
|
|
179
187
|
|
|
180
188
|
// Server settings
|
|
181
189
|
if (cliArgs.mode !== null) {
|
package/src/server/process.js
CHANGED
|
@@ -31,8 +31,17 @@ const KILL_GRACE_PERIOD = 3000;
|
|
|
31
31
|
const RESTART_DELAY = 500;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Parse a command string into command and arguments
|
|
35
|
-
* Handles quoted strings
|
|
34
|
+
* Parse a command string into command and arguments.
|
|
35
|
+
* Handles quoted strings, backslash escapes (e.g. `\"`, `\\`, `\ `),
|
|
36
|
+
* and empty quoted arguments (`""`).
|
|
37
|
+
*
|
|
38
|
+
* Rules (POSIX-ish):
|
|
39
|
+
* - Inside single quotes, characters are literal — backslashes do NOT escape.
|
|
40
|
+
* - Inside double quotes or outside any quotes, a backslash causes the next
|
|
41
|
+
* character to be treated literally (so `\"` yields `"`, `\\` yields `\`,
|
|
42
|
+
* and `\ ` yields a literal space that doesn't split the argument).
|
|
43
|
+
* - A trailing backslash with no following character is left literal.
|
|
44
|
+
*
|
|
36
45
|
* @param {string} commandString - Command string to parse
|
|
37
46
|
* @returns {{command: string, args: string[]}}
|
|
38
47
|
*/
|
|
@@ -41,27 +50,43 @@ function parseCommand(commandString) {
|
|
|
41
50
|
let current = '';
|
|
42
51
|
let inQuotes = false;
|
|
43
52
|
let quoteChar = '';
|
|
53
|
+
// Tracks whether we've started accumulating an argument — distinguishes
|
|
54
|
+
// `""` (empty argument) from whitespace between arguments.
|
|
55
|
+
let hasCurrent = false;
|
|
44
56
|
|
|
45
57
|
for (let i = 0; i < commandString.length; i++) {
|
|
46
58
|
const char = commandString[i];
|
|
47
59
|
|
|
60
|
+
// Backslash escapes: unless we're inside single quotes, a backslash
|
|
61
|
+
// causes the next character to be treated literally. A trailing
|
|
62
|
+
// backslash (no following character) falls through and is kept literal.
|
|
63
|
+
if (char === '\\' && quoteChar !== "'" && i + 1 < commandString.length) {
|
|
64
|
+
current += commandString[i + 1];
|
|
65
|
+
hasCurrent = true;
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
if ((char === '"' || char === "'") && !inQuotes) {
|
|
49
71
|
inQuotes = true;
|
|
50
72
|
quoteChar = char;
|
|
73
|
+
hasCurrent = true;
|
|
51
74
|
} else if (char === quoteChar && inQuotes) {
|
|
52
75
|
inQuotes = false;
|
|
53
76
|
quoteChar = '';
|
|
54
77
|
} else if (char === ' ' && !inQuotes) {
|
|
55
|
-
if (
|
|
78
|
+
if (hasCurrent) {
|
|
56
79
|
args.push(current);
|
|
57
80
|
current = '';
|
|
81
|
+
hasCurrent = false;
|
|
58
82
|
}
|
|
59
83
|
} else {
|
|
60
84
|
current += char;
|
|
85
|
+
hasCurrent = true;
|
|
61
86
|
}
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
if (
|
|
89
|
+
if (hasCurrent) {
|
|
65
90
|
args.push(current);
|
|
66
91
|
}
|
|
67
92
|
|