git-watchtower 1.12.0 → 1.12.1

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.
@@ -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;
@@ -742,7 +756,7 @@ function stopServerProcess() {
742
756
  } catch (e) {
743
757
  // Process group may already be dead
744
758
  }
745
- }, 3000);
759
+ }, FORCE_KILL_GRACE_MS);
746
760
 
747
761
  // Clear the force-kill timer if the process exits cleanly
748
762
  proc.once('close', () => {
@@ -760,7 +774,7 @@ function restartServerProcess() {
760
774
  setTimeout(() => {
761
775
  startServerProcess();
762
776
  render();
763
- }, 500);
777
+ }, SERVER_RESTART_DELAY_MS);
764
778
  }
765
779
 
766
780
  // Network and polling state
@@ -857,7 +871,7 @@ function execCli(cmd, args = [], options = {}) {
857
871
  if (timeout > 0) {
858
872
  const killTimer = setTimeout(() => {
859
873
  try { child.kill('SIGKILL'); } catch (e) { /* already dead */ }
860
- }, timeout + 5000);
874
+ }, timeout + SIGKILL_GRACE_AFTER_TIMEOUT_MS);
861
875
  child.on('close', () => clearTimeout(killTimer));
862
876
  }
863
877
  });
@@ -1336,7 +1350,7 @@ function showFlash(message) {
1336
1350
  flashTimeout = setTimeout(() => {
1337
1351
  store.setState({ flashMessage: null });
1338
1352
  render();
1339
- }, 3000);
1353
+ }, FLASH_MESSAGE_DURATION_MS);
1340
1354
  }
1341
1355
 
1342
1356
  function hideFlash() {
@@ -2194,7 +2208,7 @@ function setupFileWatcher() {
2194
2208
  addLog(`File changed: ${filename}`, 'info');
2195
2209
  notifyClients();
2196
2210
  render();
2197
- }, 100);
2211
+ }, FILE_WATCHER_DEBOUNCE_MS);
2198
2212
  });
2199
2213
 
2200
2214
  fileWatcher.on('error', (err) => {
@@ -3250,7 +3264,7 @@ async function shutdown() {
3250
3264
  clients.clear();
3251
3265
 
3252
3266
  const serverClosePromise = new Promise(resolve => server.close(resolve));
3253
- const timeoutPromise = new Promise(resolve => setTimeout(resolve, 2000));
3267
+ const timeoutPromise = new Promise(resolve => setTimeout(resolve, SERVER_CLOSE_TIMEOUT_MS));
3254
3268
  await Promise.race([serverClosePromise, timeoutPromise]);
3255
3269
  }
3256
3270
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.12.0",
3
+ "version": "1.12.1",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
@@ -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 properly
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 (current) {
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 (current) {
89
+ if (hasCurrent) {
65
90
  args.push(current);
66
91
  }
67
92