mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(tui): deduplicate plugin targets (#49112)
This commit is contained in:
@@ -29,7 +29,7 @@ import { useData } from "../context/data"
|
||||
import { errorMessage } from "../util/error"
|
||||
import { createPluginContext, usePluginHost, type Dispose, type RegisteredSlot, type SlotRender } from "./api"
|
||||
import { createSourceWatcher } from "./watch"
|
||||
import { discoverPluginTargets, localSource } from "./discovery"
|
||||
import { discoverPluginTargets, localSource, mergePluginTargets } from "./discovery"
|
||||
import { createPluginSources } from "./source"
|
||||
import { isMissingPath } from "../util/config-directories"
|
||||
import { createMarkdownRenderer } from "./markdown"
|
||||
@@ -285,23 +285,28 @@ export function PluginProvider(props: ParentProps<{ packages: PackageSource; dir
|
||||
await trace("watch", { reconciliation: id, directories: props.directories }, () =>
|
||||
Promise.all(props.directories.map(watcher.wait)).then(() => undefined),
|
||||
)
|
||||
const entries = [
|
||||
...(
|
||||
await trace("discover", { reconciliation: id, directories: props.directories }, () =>
|
||||
discoverPluginTargets(props.directories),
|
||||
)
|
||||
).map((entry) => ({
|
||||
entry,
|
||||
install: true,
|
||||
optional: true,
|
||||
})),
|
||||
...serverTuiPlugins().map((plugin) => ({
|
||||
entry: plugin.source.type === "package" ? plugin.source.target : path.dirname(plugin.source.path),
|
||||
install: false,
|
||||
optional: true,
|
||||
})),
|
||||
...(config.data.plugins ?? []).map((entry) => ({ entry, install: true, optional: false })),
|
||||
]
|
||||
// Discovery admits TUI-only plugins while server inventory carries combined plugins.
|
||||
// Their overlap is intentional; explicit configuration remains the final authority.
|
||||
const entries = mergePluginTargets(
|
||||
[
|
||||
...(
|
||||
await trace("discover", { reconciliation: id, directories: props.directories }, () =>
|
||||
discoverPluginTargets(props.directories),
|
||||
)
|
||||
).map((entry) => ({
|
||||
entry,
|
||||
install: true,
|
||||
optional: true,
|
||||
})),
|
||||
...serverTuiPlugins().map((plugin) => ({
|
||||
entry: plugin.source.type === "package" ? plugin.source.target : path.dirname(plugin.source.path),
|
||||
install: false,
|
||||
optional: true,
|
||||
})),
|
||||
...(config.data.plugins ?? []).map((entry) => ({ entry, install: true, optional: false })),
|
||||
],
|
||||
directory,
|
||||
)
|
||||
|
||||
// Resolve: fold entries into one desired generation. A source that fails
|
||||
// to import keeps its running previous version and only reports failure.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { readdir, stat } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
export { localSource } from "@opencode/plugin/source"
|
||||
import { localSource } from "@opencode/plugin/source"
|
||||
import { isMissingPath, localProjectDirectory, projectConfigDirectories } from "../util/config-directories"
|
||||
|
||||
export { localSource }
|
||||
|
||||
export async function localPluginDirectories(cwd: string, configDirectory: string) {
|
||||
const projectDirectory = await localProjectDirectory(cwd)
|
||||
const projectConfig = path.join(projectDirectory, ".opencode")
|
||||
@@ -49,3 +51,17 @@ export async function discoverPluginTargets(directories: string[]) {
|
||||
)
|
||||
).flat()
|
||||
}
|
||||
|
||||
export function mergePluginTargets<const Target extends { readonly entry: string | { readonly package: string } }>(
|
||||
targets: readonly Target[],
|
||||
directory: string,
|
||||
) {
|
||||
return [
|
||||
...targets
|
||||
.reduce((result, target) => {
|
||||
const value = typeof target.entry === "string" ? target.entry : target.entry.package
|
||||
return result.set(localSource(value, directory)?.href ?? value, target)
|
||||
}, new Map<string, Target>())
|
||||
.values(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { mergePluginTargets } from "../src/plugin/discovery"
|
||||
|
||||
test("deduplicates equivalent local plugin targets while retaining the final source", () => {
|
||||
const directory = "/project"
|
||||
const discovered = { entry: "/project/.opencode/plugins/example", install: true, optional: true }
|
||||
const server = { entry: "./.opencode/plugins/example", install: false, optional: true }
|
||||
|
||||
expect(mergePluginTargets([discovered, server], directory)).toEqual([server])
|
||||
})
|
||||
|
||||
test("preserves the first target position when a later source overrides it", () => {
|
||||
const first = { entry: "example", install: true, optional: true }
|
||||
const other = { entry: "other", install: true, optional: true }
|
||||
const configured = { entry: { package: "example", options: { enabled: true } }, install: true, optional: false }
|
||||
|
||||
expect(mergePluginTargets([first, other, configured], "/project")).toEqual([configured, other])
|
||||
})
|
||||
Reference in New Issue
Block a user