fix(discovery): load project .opencode config files
This commit is contained in:
@@ -77,9 +77,11 @@ async function loadJsonConfig(
|
||||
|
||||
/**
|
||||
* OpenCode config sources in ascending effective precedence (lowest first):
|
||||
* user `opencode.json` → user `opencode.jsonc` → project `opencode.json` →
|
||||
* project `opencode.jsonc`. This matches how OpenCode merges configs — project
|
||||
* overrides user, and within a scope `opencode.jsonc` overrides `opencode.json`.
|
||||
* user `opencode.json` → user `opencode.jsonc` → project-root
|
||||
* `opencode.json` → project-root `opencode.jsonc` → project `.opencode/opencode.json`
|
||||
* → project `.opencode/opencode.jsonc`. This matches how OpenCode merges configs:
|
||||
* project overrides user, `.opencode` overrides project-root config, and within
|
||||
* a directory `opencode.jsonc` overrides `opencode.json`.
|
||||
*
|
||||
* Both consumers apply this order low-to-high: settings deep-merge in item
|
||||
* order (last wins) and `loadMCPServers` deep-merges each server across layers
|
||||
@@ -94,6 +96,10 @@ function getConfigSources(ctx: LoadContext): OpenCodeConfigSource[] {
|
||||
for (const filename of CONFIG_FILENAMES) {
|
||||
sources.push({ path: path.join(ctx.cwd, filename), level: "project" });
|
||||
}
|
||||
for (const filename of CONFIG_FILENAMES) {
|
||||
const configPath = getProjectPath(ctx, "opencode", filename);
|
||||
if (configPath) sources.push({ path: configPath, level: "project" });
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
@@ -419,7 +425,7 @@ registerProvider(contextFileCapability.id, {
|
||||
registerProvider(mcpCapability.id, {
|
||||
id: PROVIDER_ID,
|
||||
displayName: DISPLAY_NAME,
|
||||
description: "Load MCP servers from opencode.json mcp key",
|
||||
description: "Load MCP servers from OpenCode config files",
|
||||
priority: PRIORITY,
|
||||
load: loadMCPServers,
|
||||
});
|
||||
@@ -451,7 +457,7 @@ registerProvider(slashCommandCapability.id, {
|
||||
registerProvider(settingsCapability.id, {
|
||||
id: PROVIDER_ID,
|
||||
displayName: DISPLAY_NAME,
|
||||
description: "Load settings from opencode.json",
|
||||
description: "Load settings from OpenCode config files",
|
||||
priority: PRIORITY,
|
||||
load: loadSettings,
|
||||
});
|
||||
|
||||
@@ -88,6 +88,42 @@ describe("OpenCode MCP discovery", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("loads project .opencode config after project-root config", async () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
const projectConfigDir = path.join(projectDir, ".opencode");
|
||||
await fs.mkdir(projectConfigDir, { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(projectDir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
model: "root-model",
|
||||
mcp: { shared: { type: "local", command: ["root-server"] } },
|
||||
}),
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(projectConfigDir, "opencode.jsonc"),
|
||||
`{
|
||||
// Project .opencode config has higher precedence.
|
||||
"model": "dotdir-model",
|
||||
"mcp": { "shared": { "command": ["dotdir-server"] } }
|
||||
}`,
|
||||
);
|
||||
|
||||
const [servers, discoveredSettings] = await Promise.all([
|
||||
loadOpenCodeMcpConfig(projectDir),
|
||||
loadOpenCodeSettings(projectDir),
|
||||
]);
|
||||
|
||||
expect(servers.filter(server => server.name === "shared")).toEqual([
|
||||
expect.objectContaining({ command: "dotdir-server", transport: "stdio" }),
|
||||
]);
|
||||
expect(discoveredSettings.at(-1)).toMatchObject({
|
||||
path: path.join(projectConfigDir, "opencode.jsonc"),
|
||||
level: "project",
|
||||
data: expect.objectContaining({ model: "dotdir-model" }),
|
||||
});
|
||||
});
|
||||
|
||||
test("resolves same-named MCP servers by OpenCode precedence", async () => {
|
||||
const projectDir = path.join(tempDir, "project");
|
||||
const userConfigDir = path.join(tempDir, ".config", "opencode");
|
||||
|
||||
Reference in New Issue
Block a user