git-watchtower 1.14.1 → 1.14.3

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.
@@ -654,7 +654,7 @@ async function loadAsyncActionData(branch, currentData) {
654
654
  try {
655
655
  const host = new URL(env.webUrlBase).hostname;
656
656
  webUrl = buildBranchUrl(env.webUrlBase, host, branch.name);
657
- } catch (e) { /* ignore */ }
657
+ } catch (e) { /* invalid webUrlBase — leave webUrl null, modal hides the link */ }
658
658
  }
659
659
 
660
660
  // Fetch session URL (local git, fast but async)
@@ -1963,7 +1963,7 @@ async function pollGitChanges() {
1963
1963
  }
1964
1964
 
1965
1965
  // Background ahead/behind fetch for visible branches
1966
- fetchAheadBehindForBranches(pollFilteredBranches).catch(() => {});
1966
+ fetchAheadBehindForBranches(pollFilteredBranches).catch(() => { /* transient git/network error — next poll will retry */ });
1967
1967
 
1968
1968
  // AUTO-PULL: If current branch has remote updates, pull automatically (if enabled)
1969
1969
  const autoPullBranchName = store.get('currentBranch');
@@ -2447,7 +2447,7 @@ function setupKeyboardInput() {
2447
2447
  store.setState({ actionData: fullData, actionLoading: false });
2448
2448
  render();
2449
2449
  }
2450
- }).catch(() => {});
2450
+ }).catch(() => { /* PR was created; modal refresh is a nice-to-have, user can reopen */ });
2451
2451
  } catch (e) {
2452
2452
  const msg = (e && e.stderr) || (e && e.message) || String(e);
2453
2453
  addLog(`Failed to create ${prLabel}: ${msg.split('\n')[0]}`, 'error');
@@ -2930,7 +2930,7 @@ function setupKeyboardInput() {
2930
2930
  } else {
2931
2931
  startWebDashboard(true).then(() => {
2932
2932
  showFlash(`Web dashboard on :${WEB_PORT}`);
2933
- }).catch(() => {});
2933
+ }).catch(() => { /* startWebDashboard surfaces its own errors via addLog/showErrorToast */ });
2934
2934
  }
2935
2935
  break;
2936
2936
  }
@@ -3190,7 +3190,7 @@ async function startWebDashboard(openBrowser) {
3190
3190
  // Resolve and cache the repo web URL for link building in the web UI
3191
3191
  getRemoteWebUrl(null).then((url) => {
3192
3192
  if (url) webDashboard.setRepoWebUrl(url);
3193
- }).catch(() => {});
3193
+ }).catch(() => { /* no remote or unreachable — web UI falls back to branch names without links */ });
3194
3194
 
3195
3195
  // Atomically try to claim the coordinator role. If another live instance
3196
3196
  // already owns the lock, connect as a worker instead. This prevents a
@@ -3317,10 +3317,10 @@ async function startWebDashboard(openBrowser) {
3317
3317
  webStateInterval = null;
3318
3318
  }
3319
3319
  if (webDashboard) {
3320
- try { webDashboard.stop(); } catch (_) { /* ignore */ }
3320
+ try { webDashboard.stop(); } catch (_) { /* web server may not have bound yet — nothing to stop */ }
3321
3321
  }
3322
3322
  if (coordinator) {
3323
- try { coordinator.stop(); } catch (_) { /* ignore */ }
3323
+ try { coordinator.stop(); } catch (_) { /* coordinator may not have started its IPC server */ }
3324
3324
  }
3325
3325
  removeLock();
3326
3326
  removeSocket();
