mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-11 11:51:14 +00:00
Fixes #1579. ## Problem The release executable is built with `bun build --compile`. By default a compiled Bun executable loads `.env`, `.env.local` and `bunfig.toml` from its working directory at startup. The editor starts the agent in the user's project, so: - The project's dotenv values land in the agent's `process.env` and are passed on to Claude and every Bash command as real environment variables. Most frameworks never override a variable that is already set, so the project's own `.env.test` is silently ignored. In the reported case this made a test suite drop the dev database. - A `bunfig.toml` in the project that Bun cannot parse stops the agent from starting (`SyntaxError: An error occurred while parsing code`, exit 1). Only the executable is affected; running the npm package under Node does not load these files. ## Fix `scripts/build-binary.mjs` passes `--no-compile-autoload-dotenv` and `--no-compile-autoload-bunfig`. Bun's other two autoloads (`tsconfig.json`, `package.json`) are already off by default. ## Testing `scripts/smoke-binary.mjs` gains a check that runs the executable in a directory containing a `.env`, a `.env.local` and an unparseable `bunfig.toml`, with Node standing in for `claude` to print the environment the CLI is spawned with. It fails on an executable built without the flags and passes with them. Run locally on macOS arm64 only; the Linux, Alpine and Windows targets are covered by the `binaries.yml` matrix.
108 lines
4.0 KiB
JavaScript
108 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
// Builds an executable of the agent with `bun build --compile` and copies the
|
|
// target platform's Claude CLI next to it. Both files go in the release
|
|
// archive: the executable runs the CLI from its own directory.
|
|
//
|
|
// node scripts/build-binary.mjs [--target bun-linux-x64-musl] [--outfile path]
|
|
//
|
|
// --target defaults to the host. Cross-compiling needs the target's SDK
|
|
// platform package installed; the script prints the install command when it
|
|
// is missing.
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { chmodSync, copyFileSync, readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { parseArgs } from "node:util";
|
|
|
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
function hostTarget() {
|
|
const os = process.platform === "win32" ? "windows" : process.platform;
|
|
// Node linked against glibc reports its version; musl builds don't.
|
|
const musl =
|
|
process.platform === "linux" && !process.report.getReport().header.glibcVersionRuntime;
|
|
return `bun-${os}-${process.arch}${musl ? "-musl" : ""}`;
|
|
}
|
|
|
|
const { values } = parseArgs({
|
|
options: {
|
|
target: { type: "string", default: hostTarget() },
|
|
outfile: { type: "string" },
|
|
},
|
|
});
|
|
const target = values.target;
|
|
|
|
// Bun targets look like `bun-<os>-<arch>` with optional `-musl`, `-baseline` or
|
|
// `-modern` suffixes; the SDK names its packages `<platform>-<arch>[-musl]`.
|
|
const match = /^bun-(linux|darwin|windows)-(x64|arm64|aarch64)(?:-.+)?$/.exec(target);
|
|
if (!match) {
|
|
console.error(
|
|
`Unsupported target "${target}". Expected e.g. bun-linux-x64, bun-darwin-arm64, bun-windows-x64.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const [, os, rawArch] = match;
|
|
const platform = os === "windows" ? "win32" : os;
|
|
const arch = rawArch === "aarch64" ? "arm64" : rawArch;
|
|
const musl = target.split("-").includes("musl");
|
|
const exe = os === "windows" ? ".exe" : "";
|
|
const cliPackage = `@anthropic-ai/claude-agent-sdk-${platform}-${arch}${musl ? "-musl" : ""}`;
|
|
const cliSpecifier = `${cliPackage}/claude${exe}`;
|
|
const outfile = resolve(values.outfile ?? join(repoRoot, "dist-bin", `claude-agent-acp${exe}`));
|
|
|
|
// Resolve from the SDK, the way claudeCliPath() does at runtime, so a nested
|
|
// install is checked rather than only a hoisted one.
|
|
const sdkRequire = createRequire(
|
|
createRequire(join(repoRoot, "package.json")).resolve("@anthropic-ai/claude-agent-sdk"),
|
|
);
|
|
let cliPath;
|
|
try {
|
|
cliPath = sdkRequire.resolve(cliSpecifier);
|
|
} catch {
|
|
const sdkVersion = JSON.parse(
|
|
readFileSync(
|
|
join(dirname(sdkRequire.resolve("@anthropic-ai/claude-agent-sdk")), "package.json"),
|
|
"utf8",
|
|
),
|
|
).version;
|
|
console.error(
|
|
`${cliSpecifier} is not installed. npm only installs the host's platform package; for ${target} run:\n\n` +
|
|
` npm install --no-save --force ${cliPackage}@${sdkVersion}\n`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// By default a compiled executable loads `.env`, `.env.local` and `bunfig.toml`
|
|
// from its working directory at startup. That directory is the user's project:
|
|
// its dotenv values would reach Claude and every Bash command as real
|
|
// environment variables (overriding the project's own `.env.test`), and a
|
|
// `bunfig.toml` that fails to parse would stop the agent from starting.
|
|
const bun = spawnSync(
|
|
"bun",
|
|
[
|
|
"build",
|
|
"src/binary-entry.ts",
|
|
"--compile",
|
|
"--no-compile-autoload-dotenv",
|
|
"--no-compile-autoload-bunfig",
|
|
"--target",
|
|
target,
|
|
"--outfile",
|
|
outfile,
|
|
],
|
|
{ cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32" },
|
|
);
|
|
if (bun.error) {
|
|
console.error(`Could not run bun: ${bun.error.message}. Install it from https://bun.sh.`);
|
|
process.exit(1);
|
|
}
|
|
if (bun.status !== 0) process.exit(bun.status ?? 1);
|
|
|
|
// The executable runs the CLI from its own directory (src/binary-entry.ts).
|
|
const cliOutfile = join(dirname(outfile), `claude${exe}`);
|
|
copyFileSync(cliPath, cliOutfile);
|
|
chmodSync(cliOutfile, 0o755);
|
|
console.log(`Copied ${cliSpecifier} to ${cliOutfile}`);
|