From 2ced7283b027fe7b990ebd258a489e189f7c13f2 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Mon, 15 Dec 2025 03:06:32 +0900 Subject: [PATCH 1/5] fix: resolve unknown version display in startup toast - Use fileURLToPath for proper URL-to-path conversion - Support both opencode.json and opencode.jsonc config files - Add debug logging for version detection --- src/hooks/auto-update-checker/checker.ts | 40 +++++++++++++++++----- src/hooks/auto-update-checker/constants.ts | 1 + 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/hooks/auto-update-checker/checker.ts b/src/hooks/auto-update-checker/checker.ts index 15a150e..64b6917 100644 --- a/src/hooks/auto-update-checker/checker.ts +++ b/src/hooks/auto-update-checker/checker.ts @@ -8,6 +8,7 @@ import { NPM_FETCH_TIMEOUT, INSTALLED_PACKAGE_JSON, USER_OPENCODE_CONFIG, + USER_OPENCODE_CONFIG_JSONC, } from "./constants" import { log } from "../../shared/logger" @@ -21,8 +22,9 @@ function stripJsonComments(json: string): string { export function getLocalDevPath(directory: string): string | null { const projectConfig = path.join(directory, ".opencode", "opencode.json") + const projectConfigJsonc = path.join(directory, ".opencode", "opencode.jsonc") - for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) { + for (const configPath of [projectConfig, projectConfigJsonc, USER_OPENCODE_CONFIG, USER_OPENCODE_CONFIG_JSONC]) { try { if (!fs.existsSync(configPath)) continue const content = fs.readFileSync(configPath, "utf-8") @@ -31,7 +33,11 @@ export function getLocalDevPath(directory: string): string | null { for (const entry of plugins) { if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) { - return entry.replace("file://", "") + try { + return fileURLToPath(entry) + } catch { + return entry.replace("file://", "") + } } } } catch { @@ -66,15 +72,23 @@ function findPackageJsonUp(startPath: string): string | null { export function getLocalDevVersion(directory: string): string | null { const localPath = getLocalDevPath(directory) - if (!localPath) return null + if (!localPath) { + log("[auto-update-checker] getLocalDevVersion: no local path found") + return null + } try { const pkgPath = findPackageJsonUp(localPath) - if (!pkgPath) return null + if (!pkgPath) { + log(`[auto-update-checker] getLocalDevVersion: no package.json found from ${localPath}`) + return null + } const content = fs.readFileSync(pkgPath, "utf-8") const pkg = JSON.parse(content) as PackageJson + log(`[auto-update-checker] getLocalDevVersion: found version ${pkg.version} at ${pkgPath}`) return pkg.version ?? null - } catch { + } catch (err) { + log("[auto-update-checker] getLocalDevVersion: error reading package.json", err) return null } } @@ -87,8 +101,9 @@ export interface PluginEntryInfo { export function findPluginEntry(directory: string): PluginEntryInfo | null { const projectConfig = path.join(directory, ".opencode", "opencode.json") + const projectConfigJsonc = path.join(directory, ".opencode", "opencode.jsonc") - for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) { + for (const configPath of [projectConfig, projectConfigJsonc, USER_OPENCODE_CONFIG, USER_OPENCODE_CONFIG_JSONC]) { try { if (!fs.existsSync(configPath)) continue const content = fs.readFileSync(configPath, "utf-8") @@ -118,7 +133,10 @@ export function getCachedVersion(): string | null { if (fs.existsSync(INSTALLED_PACKAGE_JSON)) { const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8") const pkg = JSON.parse(content) as PackageJson - if (pkg.version) return pkg.version + if (pkg.version) { + log(`[auto-update-checker] getCachedVersion: found ${pkg.version} at ${INSTALLED_PACKAGE_JSON}`) + return pkg.version + } } } catch {} @@ -128,10 +146,14 @@ export function getCachedVersion(): string | null { if (pkgPath) { const content = fs.readFileSync(pkgPath, "utf-8") const pkg = JSON.parse(content) as PackageJson - if (pkg.version) return pkg.version + if (pkg.version) { + log(`[auto-update-checker] getCachedVersion: found ${pkg.version} at ${pkgPath}`) + return pkg.version + } } + log(`[auto-update-checker] getCachedVersion: no package.json found from ${currentDir}`) } catch (err) { - log("[auto-update-checker] Failed to resolve version from current directory:", err) + log("[auto-update-checker] getCachedVersion: error resolving version from current directory:", err) } return null diff --git a/src/hooks/auto-update-checker/constants.ts b/src/hooks/auto-update-checker/constants.ts index 15c0b63..f216d81 100644 --- a/src/hooks/auto-update-checker/constants.ts +++ b/src/hooks/auto-update-checker/constants.ts @@ -38,3 +38,4 @@ function getUserConfigDir(): string { export const USER_CONFIG_DIR = getUserConfigDir() export const USER_OPENCODE_CONFIG = path.join(USER_CONFIG_DIR, "opencode", "opencode.json") +export const USER_OPENCODE_CONFIG_JSONC = path.join(USER_CONFIG_DIR, "opencode", "opencode.jsonc") From 6c65ac155a6c549da849c91deaef3fd868ba00f7 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Mon, 15 Dec 2025 03:32:40 +0900 Subject: [PATCH 2/5] refactor(auto-update-checker): improve config handling and cleanup - Add block comment handling to stripJsonComments for JSONC - Extract getConfigPaths helper to fix DRY violation - Fix findPluginEntry to recognize file:// dev entries - Remove debug log statements --- src/hooks/auto-update-checker/checker.ts | 65 +++++++++--------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/src/hooks/auto-update-checker/checker.ts b/src/hooks/auto-update-checker/checker.ts index 64b6917..67b6393 100644 --- a/src/hooks/auto-update-checker/checker.ts +++ b/src/hooks/auto-update-checker/checker.ts @@ -10,21 +10,29 @@ import { USER_OPENCODE_CONFIG, USER_OPENCODE_CONFIG_JSONC, } from "./constants" -import { log } from "../../shared/logger" export function isLocalDevMode(directory: string): boolean { return getLocalDevPath(directory) !== null } function stripJsonComments(json: string): string { - return json.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1") + return json + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/^\s*\/\/.*$/gm, "") + .replace(/,(\s*[}\]])/g, "$1") } -export function getLocalDevPath(directory: string): string | null { - const projectConfig = path.join(directory, ".opencode", "opencode.json") - const projectConfigJsonc = path.join(directory, ".opencode", "opencode.jsonc") +function getConfigPaths(directory: string): string[] { + return [ + path.join(directory, ".opencode", "opencode.json"), + path.join(directory, ".opencode", "opencode.jsonc"), + USER_OPENCODE_CONFIG, + USER_OPENCODE_CONFIG_JSONC, + ] +} - for (const configPath of [projectConfig, projectConfigJsonc, USER_OPENCODE_CONFIG, USER_OPENCODE_CONFIG_JSONC]) { +export function getLocalDevPath(directory: string): string | null { + for (const configPath of getConfigPaths(directory)) { try { if (!fs.existsSync(configPath)) continue const content = fs.readFileSync(configPath, "utf-8") @@ -72,23 +80,15 @@ function findPackageJsonUp(startPath: string): string | null { export function getLocalDevVersion(directory: string): string | null { const localPath = getLocalDevPath(directory) - if (!localPath) { - log("[auto-update-checker] getLocalDevVersion: no local path found") - return null - } + if (!localPath) return null try { const pkgPath = findPackageJsonUp(localPath) - if (!pkgPath) { - log(`[auto-update-checker] getLocalDevVersion: no package.json found from ${localPath}`) - return null - } + if (!pkgPath) return null const content = fs.readFileSync(pkgPath, "utf-8") const pkg = JSON.parse(content) as PackageJson - log(`[auto-update-checker] getLocalDevVersion: found version ${pkg.version} at ${pkgPath}`) return pkg.version ?? null - } catch (err) { - log("[auto-update-checker] getLocalDevVersion: error reading package.json", err) + } catch { return null } } @@ -100,10 +100,7 @@ export interface PluginEntryInfo { } export function findPluginEntry(directory: string): PluginEntryInfo | null { - const projectConfig = path.join(directory, ".opencode", "opencode.json") - const projectConfigJsonc = path.join(directory, ".opencode", "opencode.jsonc") - - for (const configPath of [projectConfig, projectConfigJsonc, USER_OPENCODE_CONFIG, USER_OPENCODE_CONFIG_JSONC]) { + for (const configPath of getConfigPaths(directory)) { try { if (!fs.existsSync(configPath)) continue const content = fs.readFileSync(configPath, "utf-8") @@ -119,6 +116,9 @@ export function findPluginEntry(directory: string): PluginEntryInfo | null { const isPinned = pinnedVersion !== "latest" return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null } } + if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) { + return { entry, isPinned: false, pinnedVersion: null } + } } } catch { continue @@ -133,10 +133,7 @@ export function getCachedVersion(): string | null { if (fs.existsSync(INSTALLED_PACKAGE_JSON)) { const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8") const pkg = JSON.parse(content) as PackageJson - if (pkg.version) { - log(`[auto-update-checker] getCachedVersion: found ${pkg.version} at ${INSTALLED_PACKAGE_JSON}`) - return pkg.version - } + if (pkg.version) return pkg.version } } catch {} @@ -146,15 +143,9 @@ export function getCachedVersion(): string | null { if (pkgPath) { const content = fs.readFileSync(pkgPath, "utf-8") const pkg = JSON.parse(content) as PackageJson - if (pkg.version) { - log(`[auto-update-checker] getCachedVersion: found ${pkg.version} at ${pkgPath}`) - return pkg.version - } + if (pkg.version) return pkg.version } - log(`[auto-update-checker] getCachedVersion: no package.json found from ${currentDir}`) - } catch (err) { - log("[auto-update-checker] getCachedVersion: error resolving version from current directory:", err) - } + } catch {} return null } @@ -182,36 +173,28 @@ export async function getLatestVersion(): Promise { export async function checkForUpdate(directory: string): Promise { if (isLocalDevMode(directory)) { - log("[auto-update-checker] Local dev mode detected, skipping update check") return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true, isPinned: false } } const pluginInfo = findPluginEntry(directory) if (!pluginInfo) { - log("[auto-update-checker] Plugin not found in config") return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false } } - // Respect version pinning if (pluginInfo.isPinned) { - log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`) return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true } } const currentVersion = getCachedVersion() if (!currentVersion) { - log("[auto-update-checker] No cached version found") return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false } } const latestVersion = await getLatestVersion() if (!latestVersion) { - log("[auto-update-checker] Failed to fetch latest version") return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: false } } const needsUpdate = currentVersion !== latestVersion - log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`) - return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false } } From 8b1232ad4f41f708f0b0574287780a5666e8792d Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Mon, 15 Dec 2025 04:06:57 +0900 Subject: [PATCH 3/5] fix(auto-update-checker): make stripJsonComments string-aware Use regex that skips comment markers inside quoted strings. Prevents corruption of URLs like http://example.com or text containing /* */. --- src/hooks/auto-update-checker/checker.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/auto-update-checker/checker.ts b/src/hooks/auto-update-checker/checker.ts index 67b6393..de14ac6 100644 --- a/src/hooks/auto-update-checker/checker.ts +++ b/src/hooks/auto-update-checker/checker.ts @@ -17,8 +17,7 @@ export function isLocalDevMode(directory: string): boolean { function stripJsonComments(json: string): string { return json - .replace(/\/\*[\s\S]*?\*\//g, "") - .replace(/^\s*\/\/.*$/gm, "") + .replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? "" : m)) .replace(/,(\s*[}\]])/g, "$1") } From b57ed423086768a132a81ae08f72db0794dfab4f Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Mon, 15 Dec 2025 04:07:39 +0900 Subject: [PATCH 4/5] refactor(auto-update-checker): remove dead file:// check from findPluginEntry This branch is unreachable because isLocalDevMode() catches file:// entries first via getLocalDevPath(), causing checkForUpdate() to return early. --- src/hooks/auto-update-checker/checker.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/hooks/auto-update-checker/checker.ts b/src/hooks/auto-update-checker/checker.ts index de14ac6..3472115 100644 --- a/src/hooks/auto-update-checker/checker.ts +++ b/src/hooks/auto-update-checker/checker.ts @@ -115,9 +115,6 @@ export function findPluginEntry(directory: string): PluginEntryInfo | null { const isPinned = pinnedVersion !== "latest" return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null } } - if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) { - return { entry, isPinned: false, pinnedVersion: null } - } } } catch { continue From 1671435da9fa9ae65daf1a6365c9ead34801aacb Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Mon, 15 Dec 2025 04:25:59 +0900 Subject: [PATCH 5/5] chore: restore original log statements --- src/hooks/auto-update-checker/checker.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/hooks/auto-update-checker/checker.ts b/src/hooks/auto-update-checker/checker.ts index 3472115..34ac355 100644 --- a/src/hooks/auto-update-checker/checker.ts +++ b/src/hooks/auto-update-checker/checker.ts @@ -10,6 +10,7 @@ import { USER_OPENCODE_CONFIG, USER_OPENCODE_CONFIG_JSONC, } from "./constants" +import { log } from "../../shared/logger" export function isLocalDevMode(directory: string): boolean { return getLocalDevPath(directory) !== null @@ -141,7 +142,9 @@ export function getCachedVersion(): string | null { const pkg = JSON.parse(content) as PackageJson if (pkg.version) return pkg.version } - } catch {} + } catch (err) { + log("[auto-update-checker] Failed to resolve version from current directory:", err) + } return null } @@ -169,28 +172,34 @@ export async function getLatestVersion(): Promise { export async function checkForUpdate(directory: string): Promise { if (isLocalDevMode(directory)) { + log("[auto-update-checker] Local dev mode detected, skipping update check") return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true, isPinned: false } } const pluginInfo = findPluginEntry(directory) if (!pluginInfo) { + log("[auto-update-checker] Plugin not found in config") return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false } } if (pluginInfo.isPinned) { + log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`) return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true } } const currentVersion = getCachedVersion() if (!currentVersion) { + log("[auto-update-checker] No cached version found") return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false } } const latestVersion = await getLatestVersion() if (!latestVersion) { + log("[auto-update-checker] Failed to fetch latest version") return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: false } } const needsUpdate = currentVersion !== latestVersion + log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`) return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false } }