@@ -3387,7 +3387,7 @@ function restartProcess() {
3387
3387
  // child can acquire it. The parent stays alive waiting on child.on('close'),
3388
3388
  // so without this the child sees the parent as an active owner and refuses.
3389
3389
  if (monitorLockFile) {
3390
- try { monitorLock.release(monitorLockFile); } catch (_) { /* ignore */ }
3390
+ try { monitorLock.release(monitorLockFile); } catch (_) { /* lock file may have already been unlinked */ }
3391
3391
  monitorLockFile = null;
3392
3392
  }
3393
3393
 
@@ -3433,23 +3433,23 @@ function cleanupResources() {
3433
3433
 
3434
3434
  // Restore terminal first so the user sees a clean prompt even if a
3435
3435
  // later step throws.
3436
- try { write(ansi.showCursor); } catch (_) { /* ignore */ }
3437
- try { write(ansi.restoreScreen); } catch (_) { /* ignore */ }
3438
- try { restoreTerminalTitle(); } catch (_) { /* ignore */ }
3439
- try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch (_) { /* ignore */ }
3440
- try { process.stdin.pause(); } catch (_) { /* ignore */ }
3436
+ try { write(ansi.showCursor); } catch (_) { /* stdout may be closed during crash cleanup */ }
3437
+ try { write(ansi.restoreScreen); } catch (_) { /* stdout may be closed during crash cleanup */ }
3438
+ try { restoreTerminalTitle(); } catch (_) { /* stdout may be closed during crash cleanup */ }
3439
+ try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch (_) { /* stdin may already be unraw or detached */ }
3440
+ try { process.stdin.pause(); } catch (_) { /* stdin may already be paused or destroyed */ }
3441
3441
 
3442
3442
  if (pollIntervalId) {
3443
- try { clearTimeout(pollIntervalId); } catch (_) { /* ignore */ }
3443
+ try { clearTimeout(pollIntervalId); } catch (_) { /* defensive — clearTimeout normally won't throw */ }
3444
3444
  pollIntervalId = null;
3445
3445
  }
3446
3446
 
3447
3447
  if (periodicUpdateCheck) {
3448
- try { periodicUpdateCheck.stop(); } catch (_) { /* ignore */ }
3448
+ try { periodicUpdateCheck.stop(); } catch (_) { /* interval handle may already be cleared */ }
3449
3449
  }
3450
3450
 
3451
3451
  if (fileWatcher) {
3452
- try { fileWatcher.close(); } catch (_) { /* ignore */ }
3452
+ try { fileWatcher.close(); } catch (_) { /* watcher may already be closed by OS or previous cleanup */ }
3453
3453
  fileWatcher = null;
3454
3454
  }
3455
3455
 
@@ -3457,24 +3457,24 @@ function cleanupResources() {
3457
3457
  if (SERVER_MODE === 'static') {
3458
3458
  try {
3459
3459
  clients.forEach((client) => {
3460
- try { client.end(); } catch (_) { /* ignore */ }
3460
+ try { client.end(); } catch (_) { /* SSE client socket already closed */ }
3461
3461
  });
3462
3462
  clients.clear();
3463
- } catch (_) { /* ignore */ }
3463
+ } catch (_) { /* clients set may have mutated mid-iteration during shutdown */ }
3464
3464
  }
3465
3465
 
3466
3466
  // User's dev-server process (command mode)
3467
3467
  if (SERVER_MODE === 'command') {
3468
- try { stopServerProcess(); } catch (_) { /* ignore */ }
3468
+ try { stopServerProcess(); } catch (_) { /* dev-server child may already be gone */ }
3469
3469
  }
3470
3470
 
3471
3471
  // Web dashboard + worker/coordinator (unlinks lock file + IPC socket)
3472
- try { stopWebDashboard(); } catch (_) { /* ignore */ }
3472
+ try { stopWebDashboard(); } catch (_) { /* web dashboard may never have been started */ }
3473
3473
 
3474
3474
  // Per-repo monitor lock — release last so the slot stays reserved for the
3475
3475
  // entire lifetime of this process, including any errors in the steps above.
3476
3476
  if (monitorLockFile) {
3477
- try { monitorLock.release(monitorLockFile); } catch (_) { /* ignore */ }
3477
+ try { monitorLock.release(monitorLockFile); } catch (_) { /* lock file may have been unlinked externally */ }
3478
3478
  monitorLockFile = null;
3479
3479
  }
3480
3480
  }
@@ -3523,9 +3523,9 @@ process.on('uncaughtException', async (err) => {
3523
3523
  // if telemetry shutdown hangs or throws.
3524
3524
  cleanupResources();
3525
3525
 
3526
- try { telemetry.captureError(err); } catch (_) { /* ignore */ }
3526
+ try { telemetry.captureError(err); } catch (_) { /* telemetry must never prevent crash cleanup */ }
3527
3527
  console.error('Uncaught exception:', err);
3528
- try { await telemetry.shutdown(); } catch (_) { /* ignore */ }
3528
+ try { await telemetry.shutdown(); } catch (_) { /* telemetry must never prevent crash cleanup */ }
3529
3529
  process.exit(1);
3530
3530
  });
3531
3531
 
@@ -3652,11 +3652,11 @@ async function start() {
3652
3652
 
3653
3653
  // Detect default branch for ahead/behind counts, then fetch initial data
3654
3654
  detectDefaultBranch().then(() => {
3655
- fetchAheadBehindForBranches(initBranches).catch(() => {});
3656
- }).catch(() => {});
3655
+ fetchAheadBehindForBranches(initBranches).catch(() => { /* ahead/behind is background-only — stale counts are better than a noisy startup */ });
3656
+ }).catch(() => { /* no default branch detectable (no remote refs yet) — ahead/behind stays hidden */ });
3657
3657
 
