git-watchtower 1.11.10 → 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.11.10",
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
 
package/src/ui/ansi.js CHANGED
@@ -7,6 +7,22 @@
7
7
  const ESC = '\x1b';
8
8
  const CSI = `${ESC}[`;
9
9
 
10
+ /**
11
+ * Whether color output is enabled.
12
+ * Honors the NO_COLOR convention (https://no-color.org/) and TERM=dumb.
13
+ * Structural codes (cursor movement, screen control) are still emitted
14
+ * so the TUI layout remains functional — only color/style codes are stripped.
15
+ */
16
+ const colorsEnabled = !(
17
+ (process.env.NO_COLOR && process.env.NO_COLOR !== '') ||
18
+ process.env.TERM === 'dumb'
19
+ );
20
+
21
+ /** Empty string for disabled color codes, or the given code when enabled. */
22
+ const c = (code) => (colorsEnabled ? code : '');
23
+ /** Empty-returning function for disabled color functions. */
24
+ const cFn = (fn) => (colorsEnabled ? fn : () => '');
25
+
10
26
  /**
11
27
  * ANSI escape codes for terminal control and styling
12
28
  */
@@ -44,81 +60,81 @@ const ansi = {
44
60
  restoreCursor: `${CSI}u`,
45
61
 
46
62
  // Text styles
47
- reset: `${CSI}0m`,
48
- bold: `${CSI}1m`,
49
- dim: `${CSI}2m`,
50
- italic: `${CSI}3m`,
51
- underline: `${CSI}4m`,
52
- blink: `${CSI}5m`,
53
- inverse: `${CSI}7m`,
54
- hidden: `${CSI}8m`,
55
- strikethrough: `${CSI}9m`,
63
+ reset: c(`${CSI}0m`),
64
+ bold: c(`${CSI}1m`),
65
+ dim: c(`${CSI}2m`),
66
+ italic: c(`${CSI}3m`),
67
+ underline: c(`${CSI}4m`),
68
+ blink: c(`${CSI}5m`),
69
+ inverse: c(`${CSI}7m`),
70
+ hidden: c(`${CSI}8m`),
71
+ strikethrough: c(`${CSI}9m`),
56
72
 
57
73
  // Reset specific styles
58
- resetBold: `${CSI}22m`,
59
- resetDim: `${CSI}22m`,
60
- resetItalic: `${CSI}23m`,
61
- resetUnderline: `${CSI}24m`,
62
- resetBlink: `${CSI}25m`,
63
- resetInverse: `${CSI}27m`,
64
- resetHidden: `${CSI}28m`,
65
- resetStrikethrough: `${CSI}29m`,
74
+ resetBold: c(`${CSI}22m`),
75
+ resetDim: c(`${CSI}22m`),
76
+ resetItalic: c(`${CSI}23m`),
77
+ resetUnderline: c(`${CSI}24m`),
78
+ resetBlink: c(`${CSI}25m`),
79
+ resetInverse: c(`${CSI}27m`),
80
+ resetHidden: c(`${CSI}28m`),
81
+ resetStrikethrough: c(`${CSI}29m`),
66
82
 
67
83
  // Foreground colors (standard)
68
- black: `${CSI}30m`,
69
- red: `${CSI}31m`,
70
- green: `${CSI}32m`,
71
- yellow: `${CSI}33m`,
72
- blue: `${CSI}34m`,
73
- magenta: `${CSI}35m`,
74
- cyan: `${CSI}36m`,
75
- white: `${CSI}37m`,
76
- default: `${CSI}39m`,
84
+ black: c(`${CSI}30m`),
85
+ red: c(`${CSI}31m`),
86
+ green: c(`${CSI}32m`),
87
+ yellow: c(`${CSI}33m`),
88
+ blue: c(`${CSI}34m`),
89
+ magenta: c(`${CSI}35m`),
90
+ cyan: c(`${CSI}36m`),
91
+ white: c(`${CSI}37m`),
92
+ default: c(`${CSI}39m`),
77
93
 
78
94
  // Foreground colors (bright)
79
- gray: `${CSI}90m`,
80
- brightRed: `${CSI}91m`,
81
- brightGreen: `${CSI}92m`,
82
- brightYellow: `${CSI}93m`,
83
- brightBlue: `${CSI}94m`,
84
- brightMagenta: `${CSI}95m`,
85
- brightCyan: `${CSI}96m`,
86
- brightWhite: `${CSI}97m`,
95
+ gray: c(`${CSI}90m`),
96
+ brightRed: c(`${CSI}91m`),
97
+ brightGreen: c(`${CSI}92m`),
98
+ brightYellow: c(`${CSI}93m`),
99
+ brightBlue: c(`${CSI}94m`),
100
+ brightMagenta: c(`${CSI}95m`),
101
+ brightCyan: c(`${CSI}96m`),
102
+ brightWhite: c(`${CSI}97m`),
87
103
 
88
104
  // Background colors (standard)
89
- bgBlack: `${CSI}40m`,
90
- bgRed: `${CSI}41m`,
91
- bgGreen: `${CSI}42m`,
92
- bgYellow: `${CSI}43m`,
93
- bgBlue: `${CSI}44m`,
94
- bgMagenta: `${CSI}45m`,
95
- bgCyan: `${CSI}46m`,
96
- bgWhite: `${CSI}47m`,
97
- bgDefault: `${CSI}49m`,
105
+ bgBlack: c(`${CSI}40m`),
106
+ bgRed: c(`${CSI}41m`),
107
+ bgGreen: c(`${CSI}42m`),
108
+ bgYellow: c(`${CSI}43m`),
109
+ bgBlue: c(`${CSI}44m`),
110
+ bgMagenta: c(`${CSI}45m`),
111
+ bgCyan: c(`${CSI}46m`),
112
+ bgWhite: c(`${CSI}47m`),
113
+ bgDefault: c(`${CSI}49m`),
98
114
 
99
115
  // Background colors (bright)
100
- bgGray: `${CSI}100m`,
101
- bgBrightRed: `${CSI}101m`,
102
- bgBrightGreen: `${CSI}102m`,
103
- bgBrightYellow: `${CSI}103m`,
104
- bgBrightBlue: `${CSI}104m`,
105
- bgBrightMagenta: `${CSI}105m`,
106
- bgBrightCyan: `${CSI}106m`,
107
- bgBrightWhite: `${CSI}107m`,
116
+ bgGray: c(`${CSI}100m`),
117
+ bgBrightRed: c(`${CSI}101m`),
118
+ bgBrightGreen: c(`${CSI}102m`),
119
+ bgBrightYellow: c(`${CSI}103m`),
120
+ bgBrightBlue: c(`${CSI}104m`),
121
+ bgBrightMagenta: c(`${CSI}105m`),
122
+ bgBrightCyan: c(`${CSI}106m`),
123
+ bgBrightWhite: c(`${CSI}107m`),
108
124
 
109
125
  /**
110
126
  * Set foreground color using 256-color palette
111
127
  * @param {number} n - Color number (0-255)
112
128
  * @returns {string}
113
129
  */
114
- fg256: (n) => `${CSI}38;5;${n}m`,
130
+ fg256: cFn((n) => `${CSI}38;5;${n}m`),
115
131
 
116
132
  /**
117
133
  * Set background color using 256-color palette
118
134
  * @param {number} n - Color number (0-255)
119
135
  * @returns {string}
120
136
  */
121
- bg256: (n) => `${CSI}48;5;${n}m`,
137
+ bg256: cFn((n) => `${CSI}48;5;${n}m`),
122
138
 
123
139
  /**
124
140
  * Set foreground color using RGB
@@ -127,7 +143,7 @@ const ansi = {
127
143
  * @param {number} b - Blue (0-255)
128
144
  * @returns {string}
129
145
  */
130
- fgRgb: (r, g, b) => `${CSI}38;2;${r};${g};${b}m`,
146
+ fgRgb: cFn((r, g, b) => `${CSI}38;2;${r};${g};${b}m`),
131
147
 
132
148
  /**
133
149
  * Set background color using RGB
@@ -136,7 +152,7 @@ const ansi = {
136
152
  * @param {number} b - Blue (0-255)
137
153
  * @returns {string}
138
154
  */
139
- bgRgb: (r, g, b) => `${CSI}48;2;${r};${g};${b}m`,
155
+ bgRgb: cFn((r, g, b) => `${CSI}48;2;${r};${g};${b}m`),
140
156
  };
141
157
 
142
158
  /**
@@ -492,6 +508,7 @@ module.exports = {
492
508
  wordWrap,
493
509
  horizontalLine,
494
510
  style,
511
+ colorsEnabled,
495
512
  // Export constants for direct use
496
513
  ESC,
497
514
  CSI,