From 7f7527047ed1405e48401a1010dc3ffc627117b4 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 14 Mar 2026 13:46:32 +0900 Subject: [PATCH] fix(cli): validate and detect OpenCode Go install settings Reject invalid --opencode-go values during non-TUI installs and detect existing OpenCode Go usage from the generated oh-my-opencode config so updates preserve the right defaults. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../config-manager/detect-current-config.ts | 31 ++++++++++++++--- .../config-manager/plugin-detection.test.ts | 19 +++++++++++ src/cli/install-validators.test.ts | 34 +++++++++++++++++++ src/cli/install-validators.ts | 10 ++++-- 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/cli/install-validators.test.ts diff --git a/src/cli/config-manager/detect-current-config.ts b/src/cli/config-manager/detect-current-config.ts index 246d27c9e..b251debdb 100644 --- a/src/cli/config-manager/detect-current-config.ts +++ b/src/cli/config-manager/detect-current-config.ts @@ -10,17 +10,30 @@ function detectProvidersFromOmoConfig(): { hasOpencodeZen: boolean hasZaiCodingPlan: boolean hasKimiForCoding: boolean + hasOpencodeGo: boolean } { const omoConfigPath = getOmoConfigPath() if (!existsSync(omoConfigPath)) { - return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false } + return { + hasOpenAI: true, + hasOpencodeZen: true, + hasZaiCodingPlan: false, + hasKimiForCoding: false, + hasOpencodeGo: false, + } } try { const content = readFileSync(omoConfigPath, "utf-8") const omoConfig = parseJsonc>(content) if (!omoConfig || typeof omoConfig !== "object") { - return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false } + return { + hasOpenAI: true, + hasOpencodeZen: true, + hasZaiCodingPlan: false, + hasKimiForCoding: false, + hasOpencodeGo: false, + } } const configStr = JSON.stringify(omoConfig) @@ -28,10 +41,17 @@ function detectProvidersFromOmoConfig(): { const hasOpencodeZen = configStr.includes('"opencode/') const hasZaiCodingPlan = configStr.includes('"zai-coding-plan/') const hasKimiForCoding = configStr.includes('"kimi-for-coding/') + const hasOpencodeGo = configStr.includes('"opencode-go/') - return { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } + return { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding, hasOpencodeGo } } catch { - return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false } + return { + hasOpenAI: true, + hasOpencodeZen: true, + hasZaiCodingPlan: false, + hasKimiForCoding: false, + hasOpencodeGo: false, + } } } @@ -72,11 +92,12 @@ result.isInstalled = plugins.some((p) => p.startsWith(OLD_PACKAGE_NAME) || p.sta const providers = openCodeConfig.provider as Record | undefined result.hasGemini = providers ? "google" in providers : false - const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig() + const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding, hasOpencodeGo } = detectProvidersFromOmoConfig() result.hasOpenAI = hasOpenAI result.hasOpencodeZen = hasOpencodeZen result.hasZaiCodingPlan = hasZaiCodingPlan result.hasKimiForCoding = hasKimiForCoding + result.hasOpencodeGo = hasOpencodeGo return result } diff --git a/src/cli/config-manager/plugin-detection.test.ts b/src/cli/config-manager/plugin-detection.test.ts index b95ba75fd..c5a183650 100644 --- a/src/cli/config-manager/plugin-detection.test.ts +++ b/src/cli/config-manager/plugin-detection.test.ts @@ -10,10 +10,12 @@ import { addPluginToOpenCodeConfig } from "./add-plugin-to-opencode-config" describe("detectCurrentConfig - dual name detection", () => { let testConfigDir = "" let testConfigPath = "" + let testOmoConfigPath = "" beforeEach(() => { testConfigDir = join(tmpdir(), `omo-detect-config-${Date.now()}-${Math.random().toString(36).slice(2)}`) testConfigPath = join(testConfigDir, "opencode.json") + testOmoConfigPath = join(testConfigDir, "oh-my-opencode.json") mkdirSync(testConfigDir, { recursive: true }) process.env.OPENCODE_CONFIG_DIR = testConfigDir @@ -85,6 +87,23 @@ describe("detectCurrentConfig - dual name detection", () => { // then expect(result.isInstalled).toBe(false) }) + + it("detects OpenCode Go from the existing omo config", () => { + // given + writeFileSync(testConfigPath, JSON.stringify({ plugin: ["oh-my-openagent"] }, null, 2) + "\n", "utf-8") + writeFileSync( + testOmoConfigPath, + JSON.stringify({ agents: { atlas: { model: "opencode-go/kimi-k2.5" } } }, null, 2) + "\n", + "utf-8", + ) + + // when + const result = detectCurrentConfig() + + // then + expect(result.isInstalled).toBe(true) + expect(result.hasOpencodeGo).toBe(true) + }) }) describe("addPluginToOpenCodeConfig - dual name detection", () => { diff --git a/src/cli/install-validators.test.ts b/src/cli/install-validators.test.ts new file mode 100644 index 000000000..119f21ab8 --- /dev/null +++ b/src/cli/install-validators.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "bun:test" + +import { validateNonTuiArgs } from "./install-validators" +import type { InstallArgs } from "./types" + +function createArgs(overrides: Partial = {}): InstallArgs { + return { + tui: false, + claude: "no", + openai: "no", + gemini: "no", + copilot: "no", + opencodeZen: "no", + zaiCodingPlan: "no", + kimiForCoding: "no", + opencodeGo: "no", + skipAuth: false, + ...overrides, + } +} + +describe("validateNonTuiArgs", () => { + test("rejects invalid --opencode-go values", () => { + // #given + const args = createArgs({ opencodeGo: "maybe" as InstallArgs["opencodeGo"] }) + + // #when + const result = validateNonTuiArgs(args) + + // #then + expect(result.valid).toBe(false) + expect(result.errors).toContain("Invalid --opencode-go value: maybe (expected: no, yes)") + }) +}) diff --git a/src/cli/install-validators.ts b/src/cli/install-validators.ts index 58688e752..be429eee6 100644 --- a/src/cli/install-validators.ts +++ b/src/cli/install-validators.ts @@ -17,6 +17,8 @@ export const SYMBOLS = { star: color.yellow("*"), } +const ANSI_COLOR_PATTERN = new RegExp("\u001b\\[[0-9;]*m", "g") + function formatProvider(name: string, enabled: boolean, detail?: string): string { const status = enabled ? SYMBOLS.check : color.dim("○") const label = enabled ? color.white(name) : color.dim(name) @@ -83,7 +85,7 @@ export function printBox(content: string, title?: string): void { const lines = content.split("\n") const maxWidth = Math.max( - ...lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").length), + ...lines.map((line) => line.replace(ANSI_COLOR_PATTERN, "").length), title?.length ?? 0, ) + 4 const border = color.dim("─".repeat(maxWidth)) @@ -101,7 +103,7 @@ export function printBox(content: string, title?: string): void { } for (const line of lines) { - const stripped = line.replace(/\x1b\[[0-9;]*m/g, "") + const stripped = line.replace(ANSI_COLOR_PATTERN, "") const padding = maxWidth - stripped.length console.log(color.dim("│") + ` ${line}${" ".repeat(padding - 1)}` + color.dim("│")) } @@ -135,6 +137,10 @@ export function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: errors.push(`Invalid --openai value: ${args.openai} (expected: no, yes)`) } + if (args.opencodeGo !== undefined && !["no", "yes"].includes(args.opencodeGo)) { + errors.push(`Invalid --opencode-go value: ${args.opencodeGo} (expected: no, yes)`) + } + if (args.opencodeZen !== undefined && !["no", "yes"].includes(args.opencodeZen)) { errors.push(`Invalid --opencode-zen value: ${args.opencodeZen} (expected: no, yes)`) }