From cdd6e88557e3f3bf133ed4fb82a380048d66a4ae Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 8 Apr 2026 13:26:33 +0900 Subject: [PATCH] fix(installer): add upgrade path safety checks Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../add-plugin-to-opencode-config.ts | 25 +++++++++++++++++++ .../config-manager/detect-current-config.ts | 13 +++++++++- .../config-manager/write-omo-config.test.ts | 1 + src/cli/config-manager/write-omo-config.ts | 10 ++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/cli/config-manager/add-plugin-to-opencode-config.ts b/src/cli/config-manager/add-plugin-to-opencode-config.ts index 19b265ec5..208abd56a 100644 --- a/src/cli/config-manager/add-plugin-to-opencode-config.ts +++ b/src/cli/config-manager/add-plugin-to-opencode-config.ts @@ -1,12 +1,14 @@ import { readFileSync, writeFileSync } from "node:fs" import type { ConfigMergeResult } from "../types" import { PLUGIN_NAME, LEGACY_PLUGIN_NAME } from "../../shared" +import { backupConfigFile } from "./backup-config" import { getConfigDir } from "./config-context" import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists" import { formatErrorWithSuggestion } from "./format-error-with-suggestion" import { detectConfigFormat } from "./opencode-config-format" import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file" import { getPluginNameWithVersion } from "./plugin-name-with-version" +import { checkVersionCompatibility, extractVersionFromPluginEntry } from "./version-compatibility" export async function addPluginToOpenCodeConfig(currentVersion: string): Promise { try { @@ -52,6 +54,29 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise && !(plugin === LEGACY_PLUGIN_NAME || plugin.startsWith(`${LEGACY_PLUGIN_NAME}@`)) ) + const existingEntry = canonicalEntries[0] ?? legacyEntries[0] + if (existingEntry) { + const installedVersion = extractVersionFromPluginEntry(existingEntry) + const compatibility = checkVersionCompatibility(installedVersion, currentVersion) + + if (!compatibility.canUpgrade) { + return { + success: false, + configPath: path, + error: compatibility.reason ?? "Version compatibility check failed", + } + } + + const backupResult = backupConfigFile(path) + if (!backupResult.success) { + return { + success: false, + configPath: path, + error: `Failed to create backup: ${backupResult.error}`, + } + } + } + const normalizedPlugins = [...otherPlugins] if (canonicalEntries.length > 0) { diff --git a/src/cli/config-manager/detect-current-config.ts b/src/cli/config-manager/detect-current-config.ts index 3679d5bd6..f158e18e2 100644 --- a/src/cli/config-manager/detect-current-config.ts +++ b/src/cli/config-manager/detect-current-config.ts @@ -4,6 +4,7 @@ import type { DetectedConfig } from "../types" import { getOmoConfigPath } from "./config-context" import { detectConfigFormat } from "./opencode-config-format" import { parseOpenCodeConfigFileWithError } from "./parse-opencode-config-file" +import { extractVersionFromPluginEntry } from "./version-compatibility" function detectProvidersFromOmoConfig(): { hasOpenAI: boolean @@ -60,9 +61,14 @@ function isOurPlugin(plugin: string): boolean { plugin === LEGACY_PLUGIN_NAME || plugin.startsWith(`${LEGACY_PLUGIN_NAME}@`) } +function findOurPluginEntry(plugins: string[]): string | null { + return plugins.find(isOurPlugin) ?? null +} + export function detectCurrentConfig(): DetectedConfig { const result: DetectedConfig = { isInstalled: false, + installedVersion: null, hasClaude: true, isMax20: true, hasOpenAI: true, @@ -86,7 +92,12 @@ export function detectCurrentConfig(): DetectedConfig { const openCodeConfig = parseResult.config const plugins = openCodeConfig.plugin ?? [] - result.isInstalled = plugins.some(isOurPlugin) + const ourPluginEntry = findOurPluginEntry(plugins) + result.isInstalled = !!ourPluginEntry + + if (ourPluginEntry) { + result.installedVersion = extractVersionFromPluginEntry(ourPluginEntry) + } if (!result.isInstalled) { return result diff --git a/src/cli/config-manager/write-omo-config.test.ts b/src/cli/config-manager/write-omo-config.test.ts index 5701b53dc..48ae5c620 100644 --- a/src/cli/config-manager/write-omo-config.test.ts +++ b/src/cli/config-manager/write-omo-config.test.ts @@ -18,6 +18,7 @@ const installConfig: InstallConfig = { hasOpencodeZen: false, hasZaiCodingPlan: false, hasKimiForCoding: false, + hasOpencodeGo: false, } function getRecord(value: unknown): Record { diff --git a/src/cli/config-manager/write-omo-config.ts b/src/cli/config-manager/write-omo-config.ts index 261175e7a..697322584 100644 --- a/src/cli/config-manager/write-omo-config.ts +++ b/src/cli/config-manager/write-omo-config.ts @@ -1,6 +1,7 @@ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs" import { parseJsonc } from "../../shared" import type { ConfigMergeResult, InstallConfig } from "../types" +import { backupConfigFile } from "./backup-config" import { getConfigDir, getOmoConfigPath } from "./config-context" import { deepMergeRecord } from "./deep-merge-record" import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists" @@ -28,6 +29,15 @@ export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult const newConfig = generateOmoConfig(installConfig) if (existsSync(omoConfigPath)) { + const backupResult = backupConfigFile(omoConfigPath) + if (!backupResult.success) { + return { + success: false, + configPath: omoConfigPath, + error: `Failed to create backup: ${backupResult.error}`, + } + } + try { const stat = statSync(omoConfigPath) const content = readFileSync(omoConfigPath, "utf-8")