Merge branch 'farm/6a62fa2b/fix-bunfs-override-validation-fallback'

Serve bundled pi-* and TypeBox through an in-process virtual namespace
on Bun 1.3.14+ where `--compile` extras are unreachable via any
filesystem API (issue #3423).

Merge resolution:
- Reconciled `TYPEBOX_SHIM_PATH` with main's #3414 fall-through:
  `__resolveTypeBoxShimPath(isCompiled, sourcePath, exists)` returns the
  `omp-legacy-pi-bundled:` virtual specifier in compiled mode (no FS
  probe) and the on-disk source path otherwise, dropping to null when
  the shim file is missing so bare typebox imports fall through to native
  resolution.
- Removed the now-dead `--compile` extras path: dropped
  `LEGACY_COMPAT_BUILD_ENTRYPOINTS` usage from both build-binary.ts and
  ci-release-build-binaries.ts and deleted scripts/binary-entrypoints.ts;
  the bundler reaches every surface via legacy-pi-bundled-registry.ts.
- Deleted obsolete tests for the removed bunfs machinery
  (legacy-pi-compat-entrypoints, legacy-pi-typebox-shim-validation) and
  added regression coverage for __resolveTypeBoxShimPath.
- Dropped the binary-compiling smoke driver per maintainer request.

Verified: bun check clean; 77 extensibility tests pass; instrumented
compiled-binary run confirmed onLoad fires for all bundled specifiers.
This commit is contained in:
can1357
2026-06-25 05:08:36 +02:00
11 changed files with 477 additions and 422 deletions
+3 -1
View File
@@ -16,7 +16,9 @@
### Fixed
- Fixed `omp install` of legacy pi extensions failing with `Cannot find module '/$bunfs/root/packages/coding-agent/src/extensibility/typebox.js'` on every released `omp-<platform>-<arch>` binary. Commit `dc5c93462f` removed worker entrypoints from `scripts/ci-release-build-binaries.ts`; the inline comment then claimed the legacy-shim and package-barrel entrypoints (`typebox.ts`, `legacy-pi-{ai,coding-agent}-shim.ts`, `packages/{agent,natives,tui,utils}/...`) were "still" passed to `bun build --compile`, but they had never been re-added. The release binaries shipped without those files in bunfs, so `legacy-pi-compat.ts` redirected `typebox` imports to a bunfs path that didn't exist. The two build scripts (release CI + local dev) now share `scripts/binary-entrypoints.ts` so the lists cannot drift apart, and `__resolveTypeBoxShimPath` mirrors `__validateLegacyPiPackageRootOverrides` (#2168) by dropping the override when the shim file is missing so future regressions fall through to native `node_modules` resolution instead of emitting a dead bunfs URL ([#3414](https://github.com/can1357/oh-my-pi/issues/3414)).
- Fixed `omp install` of legacy pi extensions failing with `Cannot find module '/$bunfs/root/packages/coding-agent/src/extensibility/typebox.js'` on every released `omp-<platform>-<arch>` binary. Commit `dc5c93462f` removed worker entrypoints from `scripts/ci-release-build-binaries.ts`; the inline comment then claimed the legacy-shim and package-barrel entrypoints (`typebox.ts`, `legacy-pi-{ai,coding-agent}-shim.ts`, `packages/{agent,natives,tui,utils}/...`) were "still" passed to `bun build --compile`, but they had never been re-added. The release binaries shipped without those files in bunfs, so `legacy-pi-compat.ts` redirected `typebox` imports to a bunfs path that didn't exist. `__resolveTypeBoxShimPath` now mirrors `__validateLegacyPiPackageRootOverrides` (#2168) by dropping the override when the shim file is missing, so a missing shim falls through to native `node_modules` resolution instead of emitting a dead bunfs URL ([#3414](https://github.com/can1357/oh-my-pi/issues/3414)).
- Fixed every legacy `@(scope)/pi-*` and `@sinclair/typebox` import failing to load on the `omp-darwin-arm64` release binary (and any other `omp` built with Bun 1.3.14). `__validateLegacyPiPackageRootOverrides` and the `rewriteLegacyPiImports` emit path both depended on `--compile` extras being reachable as `/$bunfs/root/...` filesystem entries, but Bun 1.3.14 stopped exposing them through every API (`fs.existsSync`, `Bun.file().exists()`, `Bun.resolveSync`, `await import()` on the bunfs path or its `file://` URL all fail; only `/$bunfs/root/<binary-name>` itself answers). `legacy-pi-compat.ts` now keeps a JS-heap reference to every bundled pi-* surface in a lazy-loaded sibling `legacy-pi-bundled-registry.ts` and serves them through an `omp-legacy-pi-bundled:` virtual namespace whose `Bun.plugin().onLoad` returns synthetic re-exports — no bunfs path ever leaves the module in compiled mode, and dev / source-link / installed-package modes keep the historical `file://` rewrite. The matching `--compile` extras in `scripts/build-binary.ts`, the shared `scripts/binary-entrypoints.ts` list, and the dead `BUNFS_PACKAGE_ROOT` / `bunfsPath` / `__computeBunfsPackageRoot` / `__joinBunfsPath` helpers are gone. ([#3423](https://github.com/can1357/oh-my-pi/issues/3423))
## [16.1.17] - 2026-06-24
@@ -2,7 +2,6 @@
import { createRequire } from "node:module";
import * as path from "node:path";
import { LEGACY_COMPAT_BUILD_ENTRYPOINTS } from "../../../scripts/binary-entrypoints";
const packageDir = path.join(import.meta.dir, "..");
const repoRoot = path.join(packageDir, "..", "..");
@@ -83,7 +82,15 @@ async function main(): Promise<void> {
"--root",
".",
"./packages/coding-agent/src/cli.ts",
...LEGACY_COMPAT_BUILD_ENTRYPOINTS,
// Legacy pi-* extension compat surfaces (host packages + shims)
// were previously listed as explicit `--compile` entries so the
// rewrite path could emit `/$bunfs/root/...` URLs against them.
// Bun 1.3.14 made bunfs files unreachable at runtime (issue
// #3423), so `legacy-pi-compat.ts` now serves them through a
// virtual namespace backed by `legacy-pi-bundled-registry.ts`,
// which static-imports each surface — the bundler already
// includes them via the main module graph, so no `--compile`
// extras are required.
"--outfile",
`packages/coding-agent/dist/${outName}`,
],
@@ -0,0 +1,40 @@
/**
* Static handles on every bundled `@oh-my-pi/pi-*` surface a legacy extension
* may import. Loaded lazily by `legacy-pi-compat.ts` in compiled-binary mode
* (issue #3423) and re-exported through the `omp-legacy-pi-bundled:` virtual
* namespace — bunfs paths cannot be resolved at runtime on Bun 1.3.14+, so
* the only way to re-route extension imports onto the host's in-process copy
* is via live module references captured at compile time.
*
* This module is split out from `legacy-pi-compat.ts` so dev/test runs that
* touch the compat layer never trigger the cascade through
* `legacy-pi-coding-agent-shim.ts → ../index → export/html/...` (which
* requires generated artifacts that only exist after a `bun run build`).
*
* The bundler reaches every entry below via standard static-import analysis,
* so the matching `--compile` extras can be dropped from
* `scripts/build-binary.ts`.
*/
import * as bundledPiAgentCore from "@oh-my-pi/pi-agent-core";
import * as bundledPiNatives from "@oh-my-pi/pi-natives";
import * as bundledPiTui from "@oh-my-pi/pi-tui";
import * as bundledPiUtils from "@oh-my-pi/pi-utils";
import * as bundledLegacyPiAiShim from "../legacy-pi-ai-shim";
import * as bundledLegacyPiCodingAgentShim from "../legacy-pi-coding-agent-shim";
import * as bundledTypeBoxShim from "../typebox";
/**
* Canonical specifier → live module namespace. Keys MUST match the right-hand
* side of `bundledRegistryVirtualSpecifier(...)` calls in
* `legacy-pi-compat.ts`; the synthesizer enumerates each namespace's own
* enumerable exports at extension load time.
*/
export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
"@oh-my-pi/pi-agent-core": bundledPiAgentCore,
"@oh-my-pi/pi-ai": bundledLegacyPiAiShim,
"@oh-my-pi/pi-coding-agent": bundledLegacyPiCodingAgentShim,
"@oh-my-pi/pi-natives": bundledPiNatives,
"@oh-my-pi/pi-tui": bundledPiTui,
"@oh-my-pi/pi-utils": bundledPiUtils,
typebox: bundledTypeBoxShim,
};
@@ -5,6 +5,156 @@ import { isCompiledBinary } from "@oh-my-pi/pi-utils";
const IS_COMPILED_BINARY = isCompiledBinary();
// === Bundled host-package registry (issue #3423) ===
//
// Bun 1.3.14 stopped exposing `--compile` extras through any filesystem-style
// API: `fs.existsSync`, `Bun.file().exists()`, `Bun.resolveSync`, and even
// `import("/$bunfs/...")` / `import("file:///$bunfs/...")` all fail for the
// embedded entries — only the main binary itself answers from
// `/$bunfs/root/<binary-name>`. The previous strategy of rewriting
// `@(scope)/pi-*` imports to a `file:///$bunfs/...` URL therefore breaks
// every legacy extension in compiled mode (issue #3423; see also issues
// #3329, #2168). Bun.plugin `onResolve` for bare specifiers also no longer
// fires for transitive imports inside runtime-loaded extensions, so the
// fallback hook in `installLegacyPiSpecifierShim()` cannot rescue them.
//
// Instead we keep a JS-heap reference to every bundled pi-* surface (the
// canonical host packages and the legacy shims) and re-export them through a
// Bun.plugin `onLoad` against a custom namespace. Extension source rewrites
// emit `omp-legacy-pi-bundled:<key>` specifiers that the synthetic loader
// resolves against the registry — no bunfs path ever leaves this module in
// compiled mode. Dev / source-link / installed-package modes keep the
// historical `file://` rewrite (the source files exist on disk and load fine
// through Bun's standard URL loader).
//
// The registry lives in a sibling file (`legacy-pi-bundled-registry.ts`)
// loaded via a conditional dynamic import: its transitive deps include the
// coding-agent root which pulls in generated artifacts (e.g.
// `export/html/tool-views.generated.js`) that only exist after a build, so a
// static import would crash every dev/test run that touches
// `legacy-pi-compat.ts`. This is the documented "conditional platform code"
// exception to the static-import rule.
const BUNDLED_VIRTUAL_SCHEME = "omp-legacy-pi-bundled:";
const BUNDLED_VIRTUAL_NAMESPACE = "omp-legacy-pi-bundled";
const BUNDLED_REGISTRY_GLOBAL = "__ompLegacyPiBundledRegistry";
const TYPEBOX_BUNDLED_REGISTRY_KEY = "typebox";
type BundledRegistry = Readonly<Record<string, Readonly<Record<string, unknown>>>>;
let bundledRegistryPromise: Promise<BundledRegistry> | null = null;
/**
* Lazy-load the bundled host-package registry and stash it on `globalThis`
* for the synthetic loader emitted by `synthesizeBundledModuleSource`. The
* dynamic import is gated by `IS_COMPILED_BINARY` so dev/test runs (where
* the registry's transitive deps include build-time-generated artifacts)
* never trigger the cascade.
*/
function ensureBundledRegistryLoaded(): Promise<BundledRegistry> {
if (!IS_COMPILED_BINARY) {
return Promise.reject(
new Error("omp:legacy-pi-shim: bundled registry is only available in compiled-binary mode"),
);
}
if (!bundledRegistryPromise) {
bundledRegistryPromise = import("./legacy-pi-bundled-registry").then(m => {
(globalThis as Record<string, unknown>)[BUNDLED_REGISTRY_GLOBAL] = m.BUNDLED_PI_REGISTRY;
return m.BUNDLED_PI_REGISTRY;
});
}
return bundledRegistryPromise;
}
function bundledRegistryVirtualSpecifier(registryKey: string): string {
return `${BUNDLED_VIRTUAL_SCHEME}${registryKey}`;
}
function isBundledVirtualSpecifier(value: string): boolean {
return value.startsWith(BUNDLED_VIRTUAL_SCHEME);
}
/**
* Build the synthetic ES module source for a `omp-legacy-pi-bundled:<key>`
* import against an explicit registry. Pure: takes the live module namespace
* and emits a string of ES exports rooted in `globalThis[BUNDLED_REGISTRY_GLOBAL]`.
* `synthesizeBundledModuleSource` wraps this with the lazy registry load —
* tests use this sync helper directly to assert export-shape preservation.
*/
function synthesizeBundledModuleSourceFromRegistry(registryKey: string, registry: BundledRegistry): string {
const mod = registry[registryKey];
if (!mod) {
throw new Error(`omp:legacy-pi-shim: no bundled module registered for ${registryKey}`);
}
const lines: string[] = [
`const __omp_bundled = globalThis[${JSON.stringify(BUNDLED_REGISTRY_GLOBAL)}][${JSON.stringify(registryKey)}];`,
];
let hasDefault = false;
for (const exportName in mod) {
if (exportName === "default") {
hasDefault = true;
continue;
}
lines.push(`export const ${exportName} = __omp_bundled[${JSON.stringify(exportName)}];`);
}
if (hasDefault) {
lines.push("export default __omp_bundled.default;");
}
lines.push("");
return lines.join("\n");
}
/**
* Build the synthetic ES module source served for an
* `omp-legacy-pi-bundled:<key>` import. Enumerates the live module namespace
* so legacy extensions see the same named/default exports they would have
* gotten from a real `file://` load — without touching the inaccessible bunfs
* filesystem.
*/
async function synthesizeBundledModuleSource(registryKey: string): Promise<string> {
const registry = await ensureBundledRegistryLoaded();
return synthesizeBundledModuleSourceFromRegistry(registryKey, registry);
}
/**
* Test seam: lazily loads the canonical-specifier → live module registry used
* by the compiled-binary virtual loader. The synthesizer enumerates this at
* extension load time, so the test only needs to confirm registry shape +
* identity. In non-compiled mode this rejects — callers MUST guard.
*/
export function __getLegacyPiBundledRegistry(): Promise<BundledRegistry> {
return ensureBundledRegistryLoaded();
}
/**
* Test seam: builds the synthetic ES module source for a virtual specifier
* against an explicit registry. Pure (no globalThis read); the emitted source
* still routes runtime lookups through `globalThis[BUNDLED_REGISTRY_GLOBAL]`.
*/
export function __synthesizeLegacyPiBundledSourceWithRegistry(
registryKey: string,
registry: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
): string {
return synthesizeBundledModuleSourceFromRegistry(registryKey, registry);
}
/**
* Test seam: returns the synthetic ES module source served for a virtual
* specifier. Convenience wrapper around the lazy registry load + synth path
* for tests that already run in compiled-binary mode.
*/
export function __synthesizeLegacyPiBundledSource(registryKey: string): Promise<string> {
return synthesizeBundledModuleSource(registryKey);
}
/**
* Test seam: returns the globalThis key the synthetic loader reads from. Tests
* assert that the emitted source addresses the exact stash key the install
* function writes to, so a rename can't break extension loads silently.
*/
export function __getLegacyPiBundledRegistryGlobal(): string {
return BUNDLED_REGISTRY_GLOBAL;
}
// Canonical scope for in-process pi packages. Plugins published against any of
// the aliased scopes below (mariozechner's original publish, earendil-works'
// fork, or the canonical @oh-my-pi scope itself) are remapped to this scope and
@@ -73,57 +223,14 @@ const PACKAGE_IMPORT_EXCLUDED = Symbol("packageImportExcluded");
// not provide and plugins relying on them must vendor TypeBox directly.
const TYPEBOX_SPECIFIER_FILTER = /^(?:@sinclair\/typebox|typebox)$/;
// Compat shim and bundled-package paths used in compiled-binary mode. The shim
// paths must point at files that ship inside the bunfs root; in dev /
// source-link / installed-package mode the canonical specifier resolves via
// `Bun.resolveSync` so only the shim files need explicit paths there.
//
// `BUNFS_PACKAGE_ROOT` is derived from `import.meta.dir` rather than hardcoded
// as `/$bunfs/root/packages` so the prefix stays platform-native: on Windows
// the bunfs mount appears as `<drive>:\~BUN\root\…` (see oven-sh/bun#15766),
// and a hardcoded POSIX literal would normalize to `\$bunfs\root\…` and fail
// to resolve. Compiled Bun modules currently report the bunfs root itself from
// `import.meta.dir`, so appending `packages` lands on the `--root ../..`
// package directory used by `scripts/build-binary.ts`.
//
// Every shim listed below must also be registered as an explicit `--compile`
// entrypoint in `scripts/build-binary.ts` or release builds fail with
// missing-module errors. Non-shim bundled packages are resolved via
// `Bun.resolveSync` (see `resolveCanonicalPiSpecifier`) outside compiled mode,
// so they keep working when on-disk layout differs from the monorepo tree.
/**
* Compute the bunfs package root from the compiled binary's `import.meta.dir`
* (or any stand-in supplied by tests). Bun compiled binaries report one of:
*
* - the bunfs mount root itself — `/$bunfs/root` or `<drive>:\~BUN\root` (Bun
* 1.2.x and early 1.3.x). Append `packages` for the canonical layout.
* - the bunfs mount root followed by the binary's basename — `//root/<bin>`
* on POSIX or `<drive>:\~BUN\root\<bin>.exe` on Windows (observed on Bun
* 1.3.14 with the cross-compiled `omp-darwin-arm64` release asset — issue
* #3329). The trailing segment is stripped so the result still lands on
* `<root>/packages`.
* - the module's own source directory if a future Bun release switches to
* module-specific `import.meta.dir` values:
* `<bunfs>/packages/coding-agent/src/extensibility/plugins`.
* The bunfs-root-with-binary branch slices the original `metaDir`, and
* `bunfsPath` uses a matching double-slash-preserving join, so the bunfs-native
* prefix is preserved verbatim — `path.posix.join` collapses `//root` to
* `/root`, but Bun's bunfs lookup is keyed on the exact `//root` form.
*
* Exported for tests; production callers use `BUNFS_PACKAGE_ROOT` below.
*/
export function __computeBunfsPackageRoot(metaDir: string, pathImpl: typeof path = path): string {
const pluginsDirSuffix = pathImpl.join("packages", "coding-agent", "src", "extensibility", "plugins");
const normalizedMetaDir = pathImpl.normalize(metaDir);
if (normalizedMetaDir.endsWith(pluginsDirSuffix)) {
return pathImpl.resolve(metaDir, "..", "..", "..", "..");
}
const parent = pathImpl.dirname(metaDir);
if (pathImpl.basename(pathImpl.normalize(parent)) === "root") {
return `${parent + pathImpl.sep}packages`;
}
return pathImpl.join(metaDir, "packages");
}
// Compat-shim path resolution. In compiled-binary mode every bundled surface
// is served through the `omp-legacy-pi-bundled:` virtual namespace (see the
// registry block above) — bunfs paths are unreachable on Bun 1.3.14+, so the
// pre-#3423 helpers that derived `/$bunfs/root/...` paths from
// `import.meta.dir` are gone. Dev / source-link / installed-package modes
// still need a real filesystem path for the source shims, which
// `sourceShimPath` computes either from the npm prebuilt `dist/cli.js`
// bundle (`PI_BUNDLED=true`) or directly from the monorepo source tree.
/**
* Compute the package root for the npm prebuilt `dist/cli.js` bundle.
@@ -147,35 +254,6 @@ export function __computeBundledSelfPackageRoot(metaDir: string, pathImpl: typeo
return pathImpl.resolve(metaDir);
}
const BUNFS_PACKAGE_ROOT = IS_COMPILED_BINARY ? __computeBunfsPackageRoot(import.meta.dir) : null;
/**
* Join a computed bunfs package root with descendants without collapsing
* Bun's POSIX `//root` mount prefix.
*
* Exported for tests; production callers use `bunfsPath` below.
*/
export function __joinBunfsPath(root: string, segments: readonly string[], pathImpl: typeof path = path): string {
const joined = pathImpl.join(root, ...segments);
const doubleRootPrefix = pathImpl.sep + pathImpl.sep;
const tripleRootPrefix = doubleRootPrefix + pathImpl.sep;
if (
root.startsWith(doubleRootPrefix) &&
!root.startsWith(tripleRootPrefix) &&
!joined.startsWith(doubleRootPrefix)
) {
return pathImpl.sep + joined;
}
return joined;
}
function bunfsPath(...segments: string[]): string {
if (!BUNFS_PACKAGE_ROOT) {
throw new Error("bunfsPath is only valid in compiled-binary mode");
}
return __joinBunfsPath(BUNFS_PACKAGE_ROOT, segments);
}
function resolveBundledSelfPackageRoot(): string | undefined {
if (!process.env.PI_BUNDLED) return undefined;
return __computeBundledSelfPackageRoot(import.meta.dir);
@@ -190,34 +268,35 @@ function sourceShimPath(file: string): string {
}
/**
* Resolve the path the TypeBox compatibility shim ships at — bunfs in compiled
* mode, on-disk source elsewhere — then drop it when the file is missing.
* Resolve the path the TypeBox compatibility shim ships at, then drop it when
* the source file is missing.
*
* Validation mirrors `__validateLegacyPiPackageRootOverrides` (#2168): if the
* computed candidate doesn't exist (e.g. a release build silently omitted the
* `--compile` entrypoint — issue #3414), `resolveTypeBoxSpecifier` returns
* `undefined` and `rewriteLegacyExtensionSource` leaves bare `typebox` /
* `@sinclair/typebox` specifiers alone, so Bun falls through to native
* resolution against the extension's own `node_modules`.
* In compiled-binary mode the shim is served through the
* `omp-legacy-pi-bundled:` virtual namespace (issue #3423) — bunfs paths are
* unreachable on Bun 1.3.14+, so the virtual specifier is always available and
* needs no filesystem probe. In dev / source-link / installed-package mode the
* shim is an on-disk source file; validation mirrors
* `__validateLegacyPiPackageRootOverrides` (#2168): if the computed candidate
* doesn't exist (e.g. an install that dropped the source — issue #3414),
* `resolveTypeBoxSpecifier` returns `undefined` and
* `rewriteLegacyExtensionSource` leaves bare `typebox` / `@sinclair/typebox`
* specifiers alone, so Bun falls through to native resolution against the
* extension's own `node_modules`.
*
* Exported for tests; production callers use `TYPEBOX_SHIM_PATH`.
*/
export function __resolveTypeBoxShimPath(
bunfsRoot: string | null,
bundledSelfRoot: string | undefined,
metaDir: string,
isCompiled: boolean,
sourcePath: string,
pathExistsSync: (p: string) => boolean = fs.existsSync,
pathImpl: typeof path = path,
): string | null {
const candidate = bunfsRoot
? __joinBunfsPath(bunfsRoot, ["coding-agent", "src", "extensibility", "typebox.js"], pathImpl)
: bundledSelfRoot
? pathImpl.join(bundledSelfRoot, "src", "extensibility", "typebox.ts")
: pathImpl.resolve(metaDir, "..", "typebox.ts");
return pathExistsSync(candidate) ? candidate : null;
if (isCompiled) {
return bundledRegistryVirtualSpecifier(TYPEBOX_BUNDLED_REGISTRY_KEY);
}
return pathExistsSync(sourcePath) ? sourcePath : null;
}
const TYPEBOX_SHIM_PATH = __resolveTypeBoxShimPath(BUNFS_PACKAGE_ROOT, BUNDLED_SELF_PACKAGE_ROOT, import.meta.dir);
const TYPEBOX_SHIM_PATH = __resolveTypeBoxShimPath(IS_COMPILED_BINARY, sourceShimPath("typebox.ts"));
// Legacy extensions historically imported `Type` (and `Static`/`TSchema`) from
// the package root of `@(scope)/pi-ai`. pi-ai 15.1.0 removed the runtime `Type`
@@ -227,59 +306,67 @@ const TYPEBOX_SHIM_PATH = __resolveTypeBoxShimPath(BUNFS_PACKAGE_ROOT, BUNDLED_S
// plus the borrowed `Type` runtime from the Zod-backed TypeBox shim. Subpath
// imports such as `@oh-my-pi/pi-ai/oauth` continue to resolve directly
// against the bundled pi-ai package.
const LEGACY_PI_AI_SHIM_PATH = BUNFS_PACKAGE_ROOT
? bunfsPath("coding-agent", "src", "extensibility", "legacy-pi-ai-shim.js")
const LEGACY_PI_AI_SHIM_PATH = IS_COMPILED_BINARY
? bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-ai`)
: sourceShimPath("legacy-pi-ai-shim.ts");
// The coding-agent's own `./src/index.ts` cannot be listed as an extra
// `bun --compile` entrypoint alongside the CLI entry without breaking binary
// startup (issue #1474 follow-up). Legacy `@(scope)/pi-coding-agent` root
// imports therefore resolve through a sibling shim whose distinct file path
// avoids that collision while re-exporting the canonical package surface.
const LEGACY_PI_CODING_AGENT_SHIM_PATH = BUNFS_PACKAGE_ROOT
? bunfsPath("coding-agent", "src", "extensibility", "legacy-pi-coding-agent-shim.js")
// startup (issue #1474 follow-up). In compiled-binary mode the legacy
// `@(scope)/pi-coding-agent` root therefore resolves through the bundled
// registry shim; in dev / source-link / installed-package mode it points at
// the sibling source shim whose distinct file path avoids the #1474 collision
// while still re-exporting the canonical package surface.
const LEGACY_PI_CODING_AGENT_SHIM_PATH = IS_COMPILED_BINARY
? bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-coding-agent`)
: sourceShimPath("legacy-pi-coding-agent-shim.ts");
// Package-root overrides. Shim entries are always applied because they replace
// (or augment) the canonical surface even in non-compiled installs. The bunfs
// entries are added only in compiled-binary mode — in dev / source-link /
// installed-package mode the canonical specifier resolves cleanly through
// `Bun.resolveSync`, and hardcoding a relative source-tree path would break
// installs where the bundled packages live at `node_modules/@oh-my-pi/pi-*`
// rather than `packages/*`.
// Package-root overrides. Shim entries (`pi-ai`, `pi-coding-agent`) always
// replace the canonical surface so the legacy `Type` runtime and the legacy
// helpers stay reachable. The bundled host packages (`pi-agent-core`,
// `pi-natives`, `pi-tui`, `pi-utils`) are added only in compiled-binary mode
// to route extensions onto the in-process module instance — in dev /
// source-link / installed-package mode the canonical specifier resolves
// cleanly through `Bun.resolveSync` and hardcoding a source-tree path would
// miss installs where the bundled packages live at `node_modules/@oh-my-pi/pi-*`.
//
// Every override target is validated against the on-disk filesystem at module
// init: any entry whose file is missing (e.g. a compiled binary where Bun's
// `--compile` quietly dropped an additional entrypoint — issue #2168) is left
// out so `resolveCanonicalPiSpecifier` falls through to `getResolvedSpecifier`,
// which throws under bunfs and triggers the catch in `rewriteLegacyPiImports`.
// That catch leaves the specifier untouched so Bun resolves the canonical
// `@oh-my-pi/pi-*` import from the extension's own `node_modules` instead of
// emitting a bunfs `file://` URL to a module that isn't actually present.
// Compiled-binary entries are `omp-legacy-pi-bundled:<key>` specifiers handed
// to the synthetic onLoad in `installLegacyPiSpecifierShim()` — bunfs paths
// are unusable on Bun 1.3.14+ (issue #3423). Filesystem-shaped overrides are
// still validated against on-disk presence so a missing dev-mode shim falls
// through to `getResolvedSpecifier`.
/**
* Drop overrides whose targets are missing on disk so they can fall through to
* the canonical-resolution path. Exported for the test seam in #2168.
* Drop overrides whose filesystem targets are missing so they can fall
* through to the canonical-resolution path. Virtual `omp-legacy-pi-bundled:`
* entries always pass — the bundled registry is the source of truth in
* compiled-binary mode where bunfs paths are unreachable (issue #3423).
*
* `pathExistsSync` defaults to `fs.existsSync`; the tests inject a stub to
* `pathExistsSync` defaults to `fs.existsSync`; tests inject a stub to
* simulate the missing-entrypoint failure mode without touching the real FS.
*/
export function __validateLegacyPiPackageRootOverrides(
candidates: Record<string, string>,
pathExistsSync: (p: string) => boolean = fs.existsSync,
): Record<string, string> {
return Object.fromEntries(Object.entries(candidates).filter(([, candidate]) => pathExistsSync(candidate)));
return Object.fromEntries(
Object.entries(candidates).filter(
([, candidate]) => isBundledVirtualSpecifier(candidate) || pathExistsSync(candidate),
),
);
}
const LEGACY_PI_PACKAGE_ROOT_OVERRIDES = __validateLegacyPiPackageRootOverrides({
[`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
[`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
...(BUNFS_PACKAGE_ROOT
...(IS_COMPILED_BINARY
? {
[`${CANONICAL_PI_SCOPE}/pi-agent-core`]: bunfsPath("agent", "src", "index.js"),
[`${CANONICAL_PI_SCOPE}/pi-natives`]: bunfsPath("natives", "native", "index.js"),
[`${CANONICAL_PI_SCOPE}/pi-tui`]: bunfsPath("tui", "src", "index.js"),
[`${CANONICAL_PI_SCOPE}/pi-utils`]: bunfsPath("utils", "src", "index.js"),
[`${CANONICAL_PI_SCOPE}/pi-agent-core`]: bundledRegistryVirtualSpecifier(
`${CANONICAL_PI_SCOPE}/pi-agent-core`,
),
[`${CANONICAL_PI_SCOPE}/pi-natives`]: bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-natives`),
[`${CANONICAL_PI_SCOPE}/pi-tui`]: bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-tui`),
[`${CANONICAL_PI_SCOPE}/pi-utils`]: bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-utils`),
}
: {}),
});
@@ -328,6 +415,12 @@ function resolveCanonicalPiSpecifier(remappedSpecifier: string): string {
}
function toImportSpecifier(resolvedPath: string): string {
// Virtual `omp-legacy-pi-bundled:` specifiers are served by the synthetic
// onLoad in `installLegacyPiSpecifierShim()`; wrapping them as `file://`
// would corrupt the scheme and bypass the bundled registry.
if (isBundledVirtualSpecifier(resolvedPath)) {
return resolvedPath;
}
return url.pathToFileURL(resolvedPath).href;
}
@@ -765,6 +858,12 @@ export function installLegacyPiSpecifierShim(): void {
setup(build) {
build.onResolve({ filter: LEGACY_PI_SPECIFIER_FILTER, namespace: "file" }, resolveLegacyPiSpecifier);
build.onResolve({ filter: TYPEBOX_SPECIFIER_FILTER, namespace: "file" }, resolveTypeBoxSpecifier);
// Compiled-binary mode: serve `omp-legacy-pi-bundled:<key>` imports
// from the JS-heap registry. The rewrite path emits these specifiers
// in place of unreachable `file:///$bunfs/...` URLs (issue #3423).
build.onLoad({ filter: /.*/, namespace: BUNDLED_VIRTUAL_NAMESPACE }, async args => {
return { contents: await synthesizeBundledModuleSource(args.path), loader: "js" };
});
},
});
}
@@ -0,0 +1,96 @@
import { describe, expect, it } from "bun:test";
import {
__getLegacyPiBundledRegistryGlobal,
__synthesizeLegacyPiBundledSourceWithRegistry,
} from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
// Regression for issue #3423: Bun 1.3.14 made `--compile` extras unreachable
// via every filesystem-style API, so `legacy-pi-compat.ts` now routes
// canonical `@oh-my-pi/pi-*` imports through a virtual specifier whose body
// re-exports a live registry entry from `globalThis`. The synthesizer must
// preserve every named export (and a default if present) so legacy
// extensions see the same surface they would have through a real `file://`
// load — otherwise `import { foo } from "@oh-my-pi/pi-coding-agent"` raises
// `Export named 'foo' not found in module ...`.
describe("legacy-pi bundled virtual module synthesizer (issue #3423)", () => {
const registry = {
"@oh-my-pi/pi-coding-agent": {
VERSION: "16.1.17",
defineTool: () => undefined,
Type: { Object: () => undefined },
},
"@oh-my-pi/pi-utils": {
isCompiledBinary: () => false,
default: () => "default-export",
VERSION: "16.1.17",
},
typebox: {
Type: { Object: () => undefined },
},
};
const globalKey = __getLegacyPiBundledRegistryGlobal();
it("emits one ES named export per enumerable namespace key", () => {
const src = __synthesizeLegacyPiBundledSourceWithRegistry("@oh-my-pi/pi-coding-agent", registry);
expect(src).toContain(
`const __omp_bundled = globalThis[${JSON.stringify(globalKey)}]["@oh-my-pi/pi-coding-agent"];`,
);
expect(src).toContain('export const VERSION = __omp_bundled["VERSION"];');
expect(src).toContain('export const defineTool = __omp_bundled["defineTool"];');
expect(src).toContain('export const Type = __omp_bundled["Type"];');
// Every named export emerges from a live registry lookup — never the FS.
expect(src).not.toMatch(/\$bunfs|file:\/\//);
});
it("forwards `default` through `export default` so default imports survive", () => {
const src = __synthesizeLegacyPiBundledSourceWithRegistry("@oh-my-pi/pi-utils", registry);
expect(src).toContain("export default __omp_bundled.default;");
// Default and named exports coexist on the same module.
expect(src).toContain('export const VERSION = __omp_bundled["VERSION"];');
expect(src).toContain('export const isCompiledBinary = __omp_bundled["isCompiledBinary"];');
});
it("omits `default` line when the registered namespace has no default export", () => {
const src = __synthesizeLegacyPiBundledSourceWithRegistry("@oh-my-pi/pi-coding-agent", registry);
expect(src).not.toContain("export default");
});
it("throws when asked to synthesize a key the registry does not cover", () => {
expect(() => __synthesizeLegacyPiBundledSourceWithRegistry("@oh-my-pi/pi-not-bundled", registry)).toThrow(
/no bundled module registered for @oh-my-pi\/pi-not-bundled/,
);
});
it("addresses the same globalThis key the install function would stash to", () => {
// The emitted source MUST read from the exact key the install function
// writes to — a rename of either side breaks every legacy extension
// load with a `Cannot read properties of undefined` at first import.
const src = __synthesizeLegacyPiBundledSourceWithRegistry("typebox", registry);
expect(src.startsWith(`const __omp_bundled = globalThis[${JSON.stringify(globalKey)}]["typebox"];`)).toBe(true);
});
it("end-to-end: synthesized source resolves named bindings against a runtime globalThis entry", () => {
// Evaluate the synthesized source in isolation. Bun's loader normally
// turns it into an ES module; here we use `new Function` to exercise
// the inner globalThis lookup + property-getter pattern in isolation —
// it would `throw` if the emitted code addressed the wrong stash key
// or skipped an enumerable export.
(globalThis as Record<string, unknown>)[globalKey] = registry;
try {
const src = __synthesizeLegacyPiBundledSourceWithRegistry("@oh-my-pi/pi-coding-agent", registry);
// Strip the ES export prefix and run the body as a plain script so
// we can read `__omp_bundled` from the returned closure.
const body = src
.split("\n")
.filter(line => line.startsWith("const __omp_bundled"))
.join("\n");
const fn = new Function(`${body}; return __omp_bundled;`);
const live = fn() as Record<string, unknown>;
expect(live.VERSION).toBe("16.1.17");
expect(typeof live.defineTool).toBe("function");
expect(typeof live.Type).toBe("object");
} finally {
delete (globalThis as Record<string, unknown>)[globalKey];
}
});
});
@@ -1,85 +1,36 @@
import { describe, expect, it } from "bun:test";
import * as path from "node:path";
import {
__computeBundledSelfPackageRoot,
__computeBunfsPackageRoot,
__joinBunfsPath,
} from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
// Regression for issue #1514: legacy pi compat shim paths were built from a
// hardcoded POSIX literal `/$bunfs/root/packages`. On Windows the bunfs root
// mounts at `<drive>:\~BUN\root\…` (oven-sh/bun#15766) and the POSIX literal
// normalises to `\$bunfs\root\…`, which is unresolvable. The fix derives the
// package root from the compiled binary's `import.meta.dir`, so the host OS's
// separators are preserved end-to-end.
describe("legacy pi compat bunfs root computation (issue #1514)", () => {
it("appends packages to the Windows-native compiled bunfs root", () => {
const winMetaDir = "B:\\~BUN\\root";
const root = __computeBunfsPackageRoot(winMetaDir, path.win32);
expect(root).toBe("B:\\~BUN\\root\\packages");
// The shim path joined from this root must still live under the bunfs
// mount, never collapse onto the working drive (which is what
// `path.win32.resolve("/$bunfs/root/packages/...")` would produce).
expect(path.win32.join(root, "coding-agent", "src", "extensibility", "legacy-pi-ai-shim.js")).toBe(
"B:\\~BUN\\root\\packages\\coding-agent\\src\\extensibility\\legacy-pi-ai-shim.js",
);
});
it("appends packages to the POSIX compiled bunfs root on Linux and macOS", () => {
expect(__computeBunfsPackageRoot("/$bunfs/root", path.posix)).toBe("/$bunfs/root/packages");
});
it("also supports module-specific import.meta.dir values if Bun changes compiled semantics", () => {
const winMetaDir = "B:\\~BUN\\root\\packages\\coding-agent\\src\\extensibility\\plugins";
expect(__computeBunfsPackageRoot(winMetaDir, path.win32)).toBe("B:\\~BUN\\root\\packages");
const posixMetaDir = "/$bunfs/root/packages/coding-agent/src/extensibility/plugins";
expect(__computeBunfsPackageRoot(posixMetaDir, path.posix)).toBe("/$bunfs/root/packages");
});
it("preserves the double-slash bunfs prefix through production shim path joins (issue #3329)", () => {
// Bun 1.3.14 on the cross-compiled `omp-darwin-arm64` release asset
// reports `import.meta.dir` as `//root/omp-darwin-arm64`. The leading
// `//` is part of Bun's bunfs identifier, so both the root computation
// and the production shim-path join must preserve it.
const root = __computeBunfsPackageRoot("//root/omp-darwin-arm64", path.posix);
expect(root).toBe("//root/packages");
expect(__joinBunfsPath(root, ["coding-agent", "src", "extensibility", "typebox.js"], path.posix)).toBe(
"//root/packages/coding-agent/src/extensibility/typebox.js",
);
// `$bunfs`-prefixed variant is also handled.
expect(__computeBunfsPackageRoot("/$bunfs/root/omp", path.posix)).toBe("/$bunfs/root/packages");
// Windows variant: bunfs mount at `<drive>:\~BUN\root` with the binary
// basename appended.
expect(__computeBunfsPackageRoot("B:\\~BUN\\root\\omp.exe", path.win32)).toBe("B:\\~BUN\\root\\packages");
});
it("uses the current host path implementation for production calls", () => {
const metaDir = path.join("/", "anywhere", "root");
expect(__computeBunfsPackageRoot(metaDir)).toBe(path.join("/", "anywhere", "root", "packages"));
});
import { __computeBundledSelfPackageRoot } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
// Issue #3423 removed the runtime bunfs-path computation (`__computeBunfsPackageRoot`,
// `__joinBunfsPath`, `bunfsPath`): Bun 1.3.14 stopped exposing `--compile`
// extras through any filesystem API, so the compat layer now routes the
// bundled host packages and shims through the `omp-legacy-pi-bundled:`
// virtual namespace (see `legacy-pi-bundled-virtual.test.ts`). The bunfs
// path computation is dead and its regression tests (issues #1514, #3329)
// retired alongside the code.
//
// The npm-prebuilt `dist/cli.js` self-package-root computation is still in
// use by `sourceShimPath` in installed-package mode, so its contract stays
// pinned below.
describe("legacy pi compat bundled-self package root computation", () => {
it("derives the npm prebuilt bundle package root from dist import.meta.dir", () => {
const computeBundledSelfPackageRoot = __computeBundledSelfPackageRoot;
const winMetaDir = "C:\\Users\\me\\.bun\\install\\global\\node_modules\\@oh-my-pi\\pi-coding-agent\\dist";
expect(computeBundledSelfPackageRoot(winMetaDir, path.win32)).toBe(
expect(__computeBundledSelfPackageRoot(winMetaDir, path.win32)).toBe(
"C:\\Users\\me\\.bun\\install\\global\\node_modules\\@oh-my-pi\\pi-coding-agent",
);
const posixMetaDir = "/home/me/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist";
expect(computeBundledSelfPackageRoot(posixMetaDir, path.posix)).toBe(
expect(__computeBundledSelfPackageRoot(posixMetaDir, path.posix)).toBe(
"/home/me/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent",
);
});
it("derives the source package root when PI_BUNDLED is used outside dist", () => {
const computeBundledSelfPackageRoot = __computeBundledSelfPackageRoot;
const winMetaDir = "C:\\repo\\packages\\coding-agent\\src\\extensibility\\plugins";
expect(computeBundledSelfPackageRoot(winMetaDir, path.win32)).toBe("C:\\repo\\packages\\coding-agent");
expect(__computeBundledSelfPackageRoot(winMetaDir, path.win32)).toBe("C:\\repo\\packages\\coding-agent");
const posixMetaDir = "/repo/packages/coding-agent/src/extensibility/plugins";
expect(computeBundledSelfPackageRoot(posixMetaDir, path.posix)).toBe("/repo/packages/coding-agent");
expect(__computeBundledSelfPackageRoot(posixMetaDir, path.posix)).toBe("/repo/packages/coding-agent");
});
});
@@ -1,104 +0,0 @@
import { describe, expect, it } from "bun:test";
import * as path from "node:path";
import { LEGACY_COMPAT_BUILD_ENTRYPOINTS } from "../../../../scripts/binary-entrypoints";
/**
* Regression for issue #3414.
*
* `legacy-pi-compat.ts` redirects bare `typebox` / `@sinclair/typebox` imports
* and the legacy `@(scope)/pi-*` package roots onto computed bunfs paths under
* `/$bunfs/root/packages/coding-agent/src/extensibility/` and
* `/$bunfs/root/packages/<barrel>/...`. Bun's `--compile` static analyzer
* never visits those literals, so each shim must be passed to `bun build` as
* an extra entrypoint or it is silently omitted from bunfs and plugin loads
* fail with `Cannot find module '/$bunfs/root/.../typebox.js'`.
*
* Commit `dc5c93462f` removed the worker entrypoints from
* `scripts/ci-release-build-binaries.ts` but its inline comment falsely
* claimed the legacy-shim entrypoints were "still" listed — they had never
* been re-added. Released `omp-darwin-arm64` and `omp-linux-x64` shipped
* without the shims, breaking every legacy pi plugin install.
*
* Contract pinned here:
* - Both build scripts (release CI + local dev) feed the SAME entrypoint
* list to `bun build --compile`, sourced from `binary-entrypoints.ts`.
* - That list covers every shim referenced by `legacy-pi-compat.ts`.
*/
describe("legacy pi-compat --compile entrypoints (issue #3414)", () => {
const repoRoot = path.resolve(import.meta.dir, "../../../..");
const ciScriptPath = path.join(repoRoot, "scripts/ci-release-build-binaries.ts");
const devScriptPath = path.join(repoRoot, "packages/coding-agent/scripts/build-binary.ts");
const compatPath = path.join(repoRoot, "packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts");
it("declares every shim file legacy-pi-compat redirects to as a --compile entrypoint", () => {
// The shared constant must list every bunfs file the resolver hands back
// to Bun. Drop one here and the corresponding plugin import breaks at
// runtime with `Cannot find module '/$bunfs/root/...'`.
expect(LEGACY_COMPAT_BUILD_ENTRYPOINTS).toEqual([
"./packages/agent/src/index.ts",
"./packages/natives/native/index.js",
"./packages/tui/src/index.ts",
"./packages/utils/src/index.ts",
"./packages/coding-agent/src/extensibility/typebox.ts",
"./packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts",
"./packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts",
]);
});
it("release and dev build scripts both source entrypoints from binary-entrypoints.ts", async () => {
const releaseSource = await Bun.file(ciScriptPath).text();
const devSource = await Bun.file(devScriptPath).text();
// Both scripts import the shared constant rather than inlining their
// own copy, so the two halves of the contract cannot drift apart.
expect(releaseSource).toContain("import { LEGACY_COMPAT_BUILD_ENTRYPOINTS } from");
expect(releaseSource).toContain("binary-entrypoints");
expect(releaseSource).toContain("...LEGACY_COMPAT_BUILD_ENTRYPOINTS");
expect(devSource).toContain("import { LEGACY_COMPAT_BUILD_ENTRYPOINTS } from");
expect(devSource).toContain("binary-entrypoints");
expect(devSource).toContain("...LEGACY_COMPAT_BUILD_ENTRYPOINTS");
});
it("each entrypoint actually exists on disk under the repo root", async () => {
// `--compile` silently swallows a missing entrypoint when run without
// `--root .` mismatches, then drops the file from bunfs and ships a
// broken binary. Make every path a real file so the build fails loudly
// if the constant ever lists a stale path.
for (const relative of LEGACY_COMPAT_BUILD_ENTRYPOINTS) {
const abs = path.join(repoRoot, relative.replace(/^\.\//, ""));
expect(await Bun.file(abs).exists()).toBe(true);
}
});
it("legacy-pi-compat.ts only redirects to shims declared in the entrypoint list", async () => {
// Every `bunfsPath("<pkg>", "<entry>", ..., "<file>.js")` and
// `bunfsPath("coding-agent", "src", "extensibility", "<shim>.js")` in
// the compat module must have a corresponding `.ts` (or `.js`) entry
// in `LEGACY_COMPAT_BUILD_ENTRYPOINTS`. Otherwise a plugin import goes
// to a bunfs file that wasn't compiled in.
const compatSource = await Bun.file(compatPath).text();
const bunfsPathPattern = /bunfsPath\(([^)]+)\)/g;
const segmentsList = [...compatSource.matchAll(bunfsPathPattern)]
.map(match => match[1])
.filter((segment): segment is string => typeof segment === "string");
expect(segmentsList.length).toBeGreaterThan(0);
for (const segmentsExpr of segmentsList) {
const segments = [...segmentsExpr.matchAll(/"([^"]+)"/g)]
.map(match => match[1])
.filter((seg): seg is string => typeof seg === "string");
if (segments.length === 0) continue;
// Convert the bunfs path tail (e.g. `tui/src/index.js`) to the
// matching repo-root build entrypoint (`./packages/tui/src/index.{ts,js}`).
const bunfsTail = segments.join("/");
const matched = LEGACY_COMPAT_BUILD_ENTRYPOINTS.some(entry => {
const stripped = entry.replace(/^\.\/packages\//, "").replace(/\.(ts|js)$/, "");
const bunfsExpected = bunfsTail.replace(/\.(js|ts)$/, "");
return stripped === bunfsExpected;
});
expect(matched, `bunfsPath(${segmentsExpr}) has no matching --compile entrypoint`).toBe(true);
}
});
});
@@ -1,5 +1,8 @@
import { describe, expect, it } from "bun:test";
import { __validateLegacyPiPackageRootOverrides } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
import {
__resolveTypeBoxShimPath,
__validateLegacyPiPackageRootOverrides,
} from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
// Regression for issue #2168: in compiled-binary mode the package-root
// override branch of `resolveCanonicalPiSpecifier` returned a bunfs path
@@ -10,8 +13,14 @@ import { __validateLegacyPiPackageRootOverrides } from "@oh-my-pi/pi-coding-agen
// `getResolvedSpecifier` path. The fix validates each override at module
// init so missing entries fall through to canonical resolution and Bun
// resolves the import from the extension's own `node_modules`.
//
// Follow-up (issue #3423): on Bun 1.3.14 the compiled binary's
// `/$bunfs/...` paths are unreachable via every filesystem API, so
// compiled-binary mode now routes through `omp-legacy-pi-bundled:` virtual
// specifiers instead. Those entries must always pass validation because
// the bundled registry — not the filesystem — is the source of truth.
describe("legacy pi compat package-root override validation (issue #2168)", () => {
it("keeps overrides whose targets exist", () => {
it("keeps overrides whose filesystem targets exist", () => {
const candidates = {
"@oh-my-pi/pi-ai": "/tmp/exists-ai.js",
"@oh-my-pi/pi-utils": "/tmp/exists-utils.js",
@@ -20,7 +29,7 @@ describe("legacy pi compat package-root override validation (issue #2168)", () =
expect(result).toEqual(candidates);
});
it("drops overrides whose targets are missing on disk", () => {
it("drops overrides whose filesystem targets are missing on disk", () => {
const candidates = {
"@oh-my-pi/pi-ai": "/tmp/exists-ai.js",
"@oh-my-pi/pi-coding-agent": "/tmp/exists-shim.js",
@@ -41,7 +50,7 @@ describe("legacy pi compat package-root override validation (issue #2168)", () =
expect(result).not.toHaveProperty("@oh-my-pi/pi-tui");
});
it("drops every override when none of the targets exist", () => {
it("drops every override when none of the filesystem targets exist", () => {
const candidates = {
"@oh-my-pi/pi-utils": "/$bunfs/root/packages/utils/src/index.js",
"@oh-my-pi/pi-tui": "/$bunfs/root/packages/tui/src/index.js",
@@ -49,4 +58,68 @@ describe("legacy pi compat package-root override validation (issue #2168)", () =
const result = __validateLegacyPiPackageRootOverrides(candidates, () => false);
expect(result).toEqual({});
});
it("keeps virtual omp-legacy-pi-bundled: entries without touching the filesystem (issue #3423)", () => {
// Bun 1.3.14 `fs.existsSync` returns false for every bunfs path, so the
// pre-#3423 fix dropped every override in compiled mode. The new
// virtual scheme is the source of truth in compiled-binary mode; the
// validator MUST short-circuit before any filesystem probe.
let probed = false;
const candidates = {
"@oh-my-pi/pi-ai": "omp-legacy-pi-bundled:@oh-my-pi/pi-ai",
"@oh-my-pi/pi-coding-agent": "omp-legacy-pi-bundled:@oh-my-pi/pi-coding-agent",
"@oh-my-pi/pi-agent-core": "omp-legacy-pi-bundled:@oh-my-pi/pi-agent-core",
"@oh-my-pi/pi-natives": "omp-legacy-pi-bundled:@oh-my-pi/pi-natives",
"@oh-my-pi/pi-tui": "omp-legacy-pi-bundled:@oh-my-pi/pi-tui",
"@oh-my-pi/pi-utils": "omp-legacy-pi-bundled:@oh-my-pi/pi-utils",
};
const result = __validateLegacyPiPackageRootOverrides(candidates, () => {
probed = true;
return false;
});
expect(result).toEqual(candidates);
expect(probed).toBe(false);
});
it("mixes virtual and filesystem entries: virtuals always pass, filesystems gated", () => {
const candidates = {
"@oh-my-pi/pi-ai": "omp-legacy-pi-bundled:@oh-my-pi/pi-ai",
"@oh-my-pi/pi-coding-agent": "/dev/source/legacy-pi-coding-agent-shim.ts",
"@oh-my-pi/pi-tui": "/missing/path.ts",
};
const missing = new Set(["/missing/path.ts"]);
const result = __validateLegacyPiPackageRootOverrides(candidates, p => !missing.has(p));
expect(result).toEqual({
"@oh-my-pi/pi-ai": "omp-legacy-pi-bundled:@oh-my-pi/pi-ai",
"@oh-my-pi/pi-coding-agent": "/dev/source/legacy-pi-coding-agent-shim.ts",
});
});
});
// Regression for the merge of issue #3414 (typebox shim fall-through) and
// issue #3423 (compiled-binary virtual namespace). `__resolveTypeBoxShimPath`
// must serve the virtual specifier in compiled mode without probing the FS,
// and in dev/install mode drop the shim to `null` when the source file is
// missing so `resolveTypeBoxSpecifier` returns `undefined` and bare
// `typebox` / `@sinclair/typebox` imports fall through to native resolution.
describe("legacy pi compat typebox shim path resolution (issues #3414, #3423)", () => {
it("returns the virtual specifier in compiled-binary mode without touching the filesystem", () => {
let probed = false;
const result = __resolveTypeBoxShimPath(true, "/ignored/source/typebox.ts", () => {
probed = true;
return false;
});
expect(result).toBe("omp-legacy-pi-bundled:typebox");
expect(probed).toBe(false);
});
it("returns the on-disk source path in dev mode when the shim file exists", () => {
const result = __resolveTypeBoxShimPath(false, "/dev/src/typebox.ts", () => true);
expect(result).toBe("/dev/src/typebox.ts");
});
it("drops the shim to null in dev mode when the source file is missing (issue #3414)", () => {
const result = __resolveTypeBoxShimPath(false, "/dev/src/typebox.ts", () => false);
expect(result).toBeNull();
});
});
@@ -1,74 +0,0 @@
import { describe, expect, it } from "bun:test";
import * as path from "node:path";
import { __resolveTypeBoxShimPath } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
/**
* Defensive tier for issue #3414.
*
* Even with the entrypoint list pinned in `binary-entrypoints.ts`, a future
* Bun `--compile` regression (cf. #2168) can silently drop the typebox shim
* file from bunfs. Without runtime validation the resolver would emit a
* `file://` URL to a path that no longer exists and plugin loads fail with
* `Cannot find module '/$bunfs/root/.../typebox.js'`.
*
* `__resolveTypeBoxShimPath` mirrors the `__validateLegacyPiPackageRootOverrides`
* fallback already in place for the legacy package-root overrides: if the
* computed candidate is missing, return `null` so the rewriter leaves bare
* `typebox` / `@sinclair/typebox` specifiers alone and Bun resolves a real
* install from the extension's own `node_modules`.
*/
describe("legacy pi-compat typebox shim path validation (issue #3414)", () => {
const stubMetaDir = "/repo/packages/coding-agent/src/extensibility/plugins";
it("returns the bunfs candidate when --compile bundled the shim", () => {
const result = __resolveTypeBoxShimPath(
"/$bunfs/root/packages",
undefined,
"/$bunfs/root",
() => true,
path.posix,
);
expect(result).toBe("/$bunfs/root/packages/coding-agent/src/extensibility/typebox.js");
});
it("drops the bunfs candidate when --compile silently omitted the shim", () => {
const result = __resolveTypeBoxShimPath(
"/$bunfs/root/packages",
undefined,
"/$bunfs/root",
() => false,
path.posix,
);
expect(result).toBeNull();
});
it("preserves the double-slash bunfs prefix on cross-compiled darwin-arm64 (issue #3329)", () => {
// `BUNFS_PACKAGE_ROOT` on the cross-compiled release is `//root/packages`.
// The shim path must keep that exact double-slash mount prefix so Bun's
// bunfs lookup matches.
const result = __resolveTypeBoxShimPath(
"//root/packages",
undefined,
"//root/omp-darwin-arm64",
() => true,
path.posix,
);
expect(result).toBe("//root/packages/coding-agent/src/extensibility/typebox.js");
});
it("returns the npm prebuilt dist path when PI_BUNDLED is set", () => {
const bundledRoot = "/home/me/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent";
const result = __resolveTypeBoxShimPath(null, bundledRoot, stubMetaDir, () => true, path.posix);
expect(result).toBe(`${bundledRoot}/src/extensibility/typebox.ts`);
});
it("returns the dev source path when neither bunfs nor bundled is set", () => {
const result = __resolveTypeBoxShimPath(null, undefined, stubMetaDir, () => true, path.posix);
expect(result).toBe("/repo/packages/coding-agent/src/extensibility/typebox.ts");
});
it("returns null when the dev source path is missing too", () => {
const result = __resolveTypeBoxShimPath(null, undefined, stubMetaDir, () => false, path.posix);
expect(result).toBeNull();
});
});
-30
View File
@@ -1,30 +0,0 @@
/**
* Extra `bun build --compile` entrypoints that must land in bunfs at
* `/$bunfs/root/packages/<pkg>/...`.
*
* `legacy-pi-compat.ts` redirects bare TypeBox imports and the legacy
* `@(scope)/pi-*` package roots onto computed bunfs paths — Bun's `--compile`
* static analyzer cannot trace those, so each shim and bundled package barrel
* must be passed as an explicit additional entrypoint or the file is silently
* omitted from bunfs and plugin loads fail with `Cannot find module
* '/$bunfs/root/...'` (issue #3414).
*
* The coding-agent's own `./src/index.ts` is intentionally NOT listed: bun
* --compile silently breaks the CLI entry when the same package's barrel
* appears as an extra entrypoint (issue #1474), so legacy `pi-coding-agent`
* imports resolve through `legacy-pi-coding-agent-shim.ts` instead.
*
* Repo-root-relative — both `scripts/ci-release-build-binaries.ts` (release)
* and `packages/coding-agent/scripts/build-binary.ts` (dev) invoke
* `bun build --compile --root .` so the paths land at the same bunfs
* locations the runtime computes.
*/
export const LEGACY_COMPAT_BUILD_ENTRYPOINTS: readonly string[] = [
"./packages/agent/src/index.ts",
"./packages/natives/native/index.js",
"./packages/tui/src/index.ts",
"./packages/utils/src/index.ts",
"./packages/coding-agent/src/extensibility/typebox.ts",
"./packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts",
"./packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts",
];
+6 -11
View File
@@ -2,7 +2,6 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { LEGACY_COMPAT_BUILD_ENTRYPOINTS } from "./binary-entrypoints";
interface BinaryTarget {
id: string;
@@ -15,14 +14,11 @@ interface BinaryTarget {
const repoRoot = path.join(import.meta.dir, "..");
const binariesDir = path.join(repoRoot, "packages", "coding-agent", "binaries");
const entrypoint = "./packages/coding-agent/src/cli.ts";
// Legacy extension shims and package barrels reached via computed bunfs paths.
// Declared in `binary-entrypoints.ts` so the dev build script
// (`packages/coding-agent/scripts/build-binary.ts`) and this release script
// stay in sync — drifting them silently strips the shims from the release
// `omp-<platform>` assets and breaks `omp install` for legacy pi extensions
// (issue #3414). Worker threads spawn `new Worker(Bun.main, { argv })` — they
// re-enter the binary's own entry module — so no separate worker modules
// are compiled.
// Worker threads spawn `new Worker(Bun.main, { argv })` — they re-enter the
// binary's own entry module — so no separate worker modules are compiled.
// Legacy pi-* extension compat surfaces are served through an in-process
// virtual namespace (`legacy-pi-compat.ts`), reached via the main module
// graph, so no extra `--compile` entrypoints are required (issue #3423).
const isDryRun = process.argv.includes("--dry-run");
const targets: BinaryTarget[] = [
{
@@ -115,7 +111,7 @@ async function buildBinary(target: BinaryTarget): Promise<void> {
console.log(`Building ${target.outfile}...`);
await embedNative(target);
if (isDryRun) {
console.log(`DRY RUN bun build --compile --no-compile-autoload-bunfig --no-compile-autoload-dotenv --no-compile-autoload-tsconfig --no-compile-autoload-package-json --minify-identifiers --keep-names --define process.env.PI_COMPILED="true" --root . --target=${target.target} ${entrypoint} ${LEGACY_COMPAT_BUILD_ENTRYPOINTS.join(" ")} --outfile ${target.outfile}`);
console.log(`DRY RUN bun build --compile --no-compile-autoload-bunfig --no-compile-autoload-dotenv --no-compile-autoload-tsconfig --no-compile-autoload-package-json --minify-identifiers --keep-names --define process.env.PI_COMPILED="true" --root . --target=${target.target} ${entrypoint} --outfile ${target.outfile}`);
return;
}
@@ -140,7 +136,6 @@ async function buildBinary(target: BinaryTarget): Promise<void> {
"--target",
target.target,
entrypoint,
...LEGACY_COMPAT_BUILD_ENTRYPOINTS,
"--outfile",
target.outfile,
],