refactor(plugin): migrate to V1 PluginModule format

Convert the default export from the legacy callable Plugin to the V1 PluginModule shape (`{ id, server }`) documented by opencode's plugin SDK. This aligns oh-my-openagent with the canonical plugin entry format and removes plugin-format legacy debt.

Drop the module-level `let activePluginDispose` cleanup guard: opencode instantiates plugins in a scope-bound Layer per server and dynamic-imports fresh modules on reload, so module-level state is not preserved across reloads. Individual managers already register their own SIGINT/SIGTERM cleanup (skill-mcp-manager, background-agent process-cleanup), so the orphaned createPluginDispose call provided no runtime value.

Remove the non-standard `name` return field that opencode's Hooks interface does not include. The PluginModule's `id` now carries the plugin identity instead.

Drop the unused `lspManager` import that only fed the orphaned createPluginDispose.

Update src/index.test.ts and src/index.telemetry.test.ts to call `plugin.server(ctx)` instead of `plugin(ctx)`, and assert the V1 shape.
This commit is contained in:
YeonGyu-Kim
2026-04-18 01:37:46 +09:00
parent 2892ca4adf
commit 1b10ab36d2
3 changed files with 40 additions and 40 deletions
+4 -3
View File
@@ -125,12 +125,13 @@ describe("OhMyOpenCodePlugin telemetry isolation", () => {
const { default: plugin } = await import(`./index?telemetry=${Date.now()}-${Math.random()}`) const { default: plugin } = await import(`./index?telemetry=${Date.now()}-${Math.random()}`)
// when // when
const result = await plugin({ const result = await plugin.server({
directory: "/tmp/project", directory: "/tmp/project",
client: {}, client: {},
} as Parameters<typeof plugin>[0]) } as Parameters<typeof plugin.server>[0])
// then // then
expect(result).toMatchObject({ name: "oh-my-openagent" }) expect(typeof result).toBe("object")
expect(result).not.toBeNull()
}) })
}) })
+15 -6
View File
@@ -257,7 +257,7 @@ const mockCreatePluginInterface = mock(() => ({}))
const mockInitializeOpenClaw = mock(async () => {}) const mockInitializeOpenClaw = mock(async () => {})
const mockStartTmuxCheck = mock(() => {}) const mockStartTmuxCheck = mock(() => {})
let OhMyOpenCodePlugin: (typeof import("./index"))["default"] let pluginModule: (typeof import("./index"))["default"]
function installIndexModuleMocks(): void { function installIndexModuleMocks(): void {
mock.module("./cli/config-manager/config-context", () => ({ mock.module("./cli/config-manager/config-context", () => ({
@@ -337,7 +337,7 @@ describe("OhMyOpenCodePlugin", () => {
beforeEach(async () => { beforeEach(async () => {
mock.restore() mock.restore()
installIndexModuleMocks() installIndexModuleMocks()
;({ default: OhMyOpenCodePlugin } = await importFreshIndexModule()) ;({ default: pluginModule } = await importFreshIndexModule())
mockInitConfigContext.mockClear() mockInitConfigContext.mockClear()
mockDetectExternalSkillPlugin.mockClear() mockDetectExternalSkillPlugin.mockClear()
mockGetSkillPluginConflictWarning.mockClear() mockGetSkillPluginConflictWarning.mockClear()
@@ -375,10 +375,10 @@ describe("OhMyOpenCodePlugin", () => {
}) })
// when // when
await OhMyOpenCodePlugin({ await pluginModule.server({
directory: "/tmp/project", directory: "/tmp/project",
client: {}, client: {},
} as Parameters<typeof OhMyOpenCodePlugin>[0]) } as Parameters<typeof pluginModule.server>[0])
// then // then
expect(mockInitializeOpenClaw).toHaveBeenCalledTimes(1) expect(mockInitializeOpenClaw).toHaveBeenCalledTimes(1)
@@ -390,12 +390,21 @@ describe("OhMyOpenCodePlugin", () => {
mockLoadPluginConfig.mockReturnValue({}) mockLoadPluginConfig.mockReturnValue({})
// when // when
await OhMyOpenCodePlugin({ await pluginModule.server({
directory: "/tmp/project", directory: "/tmp/project",
client: {}, client: {},
} as Parameters<typeof OhMyOpenCodePlugin>[0]) } as Parameters<typeof pluginModule.server>[0])
// then // then
expect(mockInitializeOpenClaw).not.toHaveBeenCalled() expect(mockInitializeOpenClaw).not.toHaveBeenCalled()
}) })
it("exports a V1 PluginModule shape with id and server", () => {
// given the plugin module is loaded
// when inspecting the default export
// then it has the expected V1 shape
expect(typeof pluginModule).toBe("object")
expect(pluginModule.id).toBe("oh-my-openagent")
expect(typeof pluginModule.server).toBe("function")
})
}) })
+21 -31
View File
@@ -1,5 +1,5 @@
import { initConfigContext } from "./cli/config-manager/config-context" import { initConfigContext } from "./cli/config-manager/config-context"
import type { Plugin } from "@opencode-ai/plugin" import type { Hooks, Plugin, PluginModule } from "@opencode-ai/plugin"
import type { HookName } from "./config" import type { HookName } from "./config"
@@ -9,7 +9,6 @@ import { createRuntimeTmuxConfig, isTmuxIntegrationEnabled } from "./create-runt
import { createTools } from "./create-tools" import { createTools } from "./create-tools"
import { initializeOpenClaw } from "./openclaw" import { initializeOpenClaw } from "./openclaw"
import { createPluginInterface } from "./plugin-interface" import { createPluginInterface } from "./plugin-interface"
import { createPluginDispose, type PluginDispose } from "./plugin-dispose"
import { loadPluginConfig } from "./plugin-config" import { loadPluginConfig } from "./plugin-config"
import { createModelCacheState } from "./plugin-state" import { createModelCacheState } from "./plugin-state"
@@ -17,27 +16,23 @@ import { createFirstMessageVariantGate } from "./shared/first-message-variant"
import { injectServerAuthIntoClient, log, logLegacyPluginStartupWarning } from "./shared" import { injectServerAuthIntoClient, log, logLegacyPluginStartupWarning } from "./shared"
import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector" import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector"
import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash" import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
import { lspManager } from "./tools/lsp/client"
import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog" import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog"
let activePluginDispose: PluginDispose | null = null const serverPlugin: Plugin = async (input, _options): Promise<Hooks> => {
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
initConfigContext("opencode", null) initConfigContext("opencode", null)
log("[OhMyOpenCodePlugin] ENTRY - plugin loading", { log("[OhMyOpenCodePlugin] ENTRY - plugin loading", {
directory: ctx.directory, directory: input.directory,
}) })
logLegacyPluginStartupWarning() logLegacyPluginStartupWarning()
const skillPluginCheck = detectExternalSkillPlugin(ctx.directory) const skillPluginCheck = detectExternalSkillPlugin(input.directory)
if (skillPluginCheck.detected && skillPluginCheck.pluginName) { if (skillPluginCheck.detected && skillPluginCheck.pluginName) {
console.warn(getSkillPluginConflictWarning(skillPluginCheck.pluginName)) console.warn(getSkillPluginConflictWarning(skillPluginCheck.pluginName))
} }
injectServerAuthIntoClient(ctx.client) injectServerAuthIntoClient(input.client)
await activePluginDispose?.()
const pluginConfig = loadPluginConfig(ctx.directory, ctx) const pluginConfig = loadPluginConfig(input.directory, input)
const posthog = createPluginPostHog() const posthog = createPluginPostHog()
const distinctId = getPostHogDistinctId() const distinctId = getPostHogDistinctId()
@@ -78,7 +73,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const modelCacheState = createModelCacheState() const modelCacheState = createModelCacheState()
const managers = createManagers({ const managers = createManagers({
ctx, ctx: input,
pluginConfig, pluginConfig,
tmuxConfig, tmuxConfig,
modelCacheState, modelCacheState,
@@ -86,13 +81,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
}) })
const toolsResult = await createTools({ const toolsResult = await createTools({
ctx, ctx: input,
pluginConfig, pluginConfig,
managers, managers,
}) })
const hooks = createHooks({ const hooks = createHooks({
ctx, ctx: input,
pluginConfig, pluginConfig,
modelCacheState, modelCacheState,
backgroundManager: managers.backgroundManager, backgroundManager: managers.backgroundManager,
@@ -102,15 +97,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
availableSkills: toolsResult.availableSkills, availableSkills: toolsResult.availableSkills,
}) })
const dispose = createPluginDispose({
backgroundManager: managers.backgroundManager,
skillMcpManager: managers.skillMcpManager,
lspManager,
disposeHooks: hooks.disposeHooks,
})
const pluginInterface = createPluginInterface({ const pluginInterface = createPluginInterface({
ctx, ctx: input,
pluginConfig, pluginConfig,
firstMessageVariantGate, firstMessageVariantGate,
managers, managers,
@@ -118,30 +106,32 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
tools: toolsResult.filteredTools, tools: toolsResult.filteredTools,
}) })
activePluginDispose = dispose
return { return {
name: "oh-my-openagent",
...pluginInterface, ...pluginInterface,
"experimental.session.compacting": async ( "experimental.session.compacting": async (
_input: { sessionID: string }, compactingInput: { sessionID: string },
output: { context: string[] }, output: { context: string[] },
): Promise<void> => { ): Promise<void> => {
await hooks.compactionContextInjector?.capture(_input.sessionID) await hooks.compactionContextInjector?.capture(compactingInput.sessionID)
await hooks.compactionTodoPreserver?.capture(_input.sessionID) await hooks.compactionTodoPreserver?.capture(compactingInput.sessionID)
await hooks.claudeCodeHooks?.["experimental.session.compacting"]?.( await hooks.claudeCodeHooks?.["experimental.session.compacting"]?.(
_input, compactingInput,
output, output,
) )
if (hooks.compactionContextInjector) { if (hooks.compactionContextInjector) {
output.context.push(hooks.compactionContextInjector.inject(_input.sessionID)) output.context.push(hooks.compactionContextInjector.inject(compactingInput.sessionID))
} }
}, },
} }
} }
export default OhMyOpenCodePlugin const pluginModule: PluginModule = {
id: "oh-my-openagent",
server: serverPlugin,
}
export default pluginModule
export type { export type {
OhMyOpenCodeConfig, OhMyOpenCodeConfig,