@sayknow-cli/utils 0.3.13 → 0.3.16

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.
@@ -0,0 +1,14 @@
1
+ export declare class TempDir {
2
+ #private;
3
+ private constructor();
4
+ static createSync(prefix?: string): TempDir;
5
+ static create(prefix?: string): Promise<TempDir>;
6
+ path(): string;
7
+ absolute(): string;
8
+ remove(): Promise<void>;
9
+ removeSync(): void;
10
+ toString(): string;
11
+ join(...paths: string[]): string;
12
+ [Symbol.asyncDispose](): Promise<void>;
13
+ [Symbol.dispose](): void;
14
+ }
@@ -0,0 +1,3 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
+ export declare function asRecord(value: unknown): Record<string, unknown> | null;
3
+ export declare function toError(value: unknown): Error;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Cache policy for which lookups.
3
+ */
4
+ export declare const enum WhichCachePolicy {
5
+ /**
6
+ * Use cached result if available.
7
+ */
8
+ Cached = 0,
9
+ /**
10
+ * Bypass cache and perform a new lookup.
11
+ */
12
+ Bypass = 1,
13
+ /**
14
+ * Always update cache.
15
+ */
16
+ Fresh = 2,
17
+ /**
18
+ * Read-only, serves from cache if present, but doesn't write.
19
+ */
20
+ ReadOnly = 3
21
+ }
22
+ export interface WhichOptions extends Bun.WhichOptions {
23
+ /**
24
+ * Cache policy for the lookup.
25
+ * Defaults to `WhichCachePolicy.Fresh`.
26
+ */
27
+ cache?: WhichCachePolicy;
28
+ }
29
+ declare function darwinWhich(command: string, _options?: Bun.WhichOptions): string | null;
30
+ export declare const whichFresh: typeof darwinWhich;
31
+ /**
32
+ * Locate binary on PATH (with flexible caching).
33
+ *
34
+ * @param command - Binary name to resolve
35
+ * @param options - Bun.WhichOptions plus `cache` control
36
+ * @returns Filesystem path if found, else null
37
+ */
38
+ export declare function $which(command: string, options?: WhichOptions): string | null;
39
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/utils",
4
- "version": "0.3.13",
4
+ "version": "0.3.16",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -21,7 +21,7 @@
21
21
  "streams"
22
22
  ],
23
23
  "main": "./src/index.ts",
24
- "types": "./src/index.ts",
24
+ "types": "./dist/types/index.d.ts",
25
25
  "scripts": {
26
26
  "check": "biome check . && bun run check:types",
27
27
  "check:types": "tsc -p tsconfig.json --noEmit",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@sayknow-cli/natives": "0.3.13",
34
+ "@sayknow-cli/natives": "0.3.16",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
@@ -44,15 +44,16 @@
44
44
  "bun": ">=1.3.14"
45
45
  },
46
46
  "files": [
47
- "src"
47
+ "src",
48
+ "dist/types"
48
49
  ],
