plugin meta

actual-tui-plugins
Sebastian Herrlinger 2026-03-06 21:43:39 +01:00
parent 6e21cf05f7
commit f98ad6e078
2 changed files with 125 additions and 9 deletions

View File

@ -16,7 +16,36 @@ export namespace TuiConfig {
export const Info = TuiInfo
export type Info = z.output<typeof Info>
export type PluginMeta = {
scope: "global" | "local"
source: string
}
type PluginEntry = {
item: Config.PluginSpec
meta: PluginMeta
}
export type Info = z.output<typeof Info> & {
plugin_meta?: Record<string, PluginMeta>
}
function scope(file: string): PluginMeta["scope"] {
if (Instance.containsPath(file)) return "local"
return "global"
}
function dedupePlugin(list: PluginEntry[]) {
const seen = new Set<string>()
const result: PluginEntry[] = []
for (const item of list.toReversed()) {
const name = Config.getPluginName(item.item)
if (seen.has(name)) continue
seen.add(name)
result.push(item)
}
return result.toReversed()
}
function mergeInfo(target: Info, source: Info): Info {
const merged = mergeDeep(target, source)
@ -44,35 +73,56 @@ export namespace TuiConfig {
: await ConfigPaths.projectFiles("tui", Instance.directory, Instance.worktree)
let result: Info = {}
const plugin: PluginEntry[] = []
const apply = async (file: string) => {
const data = await loadFile(file)
result = mergeInfo(result, data)
if (!data.plugin?.length) return
const level = scope(file)
for (const item of data.plugin) {
plugin.push({
item,
meta: {
scope: level,
source: file,
},
})
}
}
for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
result = mergeInfo(result, await loadFile(file))
await apply(file)
}
if (custom) {
result = mergeInfo(result, await loadFile(custom))
await apply(custom)
log.debug("loaded custom tui config", { path: custom })
}
for (const file of projectFiles) {
result = mergeInfo(result, await loadFile(file))
await apply(file)
}
for (const dir of unique(directories)) {
if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
result = mergeInfo(result, await loadFile(file))
await apply(file)
}
}
if (existsSync(managed)) {
for (const file of ConfigPaths.fileInDirectory(managed, "tui")) {
result = mergeInfo(result, await loadFile(file))
await apply(file)
}
}
const merged = dedupePlugin(plugin)
result.keybinds = Config.Keybinds.parse(result.keybinds ?? {})
result.plugin = Config.deduplicatePlugins(result.plugin ?? [])
result.plugin = merged.map((item) => item.item)
result.plugin_meta = merged.length
? Object.fromEntries(merged.map((item) => [Config.getPluginName(item.item), item.meta]))
: undefined
const deps: Promise<void>[] = []
for (const dir of unique(directories)) {

View File

@ -458,9 +458,15 @@ test("applies file substitutions when first identical token is in a commented li
test("loads managed tui config and gives it highest precedence", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project-theme" }, null, 2))
await Bun.write(
path.join(dir, "tui.json"),
JSON.stringify({ theme: "project-theme", plugin: ["shared-plugin@1.0.0"] }, null, 2),
)
await fs.mkdir(managedConfigDir, { recursive: true })
await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-theme" }, null, 2))
await Bun.write(
path.join(managedConfigDir, "tui.json"),
JSON.stringify({ theme: "managed-theme", plugin: ["shared-plugin@2.0.0"] }, null, 2),
)
},
})
@ -469,6 +475,13 @@ test("loads managed tui config and gives it highest precedence", async () => {
fn: async () => {
const config = await TuiConfig.get()
expect(config.theme).toBe("managed-theme")
expect(config.plugin).toEqual(["shared-plugin@2.0.0"])
expect(config.plugin_meta).toEqual({
"shared-plugin": {
scope: "global",
source: path.join(managedConfigDir, "tui.json"),
},
})
},
})
})
@ -526,6 +539,12 @@ test("supports tuple plugin specs with options in tui.json", async () => {
fn: async () => {
const config = await TuiConfig.get()
expect(config.plugin).toEqual([["acme-plugin@1.2.3", { enabled: true, label: "demo" }]])
expect(config.plugin_meta).toEqual({
"acme-plugin": {
scope: "local",
source: path.join(tmp.path, "tui.json"),
},
})
},
})
})
@ -559,6 +578,53 @@ test("deduplicates tuple plugin specs by name with higher precedence winning", a
["acme-plugin@2.0.0", { source: "project" }],
["second-plugin@3.0.0", { source: "project" }],
])
expect(config.plugin_meta).toEqual({
"acme-plugin": {
scope: "local",
source: path.join(tmp.path, "tui.json"),
},
"second-plugin": {
scope: "local",
source: path.join(tmp.path, "tui.json"),
},
})
},
})
})
test("tracks global and local plugin metadata in merged tui config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(Global.Path.config, "tui.json"),
JSON.stringify({
plugin: ["global-plugin@1.0.0"],
}),
)
await Bun.write(
path.join(dir, "tui.json"),
JSON.stringify({
plugin: ["local-plugin@2.0.0"],
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await TuiConfig.get()
expect(config.plugin).toEqual(["global-plugin@1.0.0", "local-plugin@2.0.0"])
expect(config.plugin_meta).toEqual({
"global-plugin": {
scope: "global",
source: path.join(Global.Path.config, "tui.json"),
},
"local-plugin": {
scope: "local",
source: path.join(tmp.path, "tui.json"),
},
})
},
})
})