summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/config/config.ts21
-rw-r--r--packages/opencode/test/config/config.test.ts81
2 files changed, 92 insertions, 10 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 5d95814d7..077a9dd11 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -22,13 +22,14 @@ import { ConfigMarkdown } from "./markdown"
export namespace Config {
const log = Log.create({ service: "config" })
- // Custom merge function that concatenates plugin arrays instead of replacing them
- function mergeConfigWithPlugins(target: Info, source: Info): Info {
+ // Custom merge function that concatenates array fields instead of replacing them
+ function mergeConfigConcatArrays(target: Info, source: Info): Info {
const merged = mergeDeep(target, source)
- // If both configs have plugin arrays, concatenate them instead of replacing
if (target.plugin && source.plugin) {
- const pluginSet = new Set([...target.plugin, ...source.plugin])
- merged.plugin = Array.from(pluginSet)
+ merged.plugin = Array.from(new Set([...target.plugin, ...source.plugin]))
+ }
+ if (target.instructions && source.instructions) {
+ merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions]))
}
return merged
}
@@ -39,19 +40,19 @@ export namespace Config {
// Override with custom config if provided
if (Flag.OPENCODE_CONFIG) {
- result = mergeConfigWithPlugins(result, await loadFile(Flag.OPENCODE_CONFIG))
+ result = mergeConfigConcatArrays(result, await loadFile(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
for (const resolved of found.toReversed()) {
- result = mergeConfigWithPlugins(result, await loadFile(resolved))
+ result = mergeConfigConcatArrays(result, await loadFile(resolved))
}
}
if (Flag.OPENCODE_CONFIG_CONTENT) {
- result = mergeConfigWithPlugins(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
+ result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
}
@@ -59,7 +60,7 @@ export namespace Config {
if (value.type === "wellknown") {
process.env[value.key] = value.token
const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any
- result = mergeConfigWithPlugins(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
+ result = mergeConfigConcatArrays(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
}
}
@@ -94,7 +95,7 @@ export namespace Config {
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
log.debug(`loading config from ${path.join(dir, file)}`)
- result = mergeConfigWithPlugins(result, await loadFile(path.join(dir, file)))
+ result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
// to satisfy the type checker
result.agent ??= {}
result.mode ??= {}
diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index c2ed3abe8..c35a391f8 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/packages/opencode/test/config/config.test.ts
@@ -488,6 +488,87 @@ Helper subagent prompt`,
})
})
+test("merges instructions arrays from global and local configs", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ const projectDir = path.join(dir, "project")
+ const opencodeDir = path.join(projectDir, ".opencode")
+ await fs.mkdir(opencodeDir, { recursive: true })
+
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ instructions: ["global-instructions.md", "shared-rules.md"],
+ }),
+ )
+
+ await Bun.write(
+ path.join(opencodeDir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ instructions: ["local-instructions.md"],
+ }),
+ )
+ },
+ })
+
+ await Instance.provide({
+ directory: path.join(tmp.path, "project"),
+ fn: async () => {
+ const config = await Config.get()
+ const instructions = config.instructions ?? []
+
+ expect(instructions).toContain("global-instructions.md")
+ expect(instructions).toContain("shared-rules.md")
+ expect(instructions).toContain("local-instructions.md")
+ expect(instructions.length).toBe(3)
+ },
+ })
+})
+
+test("deduplicates duplicate instructions from global and local configs", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ const projectDir = path.join(dir, "project")
+ const opencodeDir = path.join(projectDir, ".opencode")
+ await fs.mkdir(opencodeDir, { recursive: true })
+
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ instructions: ["duplicate.md", "global-only.md"],
+ }),
+ )
+
+ await Bun.write(
+ path.join(opencodeDir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ instructions: ["duplicate.md", "local-only.md"],
+ }),
+ )
+ },
+ })
+
+ await Instance.provide({
+ directory: path.join(tmp.path, "project"),
+ fn: async () => {
+ const config = await Config.get()
+ const instructions = config.instructions ?? []
+
+ expect(instructions).toContain("global-only.md")
+ expect(instructions).toContain("local-only.md")
+ expect(instructions).toContain("duplicate.md")
+
+ const duplicates = instructions.filter((i) => i === "duplicate.md")
+ expect(duplicates.length).toBe(1)
+ expect(instructions.length).toBe(3)
+ },
+ })
+})
+
test("deduplicates duplicate plugins from global and local configs", async () => {
await using tmp = await tmpdir({
init: async (dir) => {