|
| 1 | +import { spawnSync } from "node:child_process" |
| 2 | +import { createHash } from "node:crypto" |
| 3 | +import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises" |
| 4 | +import os from "node:os" |
| 5 | +import path from "node:path" |
| 6 | + |
| 7 | +const VERSION = "0.1.5" |
| 8 | +const RELEASE = `https://github.com/anomalyco/opencode-pty/releases/download/v${VERSION}` |
| 9 | +const SHA256 = { |
| 10 | + "aarch64-apple-darwin": "d5156e44a6783381aadbd968dbd27c1d83e7e0f1b6042c7c934e6d33541d334f", |
| 11 | + "aarch64-unknown-linux-gnu": "075d99ffb269cbd0846d3d404fdee93965a53cd6eaf046dbd1064785a7ce9351", |
| 12 | + "aarch64-unknown-linux-musl": "22fb55c944ff05fbe03e84de67333e9fd037ad4e04ffc93d8a3f0b2193c29421", |
| 13 | + "x86_64-apple-darwin": "773e363b5385c1bd56021e69ada95132efd615ed5b9c3734f878ad644ae22b01", |
| 14 | + "x86_64-unknown-linux-gnu": "d9cac2a7c09d013188f696c45ded5eb5764d308e52dd31cb2de68bf4fc675624", |
| 15 | + "x86_64-unknown-linux-musl": "2a176302de3d24f8ae3fbacf0b4afce7b4af3e00abd619906187a487b5e50bd6", |
| 16 | +} as const |
| 17 | + |
| 18 | +export type OpencodePtyAsset = { |
| 19 | + readonly source: string |
| 20 | + readonly version: string |
| 21 | + readonly sha256: string |
| 22 | +} |
| 23 | + |
| 24 | +type Target = { |
| 25 | + readonly platform: string |
| 26 | + readonly arch: string |
| 27 | + readonly libc?: "glibc" | "musl" |
| 28 | +} |
| 29 | + |
| 30 | +const pending = new Map<string, Promise<OpencodePtyAsset | undefined>>() |
| 31 | + |
| 32 | +export function resolveOpencodePty(target: Target) { |
| 33 | + const rustTarget = targetName(target) |
| 34 | + if (!rustTarget) return Promise.resolve(undefined) |
| 35 | + const existing = pending.get(rustTarget) |
| 36 | + if (existing) return existing |
| 37 | + const result = acquire(rustTarget).catch((error) => { |
| 38 | + pending.delete(rustTarget) |
| 39 | + throw error |
| 40 | + }) |
| 41 | + pending.set(rustTarget, result) |
| 42 | + return result |
| 43 | +} |
| 44 | + |
| 45 | +async function acquire(target: keyof typeof SHA256): Promise<OpencodePtyAsset> { |
| 46 | + const root = path.resolve(import.meta.dirname, "../.cache/opencode-pty", VERSION, target) |
| 47 | + const executable = path.join(root, "opencode-pty") |
| 48 | + const cached = await readFile(executable).catch(() => undefined) |
| 49 | + if (cached) |
| 50 | + return { |
| 51 | + source: executable, |
| 52 | + version: VERSION, |
| 53 | + sha256: createHash("sha256").update(cached).digest("hex"), |
| 54 | + } |
| 55 | + |
| 56 | + await mkdir(root, { recursive: true }) |
| 57 | + const archiveName = `opencode-pty-${VERSION}-${target}.tar.gz` |
| 58 | + const response = await fetch(`${RELEASE}/${archiveName}`) |
| 59 | + if (!response.ok) throw new Error(`Failed to download ${archiveName}: ${response.status}`) |
| 60 | + const archive = new Uint8Array(await response.arrayBuffer()) |
| 61 | + const actual = createHash("sha256").update(archive).digest("hex") |
| 62 | + if (actual !== SHA256[target]) throw new Error(`Checksum mismatch for ${archiveName}`) |
| 63 | + |
| 64 | + const temporary = await mkdtemp(path.join(os.tmpdir(), "opencode-pty-build-")) |
| 65 | + try { |
| 66 | + const archivePath = path.join(temporary, archiveName) |
| 67 | + await writeFile(archivePath, archive) |
| 68 | + run("tar", ["-xzf", archivePath, "-C", temporary]) |
| 69 | + const source = path.join(temporary, `opencode-pty-${VERSION}-${target}`, "opencode-pty") |
| 70 | + const bytes = await readFile(source) |
| 71 | + const staged = path.join(root, `opencode-pty.${process.pid}.${crypto.randomUUID()}.tmp`) |
| 72 | + await writeFile(staged, bytes, { flag: "wx", mode: 0o755 }) |
| 73 | + await rename(staged, executable).catch(async (error) => { |
| 74 | + await rm(staged, { force: true }) |
| 75 | + if (!(await readFile(executable).catch(() => undefined))) throw error |
| 76 | + }) |
| 77 | + const installed = await readFile(executable) |
| 78 | + return { |
| 79 | + source: executable, |
| 80 | + version: VERSION, |
| 81 | + sha256: createHash("sha256").update(installed).digest("hex"), |
| 82 | + } |
| 83 | + } finally { |
| 84 | + await rm(temporary, { recursive: true, force: true }) |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +function targetName(target: Target): keyof typeof SHA256 | undefined { |
| 89 | + const arch = target.arch === "arm64" ? "aarch64" : target.arch === "x64" ? "x86_64" : undefined |
| 90 | + if (!arch) return undefined |
| 91 | + if (target.platform === "darwin") return arch === "aarch64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin" |
| 92 | + if (target.platform === "linux" && target.libc === "musl") |
| 93 | + return arch === "aarch64" ? "aarch64-unknown-linux-musl" : "x86_64-unknown-linux-musl" |
| 94 | + if (target.platform === "linux") return arch === "aarch64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu" |
| 95 | + return undefined |
| 96 | +} |
| 97 | + |
| 98 | +function run(command: string, args: readonly string[]) { |
| 99 | + const result = spawnSync(command, args, { stdio: "inherit" }) |
| 100 | + if (result.error) throw result.error |
| 101 | + if (result.status !== 0) throw new Error(`${command} exited with status ${result.status ?? "unknown"}`) |
| 102 | +} |
0 commit comments