3658
3658
  // Load sparklines and action cache in background
3659
- refreshAllSparklines().catch(() => {});
3659
+ refreshAllSparklines().catch(() => { /* sparkline cache stays empty — activity column just renders blank */ });
3660
3660
  initActionCache().then(() => {
3661
3661
  // Once env is known, kick off initial PR status fetch
3662
3662
  fetchAllPrStatuses().then(map => {
@@ -3665,8 +3665,8 @@ async function start() {
3665
3665
  lastPrStatusFetch = Date.now();
3666
3666
  render();
3667
3667
  }
3668
- }).catch(() => {});
3669
- }).catch(() => {});
3668
+ }).catch(() => { /* gh/glab unreachable — inline PR indicators stay hidden, poller will retry */ });
3669
+ }).catch(() => { /* cliEnv detection failed — PR actions fall back to web links where possible */ });
3670
3670
 
3671
3671
  // Start server based on mode
3672
3672
  const startBranchName = store.get('currentBranch');
@@ -3737,7 +3737,7 @@ async function start() {
3737
3737
  addLog(`New version available: ${latestVersion} \u2192 npm i -g git-watchtower`, 'update');
3738
3738
  render();
3739
3739
  }
3740
- }).catch(() => {});
3740
+ }).catch(() => { /* npm registry unreachable — periodic check will try again in 4h */ });
3741
3741
 
3742
3742
  // Re-check for updates periodically (every 4 hours) while running.
3743
3743
  // Assigned to module scope so the top-level exit handler can stop it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.14.1",
3
+ "version": "1.14.3",
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": {
package/src/git/remote.js CHANGED
@@ -77,7 +77,7 @@ function detectPlatform(webUrl) {
77
77
  if (host === 'gitlab.com' || parts.includes('gitlab')) return 'gitlab';
78
78
  if (host === 'bitbucket.org' || parts.includes('bitbucket')) return 'bitbucket';
79
79
  if (host === 'dev.azure.com' || host.endsWith('.visualstudio.com')) return 'azure';
80
- } catch (e) { /* ignore */ }
80
+ } catch (e) { /* webUrl isn't a valid URL — fall through to the self-hosted default */ }
81
81
  return 'github'; // default assumption for self-hosted
82
82
  }
83
83
 
