fix(config-manager): support silent bun install execution
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,10 +1,25 @@
|
|||||||
import { beforeEach, afterEach, describe, expect, it, spyOn } from "bun:test"
|
|
||||||
import * as fs from "node:fs"
|
import * as fs from "node:fs"
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"
|
||||||
|
|
||||||
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"
|
||||||
import { runBunInstallWithDetails } from "./bun-install"
|
import { runBunInstallWithDetails } from "./bun-install"
|
||||||
|
|
||||||
|
function createProc(
|
||||||
|
exitCode: number,
|
||||||
|
output?: { stdout?: string; stderr?: string }
|
||||||
|
): ReturnType<typeof spawnHelpers.spawnWithWindowsHide> {
|
||||||
|
return {
|
||||||
|
exited: Promise.resolve(exitCode),
|
||||||
|
exitCode,
|
||||||
|
stdout: output?.stdout !== undefined ? new Blob([output.stdout]).stream() : undefined,
|
||||||
|
stderr: output?.stderr !== undefined ? new Blob([output.stderr]).stream() : undefined,
|
||||||
|
kill: () => {},
|
||||||
|
} satisfies ReturnType<typeof spawnHelpers.spawnWithWindowsHide>
|
||||||
|
}
|
||||||
|
|
||||||
describe("runBunInstallWithDetails", () => {
|
describe("runBunInstallWithDetails", () => {
|
||||||
let getOpenCodeCacheDirSpy: ReturnType<typeof spyOn>
|
let getOpenCodeCacheDirSpy: ReturnType<typeof spyOn>
|
||||||
let logSpy: ReturnType<typeof spyOn>
|
let logSpy: ReturnType<typeof spyOn>
|
||||||
@@ -14,11 +29,7 @@ describe("runBunInstallWithDetails", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getOpenCodeCacheDirSpy = spyOn(dataPath, "getOpenCodeCacheDir").mockReturnValue("/tmp/opencode-cache")
|
getOpenCodeCacheDirSpy = spyOn(dataPath, "getOpenCodeCacheDir").mockReturnValue("/tmp/opencode-cache")
|
||||||
logSpy = spyOn(logger, "log").mockImplementation(() => {})
|
logSpy = spyOn(logger, "log").mockImplementation(() => {})
|
||||||
spawnWithWindowsHideSpy = spyOn(spawnHelpers, "spawnWithWindowsHide").mockReturnValue({
|
spawnWithWindowsHideSpy = spyOn(spawnHelpers, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
||||||
exited: Promise.resolve(0),
|
|
||||||
exitCode: 0,
|
|
||||||
kill: () => {},
|
|
||||||
} as ReturnType<typeof spawnHelpers.spawnWithWindowsHide>)
|
|
||||||
existsSyncSpy = spyOn(fs, "existsSync").mockReturnValue(true)
|
existsSyncSpy = spyOn(fs, "existsSync").mockReturnValue(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -29,9 +40,13 @@ describe("runBunInstallWithDetails", () => {
|
|||||||
existsSyncSpy.mockRestore()
|
existsSyncSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("runs bun install in the OpenCode cache directory", async () => {
|
it("runs bun install in the OpenCode cache directory with inherited output by default", async () => {
|
||||||
|
// given
|
||||||
|
|
||||||
|
// when
|
||||||
const result = await runBunInstallWithDetails()
|
const result = await runBunInstallWithDetails()
|
||||||
|
|
||||||
|
// then
|
||||||
expect(result).toEqual({ success: true })
|
expect(result).toEqual({ success: true })
|
||||||
expect(getOpenCodeCacheDirSpy).toHaveBeenCalledTimes(1)
|
expect(getOpenCodeCacheDirSpy).toHaveBeenCalledTimes(1)
|
||||||
expect(spawnWithWindowsHideSpy).toHaveBeenCalledWith(["bun", "install"], {
|
expect(spawnWithWindowsHideSpy).toHaveBeenCalledWith(["bun", "install"], {
|
||||||
@@ -40,4 +55,42 @@ describe("runBunInstallWithDetails", () => {
|
|||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("pipes install output when requested", async () => {
|
||||||
|
// given
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await runBunInstallWithDetails({ outputMode: "pipe" })
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({ success: true })
|
||||||
|
expect(spawnWithWindowsHideSpy).toHaveBeenCalledWith(["bun", "install"], {
|
||||||
|
cwd: "/tmp/opencode-cache",
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("logs captured output when piped install fails", async () => {
|
||||||
|
// given
|
||||||
|
spawnWithWindowsHideSpy.mockReturnValue(
|
||||||
|
createProc(1, {
|
||||||
|
stdout: "resolved 10 packages",
|
||||||
|
stderr: "network error",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await runBunInstallWithDetails({ outputMode: "pipe" })
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: "bun install failed with exit code 1",
|
||||||
|
})
|
||||||
|
expect(logSpy).toHaveBeenCalledWith("[bun-install] Captured output from failed bun install", {
|
||||||
|
stdout: "resolved 10 packages",
|
||||||
|
stderr: "network error",
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { existsSync } from "node:fs"
|
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"
|
||||||
@@ -6,6 +7,26 @@ import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
|||||||
const BUN_INSTALL_TIMEOUT_SECONDS = 60
|
const BUN_INSTALL_TIMEOUT_SECONDS = 60
|
||||||
const BUN_INSTALL_TIMEOUT_MS = BUN_INSTALL_TIMEOUT_SECONDS * 1000
|
const BUN_INSTALL_TIMEOUT_MS = BUN_INSTALL_TIMEOUT_SECONDS * 1000
|
||||||
|
|
||||||
|
type BunInstallOutputMode = "inherit" | "pipe"
|
||||||
|
|
||||||
|
interface RunBunInstallOptions {
|
||||||
|
outputMode?: BunInstallOutputMode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BunInstallOutput {
|
||||||
|
stdout: string
|
||||||
|
stderr: string
|
||||||
|
}
|
||||||
|
|
||||||
|
declare function setTimeout(callback: () => void, delay?: number): number
|
||||||
|
declare function clearTimeout(timeout: number): void
|
||||||
|
|
||||||
|
type ProcessOutputStream = ReturnType<typeof spawnWithWindowsHide>["stdout"]
|
||||||
|
|
||||||
|
declare const Bun: {
|
||||||
|
readableStreamToText(stream: NonNullable<ProcessOutputStream>): Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
export interface BunInstallResult {
|
export interface BunInstallResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
timedOut?: boolean
|
timedOut?: boolean
|
||||||
@@ -17,7 +38,33 @@ export async function runBunInstall(): Promise<boolean> {
|
|||||||
return result.success
|
return result.success
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
function readProcessOutput(stream: ProcessOutputStream): Promise<string> {
|
||||||
|
if (!stream) {
|
||||||
|
return Promise.resolve("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bun.readableStreamToText(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
function logCapturedOutputOnFailure(outputMode: BunInstallOutputMode, output: BunInstallOutput): void {
|
||||||
|
if (outputMode !== "pipe") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdout = output.stdout.trim()
|
||||||
|
const stderr = output.stderr.trim()
|
||||||
|
if (!stdout && !stderr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[bun-install] Captured output from failed bun install", {
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runBunInstallWithDetails(options?: RunBunInstallOptions): Promise<BunInstallResult> {
|
||||||
|
const outputMode = options?.outputMode ?? "inherit"
|
||||||
const cacheDir = getOpenCodeCacheDir()
|
const cacheDir = getOpenCodeCacheDir()
|
||||||
const packageJsonPath = `${cacheDir}/package.json`
|
const packageJsonPath = `${cacheDir}/package.json`
|
||||||
|
|
||||||
@@ -31,17 +78,23 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
|||||||
try {
|
try {
|
||||||
const proc = spawnWithWindowsHide(["bun", "install"], {
|
const proc = spawnWithWindowsHide(["bun", "install"], {
|
||||||
cwd: cacheDir,
|
cwd: cacheDir,
|
||||||
stdout: "inherit",
|
stdout: outputMode,
|
||||||
stderr: "inherit",
|
stderr: outputMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout>
|
const outputPromise = Promise.all([readProcessOutput(proc.stdout), readProcessOutput(proc.stderr)]).then(
|
||||||
|
([stdout, stderr]) => ({ stdout, stderr })
|
||||||
|
)
|
||||||
|
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||||
const timeoutPromise = new Promise<"timeout">((resolve) => {
|
const timeoutPromise = new Promise<"timeout">((resolve) => {
|
||||||
timeoutId = setTimeout(() => resolve("timeout"), BUN_INSTALL_TIMEOUT_MS)
|
timeoutId = setTimeout(() => resolve("timeout"), BUN_INSTALL_TIMEOUT_MS)
|
||||||
})
|
})
|
||||||
const exitPromise = proc.exited.then(() => "completed" as const)
|
const exitPromise = proc.exited.then(() => "completed" as const)
|
||||||
const result = await Promise.race([exitPromise, timeoutPromise])
|
const result = await Promise.race([exitPromise, timeoutPromise])
|
||||||
clearTimeout(timeoutId!)
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
|
||||||
if (result === "timeout") {
|
if (result === "timeout") {
|
||||||
try {
|
try {
|
||||||
@@ -49,6 +102,10 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("[cli/install] Failed to kill timed out bun install process:", err)
|
log("[cli/install] Failed to kill timed out bun install process:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await proc.exited
|
||||||
|
logCapturedOutputOnFailure(outputMode, await outputPromise)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
timedOut: true,
|
timedOut: true,
|
||||||
@@ -56,7 +113,11 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const output = await outputPromise
|
||||||
|
|
||||||
if (proc.exitCode !== 0) {
|
if (proc.exitCode !== 0) {
|
||||||
|
logCapturedOutputOnFailure(outputMode, output)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `bun install failed with exit code ${proc.exitCode}`,
|
error: `bun install failed with exit code ${proc.exitCode}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user