diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 745c6953c..fc6cfa11d 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Restored the `pollOAuthDeviceCodeFlow` export from `@oh-my-pi/pi-ai/oauth` so legacy provider extensions can reuse the host OAuth device-code poller. ([#3508](https://github.com/can1357/oh-my-pi/issues/3508)) + ## [16.1.20] - 2026-06-25 ### Fixed diff --git a/packages/ai/src/registry/oauth/index.ts b/packages/ai/src/registry/oauth/index.ts index 5f293fb01..e2ae2722a 100644 --- a/packages/ai/src/registry/oauth/index.ts +++ b/packages/ai/src/registry/oauth/index.ts @@ -13,6 +13,94 @@ import type { export type * from "./types"; +const DEVICE_FLOW_CANCEL_MESSAGE = "Login cancelled"; +const DEVICE_FLOW_TIMEOUT_MESSAGE = "Device flow timed out"; +const DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE = + "Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again."; +const MINIMUM_DEVICE_FLOW_INTERVAL_MS = 1000; +const DEFAULT_DEVICE_FLOW_INTERVAL_SECONDS = 5; +const SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000; + +/** Result returned by one OAuth device-code polling attempt. */ +export type OAuthDeviceCodePollResult = + | { status: "complete"; value: T } + | { status: "pending" } + | { status: "slow_down" } + | { status: "failed"; message: string }; + +/** Options for polling an RFC 8628-style OAuth device-code flow. */ +export interface OAuthDeviceCodeFlowOptions { + /** Poll the provider once and classify the response. */ + poll(): OAuthDeviceCodePollResult | Promise>; + /** Provider-requested polling cadence; defaults to RFC 8628's five seconds. */ + intervalSeconds?: number; + /** Provider-issued expiry window for the device code. */ + expiresInSeconds?: number; + /** Cancels the flow with the legacy "Login cancelled" error. */ + signal?: AbortSignal; +} + +async function abortableDeviceFlowSleep(ms: number, signal: AbortSignal | undefined): Promise { + if (!signal) { + await Bun.sleep(ms); + return; + } + if (signal.aborted) { + throw new Error(DEVICE_FLOW_CANCEL_MESSAGE); + } + + const { promise, resolve, reject } = Promise.withResolvers(); + let timer: Timer | undefined; + const onAbort = () => { + if (timer) clearTimeout(timer); + reject(new Error(DEVICE_FLOW_CANCEL_MESSAGE)); + }; + timer = setTimeout(() => { + signal.removeEventListener("abort", onAbort); + resolve(); + }, ms); + signal.addEventListener("abort", onAbort, { once: true }); + await promise; +} + +/** Poll an OAuth device-code flow until completion, provider failure, timeout, or cancellation. */ +export async function pollOAuthDeviceCodeFlow(options: OAuthDeviceCodeFlowOptions): Promise { + const deadline = + typeof options.expiresInSeconds === "number" + ? Date.now() + options.expiresInSeconds * 1000 + : Number.POSITIVE_INFINITY; + let intervalMs = Math.max( + MINIMUM_DEVICE_FLOW_INTERVAL_MS, + Math.floor((options.intervalSeconds ?? DEFAULT_DEVICE_FLOW_INTERVAL_SECONDS) * 1000), + ); + let slowDownResponses = 0; + + while (Date.now() < deadline) { + if (options.signal?.aborted) { + throw new Error(DEVICE_FLOW_CANCEL_MESSAGE); + } + const result = await options.poll(); + if (result.status === "complete") { + return result.value; + } + if (result.status === "failed") { + throw new Error(result.message); + } + if (result.status === "slow_down") { + slowDownResponses += 1; + intervalMs = Math.max(MINIMUM_DEVICE_FLOW_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS); + } + + const remainingMs = deadline - Date.now(); + if (remainingMs <= 0) { + break; + } + await abortableDeviceFlowSleep(Math.min(intervalMs, remainingMs), options.signal); + } + + throw new Error(slowDownResponses > 0 ? DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE : DEVICE_FLOW_TIMEOUT_MESSAGE); +} + const builtInOAuthProviders: OAuthProviderInfo[] = PROVIDER_REGISTRY.filter( provider => provider.login && provider.showInLoginList !== false, ).map(provider => ({ diff --git a/packages/ai/test/oauth-device-code.test.ts b/packages/ai/test/oauth-device-code.test.ts new file mode 100644 index 000000000..d84e77924 --- /dev/null +++ b/packages/ai/test/oauth-device-code.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "bun:test"; +import { pollOAuthDeviceCodeFlow } from "@oh-my-pi/pi-ai/oauth"; + +describe("OAuth device-code polling", () => { + it("exports the legacy device-code poll helper for external providers", async () => { + const value = await pollOAuthDeviceCodeFlow({ + poll: () => ({ status: "complete", value: { access: "token" } }), + }); + + expect(value).toEqual({ access: "token" }); + }); + + it("surfaces provider failure messages", async () => { + expect( + pollOAuthDeviceCodeFlow({ + poll: () => ({ status: "failed", message: "authorization denied" }), + }), + ).rejects.toThrow("authorization denied"); + }); + + it("times out pending device flows", async () => { + expect( + pollOAuthDeviceCodeFlow({ + expiresInSeconds: 0.001, + poll: () => ({ status: "pending" }), + }), + ).rejects.toThrow("Device flow timed out"); + }); +}); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index cacd922af..027ab1e0f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed compiled-binary validation for legacy `pi.extensions` packages whose source imports worker-only coding-agent subpaths or extension-local package subpaths such as `typebox/value`; `omp install @charmland/pi-hyper-provider`, `omp plugin doctor`, and runtime provider discovery now use a main-thread-safe load path. ([#3508](https://github.com/can1357/oh-my-pi/issues/3508)) + ## [16.1.20] - 2026-06-25 ### Fixed diff --git a/packages/coding-agent/scripts/generate-legacy-pi-bundled-registry.ts b/packages/coding-agent/scripts/generate-legacy-pi-bundled-registry.ts index 8724b60db..c1f7f1a12 100755 --- a/packages/coding-agent/scripts/generate-legacy-pi-bundled-registry.ts +++ b/packages/coding-agent/scripts/generate-legacy-pi-bundled-registry.ts @@ -117,6 +117,15 @@ function isSafeWildcardBasename(basename: string): boolean { return true; } +// Worker entry modules intentionally throw when imported outside a Worker. The +// bundled registry loads on the main thread during legacy extension validation, +// so these exported subpaths must stay out of the static registry. +const MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES = new Set(["worker-entry"]); + +function isMainThreadSafeWildcardBasename(basename: string): boolean { + return !MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES.has(basename); +} + interface WildcardPattern { readonly exportPrefix: string; readonly exportSuffix: string; @@ -217,6 +226,7 @@ async function collectEntries(): Promise { if (!match.endsWith(pattern.sourceSuffix)) continue; const basename = match.slice(0, match.length - pattern.sourceSuffix.length); if (!isSafeWildcardBasename(basename)) continue; + if (!isMainThreadSafeWildcardBasename(basename)) continue; if (basename.includes("/")) continue; const subpath = `${pattern.exportPrefix}${basename}${pattern.exportSuffix}`; const key = `${pkg.name}/${subpath}`; 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 index f2fff5757..7e015cd1e 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-keys.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-bundled-keys.ts @@ -360,6 +360,7 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet = new Set([ "@oh-my-pi/pi-coding-agent/config/models-config", "@oh-my-pi/pi-coding-agent/config/prompt-templates", "@oh-my-pi/pi-coding-agent/config/resolve-config-value", + "@oh-my-pi/pi-coding-agent/config/service-tier", "@oh-my-pi/pi-coding-agent/config/settings-schema", "@oh-my-pi/pi-coding-agent/config/settings", "@oh-my-pi/pi-coding-agent/dap/client", @@ -483,7 +484,6 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet = new Set([ "@oh-my-pi/pi-coding-agent/eval/js/executor", "@oh-my-pi/pi-coding-agent/eval/js/tool-bridge", "@oh-my-pi/pi-coding-agent/eval/js/worker-core", - "@oh-my-pi/pi-coding-agent/eval/js/worker-entry", "@oh-my-pi/pi-coding-agent/eval/js/worker-protocol", "@oh-my-pi/pi-coding-agent/eval/py/display", "@oh-my-pi/pi-coding-agent/eval/py/executor", 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 92479f89e..4964c4d0c 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 @@ -328,6 +328,7 @@ import * as bundledPiCodingAgentConfigModelsConfig from "@oh-my-pi/pi-coding-age import * as bundledPiCodingAgentConfigModelsConfigSchema from "@oh-my-pi/pi-coding-agent/config/models-config-schema"; import * as bundledPiCodingAgentConfigPromptTemplates from "@oh-my-pi/pi-coding-agent/config/prompt-templates"; import * as bundledPiCodingAgentConfigResolveConfigValue from "@oh-my-pi/pi-coding-agent/config/resolve-config-value"; +import * as bundledPiCodingAgentConfigServiceTier from "@oh-my-pi/pi-coding-agent/config/service-tier"; import * as bundledPiCodingAgentConfigSettings from "@oh-my-pi/pi-coding-agent/config/settings"; import * as bundledPiCodingAgentConfigSettingsSchema from "@oh-my-pi/pi-coding-agent/config/settings-schema"; import * as bundledPiCodingAgentDap from "@oh-my-pi/pi-coding-agent/dap"; @@ -385,7 +386,6 @@ import * as bundledPiCodingAgentEvalJsContextManager from "@oh-my-pi/pi-coding-a import * as bundledPiCodingAgentEvalJsExecutor from "@oh-my-pi/pi-coding-agent/eval/js/executor"; import * as bundledPiCodingAgentEvalJsToolBridge from "@oh-my-pi/pi-coding-agent/eval/js/tool-bridge"; import * as bundledPiCodingAgentEvalJsWorkerCore from "@oh-my-pi/pi-coding-agent/eval/js/worker-core"; -import * as bundledPiCodingAgentEvalJsWorkerEntry from "@oh-my-pi/pi-coding-agent/eval/js/worker-entry"; import * as bundledPiCodingAgentEvalJsWorkerProtocol from "@oh-my-pi/pi-coding-agent/eval/js/worker-protocol"; import * as bundledPiCodingAgentEvalPyDisplay from "@oh-my-pi/pi-coding-agent/eval/py/display"; import * as bundledPiCodingAgentEvalPyExecutor from "@oh-my-pi/pi-coding-agent/eval/py/executor"; @@ -1802,6 +1802,9 @@ export const BUNDLED_PI_REGISTRY: Readonly>, "@oh-my-pi/pi-coding-agent/config/resolve-config-value": bundledPiCodingAgentConfigResolveConfigValue as unknown as Readonly>, + "@oh-my-pi/pi-coding-agent/config/service-tier": bundledPiCodingAgentConfigServiceTier as unknown as Readonly< + Record + >, "@oh-my-pi/pi-coding-agent/config/settings-schema": bundledPiCodingAgentConfigSettingsSchema as unknown as Readonly< Record >, @@ -2100,9 +2103,6 @@ export const BUNDLED_PI_REGISTRY: Readonly >, - "@oh-my-pi/pi-coding-agent/eval/js/worker-entry": bundledPiCodingAgentEvalJsWorkerEntry as unknown as Readonly< - Record - >, "@oh-my-pi/pi-coding-agent/eval/js/worker-protocol": bundledPiCodingAgentEvalJsWorkerProtocol as unknown as Readonly< Record >, 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 4771632ed..2d5e70997 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts @@ -1,4 +1,5 @@ import * as fs from "node:fs"; +import { isBuiltin } from "node:module"; import * as path from "node:path"; import * as url from "node:url"; import { isCompiledBinary } from "@oh-my-pi/pi-utils"; @@ -456,10 +457,11 @@ const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["' /** * Rewrite the extension-owned specifiers OMP must host-resolve — legacy - * `@(scope)/pi-*`, bare TypeBox packages, and package `imports` aliases like - * `#src/*` — to absolute `file://` URLs. Every other specifier (relative - * siblings and third-party dependencies) is left untouched so Bun resolves it - * natively from the extension's real on-disk location. + * `@(scope)/pi-*`, bare TypeBox packages, package `imports` aliases like + * `#src/*`, and extension-local bare dependencies — to absolute `file://` URLs + * or compiled-mode virtual specifiers. Relative siblings and built-in modules + * are left untouched so Bun resolves them from the extension's real on-disk + * location. */ async function rewriteLegacyExtensionSource(source: string, importerPath: string): Promise { const withPi = rewriteLegacyPiImports(source); @@ -474,7 +476,12 @@ async function rewriteLegacyExtensionSource(source: string, importerPath: string `${prefix}${toImportSpecifier(TYPEBOX_SHIM_PATH)}${suffix}`, ) : withPi; - return rewriteExtensionPackageImports(withTypeBox, importerPath); + return rewriteExtensionBareImports(await rewriteExtensionPackageImports(withTypeBox, importerPath), importerPath); +} + +/** Test seam for compiled-binary legacy extension source rewriting. */ +export async function __rewriteLegacyExtensionSourceForTests(source: string, importerPath: string): Promise { + return rewriteLegacyExtensionSource(source, importerPath); } function isRecord(value: unknown): value is Record { @@ -688,6 +695,183 @@ async function rewriteExtensionPackageImports(source: string, importerPath: stri return `${rewritten}${source.slice(lastIndex)}`; } +const BARE_EXTENSION_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])([^"'()\s]+)(["'])/g; + +function isBareExtensionDependencySpecifier(specifier: string): boolean { + if ( + specifier.startsWith(".") || + specifier.startsWith("/") || + specifier.startsWith("#") || + specifier.startsWith("node:") || + specifier.startsWith("bun:") || + /^[a-z][a-z0-9+.-]*:/i.test(specifier) + ) { + return false; + } + const packageName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0]; + return Boolean(packageName && !isBuiltin(packageName)); +} + +interface BarePackageSpecifier { + readonly name: string; + readonly subpath: string | null; +} + +function splitBarePackageSpecifier(specifier: string): BarePackageSpecifier | null { + const parts = specifier.split("/"); + if (specifier.startsWith("@")) { + const [scope, name, ...rest] = parts; + if (!scope || !name) return null; + return { name: `${scope}/${name}`, subpath: rest.length > 0 ? rest.join("/") : null }; + } + const [name, ...rest] = parts; + if (!name) return null; + return { name, subpath: rest.length > 0 ? rest.join("/") : null }; +} + +async function findNodePackageRoot(packageName: string, importerPath: string): Promise { + let dir = path.dirname(importerPath); + while (true) { + const candidate = path.join(dir, "node_modules", packageName); + if (await pathExists(path.join(candidate, "package.json"))) { + return candidate; + } + const parent = path.dirname(dir); + if (parent === dir) { + return null; + } + dir = parent; + } +} + +async function readPackageManifest(packageRoot: string): Promise | null> { + try { + const manifest = await Bun.file(path.join(packageRoot, "package.json")).json(); + return isRecord(manifest) ? manifest : null; + } catch { + return null; + } +} + +async function resolvePackageExportTarget( + packageRoot: string, + target: string, + wildcard: string | null, +): Promise { + if (!target.startsWith("./")) { + return null; + } + const substituted = wildcard === null ? target : target.replaceAll("*", wildcard); + return resolveSourceModuleFile(path.resolve(packageRoot, substituted)); +} + +async function resolveNodePackageExport( + packageRoot: string, + subpath: string | null, + manifest: Record, +): Promise { + const exportsField = manifest.exports; + if (!isRecord(exportsField)) { + return null; + } + + const exactKey = subpath === null ? "." : `./${subpath}`; + const exactTarget = selectPackageImportTarget(exportsField[exactKey]); + if (exactTarget !== null && exactTarget !== PACKAGE_IMPORT_EXCLUDED) { + return resolvePackageExportTarget(packageRoot, exactTarget, null); + } + + for (const [key, entry] of Object.entries(exportsField)) { + const starIndex = key.indexOf("*"); + if (starIndex === -1 || subpath === null) continue; + const prefix = key.slice(2, starIndex); + const suffix = key.slice(starIndex + 1); + if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) { + continue; + } + const target = selectPackageImportTarget(entry); + if (target === null || target === PACKAGE_IMPORT_EXCLUDED) { + continue; + } + return resolvePackageExportTarget( + packageRoot, + target, + subpath.slice(prefix.length, subpath.length - suffix.length), + ); + } + return null; +} + +async function resolveNodePackageFallback( + packageRoot: string, + subpath: string | null, + manifest: Record, +): Promise { + if (subpath !== null) { + return resolveSourceModuleFile(path.join(packageRoot, subpath)); + } + for (const field of ["module", "main"]) { + const target = manifest[field]; + if (typeof target === "string") { + const resolved = await resolveSourceModuleFile(path.resolve(packageRoot, target)); + if (resolved) return resolved; + } + } + return resolveSourceModuleFile(path.join(packageRoot, "index")); +} + +async function resolveNodePackageDependency(specifier: string, importerPath: string): Promise { + const parsed = splitBarePackageSpecifier(specifier); + if (!parsed) return null; + const packageRoot = await findNodePackageRoot(parsed.name, importerPath); + if (!packageRoot) return null; + const manifest = await readPackageManifest(packageRoot); + if (!manifest) return null; + return ( + (await resolveNodePackageExport(packageRoot, parsed.subpath, manifest)) ?? + (await resolveNodePackageFallback(packageRoot, parsed.subpath, manifest)) + ); +} + +async function resolveExtensionBareDependency(specifier: string, importerPath: string): Promise { + if (!isBareExtensionDependencySpecifier(specifier)) { + return null; + } + try { + const resolved = Bun.resolveSync(specifier, path.dirname(importerPath)); + if (resolved && resolved !== specifier && !resolved.startsWith("node:") && !resolved.startsWith("bun:")) { + return resolved; + } + } catch { + // Compiled binaries do not reliably resolve runtime extension node_modules. + } + return resolveNodePackageDependency(specifier, importerPath); +} + +async function rewriteExtensionBareImports(source: string, importerPath: string): Promise { + let rewritten = ""; + let lastIndex = 0; + for (const match of source.matchAll(BARE_EXTENSION_IMPORT_SPECIFIER_REGEX)) { + const matchIndex = match.index; + if (matchIndex === undefined) continue; + + const [fullMatch, prefix, specifier, suffix] = match; + if (!prefix || !specifier || !suffix) continue; + + const resolved = await resolveExtensionBareDependency(specifier, importerPath); + if (!resolved) continue; + + rewritten += source.slice(lastIndex, matchIndex); + rewritten += `${prefix}${toImportSpecifier(resolved)}${suffix}`; + lastIndex = matchIndex + fullMatch.length; + } + + if (lastIndex === 0) { + return source; + } + return `${rewritten}${source.slice(lastIndex)}`; +} + function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } 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 index 19bf34895..c1d9b3fa2 100644 --- 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 @@ -44,6 +44,14 @@ describe("legacy pi compat compiled-mode subpath overrides (issue #3442)", () => expect(BUNDLED_PI_REGISTRY_KEYS.has("@oh-my-pi/pi-coding-agent/main")).toBe(false); }); + it("does not bundle main-thread-unsafe worker entrypoints", () => { + // Worker entry modules throw at top level unless `parentPort` exists. + // The compiled legacy registry is imported on the main thread while + // validating plugin extensions, so enumerating these files recreates the + // `js worker-entry: missing parentPort` failure from #3508. + expect(BUNDLED_PI_REGISTRY_KEYS.has("@oh-my-pi/pi-coding-agent/eval/js/worker-entry")).toBe(false); + }); + it("maps every bundled key (minus shimmed roots + typebox) to its virtual specifier in compiled mode", () => { const overrides = __buildLegacyPiPackageRootOverrides(true); const missing: string[] = []; diff --git a/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts index 996d9e7c3..1599d865a 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts @@ -3,7 +3,10 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import * as url from "node:url"; -import { loadLegacyPiModule } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat"; +import { + __rewriteLegacyExtensionSourceForTests, + loadLegacyPiModule, +} from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat"; // Issue #1674: legacy Pi extensions load browser-UI assets (HTML/CSS) at module // init via `readFileSync(join(__dirname, "ui.html"))`. The compat layer must run @@ -103,6 +106,32 @@ describe("legacy-pi in-place module loading (issue #1674)", () => { expect(mod.hasZod).toBe(true); }); + it("rewrites extension bare deps to file URLs for compiled-binary loading", async () => { + const dir = await writePackage({ + "package.json": JSON.stringify({ name: "compiled-dep-ext", version: "1.0.0" }), + "node_modules/esmdep/package.json": JSON.stringify({ + name: "esmdep", + version: "1.0.0", + type: "module", + exports: { "./value": "./value.js" }, + }), + "node_modules/esmdep/value.js": "export const value = 1;", + "index.ts": "", + }); + const importer = path.join(dir, "index.ts"); + const rewritten = await __rewriteLegacyExtensionSourceForTests( + [ + 'import * as path from "node:path";', + 'import { value } from "esmdep/value";', + "export const loaded = value;", + ].join("\n"), + importer, + ); + + expect(rewritten).toContain(url.pathToFileURL(path.join(dir, "node_modules/esmdep/value.js")).href); + expect(rewritten).toContain('from "node:path"'); + }); + it("remaps legacy pi-ai utils/oauth subpaths to registry OAuth exports", async () => { const dir = await writePackage({ "package.json": JSON.stringify({ name: "legacy-oauth-ext", version: "1.0.0" }),