git-watchtower 2.3.5 → 2.3.7

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.
@@ -859,7 +859,7 @@ let verySlowFetchWarningShown = false;
859
859
  let pollIntervalId = null;
860
860
 
861
861
  // ANSI escape codes and box drawing imported from src/ui/ansi.js
862
- const { ansi, box, truncate, sparkline: uiSparkline, visibleLength, stripAnsi, padRight, padLeft, getMaxBranchesForScreen: calcMaxBranches, drawBox: renderBox, clearArea: renderClearArea } = require('../src/ui/ansi');
862
+ const { ansi, box, truncate, sparkline: uiSparkline, visibleLength, stripAnsi, sanitizeForRender, padRight, padLeft, getMaxBranchesForScreen: calcMaxBranches, drawBox: renderBox, clearArea: renderClearArea } = require('../src/ui/ansi');
863
863
 
864
864
  // Error detection utilities imported from src/utils/errors.js
865
865
  const { isAuthError, isMergeConflict, isNetworkError } = require('../src/utils/errors');
@@ -1111,10 +1111,16 @@ function getCasinoMessage(type) {
1111
1111
  function addLog(message, type = 'info') {
1112
1112
  const icons = { info: '○', success: '✓', warning: '●', error: '✗', update: '⟳' };
1113
1113
  const colors = { info: 'white', success: 'green', warning: 'yellow', error: 'red', update: 'cyan' };
1114
- // Collapse any whitespace (newlines, tabs, CRs) into a single space so that
1115
- // multi-line content (e.g. git stderr from a failed auto-pull) cannot leak
1116
- // cursor movement into the rendered box and corrupt the surrounding UI.
1117
- const safeMessage = String(message == null ? '' : message).replace(/\s+/g, ' ').trim();
1114
+ // Collapse any whitespace (newlines, tabs, CRs) into a single space, then
1115
+ // strip dangerous escape sequences (cursor moves, screen clears, OSC,
1116
+ // bell, etc.). git stderr and remote git output regularly contain ANSI
1117
+ // colour AND raw control codes; without sanitising, a colourised
1118
+ // `git fetch` error or a malicious commit subject can corrupt the
1119
+ // rendered box. SGR colour codes survive — sanitizeForRender preserves
1120
+ // them so any styling we want stays.
1121
+ const safeMessage = sanitizeForRender(
1122
+ String(message == null ? '' : message).replace(/\s+/g, ' ').trim()
1123
+ );
1118
1124
  const entry = {
1119
1125
  message: safeMessage, type,
1120
1126
  timestamp: new Date().toLocaleTimeString(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.5",
3
+ "version": "2.3.7",
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": {
@@ -283,6 +283,16 @@ function startSlotReels(renderCallback) {
283
283
  * @param {Object|null} winLevel - The win level object from getWinLevel()
284
284
  */
285
285
  function stopSlotReels(hadUpdates = false, renderCallback = null, winLevel = null) {
286
+ // No-op when casino mode is off. The bin's polling path captures
287
+ // `casinoOn` once at the top of pollGitChanges and continues to use that
288
+ // snapshot for the rest of the cycle, so a poll completing AFTER the
289
+ // user toggled casino off would otherwise install a fresh
290
+ // slotResultInterval that fires render() ~20× over the next 3s. The
291
+ // display getters are already guarded by `casinoEnabled`, so the user
292
+ // sees nothing — but render() still burns full-screen redraws and the
293
+ // interval keeps a closure alive. Drop the call cleanly here instead.
294
+ if (!casinoEnabled) return;
295
+
286
296
  if (slotReelInterval) {
287
297
  clearInterval(slotReelInterval);
288
298
  slotReelInterval = null;
package/src/ui/ansi.js CHANGED
@@ -282,14 +282,79 @@ const indicators = {
282
282
  * Helper functions for working with ANSI codes
283
283
  */
284
284
 
285
+ // Match any ECMA-48 CSI sequence: ESC [ <params> <intermediates> <final byte>.
286
+ // final byte is in 0x40-0x7E. Covers SGR (m), cursor movement (A-H), erase
287
+ // (J/K), scroll, mode set/reset (h/l), and everything else terminals
288
+ // recognise via CSI.
289
+ // eslint-disable-next-line no-control-regex
290
+ const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
291
+
292
+ // Match CSI sequences that are NOT SGR (final byte 'm' / 0x6D). Used by the
293
+ // render-safe sanitiser to drop dangerous controls while preserving colour.
294
+ // eslint-disable-next-line no-control-regex
295
+ const NON_SGR_CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x6c\x6e-\x7e]/g;
296
+
297
+ // Match OSC sequences: ESC ] ... terminator (BEL or ESC \). These set
298
+ // terminal title, hyperlinks, etc. — all undesirable in untrusted input.
299
+ // eslint-disable-next-line no-control-regex
300
+ const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
301
+
302
+ // Match other 2-byte ESC sequences (Fe codes 0x40-0x5F) excluding CSI ([)
303
+ // and OSC (]) which are handled separately above.
304
+ // eslint-disable-next-line no-control-regex
305
+ const ESC_RE = /\x1b[@-Z\\-_]/g;
306
+
307
+ // Match dangerous C0 control characters. Whitespace (\t, \n, \r) and ESC
308
+ // (\x1b) are excluded — ESC is consumed by the escape-sequence regexes
309
+ // above, and SGR codes preserved by sanitizeForRender contain it. Bell,
310
+ // BS, VT, FF, SO, SI, DEL, etc. all get stripped.
311
+ // eslint-disable-next-line no-control-regex
312
+ const C0_RE = /[\x00-\x08\x0b\x0c\x0e-\x1a\x1c-\x1f\x7f]/g;
313
+
285
314
  /**
286
- * Strip ANSI codes from a string
315
+ * Strip ALL ANSI escape sequences and dangerous C0 control characters.
316
+ * Use this for measuring display width or for fully sanitising untrusted
317
+ * input that won't be styled (e.g. JSON pushed to the web dashboard).
318
+ *
319
+ * Catches CSI (cursor moves, screen clears, SGR colour), OSC (terminal
320
+ * title), other 2-byte ESC sequences, and dangerous C0 controls (bell,
321
+ * backspace, etc). Preserves whitespace (\t, \n, \r).
322
+ *
287
323
  * @param {string} str - String potentially containing ANSI codes
288
324
  * @returns {string} String with ANSI codes removed
289
325
  */
290
326
  function stripAnsi(str) {
291
- // eslint-disable-next-line no-control-regex
292
- return str.replace(/\x1b\[[0-9;]*m/g, '');
327
+ return String(str)
328
+ .replace(OSC_RE, '')
329
+ .replace(CSI_RE, '')
330
+ .replace(ESC_RE, '')
331
+ .replace(C0_RE, '')
332
+ // Final pass: any stray ESC bytes left over from malformed sequences.
333
+ // Display surfaces that ask for "no escapes" should never see one.
334
+ // eslint-disable-next-line no-control-regex
335
+ .replace(/\x1b/g, '');
336
+ }
337
+
338
+ /**
339
+ * Sanitise a string for safe terminal rendering: strip dangerous escape
340
+ * sequences (cursor moves, screen clears, OSC terminal-title, bell, etc.)
341
+ * while PRESERVING SGR colour/style codes so legitimate styling we built
342
+ * ourselves still renders.
343
+ *
344
+ * Use this on any string that came from outside (commit subjects, branch
345
+ * names, git stderr, PR titles) before it flows to the renderer. A
346
+ * malicious commit subject containing `\x1b[2J` would otherwise clear the
347
+ * screen on every render.
348
+ *
349
+ * @param {string} str - Untrusted string that will be written to a TTY
350
+ * @returns {string} String with dangerous escapes removed, SGR preserved
351
+ */
352
+ function sanitizeForRender(str) {
353
+ return String(str)
354
+ .replace(OSC_RE, '')
355
+ .replace(NON_SGR_CSI_RE, '')
356
+ .replace(ESC_RE, '')
357
+ .replace(C0_RE, '');
293
358
  }
294
359
 
295
360
  /**
@@ -302,19 +367,27 @@ function visibleLength(str) {
302
367
  }
303
368
 
304
369
  /**
305
- * Truncate a string to a maximum visible length, preserving ANSI codes
306
- * @param {string} str - String to truncate
370
+ * Truncate a string to a maximum visible length, preserving SGR colour
371
+ * codes but stripping any other escape sequences (cursor movement, screen
372
+ * clears, OSC, bell, etc.). This guarantees that even short, non-truncated
373
+ * strings cannot leak terminal-corrupting escapes to the renderer.
374
+ *
375
+ * @param {string} str - String to truncate (may contain SGR + dangerous escapes)
307
376
  * @param {number} maxLen - Maximum visible length
308
377
  * @param {string} [suffix='…'] - Suffix to append if truncated
309
378
  * @returns {string} Truncated string
310
379
  */
311
380
  function truncate(str, maxLen, suffix = '…') {
312
- const visible = stripAnsi(str);
381
+ // Strip dangerous escapes up front so the fast-path can never return a
382
+ // raw input with cursor/screen-control sequences in it. SGR survives.
383
+ const safeStr = sanitizeForRender(str);
384
+ const visible = stripAnsi(safeStr);
313
385
  if (visible.length <= maxLen) {
314
- return str;
386
+ return safeStr;
315
387
  }
316
388
 
317
- // Simple approach: strip ANSI, truncate, add suffix and reset
389
+ // Long-string path: drop ALL escapes (including SGR), truncate, append
390
+ // ellipsis + reset. Existing behaviour, kept for layout determinism.
318
391
  const truncated = visible.slice(0, maxLen - suffix.length);
319
392
  return truncated + suffix + ansi.reset;
320
393
  }
@@ -497,6 +570,7 @@ module.exports = {
497
570
  generateSparkline,
498
571
  indicators,
499
572
  stripAnsi,
573
+ sanitizeForRender,
500
574
  visibleLength,
501
575
  truncate,
502
576
  pad,