From 2a08cd3b96702c08cad58ad5727e996996cad440 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 13 Jul 2026 22:45:15 -0400 Subject: [PATCH] refactor(core): simplify plugin entrypoint resolution --- packages/core/src/npm.ts | 32 +++++++++------ packages/core/src/plugin/supervisor.ts | 40 +------------------ .../fixtures/plugins/folder-plugin/index.ts | 13 ------ packages/core/test/config/plugin.test.ts | 6 +-- packages/core/test/npm.test.ts | 16 ++++++-- 5 files changed, 35 insertions(+), 72 deletions(-) delete mode 100644 packages/core/test/config/fixtures/plugins/folder-plugin/index.ts diff --git a/packages/core/src/npm.ts b/packages/core/src/npm.ts index 30e12cff12..48e71da064 100644 --- a/packages/core/src/npm.ts +++ b/packages/core/src/npm.ts @@ -25,7 +25,10 @@ export interface EntryPoint { } export interface Interface { - readonly add: (pkg: string) => Effect.Effect + readonly add: ( + pkg: string, + options?: { readonly subpaths?: readonly string[] }, + ) => Effect.Effect readonly install: ( dir: string, input?: { @@ -47,13 +50,18 @@ export function sanitize(pkg: string) { return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") } -const resolveEntryPoint = (name: string, dir: string): EntryPoint => { - let entrypoint: string | undefined - try { - entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) - } catch { - entrypoint = undefined - } +const resolveEntryPoint = (name: string, dir: string, subpaths: readonly string[] = [""]): EntryPoint => { + const entrypoint = subpaths + .map((subpath) => { + try { + return typeof Bun !== "undefined" + ? import.meta.resolve([name, subpath].filter(Boolean).join("/"), dir) + : import.meta.resolve(dir) + } catch { + return undefined + } + }) + .find((entrypoint) => entrypoint !== undefined) return { directory: dir, entrypoint, @@ -112,7 +120,7 @@ const layer = Layer.effect( }), ) - const add = Effect.fn("Npm.add")(function* (pkg: string) { + const add = Effect.fn("Npm.add")(function* (pkg: string, options?: { readonly subpaths?: readonly string[] }) { const dir = directory(pkg) const name = (() => { try { @@ -123,17 +131,17 @@ const layer = Layer.effect( })() if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) { - return resolveEntryPoint(name, path.join(dir, "node_modules", name)) + return resolveEntryPoint(name, path.join(dir, "node_modules", name), options?.subpaths) } const tree = yield* reify({ dir, add: [pkg] }) const first = tree.edgesOut.values().next().value?.to if (!first) { - const result = resolveEntryPoint(name, path.join(dir, "node_modules", name)) + const result = resolveEntryPoint(name, path.join(dir, "node_modules", name), options?.subpaths) if (result.entrypoint) return result return yield* new InstallFailedError({ add: [pkg], dir }) } - return resolveEntryPoint(first.name, first.path) + return resolveEntryPoint(first.name, first.path, options?.subpaths) }, Effect.scoped) const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) { diff --git a/packages/core/src/plugin/supervisor.ts b/packages/core/src/plugin/supervisor.ts index c29c6a8647..f41be2ad7c 100644 --- a/packages/core/src/plugin/supervisor.ts +++ b/packages/core/src/plugin/supervisor.ts @@ -54,12 +54,6 @@ const PluginModule = Schema.Struct({ ]), }) -const PluginPackage = Schema.Struct({ - exports: Schema.optional(Schema.Unknown), - main: Schema.optional(Schema.String), - module: Schema.optional(Schema.String), -}) - type Operation = | { readonly type: "add" @@ -165,7 +159,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract [])) - const directories = yield* fs - .glob("{plugin,plugins}/*", { - cwd: directory, - absolute: true, - include: "all", - dot: true, - symlink: true, - }) - .pipe( - Effect.flatMap((items) => Effect.filter(items, (item) => fs.isDir(item), { concurrency: "unbounded" })), - Effect.orElseSucceed(() => []), - ) - const packages = yield* Effect.forEach(directories.sort(), (directory) => resolvePackageEntrypoint(fs, directory), { - concurrency: "unbounded", - }).pipe(Effect.map((items) => items.filter((item): item is string => item !== undefined))) - return [...files.sort(), ...packages].map((target): Operation => ({ type: "add", target, options: {} })) + return files.sort().map((target): Operation => ({ type: "add", target, options: {} })) }) } -const resolvePackageEntrypoint = Effect.fnUntraced(function* (fs: FSUtil.Interface, directory: string) { - const pkg = yield* fs.readJson(path.join(directory, "package.json")).pipe( - Effect.flatMap(Schema.decodeUnknownEffect(PluginPackage)), - Effect.catch(() => Effect.succeed(undefined)), - ) - const exported = typeof pkg?.exports === "string" ? pkg.exports : undefined - const entries = [exported, pkg?.module, pkg?.main, "index.ts", "index.js"] - - return yield* Effect.forEach(entries, (entry) => { - if (!entry) return Effect.succeed(undefined) - const file = path.resolve(directory, entry) - return fs.isFile(file).pipe(Effect.map((exists) => (exists ? file : undefined))) - }).pipe(Effect.map((items) => items.find((item): item is string => item !== undefined))) -}) - export interface Interface { /** Wait for the initial plugin generation and startup updates to settle. */ readonly flush: Effect.Effect diff --git a/packages/core/test/config/fixtures/plugins/folder-plugin/index.ts b/packages/core/test/config/fixtures/plugins/folder-plugin/index.ts deleted file mode 100644 index 365d5566d2..0000000000 --- a/packages/core/test/config/fixtures/plugins/folder-plugin/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Plugin } from "@opencode-ai/plugin/v2" - -export default Plugin.define({ - id: "folder-plugin", - setup: async (ctx) => { - await ctx.agent.transform((agents) => { - agents.update("folder", (agent) => { - agent.description = "Loaded from plugin folder" - agent.mode = "subagent" - }) - }) - }, -}) diff --git a/packages/core/test/config/plugin.test.ts b/packages/core/test/config/plugin.test.ts index 4c83d3bb4b..626e94f758 100644 --- a/packages/core/test/config/plugin.test.ts +++ b/packages/core/test/config/plugin.test.ts @@ -134,7 +134,7 @@ describe("PluginSupervisor config", () => { ), ) - it.live("loads auto-discovered plugin files and packages", () => + it.live("loads auto-discovered plugin files", () => withLocation( undefined, Effect.gen(function* () { @@ -143,9 +143,6 @@ describe("PluginSupervisor config", () => { expect(yield* agents.get(AgentV2.ID.make("directory"))).toMatchObject({ description: "Loaded from plugin directory", }) - expect(yield* agents.get(AgentV2.ID.make("folder"))).toMatchObject({ - description: "Loaded from plugin folder", - }) }), true, ), @@ -195,7 +192,6 @@ describe("PluginSupervisor config", () => { yield* ready() const agents = yield* AgentV2.Service expect(yield* agents.get(AgentV2.ID.make("directory"))).toBeUndefined() - expect(yield* agents.get(AgentV2.ID.make("folder"))).toBeUndefined() }), true, ), diff --git a/packages/core/test/npm.test.ts b/packages/core/test/npm.test.ts index 7e4a5763bf..3a2a956299 100644 --- a/packages/core/test/npm.test.ts +++ b/packages/core/test/npm.test.ts @@ -41,19 +41,27 @@ describe("Npm.add", () => { await fs.mkdir(path.join(tmp.path, "fixture-provider")) await writePackage(path.join(tmp.path, "fixture-provider"), { name: "fixture-provider", - main: "index.js", + exports: { + ".": "./index.js", + "./tui": "./tui.js", + }, }) await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n") + await Bun.write(path.join(tmp.path, "fixture-provider", "tui.js"), "export const tui = true\n") const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}` await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true }) - const entry = await Effect.gen(function* () { + const entries = await Effect.gen(function* () { const npm = yield* Npm.Service - return yield* npm.add(spec) + return { + tui: yield* npm.add(spec, { subpaths: ["tui", ""] }), + fallback: yield* npm.add(spec, { subpaths: ["missing", ""] }), + } }).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise) - expect(entry.entrypoint).toBeDefined() + expect(entries.tui.entrypoint).toEndWith("/tui.js") + expect(entries.fallback.entrypoint).toEndWith("/index.js") }) })