Skip to content

Commit 66bc046

Browse files
authored
fix: merge instructions arrays across config files (anomalyco#6663)
1 parent 6e68ea0 commit 66bc046

2 files changed

Lines changed: 92 additions & 10 deletions

File tree

packages/opencode/src/config/config.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@ import { ConfigMarkdown } from "./markdown"
2222
export namespace Config {
2323
const log = Log.create({ service: "config" })
2424

25-
// Custom merge function that concatenates plugin arrays instead of replacing them
26-
function mergeConfigWithPlugins(target: Info, source: Info): Info {
25+
// Custom merge function that concatenates array fields instead of replacing them
26+
function mergeConfigConcatArrays(target: Info, source: Info): Info {
2727
const merged = mergeDeep(target, source)
28-
// If both configs have plugin arrays, concatenate them instead of replacing
2928
if (target.plugin && source.plugin) {
30-
const pluginSet = new Set([...target.plugin, ...source.plugin])
31-
merged.plugin = Array.from(pluginSet)
29+
merged.plugin = Array.from(new Set([...target.plugin, ...source.plugin]))
30+
}
31+
if (target.instructions && source.instructions) {
32+
merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions]))
3233
}
3334
return merged
3435
}
@@ -39,27 +40,27 @@ export namespace Config {
3940

4041
// Override with custom config if provided
4142
if (Flag.OPENCODE_CONFIG) {
42-
result = mergeConfigWithPlugins(result, await loadFile(Flag.OPENCODE_CONFIG))
43+
result = mergeConfigConcatArrays(result, await loadFile(Flag.OPENCODE_CONFIG))
4344
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
4445
}
4546

4647
for (const file of ["opencode.jsonc", "opencode.json"]) {
4748
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
4849
for (const resolved of found.toReversed()) {
49-
result = mergeConfigWithPlugins(result, await loadFile(resolved))
50+
result = mergeConfigConcatArrays(result, await loadFile(resolved))
5051
}
5152
}
5253

5354
if (Flag.OPENCODE_CONFIG_CONTENT) {
54-
result = mergeConfigWithPlugins(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
55+
result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
5556
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
5657
}
5758

5859
for (const [key, value] of Object.entries(auth)) {
5960
if (value.type === "wellknown") {
6061
process.env[value.key] = value.token
6162
const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any
62-
result = mergeConfigWithPlugins(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
63+
result = mergeConfigConcatArrays(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
6364
}
6465
}
6566

@@ -94,7 +95,7 @@ export namespace Config {
9495
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
9596
for (const file of ["opencode.jsonc", "opencode.json"]) {
9697
log.debug(`loading config from ${path.join(dir, file)}`)
97-
result = mergeConfigWithPlugins(result, await loadFile(path.join(dir, file)))
98+
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
9899
// to satisfy the type checker
99100
result.agent ??= {}
100101
result.mode ??= {}

packages/opencode/test/config/config.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,87 @@ Helper subagent prompt`,
488488
})
489489
})
490490

491+
test("merges instructions arrays from global and local configs", async () => {
492+
await using tmp = await tmpdir({
493+
init: async (dir) => {
494+
const projectDir = path.join(dir, "project")
495+
const opencodeDir = path.join(projectDir, ".opencode")
496+
await fs.mkdir(opencodeDir, { recursive: true })
497+
498+
await Bun.write(
499+
path.join(dir, "opencode.json"),
500+
JSON.stringify({
501+
$schema: "https://opencode.ai/config.json",
502+
instructions: ["global-instructions.md", "shared-rules.md"],
503+
}),
504+
)
505+
506+
await Bun.write(
507+
path.join(opencodeDir, "opencode.json"),
508+
JSON.stringify({
509+
$schema: "https://opencode.ai/config.json",
510+
instructions: ["local-instructions.md"],
511+
}),
512+
)
513+
},
514+
})
515+
516+
await Instance.provide({
517+
directory: path.join(tmp.path, "project"),
518+
fn: async () => {
519+
const config = await Config.get()
520+
const instructions = config.instructions ?? []
521+
522+
expect(instructions).toContain("global-instructions.md")
523+
expect(instructions).toContain("shared-rules.md")
524+
expect(instructions).toContain("local-instructions.md")
525+
expect(instructions.length).toBe(3)
526+
},
527+
})
528+
})
529+
530+
test("deduplicates duplicate instructions from global and local configs", async () => {
531+
await using tmp = await tmpdir({
532+
init: async (dir) => {
533+
const projectDir = path.join(dir, "project")
534+
const opencodeDir = path.join(projectDir, ".opencode")
535+
await fs.mkdir(opencodeDir, { recursive: true })
536+
537+
await Bun.write(
538+
path.join(dir, "opencode.json"),
539+
JSON.stringify({
540+
$schema: "https://opencode.ai/config.json",
541+
instructions: ["duplicate.md", "global-only.md"],
542+
}),
543+
)
544+
545+
await Bun.write(
546+
path.join(opencodeDir, "opencode.json"),
547+
JSON.stringify({
548+
$schema: "https://opencode.ai/config.json",
549+
instructions: ["duplicate.md", "local-only.md"],
550+
}),
551+
)
552+
},
553+
})
554+
555+
await Instance.provide({
556+
directory: path.join(tmp.path, "project"),
557+
fn: async () => {
558+
const config = await Config.get()
559+
const instructions = config.instructions ?? []
560+
561+
expect(instructions).toContain("global-only.md")
562+
expect(instructions).toContain("local-only.md")
563+
expect(instructions).toContain("duplicate.md")
564+
565+
const duplicates = instructions.filter((i) => i === "duplicate.md")
566+
expect(duplicates.length).toBe(1)
567+
expect(instructions.length).toBe(3)
568+
},
569+
})
570+
})
571+
491572
test("deduplicates duplicate plugins from global and local configs", async () => {
492573
await using tmp = await tmpdir({
493574
init: async (dir) => {

0 commit comments

Comments
 (0)