@@ -161,14 +161,14 @@ function finalizeLock(pid, port, socketPath) {
161
161
  * Remove the lock file.
162
162
  */
163
163
  function removeLock() {
164
- try { fs.unlinkSync(LOCK_FILE); } catch (e) { /* ignore */ }
164
+ try { fs.unlinkSync(LOCK_FILE); } catch (e) { /* lock file may not exist */ }
165
165
  }
166
166
 
167
167
  /**
168
168
  * Remove stale socket file.
169
169
  */
170
170
  function removeSocket() {
171
- try { fs.unlinkSync(SOCKET_PATH); } catch (e) { /* ignore */ }
171
+ try { fs.unlinkSync(SOCKET_PATH); } catch (e) { /* socket file may not exist */ }
172
172
  }
173
173
 
174
174
  /**
@@ -255,7 +255,7 @@ class Coordinator {
255
255
  stop() {
256
256
  // Close all worker sockets
257
257
  for (const socket of this.workerSockets.values()) {
258
- try { socket.destroy(); } catch (e) { /* ignore */ }
258
+ try { socket.destroy(); } catch (e) { /* socket may already be destroyed */ }
259
259
  }
260
260
  this.workerSockets.clear();
261
261
  this.projects.clear();
@@ -359,7 +359,7 @@ class Coordinator {
359
359
  try {
360
360
  const msg = JSON.parse(line);
361
361
  this._handleWorkerMessage(socket, msg, (id) => { workerId = id; }, () => workerId);
362
- } catch (e) { /* ignore bad JSON */ }
362
+ } catch (e) { /* malformed IPC frame from worker — skip it and keep reading */ }
363
363
  }
364
364
  }
365
365
  });
@@ -445,7 +445,7 @@ class Coordinator {
445
445
  _sendMessage(socket, msg) {
446
446
  try {
447
447
  socket.write(JSON.stringify(msg) + '\n');
448
- } catch (e) { /* ignore write errors on dead sockets */ }
448
+ } catch (e) { /* peer socket closed between iteration and write peer will reconnect if it recovers */ }
449
449
  }
450
450
 
451
451
  /**
@@ -517,7 +517,7 @@ class Worker {
517
517
  try {
518
518
  const msg = JSON.parse(line);
519
519
  this._handleMessage(msg);
520
- } catch (e) { /* ignore */ }
520
+ } catch (e) { /* malformed IPC frame from coordinator — skip it and keep reading */ }
521
521
  }
522
522
  }
523
523
  });
@@ -574,7 +574,7 @@ class Worker {
574
574
  if (this.socket && this._connected) {
575
575
  try {
576
576
  this.socket.write(JSON.stringify(msg) + '\n');
577
- } catch (e) { /* ignore */ }
577
+ } catch (e) { /* coordinator socket closed between isConnected() check and write */ }
578
578
  }
579
579
  }
580
580
 
@@ -265,38 +265,13 @@ class ProcessManager {
265
265
  return false;
266
266
  }
267
267
 
268
- // Capture reference before nulling — needed for deferred SIGKILL
268
+ // Capture reference before nulling — needed for deferred force-kill
269
269
  const proc = this.process;
270
270
 
271
- // Try graceful shutdown first
272
271
  if (process.platform === 'win32') {
273
- try {
274
- spawn('taskkill', ['/pid', proc.pid.toString(), '/f', '/t']);
275
- } catch (e) {
276
- // Ignore taskkill errors
277
- }
272
+ this._stopWindows(proc);
278
273
  } else {
279
- try {
280
- // Kill the entire process group (negative PID) so that
281
- // grandchildren (e.g. npm -> node -> vite) are also terminated.
282
- process.kill(-proc.pid, 'SIGTERM');
283
-
284
- // Force kill after grace period
285
- const forceKillTimeout = setTimeout(() => {
286
- try {
287
- process.kill(-proc.pid, 'SIGKILL');
288
- } catch (e) {
289
- // Process group may already be dead
290
- }
291
- }, KILL_GRACE_PERIOD);
292
-
293
- // Clear timeout if process exits cleanly
294
- proc.once('close', () => {
295
- clearTimeout(forceKillTimeout);
296
- });
297
- } catch (e) {
298
- // Process group may already be dead
299
- }
274
+ this._stopUnix(proc);
300
275
  }
301
276
 
302
277
  this.process = null;
@@ -305,6 +280,77 @@ class ProcessManager {
305
280
  return true;
306
281
  }
307
282
 
