@oh-my-pi/pi-utils 18.3.1 → 18.3.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.
@@ -119,6 +119,25 @@ export declare function registerWorkerIpcFaultHandler(handler: (err: Error) => v
119
119
  * when the last registrant unregisters.
120
120
  */
121
121
  export declare function registerStdioDisconnectHandling(): () => void;
122
+ /**
123
+ * Fail a process entry whose event loop drains while `work` is still pending.
124
+ *
125
+ * The CLI starts from a floating `runCli()` call because top-level await breaks
126
+ * `--bytecode` builds. When a one-shot command awaits a promise that never
127
+ * settles and holds no live handle, the loop drains and Bun exits 0 without
128
+ * output: an unfinished command reported as success. `beforeExit` fires in
129
+ * exactly that state; explicit exits (`process.exit`, {@link quit},
130
+ * {@link exitProcess}, the signal handlers) never emit it.
131
+ *
132
+ * The report runs once and only sets `process.exitCode`, so later `beforeExit`
133
+ * listeners (LSP shutdown) still run before the process exits 1. It starts no
134
+ * work of its own beyond the log write; `once` keeps the second `beforeExit`
135
+ * that any drained follow-up work triggers from reporting again.
136
+ *
137
+ * `describe` names what was running (e.g. the resolved subcommand) at report
138
+ * time; it must return only a command name, never user arguments.
139
+ */
140
+ export declare function reportUnsettledEntry(work: Promise<unknown>, describe?: () => string | undefined): void;
122
141
  /**
123
142
  * Mark an error as expected cleanup fallout so the global fatal handlers
124
143
  * downgrade it to a log line instead of tearing down the process. Use for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.3.1",
3
+ "version": "18.3.3",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -57,7 +57,7 @@
57
57
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
58
58
  },
59
59
  "dependencies": {
60
- "@oh-my-pi/pi-natives": "18.3.1"
60
+ "@oh-my-pi/pi-natives": "18.3.3"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"
package/src/postmortem.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import * as fs from "node:fs";
10
+ import { APP_NAME } from "./dirs";
10
11
  import * as logger from "./logger";
11
12
  import { restoreTerminalStderr } from "./stderr-guard";
12
13
 
@@ -368,6 +369,44 @@ export function registerStdioDisconnectHandling(): () => void {
368
369
  };
369
370
  }
370
371
 
372
+ /**
373
+ * Fail a process entry whose event loop drains while `work` is still pending.
374
+ *
375
+ * The CLI starts from a floating `runCli()` call because top-level await breaks
376
+ * `--bytecode` builds. When a one-shot command awaits a promise that never
377
+ * settles and holds no live handle, the loop drains and Bun exits 0 without
378
+ * output: an unfinished command reported as success. `beforeExit` fires in
379
+ * exactly that state; explicit exits (`process.exit`, {@link quit},
380
+ * {@link exitProcess}, the signal handlers) never emit it.
381
+ *
382
+ * The report runs once and only sets `process.exitCode`, so later `beforeExit`
383
+ * listeners (LSP shutdown) still run before the process exits 1. It starts no
384
+ * work of its own beyond the log write; `once` keeps the second `beforeExit`
385
+ * that any drained follow-up work triggers from reporting again.
386
+ *
387
+ * `describe` names what was running (e.g. the resolved subcommand) at report
388
+ * time; it must return only a command name, never user arguments.
389
+ */
390
+ export function reportUnsettledEntry(work: Promise<unknown>, describe?: () => string | undefined): void {
391
+ let pending = true;
392
+ const settled = (): void => {
393
+ pending = false;
394
+ };
395
+ // Observe both outcomes without claiming the rejection: the caller's `.catch` still owns it.
396
+ void work.then(settled, settled);
397
+ process.once("beforeExit", () => {
398
+ if (!pending) return;
399
+ const command = describe?.();
400
+ const subject = command ? `\`${APP_NAME} ${command}\`` : "command";
401
+ const message = `${subject} ended before completing: the event loop drained while it was still pending (rerun with PI_DEBUG_STARTUP=1 to see the last phase reached)`;
402
+ try {
403
+ fs.writeSync(2, `${APP_NAME}: ${message}\n`);
404
+ } catch {}
405
+ logger.error(message, { command });
406
+ process.exitCode = 1;
407
+ });
408
+ }
409
+
371
410
  // Well-known key marking an error as an *expected* teardown artifact (e.g. a
372
411
  // browser run-scope abort at normal run end). `Symbol.for` so the marker
373
412
  // survives duplicate module instances across bundles/realms.