diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 6407f7dbd61..99870a46654 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -7,6 +7,7 @@ import { Plugin } from "../plugin" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" import { MCP } from "../mcp" +import type { Hooks } from "@opencode-ai/plugin" export namespace Command { export const Event = { @@ -129,7 +130,7 @@ export namespace Command { // Plugin commands const plugins = await Plugin.list() for (const plugin of plugins) { - const commands = plugin["plugin.command"] + const commands: NonNullable | undefined = plugin["plugin.command"] if (!commands) continue for (const [name, cmd] of Object.entries(commands)) { if (result[name]) continue // Don't override existing commands diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index daff5c9ac10..b506349c7b7 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -83,6 +83,10 @@ export namespace Plugin { } } + function isModuleResolutionError(err: Error): boolean { + return err.message?.includes("Cannot find module") || err.message?.includes("Cannot find package") + } + const state = Instance.state(async () => { const client = createOpencodeClient({ baseUrl: "http://localhost:4096", @@ -90,7 +94,7 @@ export namespace Plugin { fetch: async (...args) => Server.App().fetch(...args), }) const config = await Config.get() - const hooks = [] + const hooks: Hooks[] = [] const input: PluginInput = { client, project: Instance.project, @@ -106,6 +110,7 @@ export namespace Plugin { for (let plugin of plugins) { log.info("loading plugin", { path: plugin }) let pluginUrl: string + let localPluginPath: string | undefined if (!plugin.startsWith("file://")) { const lastAtIndex = plugin.lastIndexOf("@") const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin @@ -122,14 +127,14 @@ export namespace Plugin { // Resolve relative file:// paths against the working directory const filePath = plugin.substring("file://".length) const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(Instance.directory, filePath) - // Bundle local plugins with their dependencies for compiled binary compatibility - const bundledPath = await bundleLocalPlugin(absolutePath) - pluginUrl = pathToFileURL(bundledPath).href + localPluginPath = absolutePath + pluginUrl = pathToFileURL(absolutePath).href } - try { + + const loadPluginModule = async (url: string) => { // Use dynamic import() with absolute file:// URLs for ES module compatibility // pathToFileURL ensures proper URL encoding regardless of import.meta.url context - const mod = await import(pluginUrl) + const mod = await import(url) // Prevent duplicate initialization when plugins export the same function // as both a named export and default export (e.g., `export const X` and `export default X`). // Object.entries(mod) would return both entries pointing to the same function reference. @@ -141,10 +146,23 @@ export namespace Plugin { const init = await fn(input) hooks.push(init) } + } + + try { + await loadPluginModule(pluginUrl) } catch (e) { const err = e as Error + if (localPluginPath && isModuleResolutionError(err)) { + log.warn("failed to load local plugin directly, bundling fallback", { + plugin, + error: err.message, + }) + const bundledPath = await bundleLocalPlugin(localPluginPath) + await loadPluginModule(pathToFileURL(bundledPath).href) + continue + } // Check for module resolution issues - if (err.message?.includes("Cannot find module") || err.message?.includes("Cannot find package")) { + if (isModuleResolutionError(err)) { log.error("failed to load plugin", { plugin, error: err.message, @@ -175,7 +193,7 @@ export namespace Plugin { for (const hook of await state().then((x) => x.hooks)) { const fn = hook[name] if (!fn) continue - // @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you + // @ts-ignore if you feel adventurous, please fix the typing, make sure to bump the try-counter if you // give up. // try-counter: 2 await fn(input, output) @@ -195,7 +213,7 @@ export namespace Plugin { const hooks = await state().then((x) => x.hooks) const config = await Config.get() for (const hook of hooks) { - // @ts-expect-error this is because we haven't moved plugin to sdk v2 + // @ts-ignore this is because we haven't moved plugin to sdk v2 await hook.config?.(config) } Bus.subscribeAll(async (input) => { diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index d06253ab4ad..e59452e7403 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -30,14 +30,15 @@ export namespace ProviderAuth { export async function methods() { const s = await state().then((x) => x.methods) - return mapValues(s, (x) => - x.methods.map( - (y): Method => ({ - type: y.type, - label: y.label, + return mapValues(s, (x: Hooks["auth"] | undefined) => { + if (!x) return [] + return x.methods.map( + (method): Method => ({ + type: method.type, + label: method.label, }), - ), - ) + ) + }) } export const Authorization = z diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index eb76681ded4..54bc61112ce 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -50,7 +50,9 @@ export namespace ToolRegistry { const plugins = await Plugin.list() for (const plugin of plugins) { - for (const [id, def] of Object.entries(plugin.tool ?? {})) { + const tools = plugin.tool as Record | undefined + if (!tools) continue + for (const [id, def] of Object.entries(tools)) { custom.push(fromPlugin(id, def)) } }