fix(plugins): route bundled pi-* subpath imports through the virtual registry

Compiled-binary extension validation rewrote @(scope)/pi-ai/oauth →
@oh-my-pi/pi-ai/oauth, but LEGACY_PI_PACKAGE_ROOT_OVERRIDES only
covered bare package roots. resolveCanonicalPiSpecifier therefore
fell through to Bun.resolveSync, which fails inside bunfs on Bun
1.3.14+, then the rewriteLegacyPiImports catch left the original
specifier alone. Bun's native resolver then failed because most
plugins (e.g. @charmland/pi-hyper-provider) declare @(scope)/pi-ai
as a peerDependency only and never materialize a real install.

A new scripts/generate-legacy-pi-bundled-registry.ts reads every
bundled pi-* package's non-wildcard exports field and emits both
the heavy legacy-pi-bundled-registry.ts (static imports + map) and
a light legacy-pi-bundled-keys.ts. legacy-pi-compat.ts statically
imports the keys file to seed the override map without paying the
legacy-pi-coding-agent-shim → ../index → export/html/... cascade,
so subpath imports now route to the same omp-legacy-pi-bundled:
virtual namespace that already serves the roots.
scripts/build-binary.ts runs the generator before bun build
--compile so new pi-* subpaths added under packages/*/package.json
ship without manual regeneration; --check verifies the committed
output stays in sync.

Fixes #3442
This commit is contained in:
roboomp
2026-06-25 05:52:38 +00:00
parent b2b818af6f
commit aa29b79642
7 changed files with 669 additions and 32 deletions
+1
View File
@@ -4,6 +4,7 @@
### Fixed
- Fixed `omp install <plugin>` failing extension validation in compiled-binary mode with `Cannot find module '@(scope)/pi-ai/oauth' from '<plugin>/src/oauth.ts'` (and any other non-wildcard pi-* subpath import like `@oh-my-pi/pi-coding-agent/tools`). The bundled-registry override map seeded by `__buildLegacyPiPackageRootOverrides` only covered the bare package roots, so `rewriteLegacyPiImports` rewrote `@(scope)/pi-ai/oauth` to `@oh-my-pi/pi-ai/oauth`, fell through to `Bun.resolveSync` (which bunfs can't satisfy on Bun 1.3.14+), then left the original specifier alone — at which point Bun's native resolver failed because most plugins declare `@(scope)/pi-ai` as a `peerDependency` only and never materialize a real install. The new `scripts/generate-legacy-pi-bundled-registry.ts` reads every bundled pi-* package's non-wildcard `exports` field and emits both the heavy `legacy-pi-bundled-registry.ts` (static imports + map) and a light `legacy-pi-bundled-keys.ts` (statically imported by `legacy-pi-compat.ts` to seed the override map without the cascade through `legacy-pi-coding-agent-shim → ../index → export/html/...`). `scripts/build-binary.ts` now runs the generator before `bun build --compile`. ([#3442](https://github.com/can1357/oh-my-pi/issues/3442))
- Fixed `skill://` tool resolution losing loaded session skills when a tool runs outside the session-initialization module state. Internal URL resolution now prefers the caller's `session.skills` snapshot before falling back to the process-global skill list, so `read skill://<name>` works across tool execution boundaries. ([#3436](https://github.com/can1357/oh-my-pi/issues/3436))
## [16.1.18] - 2026-06-25
@@ -58,6 +58,15 @@ async function main(): Promise<void> {
: Bun.env,
);
await runCommand(["bun", "scripts/embed-mupdf-wasm.ts", "--generate"]);
// Regenerate the bundled-pi registry + key set before the compile so any
// new pi-* subpath export added under `packages/*/package.json` is served
// from the host's in-process copy. Without this, `bun build --compile`
// would freeze whatever the committed registry happened to enumerate at
// the time of the last manual `--generate`, and a new subpath added
// since then would crash extension validation with `Cannot find module`
// (issue #3442). The generator also normalizes formatting, so the diff
// against the committed copy stays clean.
await runCommand(["bun", "scripts/generate-legacy-pi-bundled-registry.ts", "--generate"]);
try {
const buildEnv = shouldAdhocSignDarwinBinary() ? { ...Bun.env, BUN_NO_CODESIGN_MACHO_BINARY: "1" } : Bun.env;
await runCommand(
@@ -0,0 +1,308 @@
#!/usr/bin/env bun
/**
* Generate the bundled-pi registry + key set served by the compiled binary's
* `omp-legacy-pi-bundled:` virtual namespace.
*
* Compiled-mode extension validation routes every `@(scope)/pi-*` import
* through this registry — bunfs filesystem APIs are unreachable on Bun 1.3.14+
* (issue #3423), so the binary serves bundled module surfaces from JS-heap
* references captured at build time. Bare package roots and every
* `non-wildcard` subpath export declared in each bundled pi-* package.json
* become a registry entry; wildcard subpath patterns are intentionally
* unbundled — those resolve from the extension's own peer deps as before.
*
* The generator emits two files:
* - `legacy-pi-bundled-registry.ts` (heavy): static imports of every
* subpath module + the `BUNDLED_PI_REGISTRY` map. Dynamically loaded by
* `legacy-pi-compat.ts` so dev/test runs never pay the cascade.
* - `legacy-pi-bundled-keys.ts` (light): just the canonical-key set.
* Statically imported by `legacy-pi-compat.ts` to seed
* `LEGACY_PI_PACKAGE_ROOT_OVERRIDES` without touching the heavy graph.
*
* Run via `bun scripts/generate-legacy-pi-bundled-registry.ts --generate`
* (also invoked from `scripts/build-binary.ts` before `bun build --compile`).
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
const packageDir = path.resolve(import.meta.dir, "..");
const repoRoot = path.resolve(packageDir, "..", "..");
const registryOutPath = path.join(packageDir, "src/extensibility/plugins/legacy-pi-bundled-registry.ts");
const keysOutPath = path.join(packageDir, "src/extensibility/plugins/legacy-pi-bundled-keys.ts");
const GENERATE_FLAG = "--generate";
const CHECK_FLAG = "--check";
interface BundledPackage {
readonly dir: string;
readonly name: string;
/** Identifier prefix for generated namespace imports (`PiAi`, `PiCodingAgent`, …). */
readonly identifier: string;
/** Root import — the shim path for surfaces that wrap the bundled namespace, `null` otherwise. */
readonly rootShim: string | null;
}
const PACKAGES: readonly BundledPackage[] = [
{ dir: "packages/agent", name: "@oh-my-pi/pi-agent-core", identifier: "PiAgentCore", rootShim: null },
{
dir: "packages/ai",
name: "@oh-my-pi/pi-ai",
identifier: "PiAi",
// pi-ai 15.1.0 dropped the runtime `Type` builder from the package root;
// the shim re-attaches it for extensions that still import `Type` from
// `@(scope)/pi-ai`. Subpaths bypass the shim — they're untouched by the
// schema-runtime split.
rootShim: "../legacy-pi-ai-shim",
},
{
dir: "packages/coding-agent",
name: "@oh-my-pi/pi-coding-agent",
identifier: "PiCodingAgent",
// pi-coding-agent root carries legacy helpers (`defineTool`,
// `createCodingTools`, …) the canonical entry never exposed; the shim
// re-exports the canonical surface plus those helpers.
rootShim: "../legacy-pi-coding-agent-shim",
},
{ dir: "packages/natives", name: "@oh-my-pi/pi-natives", identifier: "PiNatives", rootShim: null },
{ dir: "packages/tui", name: "@oh-my-pi/pi-tui", identifier: "PiTui", rootShim: null },
{ dir: "packages/utils", name: "@oh-my-pi/pi-utils", identifier: "PiUtils", rootShim: null },
];
// `typebox` is published under an upstream alias; legacy extensions import the
// bare name expecting the host-provided Zod-backed shim. Tracked alongside the
// pi-* surfaces so the override map and synthesizer cover it uniformly.
const TYPEBOX_REGISTRY_KEY = "typebox";
const TYPEBOX_SHIM_IMPORT = "../typebox";
interface RegistryEntry {
/** Canonical registry key, e.g. `@oh-my-pi/pi-ai/oauth`. */
readonly key: string;
/** Identifier bound in the generated module's static import. */
readonly binding: string;
/**
* ES module specifier the generated file imports. For bundled subpaths this
* is the canonical `@oh-my-pi/<pkg>/<subpath>` string Bun resolves via the
* package's exports field; for shimmed surfaces (root of pi-ai / pi-coding-agent
* / typebox) it's the relative path to the in-tree shim.
*/
readonly importSpecifier: string;
}
function bindingForSubpath(identifier: string, subpath: string): string {
const segments = subpath
.split("/")
.filter(Boolean)
.map(segment =>
segment
.split(/[-_]/)
.filter(Boolean)
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(""),
);
return `bundled${identifier}${segments.join("")}`;
}
async function collectEntries(): Promise<RegistryEntry[]> {
const entries: RegistryEntry[] = [];
for (const pkg of PACKAGES) {
const manifestPath = path.join(repoRoot, pkg.dir, "package.json");
const manifest = (await Bun.file(manifestPath).json()) as { name?: string; exports?: Record<string, unknown> };
if (manifest.name !== pkg.name) {
throw new Error(
`generate-legacy-pi-bundled-registry: package.json at ${manifestPath} declares "${manifest.name}", expected "${pkg.name}"`,
);
}
const exportsField = manifest.exports ?? {};
// Root: shim if one is declared, otherwise the canonical package.
entries.push({
key: pkg.name,
binding: `bundled${pkg.identifier}`,
importSpecifier: pkg.rootShim ?? pkg.name,
});
// Each non-wildcard subpath export becomes its own registry key.
for (const exportKey in exportsField) {
if (!exportKey.startsWith("./") || exportKey === "." || exportKey.includes("*")) continue;
const subpath = exportKey.slice(2);
entries.push({
key: `${pkg.name}/${subpath}`,
binding: bindingForSubpath(pkg.identifier, subpath),
importSpecifier: `${pkg.name}/${subpath}`,
});
}
}
entries.push({
key: TYPEBOX_REGISTRY_KEY,
binding: "bundledTypeBoxShim",
importSpecifier: TYPEBOX_SHIM_IMPORT,
});
const seen = new Set<string>();
for (const entry of entries) {
if (seen.has(entry.key)) {
throw new Error(`generate-legacy-pi-bundled-registry: duplicate registry key ${entry.key}`);
}
seen.add(entry.key);
if (seen.has(entry.binding)) {
throw new Error(`generate-legacy-pi-bundled-registry: duplicate binding ${entry.binding}`);
}
seen.add(entry.binding);
}
return entries;
}
function renderRegistry(entries: readonly RegistryEntry[]): string {
const importLines = entries.map(
entry => `import * as ${entry.binding} from ${JSON.stringify(entry.importSpecifier)};`,
);
const registryLines = entries.map(
entry => `\t${JSON.stringify(entry.key)}: ${entry.binding} as unknown as Readonly<Record<string, unknown>>,`,
);
return [
"// AUTO-GENERATED by scripts/generate-legacy-pi-bundled-registry.ts.",
"// Do not edit by hand — run `bun scripts/generate-legacy-pi-bundled-registry.ts --generate`.",
"/**",
" * Static handles on every bundled `@oh-my-pi/pi-*` surface — package",
" * roots plus every non-wildcard subpath export declared in each package's",
" * `exports` field. 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 no `--compile` extras are required in `scripts/build-binary.ts`.",
" */",
...importLines,
"",
"/**",
" * 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. The companion",
" * `legacy-pi-bundled-keys.ts` mirrors `Object.keys(BUNDLED_PI_REGISTRY)` and",
" * is statically imported by `legacy-pi-compat.ts` to seed the override map",
" * without paying the cascade above.",
" */",
"export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {",
...registryLines,
"};",
"",
].join("\n");
}
function renderKeys(entries: readonly RegistryEntry[]): string {
const keyLines = entries.map(entry => `\t${JSON.stringify(entry.key)},`);
return [
"// AUTO-GENERATED by scripts/generate-legacy-pi-bundled-registry.ts.",
"// Do not edit by hand — run `bun scripts/generate-legacy-pi-bundled-registry.ts --generate`.",
"/**",
" * Canonical keys served by the `omp-legacy-pi-bundled:` virtual namespace.",
" * Mirrors `Object.keys(BUNDLED_PI_REGISTRY)` from",
" * `legacy-pi-bundled-keys.ts`'s sibling registry file. `legacy-pi-compat.ts`",
" * statically imports this set to seed `LEGACY_PI_PACKAGE_ROOT_OVERRIDES` in",
" * compiled-binary mode without dragging the heavy registry's transitive",
" * graph into dev/test runs (the registry itself stays behind a dynamic",
" * import — see `ensureBundledRegistryLoaded` in `legacy-pi-compat.ts`).",
" */",
"export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([",
...keyLines,
"]);",
"",
].join("\n");
}
async function formatInPlace(targets: readonly string[]): Promise<void> {
// `biome check --write` runs the formatter AND the assist's organizeImports
// pass; `biome format --write` alone leaves the import order untouched.
const proc = Bun.spawn(["bunx", "biome", "check", "--write", ...targets], {
cwd: packageDir,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const exit = await proc.exited;
if (exit !== 0) {
const stderr = await new Response(proc.stderr).text();
throw new Error(`biome check --write failed (exit ${exit}): ${stderr}`);
}
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
const check = args.includes(CHECK_FLAG);
if (!check && !args.includes(GENERATE_FLAG)) {
console.log(
`Skipping bundled-pi registry generation; pass ${GENERATE_FLAG} to write the output files (or ${CHECK_FLAG} to verify the committed copy).`,
);
return;
}
const entries = await collectEntries();
const registrySource = renderRegistry(entries);
const keysSource = renderKeys(entries);
if (check) {
// biome ignores paths outside its `includes` glob (the `*.ts.candidate`
// suffix above would be rejected with "No files were processed"), so
// write the candidates into a sibling `.<name>.tmp/` directory whose
// basenames match the committed copies. The directory lives under the
// same package so biome's repo-relative `includes` still cover it.
const tmpDir = path.join(packageDir, "src/extensibility/plugins/.legacy-pi-bundled-candidates");
const tmpRegistry = path.join(tmpDir, path.basename(registryOutPath));
const tmpKeys = path.join(tmpDir, path.basename(keysOutPath));
try {
await Bun.write(tmpRegistry, registrySource);
await Bun.write(tmpKeys, keysSource);
await formatInPlace([tmpRegistry, tmpKeys]);
const drift: string[] = [];
const pairs: readonly (readonly [string, string])[] = [
[registryOutPath, tmpRegistry],
[keysOutPath, tmpKeys],
];
for (const [committedPath, candidatePath] of pairs) {
let committed: string;
try {
committed = await Bun.file(committedPath).text();
} catch {
committed = "";
}
const candidate = await Bun.file(candidatePath).text();
if (committed !== candidate) {
drift.push(path.relative(repoRoot, committedPath));
}
}
if (drift.length > 0) {
console.error(
`generate-legacy-pi-bundled-registry: stale output — rerun with ${GENERATE_FLAG}. Files out of sync:\n ${drift.join("\n ")}`,
);
process.exit(1);
}
console.log("generate-legacy-pi-bundled-registry: OK");
} finally {
// `fs.rm` recursively handles both files and the parent tmp dir; the
// individual file deletes the previous draft used `Bun.file().delete()`
// for would leave the empty directory behind.
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
}
return;
}
await Bun.write(registryOutPath, registrySource);
await Bun.write(keysOutPath, keysSource);
// Hand-emitted formatting can't perfectly match biome's organizeImports +
// long-line wrapping (lineWidth 120 + 60+ entries with long keys/bindings),
// so let biome rewrite the files in place. The committed output then matches
// what `bun check` enforces, and `--check` confirms zero drift on CI.
await formatInPlace([registryOutPath, keysOutPath]);
console.log(
`Generated ${path.relative(repoRoot, registryOutPath)} and ${path.relative(repoRoot, keysOutPath)} (${entries.length} entries)`,
);
}
await main();
@@ -0,0 +1,76 @@
// AUTO-GENERATED by scripts/generate-legacy-pi-bundled-registry.ts.
// Do not edit by hand — run `bun scripts/generate-legacy-pi-bundled-registry.ts --generate`.
/**
* Canonical keys served by the `omp-legacy-pi-bundled:` virtual namespace.
* Mirrors `Object.keys(BUNDLED_PI_REGISTRY)` from
* `legacy-pi-bundled-keys.ts`'s sibling registry file. `legacy-pi-compat.ts`
* statically imports this set to seed `LEGACY_PI_PACKAGE_ROOT_OVERRIDES` in
* compiled-binary mode without dragging the heavy registry's transitive
* graph into dev/test runs (the registry itself stays behind a dynamic
* import — see `ensureBundledRegistryLoaded` in `legacy-pi-compat.ts`).
*/
export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
"@oh-my-pi/pi-agent-core",
"@oh-my-pi/pi-agent-core/compaction",
"@oh-my-pi/pi-ai",
"@oh-my-pi/pi-ai/auth-broker",
"@oh-my-pi/pi-ai/auth-gateway",
"@oh-my-pi/pi-ai/utils/harmony-leak",
"@oh-my-pi/pi-ai/dialect",
"@oh-my-pi/pi-ai/oauth",
"@oh-my-pi/pi-ai/registry",
"@oh-my-pi/pi-ai/registry/oauth",
"@oh-my-pi/pi-ai/utils/schema",
"@oh-my-pi/pi-coding-agent",
"@oh-my-pi/pi-coding-agent/async",
"@oh-my-pi/pi-coding-agent/autoresearch",
"@oh-my-pi/pi-coding-agent/capability",
"@oh-my-pi/pi-coding-agent/cli/gallery-fixtures",
"@oh-my-pi/pi-coding-agent/commit",
"@oh-my-pi/pi-coding-agent/commit/agentic",
"@oh-my-pi/pi-coding-agent/commit/agentic/tools",
"@oh-my-pi/pi-coding-agent/commit/analysis",
"@oh-my-pi/pi-coding-agent/commit/changelog",
"@oh-my-pi/pi-coding-agent/commit/map-reduce",
"@oh-my-pi/pi-coding-agent/dap",
"@oh-my-pi/pi-coding-agent/debug",
"@oh-my-pi/pi-coding-agent/discovery",
"@oh-my-pi/pi-coding-agent/edit",
"@oh-my-pi/pi-coding-agent/exa",
"@oh-my-pi/pi-coding-agent/export/html",
"@oh-my-pi/pi-coding-agent/extensibility/custom-commands",
"@oh-my-pi/pi-coding-agent/extensibility/custom-commands/bundled/ci-green",
"@oh-my-pi/pi-coding-agent/extensibility/custom-commands/bundled/review",
"@oh-my-pi/pi-coding-agent/extensibility/custom-tools",
"@oh-my-pi/pi-coding-agent/extensibility/extensions",
"@oh-my-pi/pi-coding-agent/extensibility/hooks",
"@oh-my-pi/pi-coding-agent/extensibility/plugins",
"@oh-my-pi/pi-coding-agent/extensibility/plugins/marketplace",
"@oh-my-pi/pi-coding-agent/internal-urls",
"@oh-my-pi/pi-coding-agent/eval",
"@oh-my-pi/pi-coding-agent/lsp",
"@oh-my-pi/pi-coding-agent/lsp/clients",
"@oh-my-pi/pi-coding-agent/mcp",
"@oh-my-pi/pi-coding-agent/mcp/transports",
"@oh-my-pi/pi-coding-agent/memories",
"@oh-my-pi/pi-coding-agent/memory-backend",
"@oh-my-pi/pi-coding-agent/hindsight",
"@oh-my-pi/pi-coding-agent/modes",
"@oh-my-pi/pi-coding-agent/modes/acp",
"@oh-my-pi/pi-coding-agent/modes/components",
"@oh-my-pi/pi-coding-agent/modes/components/extensions",
"@oh-my-pi/pi-coding-agent/modes/components/status-line",
"@oh-my-pi/pi-coding-agent/modes/setup-wizard",
"@oh-my-pi/pi-coding-agent/modes/theme/defaults",
"@oh-my-pi/pi-coding-agent/secrets",
"@oh-my-pi/pi-coding-agent/stt",
"@oh-my-pi/pi-coding-agent/task",
"@oh-my-pi/pi-coding-agent/tools",
"@oh-my-pi/pi-coding-agent/tui",
"@oh-my-pi/pi-coding-agent/web/scrapers",
"@oh-my-pi/pi-coding-agent/web/search",
"@oh-my-pi/pi-natives",
"@oh-my-pi/pi-tui",
"@oh-my-pi/pi-utils",
"typebox",
]);
@@ -1,10 +1,13 @@
// AUTO-GENERATED by scripts/generate-legacy-pi-bundled-registry.ts.
// Do not edit by hand — run `bun scripts/generate-legacy-pi-bundled-registry.ts --generate`.
/**
* 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.
* Static handles on every bundled `@oh-my-pi/pi-*` surface — package
* roots plus every non-wildcard subpath export declared in each package's
* `exports` field. 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
@@ -12,29 +15,191 @@
* 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`.
* so no `--compile` extras are required in `scripts/build-binary.ts`.
*/
import * as bundledPiAgentCore from "@oh-my-pi/pi-agent-core";
import * as bundledPiAgentCoreCompaction from "@oh-my-pi/pi-agent-core/compaction";
import * as bundledPiAiAuthBroker from "@oh-my-pi/pi-ai/auth-broker";
import * as bundledPiAiAuthGateway from "@oh-my-pi/pi-ai/auth-gateway";
import * as bundledPiAiDialect from "@oh-my-pi/pi-ai/dialect";
import * as bundledPiAiOauth from "@oh-my-pi/pi-ai/oauth";
import * as bundledPiAiRegistry from "@oh-my-pi/pi-ai/registry";
import * as bundledPiAiRegistryOauth from "@oh-my-pi/pi-ai/registry/oauth";
import * as bundledPiAiUtilsHarmonyLeak from "@oh-my-pi/pi-ai/utils/harmony-leak";
import * as bundledPiAiUtilsSchema from "@oh-my-pi/pi-ai/utils/schema";
import * as bundledPiCodingAgentAsync from "@oh-my-pi/pi-coding-agent/async";
import * as bundledPiCodingAgentAutoresearch from "@oh-my-pi/pi-coding-agent/autoresearch";
import * as bundledPiCodingAgentCapability from "@oh-my-pi/pi-coding-agent/capability";
import * as bundledPiCodingAgentCliGalleryFixtures from "@oh-my-pi/pi-coding-agent/cli/gallery-fixtures";
import * as bundledPiCodingAgentCommit from "@oh-my-pi/pi-coding-agent/commit";
import * as bundledPiCodingAgentCommitAgentic from "@oh-my-pi/pi-coding-agent/commit/agentic";
import * as bundledPiCodingAgentCommitAgenticTools from "@oh-my-pi/pi-coding-agent/commit/agentic/tools";
import * as bundledPiCodingAgentCommitAnalysis from "@oh-my-pi/pi-coding-agent/commit/analysis";
import * as bundledPiCodingAgentCommitChangelog from "@oh-my-pi/pi-coding-agent/commit/changelog";
import * as bundledPiCodingAgentCommitMapReduce from "@oh-my-pi/pi-coding-agent/commit/map-reduce";
import * as bundledPiCodingAgentDap from "@oh-my-pi/pi-coding-agent/dap";
import * as bundledPiCodingAgentDebug from "@oh-my-pi/pi-coding-agent/debug";
import * as bundledPiCodingAgentDiscovery from "@oh-my-pi/pi-coding-agent/discovery";
import * as bundledPiCodingAgentEdit from "@oh-my-pi/pi-coding-agent/edit";
import * as bundledPiCodingAgentEval from "@oh-my-pi/pi-coding-agent/eval";
import * as bundledPiCodingAgentExa from "@oh-my-pi/pi-coding-agent/exa";
import * as bundledPiCodingAgentExportHtml from "@oh-my-pi/pi-coding-agent/export/html";
import * as bundledPiCodingAgentExtensibilityCustomCommands from "@oh-my-pi/pi-coding-agent/extensibility/custom-commands";
import * as bundledPiCodingAgentExtensibilityCustomCommandsBundledCiGreen from "@oh-my-pi/pi-coding-agent/extensibility/custom-commands/bundled/ci-green";
import * as bundledPiCodingAgentExtensibilityCustomCommandsBundledReview from "@oh-my-pi/pi-coding-agent/extensibility/custom-commands/bundled/review";
import * as bundledPiCodingAgentExtensibilityCustomTools from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools";
import * as bundledPiCodingAgentExtensibilityExtensions from "@oh-my-pi/pi-coding-agent/extensibility/extensions";
import * as bundledPiCodingAgentExtensibilityHooks from "@oh-my-pi/pi-coding-agent/extensibility/hooks";
import * as bundledPiCodingAgentExtensibilityPlugins from "@oh-my-pi/pi-coding-agent/extensibility/plugins";
import * as bundledPiCodingAgentExtensibilityPluginsMarketplace from "@oh-my-pi/pi-coding-agent/extensibility/plugins/marketplace";
import * as bundledPiCodingAgentHindsight from "@oh-my-pi/pi-coding-agent/hindsight";
import * as bundledPiCodingAgentInternalUrls from "@oh-my-pi/pi-coding-agent/internal-urls";
import * as bundledPiCodingAgentLsp from "@oh-my-pi/pi-coding-agent/lsp";
import * as bundledPiCodingAgentLspClients from "@oh-my-pi/pi-coding-agent/lsp/clients";
import * as bundledPiCodingAgentMcp from "@oh-my-pi/pi-coding-agent/mcp";
import * as bundledPiCodingAgentMcpTransports from "@oh-my-pi/pi-coding-agent/mcp/transports";
import * as bundledPiCodingAgentMemories from "@oh-my-pi/pi-coding-agent/memories";
import * as bundledPiCodingAgentMemoryBackend from "@oh-my-pi/pi-coding-agent/memory-backend";
import * as bundledPiCodingAgentModes from "@oh-my-pi/pi-coding-agent/modes";
import * as bundledPiCodingAgentModesAcp from "@oh-my-pi/pi-coding-agent/modes/acp";
import * as bundledPiCodingAgentModesComponents from "@oh-my-pi/pi-coding-agent/modes/components";
import * as bundledPiCodingAgentModesComponentsExtensions from "@oh-my-pi/pi-coding-agent/modes/components/extensions";
import * as bundledPiCodingAgentModesComponentsStatusLine from "@oh-my-pi/pi-coding-agent/modes/components/status-line";
import * as bundledPiCodingAgentModesSetupWizard from "@oh-my-pi/pi-coding-agent/modes/setup-wizard";
import * as bundledPiCodingAgentModesThemeDefaults from "@oh-my-pi/pi-coding-agent/modes/theme/defaults";
import * as bundledPiCodingAgentSecrets from "@oh-my-pi/pi-coding-agent/secrets";
import * as bundledPiCodingAgentStt from "@oh-my-pi/pi-coding-agent/stt";
import * as bundledPiCodingAgentTask from "@oh-my-pi/pi-coding-agent/task";
import * as bundledPiCodingAgentTools from "@oh-my-pi/pi-coding-agent/tools";
import * as bundledPiCodingAgentTui from "@oh-my-pi/pi-coding-agent/tui";
import * as bundledPiCodingAgentWebScrapers from "@oh-my-pi/pi-coding-agent/web/scrapers";
import * as bundledPiCodingAgentWebSearch from "@oh-my-pi/pi-coding-agent/web/search";
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 bundledPiAi from "../legacy-pi-ai-shim";
import * as bundledPiCodingAgent 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.
* enumerable exports at extension load time. The companion
* `legacy-pi-bundled-keys.ts` mirrors `Object.keys(BUNDLED_PI_REGISTRY)` and
* is statically imported by `legacy-pi-compat.ts` to seed the override map
* without paying the cascade above.
*/
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,
"@oh-my-pi/pi-agent-core": bundledPiAgentCore as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-agent-core/compaction": bundledPiAgentCoreCompaction as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai": bundledPiAi as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/auth-broker": bundledPiAiAuthBroker as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/auth-gateway": bundledPiAiAuthGateway as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/utils/harmony-leak": bundledPiAiUtilsHarmonyLeak as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/dialect": bundledPiAiDialect as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/oauth": bundledPiAiOauth as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/registry": bundledPiAiRegistry as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/registry/oauth": bundledPiAiRegistryOauth as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-ai/utils/schema": bundledPiAiUtilsSchema as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent": bundledPiCodingAgent as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/async": bundledPiCodingAgentAsync as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/autoresearch": bundledPiCodingAgentAutoresearch as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/capability": bundledPiCodingAgentCapability as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/cli/gallery-fixtures": bundledPiCodingAgentCliGalleryFixtures as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/commit": bundledPiCodingAgentCommit as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/commit/agentic": bundledPiCodingAgentCommitAgentic as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/commit/agentic/tools": bundledPiCodingAgentCommitAgenticTools as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/commit/analysis": bundledPiCodingAgentCommitAnalysis as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/commit/changelog": bundledPiCodingAgentCommitChangelog as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/commit/map-reduce": bundledPiCodingAgentCommitMapReduce as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/dap": bundledPiCodingAgentDap as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/debug": bundledPiCodingAgentDebug as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/discovery": bundledPiCodingAgentDiscovery as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/edit": bundledPiCodingAgentEdit as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/exa": bundledPiCodingAgentExa as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/export/html": bundledPiCodingAgentExportHtml as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/extensibility/custom-commands":
bundledPiCodingAgentExtensibilityCustomCommands as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/extensibility/custom-commands/bundled/ci-green":
bundledPiCodingAgentExtensibilityCustomCommandsBundledCiGreen as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/extensibility/custom-commands/bundled/review":
bundledPiCodingAgentExtensibilityCustomCommandsBundledReview as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/extensibility/custom-tools":
bundledPiCodingAgentExtensibilityCustomTools as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/extensibility/extensions":
bundledPiCodingAgentExtensibilityExtensions as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/extensibility/hooks": bundledPiCodingAgentExtensibilityHooks as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/extensibility/plugins": bundledPiCodingAgentExtensibilityPlugins as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/extensibility/plugins/marketplace":
bundledPiCodingAgentExtensibilityPluginsMarketplace as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/internal-urls": bundledPiCodingAgentInternalUrls as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/eval": bundledPiCodingAgentEval as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/lsp": bundledPiCodingAgentLsp as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/lsp/clients": bundledPiCodingAgentLspClients as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/mcp": bundledPiCodingAgentMcp as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/mcp/transports": bundledPiCodingAgentMcpTransports as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/memories": bundledPiCodingAgentMemories as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/memory-backend": bundledPiCodingAgentMemoryBackend as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/hindsight": bundledPiCodingAgentHindsight as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/modes": bundledPiCodingAgentModes as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/modes/acp": bundledPiCodingAgentModesAcp as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/modes/components": bundledPiCodingAgentModesComponents as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/modes/components/extensions":
bundledPiCodingAgentModesComponentsExtensions as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/modes/components/status-line":
bundledPiCodingAgentModesComponentsStatusLine as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/modes/setup-wizard": bundledPiCodingAgentModesSetupWizard as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/modes/theme/defaults": bundledPiCodingAgentModesThemeDefaults as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/secrets": bundledPiCodingAgentSecrets as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/stt": bundledPiCodingAgentStt as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/task": bundledPiCodingAgentTask as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/tools": bundledPiCodingAgentTools as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/tui": bundledPiCodingAgentTui as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-coding-agent/web/scrapers": bundledPiCodingAgentWebScrapers as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-coding-agent/web/search": bundledPiCodingAgentWebSearch as unknown as Readonly<
Record<string, unknown>
>,
"@oh-my-pi/pi-natives": bundledPiNatives as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-tui": bundledPiTui as unknown as Readonly<Record<string, unknown>>,
"@oh-my-pi/pi-utils": bundledPiUtils as unknown as Readonly<Record<string, unknown>>,
typebox: bundledTypeBoxShim as unknown as Readonly<Record<string, unknown>>,
};
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as url from "node:url";
import { isCompiledBinary } from "@oh-my-pi/pi-utils";
import { BUNDLED_PI_REGISTRY_KEYS } from "./legacy-pi-bundled-keys";
const IS_COMPILED_BINARY = isCompiledBinary();
@@ -344,20 +345,34 @@ export function __validateLegacyPiPackageRootOverrides(
);
}
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,
...(IS_COMPILED_BINARY
? {
[`${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`),
}
: {}),
});
/**
* Compute the override map keyed by every canonical specifier the host serves
* directly: the pi-ai / pi-coding-agent roots (compat shims that re-attach
* legacy helpers) plus, in compiled-binary mode, every other canonical pi-*
* package root AND every non-wildcard subpath registered in the bundled
* registry (see `legacy-pi-bundled-keys.ts`). Subpath coverage is what stops
* `@(scope)/pi-ai/oauth` and friends from falling through to the extension's
* own — possibly absent — peer install when bunfs filesystem walks fail
* (issue #3442 follow-up to #3423). Exported as a test seam so the
* compiled-binary branch is verifiable from dev tests.
*/
export function __buildLegacyPiPackageRootOverrides(isCompiled: boolean): Record<string, string> {
const candidates: Record<string, string> = {
[`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
[`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
};
if (isCompiled) {
for (const key of BUNDLED_PI_REGISTRY_KEYS) {
// Shim-bearing roots above already mapped to their compat surface;
// the bundled typebox shim has a dedicated TYPEBOX_SHIM_PATH route.
if (key in candidates || key === TYPEBOX_BUNDLED_REGISTRY_KEY) continue;
candidates[key] = bundledRegistryVirtualSpecifier(key);
}
}
return __validateLegacyPiPackageRootOverrides(candidates);
}
const LEGACY_PI_PACKAGE_ROOT_OVERRIDES = __buildLegacyPiPackageRootOverrides(IS_COMPILED_BINARY);
let isLegacyPiSpecifierShimInstalled = false;
@@ -0,0 +1,63 @@
import { describe, expect, it } from "bun:test";
import { BUNDLED_PI_REGISTRY_KEYS } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-bundled-keys";
import { __buildLegacyPiPackageRootOverrides } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
// Regression for issue #3442: extension validation in compiled-binary mode
// failed to resolve `@earendil-works/pi-ai/oauth` because the override map
// only covered bare package roots — every non-wildcard subpath fell through
// to `Bun.resolveSync`, which bunfs can't satisfy on Bun 1.3.14+, then the
// `rewriteLegacyPiImports` catch left the original specifier in place and
// Bun's native resolver couldn't find a peer install. The fix seeds the
// override map with every key in `BUNDLED_PI_REGISTRY_KEYS` so subpath
// imports route to the same `omp-legacy-pi-bundled:` virtual namespace
// that already serves the roots.
describe("legacy pi compat compiled-mode subpath overrides (issue #3442)", () => {
it("serves @oh-my-pi/pi-ai/oauth through the bundled virtual namespace in compiled mode", () => {
const overrides = __buildLegacyPiPackageRootOverrides(true);
expect(overrides["@oh-my-pi/pi-ai/oauth"]).toBe("omp-legacy-pi-bundled:@oh-my-pi/pi-ai/oauth");
});
it("maps every bundled key (minus shimmed roots + typebox) to its virtual specifier in compiled mode", () => {
const overrides = __buildLegacyPiPackageRootOverrides(true);
const missing: string[] = [];
for (const key of BUNDLED_PI_REGISTRY_KEYS) {
// pi-ai/pi-coding-agent roots intentionally use the legacy compat shims
// (they re-attach `Type`, `defineTool`, etc. dropped from the canonical
// package surface); typebox is served via TYPEBOX_SHIM_PATH.
if (key === "@oh-my-pi/pi-ai" || key === "@oh-my-pi/pi-coding-agent" || key === "typebox") continue;
if (overrides[key] !== `omp-legacy-pi-bundled:${key}`) {
missing.push(key);
}
}
expect(missing).toEqual([]);
});
it("keeps pi-ai/pi-coding-agent roots routed to their compat shims in compiled mode", () => {
// The shim entries themselves resolve to virtual bundled specifiers in
// compiled mode (the shim files are bundled under their own registry
// keys); the test asserts only that the roots stay distinct from the
// canonical pi-* surface — extensions still see the `Type` /
// `defineTool` helpers the canonical entrypoints dropped.
const overrides = __buildLegacyPiPackageRootOverrides(true);
expect(overrides["@oh-my-pi/pi-ai"]).toBeDefined();
expect(overrides["@oh-my-pi/pi-ai"]).not.toBe("omp-legacy-pi-bundled:@oh-my-pi/pi-ai/oauth");
expect(overrides["@oh-my-pi/pi-coding-agent"]).toBeDefined();
});
it("does not register subpath overrides in dev/install mode", () => {
const overrides = __buildLegacyPiPackageRootOverrides(false);
expect(overrides).not.toHaveProperty("@oh-my-pi/pi-ai/oauth");
expect(overrides).not.toHaveProperty("@oh-my-pi/pi-coding-agent/tools");
// Dev keeps only the historical shim entries so canonical subpath
// imports continue to flow through `Bun.resolveSync` against the live
// monorepo / installed `node_modules` tree.
});
it("never emits a virtual specifier for typebox via the override map", () => {
// typebox is routed through `TYPEBOX_SHIM_PATH` + a dedicated onResolve
// hook; mirroring it in the override map would double-register and the
// virtual loader would race the dedicated shim path.
const overrides = __buildLegacyPiPackageRootOverrides(true);
expect(overrides).not.toHaveProperty("typebox");
});
});