diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a5b2f8530..90969d9061 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -146,6 +146,42 @@ jobs: run: | ./packages/opencode/script/build.ts + - name: Build serve-only variant and upload to release + # Additive: publishes `bcode-linux-{arm64,x64}[-musl]-serve.tar.gz` for + # headless containers, from its own package so the upstream-forked + # `packages/opencode` and the canonical step above stay untouched. + # + # `continue-on-error` so this variant can never block a normal release; + # its asset names differ, so `--clobber` cannot touch the standard ones. + # Invoked via `bun` so a lost executable bit cannot silently disable it. + # + # No baseline (non-AVX2 x64) target — install-bytecode.sh rejects that + # case with a pointer to /install rather than shipping a binary that + # SIGILLs. + 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, not the selected tag, while the upload still targets + # that tag. Only build when the tree really is the tag. (The canonical + # step has the same exposure; fixing it means changing `Checkout` for + # the whole job, which is out of scope here.) + 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..2db2710ef1 --- /dev/null +++ b/install-bytecode.sh @@ -0,0 +1,169 @@ +#!/bin/sh +# +# BrowserCode installer — serve-only build. Hosted at https://bcode.sh/bytecode, +# alongside (not replacing) https://bcode.sh/install, which stays the path for +# the full CLI. +# +# curl -fsSL https://bcode.sh/bytecode | sh -s -- --no-modify-path --version 0.1.19 +# +# Installs `bcode-linux-[-musl]-serve`, which provides ONLY `bcode serve`; +# run, tui, web, github and the rest are absent and exit 1. It exists for headless +# containers. +# +# POSIX sh, not bash: Alpine is a supported target here and ships no bash. +set -eu + +REPO=browser-use/browsercode +DIM='\033[0;2m' +RED='\033[0;31m' +OFF='\033[0m' + +say() { printf "${DIM}%s${OFF}\n" "$1" >&2; } +die() { + printf "${RED}%s${OFF}\n" "$1" >&2 + shift + for line in "$@"; do [ -n "$line" ] && printf "${DIM}%s${OFF}\n" "$line" >&2; done + exit 1 +} + +# Reject empty and flag-shaped values: `--version "$UNSET"` would otherwise +# silently install latest, which is exactly what pinning exists to prevent. +need() { + case $2 in + "" | -*) die "$1 needs a value (got: ${2:-})" ;; + esac +} + +usage() { + cat >&2 < version to install (default: latest release) + --install-dir where to install (default: \$HOME/.bcode/bin) + --no-modify-path accepted for install.sh parity; this script never edits + shell config files + -h, --help show this help + +Installs a bcode that provides ONLY 'bcode serve'. +For the full CLI: curl -fsSL https://bcode.sh/install | bash +EOF +} + +# Namespaced on purpose: a bare VERSION is a common Dockerfile ARG, and picking +# it up here would silently install the wrong build. +version=${BCODE_VERSION:-} +# HOME is routinely unset under `--user`/runAsUser with no passwd entry. +install_dir=${BCODE_INSTALL_DIR:-${HOME:-/root}/.bcode/bin} + +while [ $# -gt 0 ]; do + case $1 in + -v | --version) need "--version" "${2:-}"; version=$2; shift 2 ;; + --install-dir) need "--install-dir" "${2:-}"; install_dir=$2; shift 2 ;; + --no-modify-path) shift ;; + # Tolerated: wrappers relaying args often forward an extra one. + --) shift ;; + -h | --help) usage; exit 0 ;; + # Fail rather than warn: a typo'd flag would otherwise quietly install + # "latest" into a build that meant to pin a version. + *) die "Unknown option: $1" "Run with --help for usage." ;; + esac +done + +for tool in curl tar; do + command -v "$tool" >/dev/null 2>&1 || die "'$tool' is required but not installed." +done + +[ "$(uname -s)" = Linux ] || die \ + "The serve build is published for linux only (got $(uname -s))." \ + "Use https://bcode.sh/install for the standard cross-platform binary." + +case $(uname -m) in + aarch64 | arm64) arch=arm64 ;; + x86_64 | amd64) arch=x64 ;; + *) die "Unsupported architecture: $(uname -m)." "Supported: arm64, x64." ;; +esac + +# Non-baseline x64 builds need AVX2 and no baseline serve asset is published. +# Only decide when /proc/cpuinfo actually carries a flags line: emulated or +# redacted cpuinfo (qemu `--platform linux/amd64` on arm64, lxcfs, some +# hypervisors) has none, and unknown is not the same as absent. +if [ "$arch" = x64 ] && grep -qi '^flags' /proc/cpuinfo 2>/dev/null && ! grep -qwi avx2 /proc/cpuinfo; then + die "This CPU has no AVX2 and no baseline serve build is published." \ + "Use https://bcode.sh/install, which ships a baseline binary." +fi + +# A glibc binary cannot exec on musl. musl's ldd prints its banner and then exits +# non-zero, so match its output — the pipeline's status is grep's, not ldd's. +target=linux-$arch +if [ -f /etc/alpine-release ] || ldd --version 2>&1 | grep -qi musl; then + target=$target-musl +fi +asset=bcode-$target-serve.tar.gz + +if [ -n "$version" ]; then + version=${version#v} +else + version=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null | + sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p') + [ -n "$version" ] || die "Could not resolve the latest version." "Pin one with --version ." +fi +url=https://github.com/$REPO/releases/download/v$version/$asset + +say "Installing bcode $version ($target, serve build)" + +tmp=$(mktemp -d) +staged= +# `return 0` is load-bearing: the `&&` above it returns 1 whenever staged is +# empty, which under `set -e` would turn a clean run into a non-zero exit. +cleanup() { + rm -rf "$tmp" + [ -n "$staged" ] && rm -f "$staged" + return 0 +} +trap cleanup EXIT +# ash/dash do not run the EXIT trap on a signal; `docker build` cancellation +# sends TERM. Exiting from the handler routes through the EXIT trap above. +trap 'exit 130' INT +trap 'exit 143' TERM +trap 'exit 129' HUP + +curl -fsSL -o "$tmp/$asset" "$url" || die \ + "Could not download $asset for v$version." \ + "URL: $url" \ + "Releases published before this variant existed do not carry the asset." + +tar -xzf "$tmp/$asset" -C "$tmp" +[ -f "$tmp/bcode" ] || die "Archive did not contain a bcode binary." + +# Stage inside the install dir, validate, then swap. Validating first means a bad +# download never replaces a working bcode; staging here rather than in /tmp avoids +# requiring an exec-capable /tmp (noexec there is common hardening) and makes the +# final step a same-filesystem rename, so the swap is atomic. +case $install_dir in + "") die "Install directory cannot be empty." ;; + -*) die "Install directory must not start with '-' (got: $install_dir)." ;; +esac +mkdir -p -- "$install_dir" +if [ -d "$install_dir/bcode" ]; then + die "$install_dir/bcode is a directory; refusing to install over it." +fi +staged=$(mktemp "$install_dir/.bcode.XXXXXX") +mv "$tmp/bcode" "$staged" +chmod 755 "$staged" + +# Keep the binary's own stderr: on a libc mismatch it names the missing loader, +# which is the actual diagnosis. +if ! installed=$("$staged" --version 2>"$tmp/err"); then + die "Downloaded binary failed to run; existing install left untouched." \ + "$(cat "$tmp/err" 2>/dev/null)" \ + "If $install_dir is mounted noexec, pass --install-dir." +fi + +mv "$staged" "$install_dir/bcode" +staged= + +say "Installed $install_dir/bcode ($installed) — provides 'bcode serve' only" +case ":$PATH:" in + *":$install_dir:"*) ;; + *) say "Not on PATH. Add it with: export PATH=\"$install_dir:\$PATH\"" ;; +esac 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..2c514f5844 --- /dev/null +++ b/packages/bcode-serve/script/build.ts @@ -0,0 +1,178 @@ +#!/usr/bin/env bun +// +// Builds the `bcode--serve` binary variant: registers only `bcode serve` +// and is bytecode-compiled, for headless containers. +// +// Separate from packages/opencode/script/build.ts so that package — forked from +// upstream and synced regularly — stays untouched. Produces additional release +// assets; never writes the standard ones. +// +// bun run script/build.ts # host target +// bun run script/build.ts --targets linux-arm64,linux-x64 # cross-compile +// OPENCODE_RELEASE=1 bun run script/build.ts --targets ... # archive + upload + +import { $ } from "bun" +import path from "path" +import { fileURLToPath } from "url" + +const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const opencode = path.resolve(dir, "../opencode") + +// generate.ts chdirs into packages/opencode on import; restore ours so the +// skills bundle's relative specifiers resolve. +const { modelsData } = await import(path.join(opencode, "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 flag = (name: string) => process.argv.includes(`--${name}`) +const opt = (name: string) => (flag(name) ? process.argv[process.argv.indexOf(`--${name}`) + 1] : undefined) + +const host = `${process.platform}-${process.arch}` +const targets = (opt("targets") ?? host) + .split(",") + .map((t) => t.trim()) + .filter(Boolean) + +const skills = await createEmbeddedSkillsBundle(dir) + +await $`rm -rf dist` + +// Cross-compiling needs every platform's native artifacts on disk. Both of these +// are in the serve graph — fff-bun via core/filesystem/fff.bun.ts, @parcel/watcher +// via core/filesystem/watcher.ts — and a plain install only fetches the host's. +if (!flag("skip-install")) { + for (const dep of ["@ff-labs/fff-bun", "@parcel/watcher"] as const) { + await $`bun install --os="*" --cpu="*" ${dep}@${opencodePkg.dependencies[dep]}`.cwd(opencode) + } +} + +// Boot the server and wait for its banner. `--version` alone would pass with a +// broken server graph. +// +// The timeout has to be a racing rejection, not just a kill: killing the child +// does not end the read loop if any descendant inherited stdout, so a hung +// smoke test would hang the build instead of failing it. +async function smoke(bin: string) { + const proc = Bun.spawn([bin, "serve", "--port", "0", "--hostname", "127.0.0.1"], { + stdout: "pipe", + stderr: "pipe", + env: { ...process.env, OPENCODE_SERVER_PASSWORD: "smoke-test" }, + }) + let timer: ReturnType | undefined + const banner = (async () => { + const reader = proc.stdout.getReader() + const decoder = new TextDecoder() + for (let out = ""; ; ) { + const { value, done } = await reader.read() + if (done) throw new Error(`serve exited before listening:\n${out}\n${await new Response(proc.stderr).text()}`) + out += decoder.decode(value, { stream: true }) + if (out.includes("listening on")) return out.trim().split("\n").at(-1) + } + })() + try { + return await Promise.race([ + banner, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error("serve did not print its listening banner within 60s")), 60_000) + }), + ]) + } finally { + clearTimeout(timer) + // SIGKILL, not the default SIGTERM: a wedged child must not outlive us. + proc.kill("SIGKILL") + await proc.exited + } +} + +const archives: string[] = [] + +for (const target of targets) { + const asset = `bcode-${target}-serve` + const bin = path.join(dir, "dist", asset, "bin/bcode") + const musl = /(^|-)musl(-|$)/.test(target) + console.log(`building ${asset}`) + + const built = await Bun.build({ + conditions: ["bun", "node"], + // opencode's tsconfig supplies the `@/*` -> packages/opencode/src/* mapping + // that its own sources rely on. + tsconfig: path.join(opencode, "tsconfig.json"), + external: ["node-gyp"], + format: "esm", + minify: true, + sourcemap: "none", + splitting: true, + bytecode: !flag("no-bytecode"), + compile: { + autoloadBunfig: false, + autoloadDotenv: false, + autoloadTsconfig: true, + autoloadPackageJson: true, + target: `bun-${target}` as any, + outfile: bin, + execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], + windows: {}, + }, + files: { + // Hard requirement: skills.ts throws when this is missing in a compiled + // binary, rather than degrading. + "bcode-skills.gen.ts": skills, + // Must stay embedded even though this build ships no web UI. Leaving it + // out does NOT fail closed: Bun keeps the bare `import("opencode-web-ui.gen.ts")` + // in server/shared/ui.ts live and resolves it at runtime against the + // server's cwd. A file planted at ./node_modules/opencode-web-ui.gen.ts + // then executes in-process, and its default export is used as a path map + // that serveUIEffect reads and returns over HTTP. PUBLIC_UI_PATHS lets + // /site.webmanifest and the two manifest icons skip auth entirely, so that + // read is reachable even with a server password set. An empty stub + // resolves the specifier hermetically and every UI path 404s, which is + // what a headless server wants anyway. + "opencode-web-ui.gen.ts": "export default {}", + }, + entrypoints: ["./src/index.ts", "bcode-skills.gen.ts", "opencode-web-ui.gen.ts"], + // Every consumer of these reads them behind a `typeof` guard, so a missing + // one degrades silently rather than throwing — hence the assertion below. + define: { + OPENCODE_VERSION: `'${Script.version}'`, + OPENCODE_CHANNEL: `'${Script.channel}'`, + OPENCODE_MODELS_DEV: modelsData, + // Native-binding selectors: fff-bun and @parcel/watcher each pick their + // .so by libc, so musl builds must say so or file watching silently dies. + FFF_LIBC: JSON.stringify(musl ? "musl" : "gnu"), + OPENCODE_LIBC: target.startsWith("linux-") ? `'${musl ? "musl" : "glibc"}'` : "''", + // Release CI supplies the key; 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 ?? ""), + }, + }) + if (!built.success) { + console.error(built.logs) + process.exit(1) + } + + if (target === host) { + const banner = await smoke(bin) + // Assert a define actually landed. The banner above cannot tell: every + // define read upstream is `typeof`-guarded, so a dropped one just falls + // back to a default and the server still starts. + const version = (await $`${bin} --version`.text()).trim() + if (version !== Script.version) { + console.error(`define check failed: --version printed ${version}, expected ${Script.version}`) + process.exit(1) + } + console.log(`smoke: ${banner}`) + } + + if (Script.release) { + await $`tar -czf ../../${asset}.tar.gz *`.cwd(`dist/${asset}/bin`) + archives.push(`./dist/${asset}.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..263c0bb64d --- /dev/null +++ b/packages/bcode-serve/src/index.ts @@ -0,0 +1,134 @@ +// 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, and nothing enforces it — the +// build's smoke test boots `serve` for real, which catches a broken module +// graph but not a missing option, since `.strict()` only rejects a flag at the +// moment a caller passes one. + +// 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"] +}