49
50
  "exports": {
50
51
  ".": {
51
- "types": "./src/index.ts",
52
+ "types": "./dist/types/index.d.ts",
52
53
  "import": "./src/index.ts"
53
54
  },
54
55
  "./*": {
55
- "types": "./src/*.ts",
56
+ "types": "./dist/types/*.d.ts",
56
57
  "import": "./src/*.ts"
57
58
  },
58
59
  "./*.js": "./src/*.ts"
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Broken-pipe classification for writers that own a local output sink and for
3
+ * the process-wide postmortem fallback.
4
+ *
5
+ * A local writer may classify its own sink closure from the error code. The
6
+ * process-wide fallback is deliberately stricter: an `EPIPE` must be
7
+ * attributable to process stdout before it receives SIGPIPE-style shutdown.
8
+ */
9
+ import * as fs from "node:fs";
10
+
11
+ const KNOWN_SINK_PEER_CLOSED_CODES = new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
12
+
13
+ type ErrorProperty = "code" | "fd" | "syscall";
14
+
15
+ interface ErrorPropertyRead {
16
+ available: boolean;
17
+ value: unknown;
18
+ }
19
+
20
+ function isObjectLike(value: unknown): value is object {
21
+ return value !== null && (typeof value === "object" || typeof value === "function");
22
+ }
23
+
24
+ function readErrorProperty(error: object, property: ErrorProperty): ErrorPropertyRead {
25
+ try {
26
+ return { available: true, value: Reflect.get(error, property) };
27
+ } catch {
28
+ return { available: false, value: undefined };
29
+ }
30
+ }
31
+
32
+ function isUsableFileDescriptor(value: unknown): value is number {
33
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
34
+ }
35
+ interface FileIdentity {
36
+ dev: number | bigint;
37
+ ino: number | bigint;
38
+ }
39
+
40
+ function fileIdentity(fd: number): FileIdentity | undefined {
41
+ try {
42
+ const stat = fs.fstatSync(fd);
43
+ return { dev: stat.dev, ino: stat.ino };
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ function sameFileIdentity(left: FileIdentity, right: FileIdentity): boolean {
50
+ return left.dev === right.dev && left.ino === right.ino;
51
+ }
52
+
53
+ /**
54
+ * True when an error from a writer that owns its sink reports that the peer
55
+ * closed that sink. This intentionally accepts structural error objects rather
56
+ * than requiring an `Error` instance.
57
+ */
58
+ export function isKnownSinkPeerClosedError(error: unknown): boolean {
59
+ if (!isObjectLike(error)) return false;
60
+ const code = readErrorProperty(error, "code");
61
+ return code.available && typeof code.value === "string" && KNOWN_SINK_PEER_CLOSED_CODES.has(code.value);
62
+ }
63
+
64
+ /**
65
+ * Backward-compatible local-sink classifier for output writers. Callers must
66
+ * use it only when they own the sink that produced the error.
67
+ */
68
+ export function isBrokenPipeError(error: unknown): boolean {
69
+ return isKnownSinkPeerClosedError(error);
70
+ }
71
+
72
+ /**
73
+ * A classifier for process-level stdout `EPIPE` errors. Its direct-write
74
+ * evidence is private to each factory instance, so only the owner that
75
+ * intercepted `process.stdout.write` can mark an error for this classifier.
76
+ */
77
+ export interface ProcessStdoutEpipeClassifier {
78
+ markDirectProcessStdoutWriteError(error: unknown): void;
79
+ isAttributableProcessStdoutEpipe(error: unknown): boolean;
80
+ }
81
+
82
+ export function createProcessStdoutEpipeClassifier(): ProcessStdoutEpipeClassifier {
83
+ const directProcessStdoutWriteErrors = new WeakSet<object>();
84
+ const initialStdoutIdentity = isUsableFileDescriptor(process.stdout.fd)
85
+ ? fileIdentity(process.stdout.fd)
86
+ : undefined;
87
+
88
+ const hasCurrentStdoutIdentity = (fd: number): boolean => {
89
+ if (!initialStdoutIdentity) return false;
90
+ let stdoutFd: number;
91
+ try {
92
+ stdoutFd = process.stdout.fd;
93
+ } catch {
94
+ return false;
95
+ }
96
+ if (!isUsableFileDescriptor(stdoutFd)) return false;
97
+ const currentStdoutIdentity = fileIdentity(stdoutFd);
98
+ const errorFdIdentity = fileIdentity(fd);
99
+ if (!currentStdoutIdentity || !errorFdIdentity) return false;
100
+ if (!sameFileIdentity(currentStdoutIdentity, initialStdoutIdentity)) return false;
101
+ return fd === stdoutFd || sameFileIdentity(errorFdIdentity, currentStdoutIdentity);
102
+ };
103
+
104
+ return {
105
+ markDirectProcessStdoutWriteError(error: unknown): void {
106
+ if (isObjectLike(error)) directProcessStdoutWriteErrors.add(error);
107
+ },
108
+ isAttributableProcessStdoutEpipe(error: unknown): boolean {
109
+ if (!isObjectLike(error)) return false;
110
+
111
+ const code = readErrorProperty(error, "code");
112
+ if (!code.available || code.value !== "EPIPE") return false;
113
+
114
+ if (directProcessStdoutWriteErrors.has(error)) return true;
115
+
116
+ const syscall = readErrorProperty(error, "syscall");
117
+ if (!syscall.available || syscall.value !== "write") return false;
118
+
119
+ const fd = readErrorProperty(error, "fd");
120
+ if (!fd.available || !isUsableFileDescriptor(fd.value)) return false;
121
+
122
+ return hasCurrentStdoutIdentity(fd.value);
123
+ },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Exit code for a producer terminated because its output pipe broke:
129
+ * 128 + SIGPIPE (13), matching what shells report for SIGPIPE-killed tools
130
+ * in `foo | head`-style pipelines.
131
+ */
132
+ export const BROKEN_PIPE_EXIT_CODE = 141;
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createAbortableStream, once, untilAborted } from "./abortable";
2
2
  export * from "./async";
3
+ export * from "./broken-pipe";
3
4
  export * from "./color";
4
5
  export * from "./dirs";
5
6
  export * from "./env";
package/src/postmortem.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import inspector from "node:inspector";
9
9
  import { isMainThread } from "node:worker_threads";
10
+ import { BROKEN_PIPE_EXIT_CODE, createProcessStdoutEpipeClassifier } from "./broken-pipe";
10
11
  import * as logger from "./logger";
11
12
  import { safeStderrWrite } from "./safe-stderr";
12
13
 
@@ -22,10 +23,24 @@ export enum Reason {
22
23
  MANUAL = "manual", // Manual cleanup (not triggered by process)
23
24
  }
24
25
 
26
+ interface CleanupOptions {
27
+ quiet?: boolean;
28
+ }
29
+
30
+ type StdoutWriteCallback = (error?: Error | null) => void;
31
+
25
32
  // Internal list of active cleanup callbacks (in registration order)
26
33
  const callbackList: ((reason: Reason) => Promise<void> | void)[] = [];
27
34
  // Tracks cleanup run state (to prevent recursion/reentry issues)
28
35
  let cleanupStage: "idle" | "running" | "complete" = "idle";
36
+ let cleanupPromise: Promise<void> | undefined;
37
+ let quietShutdownStarted = false;
38
+ let ordinaryFatalStarted = false;
39
+ const stdoutEpipeClassifier = createProcessStdoutEpipeClassifier();
40
+
41
+ function shouldSuppressCleanupLogging(quiet: boolean): boolean {
42
+ return quiet || quietShutdownStarted;
43
+ }
29
44
 
30
45
  /**
31
46
  * Internal: runs all registered cleanup callbacks for the given reason.
@@ -33,36 +48,87 @@ let cleanupStage: "idle" | "running" | "complete" = "idle";
33
48
  *
34
49
  * Returns a Promise that settles after all cleanups complete or error out.
35
50
  */
36
- function runCleanup(reason: Reason): Promise<void> {
51
+ function runCleanup(reason: Reason, options: CleanupOptions = {}): Promise<void> {
52
+ const quiet = options.quiet === true;
37
53
  switch (cleanupStage) {
38
54
  case "idle":
39
55
  cleanupStage = "running";
40
56
  break;
41
57
  case "running":
42
- if (reason === Reason.EXIT) {
43
- return Promise.resolve();
58
+ if (reason !== Reason.EXIT && !shouldSuppressCleanupLogging(quiet)) {
59
+ logger.error("Cleanup invoked recursively", { stack: new Error().stack });
44
60
  }
45
- logger.error("Cleanup invoked recursively", { stack: new Error().stack });
46
61
  return Promise.resolve();
47
62
  case "complete":
48
63
  return Promise.resolve();
49
64
  }
50
65
 
66
+ const { promise, resolve } = Promise.withResolvers<void>();
67
+ cleanupPromise = promise;
68
+
51
69
  // Call .cleanup() for each callback that is still "armed".
52
- // Use Promise.try to handle sync/async, but only those armed.
70
+ // Assign the shared completion promise first so synchronous re-entry joins it.
53
71
  const promises = callbackList.toReversed().map(callback => {
54
72
  return Promise.try(() => callback(reason));
55
73
  });
56
74
 
57
- return Promise.allSettled(promises).then(results => {
58
- for (const result of results) {
59
- if (result.status === "rejected") {
60
- const err = result.reason instanceof Error ? result.reason : new Error(String(result.reason));
61
- logger.error("Cleanup callback failed", { err, stack: err.stack });
75
+ void Promise.allSettled(promises).then(results => {
76
+ try {
77
+ if (!shouldSuppressCleanupLogging(quiet)) {
78
+ for (const result of results) {
79
+ if (result.status === "rejected") {
80
+ const err = result.reason instanceof Error ? result.reason : new Error(String(result.reason));
81
+ logger.error("Cleanup callback failed", { err, stack: err.stack });
82
+ }
83
+ }
62
84
  }
85
+ } finally {
86
+ cleanupStage = "complete";
87
+ resolve();
63
88
  }
64
- cleanupStage = "complete";
65
89
  });
90
+ return promise;
91
+ }
92
+
93
+ async function runCleanupAndWait(reason: Reason, options: CleanupOptions = {}): Promise<void> {
94
+ void runCleanup(reason, options);
95
+ await (cleanupPromise ?? Promise.resolve());
96
+ }
97
+
98
+ function installProcessStdoutWriteClassifier(): void {
99
+ const originalWrite = process.stdout.write.bind(process.stdout);
100
+ const markCallback = (callback: StdoutWriteCallback): StdoutWriteCallback => {
101
+ return error => {
102
+ stdoutEpipeClassifier.markDirectProcessStdoutWriteError(error);
103
+ callback(error);
104
+ };
105
+ };
106
+
107
+ const markedWrite = (
108
+ chunk: string | Uint8Array,
109
+ encoding?: BufferEncoding | StdoutWriteCallback,
110
+ callback?: StdoutWriteCallback,
111
+ ): boolean => {
112
+ try {
113
+ if (typeof encoding === "function") return originalWrite(chunk, markCallback(encoding));
114
+ if (callback) {
115
+ return typeof chunk === "string"
116
+ ? originalWrite(chunk, encoding, markCallback(callback))
117
+ : originalWrite(chunk, markCallback(callback));
118
+ }
119
+ if (encoding === undefined) return originalWrite(chunk);
120
+ return typeof chunk === "string" ? originalWrite(chunk, encoding) : originalWrite(chunk);
121
+ } catch (error) {
122
+ stdoutEpipeClassifier.markDirectProcessStdoutWriteError(error);
123
+ throw error;
124
+ }
125
+ };
126
+
127
+ process.stdout.write = markedWrite as typeof process.stdout.write;
128
+ }
129
+
130
+ function errorForDiagnostic(reason: unknown): Error {
131
+ return reason instanceof Error ? reason : new Error(String(reason));
66
132
  }
67
133
 
68
134
  // Register signal and error event handlers to trigger cleanup before exit.
@@ -80,15 +146,10 @@ function formatFatalError(label: string, err: Error): string {
80
146
  }
81
147
 
82
148
  /**
83
- * True for terminal/pipe write failures (EIO, EPIPE, EBADF, …) on the write side.
84
- *
85
- * Bun flushes TTY writes asynchronously, so when the controlling terminal hangs up
86
- * (terminal closed, external volume stalled, SSH dropped) a restore write issued during
87
- * shutdown can surface as an uncaughtException on a later tick — after cleanup has already
88
- * begun. Treating that as fatal logs a scary stack and exits nonzero, which a hosting
89
- * terminal renders as a crash/restart loop. When it happens mid-shutdown it is a benign
90
- * artifact, not a real fault. We only match write-side syscalls so genuine bugs (a read
91
- * EIO, a logic error that happens to carry one of these codes) are never masked.
149
+ * True for terminal/pipe/disk write failures (EIO, EPIPE, EBADF, …) on the write side.
150
+ * Covers external-volume stalls and terminal hangups that surface as a late uncaught
151
+ * exception mid-shutdown — a benign artifact upstream's stdout-EPIPE classifier misses.
152
+ * Only write-side syscalls match so genuine read/logic bugs are never masked.
92
153
  */
93
154
  function isBenignIoWriteError(err: unknown): boolean {
94
155
  if (!err || typeof err !== "object") return false;
@@ -103,11 +164,50 @@ function isBenignIoWriteError(err: unknown): boolean {
103
164
  return isIoCode && (syscall === undefined || syscall === "write" || syscall === "writev");
104
165
  }
105
166
 
167
+ async function exitQuietlyForAttributableStdoutEpipe(reason: Reason): Promise<void> {
168
+ if (ordinaryFatalStarted || quietShutdownStarted) return;
169
+ quietShutdownStarted = true;
170
+ // Set the observable status before cleanup can await or trigger another error.
171
+ process.exitCode = BROKEN_PIPE_EXIT_CODE;
172
+ await runCleanupAndWait(reason, { quiet: true });
173
+ // An ordinary fatal that arrived during quiet cleanup takes precedence.
174
+ if (process.exitCode === BROKEN_PIPE_EXIT_CODE) process.exit(BROKEN_PIPE_EXIT_CODE);
175
+ }
176
+
177
+ async function handleFatalError(label: string, reason: unknown, cleanupReason: Reason): Promise<void> {
178
+ if (cleanupStage !== "idle" && isBenignIoWriteError(reason)) {
179
+ logger.debug("Ignoring benign terminal I/O write error during shutdown", {
180
+ code: (reason as { code?: unknown }).code,
181
+ syscall: (reason as { syscall?: unknown }).syscall,
182
+ });
183
+ return;
184
+ }
185
+ if (stdoutEpipeClassifier.isAttributableProcessStdoutEpipe(reason)) {
186
+ await exitQuietlyForAttributableStdoutEpipe(cleanupReason);
187
+ return;
188
+ }
189
+
190
+ // A distinct ordinary fatal must retain its normal diagnostic and status-1
191
+ // contract, including when it arrives while quiet cleanup is still pending.
192
+ ordinaryFatalStarted = true;
193
+ process.exitCode = 1;
194
+ const err = errorForDiagnostic(reason);
195
+ safeStderrWrite(formatFatalError(label, err));
196
+ if (!quietShutdownStarted) {
197
+ logger.error(label === "Uncaught Exception" ? "Uncaught exception" : "Unhandled rejection", {
198
+ err,
199
+ stack: err.stack,
200
+ });
201
+ }
202
+ await runCleanupAndWait(cleanupReason);
203
+ process.exit(1);
204
+ }
205
+
106
206
  if (isMainThread) {
207
+ installProcessStdoutWriteClassifier();
107
208
  process
108
209
  .on("SIGINT", async () => {
109
- logger.debug("Shutting down on SIGINT");
110
- await runCleanup(Reason.SIGINT);
210
+ await runCleanupAndWait(Reason.SIGINT);
111
211
  process.exit(130); // 128 + SIGINT (2)
112
212
  })
113
213
  .on("SIGUSR1", () => {
@@ -117,43 +217,21 @@ if (isMainThread) {
117
217
  const url = inspector.url();
118
218
  safeStderrWrite(`Inspector opened: ${url}\n`);
119
219
  })
120
- .on("uncaughtException", async err => {
121
- // A terminal/stdout write can fail asynchronously (EIO/EPIPE) while we are
122
- // already tearing down — e.g. the controlling terminal hung up, or the volume
123
- // the runtime lives on stalled. Bun surfaces that on a later tick, after cleanup
124
- // has begun. Treat it as a benign shutdown artifact: log at debug and let the
125
- // in-progress shutdown own the exit, instead of logging a fatal error and exiting
126
- // nonzero (which a hosting terminal renders as a crash/restart loop).
127
- if (cleanupStage !== "idle" && isBenignIoWriteError(err)) {
128
- logger.debug("Ignoring benign terminal I/O write error during shutdown", {
129
- code: (err as { code?: unknown }).code,
130
- syscall: (err as { syscall?: unknown }).syscall,
131
- });
132
- return;
133
- }
134
- safeStderrWrite(formatFatalError("Uncaught Exception", err));
135
- logger.error("Uncaught exception", { err, stack: err.stack });
136
- await runCleanup(Reason.UNCAUGHT_EXCEPTION);
137
- process.exit(1);
220
+ .on("uncaughtException", async error => {
221
+ await handleFatalError("Uncaught Exception", error, Reason.UNCAUGHT_EXCEPTION);
138
222
  })
139
223
  .on("unhandledRejection", async reason => {
140
- const err = reason instanceof Error ? reason : new Error(String(reason));
141
- safeStderrWrite(formatFatalError("Unhandled Rejection", err));
142
- logger.error("Unhandled rejection", { err, stack: err.stack });
143
- await runCleanup(Reason.UNHANDLED_REJECTION);
144
- process.exit(1);
224
+ await handleFatalError("Unhandled Rejection", reason, Reason.UNHANDLED_REJECTION);
145
225
  })
146
226
  .on("exit", async () => {
147
227
  void runCleanup(Reason.EXIT); // fire and forget (exit imminent)
148
228
  })
149
229
  .on("SIGTERM", async () => {
150
- logger.warn("Shutting down on SIGTERM (terminated by another process)");
151
- await runCleanup(Reason.SIGTERM);
230
+ await runCleanupAndWait(Reason.SIGTERM);
152
231
  process.exit(143); // 128 + SIGTERM (15)
153
232
  })
154
233
  .on("SIGHUP", async () => {
155
- logger.warn("Shutting down on SIGHUP (controlling terminal hung up / disconnected)");
156
- await runCleanup(Reason.SIGHUP);
234
+ await runCleanupAndWait(Reason.SIGHUP);
157
235
  process.exit(129); // 128 + SIGHUP (1)
158
236
  });
159
237
  } else {
@@ -179,8 +257,9 @@ export function register(id: string, callback: (reason: Reason) => void | Promis
179
257
  done = true;
180
258
  try {
181
259
  return callback(reason);
182
- } catch (e) {
183
- const err = e instanceof Error ? e : new Error(String(e));
260
+ } catch (error) {
261
+ if (quietShutdownStarted) return;
262
+ const err = error instanceof Error ? error : new Error(String(error));
184
263
  logger.error("Cleanup callback failed", { err, id, stack: err.stack });
185
264
  }
186
265
  };
@@ -194,12 +273,20 @@ export function register(id: string, callback: (reason: Reason) => void | Promis
194
273
  };
195
274
 
196
275
  if (cleanupStage !== "idle") {
276
+ if (quietShutdownStarted) {
277
+ queueMicrotask(() => {
278
+ void Promise.try(() => exec(Reason.MANUAL)).catch(() => {});
279
+ });
280
+ return () => {
281
+ done = true;
282
+ };
283
+ }
197
284
  // If cleanup is already running/completed, warn and run on microtask.
198
285
  logger.warn("Cleanup invoked recursively", { id });
199
286
  try {
200
287
  callback(Reason.MANUAL);
201
- } catch (e) {
202
- const err = e instanceof Error ? e : new Error(String(e));
288
+ } catch (error) {
289
+ const err = error instanceof Error ? error : new Error(String(error));
203
290
  logger.error("Cleanup callback failed", { err, id, stack: err.stack });
204
291
  }
205
292
  return () => {};
@@ -225,16 +312,28 @@ export function cleanup(): Promise<void> {
225
312
  * In workers: runs cleanup only (process.exit would kill entire process).
226
313
  */
227
314
  export async function quit(code: number = 0): Promise<void> {
228
- await runCleanup(Reason.MANUAL);
315
+ const cleanupWasRunning = cleanupStage === "running";
316
+ void runCleanup(Reason.MANUAL);
317
+ const completion = cleanupPromise ?? Promise.resolve();
229
318
 
230
319
  if (!isMainThread) {
231
- return; // Workers: cleanup done, let worker exit naturally
320
+ if (!cleanupWasRunning) await completion;
321
+ return;
232
322
  }
233
323
 
234
- if (process.stdout.writableLength > 0) {
235
- const { promise, resolve } = Promise.withResolvers<void>();
236
- process.stdout.once("drain", resolve);
237
- await Promise.race([promise, Bun.sleep(5000)]);
324
+ const exitAfterCleanup = async (): Promise<void> => {
325
+ await completion;
326
+ if (process.stdout.writableLength > 0) {
327
+ const { promise, resolve } = Promise.withResolvers<void>();
328
+ process.stdout.once("drain", resolve);
329
+ await Promise.race([promise, Bun.sleep(5000)]);
330
+ }
331
+ process.exit(code);
332
+ };
333
+
334
+ if (cleanupWasRunning) {
335
+ void exitAfterCleanup();
336
+ return;
238
337
  }
239
- process.exit(code);
338
+ await exitAfterCleanup();
240
339
  }