Skip to content

fix: export only the plugin factory from the plugin entry point - #100

Open
vtemian wants to merge 1 commit into
mainfrom
fix/plugin-export-surface
Open

fix: export only the plugin factory from the plugin entry point#100
vtemian wants to merge 1 commit into
mainfrom
fix/plugin-export-surface

Conversation

@vtemian

@vtemian vtemian commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Found while researching an e2e harness: micode currently logs a plugin load error on every opencode start, and only registers its hooks by luck.

What opencode actually does

It calls every exported function in a plugin module as a plugin factory, and installs each return value as hooks:

function rk($) { let Q=[]; for (let Y of Object.values($)) { Q.push(ok(Y)) } return Q }
async function tk($,Z,Q) { for (let J of rk($.mod)) Q.push(await J(Z, $.options)) }

src/index.ts exported four helpers alongside the plugin. Invoked as (input, options), mergePluginAgents receives options === undefined and throws:

level=ERROR message="failed to load plugin" path=file:///.../dist/index.js
  error="Object.entries requires that input parameter not be null or undefined"

opencode swallows that error and still exits 0, so nothing surfaced it.

Why it worked anyway, and why that is not reassuring

ESM namespace keys are sorted alphabetically, not by declaration order. OpenCodeConfigPlugin starts with a capital O (charCode 79), which sorts ahead of lowercase b (98), so the real plugin registered its hooks before the throw.

Rename that export to something lowercase, or add any export sorting in AN, and every micode hook disappears silently with a clean exit code.

Verified against the shipped bundle before the fix:

OpenCodeConfigPlugin    OK
buildMcpServers         OK
mergeMcpServers         OK
mergePluginAgentConfig  OK
mergePluginAgents       THREW: Object.entries requires that input parameter not be null or undefined

Note the non-throwing helpers were not harmless either. buildMcpServers(ctx, undefined) returns a map of MCP servers, which opencode would then install as a hooks object.

Fix

Move the four helpers to src/plugin-config.ts. The entry point now exports the plugin alone:

$ bun -e "import('./dist/index.js').then(m => console.log(Object.keys(m)))"
[ "OpenCodeConfigPlugin" ]

Tests import them from their new home; no behaviour changed.

Verification

489 tests pass. A new guard asserts the module's callable surface is exactly ["OpenCodeConfigPlugin"], which is the invariant rather than a proxy for it, since a helper that returns rather than throws is equally wrong.

Mutation-checked: re-exporting a single helper from src/index.ts fails the guard.

The guard deliberately does not invoke the factories. Doing so loads the PTY library and probes for btca, which leaks output into the reporter and takes 20x longer.

opencode calls every exported function in a plugin module as a plugin
factory and installs each return value as hooks. src/index.ts exported
four helpers alongside the plugin, so mergePluginAgents was being invoked
as (input, undefined) and threw on Object.entries, which opencode
swallowed while still exiting 0:

  failed to load plugin ... Object.entries requires that input parameter
  not be null or undefined

Hook registration survived only because ESM namespace keys sort
alphabetically and OpenCodeConfigPlugin starts with a capital letter,
placing it ahead of the throw. Renaming that export, or adding one
sorting earlier, would have silently dropped every hook.

Move the helpers to plugin-config.ts and leave the entry point exporting
the plugin alone.
@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant