Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242#!/usr/bin/env bun
import { spawnSync } from "node:child_process"import { createHash } from "node:crypto"import { chmod, copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises"import os from "node:os"import path from "node:path"import { build } from "vite"import { Script } from "@opencode/script"import pkg from "../package.json"import { collectNodeAssets, copyNodeAssets, hashNodeAssets, seaAssetMap } from "./node-assets"import { mainConfig } from "../vite.node.config"import { nodeExecArgv, nodeTarget, type NodeTarget } from "../src/node/target"import { buildAppArchive } from "./app-assets"import { verifyArtifact } from "./verify-artifact"
const NODE_VERSION = "26.4.0"const dir = path.resolve(import.meta.dirname, "..")const outdir = path.resolve( dir, process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",)if (outdir === dir) throw new Error("--outdir must not be the package directory")if (outdir === path.join(dir, "dist-node")) { throw new Error("--outdir must not be dist-node because it contains temporary files")}const bundleOnly = process.argv.includes("--bundle-only")const single = process.argv.includes("--single")const skipInstall = process.argv.includes("--skip-install")const appArchiveOnly = process.argv.includes("--app-archive-only")const requestedArchive = process.argv.find((arg) => arg.startsWith("--app-archive="))?.slice("--app-archive=".length)const archivePath = requestedArchive ? path.resolve(dir, requestedArchive) : undefinedconst requested = process.argv.find((arg) => arg.startsWith("--target="))?.slice("--target=".length)const allTargets = [ nodeTarget("linux", "arm64"), nodeTarget("linux", "x64"), nodeTarget("darwin", "arm64"), nodeTarget("win32", "arm64"), nodeTarget("win32", "x64"),]const targets = requested ? allTargets.filter((target) => targetName(target) === requested) : single || bundleOnly ? [nodeTarget(process.platform, process.arch)] : allTargets
process.chdir(dir)if (!skipInstall) run(process.execPath, ["install", "--os=*", "--cpu=*"])if (appArchiveOnly) { if (!archivePath) throw new Error("--app-archive-only requires --app-archive=<path>") await mkdir(path.dirname(archivePath), { recursive: true }) await writeFile(archivePath, await buildAppArchive(Script.channel)) process.exit(0)}if (targets.length === 0) { if (requested === "darwin-x64") throw new Error("Node 26.4 SEA does not support macOS x64") throw new Error(`Unknown Node target: ${requested}`)}if (!bundleOnly && targets.some((target) => target.platform === "darwin" && target.arch === "x64")) { throw new Error("Node 26.4 SEA does not support macOS x64")}const appArchive = archivePath ? (await Bun.file(archivePath).text()).trim() : await buildAppArchive(Script.channel)if (!bundleOnly) await rm(outdir, { recursive: true, force: true })const builder = !bundleOnly || targets.some((target) => target.platform === process.platform && target.arch === process.arch) ? await resolveHostNode() : undefined
// Vite silently rewrites text imports of known asset types (.txt) to asset// URL strings when the raw-text plugin doesn't intercept them first — the// bundle still builds and `--help` still runs, so only content assertions// catch it. Guards the models.dev snapshot and the prompt/tool description// text that ships inside the bundle.async function assertTextImportsInlined(bundlePath: string) { const bundle = await readFile(bundlePath, "utf8") const markers = [ { marker: '"zhipuai"', source: "models-dev snapshot" }, { marker: "/assets/snapshot", source: "models-dev snapshot inlined as asset URL", forbidden: true }, { marker: '="/assets/', source: "text import inlined as asset URL", forbidden: true }, ] for (const { marker, source, forbidden } of markers) { const present = bundle.includes(marker) if (forbidden ? present : !present) throw new Error(`${bundlePath}: ${source} — text imports are not inlined as content (marker ${marker})`) }}
for (const target of targets) { console.log(`building cli-node-${targetName(target)}`) const assets = await collectNodeAssets(target) await rm("dist-node", { recursive: true, force: true }) const assetHash = await hashNodeAssets(assets) const input = { version: Script.version, channel: Script.channel, assetHash, target, appArchive, } await copyNodeAssets(assets) await build(mainConfig(input)) await assertTextImportsInlined("dist-node/opencode.mjs") if (bundleOnly) await verifyArtifact("dist-node/opencode.mjs")
const host = target.platform === process.platform && target.arch === process.arch if (host) { if (!builder) throw new Error("Node SEA builder is unavailable") run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--version"]) run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--help"]) } if (bundleOnly) continue
const name = `cli-node-${targetName(target)}` const binary = target.platform === "win32" ? "opencode2-node.exe" : "opencode2-node" const output = path.join(outdir, name, "bin", binary) if (!builder) throw new Error("Node SEA builder is unavailable") await mkdir(path.dirname(output), { recursive: true }) const config = { main: "dist-node/opencode.mjs", mainFormat: "module", executable: await resolveTargetNode(target, builder), output: path.relative(dir, output), disableExperimentalSEAWarning: true, useSnapshot: false, useCodeCache: false, execArgv: nodeExecArgv, execArgvExtension: "none", assets: await seaAssetMap(), } await writeFile("dist-node/sea.json", `${JSON.stringify(config, null, 2)}\n`) run(builder, ["--build-sea", "dist-node/sea.json"]) if (target.platform !== "win32") await chmod(output, 0o755) if (target.platform === "darwin" && process.platform === "darwin") run("codesign", ["--sign", "-", output]) if (target.platform === "darwin" && process.platform !== "darwin") { console.warn(`${output} must be signed on macOS before it can run`) } await writeFile( path.join(outdir, name, "package.json"), `${JSON.stringify( { name: `@opencode/${name}`, version: Script.version, license: pkg.license, repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" }, os: [target.platform], cpu: [target.arch], }, null, 2, )}\n`, ) await verifyArtifact(path.join(outdir, name)) if (host) await smoke(output)}
async function resolveHostNode() { const candidates = [process.env.NODE_BIN, "node"].filter((item): item is string => Boolean(item)) for (const candidate of candidates) { const result = spawnSync( candidate, ["-p", "JSON.stringify({version:process.versions.node,path:process.execPath})"], { encoding: "utf8", }, ) if (result.status !== 0) continue const info = JSON.parse(result.stdout) as { version: string; path: string } if (info.version === NODE_VERSION) return realpath(info.path) } return resolveTargetNode(nodeTarget(process.platform, process.arch))}
async function resolveTargetNode(target: NodeTarget, host?: string) { if (host && target.platform === process.platform && target.arch === process.arch) return host const cache = path.resolve(dir, ".cache", "node") const platform = target.platform === "win32" ? "win" : target.platform const archiveName = `node-v${NODE_VERSION}-${platform}-${target.arch}` const targetDirectory = path.join(cache, archiveName) const executable = path.join(targetDirectory, target.platform === "win32" ? "node.exe" : "bin/node") if ( (await stat(executable).then( () => true, () => false, )) && (await stat(path.join(targetDirectory, ".verified")).then( () => true, () => false, )) ) return realpath(executable) await mkdir(cache, { recursive: true }) const extension = target.platform === "win32" ? "zip" : "tar.gz" const filename = `${archiveName}.${extension}` const archive = path.join(cache, filename) const base = `https://nodejs.org/dist/v${NODE_VERSION}` const [response, sums] = await Promise.all([fetch(`${base}/${filename}`), fetch(`${base}/SHASUMS256.txt`)]) if (!response.ok) throw new Error(`Failed to download Node ${NODE_VERSION}: ${response.status}`) if (!sums.ok) throw new Error(`Failed to download Node ${NODE_VERSION} checksums: ${sums.status}`) const data = new Uint8Array(await response.arrayBuffer()) const expected = (await sums.text()) .split("\n") .find((line) => line.endsWith(` ${filename}`)) ?.split(/\s+/)[0] if (!expected) throw new Error(`Missing checksum for ${filename}`) if (createHash("sha256").update(data).digest("hex") !== expected) throw new Error(`Checksum mismatch for ${filename}`) await writeFile(archive, data) const temporary = path.join(cache, `${archiveName}.${process.pid}.tmp`) await rm(temporary, { recursive: true, force: true }) await mkdir(temporary) if (target.platform !== "win32") run("tar", ["-xzf", archive, "-C", temporary]) if (target.platform === "win32" && process.platform === "win32") { run(path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe"), ["-xf", archive, "-C", temporary]) } if (target.platform === "win32" && process.platform !== "win32") run("unzip", ["-q", archive, "-d", temporary]) await rm(targetDirectory, { recursive: true, force: true }) await rename(path.join(temporary, archiveName), targetDirectory) await writeFile(path.join(targetDirectory, ".verified"), `${expected}\n`) await rm(temporary, { recursive: true, force: true }) await rm(archive, { force: true }) return realpath(executable)}
async function smoke(output: string) { const root = await mkdtemp(path.join(os.tmpdir(), "opencode-node-smoke-")) const executable = path.join(root, path.basename(output)) await copyFile(output, executable) if (process.platform !== "win32") await chmod(executable, 0o755) run(executable, ["--version"], root) run(executable, ["--help"], root) await rm(root, { recursive: true, force: true })}
function targetName(target: NodeTarget) { return `${target.platform === "win32" ? "windows" : target.platform}-${target.arch}`}
function run(command: string, args: readonly string[], cwd = dir) { const result = spawnSync(command, args, { cwd, stdio: "inherit", env: process.env }) if (result.error) throw result.error if (result.status !== 0) throw new Error(`${command} exited with status ${result.status ?? "unknown"}`)}