61ca69317a
Remove dynamic installation of built-in plugins. GitLab auth is now imported directly as an internal plugin, eliminating network requests during startup and simplifying the plugin loading logic. Removes the need for test mocks since plugins are no longer dynamically installed at runtime.
113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
import type { Hooks, PluginInput, Plugin as PluginInstance } from "@opencode-ai/plugin"
|
|
import { Config } from "../config/config"
|
|
import { Bus } from "../bus"
|
|
import { Log } from "../util/log"
|
|
import { createOpencodeClient } from "@opencode-ai/sdk"
|
|
import { Server } from "../server/server"
|
|
import { BunProc } from "../bun"
|
|
import { Instance } from "../project/instance"
|
|
import { CodexAuthPlugin } from "./codex"
|
|
import { CopilotAuthPlugin } from "./copilot"
|
|
import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth"
|
|
|
|
export namespace Plugin {
|
|
const log = Log.create({ service: "plugin" })
|
|
|
|
// Built-in plugins that are directly imported (not installed from npm)
|
|
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin]
|
|
|
|
const state = Instance.state(async () => {
|
|
const client = createOpencodeClient({
|
|
baseUrl: "http://localhost:4096",
|
|
// @ts-ignore - fetch type incompatibility
|
|
fetch: async (...args) => Server.App().fetch(...args),
|
|
})
|
|
const config = await Config.get()
|
|
const hooks: Hooks[] = []
|
|
const input: PluginInput = {
|
|
client,
|
|
project: Instance.project,
|
|
worktree: Instance.worktree,
|
|
directory: Instance.directory,
|
|
serverUrl: Server.url(),
|
|
$: Bun.$,
|
|
}
|
|
|
|
for (const plugin of INTERNAL_PLUGINS) {
|
|
log.info("loading internal plugin", { name: plugin.name })
|
|
const init = await plugin(input)
|
|
hooks.push(init)
|
|
}
|
|
|
|
const plugins = [...(config.plugin ?? [])]
|
|
if (plugins.length) await Config.waitForDependencies()
|
|
|
|
for (let plugin of plugins) {
|
|
// ignore old codex plugin since it is supported first party now
|
|
if (plugin.includes("opencode-openai-codex-auth") || plugin.includes("opencode-copilot-auth")) continue
|
|
log.info("loading plugin", { path: plugin })
|
|
if (!plugin.startsWith("file://")) {
|
|
const lastAtIndex = plugin.lastIndexOf("@")
|
|
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
|
|
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
|
|
plugin = await BunProc.install(pkg, version)
|
|
if (!plugin) continue
|
|
}
|
|
const mod = await import(plugin)
|
|
// 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.
|
|
const seen = new Set<PluginInstance>()
|
|
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
|
|
if (seen.has(fn)) continue
|
|
seen.add(fn)
|
|
const init = await fn(input)
|
|
hooks.push(init)
|
|
}
|
|
}
|
|
|
|
return {
|
|
hooks,
|
|
input,
|
|
}
|
|
})
|
|
|
|
export async function trigger<
|
|
Name extends Exclude<keyof Required<Hooks>, "auth" | "event" | "tool">,
|
|
Input = Parameters<Required<Hooks>[Name]>[0],
|
|
Output = Parameters<Required<Hooks>[Name]>[1],
|
|
>(name: Name, input: Input, output: Output): Promise<Output> {
|
|
if (!name) return output
|
|
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
|
|
// give up.
|
|
// try-counter: 2
|
|
await fn(input, output)
|
|
}
|
|
return output
|
|
}
|
|
|
|
export async function list() {
|
|
return state().then((x) => x.hooks)
|
|
}
|
|
|
|
export async function init() {
|
|
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
|
|
await hook.config?.(config)
|
|
}
|
|
Bus.subscribeAll(async (input) => {
|
|
const hooks = await state().then((x) => x.hooks)
|
|
for (const hook of hooks) {
|
|
hook["event"]?.({
|
|
event: input,
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|