mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-24 20:25:36 -04:00
464 lines
14 KiB
TypeScript
Executable File
464 lines
14 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
|
|
import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin"
|
|
import path from "path"
|
|
import fs from "fs"
|
|
import { $ } from "bun"
|
|
import { fileURLToPath } from "url"
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
const dir = path.resolve(__dirname, "..")
|
|
const repoRoot = path.resolve(dir, "..", "..")
|
|
|
|
process.chdir(dir)
|
|
|
|
import pkg from "../package.json"
|
|
import { Script } from "@opencode-ai/script"
|
|
|
|
const personasRoot = path.resolve(repoRoot, "vendor", "personas")
|
|
const zeeRoot = path.join(personasRoot, "zee")
|
|
const stanleyRoot = path.join(personasRoot, "stanley")
|
|
const tiaraRoot = path.resolve(repoRoot, "vendor", "tiara")
|
|
const agentCoreAssetsRoot = path.join(repoRoot, ".agent-core")
|
|
const claudeSkillsRoot = path.join(repoRoot, ".claude", "skills")
|
|
|
|
async function ensureZeeDependencies() {
|
|
const nodeModules = path.join(zeeRoot, "node_modules")
|
|
if (fs.existsSync(nodeModules)) return
|
|
if (!fs.existsSync(path.join(zeeRoot, "package.json"))) return
|
|
console.log("installing zee dependencies for bundling")
|
|
await $`pnpm install --prod --ignore-scripts`.cwd(zeeRoot)
|
|
}
|
|
|
|
function resolveStanleyRuntime(): string | undefined {
|
|
const configured = process.env.STANLEY_PYTHON_RUNTIME
|
|
if (configured && fs.existsSync(configured)) return configured
|
|
const pyenvRoot = process.env.PYENV_ROOT
|
|
const pyenvVersion = process.env.PYENV_VERSION
|
|
if (pyenvRoot && pyenvVersion) {
|
|
const candidate = path.join(pyenvRoot, "versions", pyenvVersion)
|
|
if (fs.existsSync(candidate)) return candidate
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function resolveStanleyPythonBinary(): string {
|
|
const runtimeRoot = resolveStanleyRuntime()
|
|
if (!runtimeRoot) return "python3"
|
|
const candidates = ["python3.12", "python3.13", "python3"]
|
|
for (const candidate of candidates) {
|
|
const python = path.join(runtimeRoot, "bin", candidate)
|
|
if (fs.existsSync(python)) return python
|
|
}
|
|
return "python3"
|
|
}
|
|
|
|
async function getPythonVersion(pythonBin: string): Promise<string | undefined> {
|
|
const version = await $`${pythonBin} -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"`
|
|
.quiet()
|
|
.nothrow()
|
|
.text()
|
|
.then((v) => v.trim())
|
|
return version || undefined
|
|
}
|
|
|
|
async function ensureStanleyRuntime() {
|
|
const runtimeRoot = resolveStanleyRuntime()
|
|
if (!runtimeRoot) return
|
|
const destRoot = path.join(stanleyRoot, ".python-runtime")
|
|
if (fs.existsSync(destRoot)) {
|
|
const srcPython = resolveStanleyPythonBinary()
|
|
const destPythonCandidates = ["python3.12", "python3.13", "python3"].map((candidate) =>
|
|
path.join(destRoot, "bin", candidate),
|
|
)
|
|
const destPython = destPythonCandidates.find((candidate) => fs.existsSync(candidate))
|
|
if (!destPython) {
|
|
fs.rmSync(destRoot, { recursive: true, force: true })
|
|
} else {
|
|
const [srcVersion, destVersion] = await Promise.all([
|
|
getPythonVersion(srcPython),
|
|
getPythonVersion(destPython),
|
|
])
|
|
if (srcVersion && destVersion && srcVersion !== destVersion) {
|
|
fs.rmSync(destRoot, { recursive: true, force: true })
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
console.log("bundling stanley python runtime")
|
|
fs.cpSync(runtimeRoot, destRoot, {
|
|
recursive: true,
|
|
dereference: true,
|
|
})
|
|
}
|
|
|
|
function getStanleyRequirementsFile(): string | undefined {
|
|
const locked = path.join(stanleyRoot, "requirements-lock.txt")
|
|
if (fs.existsSync(locked)) return locked
|
|
const fallback = path.join(stanleyRoot, "requirements.txt")
|
|
if (fs.existsSync(fallback)) return fallback
|
|
return undefined
|
|
}
|
|
|
|
function readStanleyDepsStamp(
|
|
stampPath: string,
|
|
): { requirements: string; mtimeMs: number; depsKey?: string } | null {
|
|
try {
|
|
const raw = fs.readFileSync(stampPath, "utf-8")
|
|
return JSON.parse(raw) as { requirements: string; mtimeMs: number }
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function ensureStanleyDependencies() {
|
|
const requirementsFile = getStanleyRequirementsFile()
|
|
if (!requirementsFile) return
|
|
const pythonBin = resolveStanleyPythonBinary()
|
|
const pythonDepsRoot = path.join(stanleyRoot, ".python")
|
|
const stampPath = path.join(pythonDepsRoot, ".stanley-deps.json")
|
|
const constraintsPath = path.join(pythonDepsRoot, ".stanley-constraints.txt")
|
|
const requirementsStat = fs.statSync(requirementsFile)
|
|
fs.mkdirSync(pythonDepsRoot, { recursive: true })
|
|
const pythonVersion = await $`${pythonBin} -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"`
|
|
.cwd(stanleyRoot)
|
|
.quiet()
|
|
.nothrow()
|
|
.text()
|
|
.then((v) => v.trim())
|
|
const [major, minor] = pythonVersion.split(".").map((v) => Number(v))
|
|
const supportsOpenbb =
|
|
Number.isFinite(major) && Number.isFinite(minor) ? major < 3 || (major === 3 && minor < 14) : true
|
|
const deps = supportsOpenbb
|
|
? ["openbb==4.6.0", "yfinance==0.2.66", "dbnomics==1.2.0", "nautilus_trader==1.222.0"]
|
|
: ["yfinance==0.2.66", "dbnomics==1.2.0", "nautilus_trader==1.222.0"]
|
|
const depsKey = deps.join("|")
|
|
const stamp = readStanleyDepsStamp(stampPath)
|
|
if (stamp && stamp.requirements === requirementsFile && stamp.mtimeMs === requirementsStat.mtimeMs && stamp.depsKey === depsKey) {
|
|
return
|
|
}
|
|
if (
|
|
(
|
|
await $`PYTHONPATH=${pythonDepsRoot} ${pythonBin} -c "import openbb, yfinance, dbnomics, nautilus_trader"`
|
|
.cwd(stanleyRoot)
|
|
.quiet()
|
|
.nothrow()
|
|
).exitCode === 0
|
|
) {
|
|
fs.writeFileSync(
|
|
stampPath,
|
|
JSON.stringify({ requirements: requirementsFile, mtimeMs: requirementsStat.mtimeMs, depsKey }),
|
|
)
|
|
return
|
|
}
|
|
|
|
fs.writeFileSync(constraintsPath, deps.join("\n"))
|
|
console.log("installing minimal stanley dependencies for bundling")
|
|
await $`${pythonBin} -m pip install --only-binary=:all: --target ${pythonDepsRoot} -r ${constraintsPath}`.cwd(
|
|
stanleyRoot,
|
|
)
|
|
fs.writeFileSync(
|
|
stampPath,
|
|
JSON.stringify({ requirements: requirementsFile, mtimeMs: requirementsStat.mtimeMs, depsKey }),
|
|
)
|
|
}
|
|
|
|
function bundlePersonas(distRoot: string) {
|
|
if (!fs.existsSync(personasRoot)) return
|
|
const destRoot = path.join(distRoot, "vendor", "personas")
|
|
fs.mkdirSync(destRoot, { recursive: true })
|
|
for (const entry of fs.readdirSync(personasRoot, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue
|
|
const src = path.join(personasRoot, entry.name)
|
|
const dest = path.join(destRoot, entry.name)
|
|
fs.cpSync(src, dest, {
|
|
recursive: true,
|
|
dereference: true,
|
|
filter: (srcPath) => {
|
|
const base = path.basename(srcPath)
|
|
if (entry.name === "zee") {
|
|
return base !== ".git" && base !== ".venv" && base !== "venv"
|
|
}
|
|
if (entry.name === "stanley") {
|
|
return base !== ".git" && base !== "node_modules" && base !== ".venv" && base !== "venv"
|
|
}
|
|
return base !== ".git" && base !== "node_modules" && base !== ".venv" && base !== "venv"
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
function bundleTiara(distRoot: string) {
|
|
if (!fs.existsSync(tiaraRoot)) return
|
|
const destRoot = path.join(distRoot, "vendor", "tiara")
|
|
fs.mkdirSync(destRoot, { recursive: true })
|
|
fs.cpSync(tiaraRoot, destRoot, {
|
|
recursive: true,
|
|
dereference: true,
|
|
filter: (srcPath) => {
|
|
const base = path.basename(srcPath)
|
|
return base !== ".git" && base !== "node_modules" && base !== ".venv" && base !== "venv"
|
|
},
|
|
})
|
|
}
|
|
|
|
function bundleAgentCoreAssets(distRoot: string) {
|
|
if (!fs.existsSync(agentCoreAssetsRoot)) return
|
|
const destRoot = path.join(distRoot, ".agent-core")
|
|
fs.mkdirSync(destRoot, { recursive: true })
|
|
const entries = ["agent", "command", "themes", "skill"]
|
|
for (const entry of entries) {
|
|
const src = path.join(agentCoreAssetsRoot, entry)
|
|
if (!fs.existsSync(src)) continue
|
|
const dest = path.join(destRoot, entry)
|
|
fs.cpSync(src, dest, {
|
|
recursive: true,
|
|
dereference: true,
|
|
filter: (srcPath) => {
|
|
const base = path.basename(srcPath)
|
|
return base !== ".git" && base !== "node_modules" && base !== ".venv" && base !== "venv"
|
|
},
|
|
})
|
|
}
|
|
|
|
// Bundle a safe version of the config with default_agent set
|
|
const configSrc = path.join(agentCoreAssetsRoot, "agent-core.jsonc")
|
|
if (fs.existsSync(configSrc)) {
|
|
const configDest = path.join(destRoot, "agent-core.jsonc")
|
|
const raw = fs.readFileSync(configSrc, "utf-8")
|
|
// Strip local-only MCP paths that won't work in dist
|
|
const safeConfig = raw.replace(/"command":\s*\[.*?\]/g, '"command": []')
|
|
fs.writeFileSync(configDest, safeConfig)
|
|
}
|
|
}
|
|
|
|
function bundlePersonaSkills(distRoot: string) {
|
|
if (!fs.existsSync(claudeSkillsRoot)) return
|
|
const destRoot = path.join(distRoot, ".agent-core", "skill")
|
|
fs.mkdirSync(destRoot, { recursive: true })
|
|
const skills = ["zee", "stanley", "johny", "personas"]
|
|
for (const skill of skills) {
|
|
const src = path.join(claudeSkillsRoot, skill)
|
|
if (!fs.existsSync(src)) continue
|
|
const dest = path.join(destRoot, skill)
|
|
fs.cpSync(src, dest, {
|
|
recursive: true,
|
|
dereference: true,
|
|
filter: (srcPath) => {
|
|
const base = path.basename(srcPath)
|
|
return base !== ".git" && base !== "node_modules" && base !== ".venv" && base !== "venv"
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
const singleFlag = process.argv.includes("--single")
|
|
const baselineFlag = process.argv.includes("--baseline")
|
|
const skipInstall = process.argv.includes("--skip-install")
|
|
const binarySuffix = process.env.OPENCODE_BINARY_SUFFIX?.trim()
|
|
const targetsArg =
|
|
process.env.AGENT_CORE_TARGETS ??
|
|
(() => {
|
|
const idx = process.argv.indexOf("--targets")
|
|
if (idx === -1) return undefined
|
|
return process.argv[idx + 1]
|
|
})()
|
|
|
|
const allTargets: {
|
|
os: string
|
|
arch: "arm64" | "x64"
|
|
abi?: "musl"
|
|
avx2?: false
|
|
}[] = [
|
|
{
|
|
os: "linux",
|
|
arch: "arm64",
|
|
},
|
|
{
|
|
os: "linux",
|
|
arch: "x64",
|
|
},
|
|
{
|
|
os: "linux",
|
|
arch: "x64",
|
|
avx2: false,
|
|
},
|
|
{
|
|
os: "linux",
|
|
arch: "arm64",
|
|
abi: "musl",
|
|
},
|
|
{
|
|
os: "linux",
|
|
arch: "x64",
|
|
abi: "musl",
|
|
},
|
|
{
|
|
os: "linux",
|
|
arch: "x64",
|
|
abi: "musl",
|
|
avx2: false,
|
|
},
|
|
{
|
|
os: "darwin",
|
|
arch: "arm64",
|
|
},
|
|
{
|
|
os: "darwin",
|
|
arch: "x64",
|
|
},
|
|
{
|
|
os: "darwin",
|
|
arch: "x64",
|
|
avx2: false,
|
|
},
|
|
{
|
|
os: "win32",
|
|
arch: "x64",
|
|
},
|
|
{
|
|
os: "win32",
|
|
arch: "x64",
|
|
avx2: false,
|
|
},
|
|
]
|
|
|
|
const targetsFilter = (() => {
|
|
if (!targetsArg) return undefined
|
|
const requested = targetsArg
|
|
.split(",")
|
|
.map((item) => item.trim())
|
|
.filter(Boolean)
|
|
.map((item) => item.split("-"))
|
|
return (item: (typeof allTargets)[number]) => {
|
|
return requested.some(([os, arch, variant]) => {
|
|
if (os && item.os !== os) return false
|
|
if (arch && item.arch !== arch) return false
|
|
if (variant === "baseline") return item.avx2 === false
|
|
if (variant === "musl") return item.abi === "musl"
|
|
return item.avx2 !== false && item.abi === undefined
|
|
})
|
|
}
|
|
})()
|
|
|
|
const targets = targetsFilter
|
|
? allTargets.filter(targetsFilter)
|
|
: 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.filter((item) => {
|
|
if (item.os === "linux" && item.arch === "x64" && item.abi === undefined && item.avx2 !== false) {
|
|
return true
|
|
}
|
|
if (item.os === "darwin" && item.arch === "arm64" && item.abi === undefined && item.avx2 !== false) {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
|
|
await $`rm -rf dist`
|
|
|
|
const binaries: Record<string, string> = {}
|
|
if (!skipInstall) {
|
|
await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
|
await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
|
|
}
|
|
if (fs.existsSync(zeeRoot)) {
|
|
await ensureZeeDependencies()
|
|
}
|
|
if (fs.existsSync(stanleyRoot)) {
|
|
await ensureStanleyDependencies()
|
|
await ensureStanleyRuntime()
|
|
}
|
|
|
|
for (const item of targets) {
|
|
const baseName = [
|
|
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("-")
|
|
const name = [baseName, binarySuffix].filter(Boolean).join("-")
|
|
console.log(`building ${name}`)
|
|
await $`mkdir -p dist/${name}/bin`
|
|
|
|
const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/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: baseName.replace(pkg.name, "bun") as any,
|
|
outfile: `dist/${name}/bin/agent-core`,
|
|
execArgv: [`--user-agent=agent-core/${Script.version}`, "--use-system-ca", "--"],
|
|
windows: {},
|
|
},
|
|
entrypoints: ["./src/index.ts", parserWorker, workerPath],
|
|
define: {
|
|
AGENT_CORE_VERSION: `'${Script.version}'`,
|
|
AGENT_CORE_CHANNEL: `'${Script.channel}'`,
|
|
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 $`rm -rf ./dist/${name}/bin/tui`
|
|
await Bun.file(`dist/${name}/package.json`).write(
|
|
JSON.stringify(
|
|
{
|
|
name,
|
|
version: Script.version,
|
|
os: [item.os],
|
|
cpu: [item.arch],
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
)
|
|
// Bundle personas so standalone installs can resolve them via AGENT_CORE_ROOT.
|
|
bundlePersonas(path.join(dir, "dist", name))
|
|
bundleTiara(path.join(dir, "dist", name))
|
|
bundleAgentCoreAssets(path.join(dir, "dist", name))
|
|
bundlePersonaSkills(path.join(dir, "dist", name))
|
|
binaries[name] = Script.version
|
|
}
|
|
|
|
export { binaries }
|