diff --git a/src/plugin/config/loader.test.ts b/src/plugin/config/loader.test.ts new file mode 100644 index 0000000..6da8c13 --- /dev/null +++ b/src/plugin/config/loader.test.ts @@ -0,0 +1,83 @@ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { __testExports, loadConfig } from "./loader"; + +describe("config loader legacy Windows migration", () => { + let tempRoot: string; + let previousConfigDir: string | undefined; + let previousAppData: string | undefined; + + beforeEach(() => { + tempRoot = mkdtempSync(join(tmpdir(), "antigravity-config-")); + + previousConfigDir = process.env.OPENCODE_CONFIG_DIR; + previousAppData = process.env.APPDATA; + + process.env.OPENCODE_CONFIG_DIR = join(tempRoot, "new-config"); + process.env.APPDATA = join(tempRoot, "legacy-appdata"); + }); + + afterEach(() => { + if (previousConfigDir === undefined) { + delete process.env.OPENCODE_CONFIG_DIR; + } else { + process.env.OPENCODE_CONFIG_DIR = previousConfigDir; + } + + if (previousAppData === undefined) { + delete process.env.APPDATA; + } else { + process.env.APPDATA = previousAppData; + } + + rmSync(tempRoot, { recursive: true, force: true }); + }); + + it("migrates %APPDATA%/opencode/antigravity.json to current config path", () => { + const legacyDir = join(process.env.APPDATA!, "opencode"); + const legacyPath = join(legacyDir, "antigravity.json"); + const expectedPath = join(process.env.OPENCODE_CONFIG_DIR!, "antigravity.json"); + + mkdirSync(legacyDir, { recursive: true }); + writeFileSync(legacyPath, JSON.stringify({ scheduling_mode: "performance_first" }), "utf-8"); + + const resolvedPath = __testExports.resolveUserConfigPath("win32"); + + expect(resolvedPath).toBe(expectedPath); + expect(existsSync(expectedPath)).toBe(true); + expect(existsSync(legacyPath)).toBe(false); + }); + + it("loads settings from migrated legacy config", () => { + const legacyDir = join(process.env.APPDATA!, "opencode"); + const legacyPath = join(legacyDir, "antigravity.json"); + + mkdirSync(legacyDir, { recursive: true }); + writeFileSync( + legacyPath, + JSON.stringify({ + account_selection_strategy: "round-robin", + scheduling_mode: "performance_first", + switch_on_first_rate_limit: true, + max_rate_limit_wait_seconds: 5, + }), + "utf-8", + ); + + // Force legacy -> new path migration even when tests run on non-Windows CI. + __testExports.resolveUserConfigPath("win32"); + + const loaded = loadConfig(tempRoot); + + expect(loaded.account_selection_strategy).toBe("round-robin"); + expect(loaded.scheduling_mode).toBe("performance_first"); + expect(loaded.switch_on_first_rate_limit).toBe(true); + expect(loaded.max_rate_limit_wait_seconds).toBe(5); + + const migratedPath = join(process.env.OPENCODE_CONFIG_DIR!, "antigravity.json"); + const persisted = JSON.parse(readFileSync(migratedPath, "utf-8")) as Record; + expect(persisted.scheduling_mode).toBe("performance_first"); + }); +}); diff --git a/src/plugin/config/loader.ts b/src/plugin/config/loader.ts index 328a663..d7b952a 100644 --- a/src/plugin/config/loader.ts +++ b/src/plugin/config/loader.ts @@ -9,8 +9,8 @@ * 4. Environment variables */ -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync } from "node:fs"; +import { dirname, join } from "node:path"; import { homedir } from "node:os"; import { AccountSelectionStrategySchema, AntigravityConfigSchema, DEFAULT_CONFIG, type AntigravityConfig } from "./schema"; import { createLogger } from "../logger"; @@ -37,11 +37,61 @@ function getConfigDir(): string { return join(xdgConfig, "opencode"); } +/** + * Gets the legacy Windows config directory (%APPDATA%\opencode). + */ +function getLegacyWindowsConfigDir(): string { + return join( + process.env.APPDATA || join(homedir(), "AppData", "Roaming"), + "opencode", + ); +} + +/** + * Resolves user config path and migrates legacy Windows config if needed. + * + * Legacy path: %APPDATA%\opencode\antigravity.json + * Current path: ~/.config/opencode/antigravity.json (or OPENCODE_CONFIG_DIR override) + */ +function resolveUserConfigPath(platform: NodeJS.Platform = process.platform): string { + const newPath = join(getConfigDir(), "antigravity.json"); + + if (platform !== "win32") { + return newPath; + } + + const legacyPath = join(getLegacyWindowsConfigDir(), "antigravity.json"); + if (!existsSync(legacyPath) || existsSync(newPath)) { + return newPath; + } + + try { + mkdirSync(dirname(newPath), { recursive: true }); + try { + renameSync(legacyPath, newPath); + log.info("Migrated Windows config via rename", { from: legacyPath, to: newPath }); + } catch { + copyFileSync(legacyPath, newPath); + unlinkSync(legacyPath); + log.info("Migrated Windows config via copy+delete", { from: legacyPath, to: newPath }); + } + return newPath; + } catch (error) { + // Fallback to legacy path if migration failed, so user settings are still respected. + log.warn("Failed to migrate legacy Windows config, falling back to legacy path", { + legacyPath, + newPath, + error: String(error), + }); + return legacyPath; + } +} + /** * Get the user-level config file path. */ export function getUserConfigPath(): string { - return join(getConfigDir(), "antigravity.json"); + return resolveUserConfigPath(); } /** @@ -231,3 +281,7 @@ export function initRuntimeConfig(config: AntigravityConfig): void { export function getKeepThinking(): boolean { return runtimeConfig?.keep_thinking ?? false; } + +export const __testExports = { + resolveUserConfigPath, +};