diff --git a/bun.lock b/bun.lock index 6e3a92f5e6f..43d3a81c20e 100644 --- a/bun.lock +++ b/bun.lock @@ -125,6 +125,7 @@ "@effect/platform-node": "catalog:", "@opencode-ai/client": "workspace:*", "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/pty": "0.1.8", "@opencode-ai/schema": "workspace:*", "@opencode-ai/server": "workspace:*", "@opencode-ai/tui": "workspace:*", @@ -364,6 +365,7 @@ "@opencode-ai/ai": "workspace:*", "@opencode-ai/codemode": "workspace:*", "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/pty": "0.1.8", "@opencode-ai/schema": "workspace:*", "@opencode-ai/util": "workspace:*", "@parcel/watcher": "2.5.1", @@ -2155,6 +2157,20 @@ "@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"], + "@opencode-ai/pty": ["@opencode-ai/pty@0.1.8", "", { "optionalDependencies": { "@opencode-ai/pty-darwin-arm64": "0.1.8", "@opencode-ai/pty-darwin-x64": "0.1.8", "@opencode-ai/pty-linux-arm64-gnu": "0.1.8", "@opencode-ai/pty-linux-arm64-musl": "0.1.8", "@opencode-ai/pty-linux-x64-gnu": "0.1.8", "@opencode-ai/pty-linux-x64-musl": "0.1.8" }, "bin": { "opencode-pty": "bin/opencode-pty.js" } }, "sha512-+OHAANB6xgtO4zVMK5Q3l50Iot2fYcHL003rNpvxxCXLkpdO90UnnRCL9jrll1SWkWoDZKaUDlR/Scw6TUTN4A=="], + + "@opencode-ai/pty-darwin-arm64": ["@opencode-ai/pty-darwin-arm64@0.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5DqVcCfa9P5zpM0QpvXD/QqZXFdXvtIoaBHVHjYnTt8oqlnmMWqG4OVowzPNQg07NudW6apwX1Jop1wuERgWGw=="], + + "@opencode-ai/pty-darwin-x64": ["@opencode-ai/pty-darwin-x64@0.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-keEDxfpy0mFJYmsTqea9IUbpLKpbe9yL4ISU8tYA0daWiKuR8ZlfLTlLGltaFNO7l7q6mURMeI4cZzwySOX/Kw=="], + + "@opencode-ai/pty-linux-arm64-gnu": ["@opencode-ai/pty-linux-arm64-gnu@0.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-NVJvlVtPbYRyjABwLkm1iboijpSZMXv4lfg4qda7UCzAgiDZ0cWauXeW5wtd9bbnUbHw4rdNVU6YweuwASgZXw=="], + + "@opencode-ai/pty-linux-arm64-musl": ["@opencode-ai/pty-linux-arm64-musl@0.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-GAhnCs7b7B8Hqa7AEN7k5ra4IqybFYePgbCSOHn3Y4mofp/5NjDlkOG/VFM7rHAy05iEmRi5WtOPRv0Mkca8zA=="], + + "@opencode-ai/pty-linux-x64-gnu": ["@opencode-ai/pty-linux-x64-gnu@0.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-vSbty1L0BuOQlK7DmukuhdxNKv2vzqyxOjHmh+TULptMDbDZwE1EybASoXB7dYog9as85adFCz8ZTF7Boyfxjg=="], + + "@opencode-ai/pty-linux-x64-musl": ["@opencode-ai/pty-linux-x64-musl@0.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-2GhmJLl+Ly/JjzMS2Eqvk7SgeuHIB0cQ4Wrmnj4WKhr2V7lS+pdUEApN6S0SG/FK0TYJlWeHeOIpNZgDqzEPPg=="], + "@opencode-ai/schema": ["@opencode-ai/schema@workspace:packages/schema"], "@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"], diff --git a/bunfig.toml b/bunfig.toml index 566af3d73b1..0a370b6f2c2 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,7 +2,7 @@ exact = true # Only install newly resolved package versions published at least 3 days ago. minimumReleaseAge = 259200 -minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@opencode-ai/sdk", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish", "blume"] +minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@opencode-ai/sdk", "@opencode-ai/pty", "@opencode-ai/pty-darwin-arm64", "@opencode-ai/pty-darwin-x64", "@opencode-ai/pty-linux-arm64-gnu", "@opencode-ai/pty-linux-arm64-musl", "@opencode-ai/pty-linux-x64-gnu", "@opencode-ai/pty-linux-x64-musl", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish", "blume"] [test] root = "./do-not-run-tests-from-root" diff --git a/packages/cli/package.json b/packages/cli/package.json index b87857f86b8..6f470ebbc17 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,6 +27,7 @@ "@effect/platform-node": "catalog:", "@opencode-ai/client": "workspace:*", "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/pty": "0.1.8", "@opencode-ai/schema": "workspace:*", "@opencode-ai/server": "workspace:*", "@opencode-ai/tui": "workspace:*", diff --git a/packages/cli/script/build.ts b/packages/cli/script/build.ts index 9f0ee84fffc..cb482b4f593 100755 --- a/packages/cli/script/build.ts +++ b/packages/cli/script/build.ts @@ -62,7 +62,8 @@ const targets = : allTargets if (!targets.length) throw new Error(`Unknown build target: ${requestedTarget}`) -if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` +if (!skipInstall) + await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]} @opencode-ai/pty@${pkg.dependencies["@opencode-ai/pty"]}` const appArchive = await buildAppArchive(Script.channel, { skipBuild: skipWebUi }) const appAssetsPlugin: BunPlugin = { name: "opencode-app-assets", diff --git a/packages/cli/script/opencode-pty.ts b/packages/cli/script/opencode-pty.ts index 7b5a0169476..feda2e798e9 100644 --- a/packages/cli/script/opencode-pty.ts +++ b/packages/cli/script/opencode-pty.ts @@ -1,20 +1,8 @@ -import { spawnSync } from "node:child_process" import { createHash } from "node:crypto" -import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises" -import os from "node:os" +import { readFile } from "node:fs/promises" +import { createRequire } from "node:module" import path from "node:path" -const VERSION = "0.1.5" -const RELEASE = `https://github.com/anomalyco/opencode-pty/releases/download/v${VERSION}` -const SHA256 = { - "aarch64-apple-darwin": "d5156e44a6783381aadbd968dbd27c1d83e7e0f1b6042c7c934e6d33541d334f", - "aarch64-unknown-linux-gnu": "075d99ffb269cbd0846d3d404fdee93965a53cd6eaf046dbd1064785a7ce9351", - "aarch64-unknown-linux-musl": "22fb55c944ff05fbe03e84de67333e9fd037ad4e04ffc93d8a3f0b2193c29421", - "x86_64-apple-darwin": "773e363b5385c1bd56021e69ada95132efd615ed5b9c3734f878ad644ae22b01", - "x86_64-unknown-linux-gnu": "d9cac2a7c09d013188f696c45ded5eb5764d308e52dd31cb2de68bf4fc675624", - "x86_64-unknown-linux-musl": "2a176302de3d24f8ae3fbacf0b4afce7b4af3e00abd619906187a487b5e50bd6", -} as const - export type OpencodePtyAsset = { readonly source: string readonly version: string @@ -27,76 +15,28 @@ type Target = { readonly libc?: "glibc" | "musl" } -const pending = new Map>() +const core = createRequire(path.resolve(import.meta.dirname, "../../core/package.json")) +const pty = createRequire(core.resolve("@opencode-ai/pty/package.json")) -export function resolveOpencodePty(target: Target) { - const rustTarget = targetName(target) - if (!rustTarget) return Promise.resolve(undefined) - const existing = pending.get(rustTarget) - if (existing) return existing - const result = acquire(rustTarget).catch((error) => { - pending.delete(rustTarget) - throw error - }) - pending.set(rustTarget, result) - return result -} +export async function resolveOpencodePty(target: Target): Promise { + if (target.platform !== "darwin" && target.platform !== "linux") return undefined + if (target.arch !== "arm64" && target.arch !== "x64") return undefined -async function acquire(target: keyof typeof SHA256): Promise { - const root = path.resolve(import.meta.dirname, "../.cache/opencode-pty", VERSION, target) - const executable = path.join(root, "opencode-pty") - const cached = await readFile(executable).catch(() => undefined) - if (cached) - return { - source: executable, - version: VERSION, - sha256: createHash("sha256").update(cached).digest("hex"), - } + const suffix = [target.platform, target.arch, target.platform === "linux" ? (target.libc ?? "glibc") : undefined] + .filter(Boolean) + .map((value) => (value === "glibc" ? "gnu" : value)) + .join("-") + const name = `@opencode-ai/pty-${suffix}` + const source = pty.resolve(`${name}/bin/opencode-pty`) + const manifest: unknown = JSON.parse(await readFile(pty.resolve(`${name}/package.json`), "utf8")) + if (!manifest || typeof manifest !== "object" || !("version" in manifest) || typeof manifest.version !== "string") + throw new Error(`Invalid package metadata for ${name}`) - await mkdir(root, { recursive: true }) - const archiveName = `opencode-pty-${VERSION}-${target}.tar.gz` - const response = await fetch(`${RELEASE}/${archiveName}`) - if (!response.ok) throw new Error(`Failed to download ${archiveName}: ${response.status}`) - const archive = new Uint8Array(await response.arrayBuffer()) - const actual = createHash("sha256").update(archive).digest("hex") - if (actual !== SHA256[target]) throw new Error(`Checksum mismatch for ${archiveName}`) - - const temporary = await mkdtemp(path.join(os.tmpdir(), "opencode-pty-build-")) - try { - const archivePath = path.join(temporary, archiveName) - await writeFile(archivePath, archive) - run("tar", ["-xzf", archivePath, "-C", temporary]) - const source = path.join(temporary, `opencode-pty-${VERSION}-${target}`, "opencode-pty") - const bytes = await readFile(source) - const staged = path.join(root, `opencode-pty.${process.pid}.${crypto.randomUUID()}.tmp`) - await writeFile(staged, bytes, { flag: "wx", mode: 0o755 }) - await rename(staged, executable).catch(async (error) => { - await rm(staged, { force: true }) - if (!(await readFile(executable).catch(() => undefined))) throw error - }) - const installed = await readFile(executable) - return { - source: executable, - version: VERSION, - sha256: createHash("sha256").update(installed).digest("hex"), - } - } finally { - await rm(temporary, { recursive: true, force: true }) + return { + source, + version: manifest.version, + sha256: createHash("sha256") + .update(await readFile(source)) + .digest("hex"), } } - -function targetName(target: Target): keyof typeof SHA256 | undefined { - const arch = target.arch === "arm64" ? "aarch64" : target.arch === "x64" ? "x86_64" : undefined - if (!arch) return undefined - if (target.platform === "darwin") return arch === "aarch64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin" - if (target.platform === "linux" && target.libc === "musl") - return arch === "aarch64" ? "aarch64-unknown-linux-musl" : "x86_64-unknown-linux-musl" - if (target.platform === "linux") return arch === "aarch64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu" - return undefined -} - -function run(command: string, args: readonly string[]) { - const result = spawnSync(command, args, { stdio: "inherit" }) - if (result.error) throw result.error - if (result.status !== 0) throw new Error(`${command} exited with status ${result.status ?? "unknown"}`) -} diff --git a/packages/core/package.json b/packages/core/package.json index c35bac60f06..65c20f14b67 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -119,6 +119,7 @@ "@ff-labs/fff-node": "0.10.5", "@opencode-ai/codemode": "workspace:*", "@opencode-ai/ai": "workspace:*", + "@opencode-ai/pty": "0.1.8", "@opencode-ai/schema": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/util": "workspace:*", diff --git a/packages/core/script/build.ts b/packages/core/script/build.ts index ec62b1d9c1a..f7ea4534aec 100644 --- a/packages/core/script/build.ts +++ b/packages/core/script/build.ts @@ -19,7 +19,16 @@ const result = await Bun.build({ target: "node", format: "esm", packages: "external", - external: ["#sqlite", "#pty", "#fff", "#photon-wasm", "#shell-parser-wasm", "#process-lock-ffi", "#v1-migration"], + external: [ + "#sqlite", + "#pty", + "#persistent-pty-binary", + "#fff", + "#photon-wasm", + "#shell-parser-wasm", + "#process-lock-ffi", + "#v1-migration", + ], splitting: true, loader: { ".txt": "text", @@ -51,16 +60,15 @@ const rewritten = await Promise.all( .replace(eagerRequire, "") if (/\bnew\s+__require\s*\(/.test(generatedUses)) throw new Error(`Unsupported generated require constructor in ${output.path}`) - const unsupported = generatedUses - .replace(/\b__require\.resolve\s*\(/g, "") - .replace(/\b__require\s*\(/g, "") + const unsupported = generatedUses.replace(/\b__require\.resolve\s*\(/g, "").replace(/\b__require\s*\(/g, "") if (/\b__require\b/.test(unsupported)) throw new Error(`Unsupported generated require usage in ${output.path}`) if (!source.includes(eagerRequire)) return false if (source.indexOf(eagerRequire) !== source.lastIndexOf(eagerRequire)) throw new Error(`Multiple eager require helpers in ${output.path}`) const rewrittenSource = source.replace(eagerRequire, lazyRequire) - if (rewrittenSource.includes(eagerRequire)) throw new Error(`Failed to rewrite eager require helper in ${output.path}`) + if (rewrittenSource.includes(eagerRequire)) + throw new Error(`Failed to rewrite eager require helper in ${output.path}`) await Bun.write(output.path, rewrittenSource) return true }), diff --git a/packages/core/src/persistent-pty/binary.bun.ts b/packages/core/src/persistent-pty/binary.bun.ts index 157d91b6b8a..1a7b0a2a04c 100644 --- a/packages/core/src/persistent-pty/binary.bun.ts +++ b/packages/core/src/persistent-pty/binary.bun.ts @@ -6,6 +6,7 @@ import asset from "./pty-binding.js" export async function resolveBinary(bin: string) { if (process.env.OPENCODE_PTY_BIN) return process.env.OPENCODE_PTY_BIN if (!asset) return "opencode-pty" + if (typeof asset === "string") return asset return install(bin, asset) } diff --git a/packages/core/src/persistent-pty/pty-binding.ts b/packages/core/src/persistent-pty/pty-binding.ts index 836959e3b59..d0fe7614408 100644 --- a/packages/core/src/persistent-pty/pty-binding.ts +++ b/packages/core/src/persistent-pty/pty-binding.ts @@ -1,3 +1,6 @@ -const asset: { readonly path: string; readonly version: string; readonly sha256: string } | undefined = undefined +import { binaryPath } from "@opencode-ai/pty" -export default asset +const binding: string | { readonly path: string; readonly version: string; readonly sha256: string } | undefined = + binaryPath + +export default binding