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:
@@ -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 dataPath from "../../shared/data-path"
|
||||||
import * as logger from "../../shared/logger"
|
import * as logger from "../../shared/logger"
|
||||||
import * as spawnHelpers from "../../shared/spawn-with-windows-hide"
|
import * as spawnHelpers from "../../shared/spawn-with-windows-hide"
|
||||||
@@ -8,6 +9,7 @@ describe("runBunInstallWithDetails", () => {
|
|||||||
let getOpenCodeCacheDirSpy: ReturnType<typeof spyOn>
|
let getOpenCodeCacheDirSpy: ReturnType<typeof spyOn>
|
||||||
let logSpy: ReturnType<typeof spyOn>
|
let logSpy: ReturnType<typeof spyOn>
|
||||||
let spawnWithWindowsHideSpy: ReturnType<typeof spyOn>
|
let spawnWithWindowsHideSpy: ReturnType<typeof spyOn>
|
||||||
|
let existsSyncSpy: ReturnType<typeof spyOn>
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getOpenCodeCacheDirSpy = spyOn(dataPath, "getOpenCodeCacheDir").mockReturnValue("/tmp/opencode-cache")
|
getOpenCodeCacheDirSpy = spyOn(dataPath, "getOpenCodeCacheDir").mockReturnValue("/tmp/opencode-cache")
|
||||||
@@ -15,14 +17,16 @@ describe("runBunInstallWithDetails", () => {
|
|||||||
spawnWithWindowsHideSpy = spyOn(spawnHelpers, "spawnWithWindowsHide").mockReturnValue({
|
spawnWithWindowsHideSpy = spyOn(spawnHelpers, "spawnWithWindowsHide").mockReturnValue({
|
||||||
exited: Promise.resolve(0),
|
exited: Promise.resolve(0),
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
kill: mock(() => {}),
|
kill: () => {},
|
||||||
} as ReturnType<typeof spawnHelpers.spawnWithWindowsHide>)
|
} as ReturnType<typeof spawnHelpers.spawnWithWindowsHide>)
|
||||||
|
existsSyncSpy = spyOn(fs, "existsSync").mockReturnValue(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
getOpenCodeCacheDirSpy.mockRestore()
|
getOpenCodeCacheDirSpy.mockRestore()
|
||||||
logSpy.mockRestore()
|
logSpy.mockRestore()
|
||||||
spawnWithWindowsHideSpy.mockRestore()
|
spawnWithWindowsHideSpy.mockRestore()
|
||||||
|
existsSyncSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("runs bun install in the OpenCode cache directory", async () => {
|
it("runs bun install in the OpenCode cache directory", async () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { existsSync } from "node:fs"
|
||||||
import { getOpenCodeCacheDir } from "../../shared/data-path"
|
import { getOpenCodeCacheDir } from "../../shared/data-path"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
@@ -18,6 +19,14 @@ export async function runBunInstall(): Promise<boolean> {
|
|||||||
|
|
||||||
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||||
const cacheDir = getOpenCodeCacheDir()
|
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 {
|
try {
|
||||||
const proc = spawnWithWindowsHide(["bun", "install"], {
|
const proc = spawnWithWindowsHide(["bun", "install"], {
|
||||||
@@ -43,7 +52,7 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
timedOut: true,
|
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`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,31 +16,49 @@ function stripTrailingCommas(json: string): string {
|
|||||||
return json.replace(/,(\s*[}\]])/g, "$1")
|
return json.replace(/,(\s*[}\]])/g, "$1")
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromBunLock(packageName: string): boolean {
|
function removeFromTextBunLock(lockPath: string, packageName: string): boolean {
|
||||||
const lockPath = path.join(CACHE_DIR, "bun.lock")
|
|
||||||
if (!fs.existsSync(lockPath)) return false
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(lockPath, "utf-8")
|
const content = fs.readFileSync(lockPath, "utf-8")
|
||||||
const lock = JSON.parse(stripTrailingCommas(content)) as BunLockfile
|
const lock = JSON.parse(stripTrailingCommas(content)) as BunLockfile
|
||||||
let modified = false
|
|
||||||
|
|
||||||
if (lock.packages?.[packageName]) {
|
if (lock.packages?.[packageName]) {
|
||||||
delete lock.packages[packageName]
|
delete lock.packages[packageName]
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modified) {
|
|
||||||
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2))
|
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2))
|
||||||
log(`[auto-update-checker] Removed from bun.lock: ${packageName}`)
|
log(`[auto-update-checker] Removed from bun.lock: ${packageName}`)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
return modified
|
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
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 {
|
export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
|
||||||
try {
|
try {
|
||||||
const pkgDirs = [
|
const pkgDirs = [
|
||||||
|
|||||||
Reference in New Issue
Block a user