fix: respect opencode.jsonc in configure models flow

Prefer an existing opencode.jsonc file and parse JSONC content when updating providers so the CLI no longer creates duplicate opencode.json files.
This commit is contained in:
Noe
2026-02-06 15:58:16 +00:00
parent a798509250
commit 80b3ffe99c
3 changed files with 84 additions and 7 deletions

View File

@@ -8,14 +8,22 @@ import { OPENCODE_MODEL_DEFINITIONS } from "./models";
describe("updateOpencodeConfig", () => {
let tempDir: string;
let configPath: string;
let originalXdgConfigHome: string | undefined;
beforeEach(() => {
originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
// Create a temporary directory for each test
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-test-"));
configPath = path.join(tempDir, "opencode.json");
});
afterEach(() => {
if (originalXdgConfigHome === undefined) {
delete process.env.XDG_CONFIG_HOME;
} else {
process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
}
// Clean up temp directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
@@ -203,6 +211,50 @@ describe("updateOpencodeConfig", () => {
}
});
test("parses existing jsonc config files with comments and trailing commas", async () => {
const jsoncPath = path.join(tempDir, "opencode.jsonc");
const existingJsoncConfig = `{
// Keep existing plugin
"plugin": [
"other-plugin",
],
"provider": {
"google": {
"region": "us-central1",
},
},
}`;
fs.writeFileSync(jsoncPath, existingJsoncConfig);
const result = await updateOpencodeConfig({ configPath: jsoncPath });
expect(result.success).toBe(true);
expect(result.configPath).toBe(jsoncPath);
const writtenConfig = JSON.parse(fs.readFileSync(jsoncPath, "utf-8"));
expect(writtenConfig.plugin).toContain("other-plugin");
expect(writtenConfig.plugin).toContain("opencode-antigravity-auth@latest");
expect(writtenConfig.provider.google.region).toBe("us-central1");
expect(writtenConfig.provider.google.models["antigravity-gemini-3-pro"]).toBeDefined();
});
test("prefers existing opencode.jsonc when using default config path", async () => {
const opencodeDir = path.join(tempDir, "opencode");
const jsonPath = path.join(opencodeDir, "opencode.json");
const jsoncPath = path.join(opencodeDir, "opencode.jsonc");
fs.mkdirSync(opencodeDir, { recursive: true });
fs.writeFileSync(jsoncPath, JSON.stringify({ plugin: ["other-plugin"], provider: {} }, null, 2));
process.env.XDG_CONFIG_HOME = tempDir;
const result = await updateOpencodeConfig();
expect(result.success).toBe(true);
expect(result.configPath).toBe(jsoncPath);
expect(fs.existsSync(jsonPath)).toBe(false);
expect(fs.existsSync(jsoncPath)).toBe(true);
});
test("creates parent directory if it does not exist", async () => {
const nestedPath = path.join(tempDir, "nested", "dir", "opencode.json");

View File

@@ -1,7 +1,7 @@
/**
* OpenCode configuration file updater.
*
* Updates ~/.config/opencode/opencode.json with plugin models.
* Updates ~/.config/opencode/opencode.json(c) with plugin models.
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
@@ -43,6 +43,17 @@ export interface UpdateConfigOptions {
const PLUGIN_NAME = "opencode-antigravity-auth@latest";
const SCHEMA_URL = "https://opencode.ai/config.json";
const OPENCODE_JSON_FILENAME = "opencode.json";
const OPENCODE_JSONC_FILENAME = "opencode.jsonc";
function stripJsonCommentsAndTrailingCommas(json: string): string {
return json
.replace(
/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
(match: string, group: string | undefined) => (group ? "" : match)
)
.replace(/,(\s*[}\]])/g, "$1");
}
/**
* Get the opencode config directory path.
@@ -53,10 +64,24 @@ export function getOpencodeConfigDir(): string {
}
/**
* Get the opencode.json config file path.
* Get the opencode config file path.
*
* Prefers opencode.jsonc when present so we update the active config file
* instead of creating a new opencode.json.
*/
export function getOpencodeConfigPath(): string {
return join(getOpencodeConfigDir(), "opencode.json");
const configDir = getOpencodeConfigDir();
const jsoncPath = join(configDir, OPENCODE_JSONC_FILENAME);
const jsonPath = join(configDir, OPENCODE_JSON_FILENAME);
if (existsSync(jsoncPath)) {
return jsoncPath;
}
if (existsSync(jsonPath)) {
return jsonPath;
}
return jsonPath;
}
// =============================================================================
@@ -64,10 +89,10 @@ export function getOpencodeConfigPath(): string {
// =============================================================================
/**
* Updates the opencode.json configuration file with plugin models.
* Updates the opencode configuration file with plugin models.
*
* This function:
* 1. Reads existing opencode.json (or creates default structure)
* 1. Reads existing opencode.json/opencode.jsonc (or creates default structure)
* 2. Replaces `provider.google.models` with plugin models
* 3. Writes back to disk with proper formatting
*
@@ -90,7 +115,7 @@ export async function updateOpencodeConfig(
// Read existing config or create default
if (existsSync(configPath)) {
const content = readFileSync(configPath, "utf-8");
config = JSON.parse(content) as OpencodeConfig;
config = JSON.parse(stripJsonCommentsAndTrailingCommas(content)) as OpencodeConfig;
} else {
// Create default config structure
config = {

View File

@@ -54,7 +54,7 @@ export async function showAuthMenu(accounts: AccountInfo[]): Promise<AuthMenuAct
{ label: 'Add new account', value: { type: 'add' } },
{ label: 'Check quotas', value: { type: 'check' } },
{ label: 'Manage accounts (enable/disable)', value: { type: 'manage' } },
{ label: 'Configure models in opencode.json', value: { type: 'configure-models' } },
{ label: 'Configure models in opencode config', value: { type: 'configure-models' } },
...accounts.map(account => {
const badge = getStatusBadge(account.status);