Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 6f00286

Browse files
fix: support scoped npm plugins (anomalyco#3785)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
1 parent aec44ab commit 6f00286

3 files changed

Lines changed: 63 additions & 2 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ playground
1111
tmp
1212
dist
1313
.turbo
14+
**/.serena
15+
.serena/

packages/opencode/src/plugin/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ export namespace Plugin {
3434
for (let plugin of plugins) {
3535
log.info("loading plugin", { path: plugin })
3636
if (!plugin.startsWith("file://")) {
37-
const [pkg, version] = plugin.split("@")
38-
plugin = await BunProc.install(pkg, version ?? "latest")
37+
const lastAtIndex = plugin.lastIndexOf("@")
38+
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
39+
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
40+
plugin = await BunProc.install(pkg, version)
3941
}
4042
const mod = await import(plugin)
4143
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {

packages/opencode/test/config/config.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Instance } from "../../src/project/instance"
44
import { tmpdir } from "../fixture/fixture"
55
import path from "path"
66
import fs from "fs/promises"
7+
import { pathToFileURL } from "url"
78

89
test("loads config with defaults when no files exist", async () => {
910
await using tmp = await tmpdir()
@@ -350,3 +351,59 @@ test("gets config directories", async () => {
350351
},
351352
})
352353
})
354+
355+
test("resolves scoped npm plugins in config", async () => {
356+
await using tmp = await tmpdir({
357+
init: async (dir) => {
358+
const pluginDir = path.join(dir, "node_modules", "@scope", "plugin")
359+
await fs.mkdir(pluginDir, { recursive: true })
360+
361+
await Bun.write(
362+
path.join(dir, "package.json"),
363+
JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2),
364+
)
365+
366+
await Bun.write(
367+
path.join(pluginDir, "package.json"),
368+
JSON.stringify(
369+
{
370+
name: "@scope/plugin",
371+
version: "1.0.0",
372+
type: "module",
373+
main: "./index.js",
374+
},
375+
null,
376+
2,
377+
),
378+
)
379+
380+
await Bun.write(path.join(pluginDir, "index.js"), "export default {}\n")
381+
382+
await Bun.write(
383+
path.join(dir, "opencode.json"),
384+
JSON.stringify(
385+
{ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] },
386+
null,
387+
2,
388+
),
389+
)
390+
},
391+
})
392+
393+
await Instance.provide({
394+
directory: tmp.path,
395+
fn: async () => {
396+
const config = await Config.get()
397+
const pluginEntries = config.plugin ?? []
398+
399+
const baseUrl = pathToFileURL(path.join(tmp.path, "opencode.json")).href
400+
const expected = import.meta.resolve("@scope/plugin", baseUrl)
401+
402+
expect(pluginEntries.includes(expected)).toBe(true)
403+
404+
const scopedEntry = pluginEntries.find((entry) => entry === expected)
405+
expect(scopedEntry).toBeDefined()
406+
expect(scopedEntry?.includes("/node_modules/@scope/plugin/")).toBe(true)
407+
},
408+
})
409+
})

0 commit comments

Comments
 (0)