diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a5b2f8530..4fae0da245 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -146,6 +146,49 @@ jobs: run: | ./packages/opencode/script/build.ts + - name: Build serve-only variant and upload to release + # Additive: produces `bcode-linux-{arm64,x64}-serve.tar.gz` alongside the + # standard assets, for headless containers. Built from its own package so + # `packages/opencode` — forked from upstream — stays untouched, and the + # canonical build step above is unmodified. + # + # Runs after that step and is `continue-on-error` on purpose: this variant + # must never block or alter a normal release. Different asset names, so + # `--clobber` cannot overwrite the standard archives. + # + # Linux only — the only consumer is the container image. glibc + musl, + # matching what install-bytecode.sh can select; no baseline (non-AVX2 + # x64), which the installer rejects with a pointer to /install. + # + # Invoked through `bun` rather than as `./...` so a lost executable bit + # cannot silently disable the whole step under continue-on-error. + continue-on-error: true + env: + OPENCODE_VERSION: ${{ steps.ver.outputs.version }} + OPENCODE_RELEASE: "1" + OPENCODE_CHANNEL: latest + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.ver.outputs.tag }} + BCODE_DEFAULT_LMNR_KEY: ${{ secrets.LMNR_PROJECT_API_KEY_OSS }} + run: | + # Checkout has no `ref:`, so on `workflow_dispatch` the tree is the + # dispatch ref (usually main), not the selected tag — while the upload + # still targets that tag. Publishing then would put main's code inside + # the tag's assets. Build only when the tree really is the tag. + # + # The canonical build step above has the same exposure; fixing that + # means changing `Checkout` for the whole job, which is out of scope + # here. Tracked separately. + TAG_SHA=$(git rev-parse -q --verify "refs/tags/${TAG}^{commit}" || true) + HEAD_SHA=$(git rev-parse HEAD) + if [ "$HEAD_SHA" != "$TAG_SHA" ]; then + echo "::warning::Skipped the serve variant: checkout $(git rev-parse --short HEAD) is not tag ${TAG}. Re-run from a state where they match to publish it." + exit 0 + fi + bun ./packages/bcode-serve/script/build.ts \ + --targets linux-arm64,linux-x64,linux-arm64-musl,linux-x64-musl + - name: Summarise uploaded assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/bun.lock b/bun.lock index a90b74132e..836eb116c7 100644 --- a/bun.lock +++ b/bun.lock @@ -123,6 +123,22 @@ "@types/bun": "catalog:", }, }, + "packages/bcode-serve": { + "name": "@browser-use/bcode-serve", + "version": "0.0.0", + "dependencies": { + "@browser-use/bcode-browser": "workspace:*", + "@browser-use/browsercode-core": "workspace:*", + "@opencode-ai/core": "workspace:*", + "@opencode-ai/script": "workspace:*", + "yargs": "18.0.0", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/yargs": "17.0.33", + }, + }, "packages/cli": { "name": "@opencode-ai/cli", "version": "1.18.4", @@ -1483,6 +1499,8 @@ "@browser-use/bcode-laminar": ["@browser-use/bcode-laminar@workspace:packages/bcode-laminar"], + "@browser-use/bcode-serve": ["@browser-use/bcode-serve@workspace:packages/bcode-serve"], + "@browser-use/browsercode-core": ["@browser-use/browsercode-core@workspace:packages/opencode"], "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="], diff --git a/install-bytecode.sh b/install-bytecode.sh new file mode 100755 index 0000000000..17611ad703 --- /dev/null +++ b/install-bytecode.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# +# BrowserCode installer — bytecode / serve-only build. +# +# Hosted at https://bcode.sh/bytecode, alongside (not replacing) +# https://bcode.sh/install: +# +# curl -fsSL https://bcode.sh/bytecode | bash -s -- --no-modify-path --version 0.0.3 +# +# Installs the `bcode-linux--serve` asset, which provides ONLY +# `bcode serve` — `run`, `tui`, `web`, `github` and the rest are absent and +# exit 1. It exists for headless containers; anyone installing bcode on a +# laptop wants https://bcode.sh/install. +# +# `install.sh` is the published path docs point at and stays untouched. This is +# additive: a second script, a second URL, a second release asset. +# +# Linux only. Deliberately much shorter than install.sh: one build per arch (no +# baseline/AVX2 detection), no shell-rc editing (containers set PATH in the +# Dockerfile), no uv hint. Takes install.sh's flags so switching is a one-word +# change to the URL in a Dockerfile. +set -euo pipefail + +APP=bcode +VARIANT=serve +REPO=browser-use/browsercode + +MUTED='\033[0;2m' +RED='\033[0;31m' +ORANGE='\033[38;5;214m' +NC='\033[0m' + +usage() { + cat < Install a specific version (e.g. 0.0.3) + --install-dir Install to (default: \$HOME/.bcode/bin) + --no-modify-path Accepted for parity with install.sh; this script + never edits shell config files. + +Examples: + curl -fsSL https://bcode.sh/bytecode | bash + curl -fsSL https://bcode.sh/bytecode | bash -s -- --no-modify-path --version 0.0.3 +EOF +} + +requested_version=${VERSION:-} +install_dir=${BCODE_INSTALL_DIR:-$HOME/.bcode/bin} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + -v|--version) + if [[ -n "${2:-}" ]]; then + requested_version="$2" + shift 2 + else + echo -e "${RED}Error: --version requires a version argument${NC}" >&2 + exit 1 + fi + ;; + --install-dir) + if [[ -n "${2:-}" ]]; then + install_dir="$2" + shift 2 + else + echo -e "${RED}Error: --install-dir requires a path argument${NC}" >&2 + exit 1 + fi + ;; + --no-modify-path) + # No-op: this script never touches shell config. Accepted so an + # existing install.sh invocation works verbatim against this URL. + shift + ;; + *) + echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2 + shift + ;; + esac +done + +raw_os=$(uname -s) +case "$raw_os" in + Linux*) os="linux" ;; + *) + echo -e "${RED}Error: the ${VARIANT} build is published for linux only (detected: ${raw_os}).${NC}" >&2 + echo -e "${MUTED}Use https://bcode.sh/install for the standard cross-platform binary.${NC}" >&2 + exit 1 + ;; +esac + +arch=$(uname -m) +case "$arch" in + aarch64|arm64) arch="arm64" ;; + x86_64|amd64) arch="x64" ;; + *) + echo -e "${RED}Error: unsupported architecture '${arch}'. Supported: arm64, x64.${NC}" >&2 + exit 1 + ;; +esac + +for tool in curl tar; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo -e "${RED}Error: '${tool}' is required but not installed.${NC}" >&2 + exit 1 + fi +done + +# A glibc binary will not start on musl, so pick the matching build rather than +# installing something that dies at exec. +is_musl=false +if [ -f /etc/alpine-release ]; then + is_musl=true +elif command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then + is_musl=true +fi + +# Non-baseline x64 builds require AVX2. This variant publishes no baseline +# asset, so say that rather than installing a binary that SIGILLs on first run. +# (Adding it is one entry in packages/bcode-serve/script/build.ts if ever needed.) +# Only trip when /proc/cpuinfo is actually readable — unknown is not "absent". +if [ "$arch" = "x64" ] && [ -r /proc/cpuinfo ] && ! grep -qwi avx2 /proc/cpuinfo; then + echo -e "${RED}Error: this CPU lacks AVX2, and no baseline ${VARIANT} build is published.${NC}" >&2 + echo -e "${MUTED}Use https://bcode.sh/install, which ships a baseline binary.${NC}" >&2 + exit 1 +fi + +target="${os}-${arch}" +[ "$is_musl" = true ] && target="${target}-musl" +filename="${APP}-${target}-${VARIANT}.tar.gz" + +if [ -z "$requested_version" ]; then + url="https://github.com/${REPO}/releases/latest/download/${filename}" + specific_version=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null \ + | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) + if [ -z "$specific_version" ]; then + echo -e "${RED}Failed to resolve the latest version.${NC}" >&2 + echo -e "${MUTED}If the repo is private, pin a version: --version ${NC}" >&2 + exit 1 + fi +else + specific_version="${requested_version#v}" + url="https://github.com/${REPO}/releases/download/v${specific_version}/${filename}" +fi + +echo -e "${MUTED}Installing ${NC}${APP} ${MUTED}(${VARIANT} build) version: ${NC}${specific_version}" + +tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/bcode_bytecode_install.XXXXXX") +staged="" +cleanup() { + rm -rf "$tmp_dir" + [ -n "${staged:-}" ] && rm -f "$staged" + return 0 +} +trap cleanup EXIT + +# Fail loudly rather than unpacking a 404 body. Releases predating this variant +# legitimately lack the asset, so say that instead of erroring out of tar. +if ! curl -fsSL -o "${tmp_dir}/${filename}" "$url"; then + echo -e "${RED}Error: could not download ${filename} for v${specific_version}.${NC}" >&2 + echo -e "${MUTED}URL: ${url}${NC}" >&2 + echo -e "${MUTED}Releases predating the ${VARIANT} variant do not publish this asset.${NC}" >&2 + echo -e "${MUTED}Check https://github.com/${REPO}/releases for one that does, or use https://bcode.sh/install.${NC}" >&2 + exit 1 +fi + +tar -xzf "${tmp_dir}/${filename}" -C "$tmp_dir" + +if [ ! -f "${tmp_dir}/${APP}" ]; then + echo -e "${RED}Error: archive did not contain a '${APP}' binary.${NC}" >&2 + exit 1 +fi + +# Stage inside the install dir, validate, then swap into place. +# +# Validating before the swap keeps a corrupt or wrong-libc download from +# replacing a working bcode. Staging in the install dir rather than the temp dir +# keeps that safety without requiring an exec-capable /tmp — `noexec` there is +# common hardening, and the install dir has to allow exec regardless. It also +# makes the final step a same-filesystem `mv`, so the swap is atomic and no +# reader can observe a half-written binary. +mkdir -p "$install_dir" +staged=$(mktemp "${install_dir}/.${APP}.XXXXXX") +mv "${tmp_dir}/${APP}" "$staged" +chmod 755 "$staged" + +if ! installed_version=$("$staged" --version 2>/dev/null); then + echo -e "${RED}Error: downloaded binary failed to run; leaving any existing install untouched.${NC}" >&2 + echo -e "${MUTED}If ${install_dir} is mounted noexec, install elsewhere with --install-dir.${NC}" >&2 + exit 1 +fi + +mv "$staged" "${install_dir}/${APP}" +staged="" + +echo -e "${MUTED}Installed ${NC}${install_dir}/${APP}${MUTED} (${installed_version})${NC}" +echo -e "${MUTED}This build provides '${APP} serve' only.${NC}" diff --git a/package.json b/package.json index a19a602479..6d051b539a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", - "typecheck": "bun turbo typecheck --filter='@browser-use/browsercode-core...' --filter='@browser-use/bcode-browser' --filter='@browser-use/bcode-laminar'", + "typecheck": "bun turbo typecheck --filter='@browser-use/browsercode-core...' --filter='@browser-use/bcode-browser' --filter='@browser-use/bcode-laminar' --filter='@browser-use/bcode-serve'", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "postinstall": "bun run --cwd packages/core fix-node-pty", "prepare": "husky", diff --git a/packages/bcode-serve/package.json b/packages/bcode-serve/package.json new file mode 100644 index 0000000000..bfcea6dafb --- /dev/null +++ b/packages/bcode-serve/package.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "version": "0.0.0", + "name": "@browser-use/bcode-serve", + "description": "Serve-only bcode binary variant for headless containers", + "type": "module", + "license": "MIT", + "private": true, + "scripts": { + "typecheck": "tsgo --noEmit", + "build": "bun run script/build.ts" + }, + "dependencies": { + "@browser-use/bcode-browser": "workspace:*", + "@browser-use/browsercode-core": "workspace:*", + "@opencode-ai/core": "workspace:*", + "@opencode-ai/script": "workspace:*", + "yargs": "18.0.0" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/yargs": "17.0.33" + } +} diff --git a/packages/bcode-serve/script/build.ts b/packages/bcode-serve/script/build.ts new file mode 100755 index 0000000000..c90478bf0b --- /dev/null +++ b/packages/bcode-serve/script/build.ts @@ -0,0 +1,215 @@ +#!/usr/bin/env bun +// +// Builds the `bcode--serve` binary variant. +// +// Separate from `packages/opencode/script/build.ts` so that package, forked +// from upstream and synced regularly, stays untouched. Produces an additional +// release asset and never writes to the standard one — different dist dir, +// different asset name. +// +// Differences from the standard build: +// - serve-only entrypoint (1 command, not 24) +// - no embedded web UI (headless; `embeddedUI()` handles its absence) +// - no TUI worker entrypoint (reachable only from `cli/cmd/tui.ts`) +// - bytecode compilation +// - linux only by default; the only consumer is the container image +// +// The Bun.build config below mirrors the standard build's and has to be kept in +// sync by hand. The smoke test boots `serve` for real, so a missing `define` or +// a broken graph fails here rather than in production. +// +// Usage: +// bun run script/build.ts # host target, no upload +// bun run script/build.ts --targets linux-arm64 # cross-compile +// OPENCODE_RELEASE=1 bun run script/build.ts ... # archive + gh upload + +import { $ } from "bun" +import path from "path" +import { fileURLToPath } from "url" + +const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const opencodeDir = path.resolve(dir, "../opencode") + +// `generate.ts` chdirs into packages/opencode as an import side effect; restore +// ours so the skills bundle's relative specifiers resolve. +const generated = await import(path.join(opencodeDir, "script/generate.ts")) +process.chdir(dir) + +import { Script } from "@opencode-ai/script" +import { createEmbeddedSkillsBundle } from "../../bcode-browser/script/embed-skills.ts" +import opencodePkg from "../../opencode/package.json" + +const skipInstall = process.argv.includes("--skip-install") +const noBytecodeFlag = process.argv.includes("--no-bytecode") + +// Target allowlist, matched against `-[-baseline][-musl]`. Defaults +// to the host target; release CI passes linux. +const targetsArg = process.argv.includes("--targets") + ? process.argv[process.argv.indexOf("--targets") + 1] + : process.env.BCODE_SERVE_TARGETS + +const allTargets: { + os: string + arch: "arm64" | "x64" + abi?: "musl" + avx2?: false +}[] = [ + { os: "linux", arch: "arm64" }, + { os: "linux", arch: "x64" }, + { os: "linux", arch: "arm64", abi: "musl" }, + { os: "linux", arch: "x64", abi: "musl" }, + { os: "darwin", arch: "arm64" }, + { os: "darwin", arch: "x64" }, +] + +const targetSuffixFor = (item: (typeof allTargets)[number]) => + [item.os, item.arch, item.avx2 === false ? "baseline" : undefined, item.abi] + .filter(Boolean) + .join("-") + +const targets = targetsArg + ? (() => { + const wanted = new Set( + targetsArg + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean), + ) + const matched = allTargets.filter((item) => wanted.has(targetSuffixFor(item))) + if (matched.length === 0) { + console.error( + `No targets matched --targets ${targetsArg}. Known: ${allTargets.map(targetSuffixFor).join(", ")}`, + ) + process.exit(1) + } + return matched + })() + : allTargets.filter((item) => item.os === process.platform && item.arch === process.arch && !item.abi) + +const embeddedSkillsFileMap = await createEmbeddedSkillsBundle(dir) + +await $`rm -rf dist` + +if (!skipInstall) { + // Cross-compiling needs the other platforms' native artifacts present. + await $`bun install --os="*" --cpu="*" @ff-labs/fff-bun@${opencodePkg.dependencies["@ff-labs/fff-bun"]}`.cwd( + opencodeDir, + ) + await $`bun install --os="*" --cpu="*" @parcel/watcher@${opencodePkg.dependencies["@parcel/watcher"]}`.cwd( + opencodeDir, + ) +} + +const archives: string[] = [] + +for (const item of targets) { + const targetSuffix = targetSuffixFor(item) + const assetName = `bcode-${targetSuffix}-serve` // release archive basename + const outdir = `dist/${assetName}` + console.log(`building ${assetName}`) + await $`mkdir -p ${outdir}/bin` + + const skillsPath = "bcode-skills.gen.ts" + + const result = await Bun.build({ + conditions: ["bun", "node"], + // The opencode tsconfig supplies the `@/*` -> packages/opencode/src/* + // path mapping that its own sources rely on. + tsconfig: path.join(opencodeDir, "tsconfig.json"), + external: ["node-gyp"], + format: "esm", + minify: true, + sourcemap: "none", + splitting: true, + bytecode: !noBytecodeFlag, + compile: { + autoloadBunfig: false, + autoloadDotenv: false, + autoloadTsconfig: true, + autoloadPackageJson: true, + target: `bun-${targetSuffix}` as any, + outfile: path.join(dir, outdir, "bin/bcode"), + execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], + windows: {}, + }, + files: { + [skillsPath]: embeddedSkillsFileMap, + }, + entrypoints: ["./src/index.ts", skillsPath], + define: { + FFF_LIBC: JSON.stringify(item.abi === "musl" ? "musl" : "gnu"), + OPENCODE_VERSION: `'${Script.version}'`, + OPENCODE_MODELS_DEV: generated.modelsData, + // TUI-only; defined so a stray reference cannot throw ReferenceError. + OTUI_TREE_SITTER_WORKER_PATH: `''`, + OPENCODE_WORKER_PATH: `''`, + OPENCODE_CHANNEL: `'${Script.channel}'`, + OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", + // Populated by release CI from LMNR_PROJECT_API_KEY_OSS; empty locally. + // Runtime use is gated in @browser-use/bcode-browser/src/telemetry.ts. + BCODE_DEFAULT_LMNR_KEY: JSON.stringify(process.env.BCODE_DEFAULT_LMNR_KEY ?? ""), + ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}), + }, + }) + if (!result.success) { + console.error(result.logs) + process.exit(1) + } + + // Boot the server for real, not just `--version`: a missing `define` or a + // module dropped from the graph would sail straight past `--version`. + if (item.os === process.platform && item.arch === process.arch && !item.abi) { + const binaryPath = path.join(dir, outdir, "bin/bcode") + console.log(`Smoke test: ${assetName} serve`) + const proc = Bun.spawn([binaryPath, "serve", "--port", "0", "--hostname", "127.0.0.1"], { + stdout: "pipe", + stderr: "pipe", + env: { ...process.env, OPENCODE_SERVER_PASSWORD: "smoke-test" }, + }) + const listening = (async () => { + const reader = proc.stdout.getReader() + const decoder = new TextDecoder() + let buffered = "" + while (true) { + const { value, done } = await reader.read() + if (done) break + buffered += decoder.decode(value, { stream: true }) + if (buffered.includes("listening on")) return buffered + } + const stderr = await new Response(proc.stderr).text() + throw new Error(`server exited before listening.\nstdout:\n${buffered}\nstderr:\n${stderr}`) + })() + // Timer handle is cleared in `finally`: an armed timer keeps the event loop + // alive, which would stall the build for the full timeout after every + // successful smoke test. + let timer: ReturnType | undefined + try { + const banner = await Promise.race([ + listening, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error("timed out after 30s")), 30_000) + }), + ]) + console.log(`Smoke test passed: ${banner.trim().split("\n").at(-1)}`) + } catch (e) { + console.error(`Smoke test failed for ${assetName}:`, e) + proc.kill() + process.exit(1) + } finally { + clearTimeout(timer) + proc.kill() + await proc.exited + } + } + + if (Script.release) { + // linux only, so tar for everything; add a zip branch if darwin ships. + await $`tar -czf ../../${assetName}.tar.gz *`.cwd(`${outdir}/bin`) + archives.push(`./dist/${assetName}.tar.gz`) + } +} + +if (Script.release) { + await $`gh release upload v${Script.version} ${archives} --clobber --repo ${process.env.GH_REPO}` + console.log(`uploaded: ${archives.join(", ")}`) +} diff --git a/packages/bcode-serve/src/index.ts b/packages/bcode-serve/src/index.ts new file mode 100644 index 0000000000..6dcdc09ba0 --- /dev/null +++ b/packages/bcode-serve/src/index.ts @@ -0,0 +1,132 @@ +// Serve-only entrypoint for the `bcode--serve` binary variant. +// +// `packages/opencode/src/index.ts` eagerly imports all 24 command modules. +// Headless containers only ever invoke `bcode serve`, so registering just that +// one keeps the other 23 out of the bundle — which is what makes bytecode +// compilation affordable for this variant. +// +// Lives here rather than in `packages/opencode` so that tree, forked from +// upstream and synced regularly, stays untouched. +// +// DRIFT WARNING: the global-option and lifecycle wiring below is duplicated +// from `packages/opencode/src/index.ts`, which stays the source of truth. +// Options added there must be mirrored here. The build's smoke test boots +// `serve` for real, so a missing `define` fails the build, not the deploy. + +// Must stay the FIRST import: this module sets LMNR_PROJECT_API_KEY as an +// import side effect, before any downstream module-load code reads it. Same +// ordering contract as packages/opencode/src/index.ts. +import "@browser-use/bcode-browser/telemetry" + +import yargs from "yargs" +import { hideBin } from "yargs/helpers" +import { EOL } from "os" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { ServeCommand } from "@browser-use/browsercode-core/cli/cmd/serve" +import { UI } from "@browser-use/browsercode-core/cli/ui" +import { FormatError } from "@browser-use/browsercode-core/cli/error" +import { errorMessage } from "@browser-use/browsercode-core/util/error" +import { Heap } from "@browser-use/browsercode-core/cli/heap" + +const args = hideBin(process.argv) + +function show(out: string) { + const text = out.trimStart() + if (!text.startsWith("bcode ")) { + process.stderr.write(UI.logo() + EOL + EOL) + process.stderr.write(text + EOL) + return + } + process.stderr.write(out) +} + +const cli = yargs(args) + .parserConfiguration({ "populate--": true }) + .scriptName("bcode") + .wrap(100) + .help("help", "show help") + .alias("help", "h") + .version("version", "show version number", InstallationVersion) + .alias("version", "v") + .option("print-logs", { + describe: "print logs to stderr", + type: "boolean", + }) + .option("log-level", { + describe: "log level", + type: "string", + choices: ["DEBUG", "INFO", "WARN", "ERROR"], + }) + .option("pure", { + describe: "run without external plugins", + type: "boolean", + }) + .middleware(async (opts) => { + if (opts.printLogs) process.env.OPENCODE_PRINT_LOGS = "1" + if (opts.logLevel) process.env.OPENCODE_LOG_LEVEL = opts.logLevel + if (opts.pure) { + process.env.OPENCODE_PURE = "1" + } + + Heap.start() + + process.env.AGENT = "1" + process.env.OPENCODE = "1" + process.env.OPENCODE_PID = String(process.pid) + }) + .usage("") + .command(ServeCommand) + .fail((msg, err) => { + if ( + msg?.startsWith("Unknown argument") || + msg?.startsWith("Not enough non-option arguments") || + msg?.startsWith("Invalid values:") + ) { + if (err) throw err + cli.showHelp(show) + } + if (err) throw err + process.exit(1) + }) + .strict() + +try { + if (args.includes("-h") || args.includes("--help")) { + await cli.parse(args, (err: Error | undefined, _argv: unknown, out: string) => { + if (err) throw err + if (!out) return + show(out) + }) + } else { + await cli.parse() + } +} catch (e) { + const formatted = FormatError(e) + if (formatted) UI.error(formatted) + if (formatted === undefined) { + UI.error("Unexpected error" + EOL) + process.stderr.write(errorMessage(e) + EOL) + } + process.exitCode = 1 +} finally { + // Single drain point for OTel-based plugins (e.g. bcode-laminar); without it + // trailing spans are lost. Mirrors the drain in packages/opencode/src/index.ts. + try { + const { pluginShutdownHooks } = await import("@browser-use/browsercode-core/plugin/index") + await Promise.race([ + Promise.allSettled( + Array.from(pluginShutdownHooks).map((hook) => + Promise.resolve() + .then(hook) + .catch((err: Error) => console.error("plugin shutdown hook failed", err)), + ), + ), + new Promise((resolve) => setTimeout(resolve, 3000)), + ]) + } catch (err) { + console.error("plugin shutdown import failed", err) + } + // Some subprocesses don't react properly to SIGTERM and similar signals. + // Explicitly exit to avoid any hanging subprocesses. + process.exit() +} diff --git a/packages/bcode-serve/tsconfig.json b/packages/bcode-serve/tsconfig.json new file mode 100644 index 0000000000..7b2471d0f3 --- /dev/null +++ b/packages/bcode-serve/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": [], + "noUncheckedIndexedAccess": false, + "customConditions": ["browser"], + // opencode sources reach each other through `@/*`; tsc needs the same + // mapping the bundler gets from packages/opencode/tsconfig.json. + "paths": { + "@/*": ["../opencode/src/*"] + } + }, + // opencode's ambient module declarations (`*.wasm`, `*.sql`, `*.md`) are + // implicit inside that package; pull them in so typecheck here is honest. + "include": ["src/**/*", "script/**/*", "../opencode/src/*.d.ts"] +}