@sayknow-cli/utils 0.2.4 → 0.2.6

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/postmortem.ts +40 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/utils",
4
- "version": "0.2.4",
4
+ "version": "0.2.6",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://github.com/jaybeyond/Sayknow_CLI",
7
7
  "author": "jaybeyond",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@sayknow-cli/natives": "0.2.4",
34
+ "@sayknow-cli/natives": "0.2.6",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
package/src/postmortem.ts CHANGED
@@ -76,9 +76,34 @@ function formatFatalError(label: string, err: Error): string {
76
76
  return `\n[${label}] ${name}: ${message}${formattedStack}\n`;
77
77
  }
78
78
 
79
+ /**
80
+ * True for terminal/pipe write failures (EIO, EPIPE, EBADF, …) on the write side.
81
+ *
82
+ * Bun flushes TTY writes asynchronously, so when the controlling terminal hangs up
83
+ * (terminal closed, external volume stalled, SSH dropped) a restore write issued during
84
+ * shutdown can surface as an uncaughtException on a later tick — after cleanup has already
85
+ * begun. Treating that as fatal logs a scary stack and exits nonzero, which a hosting
86
+ * terminal renders as a crash/restart loop. When it happens mid-shutdown it is a benign
87
+ * artifact, not a real fault. We only match write-side syscalls so genuine bugs (a read
88
+ * EIO, a logic error that happens to carry one of these codes) are never masked.
89
+ */
90
+ function isBenignIoWriteError(err: unknown): boolean {
91
+ if (!err || typeof err !== "object") return false;
92
+ const { code, syscall } = err as { code?: unknown; syscall?: unknown };
93
+ const isIoCode =
94
+ code === "EIO" ||
95
+ code === "EPIPE" ||
96
+ code === "EBADF" ||
97
+ code === "ENXIO" ||
98
+ code === "ENOTTY" ||
99
+ code === "ECONNRESET";
100
+ return isIoCode && (syscall === undefined || syscall === "write" || syscall === "writev");
101
+ }
102
+
79
103
  if (isMainThread) {
80
104
  process
81
105
  .on("SIGINT", async () => {
106
+ logger.debug("Shutting down on SIGINT");
82
107
  await runCleanup(Reason.SIGINT);
83
108
  process.exit(130); // 128 + SIGINT (2)
84
109
  })
@@ -90,6 +115,19 @@ if (isMainThread) {
90
115
  safeStderrWrite(`Inspector opened: ${url}\n`);
91
116
  })
92
117
  .on("uncaughtException", async err => {
118
+ // A terminal/stdout write can fail asynchronously (EIO/EPIPE) while we are
119
+ // already tearing down — e.g. the controlling terminal hung up, or the volume
120
+ // the runtime lives on stalled. Bun surfaces that on a later tick, after cleanup
121
+ // has begun. Treat it as a benign shutdown artifact: log at debug and let the
122
+ // in-progress shutdown own the exit, instead of logging a fatal error and exiting
123
+ // nonzero (which a hosting terminal renders as a crash/restart loop).
124
+ if (cleanupStage !== "idle" && isBenignIoWriteError(err)) {
125
+ logger.debug("Ignoring benign terminal I/O write error during shutdown", {
126
+ code: (err as { code?: unknown }).code,
127
+ syscall: (err as { syscall?: unknown }).syscall,
128
+ });
129
+ return;
130
+ }
93
131
  safeStderrWrite(formatFatalError("Uncaught Exception", err));
94
132
  logger.error("Uncaught exception", { err, stack: err.stack });
95
133
  await runCleanup(Reason.UNCAUGHT_EXCEPTION);
@@ -106,10 +144,12 @@ if (isMainThread) {
106
144
  void runCleanup(Reason.EXIT); // fire and forget (exit imminent)
107
145
  })
108
146
  .on("SIGTERM", async () => {
147
+ logger.warn("Shutting down on SIGTERM (terminated by another process)");
109
148
  await runCleanup(Reason.SIGTERM);
110
149
  process.exit(143); // 128 + SIGTERM (15)
111
150
  })
112
151
  .on("SIGHUP", async () => {
152
+ logger.warn("Shutting down on SIGHUP (controlling terminal hung up / disconnected)");
113
153
  await runCleanup(Reason.SIGHUP);
114
154
  process.exit(129); // 128 + SIGHUP (1)
115
155
  });