@oh-my-pi/pi-utils 15.11.7 → 15.12.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,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.12.0] - 2026-06-12
6
+
7
+ ### Added
8
+
9
+ - 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))
10
+ - Added `getFastembedRuntimeDir()` (~/.omp/cache/fastembed-runtime) alongside `getFastembedCacheDir()`
11
+
5
12
  ## [15.11.4] - 2026-06-12
6
13
 
7
14
  ### Added
@@ -103,6 +103,8 @@ export declare function getGithubCacheDbPath(): string;
103
103
  export declare function getAuthBrokerSnapshotCachePath(): string;
104
104
  /** Get the local FastEmbed model cache directory (~/.omp/cache/fastembed). */
105
105
  export declare function getFastembedCacheDir(): string;
106
+ /** Get the on-demand fastembed runtime install root (~/.omp/cache/fastembed-runtime). */
107
+ export declare function getFastembedRuntimeDir(): string;
106
108
  /** Get the natives directory (~/.omp/natives). */
107
109
  export declare function getNativesDir(): string;
108
110
  /** Get the stats database path (~/.omp/stats.db). */
@@ -14,6 +14,7 @@ export declare function isValidEnvName(name: string): boolean;
14
14
  */
15
15
  export declare function isSafeEnvName(name: string): boolean;
16
16
  export declare function isSafeEnvValue(value: string): boolean;
17
+ export declare function isMacosMallocStackLoggingEnvName(name: string): boolean;
17
18
  export declare function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string>;
18
19
  /**
19
20
  * Parses a .env file synchronously and extracts key-value string pairs.
@@ -19,6 +19,7 @@ export * as procmgr from "./procmgr";
19
19
  export * as prompt from "./prompt";
20
20
  export * as ptree from "./ptree";
21
21
  export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
22
+ export * from "./runtime-install";
22
23
  export * from "./sanitize-text";
23
24
  export * from "./snowflake";
24
25
  export * from "./stream";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Walk a conditional `exports` target (string, array of fallbacks, or a
3
+ * condition object) and return the first relative path that matches a runtime
4
+ * condition in declaration order. Returns `null` when nothing applies (e.g.
5
+ * an `import`-only entry).
6
+ */
7
+ export declare function selectConditionalTarget(target: unknown): string | null;
8
+ /**
9
+ * Split a bare specifier into its package name and optional subpath, handling
10
+ * scoped packages (`@scope/name/sub` → `@scope/name` + `sub`).
11
+ */
12
+ export declare function splitBareSpecifier(specifier: string): {
13
+ packageName: string;
14
+ subpath: string | undefined;
15
+ };
16
+ /**
17
+ * Resolve a bare specifier against an installed `node_modules` directory,
18
+ * honoring `exports` (CommonJS conditions), then `main`, then `index.js`.
19
+ * Returns an absolute file path, or `null` when the package/entry is absent.
20
+ */
21
+ export declare function resolveRuntimeModule(runtimeNodeModules: string, specifier: string): string | null;
22
+ export interface RuntimeResolverOptions {
23
+ /** Absolute path to the runtime cache's `node_modules`. */
24
+ runtimeNodeModules: string;
25
+ /** Bare specifier → absolute file path overrides (e.g. `sharp` → no-op stub). */
26
+ stubs?: Record<string, string>;
27
+ }
28
+ /**
29
+ * Patch `node:module`'s resolver (idempotently) so bare specifiers that the
30
+ * stock compiled-binary resolver cannot find fall back to the registered
31
+ * runtime caches. Stock resolution is tried first and kept for anything
32
+ * outside the registered roots (bundled imports, node builtins, host or
33
+ * extension trees). Multiple runtime roots may register; they are consulted
34
+ * in registration order.
35
+ *
36
+ * One stock "success" is distrusted: the compiled-binary resolver ignores
37
+ * `main`/`exports` for real-FS packages (Bun #1763), so a package shipping
38
+ * its TS source next to `dist/` (e.g. `@huggingface/hub`'s root `index.ts`)
39
+ * resolves to the wrong file. When the stock hit lands inside a registered
40
+ * runtime root, the manifest-aware resolution wins.
41
+ */
42
+ export declare function installRuntimeModuleResolver({ runtimeNodeModules, stubs }: RuntimeResolverOptions): void;
43
+ /** Pinned dependency set materialized into a runtime cache directory. */
44
+ export interface RuntimeInstallSpec {
45
+ dependencies: Record<string, string>;
46
+ /** Packages whose lifecycle scripts bun may run during the install. */
47
+ trustedDependencies?: string[];
48
+ }
49
+ export type RuntimeInstallPhase = "initiate" | "download" | "done";
50
+ export interface EnsureRuntimeInstalledOptions {
51
+ /** Directory owning the runtime `package.json` + `node_modules`. */
52
+ runtimeDir: string;
53
+ install: RuntimeInstallSpec;
54
+ /** Package whose installed manifest marks the runtime complete; defaults to the first dependency. */
55
+ probePackage?: string;
56
+ /** Phase notifications (progress UI); not emitted when already installed. */
57
+ onPhase?: (phase: RuntimeInstallPhase) => void;
58
+ lockAttempts?: number;
59
+ lockSleepMs?: number;
60
+ }
61
+ /**
62
+ * Materialize a pinned dependency set into `runtimeDir` (idempotent,
63
+ * cross-process safe via a lock directory). Returns `runtimeDir`.
64
+ */
65
+ export declare function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string>;
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.11.7",
4
+ "version": "15.12.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.11.7",
34
+ "@oh-my-pi/pi-natives": "15.12.0",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
package/src/dirs.ts CHANGED
@@ -359,6 +359,11 @@ export function getFastembedCacheDir(): string {
359
359
  return dirs.rootSubdir(path.join("cache", "fastembed"), "cache");
360
360
  }
