git-watchtower 1.14.11 → 1.14.13

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.
@@ -103,6 +103,7 @@ const store = new Store();
103
103
  const { WebDashboardServer } = require('../src/server/web');
104
104
  const { Coordinator, Worker, generateProjectId, getActiveCoordinator, tryAcquireLock, finalizeLock, removeLock, removeSocket, isProcessAlive } = require('../src/server/coordinator');
105
105
  const monitorLock = require('../src/utils/monitor-lock');
106
+ const { createPipeErrorHandler } = require('../src/utils/pipe-error');
106
107
 
107
108
  const PROJECT_ROOT = process.cwd();
108
109
 
@@ -3602,6 +3603,27 @@ process.on('SIGTERM', shutdown);
3602
3603
  process.on('exit', () => {
3603
3604
  cleanupResources();
3604
3605
  });
3606
+
3607
+ // Defense-in-depth against a stdio pipe closing mid-run. The #13 TTY guard
3608
+ // stops `git-watchtower | head` at startup, but a TTY can still disappear
3609
+ // later (SSH drops, terminal window closes, pty tears down). Without this,
3610
+ // the next write() emits an async EPIPE which Node promotes to
3611
+ // uncaughtException — producing a crash report and telemetry noise for
3612
+ // what is a benign pipe-closed condition.
3613
+ const stdioPipeErrorHandler = createPipeErrorHandler({
3614
+ onEpipe: () => {
3615
+ isShuttingDown = true;
3616
+ try { cleanupResources(); } catch (_) { /* best-effort during pipe-close */ }
3617
+ process.exit(0);
3618
+ },
3619
+ onOther: (err) => {
3620
+ // Any other stdio error is unexpected — re-raise so the existing
3621
+ // uncaughtException handler can capture telemetry and restore terminal.
3622
+ setImmediate(() => { throw err; });
3623
+ },
3624
+ });
3625
+ process.stdout.on('error', stdioPipeErrorHandler);
3626
+ process.stderr.on('error', stdioPipeErrorHandler);
3605
3627
  process.on('uncaughtException', async (err) => {
3606
3628
  isShuttingDown = true;
3607
3629
 
@@ -3644,6 +3666,20 @@ process.on('unhandledRejection', async (reason) => {
3644
3666
  // ============================================================================
3645
3667
 
3646
3668
  async function start() {
3669
+ // git-watchtower is a full-screen TUI. If stdout isn't a TTY (piped to a
3670
+ // file, `tee`, `head`, or captured by a non-interactive CI runner) every
3671
+ // frame writes hideCursor/clearScreen/moveTo/color escapes into the pipe,
3672
+ // producing an unreadable log and wasting CPU on a render loop no human
3673
+ // will see. Refuse to start and point the user at a sensible alternative.
3674
+ if (!process.stdout.isTTY) {
3675
+ console.error('git-watchtower: stdout is not a TTY.');
3676
+ console.error('');
3677
+ console.error(' This is an interactive terminal UI and cannot render when stdout is');
3678
+ console.error(' piped or redirected. If you only need a CI-friendly status check,');
3679
+ console.error(' use `git fetch` + `git log` directly.');
3680
+ process.exit(1);
3681
+ }
3682
+
3647
3683
  // Check if git is available
3648
3684
  const gitAvailable = await checkGitAvailable();
3649
3685
  if (!gitAvailable) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.14.11",
3
+ "version": "1.14.13",
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": {
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Stdio pipe error handling.
3
+ *
4
+ * The TUI writes ANSI frames to stdout continuously. If the downstream
5
+ * consumer goes away — SSH drops, the terminal window closes, or the
6
+ * user intentionally short-circuits via `| head` on a non-guarded
7
+ * invocation — the next write() emits an async 'error' event with
8
+ * code EPIPE on the stream. Without a handler, Node promotes that to
9
+ * uncaughtException, which logs a crash report, generates telemetry
10
+ * noise, and clutters the user's prompt on exit.
11
+ *
12
+ * A normal pipe-closed condition is not a crash; the correct response
13
+ * is to clean up and exit quietly with status 0.
14
+ *
15
+ * @module utils/pipe-error
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ /**
21
+ * Build an 'error' handler for process.stdout / process.stderr.
22
+ *
23
+ * @param {Object} callbacks
24
+ * @param {() => void} callbacks.onEpipe - Invoked when the pipe is closed
25
+ * from the other end. Typically runs cleanupResources() and exits 0.
26
+ * @param {(err: Error) => void} callbacks.onOther - Invoked for any other
27
+ * stream error. Typically re-raises via setImmediate so the existing
28
+ * uncaughtException handler can capture telemetry.
29
+ * @returns {(err: Error & { code?: string }) => void}
30
+ */
31
+ function createPipeErrorHandler({ onEpipe, onOther }) {
32
+ return function pipeErrorHandler(err) {
33
+ if (err && err.code === 'EPIPE') {
34
+ onEpipe();
35
+ return;
36
+ }
37
+ onOther(err);
38
+ };
39
+ }
40
+
41
+ module.exports = {
42
+ createPipeErrorHandler,
43
+ };