fix(cli): embed native watcher binding

This commit is contained in:
Kit Langton 2026-07-28 22:32:08 -04:00 committed by GitHub
commit 7b775c2582
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 64 additions and 14 deletions

View file

@ -5,6 +5,7 @@ import { rm } from "fs/promises"
import path from "path"
import { Script } from "@opencode-ai/script"
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
import type { BunPlugin } from "bun"
import pkg from "../package.json"
import { modelsData } from "./generate"
@ -22,7 +23,7 @@ await rm(outdir, { recursive: true, force: true })
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
const plugin = createSolidTransformPlugin()
const solidPlugin = createSolidTransformPlugin()
const allTargets: {
os: string
@ -55,6 +56,16 @@ const targets = singleFlag
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
for (const item of targets) {
const parcelWatcherPackage = `@parcel/watcher-${item.os}-${item.arch}${item.os === "linux" ? `-${item.abi ?? "glibc"}` : ""}`
const parcelWatcherPlugin: BunPlugin = {
name: "parcel-watcher-binding",
setup(build) {
build.onLoad({ filter: /filesystem\/watcher-binding\.ts$/ }, () => ({
contents: `import binding from ${JSON.stringify(parcelWatcherPackage)}; export default () => binding`,
loader: "js",
}))
},
}
const target = [
binary,
item.os === "win32" ? "windows" : item.os,
@ -69,7 +80,7 @@ for (const item of targets) {
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
tsconfig: "./tsconfig.json",
plugins: [plugin],
plugins: [solidPlugin, parcelWatcherPlugin],
external: ["node-gyp"],
format: "esm",
minify: true,

View file

@ -13,7 +13,7 @@ const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["nod
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-"))
const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-")))
const env = {
...process.env,
HOME: root,
@ -29,6 +29,7 @@ const processes: Array<ReturnType<typeof Bun.spawn>> = []
const errors: Array<Promise<string>> = []
let failure: unknown
try {
await fs.mkdir(path.join(root, ".opencode"))
spawnService()
spawnService()
const registration = await waitForRegistration()
@ -49,6 +50,11 @@ try {
{ signal: AbortSignal.timeout(5_000) },
)
if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication")
if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation")
const plugin = path.join(root, ".opencode", "plugins", "smoke.ts")
await fs.mkdir(path.dirname(plugin), { recursive: true })
await fs.writeFile(plugin, pluginSource())
await waitForPlugin(info.url, headers)
const unauthorizedHealth = await fetch(new URL("/api/health", info.url), {
signal: AbortSignal.timeout(5_000),
@ -88,6 +94,7 @@ try {
} finally {
processes.forEach((process) => process.kill())
await Promise.all(processes.map((process) => process.exited))
if (failure) errors.push(fs.readFile(path.join(root, "data", "opencode", "log", "opencode.log"), "utf8").catch(() => ""))
}
const output = await Promise.all(errors)
@ -133,3 +140,31 @@ async function waitForReady(url: string, headers: HeadersInit) {
function exitsWithin(process: Bun.Subprocess, milliseconds: number) {
return Promise.race([process.exited.then(() => true), Bun.sleep(milliseconds).then(() => false)])
}
function pluginSource() {
return 'export default { id: "smoke", setup: async () => {} }\n'
}
async function pluginIDs(url: string, headers: HeadersInit) {
const endpoint = new URL("/api/plugin", url)
endpoint.searchParams.set("location[directory]", root)
const response = await fetch(endpoint, { headers, signal: AbortSignal.timeout(5_000) })
const body: unknown = await response.json()
if (typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) {
throw new Error("Compiled service returned an invalid plugin list")
}
return body.data.flatMap((plugin) =>
typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string"
? [plugin.id]
: [],
)
}
async function waitForPlugin(url: string, headers: HeadersInit) {
const deadline = Date.now() + 10_000
while (Date.now() < deadline) {
if ((await pluginIDs(url, headers)).includes("smoke")) return
await Bun.sleep(25)
}
throw new Error("Compiled service did not discover the created plugin")
}

View file

@ -0,0 +1,13 @@
import { createRequire } from "node:module"
declare const OPENCODE_LIBC: string | undefined
const require = createRequire(import.meta.url)
export default function load() {
const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
return require(
process.env.OPENCODE_PARCEL_WATCHER_PATH ??
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
)
}

View file

@ -9,23 +9,14 @@ import { Cause, Context, Effect, Layer, PubSub, RcMap, Schema, Stream } from "ef
import { lazy } from "../util/lazy"
import { watch as watchFileSystem } from "node:fs"
import path from "path"
import { createRequire } from "node:module"
declare const OPENCODE_LIBC: string | undefined
import loadBinding from "./watcher-binding"
const SUBSCRIBE_TIMEOUT_MS = 10_000
const require = createRequire(import.meta.url)
export const Event = { Updated: FileSystem.Event.Changed }
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try {
const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
const binding = require(
process.env.OPENCODE_PARCEL_WATCHER_PATH ??
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
)
return createWrapper(binding) as typeof import("@parcel/watcher")
return createWrapper(loadBinding()) as typeof import("@parcel/watcher")
} catch {
return
}