Compare commits

...

1 Commits

Author SHA1 Message Date
Dax Raad
440eb76e52 fix(config): install plugin deps per config directory 2026-02-05 16:22:13 -05:00

View File

@@ -145,11 +145,20 @@ export namespace Config {
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
}
const deps = []
const deps: Promise<void>[] = []
for (const dir of unique(directories)) {
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
const files = ["opencode.jsonc", "opencode.json"]
const isConfigDir = dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR || dir === Global.Path.config
const configs = isConfigDir
? await Promise.all(files.map((file) => loadFile(path.join(dir, file), { resolvePlugins: false })))
: []
const plugins = configs.flatMap((config) => config.plugin ?? [])
const shouldInstall = await needsInstall(dir, plugins)
if (shouldInstall) await installDependencies(dir, plugins)
if (isConfigDir) {
for (const file of files) {
log.debug(`loading config from ${path.join(dir, file)}`)
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
// to satisfy the type checker
@@ -159,13 +168,6 @@ export namespace Config {
}
}
deps.push(
iife(async () => {
const shouldInstall = await needsInstall(dir)
if (shouldInstall) await installDependencies(dir)
}),
)
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
result.agent = mergeDeep(result.agent, await loadAgent(dir))
result.agent = mergeDeep(result.agent, await loadMode(dir))
@@ -247,7 +249,7 @@ export namespace Config {
await Promise.all(deps)
}
export async function installDependencies(dir: string) {
export async function installDependencies(dir: string, plugins: string[] = []) {
const pkg = path.join(dir, "package.json")
const targetVersion = Installation.isLocal() ? "*" : Installation.VERSION
@@ -258,6 +260,10 @@ export namespace Config {
...json.dependencies,
"@opencode-ai/plugin": targetVersion,
}
const pluginDeps = deps(plugins)
for (const [name, version] of Object.entries(pluginDeps)) {
json.dependencies[name] = version
}
await Bun.write(pkg, JSON.stringify(json, null, 2))
await new Promise((resolve) => setTimeout(resolve, 3000))
@@ -286,7 +292,7 @@ export namespace Config {
}
}
async function needsInstall(dir: string) {
async function needsInstall(dir: string, plugins: string[] = []) {
// Some config dirs may be read-only.
// Installing deps there will fail; skip installation in that case.
const writable = await isWritable(dir)
@@ -318,8 +324,27 @@ export namespace Config {
})
return true
}
if (depVersion === targetVersion) return false
return true
if (depVersion !== targetVersion) return true
const pluginDeps = deps(plugins)
for (const [name, version] of Object.entries(pluginDeps)) {
if (dependencies[name] !== version) return true
}
return false
}
function deps(items: string[]) {
const result: Record<string, string> = {}
for (const item of items) {
if (!item) continue
if (item.startsWith("file://")) continue
if (item.startsWith("./") || item.startsWith("../") || item.startsWith("/") || item.startsWith("~")) continue
const lastAt = item.lastIndexOf("@")
const pkg = lastAt > 0 ? item.substring(0, lastAt) : item
const version = lastAt > 0 ? item.substring(lastAt + 1) : "latest"
result[pkg] = version
}
return result
}
function rel(item: string, patterns: string[]) {
@@ -1216,7 +1241,7 @@ export namespace Config {
return result
})
async function loadFile(filepath: string): Promise<Info> {
async function loadFile(filepath: string, options: { resolvePlugins?: boolean } = {}): Promise<Info> {
log.info("loading", { path: filepath })
let text = await Bun.file(filepath)
.text()
@@ -1225,10 +1250,10 @@ export namespace Config {
throw new JsonError({ path: filepath }, { cause: err })
})
if (!text) return {}
return load(text, filepath)
return load(text, filepath, options)
}
async function load(text: string, configFilepath: string) {
async function load(text: string, configFilepath: string, options: { resolvePlugins?: boolean } = {}) {
const original = text
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
@@ -1304,7 +1329,7 @@ export namespace Config {
await Bun.write(configFilepath, updated).catch(() => {})
}
const data = parsed.data
if (data.plugin) {
if (data.plugin && options.resolvePlugins !== false) {
for (let i = 0; i < data.plugin.length; i++) {
const plugin = data.plugin[i]
try {