283
+ /**
284
+ * Unix stop: SIGTERM the process group, then SIGKILL after a grace period.
285
+ * The grace timer is unref'd so it doesn't keep the event loop alive when
286
+ * the main process wants to exit.
287
+ * @param {import('child_process').ChildProcess} proc
288
+ * @private
289
+ */
290
+ _stopUnix(proc) {
291
+ // If the process has already exited, there's nothing to signal.
292
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
293
+
294
+ try {
295
+ process.kill(-proc.pid, 'SIGTERM');
296
+ } catch (e) {
297
+ // Process group may already be dead
298
+ return;
299
+ }
300
+
301
+ const forceKillTimeout = setTimeout(() => {
302
+ // Re-check: process may have exited during the grace period.
303
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
304
+ try {
305
+ process.kill(-proc.pid, 'SIGKILL');
306
+ } catch (e) {
307
+ // Process group may already be dead
308
+ }
309
+ }, KILL_GRACE_PERIOD);
310
+
311
+ // Don't let this timer keep the event loop alive on shutdown.
312
+ forceKillTimeout.unref();
313
+
314
+ // Clear early if the process exits before the grace period.
315
+ proc.once('close', () => {
316
+ clearTimeout(forceKillTimeout);
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Windows stop: taskkill /t (tree kill). If the process doesn't exit
322
+ * within the grace period, retry with /f (force).
323
+ * @param {import('child_process').ChildProcess} proc
324
+ * @private
325
+ */
326
+ _stopWindows(proc) {
327
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
328
+
329
+ try {
330
+ spawn('taskkill', ['/pid', proc.pid.toString(), '/t']);
331
+ } catch (e) {
332
+ // Ignore spawn errors (PID already gone, etc.)
333
+ return;
334
+ }
335
+
336
+ // Fallback: force-kill if the process is still alive after the
337
+ // grace period. This mirrors the Unix SIGTERM → SIGKILL pattern.
338
+ const forceKillTimeout = setTimeout(() => {
339
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
340
+ try {
341
+ spawn('taskkill', ['/pid', proc.pid.toString(), '/f', '/t']);
342
+ } catch (e) {
343
+ // Ignore — process may already be dead
344
+ }
345
+ }, KILL_GRACE_PERIOD);
346
+
347
+ forceKillTimeout.unref();
348
+
349
+ proc.once('close', () => {
350
+ clearTimeout(forceKillTimeout);
351
+ });
352
+ }
353
+
308
354
  /**
309
355
  * Restart the server process
310
356
  * @returns {Promise<{success: boolean, error?: Error, pid?: number}>}
@@ -70,7 +70,7 @@ function getDashboardJs() {
70
70
  function savePrefs(updates) {
71
71
  const prefs = loadPrefs();
72
72
  Object.keys(updates).forEach((k) => { prefs[k] = updates[k]; });
73
- try { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } catch (e) { /* ignore */ }
73
+ try { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } catch (e) { /* localStorage quota exceeded or disabled (private mode) — prefs are best-effort */ }
74
74
  return prefs;
75
75
  }
76
76
  const prefs = loadPrefs();
@@ -122,7 +122,7 @@ function getDashboardJs() {
122
122
  try {
123
123
  const n = new Notification(title, { body, tag: tag || 'git-watchtower', icon: '', silent: false });
124
124
  setTimeout(() => n.close(), 8000);
125
- } catch (e) { /* ignore */ }
125
+ } catch (e) { /* Notification constructor can throw on some browsers (e.g. permission revoked mid-session) */ }
126
126
  }
127
127
 
128
128
  function diffBranchesForNotifications(oldBranches, newBranches) {
@@ -232,14 +232,14 @@ function getDashboardJs() {
232
232
  }
233
233
  renderTabs();
234
234
  render();
235
- } catch (err) { /* ignore parse errors */ }
235
+ } catch (err) { /* malformed SSE state frame — skip this push, next one will re-render */ }
236
236
  });
237
237
 
238
238
  evtSource.addEventListener('flash', (e) => {
239
239
  try {
240
240
  const data = JSON.parse(e.data);
241
241
  showFlash(data.text, data.type);
242
- } catch (err) { /* ignore */ }
242
+ } catch (err) { /* malformed flash payload — not worth surfacing, skip */ }
243
243
  });
