Repository navigation
fix(config): keep OPENCODE_CONFIG_DIR additive for the global config layer - #62315
abdo342003 wants to merge 1 commit into
Conversation
…layer OPENCODE_CONFIG_DIR replaced the XDG global config directory outright, so setting it dropped everything behind it: the global opencode.json(c) never merged, ~/.config/opencode/AGENTS.md was never read, and the directory entries feeding commands, skills and plugins only came from the override (anomalyco#28658, anomalyco#32825). anomalyco#47468 asked for this to be ported to V2. Global now carries configDirs (override first, XDG default behind it), produced by configDirectory() which only treats a directory additively when it actually is OPENCODE_CONFIG_DIR -- explicit directories that are not the env var (tests, embedded hosts) keep replacing as before. Discovery exposes the list as Sources.globalDirs, config loading merges it lowest-priority-first so the override wins conflicts, the watch plan watches every global directory, and the instruction plugin resolves the global AGENTS.md first-match-wins across the list while relative instructions patterns scan all of them. The TUI theme source and plugin list iterate the same list; writes still target the primary directory.
There was a problem hiding this comment.
Fixes the reported problem. I ran serve from source in each checkout with a throwaway HOME. With OPENCODE_CONFIG_DIR set, the base lists only the override directory as a config source. With this change, the XDG opencode.json and directory are listed too, and the override loads after them, so it wins conflicts. The util and core tests for the change pass.
Public API: OPENCODE_CONFIG_DIR changes from replacing ~/.config/opencode to adding to it, which matches the docs. A user who set it to isolate their config from ~/.config/opencode will now also get the XDG layer: settings, commands, skills, plugins and the fallback AGENTS.md. Internal additions are Global.Interface.configDirs, Global.configDirectory(), ConfigDiscovery.Sources.globalDirs and ConfigDiscovery.globalDirectories(). createThemeSource now takes a list of directories. Writes (plugin add/remove, mcp add --global, cli.json) still target the override directory.
Concerns:
- TUI plugins are still missed.
packages/tui/src/app.tsx:231still callslocalPluginDirectories(process.cwd(), global.config). With the override set, TUI plugins in~/.config/opencode/pluginsare still not loaded, whileopencode plugin listnow lists them. It should iterateglobal.configDirslikeplugin/list.tsdoes. - How additive mode is chosen. It depends on matching the env var, and
routes.tsgains a new env fallback (see the inline comments).
The override still ranks below project .opencode directories, while the docs say it is loaded after them. This was already the case on the base and is not changed here.
| * hosts) replaces it exactly as before. | ||
| */ | ||
| export const configDirectory = (value: string | undefined): Partial<Interface> => | ||
| value && value === process.env.OPENCODE_CONFIG_DIR |
There was a problem hiding this comment.
Additive mode is chosen by comparing the value with process.env.OPENCODE_CONFIG_DIR. So Global.configDirectory(x) behaves differently depending on an ambient env var: an embedding host that passes an explicit directory that happens to equal the env var gets additive mode, and one that passes a different directory doesn't. Nothing normalizes the paths either. It would be more predictable to make the mode explicit, e.g. configDirectory({ override }) or a separate extraConfig input that the CLI and server pass, and keep util free of env sniffing.
| FileSystemSearch.node.replace(FileSystemSearch.configured({ fff: options.fs?.fff })), | ||
| Global.node.replace(Global.layerWith(options.config?.directory ? { config: options.config.directory } : {})), | ||
| Global.node.replace( | ||
| Global.layerWith(Global.configDirectory(options.config?.directory ?? process.env.OPENCODE_CONFIG_DIR)), |
There was a problem hiding this comment.
This adds a new fallback to process.env.OPENCODE_CONFIG_DIR when the caller doesn't set config.directory. Before, makeRoutes ignored the env var in that case (layerWith({}) → XDG only). The CLI server already passes directory: process.env.OPENCODE_CONFIG_DIR (server-process.ts), so the fallback isn't needed for the fix. It does change behaviour for other hosts that embed the server and leave config.directory unset. Can it be dropped?
Issue for this PR
Closes #32825
Related: #28658 (the V1 report of the same bug, closed in the V1 sweep)
Type of change
What does this PR do?
Setting
OPENCODE_CONFIG_DIRcurrently replaces the XDG global config directory instead of adding to it, soonce the variable is set
~/.config/opencodestops contributing entirely: itsopencode.json(c)is nevermerged,
AGENTS.mdis never read, and the directory entries that feed commands, skills and plugins onlycome from the override. #47468 fixed the AGENTS.md half on V1 and asked for a V2 port — this is that port,
extended to every place V2 reads the directory.
GlobalgainsconfigDirs(override first,Global.Path.configbehind it), produced byGlobal.configDirectory(value), which only treats a directory additively when it actually isOPENCODE_CONFIG_DIR. A directory that isn't the env var — what tests and embedded hosts pass — keeps theold replace behavior, so explicit configuration is unchanged.
From there:
ConfigDiscoveryexposes the list asSources.globalDirs, config loading merges it lowestpriority first so the override still wins conflicts, and the watch plan watches every global directory. The
instruction plugin resolves the global
AGENTS.mdfirst-match-wins across the list (both files staywatched) while relative
instructionspatterns scan all of them. The TUI theme source andopencode plugin listiterate the same list. Writes still target the primary directory.How did you verify your code works?
Unit tests for
Global.configDirectory(override / equal-to-XDG / unrelated directory / unset) and fordiscover(globalDirsordering,global: false→ empty). An integration test inconfig.test.tschecksthat the override document outranks the XDG one, that the XDG document still applies once the override's is
removed, and that both directories get directory entries and directory watches. An instruction test checks
that the XDG
AGENTS.mdloads when the override has none, and that the override's file wins once it exists.bun run typecheckandbun run lintare clean, and the util/core/server/tui/cli suites pass — the onlyfailures on this machine (worktree PATH, GitLab OAuth needing port 8080, upsell art, stats palette)
reproduce on a clean
devcheckout too.Checklist
Added by the opencode bot (AI-generated).
Related: #46757, #58141
Public API: OPENCODE_CONFIG_DIR now adds to ~/.config/opencode instead of replacing it (behaviour change; setups that used it to isolate config will now also load the XDG layer)