Skip to content

fix(opencode): adopt v1 default export to fix Plugin export error#1447

Closed
pratyush618 wants to merge 2 commits intoobra:mainfrom
pratyush618:fix/opencode-plugin-export-shape
Closed

fix(opencode): adopt v1 default export to fix Plugin export error#1447
pratyush618 wants to merge 2 commits intoobra:mainfrom
pratyush618:fix/opencode-plugin-export-shape

Conversation

@pratyush618
Copy link
Copy Markdown

@pratyush618 pratyush618 commented Apr 30, 2026

What problem are you trying to solve?

Issue #1444: A user (lxg07254698) on OpenCode 1.14.30 cannot install Superpowers via the documented one-liner. With

{ "plugin": ["superpowers@git+https://github.com/obra/superpowers.git"] }

OpenCode rejects the plugin at startup:

ERROR  service=plugin path=superpowers error=Plugin export is not a function failed to load plugin

The plugin never registers — no skills, no bootstrap, no hooks.

Root cause is the export shape in .opencode/plugins/superpowers.js. OpenCode's plugin loader (packages/opencode/src/plugin/index.ts + shared.ts in sst/opencode v1.14.30) tries two paths in order:

  1. v1 (preferred) — `readV1Plugin` looks for `mod.default` to be an object containing at least one of `id` / `server` / `tui`. If found, it calls `default.server(input, options)`.
  2. Legacy fallback — `getLegacyPlugins` iterates `Object.values(mod)` and throws `TypeError("Plugin export is not a function")` if any export isn't a function or `{ server: function }`.

Our plugin had no default export and one named export (`export const SuperpowersPlugin`). It happened to load via the legacy path on Node/Bun on Linux locally, but the legacy iteration is the brittle path — any non-function export added later silently breaks the plugin, and the affected user's environment is hitting that throw today.

What does this PR change?

Converts `.opencode/plugins/superpowers.js` to OpenCode's documented v1 plugin shape: a single `export default { id: "superpowers", server }`. The plugin function body and returned hooks (`config`, `experimental.chat.messages.transform`) are unchanged — only the export shape moves.

Is this change appropriate for the core library?

Yes. `.opencode/plugins/superpowers.js` is the core OpenCode harness entrypoint that ships with Superpowers. The fix benefits every OpenCode user installing Superpowers via the documented method. It is not project-specific, not a third-party integration, not a new skill.

What alternatives did you consider?

  • Leave the named export, add a default that aliases it — works but having both exports makes the legacy iteration path double-invoke the plugin (it would push the plugin into the hooks list twice once it walked both `mod.default.server` and `mod.SuperpowersPlugin`). Safe in v1.14.30 because v1 detection short-circuits before legacy runs, but leaves a footgun for any pure-legacy loader. Rejected as not "production grade."
  • Patch package.json `main` to a thin v1 wrapper — extra indirection, two files to keep in sync, no behavioral upside.
  • Default-export the function directly (`export default async () => {...}`) — `mod.default` would be a function, not a record, so `readV1Plugin` would skip it and we'd be back on the legacy path. Defeats the purpose.

The chosen approach is the only one that puts the plugin on the deterministic v1 path with a stable id and zero runtime overhead.

Does this PR contain multiple unrelated changes?

No. One file, one logical change, split into two commits per the contributor's commit hygiene preference: (1) add the v1 default export alongside the named export so v1.14.30 starts working immediately, (2) drop the now-unused named export. The intermediate commit is independently safe — v1 detection picks the default and the legacy path is never entered.

Existing PRs

  • I have reviewed all open AND closed PRs for duplicates or prior art
  • Related PRs: none found.

`gh search` for "Plugin export is not a function", "OpenCode plugin export", and "plugin failed to load" against this repo returned only #1444 itself plus unrelated work (OpenCode docs cleanups #305, #608, #847, #890; Windows path bug #1242; `config` hook silently ignored #1087; bootstrap caching #1232). None modify the plugin's export shape.

Environment tested

Harness (e.g. Claude Code, Cursor) Harness version Model Model version/ID
OpenCode plugin loader (replayed locally per sst/opencode v1.14.30 source) v1.14.30 (loader logic, Bun 1.3.11 host) n/a (loader-level fix, not a model-behavior change) n/a

I do not have an OpenCode 1.14.30 install here to run end-to-end against the issue reporter's exact setup. The verification I ran instead replays the loader's exact `readV1Plugin` + `getLegacyPlugins` logic from `sst/opencode@v1.14.30` against the modified plugin module, on both Node 22 and Bun 1.3.11.

Evaluation

This is a loader-level export-shape fix, not a behavior or skill change, so traditional eval sessions don't apply. The "before/after" is binary: the plugin either loads or throws `TypeError`. Verification:

  • Before (current main, `export const SuperpowersPlugin`): `Object.values(mod) = [AsyncFunction]` — passes legacy iteration locally on Linux/Node/Bun, but the issue reporter's environment trips the same iteration's throw.
  • After (`export default { id, server }`): `mod.default` is a record with `id` + `server`, `readV1Plugin` returns it, the v1 path is taken, legacy iteration is never entered. I confirmed both v1 detection and legacy fallback succeed against the new module by replaying the loader code:
    ```
    v1 detected: true { id: "superpowers", server: "function" }
    legacy fallback also OK: 1 plugin(s)
    ```
  • `bash tests/opencode/test-plugin-loading.sh` — all 6 tests pass.
  • `node --check .opencode/plugins/superpowers.js` — passes.
  • `npm pack` + install of the resulting tarball into a fresh project then `await import('superpowers')`: `m.default = { id: 'superpowers', server: [AsyncFunction] }`, no leftover named exports.

Rigor

  • If this is a skills change: I used `superpowers:writing-skills` and completed adversarial pressure testing (paste results below) — N/A, no skill content modified.
  • This change was tested adversarially, not just on the happy path — replayed the exact loader logic that throws the user's error, and verified both the v1 path and legacy fallback accept the new shape.
  • I did not modify carefully-tuned content (Red Flags table, rationalizations, "human partner" language) without extensive evals showing the change is an improvement — no skill content touched.

Resolves #1444 — OpenCode 1.14.30's plugin loader checks mod.default
first via readV1Plugin. Without a default export, loading fell through
to the legacy iteration path which throws 'Plugin export is not a
function' in some environments. Exposing { id, server } via default
takes the deterministic v1 path.
Now that the v1 default export is the canonical entrypoint, the named
export is unused (no callers in repo) and would double-invoke the
plugin under any pure-legacy loader path.
@obra
Copy link
Copy Markdown
Owner

obra commented Apr 30, 2026

What I read from this PR is that you made the change blind and have not tested it. I am unable to reproduce the original problem. I'm going to close this PR for now. If you can prove that you reproduced the user's problem, please feel free to reopen it.

@obra obra closed this Apr 30, 2026
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.

2 participants