From 740d39e13a6f1375c6ce0e36e8f71e8617507cd3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Mar 2026 02:08:37 +0900 Subject: [PATCH 1/2] fix(doctor): prefer config dir for loaded plugin version Check the OpenCode config install before the legacy cache install so doctor reports the actual loaded plugin version for bun-based installs. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../checks/system-loaded-version.test.ts | 103 +++++++++++++++++- .../doctor/checks/system-loaded-version.ts | 23 +++- 2 files changed, 120 insertions(+), 6 deletions(-) diff --git a/src/cli/doctor/checks/system-loaded-version.test.ts b/src/cli/doctor/checks/system-loaded-version.test.ts index ecf232f308..c75b7e920a 100644 --- a/src/cli/doctor/checks/system-loaded-version.test.ts +++ b/src/cli/doctor/checks/system-loaded-version.test.ts @@ -1,8 +1,107 @@ -import { describe, expect, it } from "bun:test" +import { afterEach, describe, expect, it } from "bun:test" +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { dirname, join } from "node:path" -import { getSuggestedInstallTag } from "./system-loaded-version" +import { PACKAGE_NAME } from "../constants" +import { getLoadedPluginVersion, getSuggestedInstallTag } from "./system-loaded-version" + +const originalOpencodeConfigDir = process.env.OPENCODE_CONFIG_DIR +const originalXdgCacheHome = process.env.XDG_CACHE_HOME +const temporaryDirectories: string[] = [] + +function createTemporaryDirectory(prefix: string): string { + const directory = mkdtempSync(join(tmpdir(), prefix)) + temporaryDirectories.push(directory) + return directory +} + +function writeJson(filePath: string, value: Record>): void { + mkdirSync(dirname(filePath), { recursive: true }) + writeFileSync(filePath, JSON.stringify(value), "utf-8") +} + +afterEach(() => { + if (originalOpencodeConfigDir === undefined) { + delete process.env.OPENCODE_CONFIG_DIR + } else { + process.env.OPENCODE_CONFIG_DIR = originalOpencodeConfigDir + } + + if (originalXdgCacheHome === undefined) { + delete process.env.XDG_CACHE_HOME + } else { + process.env.XDG_CACHE_HOME = originalXdgCacheHome + } + + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }) + } +}) describe("system loaded version", () => { + describe("getLoadedPluginVersion", () => { + it("prefers the config directory when both installs exist", () => { + //#given + const configDir = createTemporaryDirectory("omo-config-") + const cacheHome = createTemporaryDirectory("omo-cache-") + const cacheDir = join(cacheHome, "opencode") + + process.env.OPENCODE_CONFIG_DIR = configDir + process.env.XDG_CACHE_HOME = cacheHome + + writeJson(join(configDir, "package.json"), { + dependencies: { [PACKAGE_NAME]: "1.2.3" }, + }) + writeJson(join(configDir, "node_modules", PACKAGE_NAME, "package.json"), { + version: "1.2.3", + }) + writeJson(join(cacheDir, "package.json"), { + dependencies: { [PACKAGE_NAME]: "9.9.9" }, + }) + writeJson(join(cacheDir, "node_modules", PACKAGE_NAME, "package.json"), { + version: "9.9.9", + }) + + //#when + const loadedVersion = getLoadedPluginVersion() + + //#then + expect(loadedVersion.cacheDir).toBe(configDir) + expect(loadedVersion.cachePackagePath).toBe(join(configDir, "package.json")) + expect(loadedVersion.installedPackagePath).toBe(join(configDir, "node_modules", PACKAGE_NAME, "package.json")) + expect(loadedVersion.expectedVersion).toBe("1.2.3") + expect(loadedVersion.loadedVersion).toBe("1.2.3") + }) + + it("falls back to the cache directory for legacy installs", () => { + //#given + const configDir = createTemporaryDirectory("omo-config-") + const cacheHome = createTemporaryDirectory("omo-cache-") + const cacheDir = join(cacheHome, "opencode") + + process.env.OPENCODE_CONFIG_DIR = configDir + process.env.XDG_CACHE_HOME = cacheHome + + writeJson(join(cacheDir, "package.json"), { + dependencies: { [PACKAGE_NAME]: "2.3.4" }, + }) + writeJson(join(cacheDir, "node_modules", PACKAGE_NAME, "package.json"), { + version: "2.3.4", + }) + + //#when + const loadedVersion = getLoadedPluginVersion() + + //#then + expect(loadedVersion.cacheDir).toBe(cacheDir) + expect(loadedVersion.cachePackagePath).toBe(join(cacheDir, "package.json")) + expect(loadedVersion.installedPackagePath).toBe(join(cacheDir, "node_modules", PACKAGE_NAME, "package.json")) + expect(loadedVersion.expectedVersion).toBe("2.3.4") + expect(loadedVersion.loadedVersion).toBe("2.3.4") + }) + }) + describe("getSuggestedInstallTag", () => { it("returns prerelease channel when current version is prerelease", () => { //#given diff --git a/src/cli/doctor/checks/system-loaded-version.ts b/src/cli/doctor/checks/system-loaded-version.ts index bbf02516c4..a62c0f97ae 100644 --- a/src/cli/doctor/checks/system-loaded-version.ts +++ b/src/cli/doctor/checks/system-loaded-version.ts @@ -5,7 +5,7 @@ import { join } from "node:path" import { getLatestVersion } from "../../../hooks/auto-update-checker/checker" import { extractChannel } from "../../../hooks/auto-update-checker" import { PACKAGE_NAME } from "../constants" -import { getOpenCodeCacheDir, parseJsonc } from "../../../shared" +import { getOpenCodeCacheDir, getOpenCodeConfigPaths, parseJsonc } from "../../../shared" interface PackageJsonShape { version?: string @@ -54,9 +54,24 @@ function normalizeVersion(value: string | undefined): string | null { } export function getLoadedPluginVersion(): LoadedVersionInfo { + const configPaths = getOpenCodeConfigPaths({ binary: "opencode" }) const cacheDir = resolveOpenCodeCacheDir() - const cachePackagePath = join(cacheDir, "package.json") - const installedPackagePath = join(cacheDir, "node_modules", PACKAGE_NAME, "package.json") + const candidates = [ + { + cacheDir: configPaths.configDir, + cachePackagePath: configPaths.packageJson, + installedPackagePath: join(configPaths.configDir, "node_modules", PACKAGE_NAME, "package.json"), + }, + { + cacheDir, + cachePackagePath: join(cacheDir, "package.json"), + installedPackagePath: join(cacheDir, "node_modules", PACKAGE_NAME, "package.json"), + }, + ] + + const selectedCandidate = candidates.find((candidate) => existsSync(candidate.installedPackagePath)) ?? candidates[0] + + const { cacheDir: selectedDir, cachePackagePath, installedPackagePath } = selectedCandidate const cachePackage = readPackageJson(cachePackagePath) const installedPackage = readPackageJson(installedPackagePath) @@ -65,7 +80,7 @@ export function getLoadedPluginVersion(): LoadedVersionInfo { const loadedVersion = normalizeVersion(installedPackage?.version) return { - cacheDir, + cacheDir: selectedDir, cachePackagePath, installedPackagePath, expectedVersion, From 26ae247f4f21d3c53db1424804c04342fc29de84 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Mar 2026 07:07:06 +0900 Subject: [PATCH 2/2] test(doctor): isolate loaded version module import Load the doctor loaded-version module through a unique test-only specifier so Bun module mocks from system tests cannot leak into the real module assertions in CI. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/cli/doctor/checks/system-loaded-version.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli/doctor/checks/system-loaded-version.test.ts b/src/cli/doctor/checks/system-loaded-version.test.ts index c75b7e920a..3a89ee82d6 100644 --- a/src/cli/doctor/checks/system-loaded-version.test.ts +++ b/src/cli/doctor/checks/system-loaded-version.test.ts @@ -4,7 +4,11 @@ import { tmpdir } from "node:os" import { dirname, join } from "node:path" import { PACKAGE_NAME } from "../constants" -import { getLoadedPluginVersion, getSuggestedInstallTag } from "./system-loaded-version" + +const systemLoadedVersionModulePath = "./system-loaded-version?system-loaded-version-test" + +const { getLoadedPluginVersion, getSuggestedInstallTag }: typeof import("./system-loaded-version") = + await import(systemLoadedVersionModulePath) const originalOpencodeConfigDir = process.env.OPENCODE_CONFIG_DIR const originalXdgCacheHome = process.env.XDG_CACHE_HOME