5acc917591
Applies findings from the third simplify pass: 1. \`cliArgv\` is a module-level const, not a function — prebuiltCli is read once at module init and never mutated, so the per-spawn function allocation was pure overhead. 2. Help-snapshot failures surface via \`Effect.fail\` instead of \`throw\`, symmetric with the \`Effect.fail\` already inside the partition above. Keeps the failure typed in the Effect channel rather than as a defect. 3. \`prebuild-test-cli.ts\` skips the build when the binary is already newer than every file in src/ — saves the 2.8s rebuild cost on every subsequent invocation. Pass --force to bypass. 4. \`prebuild-test-cli.ts\` verifies the built binary is executable before symlinking — catches a silently-failed build leaving stale output instead of letting tests fail with confusing exec errors later. Verified: 331/331 CLI tests pass; typecheck clean; skip-if-fresh + --force behave as documented.
96 lines
3.9 KiB
TypeScript
96 lines
3.9 KiB
TypeScript
#!/usr/bin/env bun
|
|
// Build a pre-compiled `opencode` binary for subprocess tests, then expose
|
|
// it at `dist/test-cli/bin/opencode` for the harness to consume.
|
|
//
|
|
// Why: each `bun run --conditions=browser src/index.ts <cmd>` spawn pays
|
|
// ~15s of JIT + plugin init + DB migration in isolation mode. The
|
|
// pre-compiled binary cuts that to ~5s — a 3x improvement on subprocess
|
|
// tests that touch the DB (mcp, providers list, etc.).
|
|
//
|
|
// Usage:
|
|
// bun script/prebuild-test-cli.ts
|
|
// export OPENCODE_TEST_CLI_PATH="$PWD/dist/test-cli/bin/opencode"
|
|
// bun test test/cli/
|
|
//
|
|
// The harness (see test/lib/cli-process.ts) reads OPENCODE_TEST_CLI_PATH; if
|
|
// set, it spawns the binary directly instead of `bun run src/index.ts`. If
|
|
// unset, it falls back to dev mode — so this script is strictly opt-in.
|
|
//
|
|
// Build cost amortizes after ~1 spawn that touches the DB. Recommended for
|
|
// CI, manual `bun test test/cli/` runs, and any local iteration where the
|
|
// CLI surface itself isn't under change. Skip for normal src/* editing — the
|
|
// dev path picks up source changes without rebuild.
|
|
import { $ } from "bun"
|
|
import fs from "node:fs/promises"
|
|
import path from "node:path"
|
|
|
|
const dir = path.resolve(import.meta.dirname, "..")
|
|
process.chdir(dir)
|
|
|
|
const platform = process.platform === "win32" ? "win32" : process.platform === "darwin" ? "darwin" : "linux"
|
|
const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x64"
|
|
const targetDir = path.join(dir, "dist", `opencode-${platform}-${arch}`)
|
|
const binaryName = process.platform === "win32" ? "opencode.exe" : "opencode"
|
|
const builtBinary = path.join(targetDir, "bin", binaryName)
|
|
const stableBinary = path.join(dir, "dist", "test-cli", "bin", binaryName)
|
|
|
|
const force = process.argv.includes("--force")
|
|
|
|
// Walk src/ and return the newest mtime seen. Faster than `git status` for
|
|
// the freshness check and works for uncommitted edits. Returns 0 on error
|
|
// so a missing src/ tree forces a rebuild via the comparison below.
|
|
async function newestMtimeMs(root: string): Promise<number> {
|
|
let max = 0
|
|
async function walk(p: string) {
|
|
let entries: { name: string; isDirectory: () => boolean; isFile: () => boolean }[]
|
|
try {
|
|
entries = await fs.readdir(p, { withFileTypes: true })
|
|
} catch {
|
|
return
|
|
}
|
|
for (const entry of entries) {
|
|
const full = path.join(p, entry.name)
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === "node_modules" || entry.name === "dist") continue
|
|
await walk(full)
|
|
} else if (entry.isFile()) {
|
|
const stat = await fs.stat(full).catch(() => null)
|
|
if (stat && stat.mtimeMs > max) max = stat.mtimeMs
|
|
}
|
|
}
|
|
}
|
|
await walk(root)
|
|
return max
|
|
}
|
|
|
|
async function fresh(): Promise<boolean> {
|
|
const binStat = await fs.stat(builtBinary).catch(() => null)
|
|
if (!binStat) return false
|
|
const srcMs = await newestMtimeMs(path.join(dir, "src"))
|
|
return binStat.mtimeMs > srcMs
|
|
}
|
|
|
|
if (!force && (await fresh())) {
|
|
console.log(`Test CLI binary is up to date: ${builtBinary}`)
|
|
} else {
|
|
console.log(`Building test CLI binary for ${platform}-${arch}...`)
|
|
const start = Date.now()
|
|
await $`bun script/build.ts --single --skip-embed-web-ui --skip-install`
|
|
console.log(`Build complete in ${Date.now() - start}ms: ${builtBinary}`)
|
|
}
|
|
|
|
// Verify the binary exists and is executable before symlinking — catches
|
|
// a silently-failed build that left a stale or partial output behind.
|
|
await fs.access(builtBinary, fs.constants.X_OK).catch(() => {
|
|
throw new Error(`Built binary missing or not executable: ${builtBinary}`)
|
|
})
|
|
|
|
await fs.mkdir(path.dirname(stableBinary), { recursive: true })
|
|
await fs.rm(stableBinary, { force: true })
|
|
await fs.symlink(builtBinary, stableBinary)
|
|
console.log(`Symlinked stable path: ${stableBinary}`)
|
|
console.log(``)
|
|
console.log(`To use in tests:`)
|
|
console.log(` export OPENCODE_TEST_CLI_PATH="${stableBinary}"`)
|
|
console.log(` bun test test/cli/`)
|