Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,17 +510,31 @@ export namespace Config {

/**
* Extracts a canonical plugin name from a plugin specifier.
* - For file:// URLs: extracts filename without extension
* - For file:// URLs pointing into node_modules: extracts npm package name
* - For other file:// URLs: extracts filename without extension
* - For npm packages: extracts package name without version
*
* @example
* getPluginName("file:///path/to/node_modules/oh-my-opencode/dist/index.js") // "oh-my-opencode"
* getPluginName("file:///path/to/node_modules/@scope/pkg/index.js") // "@scope/pkg"
* getPluginName("file:///path/to/plugin/foo.js") // "foo"
* getPluginName("oh-my-opencode@2.4.3") // "oh-my-opencode"
* getPluginName("@scope/pkg@1.0.0") // "@scope/pkg"
*/
export function getPluginName(plugin: string): string {
if (plugin.startsWith("file://")) {
return path.parse(new URL(plugin).pathname).name
const parsed = new URL(plugin).pathname
const nm = parsed.lastIndexOf("/node_modules/")
if (nm !== -1) {
const after = parsed.substring(nm + "/node_modules/".length)
// scoped packages: @scope/pkg/dist/index.js → @scope/pkg
if (after.startsWith("@")) {
const parts = after.split("/")
return parts.slice(0, 2).join("/")
}
return after.split("/")[0]
}
return path.parse(parsed).name
}
const lastAt = plugin.lastIndexOf("@")
if (lastAt > 0) {
Expand Down
25 changes: 25 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,19 @@ describe("getPluginName", () => {
expect(Config.getPluginName("file:///some/path/my-plugin.js")).toBe("my-plugin")
})

test("extracts package name from file:// URL inside node_modules", () => {
expect(Config.getPluginName("file:///home/user/.config/opencode/node_modules/oh-my-opencode/dist/index.js")).toBe(
"oh-my-opencode",
)
expect(
Config.getPluginName("file:///home/user/.config/opencode/node_modules/opencode-openai-codex-auth/index.ts"),
).toBe("opencode-openai-codex-auth")
expect(Config.getPluginName("file:///project/node_modules/@scope/plugin/dist/index.js")).toBe("@scope/plugin")
expect(Config.getPluginName("file:///project/node_modules/@different-ai/opencode-browser/index.js")).toBe(
"@different-ai/opencode-browser",
)
})

test("extracts name from npm package with version", () => {
expect(Config.getPluginName("oh-my-opencode@2.4.3")).toBe("oh-my-opencode")
expect(Config.getPluginName("some-plugin@1.0.0")).toBe("some-plugin")
Expand Down Expand Up @@ -1781,6 +1794,18 @@ describe("deduplicatePlugins", () => {
expect(result).toEqual(["a-plugin@1.0.0", "b-plugin@1.0.0", "c-plugin@1.0.0"])
})

test("does not treat resolved node_modules index.js files as duplicates", () => {
const plugins = [
"file:///config/node_modules/oh-my-opencode/dist/index.js",
"file:///config/node_modules/opencode-openai-codex-auth/index.ts",
"file:///config/node_modules/@scope/plugin/dist/index.js",
]

const result = Config.deduplicatePlugins(plugins)

expect(result.length).toBe(3)
})

test("local plugin directory overrides global opencode.json plugin", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
Expand Down
Loading