index.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import type { Hooks, PluginInput, Plugin as PluginInstance } from "@opencode-ai/plugin"
  2. import { Config } from "../config/config"
  3. import { Bus } from "../bus"
  4. import { Log } from "../util/log"
  5. import { createOpencodeClient } from "@opencode-ai/sdk"
  6. import { Server } from "../server/server"
  7. import { BunProc } from "../bun"
  8. import { Instance } from "../project/instance"
  9. import { Flag } from "../flag/flag"
  10. import { CodexAuthPlugin } from "./codex"
  11. import { Session } from "../session"
  12. import { NamedError } from "@opencode-ai/util/error"
  13. import { CopilotAuthPlugin } from "./copilot"
  14. import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth"
  15. export namespace Plugin {
  16. const log = Log.create({ service: "plugin" })
  17. const BUILTIN = ["[email protected]"]
  18. // Built-in plugins that are directly imported (not installed from npm)
  19. const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin]
  20. const state = Instance.state(async () => {
  21. const client = createOpencodeClient({
  22. baseUrl: "http://localhost:4096",
  23. // @ts-ignore - fetch type incompatibility
  24. fetch: async (...args) => Server.App().fetch(...args),
  25. })
  26. const config = await Config.get()
  27. const hooks: Hooks[] = []
  28. const input: PluginInput = {
  29. client,
  30. project: Instance.project,
  31. worktree: Instance.worktree,
  32. directory: Instance.directory,
  33. serverUrl: Server.url(),
  34. $: Bun.$,
  35. }
  36. for (const plugin of INTERNAL_PLUGINS) {
  37. log.info("loading internal plugin", { name: plugin.name })
  38. const init = await plugin(input)
  39. hooks.push(init)
  40. }
  41. const plugins: string[] = []
  42. if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
  43. plugins.push(...BUILTIN)
  44. }
  45. plugins.push(...(config.plugin ?? []))
  46. if (plugins.length) await Config.waitForDependencies()
  47. for (let plugin of plugins) {
  48. // ignore old codex plugin since it is supported first party now
  49. if (plugin.includes("opencode-openai-codex-auth") || plugin.includes("opencode-copilot-auth")) continue
  50. log.info("loading plugin", { path: plugin })
  51. if (!plugin.startsWith("file://")) {
  52. const lastAtIndex = plugin.lastIndexOf("@")
  53. const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
  54. const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
  55. const builtin = BUILTIN.some((x) => x.startsWith(pkg + "@"))
  56. plugin = await BunProc.install(pkg, version).catch((err) => {
  57. if (!builtin) throw err
  58. const message = err instanceof Error ? err.message : String(err)
  59. log.error("failed to install builtin plugin", {
  60. pkg,
  61. version,
  62. error: message,
  63. })
  64. Bus.publish(Session.Event.Error, {
  65. error: new NamedError.Unknown({
  66. message: `Failed to install built-in plugin ${pkg}@${version}: ${message}`,
  67. }).toObject(),
  68. })
  69. return ""
  70. })
  71. if (!plugin) continue
  72. }
  73. const mod = await import(plugin)
  74. // Prevent duplicate initialization when plugins export the same function
  75. // as both a named export and default export (e.g., `export const X` and `export default X`).
  76. // Object.entries(mod) would return both entries pointing to the same function reference.
  77. const seen = new Set<PluginInstance>()
  78. for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
  79. if (seen.has(fn)) continue
  80. seen.add(fn)
  81. const init = await fn(input)
  82. hooks.push(init)
  83. }
  84. }
  85. return {
  86. hooks,
  87. input,
  88. }
  89. })
  90. export async function trigger<
  91. Name extends Exclude<keyof Required<Hooks>, "auth" | "event" | "tool">,
  92. Input = Parameters<Required<Hooks>[Name]>[0],
  93. Output = Parameters<Required<Hooks>[Name]>[1],
  94. >(name: Name, input: Input, output: Output): Promise<Output> {
  95. if (!name) return output
  96. for (const hook of await state().then((x) => x.hooks)) {
  97. const fn = hook[name]
  98. if (!fn) continue
  99. // @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
  100. // give up.
  101. // try-counter: 2
  102. await fn(input, output)
  103. }
  104. return output
  105. }
  106. export async function list() {
  107. return state().then((x) => x.hooks)
  108. }
  109. export async function init() {
  110. const hooks = await state().then((x) => x.hooks)
  111. const config = await Config.get()
  112. for (const hook of hooks) {
  113. // @ts-expect-error this is because we haven't moved plugin to sdk v2
  114. await hook.config?.(config)
  115. }
  116. Bus.subscribeAll(async (input) => {
  117. const hooks = await state().then((x) => x.hooks)
  118. for (const hook of hooks) {
  119. hook["event"]?.({
  120. event: input,
  121. })
  122. }
  123. })
  124. }
  125. }