@oh-my-pi/pi-utils 18.3.5 → 18.4.0

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.
@@ -228,6 +228,11 @@ export declare function getGithubCacheDbPath(): string;
228
228
  * Honors `OMP_COMMIT_CACHE_DB` so tests and operators can isolate the cache.
229
229
  */
230
230
  export declare function getCommitCacheDbPath(): string;
231
+ /**
232
+ * Get the judgment answer cache database path (~/.omp/cache/judgment-cache.db).
233
+ * Honors `OMP_JUDGMENT_CACHE_DB` so tests and operators can isolate the cache.
234
+ */
235
+ export declare function getJudgmentCacheDbPath(): string;
231
236
  /** Get the legacy Pi extension parse cache database path. */
232
237
  export declare function getLegacyPiExtensionCacheDbPath(): string;
233
238
  /**
@@ -126,13 +126,17 @@ export declare function registerStdioDisconnectHandling(): () => void;
126
126
  * `--bytecode` builds. When a one-shot command awaits a promise that never
127
127
  * settles and holds no live handle, the loop drains and Bun exits 0 without
128
128
  * output: an unfinished command reported as success. `beforeExit` fires in
129
- * exactly that state; explicit exits (`process.exit`, {@link quit},
129
+ * that state; explicit exits (`process.exit`, {@link quit},
130
130
  * {@link exitProcess}, the signal handlers) never emit it.
131
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.
132
+ * `beforeExit` alone is not the verdict: on Windows Bun emits it while I/O is
133
+ * still in flight and then keeps running the loop, so a command that goes on
134
+ * to complete must keep its own exit code (#13470). The verdict is taken at
135
+ * `exit` instead, and only when the last `beforeExit` was final: an unref'd
136
+ * timer armed there runs only if the loop turns again, clearing the mark
137
+ * before a later explicit exit. Any later drain emits `beforeExit` again and
138
+ * re-arms it, so other `beforeExit` listeners (LSP shutdown) still run before
139
+ * the process exits 1.
136
140
  *
137
141
  * `describe` names what was running (e.g. the resolved subcommand) at report
138
142
  * time; it must return only a command name, never user arguments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.3.5",
3
+ "version": "18.4.0",
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.5"
60
+ "@oh-my-pi/pi-natives": "18.4.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"
package/src/dirs.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  import * as fs from "node:fs";
15
15
  import * as os from "node:os";
16
16
  import * as path from "node:path";
17
+ import { expandWindowsLongPath } from "@oh-my-pi/pi-natives/path";
17
18
  import { engines, version } from "../package.json" with { type: "json" };
18
19
  import { isEnoent, isEnotdir } from "./fs-error";
19
20
 
@@ -154,6 +155,11 @@ function standardizeMacOSPath(p: string): string {
154
155
  return p;
155
156
  }
156
157
 
158
+ /** Keep the current directory's spelling while expanding Windows 8.3 aliases. */
159
+ function standardizeProjectPath(p: string): string {
160
+ return process.platform === "win32" ? expandWindowsLongPath(p) : standardizeMacOSPath(p);
161
+ }
162
+
157
163
  export function resolveEquivalentPath(inputPath: string): string {
158
164
  const resolvedPath = path.resolve(inputPath);
159
165
  try {
@@ -200,29 +206,33 @@ let projectDir: string | undefined;
200
206
  /** Get the project directory. */
201
207
  export function getProjectDir(): string {
202
208
  if (projectDir === undefined) {
209
+ let cwd: string | undefined;
203
210
  try {
204
- projectDir = standardizeMacOSPath(process.cwd());
211
+ cwd = process.cwd();
205
212
  } catch {
206
213
  const candidates = [process.env.PWD, os.homedir(), os.tmpdir()];
207
214
  for (const candidate of candidates) {
208
215
  if (!candidate || !path.isAbsolute(candidate)) continue;
209
216
  try {
210
217
  process.chdir(candidate);
211
- projectDir = standardizeMacOSPath(candidate);
218
+ cwd = candidate;
212
219
  break;
213
220
  } catch {}
214
221
  }
215
- if (projectDir === undefined) {
222
+ if (cwd === undefined) {
216
223
  throw new Error("Unable to determine an accessible working directory");
217
224
  }
218
225
  }
226
+ // Normalize outside the fallback: a native-addon failure is not an inaccessible cwd,
227
+ // and must surface as itself instead of relocating the process.
228
+ projectDir = standardizeProjectPath(cwd);
219
229
  }
220
230
  return projectDir;
221
231
  }
222
232
 
223
233
  /** Set the project directory. */
224
234
  export function setProjectDir(dir: string): void {
225
- const resolved = standardizeMacOSPath(path.resolve(dir));
235
+ const resolved = standardizeProjectPath(path.resolve(dir));
226
236
  process.chdir(resolved);
227
237
  projectDir = resolved;
228
238
  }
@@ -787,6 +797,16 @@ export function getCommitCacheDbPath(): string {
787
797
  return dirs.rootSubdir(path.join("cache", "commit-inference.db"), "cache");
788
798
  }
789
799
 
800
+ /**
801
+ * Get the judgment answer cache database path (~/.omp/cache/judgment-cache.db).
802
+ * Honors `OMP_JUDGMENT_CACHE_DB` so tests and operators can isolate the cache.
803
+ */
804
+ export function getJudgmentCacheDbPath(): string {
805
+ const override = process.env.OMP_JUDGMENT_CACHE_DB;
806
+ if (override) return override;
807
+ return dirs.rootSubdir(path.join("cache", "judgment-cache.db"), "cache");
808
+ }
809
+
790
810
  /** Get the legacy Pi extension parse cache database path. */
791
811
  export function getLegacyPiExtensionCacheDbPath(): string {
792
812
  return dirs.rootSubdir(path.join("cache", "legacy-pi-extension-cache.db"), "cache");
package/src/postmortem.ts CHANGED
@@ -376,26 +376,38 @@ export function registerStdioDisconnectHandling(): () => void {
376
376
  * `--bytecode` builds. When a one-shot command awaits a promise that never
377
377
  * settles and holds no live handle, the loop drains and Bun exits 0 without
378
378
  * output: an unfinished command reported as success. `beforeExit` fires in
379
- * exactly that state; explicit exits (`process.exit`, {@link quit},
379
+ * that state; explicit exits (`process.exit`, {@link quit},
380
380
  * {@link exitProcess}, the signal handlers) never emit it.
381
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.
382
+ * `beforeExit` alone is not the verdict: on Windows Bun emits it while I/O is
383
+ * still in flight and then keeps running the loop, so a command that goes on
384
+ * to complete must keep its own exit code (#13470). The verdict is taken at
385
+ * `exit` instead, and only when the last `beforeExit` was final: an unref'd
386
+ * timer armed there runs only if the loop turns again, clearing the mark
387
+ * before a later explicit exit. Any later drain emits `beforeExit` again and
388
+ * re-arms it, so other `beforeExit` listeners (LSP shutdown) still run before
389
+ * the process exits 1.
386
390
  *
387
391
  * `describe` names what was running (e.g. the resolved subcommand) at report
388
392
  * time; it must return only a command name, never user arguments.
389
393
  */
390
394
  export function reportUnsettledEntry(work: Promise<unknown>, describe?: () => string | undefined): void {
391
395
  let pending = true;
396
+ let drained = false;
392
397
  const settled = (): void => {
393
398
  pending = false;
394
399
  };
395
400
  // Observe both outcomes without claiming the rejection: the caller's `.catch` still owns it.
396
401
  void work.then(settled, settled);
397
- process.once("beforeExit", () => {
402
+ process.on("beforeExit", () => {
398
403
  if (!pending) return;
404
+ drained = true;
405
+ setTimeout(() => {
406
+ drained = false;
407
+ }, 0).unref();
408
+ });
409
+ process.once("exit", () => {
410
+ if (!pending || !drained) return;
399
411
  const command = describe?.();
400
412
  const subject = command ? `\`${APP_NAME} ${command}\`` : "command";
401
413
  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)`;