node v2 cli support (#36309)
This commit is contained in:
parent
27e1692848
commit
a1b274e6f8
75 changed files with 1502 additions and 367 deletions
201
packages/cli/script/build-node.ts
Normal file
201
packages/cli/script/build-node.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { createHash } from "node:crypto"
|
||||
import { chmod, copyFile, mkdir, mkdtemp, 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-ai/script"
|
||||
import pkg from "../package.json"
|
||||
import { modelsData } from "./generate"
|
||||
import { collectNodeAssets, copyNodeAssets, hashNodeAssets, seaAssetMap } from "./node-assets"
|
||||
import { mainConfig } from "../vite.node.config"
|
||||
import { nodeExecArgv, nodeTarget, type NodeTarget } from "../src/node/target"
|
||||
|
||||
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 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
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
process.chdir(dir)
|
||||
if (!skipInstall) run(process.execPath, ["install", "--os=*", "--cpu=*"])
|
||||
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
|
||||
|
||||
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, models: modelsData, assetHash, target }
|
||||
await build(mainConfig(input))
|
||||
await copyNodeAssets(assets)
|
||||
|
||||
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-ai/${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`,
|
||||
)
|
||||
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])
|
||||
else if (process.platform === "win32") run("tar", ["-xf", archive, "-C", temporary])
|
||||
else 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"}`)
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import fs from "fs"
|
||||
import { rm } from "fs/promises"
|
||||
import path from "path"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
|
|
@ -11,9 +10,14 @@ import { modelsData } from "./generate"
|
|||
|
||||
const dir = path.resolve(import.meta.dirname, "..")
|
||||
const binary = "opencode2"
|
||||
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")
|
||||
process.chdir(dir)
|
||||
|
||||
await rm("dist", { recursive: true, force: true })
|
||||
await rm(outdir, { recursive: true, force: true })
|
||||
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const baselineFlag = process.argv.includes("--baseline")
|
||||
|
|
@ -50,10 +54,6 @@ const targets = singleFlag
|
|||
|
||||
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
||||
|
||||
const localParserWorker = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
|
||||
const rootParserWorker = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
||||
const parserWorker = fs.realpathSync(fs.existsSync(localParserWorker) ? localParserWorker : rootParserWorker)
|
||||
|
||||
for (const item of targets) {
|
||||
const target = [
|
||||
binary,
|
||||
|
|
@ -67,7 +67,7 @@ for (const item of targets) {
|
|||
const name = target.replace(binary, "cli")
|
||||
console.log(`building ${name}`)
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts", parserWorker],
|
||||
entrypoints: ["./src/index.ts"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [plugin],
|
||||
external: ["node-gyp"],
|
||||
|
|
@ -81,7 +81,7 @@ for (const item of targets) {
|
|||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
|
||||
outfile: `./dist/${name}/bin/${binary}`,
|
||||
outfile: path.join(outdir, name, "bin", binary),
|
||||
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
|
||||
windows: {},
|
||||
},
|
||||
|
|
@ -93,10 +93,6 @@ for (const item of targets) {
|
|||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
|
||||
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
|
||||
FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
|
||||
OTUI_TREE_SITTER_WORKER_PATH:
|
||||
(item.os === "win32" ? '"B:/~BUN/root/' : '"/$bunfs/root/') +
|
||||
path.relative(dir, parserWorker).replaceAll("\\", "/") +
|
||||
'"',
|
||||
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
|
||||
},
|
||||
})
|
||||
|
|
@ -107,7 +103,7 @@ for (const item of targets) {
|
|||
}
|
||||
|
||||
await Bun.write(
|
||||
`./dist/${name}/package.json`,
|
||||
path.join(outdir, name, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: `@opencode-ai/${name}`,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { readFile } from "node:fs/promises"
|
||||
|
||||
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
|
||||
export const modelsData = process.env.MODELS_DEV_API_JSON
|
||||
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
|
||||
? await readFile(process.env.MODELS_DEV_API_JSON, "utf8")
|
||||
: await fetch(`${modelsUrl}/api.json`).then((response) => response.text())
|
||||
|
||||
console.log("Loaded models.dev snapshot")
|
||||
|
|
|
|||
83
packages/cli/script/node-assets.ts
Normal file
83
packages/cli/script/node-assets.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { createHash } from "node:crypto"
|
||||
import { copyFile, mkdir, readdir, readFile, stat } from "node:fs/promises"
|
||||
import { createRequire } from "node:module"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { getNodeAssets } from "@opentui/core/node-assets"
|
||||
import { attentionSoundAssets, type NodeTarget, photonWasmAsset } from "../src/node/target"
|
||||
|
||||
const dir = path.resolve(import.meta.dirname, "..")
|
||||
|
||||
// Bun's compiler discovers file imports and embeds them in its virtual filesystem. Vite only bundles the JavaScript
|
||||
// portion of the Node executable, while SEA embeds only the assets explicitly listed in its build configuration.
|
||||
// Collect and stage those files under stable keys so the SEA prelude can extract them to real paths at startup;
|
||||
// native addons, helper executables, and other path-based consumers cannot use assets directly from SEA memory.
|
||||
export type NodeAsset = {
|
||||
readonly key: string
|
||||
readonly source: string
|
||||
}
|
||||
|
||||
async function files(root: string, current = root): Promise<string[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
(await readdir(current, { withFileTypes: true })).map((entry) => {
|
||||
const target = path.join(current, entry.name)
|
||||
return entry.isDirectory() ? files(root, target) : [path.relative(root, target)]
|
||||
}),
|
||||
)
|
||||
).flat()
|
||||
}
|
||||
|
||||
export async function collectNodeAssets(target: NodeTarget) {
|
||||
const ptyEntry = fileURLToPath(import.meta.resolve(target.nodePtyPackage))
|
||||
const ptyRoot = path.resolve(path.dirname(ptyEntry), "..")
|
||||
const assets: NodeAsset[] = [
|
||||
...getNodeAssets({
|
||||
platform: target.platform,
|
||||
arch: target.arch,
|
||||
...(target.platform === "linux" ? { libc: "glibc" as const } : {}),
|
||||
}),
|
||||
{ key: target.parcelWatcherAsset, source: fileURLToPath(import.meta.resolve(target.parcelWatcherPackage)) },
|
||||
{
|
||||
key: photonWasmAsset,
|
||||
source: createRequire(path.resolve(dir, "../core/package.json")).resolve(photonWasmAsset),
|
||||
},
|
||||
...attentionSoundAssets.map((key) => ({
|
||||
key,
|
||||
source: path.resolve(dir, "../ui/src/assets/audio", path.basename(key)),
|
||||
})),
|
||||
...(await files(ptyRoot))
|
||||
.filter((relative) => !relative.endsWith(".map") && !relative.endsWith(".pdb"))
|
||||
.map((relative) => ({
|
||||
key: `${target.nodePtyPackage}/${relative}`,
|
||||
source: path.join(ptyRoot, relative),
|
||||
})),
|
||||
]
|
||||
await Promise.all(assets.map((asset) => stat(asset.source)))
|
||||
return assets
|
||||
}
|
||||
|
||||
export async function hashNodeAssets(assets: readonly NodeAsset[]) {
|
||||
const hash = createHash("sha256")
|
||||
for (const asset of assets.toSorted((left, right) => left.key.localeCompare(right.key))) {
|
||||
hash.update(asset.key)
|
||||
hash.update(await readFile(asset.source))
|
||||
}
|
||||
return hash.digest("hex").slice(0, 16)
|
||||
}
|
||||
|
||||
export async function copyNodeAssets(assets: readonly NodeAsset[]) {
|
||||
const root = path.join(dir, "dist-node", "assets")
|
||||
await Promise.all(
|
||||
assets.map(async (asset) => {
|
||||
const target = path.join(root, asset.key)
|
||||
await mkdir(path.dirname(target), { recursive: true })
|
||||
await copyFile(asset.source, target)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function seaAssetMap() {
|
||||
const root = path.join(dir, "dist-node", "assets")
|
||||
return Object.fromEntries((await files(root)).map((key) => [key.replaceAll(path.sep, "/"), path.join(root, key)]))
|
||||
}
|
||||
|
|
@ -18,37 +18,55 @@ async function publish(dir: string, name: string, version: string) {
|
|||
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
|
||||
}
|
||||
|
||||
const binaries: Record<string, string> = {}
|
||||
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
|
||||
const item = await Bun.file(`./dist/${filepath}`).json()
|
||||
binaries[item.name] = item.version
|
||||
async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) {
|
||||
const binaries: Record<string, string> = {}
|
||||
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) {
|
||||
const item = await Bun.file(`${input.root}/${filepath}`).json()
|
||||
if (!item.name.startsWith(input.packagePrefix)) continue
|
||||
binaries[item.name] = item.version
|
||||
}
|
||||
console.log(input.name, "binaries", binaries)
|
||||
const versions = new Set(Object.values(binaries))
|
||||
if (versions.size > 1) throw new Error(`Binary package versions do not match for ${input.name}`)
|
||||
const version = versions.values().next().value
|
||||
if (!version) throw new Error(`No binary packages found for ${input.name}`)
|
||||
|
||||
await $`mkdir -p ${input.root}/${input.name}/bin`
|
||||
await $`cp ./bin/opencode2.cjs ${input.root}/${input.name}/bin/${input.binary}`
|
||||
await Bun.file(`${input.root}/${input.name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: input.name,
|
||||
bin: { [input.binary]: `./bin/${input.binary}` },
|
||||
version,
|
||||
license: pkg.license,
|
||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||
os: ["darwin", "linux", "win32"],
|
||||
cpu: ["arm64", "x64"],
|
||||
optionalDependencies: binaries,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(binaries).map(([name, version]) =>
|
||||
publish(`${input.root}/${name.replace("@opencode-ai/", "")}`, name, version),
|
||||
),
|
||||
)
|
||||
await publish(`${input.root}/${input.name}`, input.name, version)
|
||||
}
|
||||
console.log("binaries", binaries)
|
||||
const version = Object.values(binaries)[0]
|
||||
const name = pkg.name
|
||||
|
||||
await $`mkdir -p ./dist/${name}/bin`
|
||||
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2`
|
||||
await Bun.file(`./dist/${name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name,
|
||||
bin: { opencode2: "./bin/opencode2" },
|
||||
version,
|
||||
license: pkg.license,
|
||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||
os: ["darwin", "linux", "win32"],
|
||||
cpu: ["arm64", "x64"],
|
||||
optionalDependencies: binaries,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(binaries).map(([name, version]) =>
|
||||
publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
|
||||
),
|
||||
)
|
||||
await publish(`./dist/${name}`, name, version)
|
||||
await publishDistribution({
|
||||
root: "./dist",
|
||||
name: pkg.name,
|
||||
binary: "opencode2",
|
||||
packagePrefix: "@opencode-ai/cli-",
|
||||
})
|
||||
await publishDistribution({
|
||||
root: "./dist/node",
|
||||
name: "opencode2-node",
|
||||
binary: "opencode2-node",
|
||||
packagePrefix: "@opencode-ai/cli-node-",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import fs from "node:fs/promises"
|
|||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
const target = `cli-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
||||
const directory = path.join(import.meta.dir, "..", "dist", target, "bin")
|
||||
const binary = path.join(directory, `opencode2${process.platform === "win32" ? ".exe" : ""}`)
|
||||
const nodeBuild = process.argv.includes("--node")
|
||||
const target = `cli${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
||||
const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin")
|
||||
const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`)
|
||||
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
|
||||
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue