Files
Ryan Vogel 476f398510 feat(browser-extension): bring Browser Control into OpenCode Browser
OpenCode Browser no longer depends on the separate Browser Control product.
Its relay, CLI, MCP server, and agent guide (anomalyco/browser-control
8c80ac2, MIT, Anomaly) now live in packages/browser-extension/cli and ship
as the opencode-browser-cli npm package:

- Renamed throughout: `opencode-browser` command and MCP server,
  OPENCODE_BROWSER_* environment, opencode-browser skill, injected page ids.
- Its own port, 19988, so a Browser Control relay on 19989 can't hold it,
  and it accepts only OpenCode Browser's store and unpacked extension IDs
  (the path-hashed bundled-extension origin is gone).
- All files under one data root (src/paths.ts: ~/.local/share/
  opencode-browser, %LOCALAPPDATA% on Windows, OPENCODE_BROWSER_HOME):
  runtime, logs, relay catalogs and lifecycle, sessions, secrets.
- One entry (src/main.ts, bundled to dist/cli.mjs with npm dependencies
  external): setup commands (install, extension, uninstall, host) go to
  setup.ts, everything else to the relay CLI; `status` prints setup state
  first. The managed relay respawns the same entry.
- install now installs this exact version into <data root>/runtime with
  npm (OPENCODE_BROWSER_PACKAGE overrides the spec), points the native host
  and a new "opencode-browser" MCP entry at it, and warns about leftover
  browser-control MCP entries instead of configuring them. uninstall also
  removes the MCP entry and the data root.
- Logging: relay.log is always on (relay ready, extension connected/
  rejected/closed with codes, every HTTP request with status and ms except
  status/version polls, faults) and rotates to relay.log.1 instead of
  truncating; host.log records each native messaging request with timing.
- cli/ has its own package.json, bun.lock, and tsconfig so its pinned
  dependencies (effect 4.0.1, playwright-core 1.63.0, ...) stay out of the
  monorepo lockfile. The release workflow installs, typechecks, builds, and
  smoke tests it.

Verified: cli typecheck; browser-control's own test suite run against the
port outside the repo: 880/888 pass, the 8 failures assert the removed
layout (dist/mcp.js, path-hashed origins, extension/dist). From an npm pack
tarball in a temp HOME: install (5.7s), the native host answered service and
plugin requests, the MCP server listed 25 tools over stdio, and `execute`
started a managed relay, drove headless Chrome with the extension to
example.com, and returned the title; status and relay.log looked right.
2026-10-06 10:18:42 -04:00

55 lines
2.3 KiB
TypeScript

// Builds the published package: dist/cli.mjs (main.ts and everything it loads, with npm dependencies left
// external and installed by npm) and extension/ (the built unpacked extension, keeping its manifest key so the
// unpacked ID matches the host's allowed origins). Both carry this package's version.
import { $ } from "bun"
import { chmod, cp, rm } from "node:fs/promises"
import path from "node:path"
const here = import.meta.dir
const extension = path.resolve(here, "..")
const pkg = await Bun.file(path.join(here, "package.json")).json()
await $`bun run build`.cwd(extension).quiet()
await rm(path.join(here, "dist"), { recursive: true, force: true })
await rm(path.join(here, "extension"), { recursive: true, force: true })
const result = await Bun.build({
entrypoints: [path.join(here, "src/main.ts")],
outdir: path.join(here, "dist"),
naming: "cli.mjs",
target: "node",
format: "esm",
// Runtime dependencies stay external; jsonc-parser (a dev dependency) is bundled.
external: Object.keys(pkg.dependencies ?? {}),
plugins: [
{
// jsonc-parser's default entry is UMD with runtime requires; its ESM build bundles cleanly.
name: "jsonc-parser-esm",
setup(build) {
build.onResolve({ filter: /^jsonc-parser$/ }, () => ({
path: path.join(path.dirname(Bun.resolveSync("jsonc-parser/package.json", here)), "lib/esm/main.js"),
}))
},
},
],
define: {
"globalThis.__OPENCODE_BROWSER_VERSION__": JSON.stringify(pkg.version),
"globalThis.__OPENCODE_BROWSER_BUILD_ID__": JSON.stringify(new Date().toISOString()),
},
})
if (!result.success) throw new AggregateError(result.logs, "build failed")
const cli = path.join(here, "dist", "cli.mjs")
const source = await Bun.file(cli).text()
await Bun.write(cli, source.startsWith("#!") ? source : `#!/usr/bin/env node\n${source}`)
await chmod(cli, 0o755)
await cp(path.join(extension, "dist"), path.join(here, "extension"), {
recursive: true,
filter: (file) => !file.endsWith(".map"),
})
const manifestFile = path.join(here, "extension", "manifest.json")
const manifest = await Bun.file(manifestFile).json()
manifest.version = pkg.version
await Bun.write(manifestFile, JSON.stringify(manifest, null, 2) + "\n")
console.log(`built dist/cli.mjs and extension/ (${pkg.version})`)