244
244
 
245
245
  evtSource.addEventListener('actionResult', (e) => {
@@ -251,7 +251,7 @@ function getDashboardJs() {
251
251
  } else {
252
252
  showToast(data.message, data.success ? 'success' : 'error');
253
253
  }
254
- } catch (err) { /* ignore */ }
254
+ } catch (err) { /* malformed actionResult payload — skip (the action already ran server-side) */ }
255
255
  });
256
256
 
257
257
  evtSource.onerror = () => {
@@ -434,7 +434,7 @@ function getDashboardJs() {
434
434
  state.serverMode = pState.serverMode || 'none';
435
435
  state.repoWebUrl = pState.repoWebUrl || null;
436
436
  render();
437
- } catch (err) { /* ignore */ }
437
+ } catch (err) { /* malformed per-project state response — keep current view until the next poll */ }
438
438
  }
439
439
  };
440
440
  xhr.send();
@@ -804,8 +804,8 @@ ${pureFnBlock}
804
804
  const btn = e.target.closest('.action-item');
805
805
  if (!btn) return;
806
806
  const key = btn.getAttribute('data-action-key');
807
- const data = {};
808
- try { data = JSON.parse(btn.getAttribute('data-action-data') || '{}'); } catch (err) { /* ignore */ }
807
+ let data = {};
808
+ try { data = JSON.parse(btn.getAttribute('data-action-data') || '{}'); } catch (err) { /* malformed data-action-data — fall through with empty object */ }
809
809
 
810
810
  hideBranchActions();
811
811
 
package/src/server/web.js CHANGED
@@ -272,7 +272,7 @@ class WebDashboardServer {
272
272
 
273
273
  // Close all SSE connections
274
274
  for (const client of this.clients) {
275
- try { client.end(); } catch (e) { /* ignore */ }
275
+ try { client.end(); } catch (e) { /* SSE client may already be disconnected */ }
276
276
  }
277
277
  this.clients.clear();
278
278
 
@@ -293,7 +293,7 @@ class WebDashboardServer {
293
293
  try {
294
294
  client.write('event: flash\n');
295
295
  client.write('data: ' + data + '\n\n');
296
- } catch (e) { /* ignore dead clients */ }
296
+ } catch (e) { /* SSE client disconnected — will be pruned when its response closes */ }
297
297
  }
298
298
  }
299
299
 
@@ -307,7 +307,7 @@ class WebDashboardServer {
307
307
  try {
308
308
  client.write('event: preview\n');
309
309
  client.write('data: ' + json + '\n\n');
310
- } catch (e) { /* ignore dead clients */ }
310
+ } catch (e) { /* SSE client disconnected — will be pruned when its response closes */ }
311
311
  }
312
312
  }
313
313
 
@@ -321,7 +321,7 @@ class WebDashboardServer {
321
321
  try {
322
322
  client.write('event: actionResult\n');
323
323
  client.write('data: ' + json + '\n\n');
324
- } catch (e) { /* ignore dead clients */ }
324
+ } catch (e) { /* SSE client disconnected — will be pruned when its response closes */ }
325
325
  }
326
326
  }
327
327
 
@@ -74,7 +74,7 @@ function readLock(file) {
74
74
  }
75
75
 
76
76
  function removeLock(file) {
77
- try { fs.unlinkSync(file); } catch (e) { /* ignore */ }
77
+ try { fs.unlinkSync(file); } catch (e) { /* lock file may not exist (already removed or never created) */ }
78
78
  }
79
79
 
80
80
  /**
@@ -100,7 +100,7 @@ function startPeriodicUpdateCheck(onUpdateFound, interval = UPDATE_CHECK_INTERVA
100
100
  .then((latestVersion) => {
101
101
  if (latestVersion) onUpdateFound(latestVersion);
102
102
  })
103
- .catch(() => {});
103
+ .catch(() => { /* npm registry unreachable — next scheduled tick will try again */ });
104
104
  }, interval);
105
105
 
106
106
  return { stop: () => clearInterval(timerId) };