diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d129ace5d..a2d23b93c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed `omp install ` failing extension validation in compiled-binary mode with `Cannot find module '@(scope)/pi-ai/oauth' from '/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://` works across tool execution boundaries. ([#3436](https://github.com/can1357/oh-my-pi/issues/3436)) ## [16.1.18] - 2026-06-25 diff --git a/packages/coding-agent/scripts/build-binary.ts b/packages/coding-agent/scripts/build-binary.ts index 734aa1762..d9f89478b 100644 --- a/packages/coding-agent/scripts/build-binary.ts +++ b/packages/coding-agent/scripts/build-binary.ts @@ -58,6 +58,15 @@ async function main(): Promise { : 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( diff --git a/packages/coding-agent/scripts/generate-legacy-pi-bundled-registry.ts b/packages/coding-agent/scripts/generate-legacy-pi-bundled-registry.ts new file mode 100755 index 000000000..07201b601 --- /dev/null +++ b/packages/coding-agent/scripts/generate-legacy-pi-bundled-registry.ts @@ -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//` 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 { + 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 }; + 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(); + 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>,`, + ); + 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>>> = {", + ...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 = new Set([", + ...keyLines, + "]);", + "", + ].join("\n"); +} + +async function formatInPlace(targets: readonly string[]): Promise { + // `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 { + 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 `..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(); diff --git a/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-keys.ts b/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-keys.ts new file mode 100644 index 000000000..a8613a8f4 --- /dev/null +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-keys.ts @@ -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 = 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", +]); 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 index 699a85dfd..1bb7d7e20 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-registry.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-registry.ts @@ -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>>> = { - "@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>, + "@oh-my-pi/pi-agent-core/compaction": bundledPiAgentCoreCompaction as unknown as Readonly>, + "@oh-my-pi/pi-ai": bundledPiAi as unknown as Readonly>, + "@oh-my-pi/pi-ai/auth-broker": bundledPiAiAuthBroker as unknown as Readonly>, + "@oh-my-pi/pi-ai/auth-gateway": bundledPiAiAuthGateway as unknown as Readonly>, + "@oh-my-pi/pi-ai/utils/harmony-leak": bundledPiAiUtilsHarmonyLeak as unknown as Readonly>, + "@oh-my-pi/pi-ai/dialect": bundledPiAiDialect as unknown as Readonly>, + "@oh-my-pi/pi-ai/oauth": bundledPiAiOauth as unknown as Readonly>, + "@oh-my-pi/pi-ai/registry": bundledPiAiRegistry as unknown as Readonly>, + "@oh-my-pi/pi-ai/registry/oauth": bundledPiAiRegistryOauth as unknown as Readonly>, + "@oh-my-pi/pi-ai/utils/schema": bundledPiAiUtilsSchema as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent": bundledPiCodingAgent as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/async": bundledPiCodingAgentAsync as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/autoresearch": bundledPiCodingAgentAutoresearch as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/capability": bundledPiCodingAgentCapability as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/cli/gallery-fixtures": bundledPiCodingAgentCliGalleryFixtures as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/commit": bundledPiCodingAgentCommit as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/commit/agentic": bundledPiCodingAgentCommitAgentic as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/commit/agentic/tools": bundledPiCodingAgentCommitAgenticTools as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/commit/analysis": bundledPiCodingAgentCommitAnalysis as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/commit/changelog": bundledPiCodingAgentCommitChangelog as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/commit/map-reduce": bundledPiCodingAgentCommitMapReduce as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/dap": bundledPiCodingAgentDap as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/debug": bundledPiCodingAgentDebug as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/discovery": bundledPiCodingAgentDiscovery as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/edit": bundledPiCodingAgentEdit as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/exa": bundledPiCodingAgentExa as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/export/html": bundledPiCodingAgentExportHtml as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/extensibility/custom-commands": + bundledPiCodingAgentExtensibilityCustomCommands as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/extensibility/custom-commands/bundled/ci-green": + bundledPiCodingAgentExtensibilityCustomCommandsBundledCiGreen as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/extensibility/custom-commands/bundled/review": + bundledPiCodingAgentExtensibilityCustomCommandsBundledReview as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/extensibility/custom-tools": + bundledPiCodingAgentExtensibilityCustomTools as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/extensibility/extensions": + bundledPiCodingAgentExtensibilityExtensions as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/extensibility/hooks": bundledPiCodingAgentExtensibilityHooks as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/extensibility/plugins": bundledPiCodingAgentExtensibilityPlugins as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/extensibility/plugins/marketplace": + bundledPiCodingAgentExtensibilityPluginsMarketplace as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/internal-urls": bundledPiCodingAgentInternalUrls as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/eval": bundledPiCodingAgentEval as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/lsp": bundledPiCodingAgentLsp as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/lsp/clients": bundledPiCodingAgentLspClients as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/mcp": bundledPiCodingAgentMcp as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/mcp/transports": bundledPiCodingAgentMcpTransports as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/memories": bundledPiCodingAgentMemories as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/memory-backend": bundledPiCodingAgentMemoryBackend as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/hindsight": bundledPiCodingAgentHindsight as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/modes": bundledPiCodingAgentModes as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/modes/acp": bundledPiCodingAgentModesAcp as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/modes/components": bundledPiCodingAgentModesComponents as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/modes/components/extensions": + bundledPiCodingAgentModesComponentsExtensions as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/modes/components/status-line": + bundledPiCodingAgentModesComponentsStatusLine as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/modes/setup-wizard": bundledPiCodingAgentModesSetupWizard as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/modes/theme/defaults": bundledPiCodingAgentModesThemeDefaults as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/secrets": bundledPiCodingAgentSecrets as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/stt": bundledPiCodingAgentStt as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/task": bundledPiCodingAgentTask as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/tools": bundledPiCodingAgentTools as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/tui": bundledPiCodingAgentTui as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/web/scrapers": bundledPiCodingAgentWebScrapers as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-coding-agent/web/search": bundledPiCodingAgentWebSearch as unknown as Readonly< + Record + >, + "@oh-my-pi/pi-natives": bundledPiNatives as unknown as Readonly>, + "@oh-my-pi/pi-tui": bundledPiTui as unknown as Readonly>, + "@oh-my-pi/pi-utils": bundledPiUtils as unknown as Readonly>, + typebox: bundledTypeBoxShim as unknown as Readonly>, }; 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 a74996526..4771632ed 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts @@ -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 { + const candidates: Record = { + [`${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; diff --git a/packages/coding-agent/test/extensibility/legacy-pi-bundled-subpath-overrides.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-bundled-subpath-overrides.test.ts new file mode 100644 index 000000000..1cb335729 --- /dev/null +++ b/packages/coding-agent/test/extensibility/legacy-pi-bundled-subpath-overrides.test.ts @@ -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"); + }); +});