diff --git a/src/plugin/config/updater.test.ts b/src/plugin/config/updater.test.ts index f8ea48e..03a1205 100644 --- a/src/plugin/config/updater.test.ts +++ b/src/plugin/config/updater.test.ts @@ -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"); diff --git a/src/plugin/config/updater.ts b/src/plugin/config/updater.ts index 52f6309..b13ca4d 100644 --- a/src/plugin/config/updater.ts +++ b/src/plugin/config/updater.ts @@ -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 = { diff --git a/src/plugin/ui/auth-menu.ts b/src/plugin/ui/auth-menu.ts index 554ba93..5476983 100644 --- a/src/plugin/ui/auth-menu.ts +++ b/src/plugin/ui/auth-menu.ts @@ -54,7 +54,7 @@ export async function showAuthMenu(accounts: AccountInfo[]): Promise { const badge = getStatusBadge(account.status);