feat(config): deduplicate plugins by name with priority-based resolution (#5957)

This commit is contained in:
Jeon Suyeol
2026-01-09 17:11:24 +09:00
committed by GitHub
parent 13305966e5
commit 8e3ab4afa7
2 changed files with 143 additions and 1 deletions

View File

@@ -178,6 +178,8 @@ export namespace Config {
result.compaction = { ...result.compaction, prune: false }
}
result.plugin = deduplicatePlugins(result.plugin ?? [])
return {
config: result,
directories,
@@ -332,6 +334,58 @@ export namespace Config {
return plugins
}
/**
* Extracts a canonical plugin name from a plugin specifier.
* - For file:// URLs: extracts filename without extension
* - For npm packages: extracts package name without version
*
* @example
* 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 lastAt = plugin.lastIndexOf("@")
if (lastAt > 0) {
return plugin.substring(0, lastAt)
}
return plugin
}
/**
* Deduplicates plugins by name, with later entries (higher priority) winning.
* Priority order (highest to lowest):
* 1. Local plugin/ directory
* 2. Local opencode.json
* 3. Global plugin/ directory
* 4. Global opencode.json
*
* Since plugins are added in low-to-high priority order,
* we reverse, deduplicate (keeping first occurrence), then restore order.
*/
export function deduplicatePlugins(plugins: string[]): string[] {
// seenNames: canonical plugin names for duplicate detection
// e.g., "oh-my-opencode", "@scope/pkg"
const seenNames = new Set<string>()
// uniqueSpecifiers: full plugin specifiers to return
// e.g., "oh-my-opencode@2.4.3", "file:///path/to/plugin.js"
const uniqueSpecifiers: string[] = []
for (const specifier of plugins.toReversed()) {
const name = getPluginName(specifier)
if (!seenNames.has(name)) {
seenNames.add(name)
uniqueSpecifiers.push(specifier)
}
}
return uniqueSpecifiers.toReversed()
}
export const McpLocal = z
.object({
type: z.literal("local").describe("Type of MCP server connection"),