diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3d1ff6105..c722ba98a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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/` 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 diff --git a/packages/coding-agent/scripts/build-binary.ts b/packages/coding-agent/scripts/build-binary.ts index b90498214..734aa1762 100644 --- a/packages/coding-agent/scripts/build-binary.ts +++ b/packages/coding-agent/scripts/build-binary.ts @@ -82,22 +82,15 @@ async function main(): Promise { "--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//.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}`, ], diff --git a/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-registry.ts b/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-registry.ts new file mode 100644 index 000000000..699a85dfd --- /dev/null +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-registry.ts @@ -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>>> = { + "@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, +}; diff --git a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts index bc69f2b43..e3e188612 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts @@ -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/`. 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:` 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>>>; + +let bundledRegistryPromise: Promise | 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 { + 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)[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:` + * 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:` 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 { + 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 { + 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>>>, +): 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 { + 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 `:\~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 `:\~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/` - * on POSIX or `:\~BUN\root\.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 - * `/packages`. - * - the module's own source directory if a future Bun release switches to - * module-specific `import.meta.dir` values: - * `/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:` 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, pathExistsSync: (p: string) => boolean = fs.existsSync, ): Record { - 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:` 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" }; + }); }, }); } diff --git a/packages/coding-agent/test/extensibility/legacy-pi-bundled-virtual.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-bundled-virtual.test.ts new file mode 100644 index 000000000..0fece9ee8 --- /dev/null +++ b/packages/coding-agent/test/extensibility/legacy-pi-bundled-virtual.test.ts @@ -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)[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; + expect(live.VERSION).toBe("16.1.17"); + expect(typeof live.defineTool).toBe("function"); + expect(typeof live.Type).toBe("object"); + } finally { + delete (globalThis as Record)[globalKey]; + } + }); +}); diff --git a/packages/coding-agent/test/extensibility/legacy-pi-bunfs-root.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-bunfs-root.test.ts index b9a4af64d..04080d93c 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-bunfs-root.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-bunfs-root.test.ts @@ -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 `:\~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 `:\~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"); }); }); diff --git a/packages/coding-agent/test/extensibility/legacy-pi-override-fallback.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-override-fallback.test.ts index 372ed6e25..bcf12ca46 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-override-fallback.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-override-fallback.test.ts @@ -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", + }); + }); }); diff --git a/scripts/smoke-3423.ts b/scripts/smoke-3423.ts new file mode 100755 index 000000000..6b7c0d32f --- /dev/null +++ b/scripts/smoke-3423.ts @@ -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", +});