361
361
 
362
+ /** Get the on-demand fastembed runtime install root (~/.omp/cache/fastembed-runtime). */
363
+ export function getFastembedRuntimeDir(): string {
364
+ return dirs.rootSubdir(path.join("cache", "fastembed-runtime"), "cache");
365
+ }
366
+
362
367
  /** Get the natives directory (~/.omp/natives). */
363
368
  export function getNativesDir(): string {
364
369
  return dirs.rootSubdir("natives", "cache");
package/src/env.ts CHANGED
@@ -30,11 +30,22 @@ export function isSafeEnvValue(value: string): boolean {
30
30
  return !value.includes("\0");
31
31
  }
32
32
 
33
+ export function isMacosMallocStackLoggingEnvName(name: string): boolean {
34
+ return name === "MallocStackLogging" || name === "MallocStackLoggingNoCompact";
35
+ }
36
+
33
37
  export function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string> {
34
38
  const result: Record<string, string> = {};
35
39
  for (const key in env) {
36
40
  const value = env[key];
37
- if (!isSafeEnvName(key) || value === undefined || !isSafeEnvValue(value)) continue;
41
+ if (
42
+ !isSafeEnvName(key) ||
43
+ isMacosMallocStackLoggingEnvName(key) ||
44
+ value === undefined ||
45
+ !isSafeEnvValue(value)
46
+ ) {
47
+ continue;
48
+ }
38
49
  result[key] = value;
39
50
  }
40
51
  return result;
@@ -93,14 +104,14 @@ const projectEnv = parseEnvFile(path.join(process.cwd(), ".env"));
93
104
 
94
105
  for (const key of Object.keys(Bun.env)) {
95
106
  const value = Bun.env[key];
96
- if (!isSafeEnvName(key) || value === undefined || !isSafeEnvValue(value)) {
107
+ if (!isSafeEnvName(key) || isMacosMallocStackLoggingEnvName(key) || value === undefined || !isSafeEnvValue(value)) {
97
108
  delete Bun.env[key];
98
109
  }
99
110
  }
100
111
 
101
112
  for (const file of [projectEnv, agentEnv, piEnv, homeEnv]) {
102
113
  for (const key in file) {
103
- if (!Bun.env[key]) {
114
+ if (!isMacosMallocStackLoggingEnvName(key) && !Bun.env[key]) {
104
115
  Bun.env[key] = file[key];
105
116
  }
106
117
  }
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export * as procmgr from "./procmgr";
19
19
  export * as prompt from "./prompt";
20
20
  export * as ptree from "./ptree";
21
21
  export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
22
+ export * from "./runtime-install";
22
23
  export * from "./sanitize-text";
23
24
  export * from "./snowflake";
24
25
  export * from "./stream";
@@ -0,0 +1,349 @@
1
+ import * as fs from "node:fs";
2
+ import * as fsp from "node:fs/promises";
3
+ import * as Module from "node:module";
4
+ import * as path from "node:path";
5
+
6
+ /**
7
+ * On-demand runtime dependency support for native-heavy optional packages
8
+ * (Transformers.js, fastembed) that are never bundled into the CLI or the
9
+ * compiled binary. Consumers `bun install` a pinned dependency set into a
10
+ * cache directory on first use ({@link ensureRuntimeInstalled}) and load the
11
+ * entrypoint via `createRequire`.
12
+ *
13
+ * Bun's compiled-binary module resolver only finds `<pkg>/index.js` for bare
14
+ * specifiers loaded from the *real* filesystem — it ignores `main`/`exports`
15
+ * (issue #1763). Runtime-installed graphs (`@huggingface/transformers` →
16
+ * `onnxruntime-node` → `onnxruntime-common`, `fastembed` →
17
+ * `@anush008/tokenizers` → platform binding) all point `main`/`exports` at
18
+ * nested files, so the stock resolver cannot load any of them. We patch
19
+ * `Module._resolveFilename` to resolve those bare specifiers against the
20
+ * registered runtime caches ourselves, honoring `main`/`exports`.
21
+ *
22
+ * This module is filesystem-pure aside from {@link installRuntimeModuleResolver}
23
+ * mutating the `node:module` resolver, so the resolution logic is unit-testable
24
+ * without a compiled binary.
25
+ */
26
+
27
+ /** Conditions honored when resolving an `exports` map for a CommonJS `require`. */
28
+ const RUNTIME_CONDITIONS: Record<string, true> = { node: true, require: true, default: true };
29
+
30
+ /** Extension probes appended to a `main`/`exports` target that lacks one. */
31
+ const RUNTIME_EXTENSIONS: readonly string[] = [".js", ".cjs", ".mjs", ".json", ".node"];
32
+
33
+ function isRecord(value: unknown): value is Record<string, unknown> {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+
37
+ /**
38
+ * Walk a conditional `exports` target (string, array of fallbacks, or a
39
+ * condition object) and return the first relative path that matches a runtime
40
+ * condition in declaration order. Returns `null` when nothing applies (e.g.
41
+ * an `import`-only entry).
42
+ */
43
+ export function selectConditionalTarget(target: unknown): string | null {
44
+ if (typeof target === "string") return target;
45
+ if (Array.isArray(target)) {
46
+ for (const entry of target) {
47
+ const resolved = selectConditionalTarget(entry);
48
+ if (resolved) return resolved;
49
+ }
50
+ return null;
51
+ }
52
+ if (isRecord(target)) {
53
+ for (const condition in target) {
54
+ if (!RUNTIME_CONDITIONS[condition]) continue;
55
+ const resolved = selectConditionalTarget(target[condition]);
56
+ if (resolved) return resolved;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /** Resolve a relative target inside a package to a concrete file path, probing extensions and `index`. */
63
+ function resolveFileTarget(pkgDir: string, relative: string): string | null {
64
+ const base = path.join(pkgDir, relative);
65
+ const candidates = [base, ...RUNTIME_EXTENSIONS.map(ext => base + ext)];
66
+ for (const candidate of candidates) {
67
+ try {
68
+ const stat = fs.statSync(candidate);
69
+ if (stat.isFile()) return candidate;
70
+ if (stat.isDirectory()) {
71
+ const indexed = resolveFileTarget(candidate, "index");
72
+ if (indexed) return indexed;
73
+ }
74
+ } catch {
75
+ // missing candidate — keep probing
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function resolveExportsEntry(
82
+ pkgDir: string,
83
+ exports: Record<string, unknown>,
84
+ subpath: string | undefined,
85
+ ): string | null {
86
+ let subpathMap = false;
87
+ for (const key in exports) {
88
+ subpathMap = key === "." || key.startsWith("./");
89
+ break;
90
+ }
91
+ if (subpathMap) {
92
+ const key = subpath ? `./${subpath}` : ".";
93
+ if (!(key in exports)) return null;
94
+ const target = selectConditionalTarget(exports[key]);
95
+ return target ? resolveFileTarget(pkgDir, target) : null;
96
+ }
97
+ // A bare condition map only describes the package root, so a subpath
98
+ // request falls through to plain path joining at the call site.
99
+ if (subpath) return null;
100
+ const target = selectConditionalTarget(exports);
101
+ return target ? resolveFileTarget(pkgDir, target) : null;
102
+ }
103
+
104
+ /**
105
+ * Split a bare specifier into its package name and optional subpath, handling
106
+ * scoped packages (`@scope/name/sub` → `@scope/name` + `sub`).
107
+ */
108
+ export function splitBareSpecifier(specifier: string): { packageName: string; subpath: string | undefined } {
109
+ const segments = specifier.split("/");
110
+ const take = specifier.startsWith("@") ? 2 : 1;
111
+ const packageName = segments.slice(0, take).join("/");
112
+ const subpath = segments.length > take ? segments.slice(take).join("/") : undefined;
113
+ return { packageName, subpath };
114
+ }
115
+
116
+ /**
117
+ * Resolve a bare specifier against an installed `node_modules` directory,
118
+ * honoring `exports` (CommonJS conditions), then `main`, then `index.js`.
119
+ * Returns an absolute file path, or `null` when the package/entry is absent.
120
+ */
121
+ export function resolveRuntimeModule(runtimeNodeModules: string, specifier: string): string | null {
122
+ const { packageName, subpath } = splitBareSpecifier(specifier);
123
+ const pkgDir = path.join(runtimeNodeModules, ...packageName.split("/"));
124
+ const manifest = readManifest(pkgDir);
125
+ if (!manifest) return subpath ? resolveFileTarget(pkgDir, subpath) : null;
126
+
127
+ const { exports } = manifest;
128
+ if (typeof exports === "string" || isRecord(exports)) {
129
+ const map = typeof exports === "string" ? { ".": exports } : exports;
130
+ const resolved = resolveExportsEntry(pkgDir, map, subpath);
131
+ if (resolved) return resolved;
132
+ }
133
+ if (subpath) return resolveFileTarget(pkgDir, subpath);
134
+ if (typeof manifest.main === "string") {
135
+ const resolved = resolveFileTarget(pkgDir, manifest.main);
136
+ if (resolved) return resolved;
137
+ }
138
+ return resolveFileTarget(pkgDir, "index.js");
139
+ }
140
+
141
+ function readManifest(pkgDir: string): Record<string, unknown> | null {
142
+ try {
143
+ const parsed: unknown = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
144
+ return isRecord(parsed) ? parsed : null;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ interface ModuleResolver {
151
+ _resolveFilename(request: string, parent: unknown, isMain: boolean, options?: unknown): string;
152
+ }
153
+
154
+ interface ResolverRegistration {
155
+ runtimeNodeModules: string;
156
+ stubs: Record<string, string>;
157
+ }
158
+
159
+ const REGISTRY = Symbol.for("omp.runtimeModuleResolver.registry");
160
+ const PATCHED = Symbol.for("omp.runtimeModuleResolver.patched");
161
+
162
+ /**
163
+ * The registration list lives on `globalThis` so a bundled copy and a
164
+ * source copy of this module in one process share the same registry — the
165
+ * resolver is patched once per process, and the patched closure must see
166
+ * every registration.
167
+ */
168
+ function resolverRegistry(): ResolverRegistration[] {
169
+ const holder = globalThis as { [REGISTRY]?: ResolverRegistration[] };
170
+ holder[REGISTRY] ??= [];
171
+ return holder[REGISTRY];
172
+ }
173
+
174
+ export interface RuntimeResolverOptions {
175
+ /** Absolute path to the runtime cache's `node_modules`. */
176
+ runtimeNodeModules: string;
177
+ /** Bare specifier → absolute file path overrides (e.g. `sharp` → no-op stub). */
178
+ stubs?: Record<string, string>;
179
+ }
180
+
181
+ /**
182
+ * Patch `node:module`'s resolver (idempotently) so bare specifiers that the
183
+ * stock compiled-binary resolver cannot find fall back to the registered
184
+ * runtime caches. Stock resolution is tried first and kept for anything
185
+ * outside the registered roots (bundled imports, node builtins, host or
186
+ * extension trees). Multiple runtime roots may register; they are consulted
187
+ * in registration order.
188
+ *
189
+ * One stock "success" is distrusted: the compiled-binary resolver ignores
190
+ * `main`/`exports` for real-FS packages (Bun #1763), so a package shipping
191
+ * its TS source next to `dist/` (e.g. `@huggingface/hub`'s root `index.ts`)
192
+ * resolves to the wrong file. When the stock hit lands inside a registered
193
+ * runtime root, the manifest-aware resolution wins.
194
+ */
195
+ export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }: RuntimeResolverOptions): void {
196
+ const registry = resolverRegistry();
197
+ const existing = registry.find(entry => entry.runtimeNodeModules === runtimeNodeModules);
198
+ if (existing) Object.assign(existing.stubs, stubs);
199
+ else registry.push({ runtimeNodeModules, stubs: { ...stubs } });
200
+
201
+ const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module;
202
+ const target = resolver as unknown as ModuleResolver & { [PATCHED]?: boolean };
203
+ if (target[PATCHED]) return;
204
+ const original = target._resolveFilename.bind(target);
205
+ target._resolveFilename = (request: string, parent: unknown, isMain: boolean, options?: unknown): string => {
206
+ let stockResolved: string | null = null;
207
+ let stockError: unknown;
208
+ try {
209
+ stockResolved = original(request, parent, isMain, options);
210
+ } catch (error) {
211
+ stockError = error;
212
+ }
213
+ const bare = !request.startsWith(".") && !request.startsWith("node:") && !path.isAbsolute(request);
214
+ if (bare) {
215
+ for (const registration of resolverRegistry()) {
216
+ if (stockResolved) {
217
+ // Correct a stock hit only inside the top-level package the
218
+ // request names. A hit in a nested node_modules (e.g. tar's
219
+ // minizlib resolving its own minipass@3 under
220
+ // <root>/minizlib/node_modules/) is version-correct — overriding
221
+ // it with the top-level instance would cross major versions.
222
+ const { packageName } = splitBareSpecifier(request);
223
+ const pkgDir = path.join(registration.runtimeNodeModules, ...packageName.split("/"));
224
+ if (!stockResolved.startsWith(pkgDir + path.sep)) continue;
225
+ if (path.relative(pkgDir, stockResolved).split(path.sep).includes("node_modules")) continue;
226
+ const expected = resolveRuntimeModule(registration.runtimeNodeModules, request);
227
+ if (expected) return expected;
228
+ } else {
229
+ const stub = registration.stubs[request];
230
+ if (stub) return stub;
231
+ const fallback = resolveRuntimeModule(registration.runtimeNodeModules, request);
232
+ if (fallback) return fallback;
233
+ }
234
+ }
235
+ }
236
+ if (stockResolved) return stockResolved;
237
+ throw stockError;
238
+ };
239
+ target[PATCHED] = true;
240
+ }
241
+
242
+ /** Pinned dependency set materialized into a runtime cache directory. */
243
+ export interface RuntimeInstallSpec {
244
+ dependencies: Record<string, string>;
245
+ /** Packages whose lifecycle scripts bun may run during the install. */
246
+ trustedDependencies?: string[];
247
+ }
248
+
249
+ export type RuntimeInstallPhase = "initiate" | "download" | "done";
250
+
251
+ export interface EnsureRuntimeInstalledOptions {
252
+ /** Directory owning the runtime `package.json` + `node_modules`. */
253
+ runtimeDir: string;
254
+ install: RuntimeInstallSpec;
255
+ /** Package whose installed manifest marks the runtime complete; defaults to the first dependency. */
256
+ probePackage?: string;
257
+ /** Phase notifications (progress UI); not emitted when already installed. */
258
+ onPhase?: (phase: RuntimeInstallPhase) => void;
259
+ lockAttempts?: number;
260
+ lockSleepMs?: number;
261
+ }
262
+
263
+ function isErrnoCode(error: unknown, code: string): boolean {
264
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
265
+ }
266
+
267
+ async function acquireInstallLock(runtimeDir: string, attempts: number, sleepMs: number): Promise<() => Promise<void>> {
268
+ const lockDir = `${runtimeDir}.lock`;
269
+ await fsp.mkdir(path.dirname(lockDir), { recursive: true });
270
+ for (let attempt = 0; attempt < attempts; attempt++) {
271
+ try {
272
+ await fsp.mkdir(lockDir);
273
+ return async () => {
274
+ await fsp.rm(lockDir, { recursive: true, force: true });
275
+ };
276
+ } catch (error) {
277
+ if (!isErrnoCode(error, "EEXIST")) throw error;
278
+ await Bun.sleep(sleepMs);
279
+ }
280
+ }
281
+ throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
282
+ }
283
+
284
+ async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
285
+ await fsp.mkdir(runtimeDir, { recursive: true });
286
+ const manifest: Record<string, unknown> = {
287
+ private: true,
288
+ type: "module",
289
+ dependencies: install.dependencies,
290
+ };
291
+ if (install.trustedDependencies?.length) manifest.trustedDependencies = install.trustedDependencies;
292
+ await Bun.write(path.join(runtimeDir, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
293
+ }
294
+
295
+ async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
296
+ if (!stream) return "";
297
+ return new Response(stream).text();
298
+ }
299
+
300
+ async function runRuntimeInstall(runtimeDir: string): Promise<void> {
301
+ // `process.execPath` is plain bun in source/bundle mode and the compiled
302
+ // binary otherwise; BUN_BE_BUN makes the compiled binary act as bun.
303
+ const proc = Bun.spawn([process.execPath, "install", "--cwd", runtimeDir, "--production"], {
304
+ env: { ...Bun.env, BUN_BE_BUN: "1" },
305
+ stdout: "pipe",
306
+ stderr: "pipe",
307
+ });
308
+ const [stdout, stderr, exitCode] = await Promise.all([
309
+ readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
310
+ readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
311
+ proc.exited,
312
+ ]);
313
+ if (exitCode === 0) return;
314
+ const output = `${stdout}\n${stderr}`.trim();
315
+ throw new Error(
316
+ `Failed to install runtime at ${runtimeDir} with ${process.execPath} install (exit ${exitCode}): ${output}`,
317
+ );
318
+ }
319
+
320
+ /**
321
+ * Materialize a pinned dependency set into `runtimeDir` (idempotent,
322
+ * cross-process safe via a lock directory). Returns `runtimeDir`.
323
+ */
324
+ export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string> {
325
+ const { runtimeDir, install, onPhase, lockAttempts = 240, lockSleepMs = 250 } = options;
326
+ let probePackage = options.probePackage;
327
+ if (!probePackage) {
328
+ for (const name in install.dependencies) {
329
+ probePackage = name;
330
+ break;
331
+ }
332
+ }
333
+ if (!probePackage) throw new Error(`Runtime install at ${runtimeDir} declares no dependencies`);
334
+ const probeManifest = Bun.file(path.join(runtimeDir, "node_modules", ...probePackage.split("/"), "package.json"));
335
+ if (await probeManifest.exists()) return runtimeDir;
336
+
337
+ onPhase?.("initiate");
338
+ const releaseLock = await acquireInstallLock(runtimeDir, lockAttempts, lockSleepMs);
339
+ try {
340
+ if (await probeManifest.exists()) return runtimeDir;
341
+ await writeRuntimeManifest(runtimeDir, install);
342
+ onPhase?.("download");
343
+ await runRuntimeInstall(runtimeDir);
344
+ onPhase?.("done");
345
+ return runtimeDir;
346
+ } finally {
347
+ await releaseLock();
348
+ }
349
+ }