diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index e8b5e995cc..9c9cbd6b00 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -1,12 +1,11 @@ #!/usr/bin/env node import fs from "fs" +import crypto from "crypto" import path from "path" import os from "os" -import { fileURLToPath } from "url" import { createRequire } from "module" -const __dirname = path.dirname(fileURLToPath(import.meta.url)) const require = createRequire(import.meta.url) function detectPlatformAndArch() { @@ -49,60 +48,91 @@ function detectPlatformAndArch() { function findBinary() { const { platform, arch } = detectPlatformAndArch() - const packageName = `opencode-${platform}-${arch}` const binaryName = platform === "windows" ? "opencode.exe" : "opencode" - try { - // Use require.resolve to find the package - const packageJsonPath = require.resolve(`${packageName}/package.json`) - const packageDir = path.dirname(packageJsonPath) - const binaryPath = path.join(packageDir, "bin", binaryName) + // Try modern variant first, then baseline (for CPUs without AVX2) + const candidates = [`opencode-${platform}-${arch}`, `opencode-${platform}-${arch}-baseline`] - if (!fs.existsSync(binaryPath)) { - throw new Error(`Binary not found at ${binaryPath}`) + for (const packageName of candidates) { + try { + const packageJsonPath = require.resolve(`${packageName}/package.json`) + const packageDir = path.dirname(packageJsonPath) + const binaryPath = path.join(packageDir, "bin", binaryName) + + if (fs.existsSync(binaryPath)) { + return { binaryPath, binaryName } + } + } catch { + // Package not installed, try next candidate } - - return { binaryPath, binaryName } - } catch (error) { - throw new Error(`Could not find package ${packageName}: ${error.message}`) } + + throw new Error(`Could not find platform binary for ${platform}-${arch}`) } -function prepareBinDirectory(binaryName) { - const binDir = path.join(__dirname, "bin") - const targetPath = path.join(binDir, binaryName) +function findBunShimPath() { + // Mirrors Bun's openGlobalBinDir() resolution order from + // src/install/PackageManager/PackageManagerOptions.zig:201-233 + // 1. $BUN_INSTALL_BIN + // 2. (bunfig.toml globalBinDir — not accessible from postinstall) + // 3. $BUN_INSTALL/bin + // 4. $XDG_CACHE_HOME/.bun/bin + // 5. $HOME/.bun/bin + const candidates = [ + process.env.BUN_INSTALL_BIN, + process.env.BUN_INSTALL && path.join(process.env.BUN_INSTALL, "bin"), + process.env.XDG_CACHE_HOME && path.join(process.env.XDG_CACHE_HOME, ".bun", "bin"), + path.join(os.homedir(), ".bun", "bin"), + ] - // Ensure bin directory exists - if (!fs.existsSync(binDir)) { - fs.mkdirSync(binDir, { recursive: true }) + for (const binDir of candidates) { + if (!binDir) continue + const shimPath = path.join(binDir, "opencode.exe") + if (fs.existsSync(shimPath)) { + return shimPath + } } - - // Remove existing binary/symlink if it exists - if (fs.existsSync(targetPath)) { - fs.unlinkSync(targetPath) - } - - return { binDir, targetPath } + return null } -function symlinkBinary(sourcePath, binaryName) { - const { targetPath } = prepareBinDirectory(binaryName) - - fs.symlinkSync(sourcePath, targetPath) - console.log(`opencode binary symlinked: ${targetPath} -> ${sourcePath}`) - - // Verify the file exists after operation - if (!fs.existsSync(targetPath)) { - throw new Error(`Failed to symlink binary to ${targetPath}`) +function replaceBunShim(realBinaryPath) { + const shimPath = findBunShimPath() + if (!shimPath) { + console.log("Bun shim not found, skipping replacement") + return } + + // Check if the shim is already the real binary by comparing file hashes + const shimHash = crypto.createHash("sha256").update(fs.readFileSync(shimPath)).digest("hex") + const realHash = crypto.createHash("sha256").update(fs.readFileSync(realBinaryPath)).digest("hex") + if (shimHash === realHash) { + console.log("Bun shim already replaced with real binary") + return + } + + console.log("Replacing Bun shim with real binary") + console.log(` shim: ${shimPath}`) + console.log(` real: ${realBinaryPath}`) + fs.copyFileSync(realBinaryPath, shimPath) + console.log("Bun shim replaced successfully") } async function main() { try { if (os.platform() === "win32") { - // On Windows, the .exe is already included in the package and bin field points to it - // No postinstall setup needed - console.log("Windows detected: binary setup not needed (using packaged .exe)") + // On Windows, Bun's global install creates a tiny shim exe (~15KB) that + // launches the JS wrapper via spawnSync. This shim receives CTRL_C_EVENT + // from the Windows console and dies instantly, killing the real opencode + // process before it can handle the signal gracefully. + // + // Fix: replace the shim with the real compiled binary so Ctrl+C goes + // directly to opencode's signal handler (which uses the anomalyco/bun + // fork with the SIGINT fix from PR #25876). + const agent = (process.env.npm_config_user_agent || "").toLowerCase() + if (agent.startsWith("bun")) { + const { binaryPath } = findBinary() + replaceBunShim(binaryPath) + } return }