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
This commit is contained in:
acamq
2026-03-08 09:15:13 -06:00
parent f89cc969ec
commit c4112f80db
3 changed files with 45 additions and 14 deletions
+6 -2
View File
@@ -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<typeof spyOn>
let logSpy: ReturnType<typeof spyOn>
let spawnWithWindowsHideSpy: ReturnType<typeof spyOn>
let existsSyncSpy: ReturnType<typeof spyOn>
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<typeof spawnHelpers.spawnWithWindowsHide>)
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 () => {
+10 -1
View File
@@ -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<boolean> {
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
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<BunInstallResult> {
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`,
}
}
+29 -11
View File
@@ -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 = [