git-watchtower 1.12.5 → 1.12.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.
@@ -3271,6 +3271,18 @@ async function startWebDashboard(openBrowser) {
3271
3271
  render();
3272
3272
  } catch (err) {
3273
3273
  addLog(`Web dashboard failed: ${err.message}`, 'error');
3274
+ // Defensive: if we got far enough to arm the state-push interval,
3275
+ // clear it. The current ordering starts the interval only after
3276
+ // webDashboard.start() resolves, but this keeps cleanup robust
3277
+ // against future reordering and against failures in the
3278
+ // post-bind statements (e.g. openInBrowser, addLog).
3279
+ if (webStateInterval) {
3280
+ clearInterval(webStateInterval);
3281
+ webStateInterval = null;
3282
+ }
3283
+ if (webDashboard) {
3284
+ try { webDashboard.stop(); } catch (_) { /* ignore */ }
3285
+ }
3274
3286
  if (coordinator) {
3275
3287
  try { coordinator.stop(); } catch (_) { /* ignore */ }
3276
3288
  }
@@ -3355,39 +3367,80 @@ function restartProcess() {
3355
3367
  // ============================================================================
3356
3368
 
3357
3369
  let isShuttingDown = false;
3370
+ let _resourcesCleaned = false;
3358
3371
 
3359
- async function shutdown() {
3360
- if (isShuttingDown) return;
3361
- isShuttingDown = true;
3372
+ /**
3373
+ * Idempotent, best-effort cleanup of every long-lived resource we own:
3374
+ * terminal state, timers, file watcher, live-reload SSE clients, the
3375
+ * user's dev-server child process, and the web-dashboard / coordinator
3376
+ * (which unlinks the lock file and IPC socket). Safe to call multiple
3377
+ * times and from any exit path (shutdown, uncaughtException, 'exit').
3378
+ *
3379
+ * Every step is wrapped in try/catch so a failure in one resource does
3380
+ * not prevent the rest from being cleaned up. Stays synchronous so it
3381
+ * can run inside an 'exit' handler where async callbacks won't execute.
3382
+ */
3383
+ function cleanupResources() {
3384
+ if (_resourcesCleaned) return;
3385
+ _resourcesCleaned = true;
3386
+
3387
+ // Restore terminal first so the user sees a clean prompt even if a
3388
+ // later step throws.
3389
+ try { write(ansi.showCursor); } catch (_) { /* ignore */ }
3390
+ try { write(ansi.restoreScreen); } catch (_) { /* ignore */ }
3391
+ try { restoreTerminalTitle(); } catch (_) { /* ignore */ }
3392
+ try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch (_) { /* ignore */ }
3393
+ try { process.stdin.pause(); } catch (_) { /* ignore */ }
3362
3394
 
3363
- // Restore terminal
3364
- write(ansi.showCursor);
3365
- write(ansi.restoreScreen);
3366
- restoreTerminalTitle();
3395
+ if (pollIntervalId) {
3396
+ try { clearTimeout(pollIntervalId); } catch (_) { /* ignore */ }
3397
+ pollIntervalId = null;
3398
+ }
3367
3399
 
3368
- if (process.stdin.isTTY) {
3369
- process.stdin.setRawMode(false);
3400
+ if (periodicUpdateCheck) {
3401
+ try { periodicUpdateCheck.stop(); } catch (_) { /* ignore */ }
3370
3402
  }
3371
- process.stdin.pause();
3372
3403
 
3373
- if (fileWatcher) fileWatcher.close();
3374
- if (pollIntervalId) clearTimeout(pollIntervalId);
3404
+ if (fileWatcher) {
3405
+ try { fileWatcher.close(); } catch (_) { /* ignore */ }
3406
+ fileWatcher = null;
3407
+ }
3375
3408
 
3376
- // Stop server based on mode
3409
+ // Live-reload SSE clients (static mode)
3410
+ if (SERVER_MODE === 'static') {
3411
+ try {
3412
+ clients.forEach((client) => {
3413
+ try { client.end(); } catch (_) { /* ignore */ }
3414
+ });
3415
+ clients.clear();
3416
+ } catch (_) { /* ignore */ }
3417
+ }
3418
+
3419
+ // User's dev-server process (command mode)
3377
3420
  if (SERVER_MODE === 'command') {
3378
- stopServerProcess();
3379
- } else if (SERVER_MODE === 'static') {
3380
- clients.forEach(client => client.end());
3381
- clients.clear();
3421
+ try { stopServerProcess(); } catch (_) { /* ignore */ }
3422
+ }
3423
+
3424
+ // Web dashboard + worker/coordinator (unlinks lock file + IPC socket)
3425
+ try { stopWebDashboard(); } catch (_) { /* ignore */ }
3426
+ }
3427
+
3428
+ async function shutdown() {
3429
+ if (isShuttingDown) return;
3430
+ isShuttingDown = true;
3431
+
3432
+ cleanupResources();
3382
3433
 
3383
- const serverClosePromise = new Promise(resolve => server.close(resolve));
3384
- const timeoutPromise = new Promise(resolve => setTimeout(resolve, SERVER_CLOSE_TIMEOUT_MS));
3434
+ // For the static HTTP server, give in-flight connections a brief
3435
+ // grace period to drain. cleanupResources ended the SSE clients; this
3436
+ // races server.close against SERVER_CLOSE_TIMEOUT_MS so we never hang
3437
+ // forever on a stuck browser.
3438
+ if (SERVER_MODE === 'static' && server) {
3439
+ const serverClosePromise = new Promise((resolve) => server.close(resolve));
3440
+ const timeoutPromise = new Promise((resolve) => setTimeout(resolve, SERVER_CLOSE_TIMEOUT_MS));
3385
3441
  await Promise.race([serverClosePromise, timeoutPromise]);
3386
3442
  }
3387
3443
 
3388
- // Stop web dashboard and coordinator
3389
- stopWebDashboard();
3390
-
3391
3444
  // Flush telemetry
3392
3445
  telemetry.capture('session_ended', {
3393
3446
  duration_seconds: sessionStartTime ? Math.round((Date.now() - sessionStartTime) / 1000) : 0,
@@ -3402,19 +3455,23 @@ async function shutdown() {
3402
3455
 
3403
3456
  process.on('SIGINT', shutdown);
3404
3457
  process.on('SIGTERM', shutdown);
3405
- // Clean up long-lived timers on exit, regardless of which code path got us
3406
- // here (normal exit, uncaught exception, early failure in start()).
3458
+ // Belt-and-suspenders: if we exit via a path that didn't call
3459
+ // cleanupResources (e.g. a hard crash in startup before handlers were
3460
+ // registered), still do synchronous best-effort cleanup.
3407
3461
  process.on('exit', () => {
3408
- if (periodicUpdateCheck) periodicUpdateCheck.stop();
3462
+ cleanupResources();
3409
3463
  });
3410
3464
  process.on('uncaughtException', async (err) => {
3411
- telemetry.captureError(err);
3412
- write(ansi.showCursor);
3413
- write(ansi.restoreScreen);
3414
- restoreTerminalTitle();
3415
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
3465
+ isShuttingDown = true;
3466
+
3467
+ // Synchronous teardown first — so the coordinator socket, lock file,
3468
+ // dev-server child process group, and SSE clients are released even
3469
+ // if telemetry shutdown hangs or throws.
3470
+ cleanupResources();
3471
+
3472
+ try { telemetry.captureError(err); } catch (_) { /* ignore */ }
3416
3473
  console.error('Uncaught exception:', err);
3417
- await telemetry.shutdown();
3474
+ try { await telemetry.shutdown(); } catch (_) { /* ignore */ }
3418
3475
  process.exit(1);
3419
3476
  });
3420
3477
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.12.5",
3
+ "version": "1.12.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": {