From 73443585e5e939522d306c601056dcdf609d67c3 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 14 Nov 2025 02:38:57 +1000 Subject: [PATCH] fix: resolve bun/pnpm global install failures on Windows (#4275) Co-authored-by: Aiden Cline --- packages/opencode/script/build.ts | 9 +-- packages/opencode/script/postinstall.mjs | 70 +----------------------- packages/opencode/script/publish.ts | 31 ++++++++++- 3 files changed, 36 insertions(+), 74 deletions(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 6731bea0..502baed0 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -66,11 +66,11 @@ const allTargets: { avx2: false, }, { - os: "windows", + os: "win32", arch: "x64", }, { - os: "windows", + os: "win32", arch: "x64", avx2: false, }, @@ -88,7 +88,8 @@ await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parc for (const item of targets) { const name = [ pkg.name, - item.os, + // changing to win32 flags npm for some reason + item.os === "win32" ? "windows" : item.os, item.arch, item.avx2 === false ? "baseline" : undefined, item.abi === undefined ? undefined : item.abi, @@ -127,7 +128,7 @@ for (const item of targets) { { name, version: Script.version, - os: [item.os === "windows" ? "win32" : item.os], + os: [item.os], cpu: [item.arch], }, null, diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 23a66517..78f022c9 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -85,18 +85,6 @@ function prepareBinDirectory(binaryName) { return { binDir, targetPath } } -function copyBinary(sourcePath, binaryName) { - const { targetPath } = prepareBinDirectory(binaryName) - - fs.copyFileSync(sourcePath, targetPath) - console.log(`opencode binary installed: ${targetPath}`) - - // Verify the file exists after operation - if (!fs.existsSync(targetPath)) { - throw new Error(`Failed to copy binary to ${targetPath}`) - } -} - function symlinkBinary(sourcePath, binaryName) { const { targetPath } = prepareBinDirectory(binaryName) @@ -109,64 +97,12 @@ function symlinkBinary(sourcePath, binaryName) { } } -async function regenerateWindowsCmdWrappers() { - console.log("Windows + npm detected: Forcing npm to rebuild bin links") - - try { - const { execSync } = require("child_process") - const pkgPath = path.join(__dirname, "..") - - // npm_config_global is string | undefined - // if it exists, the value is true - const isGlobal = process.env.npm_config_global === "true" || pkgPath.includes(path.join("npm", "node_modules")) - - // The npm rebuild command does 2 things - Execute lifecycle scripts and rebuild bin links - // We want to skip lifecycle scripts to avoid infinite loops, so we use --ignore-scripts - const cmd = `npm rebuild opencode-ai --ignore-scripts${isGlobal ? " -g" : ""}` - const opts = { - stdio: "inherit", - shell: true, - ...(isGlobal ? {} : { cwd: path.join(pkgPath, "..", "..") }), // For local, run from project root - } - - console.log(`Running: ${cmd}`) - execSync(cmd, opts) - console.log("Successfully rebuilt npm bin links") - } catch (error) { - console.error("Error rebuilding npm links:", error.message) - console.error("npm rebuild failed. You may need to manually run: npm rebuild opencode-ai --ignore-scripts") - } -} - async function main() { try { if (os.platform() === "win32") { - // NPM eg format - npm/11.4.2 node/v24.4.1 win32 x64 - // Bun eg format - bun/1.2.19 npm/? node/v24.3.0 win32 x64 - // pnpm eg format - pnpm/8.10.0 npm/? node/v20.10.0 win32 x64 - const userAgent = process.env.npm_config_user_agent || "" - - if (userAgent.startsWith("npm")) { - await regenerateWindowsCmdWrappers() - return - } - - if (userAgent.startsWith("bun")) { - console.log("Windows + bun detected: Setting up binary") - const { binaryPath, binaryName } = findBinary() - copyBinary(binaryPath, binaryName) - return - } - - if (userAgent.startsWith("pnpm")) { - console.log("Windows + pnpm detected: Setting up binary") - const { binaryPath, binaryName } = findBinary() - copyBinary(binaryPath, binaryName) - return - } - - // Unknown package manager on Windows - console.log("Windows detected but unknown package manager, skipping postinstall") + // 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)") return } diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 3e989cc6..ac2fce56 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -2,8 +2,10 @@ import { $ } from "bun" import pkg from "../package.json" import { Script } from "@opencode-ai/script" +import { fileURLToPath } from "url" +import fs from "fs" -const dir = new URL("..", import.meta.url).pathname +const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) const { binaries } = await import("./build.ts") @@ -15,14 +17,29 @@ const { binaries } = await import("./build.ts") await $`mkdir -p ./dist/${pkg.name}` await $`cp -r ./bin ./dist/${pkg.name}/bin` + +// Copy Windows .exe if any Windows binaries were built +let hasWindowsBinary = false +for (const binaryName of Object.keys(binaries)) { + if (binaryName.includes("win32")) { + const winBinaryPath = `./dist/${binaryName}/bin/opencode.exe` + if (fs.existsSync(winBinaryPath)) { + await $`cp ${winBinaryPath} ./dist/${pkg.name}/bin/opencode.exe` + hasWindowsBinary = true + break + } + } +} + await $`cp ./script/preinstall.mjs ./dist/${pkg.name}/preinstall.mjs` await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs` + await Bun.file(`./dist/${pkg.name}/package.json`).write( JSON.stringify( { name: pkg.name + "-ai", bin: { - [pkg.name]: `./bin/${pkg.name}`, + [pkg.name]: hasWindowsBinary ? `./bin/${pkg.name}.exe` : `./bin/${pkg.name}`, }, scripts: { preinstall: "bun ./preinstall.mjs || node ./preinstall.mjs", @@ -36,7 +53,15 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write( ), ) for (const [name] of Object.entries(binaries)) { - await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${Script.channel}` + try { + process.chdir(`./dist/${name}`) + if (process.platform !== "win32") { + await $`chmod 755 -R .` + } + await $`bun publish --access public --tag ${Script.channel}` + } finally { + process.chdir(dir) + } } await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${Script.channel}`