From c4112f80db595caadc557b3cefdf4f773dce8273 Mon Sep 17 00:00:00 2001 From: acamq <179265037+acamq@users.noreply.github.com> Date: Sun, 8 Mar 2026 09:15:13 -0600 Subject: [PATCH] fix(auto-updater): handle bun.lockb and add workspace validation - Support binary bun.lockb format by deleting entire file (cannot parse) - Add workspace check: verify package.json exists before bun install - Quote paths in error messages for Windows/paths with spaces --- src/cli/config-manager/bun-install.test.ts | 8 +++-- src/cli/config-manager/bun-install.ts | 11 +++++- src/hooks/auto-update-checker/cache.ts | 40 ++++++++++++++++------ 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/cli/config-manager/bun-install.test.ts b/src/cli/config-manager/bun-install.test.ts index 28afea8d3..97cd4585a 100644 --- a/src/cli/config-manager/bun-install.test.ts +++ b/src/cli/config-manager/bun-install.test.ts @@ -1,4 +1,5 @@ -import { beforeEach, afterEach, describe, expect, it, mock, spyOn } from "bun:test" +import { beforeEach, afterEach, describe, expect, it, spyOn } from "bun:test" +import * as fs from "node:fs" import * as dataPath from "../../shared/data-path" import * as logger from "../../shared/logger" import * as spawnHelpers from "../../shared/spawn-with-windows-hide" @@ -8,6 +9,7 @@ describe("runBunInstallWithDetails", () => { let getOpenCodeCacheDirSpy: ReturnType let logSpy: ReturnType let spawnWithWindowsHideSpy: ReturnType + let existsSyncSpy: ReturnType beforeEach(() => { getOpenCodeCacheDirSpy = spyOn(dataPath, "getOpenCodeCacheDir").mockReturnValue("/tmp/opencode-cache") @@ -15,14 +17,16 @@ describe("runBunInstallWithDetails", () => { spawnWithWindowsHideSpy = spyOn(spawnHelpers, "spawnWithWindowsHide").mockReturnValue({ exited: Promise.resolve(0), exitCode: 0, - kill: mock(() => {}), + kill: () => {}, } as ReturnType) + existsSyncSpy = spyOn(fs, "existsSync").mockReturnValue(true) }) afterEach(() => { getOpenCodeCacheDirSpy.mockRestore() logSpy.mockRestore() spawnWithWindowsHideSpy.mockRestore() + existsSyncSpy.mockRestore() }) it("runs bun install in the OpenCode cache directory", async () => { diff --git a/src/cli/config-manager/bun-install.ts b/src/cli/config-manager/bun-install.ts index ab3a9c27e..230b03eae 100644 --- a/src/cli/config-manager/bun-install.ts +++ b/src/cli/config-manager/bun-install.ts @@ -1,3 +1,4 @@ +import { existsSync } from "node:fs" import { getOpenCodeCacheDir } from "../../shared/data-path" import { log } from "../../shared/logger" import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide" @@ -18,6 +19,14 @@ export async function runBunInstall(): Promise { export async function runBunInstallWithDetails(): Promise { const cacheDir = getOpenCodeCacheDir() + const packageJsonPath = `${cacheDir}/package.json` + + if (!existsSync(packageJsonPath)) { + return { + success: false, + error: `Workspace not initialized: ${packageJsonPath} not found. OpenCode should create this on first run.`, + } + } try { const proc = spawnWithWindowsHide(["bun", "install"], { @@ -43,7 +52,7 @@ export async function runBunInstallWithDetails(): Promise { return { success: false, timedOut: true, - error: `bun install timed out after ${BUN_INSTALL_TIMEOUT_SECONDS} seconds. Try running manually: cd ${cacheDir} && bun i`, + error: `bun install timed out after ${BUN_INSTALL_TIMEOUT_SECONDS} seconds. Try running manually: cd "${cacheDir}" && bun i`, } } diff --git a/src/hooks/auto-update-checker/cache.ts b/src/hooks/auto-update-checker/cache.ts index 14853e78f..2235bbadd 100644 --- a/src/hooks/auto-update-checker/cache.ts +++ b/src/hooks/auto-update-checker/cache.ts @@ -16,31 +16,49 @@ function stripTrailingCommas(json: string): string { return json.replace(/,(\s*[}\]])/g, "$1") } -function removeFromBunLock(packageName: string): boolean { - const lockPath = path.join(CACHE_DIR, "bun.lock") - if (!fs.existsSync(lockPath)) return false - +function removeFromTextBunLock(lockPath: string, packageName: string): boolean { try { const content = fs.readFileSync(lockPath, "utf-8") const lock = JSON.parse(stripTrailingCommas(content)) as BunLockfile - let modified = false if (lock.packages?.[packageName]) { delete lock.packages[packageName] - modified = true - } - - if (modified) { fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2)) log(`[auto-update-checker] Removed from bun.lock: ${packageName}`) + return true } - - return modified + return false } catch { return false } } +function deleteBinaryBunLock(lockPath: string): boolean { + try { + fs.unlinkSync(lockPath) + log(`[auto-update-checker] Removed bun.lockb to force re-resolution`) + return true + } catch { + return false + } +} + +function removeFromBunLock(packageName: string): boolean { + const textLockPath = path.join(CACHE_DIR, "bun.lock") + const binaryLockPath = path.join(CACHE_DIR, "bun.lockb") + + if (fs.existsSync(textLockPath)) { + return removeFromTextBunLock(textLockPath, packageName) + } + + // Binary lockfiles cannot be parsed; deletion forces bun to re-resolve + if (fs.existsSync(binaryLockPath)) { + return deleteBinaryBunLock(binaryLockPath) + } + + return false +} + export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean { try { const pkgDirs = [