#!/usr/bin/env bun import solidPlugin from "./solid-plugin" import path from "path" import { $ } from "bun" import { fileURLToPath } from "url" import { createRequire } from "module" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const require = createRequire(import.meta.url) const dir = path.resolve(__dirname, "..") process.chdir(dir) import pkg from "../package.json" import { Script } from "@opencode-ai/script" import fs from "fs/promises" import nodefs from "fs" await $`bun run build:webgui` const webGuiDir = path.join(dir, "webgui-dist") const embedOutput = path.join(dir, "src/webgui/embed.generated.ts") async function listWebGuiFiles(current: string) { const entries = await fs.readdir(current, { withFileTypes: true }) const files: { path: string; data: string }[] = [] for (const entry of entries) { const absolute = path.join(current, entry.name) if (entry.isDirectory()) { const nested = await listWebGuiFiles(absolute) files.push(...nested) continue } const relative = path.relative(webGuiDir, absolute).split(path.sep).join("/") const file = Bun.file(absolute) const buffer = Buffer.from(await file.arrayBuffer()) files.push({ path: relative, data: buffer.toString("base64") }) } return files } async function generateEmbeddedWebGui() { const indexFile = Bun.file(path.join(webGuiDir, "index.html")) if (!(await indexFile.exists())) { await Bun.write(embedOutput, "export const embeddedWebGui = [] as const\n") return } const items = await listWebGuiFiles(webGuiDir) const lines = [ "export const embeddedWebGui = [", ...items.map((item) => ` { path: ${JSON.stringify(item.path)}, data: ${JSON.stringify(item.data)} },`), "] as const", "", ] await Bun.write(embedOutput, lines.join("\n")) } await generateEmbeddedWebGui() const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev" // Fetch and generate models.dev snapshot const modelsData = process.env.MODELS_DEV_API_JSON ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() : await fetch(`${modelsUrl}/api.json`).then((x) => x.text()) await Bun.write( path.join(dir, "src/provider/models-snapshot.ts"), `// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData} as const\n`, ) console.log("Generated models-snapshot.ts") const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") const allTargets: { os: string arch: "arm64" | "x64" abi?: "musl" avx2?: false }[] = [ { os: "linux", arch: "arm64", }, { os: "linux", arch: "x64", }, { os: "darwin", arch: "arm64", }, { os: "darwin", arch: "x64", }, { os: "win32", arch: "x64", }, ] const targets = singleFlag ? allTargets.filter((item) => { if (item.os !== process.platform || item.arch !== process.arch) { return false } // When building for the current platform, prefer a single native binary by default. // Baseline binaries require additional Bun artifacts and can be flaky to download. if (item.avx2 === false) { return baselineFlag } // also skip abi-specific builds for the same reason if (item.abi !== undefined) { return false } return true }) : allTargets await fs.rm("dist", { recursive: true, force: true }) const binaries: Record = {} if (!skipInstall) { await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` } for (const item of targets) { const name = [ pkg.name, // 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, ] .filter(Boolean) .join("-") console.log(`building ${name}`) await fs.mkdir(`dist/${name}/bin`, { recursive: true }) const opentuiCoreEntry = require.resolve("@opentui/core") const parserWorker = nodefs.realpathSync(path.join(path.dirname(opentuiCoreEntry), "parser.worker.js")) const workerPath = "./src/cli/cmd/tui/worker.ts" // Use platform-specific bunfs root path based on target OS const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/" const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/") await Bun.build({ conditions: ["browser"], tsconfig: "./tsconfig.json", plugins: [solidPlugin], sourcemap: "external", compile: { autoloadBunfig: false, autoloadDotenv: false, //@ts-ignore (bun types aren't up to date) autoloadTsconfig: true, autoloadPackageJson: true, target: name.replace(pkg.name, "bun") as any, outfile: `dist/${name}/bin/opencode`, execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], windows: {}, }, entrypoints: ["./src/index.ts", parserWorker, workerPath], define: { OPENCODE_VERSION: `'${Script.version}'`, OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath, OPENCODE_WORKER_PATH: workerPath, OPENCODE_CHANNEL: `'${Script.channel}'`, OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", }, }) await fs.rm(`./dist/${name}/bin/tui`, { recursive: true, force: true }) await Bun.file(`dist/${name}/package.json`).write( JSON.stringify( { name, version: Script.version, os: [item.os], cpu: [item.arch], }, null, 2, ), ) binaries[name] = Script.version } if (Script.release) { for (const key of Object.keys(binaries)) { if (key.includes("linux")) { await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`) } else { await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`) } } await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber` } export { binaries }