Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- ci
- dev
- beta
- fix/npm-native-binary-install
- snapshot-*
workflow_dispatch:
inputs:
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ for (const item of targets) {
{
name,
version: Script.version,
preferUnplugged: true,
os: [item.os],
cpu: [item.arch],
},
Expand Down
223 changes: 155 additions & 68 deletions packages/opencode/script/postinstall.mjs
Original file line number Diff line number Diff line change
@@ -1,102 +1,189 @@
#!/usr/bin/env node

import childProcess from "child_process"
import fs from "fs"
import path from "path"
import os from "os"
import { fileURLToPath } from "url"
import path from "path"
import { createRequire } from "module"
import { fileURLToPath } from "url"

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))

const platformMap = {
darwin: "darwin",
linux: "linux",
win32: "windows",
}
const archMap = {
x64: "x64",
arm64: "arm64",
arm: "arm",
}

const platform = platformMap[os.platform()] ?? os.platform()
const arch = archMap[os.arch()] ?? os.arch()
const base = `opencode-${platform}-${arch}`
const sourceBinary = platform === "windows" ? "opencode.exe" : "opencode"
const targetBinary = path.join(__dirname, "bin", "opencode.exe")

function supportsAvx2() {
if (arch !== "x64") return false

if (platform === "linux") {
try {
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
} catch {
return false
}
}

function detectPlatformAndArch() {
// Map platform names
let platform
switch (os.platform()) {
case "darwin":
platform = "darwin"
break
case "linux":
platform = "linux"
break
case "win32":
platform = "windows"
break
default:
platform = os.platform()
break
if (platform === "darwin") {
try {
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
encoding: "utf8",
timeout: 1500,
})
if (result.status !== 0) return false
return (result.stdout || "").trim() === "1"
} catch {
return false
}
}

// Map architecture names
let arch
switch (os.arch()) {
case "x64":
arch = "x64"
break
case "arm64":
arch = "arm64"
break
case "arm":
arch = "arm"
break
default:
arch = os.arch()
break
if (platform === "windows") {
const command =
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'

for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
try {
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
encoding: "utf8",
timeout: 3000,
windowsHide: true,
})
if (result.status !== 0) continue
const output = (result.stdout || "").trim().toLowerCase()
if (output === "true" || output === "1") return true
if (output === "false" || output === "0") return false
} catch {
continue
}
}
}

return { platform, arch }
return false
}

function findBinary() {
const { platform, arch } = detectPlatformAndArch()
const packageName = `opencode-${platform}-${arch}`
const binaryName = platform === "windows" ? "opencode.exe" : "opencode"
function isMusl() {
if (platform !== "linux") return false

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)
if (fs.existsSync("/etc/alpine-release")) return true
} catch {
// Ignore filesystem probes that are blocked by the host.
}

if (!fs.existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}`)
try {
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl")
} catch {
return false
}
}

function packageNames() {
const baseline = arch === "x64" && !supportsAvx2()

if (platform === "linux") {
if (isMusl()) {
if (arch === "x64")
return baseline
? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
: [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
return [`${base}-musl`, base]
}

return { binaryPath, binaryName }
} catch (error) {
throw new Error(`Could not find package ${packageName}: ${error.message}`, { cause: error })
if (arch === "x64")
return baseline
? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
: [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
return [base, `${base}-musl`]
}

if (arch === "x64") return baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]
return [base]
}

function resolveBinary(name) {
const packageJsonPath = require.resolve(`${name}/package.json`)
const binaryPath = path.join(path.dirname(packageJsonPath), "bin", sourceBinary)
if (!fs.existsSync(binaryPath)) throw new Error(`Binary not found at ${binaryPath}`)
return binaryPath
}

async function main() {
function installPackage(name) {
const version = packageJson.optionalDependencies?.[name]
if (!version) return

const temp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-install-"))
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)")
return
}
const result = childProcess.spawnSync(
"npm",
["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${version}`],
{ stdio: "inherit", windowsHide: true },
)
if (result.status !== 0) return
const packageDir = path.join(temp, "node_modules", name)
copyBinary(path.join(packageDir, "bin", sourceBinary), targetBinary)
return true
} finally {
fs.rmSync(temp, { recursive: true, force: true })
}
}

// On non-Windows platforms, just verify the binary package exists
// Don't replace the wrapper script - it handles binary execution
const { binaryPath } = findBinary()
const target = path.join(__dirname, "bin", ".opencode")
if (fs.existsSync(target)) fs.unlinkSync(target)
function copyBinary(source, target) {
if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
fs.mkdirSync(path.dirname(target), { recursive: true })
if (fs.existsSync(target)) fs.unlinkSync(target)
try {
fs.linkSync(source, target)
} catch {
fs.copyFileSync(source, target)
}
fs.chmodSync(target, 0o755)
}

function verifyBinary() {
const result = childProcess.spawnSync(targetBinary, ["--version"], {
encoding: "utf8",
stdio: "ignore",
windowsHide: true,
})
return result.status === 0
}

function main() {
for (const name of packageNames()) {
try {
fs.linkSync(binaryPath, target)
copyBinary(resolveBinary(name), targetBinary)
if (verifyBinary()) return
} catch {
fs.copyFileSync(binaryPath, target)
if (installPackage(name) && verifyBinary()) return
}
fs.chmodSync(target, 0o755)
} catch (error) {
console.error("Failed to setup opencode binary:", error.message)
process.exit(1)
}

throw new Error(
`It seems your package manager failed to install the right opencode CLI package. Try manually installing ${packageNames()
.map((name) => JSON.stringify(name))
.join(" or ")}.`,
)
}

try {
void main()
main()
} catch (error) {
console.error("Postinstall script error:", error.message)
process.exit(0)
console.error(error.message)
process.exit(1)
}
16 changes: 13 additions & 3 deletions packages/opencode/script/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,32 @@ console.log("binaries", binaries)
const version = Object.values(binaries)[0]

await $`mkdir -p ./dist/${pkg.name}`
await $`cp -r ./bin ./dist/${pkg.name}/bin`
await $`mkdir -p ./dist/${pkg.name}/bin`
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text())
await Bun.file(`./dist/${pkg.name}/bin/${pkg.name}.exe`).write(
[
"#!/usr/bin/env node",
"console.error('The opencode native binary was not installed. Run `node postinstall.mjs` from the opencode-ai package directory to finish setup.')",
"process.exit(1)",
"",
].join("\n"),
)

await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
{
name: pkg.name + "-ai",
bin: {
[pkg.name]: `./bin/${pkg.name}`,
[pkg.name]: `./bin/${pkg.name}.exe`,
},
scripts: {
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
postinstall: "node ./postinstall.mjs",
},
version: version,
license: pkg.license,
os: ["darwin", "linux", "win32"],
cpu: ["arm64", "x64"],
optionalDependencies: binaries,
},
null,
Expand Down
Loading