fix(coding-agent): served bundled pi-* through virtual ns on bun 1.3.14
Bun 1.3.14 stopped exposing `--compile` extras through every filesystem-style API: `fs.existsSync`, `Bun.file().exists()`, `Bun.resolveSync`, and `await import()` on `/$bunfs/...` or `file:///$bunfs/...` all fail; only `/$bunfs/root/<binary-name>` itself answers. The pre-existing legacy-pi rewrite emitted `file:///$bunfs/...` URLs that Bun then could not load, so every legacy extension that imported `@oh-my-pi/pi-*` or `@sinclair/typebox` failed on the `omp-darwin-arm64` release binary. `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` synthesizes a re-export module — no bunfs path ever leaves the module in compiled mode. Dev / source-link / installed-package modes keep the historical `file://` rewrite (source files exist on disk). The matching `--compile` extras in `scripts/build-binary.ts` are gone; `BUNFS_PACKAGE_ROOT`, `bunfsPath`, `__computeBunfsPackageRoot`, and `__joinBunfsPath` are deleted as dead code. `scripts/smoke-3423.ts` compiles a tiny binary that loads a fixture extension end-to-end through the new path. Fixes #3423
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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` and the dead `BUNFS_PACKAGE_ROOT` / `bunfsPath` / `__computeBunfsPackageRoot` / `__joinBunfsPath` helpers are gone, and `scripts/smoke-3423.ts` compiles a tiny binary that loads a fixture extension end-to-end through the new path. ([#3423](https://github.com/can1357/oh-my-pi/issues/3423))
|
||||
|
||||
## [16.1.17] - 2026-06-24
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -82,22 +82,15 @@ async function main(): Promise<void> {
|
||||
"--root",
|
||||
".",
|
||||
"./packages/coding-agent/src/cli.ts",
|
||||
// Legacy pi-* extension compat entrypoints served by
|
||||
// `legacy-pi-compat.ts`. These are reached via computed bunfs paths
|
||||
// (which `--compile`'s static analyzer cannot trace), so each must be
|
||||
// listed here to land in bunfs at
|
||||
// `/$bunfs/root/packages/<pkg>/<entry>.js`. 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.
|
||||
"./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",
|
||||
// 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);
|
||||
@@ -189,8 +267,8 @@ function sourceShimPath(file: string): string {
|
||||
: path.resolve(import.meta.dir, "..", file);
|
||||
}
|
||||
|
||||
const TYPEBOX_SHIM_PATH = BUNFS_PACKAGE_ROOT
|
||||
? bunfsPath("coding-agent", "src", "extensibility", "typebox.js")
|
||||
const TYPEBOX_SHIM_PATH = IS_COMPILED_BINARY
|
||||
? bundledRegistryVirtualSpecifier(TYPEBOX_BUNDLED_REGISTRY_KEY)
|
||||
: sourceShimPath("typebox.ts");
|
||||
|
||||
// Legacy extensions historically imported `Type` (and `Static`/`TSchema`) from
|
||||
@@ -201,59 +279,67 @@ const TYPEBOX_SHIM_PATH = BUNFS_PACKAGE_ROOT
|
||||
// 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`),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -302,6 +388,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;
|
||||
}
|
||||
|
||||
@@ -734,6 +826,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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,8 +10,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 +26,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 +47,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 +55,40 @@ 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Issue #3423 end-to-end smoke driver. Compiles the legacy-pi compat path
|
||||
* into a tiny binary, runs it against a fixture extension, asserts the
|
||||
* extension's `@(scope)/pi-*` and `@sinclair/typebox` imports all resolve
|
||||
* through the bundled-virtual loader rather than the now-unreachable
|
||||
* `/$bunfs/...` filesystem paths.
|
||||
*
|
||||
* Usage: `bun scripts/smoke-3423.ts` from the repo root.
|
||||
*/
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dir, "..");
|
||||
const driverFile = path.join(repoRoot, ".omp-smoke-3423", "main.ts");
|
||||
const outFile = path.join(repoRoot, ".omp-smoke-3423", "smoke");
|
||||
|
||||
await fs.mkdir(path.dirname(driverFile), { recursive: true });
|
||||
|
||||
// `legacy-pi-bundled-registry` cascades through the coding-agent root which
|
||||
// pulls in `export/html/tool-views.generated.js` (a build artifact). The
|
||||
// full release build runs `packages/collab-web/scripts/build-tool-views.ts`
|
||||
// to produce it; for the smoke we stub an empty file so the bundler can
|
||||
// resolve the `with { type: "text" }` import.
|
||||
const toolViewsPlaceholder = path.join(
|
||||
repoRoot,
|
||||
"packages",
|
||||
"coding-agent",
|
||||
"src",
|
||||
"export",
|
||||
"html",
|
||||
"tool-views.generated.js",
|
||||
);
|
||||
const hadToolViews = await Bun.file(toolViewsPlaceholder)
|
||||
.exists()
|
||||
.catch(() => false);
|
||||
if (!hadToolViews) {
|
||||
await Bun.write(toolViewsPlaceholder, "// smoke-3423 placeholder\n");
|
||||
}
|
||||
|
||||
// pi-natives ships a platform-specific `.node` addon that lives outside the
|
||||
// JS bundle; `embed:native` copies it next to the source so the bundler can
|
||||
// pick it up. Without this step the compiled binary crashes at startup
|
||||
// before the legacy-pi shim path is even exercised.
|
||||
const embed = Bun.spawnSync(["bun", "--cwd=../natives", "run", "embed:native"], {
|
||||
cwd: path.join(repoRoot, "packages", "coding-agent"),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
if (embed.exitCode !== 0) {
|
||||
throw new Error(`embed:native failed with exit code ${embed.exitCode}`);
|
||||
}
|
||||
|
||||
const driver = `import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
installLegacyPiSpecifierShim,
|
||||
loadLegacyPiModule,
|
||||
} from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
|
||||
|
||||
installLegacyPiSpecifierShim();
|
||||
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-3423-ext-"));
|
||||
await fs.writeFile(
|
||||
path.join(dir, "package.json"),
|
||||
JSON.stringify({ name: "legacy-3423-ext", version: "1.0.0" }),
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dir, "index.ts"),
|
||||
[
|
||||
'import { VERSION, defineTool, Type } from "@earendil-works/pi-coding-agent";',
|
||||
'import { z } from "@mariozechner/pi-ai";',
|
||||
'import { Type as TbxType } from "@sinclair/typebox";',
|
||||
'import { logger } from "@oh-my-pi/pi-utils";',
|
||||
"const sample = defineTool({",
|
||||
' name: "smoke",',
|
||||
' label: "smoke",',
|
||||
' description: "issue 3423 smoke",',
|
||||
" parameters: Type.Object({}),",
|
||||
' execute: async () => ({ content: [{ type: "text", text: "ok" }] }),',
|
||||
"});",
|
||||
"export const probe = {",
|
||||
" piCodingAgentVersion: VERSION,",
|
||||
" zIsFunction: typeof z?.object === 'function',",
|
||||
" tbxTypeIsObject: typeof TbxType === 'object',",
|
||||
" loggerIsCallable: typeof logger?.info === 'function',",
|
||||
" defineToolReturned: sample.name,",
|
||||
"};",
|
||||
].join("\\n"),
|
||||
);
|
||||
|
||||
const mod = await loadLegacyPiModule(path.join(dir, "index.ts"));
|
||||
console.log("PROBE", JSON.stringify(mod.probe));
|
||||
console.log("OK");
|
||||
`;
|
||||
|
||||
await fs.writeFile(driverFile, driver);
|
||||
|
||||
const build = Bun.spawnSync(
|
||||
[
|
||||
"bun",
|
||||
"build",
|
||||
"--compile",
|
||||
"--no-compile-autoload-bunfig",
|
||||
"--no-compile-autoload-dotenv",
|
||||
"--no-compile-autoload-tsconfig",
|
||||
"--no-compile-autoload-package-json",
|
||||
"--keep-names",
|
||||
"--define",
|
||||
'process.env.PI_COMPILED="true"',
|
||||
"--external",
|
||||
"fastembed",
|
||||
"--external",
|
||||
"onnxruntime-node",
|
||||
"--root",
|
||||
".",
|
||||
`./${path.relative(repoRoot, driverFile)}`,
|
||||
"--outfile",
|
||||
outFile,
|
||||
],
|
||||
{ cwd: repoRoot, stdout: "inherit", stderr: "inherit" },
|
||||
);
|
||||
if (build.exitCode !== 0) {
|
||||
throw new Error(`bun build --compile failed with exit code ${build.exitCode}`);
|
||||
}
|
||||
|
||||
const run = Bun.spawnSync([outFile], { cwd: repoRoot, stdout: "pipe", stderr: "pipe" });
|
||||
const stdout = run.stdout.toString();
|
||||
const stderr = run.stderr.toString();
|
||||
console.log("--- driver stdout ---");
|
||||
console.log(stdout);
|
||||
if (stderr) {
|
||||
console.log("--- driver stderr ---");
|
||||
console.log(stderr);
|
||||
}
|
||||
if (run.exitCode !== 0) {
|
||||
throw new Error(`compiled smoke binary exited with code ${run.exitCode}`);
|
||||
}
|
||||
|
||||
if (!stdout.includes("OK")) {
|
||||
throw new Error("driver did not print OK");
|
||||
}
|
||||
const probeMatch = stdout.match(/PROBE (\{.*\})/);
|
||||
if (!probeMatch) {
|
||||
throw new Error("driver did not print PROBE payload");
|
||||
}
|
||||
const probe = JSON.parse(probeMatch[1]) as {
|
||||
piCodingAgentVersion: string;
|
||||
zIsFunction: boolean;
|
||||
tbxTypeIsObject: boolean;
|
||||
loggerIsCallable: boolean;
|
||||
defineToolReturned: string;
|
||||
};
|
||||
if (!probe.piCodingAgentVersion || !/^\d+\.\d+\.\d+/.test(probe.piCodingAgentVersion)) {
|
||||
throw new Error(`pi-coding-agent VERSION not exposed: ${probe.piCodingAgentVersion}`);
|
||||
}
|
||||
if (!probe.zIsFunction) {
|
||||
throw new Error("pi-ai z.object missing — shim did not re-export canonical surface");
|
||||
}
|
||||
if (!probe.tbxTypeIsObject) {
|
||||
throw new Error("@sinclair/typebox Type missing — TypeBox shim did not load");
|
||||
}
|
||||
if (!probe.loggerIsCallable) {
|
||||
throw new Error("pi-utils logger.info missing — canonical bundled package not reachable");
|
||||
}
|
||||
if (probe.defineToolReturned !== "smoke") {
|
||||
throw new Error(`defineTool helper did not return the marked tool: ${probe.defineToolReturned}`);
|
||||
}
|
||||
console.log("issue #3423 smoke passed");
|
||||
|
||||
await fs.rm(path.dirname(driverFile), { recursive: true, force: true });
|
||||
if (!hadToolViews) {
|
||||
await fs.rm(toolViewsPlaceholder, { force: true });
|
||||
}
|
||||
Bun.spawnSync(["bun", "--cwd=../natives", "run", "embed:native", "--reset"], {
|
||||
cwd: path.join(repoRoot, "packages", "coding-agent"),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
Reference in New Issue
Block a user