@oh-my-pi/pi-utils 15.12.4 → 15.13.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.
package/CHANGELOG.md CHANGED
@@ -2,53 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [15.12.4] - 2026-06-13
6
-
7
- ### Fixed
8
-
9
- - Fixed abortable stream wrappers to cancel the source stream on abort, so timeout watchdogs release upstream HTTP bodies instead of only stopping the local reader.
10
-
11
- ## [15.12.0] - 2026-06-12
12
-
13
5
  ### Added
14
6
 
7
+ - Added support for a runtime `overrides` map in `RuntimeInstallSpec`, which is now written into generated runtime `package.json` manifests to force dependency pins (including transitive ones) across the runtime tree
8
+ - Added a lightweight loop-phase breadcrumb stack (`pushLoopPhase`/`popLoopPhase`/`currentLoopPhase`, plus `takeRecentLoopPhase` which returns the live phase or the most recently popped one and clears it) so the TUI event-loop watchdog can attribute a main-thread block to the phase that caused it — including a synchronous phase already popped before the watchdog's delayed tick runs ([#2485](https://github.com/can1357/oh-my-pi/issues/2485))
9
+ - Added `FetchWithRetryOptions.timeout` (forwarded to the underlying `fetch` call). `false` disables Bun's native ~300s pre-response timeout; a positive number overrides the ceiling. Bare browser/Node fetch ignores it ([#2422](https://github.com/can1357/oh-my-pi/issues/2422))
15
10
  - Added `runtime-install`: shared on-demand runtime dependency support — `ensureRuntimeInstalled()` (locked, idempotent `bun install` of a pinned dependency set into a cache dir) and a multi-root `installRuntimeModuleResolver()`/`resolveRuntimeModule()` for loading those graphs inside compiled binaries (Bun #1763). Extracted from the coding-agent tiny-model worker; now also backs Mnemopi's on-demand fastembed runtime ([#2389](https://github.com/can1357/oh-my-pi/issues/2389))
16
11
  - Added `getFastembedRuntimeDir()` (~/.omp/cache/fastembed-runtime) alongside `getFastembedCacheDir()`
17
-
18
- ## [15.11.4] - 2026-06-12
19
-
20
- ### Added
21
-
22
12
  - Added `getEditorConfigFormatting(file)`: returns the `.editorconfig`-pinned `tabSize`/`insertSpaces` (both optional, no fallback) so LSP-format callers can layer per-file defaults under it without paving over silence with the renderer's display tab width ([#2329](https://github.com/can1357/oh-my-pi/issues/2329)).
23
-
24
- ## [15.11.3] - 2026-06-11
25
-
26
- ### Added
27
-
28
- - Added `getEditorConfigFormatting(file)`: returns the `.editorconfig`-pinned `tabSize`/`insertSpaces` (both optional, no fallback) so LSP-format callers can layer per-file defaults under it without paving over silence with the renderer's display tab width ([#2329](https://github.com/can1357/oh-my-pi/issues/2329)).
29
-
30
- ## [15.11.1] - 2026-06-11
31
-
32
- ### Fixed
33
-
34
- - Fixed cleanup reentry noise during fatal shutdown: recursive cleanup requests now no-op idempotently instead of logging repeated `Cleanup invoked recursively` errors ([#2284](https://github.com/can1357/oh-my-pi/issues/2284)).
35
-
36
- ## [15.11.0] - 2026-06-10
37
-
38
- ### Added
39
-
40
13
  - Added the `path-tree` module (`buildPathTree`, `walkPathTree`, `formatGroupedPaths`, `isUrlLikePath`), moved from the coding agent's grouped file output so compaction file lists can share the same prefix-folded directory-tree rendering; `formatGroupedPaths` gains an optional `annotate` callback for per-file suffixes
41
-
42
- ### Fixed
43
-
44
- - Fixed the `{{join}}` prompt helper joining with a literal two-character `\n` when templates pass `"\n"` as the separator — Handlebars string literals carry no escape processing. The separator now unescapes `\n`/`\t`, matching the `{{#list}}` helper's documented convention (visible as literal `\n` between paths in compaction `<read-files>` lists).
45
-
46
- ## [15.10.11] - 2026-06-10
47
- ### Added
48
-
49
14
  - Restored `PI_DEBUG_STARTUP` streaming startup markers: `logger.time` now writes a synchronous `[startup] <op>:start` / `:done` / `:fail` stderr line per phase (independent of `PI_TIMING`), so a startup that hangs hard still names the phase it is stuck in — the `PI_TIMING` tree only prints after startup completes and is structurally unable to diagnose a hang. The CLI runner emits `cli:load:<name>` markers around each lazily-imported command module for the same reason.
50
15
  - Added `logger.openSpanPath()`: ops of the currently-open timing-span chain (root → deepest), used by the coding agent's startup watchdog to name the in-flight phase of a stalled startup.
51
16
  - Added `declareWorkerHostEntry()` / `workerHostEntry()` (env): self-dispatching CLI entrypoints declare `Bun.main` as the worker host so worker spawn sites can re-enter the single entry module with `WorkerOptions.argv` selectors across source, npm-bundle, and compiled distributions
17
+ - Added `getAuthBrokerSnapshotCachePath()` with `OMP_AUTH_BROKER_SNAPSHOT_CACHE` override support for isolating the encrypted broker snapshot cache.
18
+ - Added color helpers `colorLuma` (perceptual luma), `relativeLuminance` (WCAG, linearized sRGB), and `hslToHex` to the color utilities. The luminance helpers parse `#rgb`/`#rrggbb` hex and 256-color palette indices, returning `undefined` for unparseable values.
19
+ - Added `peekFileEnds`, a single-open head-and-tail file peek helper that reuses the head bytes for the tail when the file fits the head window.
20
+ - Added `peekFileTail`, the tail mirror of `peekFile`: reads up to the last `maxBytes` of a file ending at EOF, reusing the same pooled-buffer strategy (no per-call allocation for small reads).
21
+ - Added `getFastembedCacheDir` to return the FastEmbed model cache directory under ~/.omp/cache/fastembed
22
+ - Added an XDG-aware tiny-title model cache directory helper for coding-agent local title models.
52
23
 
53
24
  ### Changed
54
25
 
@@ -56,59 +27,52 @@
56
27
  - `Snowflake.formatParts` packs the id as a single 64-bit BigInt hex format instead of stitching four 16-bit segments (simpler and ~1.7x faster), and `getTimestamp` extracts via exact double arithmetic instead of a BigInt round-trip. Output is bit-identical.
57
28
  - Logger initialization is lazy: the winston logger, file transport, and log-directory creation now happen on first log emission instead of at module import (the import previously cost ~8ms of fs work on the CLI startup path); the in-memory timing infrastructure never touches winston
58
29
  - `prompt.format()` post-processing got cheap per-line guards and a single-pass ASCII-symbol replacement (was 7 chained regex passes per line), roughly halving render post-processing cost; output is byte-identical
30
+ - `logger.printTimings()` (the `PI_TIMING` startup tree) now surfaces two previously-invisible regions: a `(before instrumentation)` line for runtime init / uncaptured pre-marker work, and an `(unattributed self)` line for the root span's own untimed work so the gap between visible top-level spans and `Total` is no longer swallowed. `Total` is now labelled `(since first marker)` to make the window explicit. The restored `module-timer.ts` preload can feed module spans into the report: each module records `onLoad` → final top-level marker as `total`, a prepended body marker → final marker as `body/TLA`, and resolved static imports as a bounded dependency tree so the report separates graph wait from actual top-level module work.
59
31
 
60
32
  ### Fixed
61
33
 
34
+ - Made `TempDir` cleanup retry transient Windows `EBUSY`/`EPERM`/`ENOTEMPTY` removal failures so tests are less likely to fail when deleting just-used temp directories.
35
+ - Fixed abortable stream wrappers to cancel the source stream on abort, so timeout watchdogs release upstream HTTP bodies instead of only stopping the local reader.
36
+ - Fixed cleanup reentry noise during fatal shutdown: recursive cleanup requests now no-op idempotently instead of logging repeated `Cleanup invoked recursively` errors ([#2284](https://github.com/can1357/oh-my-pi/issues/2284)).
37
+ - Fixed the `{{join}}` prompt helper joining with a literal two-character `\n` when templates pass `"\n"` as the separator — Handlebars string literals carry no escape processing. The separator now unescapes `\n`/`\t`, matching the `{{#list}}` helper's documented convention (visible as literal `\n` between paths in compaction `<read-files>` lists).
62
38
  - Fixed `prompt.format()` so ASCII symbol replacements such as `-->` and `!=` still run on lines containing a closing HTML comment token when not inside a comment
63
39
  - `isCompiledBinary()` now also honors a define-folded `process.env.PI_COMPILED` (only `Bun.env` was checked), so builds that constant-fold `process.env` keep compiled-binary detection without relying on `import.meta.url` bunfs markers
64
40
  - `omp <cmd> --help` now loads only the requested command module instead of the entire command table, so an unrelated command whose import graph hangs or crashes can no longer take down every per-command help invocation.
41
+ - Hardened `getIndentation` against malformed paths: any filesystem error from the `.editorconfig` probe (e.g. `ENAMETOOLONG` on oversized garbage path segments) is now swallowed and cached as a miss instead of escaping and crashing the TUI mid-render ([#1871](https://github.com/can1357/oh-my-pi/issues/1871)).
42
+ - Fixed `getIndentation` (and the edit renderer's `replaceTabs` callers) crashing with `ENAMETOOLONG`/`ENOTDIR`/etc. when handed a path with an overlong component or a non-directory in its parent chain. Editorconfig discovery now short-circuits to the default tab width on any path component above `NAME_MAX` (255 bytes) and absorbs any `FsError` while walking the editorconfig chain — best-effort discovery must never escape as an uncaught exception ([#1872](https://github.com/can1357/oh-my-pi/issues/1872)).
43
+ - Fixed `$flag` environment parsing to accept lowercase truthy values such as `y`, `true`, `yes`, and `on`
65
44
 
66
- ## [15.10.8] - 2026-06-09
67
45
  ### Removed
68
46
 
69
47
  - Removed the exported `hookFetch` API, which previously intercepted `globalThis.fetch` via middleware handlers
70
48
  - Removed `hookFetch` from the package entrypoint, so imports from `@.../utils` no longer provide this fetch interception helper
71
49
 
72
- ## [15.10.0] - 2026-06-06
50
+ ## [15.13.0] - 2026-06-14
73
51
 
74
- ### Changed
52
+ ## [15.12.4] - 2026-06-13
75
53
 
76
- - `logger.printTimings()` (the `PI_TIMING` startup tree) now surfaces two previously-invisible regions: a `(before instrumentation)` line for runtime init / uncaptured pre-marker work, and an `(unattributed self)` line for the root span's own untimed work so the gap between visible top-level spans and `Total` is no longer swallowed. `Total` is now labelled `(since first marker)` to make the window explicit. The restored `module-timer.ts` preload can feed module spans into the report: each module records `onLoad` → final top-level marker as `total`, a prepended body marker → final marker as `body/TLA`, and resolved static imports as a bounded dependency tree so the report separates graph wait from actual top-level module work.
54
+ ## [15.12.0] - 2026-06-12
77
55
 
78
- ## [15.9.2] - 2026-06-05
56
+ ## [15.11.4] - 2026-06-12
79
57
 
80
- ### Added
58
+ ## [15.11.3] - 2026-06-11
81
59
 
82
- - Added `getAuthBrokerSnapshotCachePath()` with `OMP_AUTH_BROKER_SNAPSHOT_CACHE` override support for isolating the encrypted broker snapshot cache.
60
+ ## [15.11.1] - 2026-06-11
83
61
 
84
- ## [15.9.1] - 2026-06-04
62
+ ## [15.11.0] - 2026-06-10
85
63
 
86
- ### Fixed
64
+ ## [15.10.11] - 2026-06-10
87
65
 
88
- - Hardened `getIndentation` against malformed paths: any filesystem error from the `.editorconfig` probe (e.g. `ENAMETOOLONG` on oversized garbage path segments) is now swallowed and cached as a miss instead of escaping and crashing the TUI mid-render ([#1871](https://github.com/can1357/oh-my-pi/issues/1871)).
89
- - Fixed `getIndentation` (and the edit renderer's `replaceTabs` callers) crashing with `ENAMETOOLONG`/`ENOTDIR`/etc. when handed a path with an overlong component or a non-directory in its parent chain. Editorconfig discovery now short-circuits to the default tab width on any path component above `NAME_MAX` (255 bytes) and absorbs any `FsError` while walking the editorconfig chain — best-effort discovery must never escape as an uncaught exception ([#1872](https://github.com/can1357/oh-my-pi/issues/1872)).
66
+ ## [15.10.8] - 2026-06-09
90
67
 
91
- ## [15.9.0] - 2026-06-04
68
+ ## [15.10.0] - 2026-06-06
92
69
 
93
- ### Added
70
+ ## [15.9.2] - 2026-06-05
94
71
 
95
- - Added color helpers `colorLuma` (perceptual luma), `relativeLuminance` (WCAG, linearized sRGB), and `hslToHex` to the color utilities. The luminance helpers parse `#rgb`/`#rrggbb` hex and 256-color palette indices, returning `undefined` for unparseable values.
96
- - Added `peekFileEnds`, a single-open head-and-tail file peek helper that reuses the head bytes for the tail when the file fits the head window.
72
+ ## [15.9.1] - 2026-06-04
97
73
 
98
- - Added `peekFileTail`, the tail mirror of `peekFile`: reads up to the last `maxBytes` of a file ending at EOF, reusing the same pooled-buffer strategy (no per-call allocation for small reads).
74
+ ## [15.9.0] - 2026-06-04
99
75
 
100
76
  ## [15.7.3] - 2026-05-31
101
77
 
102
- ### Added
103
-
104
- - Added `getFastembedCacheDir` to return the FastEmbed model cache directory under ~/.omp/cache/fastembed
105
-
106
- ### Fixed
107
-
108
- - Fixed `$flag` environment parsing to accept lowercase truthy values such as `y`, `true`, `yes`, and `on`
109
-
110
78
  ## [15.6.0] - 2026-05-30
111
-
112
- ### Added
113
-
114
- - Added an XDG-aware tiny-title model cache directory helper for coding-agent local title models.
@@ -43,6 +43,14 @@ export interface FetchWithRetryOptions extends RequestInit {
43
43
  * mock during tests.
44
44
  */
45
45
  fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
46
+ /**
47
+ * Bun extension forwarded verbatim to the underlying `fetch` call. `false`
48
+ * disables Bun's native ~300s pre-response timeout (callers that own a
49
+ * configurable first-event/idle watchdog or an external `AbortSignal`
50
+ * supply this so the runtime ceiling cannot pre-empt them); a positive
51
+ * number sets a custom ceiling in ms. Bare browser/Node fetch ignores it.
52
+ */
53
+ timeout?: number | false;
46
54
  }
47
55
  /**
48
56
  * Fetch with bounded retries and sensible defaults. Retries on any
@@ -10,6 +10,7 @@ export * from "./fs-error";
10
10
  export * from "./glob";
11
11
  export * from "./json";
12
12
  export * as logger from "./logger";
13
+ export * from "./loop-phase";
13
14
  export * from "./mermaid-ascii";
14
15
  export * from "./mime";
15
16
  export * from "./path-tree";
@@ -0,0 +1,10 @@
1
+ export declare function pushLoopPhase(label: string): void;
2
+ export declare function popLoopPhase(): void;
3
+ export declare function currentLoopPhase(): string | undefined;
4
+ /**
5
+ * Phase to blame for a just-detected loop block: the live top phase if one is
6
+ * still held, else the most recent phase pushed since the last call. Clears the
7
+ * recent slot so a block in a later, phase-less interval is not misattributed
8
+ * to a phase that already finished.
9
+ */
10
+ export declare function takeRecentLoopPhase(): string | undefined;
@@ -43,6 +43,8 @@ export declare function installRuntimeModuleResolver({ runtimeNodeModules, stubs
43
43
  /** Pinned dependency set materialized into a runtime cache directory. */
44
44
  export interface RuntimeInstallSpec {
45
45
  dependencies: Record<string, string>;
46
+ /** Version pins forced across the whole runtime tree (bun `overrides`), e.g. dislodging a transitive dep. */
47
+ overrides?: Record<string, string>;
46
48
  /** Packages whose lifecycle scripts bun may run during the install. */
47
49
  trustedDependencies?: string[];
48
50
  }
@@ -58,6 +60,7 @@ export interface EnsureRuntimeInstalledOptions {
58
60
  lockAttempts?: number;
59
61
  lockSleepMs?: number;
60
62
  }
63
+ export declare function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void>;
61
64
  /**
62
65
  * Materialize a pinned dependency set into `runtimeDir` (idempotent,
63
66
  * cross-process safe via a lock directory). Returns `runtimeDir`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "15.12.4",
4
+ "version": "15.13.0",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "15.12.4",
34
+ "@oh-my-pi/pi-natives": "15.13.0",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
@@ -129,6 +129,14 @@ export interface FetchWithRetryOptions extends RequestInit {
129
129
  * mock during tests.
130
130
  */
131
131
  fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
132
+ /**
133
+ * Bun extension forwarded verbatim to the underlying `fetch` call. `false`
134
+ * disables Bun's native ~300s pre-response timeout (callers that own a
135
+ * configurable first-event/idle watchdog or an external `AbortSignal`
136
+ * supply this so the runtime ceiling cannot pre-empt them); a positive
137
+ * number sets a custom ceiling in ms. Bare browser/Node fetch ignores it.
138
+ */
139
+ timeout?: number | false;
132
140
  }
133
141
 
134
142
  const DEFAULT_MAX_DELAY_MS = 60_000;
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./fs-error";
10
10
  export * from "./glob";
11
11
  export * from "./json";
12
12
  export * as logger from "./logger";
13
+ export * from "./loop-phase";
13
14
  export * from "./mermaid-ascii";
14
15
  export * from "./mime";
15
16
  export * from "./path-tree";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Live event-loop phase breadcrumb. Hot synchronous paths push a short label
3
+ * before running and pop it after (via `try`/`finally`); the loop watchdog
4
+ * reads {@link takeRecentLoopPhase} when it detects a block, so a stall is
5
+ * logged with the work that caused it instead of an opaque "unknown".
6
+ *
7
+ * This is deliberately a process-global stack and not part of the logger span
8
+ * machinery: `main.ts` ends timing spans before the interactive TUI starts, so
9
+ * `logger.openSpanPath()` is empty in a live session.
10
+ *
11
+ * Correctness constraint: each `pushLoopPhase` must be balanced by a
12
+ * `popLoopPhase` within the SAME synchronous execution (always via `try`/
13
+ * `finally`). The stack is global and shared, so a label held across an
14
+ * `await`/async boundary — or interleaved between concurrent tasks — would
15
+ * misattribute or leak phases. Instrument only synchronous spans; for async
16
+ * work, push/pop around each synchronous chunk, not across the await.
17
+ */
18
+ const stack: string[] = [];
19
+ // The most recent label pushed, retained after it is popped. A hot path pushes
20
+ // and pops a phase entirely within one synchronous macrotask, so by the time
21
+ // the watchdog's delayed tick runs the stack is already empty; this slot keeps
22
+ // the culprit available for that one tick. Consumed (cleared) on read so it
23
+ // only attributes the just-elapsed interval.
24
+ let recentPhase: string | undefined;
25
+
26
+ export function pushLoopPhase(label: string): void {
27
+ stack.push(label);
28
+ recentPhase = label;
29
+ }
30
+
31
+ export function popLoopPhase(): void {
32
+ stack.pop();
33
+ }
34
+
35
+ export function currentLoopPhase(): string | undefined {
36
+ return stack[stack.length - 1];
37
+ }
38
+
39
+ /**
40
+ * Phase to blame for a just-detected loop block: the live top phase if one is
41
+ * still held, else the most recent phase pushed since the last call. Clears the
42
+ * recent slot so a block in a later, phase-less interval is not misattributed
43
+ * to a phase that already finished.
44
+ */
45
+ export function takeRecentLoopPhase(): string | undefined {
46
+ const phase = stack[stack.length - 1] ?? recentPhase;
47
+ recentPhase = undefined;
48
+ return phase;
49
+ }
@@ -242,6 +242,8 @@ export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }:
242
242
  /** Pinned dependency set materialized into a runtime cache directory. */
243
243
  export interface RuntimeInstallSpec {
244
244
  dependencies: Record<string, string>;
245
+ /** Version pins forced across the whole runtime tree (bun `overrides`), e.g. dislodging a transitive dep. */
246
+ overrides?: Record<string, string>;
245
247
  /** Packages whose lifecycle scripts bun may run during the install. */
246
248
  trustedDependencies?: string[];
247
249
  }
@@ -281,13 +283,14 @@ async function acquireInstallLock(runtimeDir: string, attempts: number, sleepMs:
281
283
  throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
282
284
  }
283
285
 
284
- async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
286
+ export async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
285
287
  await fsp.mkdir(runtimeDir, { recursive: true });
286
288
  const manifest: Record<string, unknown> = {
287
289
  private: true,
288
290
  type: "module",
289
291
  dependencies: install.dependencies,
290
292
  };
293
+ if (install.overrides && Object.keys(install.overrides).length) manifest.overrides = install.overrides;
291
294
  if (install.trustedDependencies?.length) manifest.trustedDependencies = install.trustedDependencies;
292
295
  await Bun.write(path.join(runtimeDir, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
293
296
  }
package/src/temp.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import * as fsPromises from "node:fs/promises";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
4
5
 
@@ -13,7 +14,7 @@ export class TempDir {
13
14
  }
14
15
 
15
16
  static async create(prefix?: string): Promise<TempDir> {
16
- return new TempDir(await fs.promises.mkdtemp(normalizePrefix(prefix)));
17
+ return new TempDir(await fsPromises.mkdtemp(normalizePrefix(prefix)));
17
18
  }
18
19
 
19
20
  #removePromise: Promise<void> | null = null;
@@ -30,13 +31,13 @@ export class TempDir {
30
31
  if (this.#removePromise) {
31
32
  return this.#removePromise;
32
33
  }
33
- const removePromise = fs.promises.rm(this.#path, { recursive: true, force: true });
34
+ const removePromise = removeWithRetries(this.#path);
34
35
  this.#removePromise = removePromise;
35
36
  return removePromise;
36
37
  }
37
38
 
38
39
  removeSync(): void {
39
- fs.rmSync(this.#path, { recursive: true, force: true });
40
+ removeSyncWithRetries(this.#path);
40
41
  this.#removePromise = Promise.resolve();
41
42
  }
42
43
 
@@ -75,3 +76,55 @@ function normalizePrefix(prefix?: string): string {
75
76
  }
76
77
  return prefix;
77
78
  }
79
+
80
+ const kRemoveOptions = { recursive: true, force: true } as const;
81
+ const kRemoveRetries = 4;
82
+ const kRemoveRetryDelayMs = 10;
83
+ const kRetryableRemoveErrorCodes = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
84
+ const kSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
85
+
86
+ async function removeWithRetries(target: string): Promise<void> {
87
+ for (let attempt = 0; ; attempt++) {
88
+ try {
89
+ await fsPromises.rm(target, kRemoveOptions);
90
+ return;
91
+ } catch (err) {
92
+ if (!shouldRetryRemove(err, attempt)) throw err;
93
+ await Bun.sleep(kRemoveRetryDelayMs);
94
+ }
95
+ }
96
+ }
97
+
98
+ function removeSyncWithRetries(target: string): void {
99
+ for (let attempt = 0; ; attempt++) {
100
+ try {
101
+ fs.rmSync(target, kRemoveOptions);
102
+ return;
103
+ } catch (err) {
104
+ if (!shouldRetryRemove(err, attempt)) throw err;
105
+ sleepSync(kRemoveRetryDelayMs);
106
+ }
107
+ }
108
+ }
109
+
110
+ function shouldRetryRemove(err: unknown, attempt: number): boolean {
111
+ return attempt < kRemoveRetries && process.platform === "win32" && isRetryableRemoveError(err);
112
+ }
113
+
114
+ function isRetryableRemoveError(err: unknown): boolean {
115
+ return (
116
+ typeof err === "object" &&
117
+ err !== null &&
118
+ "code" in err &&
119
+ typeof err.code === "string" &&
120
+ kRetryableRemoveErrorCodes.has(err.code)
121
+ );
122
+ }
123
+
124
+ function sleepSync(ms: number): void {
125
+ if ("sleepSync" in Bun && typeof Bun.sleepSync === "function") {
126
+ Bun.sleepSync(ms);
127
+ return;
128
+ }
129
+ Atomics.wait(kSleepBuffer, 0, 0, ms);
130
+ }