mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
feat(desktop): pin and stage bundled ACP bridge tools
Buzz desktop spawned the claude-agent-acp and codex-acp bridges from whatever the user had installed globally — an unpinned `npm install -g` surface with no integrity checking, which produced stale-bridge drift (the deprecated @zed-industries/codex-acp 0.16.x gate) and missing-tool failures. Bundle both bridges as app resources instead, pinned per target: - desktop/acp-tools.lock.json pins @agentclientprotocol/claude-agent-acp 0.58.1 and @agentclientprotocol/codex-acp 1.1.2 (npm `latest` at time of commit) for the four supported targets, with integrity hashes and Block Artifactory tarballs for the package, its claude-agent-sdk / @openai/codex dependency, and the per-target native package. - desktop/scripts/update-acp-tools-lock.mjs regenerates the lock from the registry's `latest` dist-tags, failing loudly on any unresolvable package — never silently pinning an older version. Ranged dependencies (codex-acp's ^0.144.0) resolve to the highest matching version when `npm view` returns an array. - desktop/scripts/ensure-acp-tools.sh installs the locked tools into a shared dev cache (~/Library/Caches/buzz-dev/acp-tools), validates versions + integrity against the lock, and stamps staged binaries next to the shared bin dir so any lock change — including a revert — forces a re-stage; binaries no longer in the lock are pruned. - desktop/scripts/prepare-acp-tools-resource.sh stages the vendored npm trees + node wrapper shims into desktop/src-tauri/resources/acp, writes the node-runtime.json manifest for the app's Node.js doctor check, and ad-hoc signs every nested Mach-O (file(1) scan — the codex package vendors rg, zsh, and codex-code-mode-host beyond the main CLIs) so Gatekeeper doesn't kill them in local builds. - desktop/scripts/lib/acp-node-wrapper.sh is the single wrapper-shim generator shared by both scripts, so the dev-cache and bundled wrappers (and the Node major they enforce) cannot drift. - Wiring: `just dev` / `just staging` stage the resources and export BUZZ_ACP_TOOLS_DIR; `just desktop-release-build` stages per target before `tauri build`; `just bump-acp-tools` reruns the lock updater; tauri.conf.json bundles resources/acp; staged artifacts are gitignored with a .gitkeep placeholder. Runtime resolution of the staged dir follows in the next commit. The sprout-releases internal pipeline will need the same staging step before its `tauri build`; that lives in a separate repo. Ports the build-time half of the Staged implementation in block/builderbot#876 (branch commits 16b115a7 bundle feature, f5c6bba9 re-stage after lock revert, 288fa7eb shared node wrapper lib, adea4017 node-runtime manifest, 5124302a sign all nested Mach-Os, de6a9782 ranged-dependency updater fix), itself ported from squareup/berd f07df1d2 + 1db993fb + 24d7518b + 07087303 + 737e33a5. Verified: update-acp-tools-lock.mjs regenerated the lock against the Block registry (byte-identical pins to the donor lock; both packages confirmed at npm `latest`); fresh stage installs both tools and the staged wrappers report 0.58.1 / 1.1.2; no-op re-run performs zero npm installs; a stamp/lock mismatch forces a re-stage and stray binaries are pruned; all five staged Mach-Os pass `codesign --verify`; cargo check on desktop/src-tauri passes with the new resources entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -38,6 +38,12 @@ node_modules/
|
||||
# Root npm lockfiles are accidental; desktop uses pnpm in /desktop.
|
||||
/package-lock.json
|
||||
|
||||
# Bundled ACP bridge tools (regenerated by desktop/scripts/prepare-acp-tools-resource.sh)
|
||||
desktop/src-tauri/resources/acp/bin/*
|
||||
!desktop/src-tauri/resources/acp/bin/.gitkeep
|
||||
desktop/src-tauri/resources/acp/node/
|
||||
desktop/src-tauri/resources/acp/node-runtime.json
|
||||
|
||||
# sqlx offline query data (generated, not portable)
|
||||
.sqlx/
|
||||
|
||||
|
||||
15
Justfile
15
Justfile
@@ -121,6 +121,10 @@ desktop-test:
|
||||
desktop-typecheck:
|
||||
cd {{desktop_dir}} && pnpm typecheck
|
||||
|
||||
# Query the latest npm releases of the bundled ACP bridge tools and update desktop/acp-tools.lock.json
|
||||
bump-acp-tools *ARGS:
|
||||
node desktop/scripts/update-acp-tools-lock.mjs {{ARGS}}
|
||||
|
||||
# Build desktop frontend assets
|
||||
desktop-build:
|
||||
cd {{desktop_dir}} && pnpm build
|
||||
@@ -206,6 +210,7 @@ desktop-release-build target="aarch64-apple-darwin":
|
||||
touch "desktop/src-tauri/binaries/buzz-dev-mcp-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET"
|
||||
touch "desktop/src-tauri/binaries/buzz-$TARGET"
|
||||
./desktop/scripts/prepare-acp-tools-resource.sh "$TARGET"
|
||||
pnpm install
|
||||
cd {{desktop_dir}} && pnpm tauri build --features mesh-llm --target {{target}}
|
||||
|
||||
@@ -330,6 +335,11 @@ dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations
|
||||
done
|
||||
fi
|
||||
cargo build -p buzz-acp -p buzz-agent -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr -p buzz-relay
|
||||
# Stage the pinned ACP bridge tools and point the app at the staged dir so
|
||||
# dev builds resolve the same bundled bridges as packaged builds.
|
||||
./desktop/scripts/prepare-acp-tools-resource.sh
|
||||
export BUZZ_ACP_TOOLS_DIR="{{justfile_directory()}}/desktop/src-tauri/resources/acp/bin"
|
||||
echo "Using ACP tools dir: ${BUZZ_ACP_TOOLS_DIR}"
|
||||
./target/debug/buzz-relay &
|
||||
RELAY_PID=$!
|
||||
sleep 1
|
||||
@@ -358,6 +368,11 @@ staging *ARGS: bootstrap _ensure-sidecar-stubs
|
||||
export PATH="{{justfile_directory()}}/bin:$PATH"
|
||||
pnpm install # unconditional: staging must always start with a clean dep tree
|
||||
cargo build --release -p buzz-acp -p buzz-agent -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr
|
||||
# Stage the pinned ACP bridge tools and point the app at the staged dir so
|
||||
# staging builds resolve the same bundled bridges as packaged builds.
|
||||
./desktop/scripts/prepare-acp-tools-resource.sh
|
||||
export BUZZ_ACP_TOOLS_DIR="{{justfile_directory()}}/desktop/src-tauri/resources/acp/bin"
|
||||
echo "Using ACP tools dir: ${BUZZ_ACP_TOOLS_DIR}"
|
||||
FEATURES=()
|
||||
if [[ -n "{{mesh}}" ]]; then
|
||||
FEATURES=(--features mesh-llm)
|
||||
|
||||
196
desktop/acp-tools.lock.json
Normal file
196
desktop/acp-tools.lock.json
Normal file
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"id": "claude-acp",
|
||||
"binary": "claude-agent-acp",
|
||||
"source": "npm",
|
||||
"package": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.58.1",
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "aarch64-apple-darwin",
|
||||
"npmOs": "darwin",
|
||||
"npmCpu": "arm64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-darwin-arm64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-darwin-arm64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-lrfJ4eVtzfPkCpbSkBOGSMQCBbvmW6nbPzgHE4IwMN3scZlpuFMUFqh2aaJa/X2SAcWD9H2S0t2WWvSRgM7BjA==",
|
||||
"nativeTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude"
|
||||
},
|
||||
{
|
||||
"id": "claude-acp",
|
||||
"binary": "claude-agent-acp",
|
||||
"source": "npm",
|
||||
"package": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.58.1",
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "aarch64-unknown-linux-gnu",
|
||||
"npmOs": "linux",
|
||||
"npmCpu": "arm64",
|
||||
"npmLibc": "glibc",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-linux-arm64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-linux-arm64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-CXzySK3PV3EizCRPXnxPqeaAtgrBFDnMFOVpMe36oC3U16yDb1b1tAJGqZi/7uFrVvAiaXvnSFxhUWnDDSaO+A==",
|
||||
"nativeTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude"
|
||||
},
|
||||
{
|
||||
"id": "claude-acp",
|
||||
"binary": "claude-agent-acp",
|
||||
"source": "npm",
|
||||
"package": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.58.1",
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "x86_64-apple-darwin",
|
||||
"npmOs": "darwin",
|
||||
"npmCpu": "x64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-darwin-x64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-darwin-x64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-G6ETPmL5mNzJ2DFsWxG3jmsmrXgZX1N2ZCJvxaGUUpjTsKZJ4Tup1cWYvcd/m7o5fYZmx9REmgzTwsAIc1fdPQ==",
|
||||
"nativeTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude"
|
||||
},
|
||||
{
|
||||
"id": "claude-acp",
|
||||
"binary": "claude-agent-acp",
|
||||
"source": "npm",
|
||||
"package": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.58.1",
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "x86_64-unknown-linux-gnu",
|
||||
"npmOs": "linux",
|
||||
"npmCpu": "x64",
|
||||
"npmLibc": "glibc",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-linux-x64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-linux-x64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-siS+1iNqBSlGFZZvJY6+mhzZ/6/ec/TbX9GMuwmTF0E6fxGhIIp797jJxR1q8r6FAq7d39mEoRNhC0Ffo60uNQ==",
|
||||
"nativeTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
"binary": "codex-acp",
|
||||
"source": "npm",
|
||||
"package": "@agentclientprotocol/codex-acp",
|
||||
"version": "1.1.2",
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "aarch64-apple-darwin",
|
||||
"npmOs": "darwin",
|
||||
"npmCpu": "arm64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-darwin-arm64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-darwin-arm64",
|
||||
"nativeIntegrity": "sha512-dABeDK+ATqMG54MGBd3VjpKfh5EOoqx9PKVQB2QYDaEXx3F6CdUCXue5QIMfr4OxziUj8pUcLAQyd+KFqiTUFw==",
|
||||
"nativeTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@openai/codex/-/codex-0.144.1-darwin-arm64.tgz",
|
||||
"nativeExecutable": "vendor/aarch64-apple-darwin/bin/codex"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
"binary": "codex-acp",
|
||||
"source": "npm",
|
||||
"package": "@agentclientprotocol/codex-acp",
|
||||
"version": "1.1.2",
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "aarch64-unknown-linux-gnu",
|
||||
"npmOs": "linux",
|
||||
"npmCpu": "arm64",
|
||||
"npmLibc": "glibc",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-linux-arm64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-linux-arm64",
|
||||
"nativeIntegrity": "sha512-451o15+XtaXCCb35t/KCyyPqXHnTPxPxtdqEYOnE3e4sH5AfnI/uVJwfdjOksMG6vRLy6R+fLvSDOMguRFLmQw==",
|
||||
"nativeTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@openai/codex/-/codex-0.144.1-linux-arm64.tgz",
|
||||
"nativeExecutable": "vendor/aarch64-unknown-linux-musl/bin/codex"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
"binary": "codex-acp",
|
||||
"source": "npm",
|
||||
"package": "@agentclientprotocol/codex-acp",
|
||||
"version": "1.1.2",
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "x86_64-apple-darwin",
|
||||
"npmOs": "darwin",
|
||||
"npmCpu": "x64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-darwin-x64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-darwin-x64",
|
||||
"nativeIntegrity": "sha512-K2g3Q3tNxzFhV0SuzO6HcsYK7EQrp/o4HyeReyhkwVrwwUPoYwyIbB0IRjHIiDzRhbKriDccid2iyF5aPqdTcg==",
|
||||
"nativeTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@openai/codex/-/codex-0.144.1-darwin-x64.tgz",
|
||||
"nativeExecutable": "vendor/x86_64-apple-darwin/bin/codex"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
"binary": "codex-acp",
|
||||
"source": "npm",
|
||||
"package": "@agentclientprotocol/codex-acp",
|
||||
"version": "1.1.2",
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "x86_64-unknown-linux-gnu",
|
||||
"npmOs": "linux",
|
||||
"npmCpu": "x64",
|
||||
"npmLibc": "glibc",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-linux-x64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-linux-x64",
|
||||
"nativeIntegrity": "sha512-HNGVI+BulrOaC/0IzBvd6EL62j7LrlbFKibrhw6hZjjCjAeUYzRB2jB4qDzXN1NfqDi6Xrvniof3kwbwab24lg==",
|
||||
"nativeTarball": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@openai/codex/-/codex-0.144.1-linux-x64.tgz",
|
||||
"nativeExecutable": "vendor/x86_64-unknown-linux-musl/bin/codex"
|
||||
}
|
||||
]
|
||||
}
|
||||
363
desktop/scripts/ensure-acp-tools.sh
Executable file
363
desktop/scripts/ensure-acp-tools.sh
Executable file
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
app_root="$(cd "$script_dir/.." && pwd)"
|
||||
lock_file="${ACP_TOOLS_LOCK_FILE:-$app_root/acp-tools.lock.json}"
|
||||
|
||||
# shellcheck source=lib/acp-node-wrapper.sh
|
||||
source "$script_dir/lib/acp-node-wrapper.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: desktop/scripts/ensure-acp-tools.sh [--target <target-triple>] [--print-bin-dir]
|
||||
|
||||
Installs the ACP bridge tools pinned in acp-tools.lock.json into the shared
|
||||
Buzz dev cache. The lockfile is target-specific; only entries matching the
|
||||
requested target are prepared. Each tool is installed as a vendored npm
|
||||
package tree with a small executable wrapper, validated against the locked
|
||||
versions and integrity hashes.
|
||||
|
||||
Environment variables:
|
||||
ACP_TOOLS_LOCK_FILE lockfile path (default: desktop/acp-tools.lock.json)
|
||||
ACP_TOOLS_CACHE_DIR cache dir override
|
||||
USAGE
|
||||
}
|
||||
|
||||
default_cache_root() {
|
||||
if [[ -n "${XDG_CACHE_HOME:-}" ]]; then
|
||||
printf '%s/buzz-dev/acp-tools\n' "$XDG_CACHE_HOME"
|
||||
return
|
||||
fi
|
||||
case "$(uname -s)" in
|
||||
Darwin) printf '%s/Library/Caches/buzz-dev/acp-tools\n' "$HOME" ;;
|
||||
*) printf '%s/.cache/buzz-dev/acp-tools\n' "$HOME" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
target=""
|
||||
print_bin_dir=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--target)
|
||||
target="${2:-}"
|
||||
[[ -n "$target" ]] || { echo "--target requires a value" >&2; exit 1; }
|
||||
shift 2
|
||||
;;
|
||||
--print-bin-dir)
|
||||
print_bin_dir=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
target="$(rustc -vV | sed -n 's|host: ||p')"
|
||||
fi
|
||||
if [[ -z "$target" ]]; then
|
||||
echo "Could not determine rust host target. Pass --target explicitly." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cache_root="${ACP_TOOLS_CACHE_DIR:-$(default_cache_root)}"
|
||||
bin_dir="$cache_root/bin/$target"
|
||||
|
||||
if [[ ! -f "$lock_file" ]]; then
|
||||
echo "ACP tools lockfile not found: $lock_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_tool() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Required tool missing: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_tool node
|
||||
require_tool npm
|
||||
|
||||
lock_entries="$(node - "$lock_file" "$target" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const [lockFile, target] = process.argv.slice(2);
|
||||
const data = JSON.parse(fs.readFileSync(lockFile, "utf8"));
|
||||
const entries = (data.tools ?? []).filter((tool) => tool.target === target);
|
||||
function requireString(entry, field) {
|
||||
if (typeof entry[field] !== "string" || entry[field].trim() === "") {
|
||||
throw new Error(`Invalid ACP tool lock entry for ${entry.id ?? "(unknown)"}: missing ${field}`);
|
||||
}
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.source !== "npm") {
|
||||
throw new Error(`Invalid ACP tool lock entry for ${entry.id}: unsupported source ${entry.source}`);
|
||||
}
|
||||
for (const field of [
|
||||
"id",
|
||||
"binary",
|
||||
"target",
|
||||
"package",
|
||||
"version",
|
||||
"integrity",
|
||||
"tarball",
|
||||
"npmOs",
|
||||
"npmCpu",
|
||||
"dependencyPackage",
|
||||
"dependencyVersion",
|
||||
"dependencyIntegrity",
|
||||
"dependencyTarball",
|
||||
"nativePackage",
|
||||
"nativePackageName",
|
||||
"nativeVersion",
|
||||
"nativeIntegrity",
|
||||
"nativeTarball",
|
||||
"nativeExecutable",
|
||||
]) {
|
||||
requireString(entry, field);
|
||||
}
|
||||
}
|
||||
process.stdout.write(JSON.stringify(entries));
|
||||
NODE
|
||||
)"
|
||||
|
||||
entry_count="$(node -e 'process.stdout.write(String(JSON.parse(process.argv[1]).length))' "$lock_entries")"
|
||||
mkdir -p "$bin_dir"
|
||||
if [[ "$entry_count" == "0" ]]; then
|
||||
find "$bin_dir" -type f -delete
|
||||
if [[ "$print_bin_dir" == "1" ]]; then
|
||||
printf '%s\n' "$bin_dir"
|
||||
else
|
||||
echo "No ACP tools locked for target $target."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
validate_npm_install() {
|
||||
local install_dir="$1"
|
||||
local package="$2"
|
||||
local version="$3"
|
||||
local integrity="$4"
|
||||
local dependency_package="$5"
|
||||
local dependency_version="$6"
|
||||
local dependency_integrity="$7"
|
||||
local native_package="$8"
|
||||
local native_package_name="$9"
|
||||
local native_version="${10}"
|
||||
local native_integrity="${11}"
|
||||
local native_executable="${12}"
|
||||
local claude_code_version="${13}"
|
||||
|
||||
node - "$install_dir" "$package" "$version" "$integrity" "$dependency_package" "$dependency_version" "$dependency_integrity" "$native_package" "$native_package_name" "$native_version" "$native_integrity" "$native_executable" "$claude_code_version" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const [
|
||||
installDir,
|
||||
packageName,
|
||||
expectedVersion,
|
||||
expectedIntegrity,
|
||||
dependencyPackageName,
|
||||
expectedDependencyVersion,
|
||||
expectedDependencyIntegrity,
|
||||
nativePackageName,
|
||||
expectedNativePackageName,
|
||||
expectedNativeVersion,
|
||||
expectedNativeIntegrity,
|
||||
nativeExecutable,
|
||||
expectedClaudeCodeVersion,
|
||||
] = process.argv.slice(2);
|
||||
|
||||
function packagePath(name, ...segments) {
|
||||
return path.join(installDir, "node_modules", ...name.split("/"), ...segments);
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, label) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${label}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
function packageLockEntry(lock, packageName) {
|
||||
const suffix = `node_modules/${packageName}`;
|
||||
const match = Object.entries(lock.packages ?? {}).find(([key]) => key === suffix || key.endsWith(`/${suffix}`));
|
||||
if (!match) {
|
||||
throw new Error(`package-lock entry not found for ${packageName}`);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
const packageJson = readJson(packagePath(packageName, "package.json"));
|
||||
assertEqual(packageJson.name, packageName, `${packageName} name`);
|
||||
assertEqual(packageJson.version, expectedVersion, `${packageName} version`);
|
||||
|
||||
const lock = readJson(path.join(installDir, "package-lock.json"));
|
||||
assertEqual(packageLockEntry(lock, packageName).integrity, expectedIntegrity, `${packageName} integrity`);
|
||||
|
||||
const dependencyPackageJson = readJson(packagePath(dependencyPackageName, "package.json"));
|
||||
assertEqual(
|
||||
dependencyPackageJson.name,
|
||||
dependencyPackageName,
|
||||
`${dependencyPackageName} name`,
|
||||
);
|
||||
assertEqual(
|
||||
dependencyPackageJson.version,
|
||||
expectedDependencyVersion,
|
||||
`${dependencyPackageName} version`,
|
||||
);
|
||||
if (expectedClaudeCodeVersion && expectedClaudeCodeVersion !== "null") {
|
||||
assertEqual(
|
||||
dependencyPackageJson.claudeCodeVersion,
|
||||
expectedClaudeCodeVersion,
|
||||
`${dependencyPackageName} claudeCodeVersion`,
|
||||
);
|
||||
}
|
||||
assertEqual(
|
||||
packageLockEntry(lock, dependencyPackageName).integrity,
|
||||
expectedDependencyIntegrity,
|
||||
`${dependencyPackageName} integrity`,
|
||||
);
|
||||
|
||||
const nativePackageJson = readJson(packagePath(nativePackageName, "package.json"));
|
||||
assertEqual(
|
||||
nativePackageJson.name,
|
||||
expectedNativePackageName,
|
||||
`${nativePackageName} package name`,
|
||||
);
|
||||
assertEqual(
|
||||
nativePackageJson.version,
|
||||
expectedNativeVersion,
|
||||
`${nativePackageName} version`,
|
||||
);
|
||||
fs.accessSync(packagePath(nativePackageName, nativeExecutable), fs.constants.X_OK);
|
||||
assertEqual(
|
||||
packageLockEntry(lock, nativePackageName).integrity,
|
||||
expectedNativeIntegrity,
|
||||
`${nativePackageName} integrity`,
|
||||
);
|
||||
NODE
|
||||
}
|
||||
|
||||
node -e '
|
||||
const entries = JSON.parse(process.argv[1]);
|
||||
for (const entry of entries) {
|
||||
console.log([
|
||||
entry.id,
|
||||
entry.binary,
|
||||
entry.package,
|
||||
entry.version,
|
||||
entry.integrity,
|
||||
entry.tarball,
|
||||
entry.npmOs,
|
||||
entry.npmCpu,
|
||||
entry.npmLibc ?? "",
|
||||
entry.nodeEngine ?? ">=22",
|
||||
entry.dependencyPackage,
|
||||
entry.dependencyVersion,
|
||||
entry.dependencyIntegrity,
|
||||
entry.dependencyTarball,
|
||||
entry.nativePackage,
|
||||
entry.nativePackageName,
|
||||
entry.nativeVersion,
|
||||
entry.nativeIntegrity,
|
||||
entry.nativeTarball,
|
||||
entry.nativeExecutable,
|
||||
entry.claudeCodeVersion ?? "",
|
||||
].join("\x1f"));
|
||||
}
|
||||
' "$lock_entries" | while IFS=$'\x1f' read -r id binary package version integrity tarball npm_os npm_cpu npm_libc node_engine dependency_package dependency_version dependency_integrity dependency_tarball native_package native_package_name native_version native_integrity native_tarball native_executable claude_code_version; do
|
||||
[[ -n "$id" ]] || continue
|
||||
|
||||
tool_dir="$cache_root/$target/$id/$version"
|
||||
install_dir="$tool_dir/npm"
|
||||
package_dir="$install_dir/node_modules/$package"
|
||||
entrypoint="$package_dir/dist/index.js"
|
||||
native_binary="$install_dir/node_modules/$native_package/$native_executable"
|
||||
staged_bin="$bin_dir/$binary"
|
||||
# The staged output is shared across lock versions, so its freshness stamp
|
||||
# must live next to it, not in the per-version tool_dir: a per-version stamp
|
||||
# stays self-consistent after a lock revert and would skip re-staging.
|
||||
stamp="$staged_bin.stamp"
|
||||
if [[ -x "$staged_bin" && -f "$stamp" && -f "$entrypoint" && -x "$native_binary" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$stamp"
|
||||
if [[ "${STAMP_PACKAGE:-}" == "$package" && "${STAMP_VERSION:-}" == "$version" && "${STAMP_INTEGRITY:-}" == "$integrity" && "${STAMP_DEPENDENCY_PACKAGE:-}" == "$dependency_package" && "${STAMP_DEPENDENCY_VERSION:-}" == "$dependency_version" && "${STAMP_DEPENDENCY_INTEGRITY:-}" == "$dependency_integrity" && "${STAMP_NATIVE_PACKAGE:-}" == "$native_package" && "${STAMP_NATIVE_PACKAGE_NAME:-}" == "$native_package_name" && "${STAMP_NATIVE_VERSION:-}" == "$native_version" && "${STAMP_NATIVE_INTEGRITY:-}" == "$native_integrity" && "${STAMP_NATIVE_EXECUTABLE:-}" == "$native_executable" ]]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Installing ACP tool $id $version from npm for $target..." >&2
|
||||
rm -rf "$install_dir"
|
||||
mkdir -p "$install_dir" "$bin_dir"
|
||||
npm_args=(
|
||||
install
|
||||
--prefix "$install_dir"
|
||||
--omit=dev
|
||||
--include=optional
|
||||
--ignore-scripts
|
||||
--no-audit
|
||||
--no-fund
|
||||
--os "$npm_os"
|
||||
--cpu "$npm_cpu"
|
||||
)
|
||||
if [[ -n "$npm_libc" ]]; then
|
||||
npm_args+=(--libc "$npm_libc")
|
||||
fi
|
||||
npm_args+=("$package@$version")
|
||||
npm "${npm_args[@]}" >&2
|
||||
|
||||
validate_npm_install "$install_dir" "$package" "$version" "$integrity" "$dependency_package" "$dependency_version" "$dependency_integrity" "$native_package" "$native_package_name" "$native_version" "$native_integrity" "$native_executable" "$claude_code_version"
|
||||
write_node_wrapper "$staged_bin" "$entrypoint" "$node_engine"
|
||||
{
|
||||
printf 'STAMP_TARGET=%q\n' "$target"
|
||||
printf 'STAMP_PACKAGE=%q\n' "$package"
|
||||
printf 'STAMP_VERSION=%q\n' "$version"
|
||||
printf 'STAMP_INTEGRITY=%q\n' "$integrity"
|
||||
printf 'STAMP_TARBALL=%q\n' "$tarball"
|
||||
printf 'STAMP_NPM_OS=%q\n' "$npm_os"
|
||||
printf 'STAMP_NPM_CPU=%q\n' "$npm_cpu"
|
||||
printf 'STAMP_NPM_LIBC=%q\n' "$npm_libc"
|
||||
printf 'STAMP_NODE_ENGINE=%q\n' "$node_engine"
|
||||
printf 'STAMP_DEPENDENCY_PACKAGE=%q\n' "$dependency_package"
|
||||
printf 'STAMP_DEPENDENCY_VERSION=%q\n' "$dependency_version"
|
||||
printf 'STAMP_DEPENDENCY_INTEGRITY=%q\n' "$dependency_integrity"
|
||||
printf 'STAMP_DEPENDENCY_TARBALL=%q\n' "$dependency_tarball"
|
||||
printf 'STAMP_CLAUDE_CODE_VERSION=%q\n' "$claude_code_version"
|
||||
printf 'STAMP_NATIVE_PACKAGE=%q\n' "$native_package"
|
||||
printf 'STAMP_NATIVE_PACKAGE_NAME=%q\n' "$native_package_name"
|
||||
printf 'STAMP_NATIVE_VERSION=%q\n' "$native_version"
|
||||
printf 'STAMP_NATIVE_INTEGRITY=%q\n' "$native_integrity"
|
||||
printf 'STAMP_NATIVE_TARBALL=%q\n' "$native_tarball"
|
||||
printf 'STAMP_NATIVE_EXECUTABLE=%q\n' "$native_executable"
|
||||
printf 'STAMP_BINARY=%q\n' "$binary"
|
||||
} > "$stamp"
|
||||
done
|
||||
|
||||
# bin_dir is prepended to the agent spawn PATH and the desktop's command
|
||||
# resolution sweep, so binaries (and stamps) for tools no longer in the lock
|
||||
# must be pruned, not just left behind.
|
||||
locked_binaries="$(node -e '
|
||||
const entries = JSON.parse(process.argv[1]);
|
||||
for (const entry of entries) console.log(entry.binary);
|
||||
' "$lock_entries" | sort -u)"
|
||||
find "$bin_dir" -type f -print0 | while IFS= read -r -d '' staged_file; do
|
||||
name="$(basename "$staged_file")"
|
||||
if ! printf '%s\n' "$locked_binaries" | grep -Fxq -- "${name%.stamp}"; then
|
||||
rm -f -- "$staged_file"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$print_bin_dir" == "1" ]]; then
|
||||
printf '%s\n' "$bin_dir"
|
||||
fi
|
||||
61
desktop/scripts/lib/acp-node-wrapper.sh
Normal file
61
desktop/scripts/lib/acp-node-wrapper.sh
Normal file
@@ -0,0 +1,61 @@
|
||||
# Shared Node wrapper generation for ACP bridge tools staged from npm.
|
||||
# Sourced by ensure-acp-tools.sh and prepare-acp-tools-resource.sh so the
|
||||
# wrapper staged into the dev cache and the wrapper bundled into app
|
||||
# resources cannot drift (a drift would make dev and bundled installs fail
|
||||
# differently on the same missing/old Node runtime).
|
||||
#
|
||||
# acp_required_node_major <node-engine>
|
||||
# Prints the minimum Node.js major version implied by a ">=N..." engine
|
||||
# range, defaulting to 22 when the range is not in that form. Shared by
|
||||
# the wrapper shim below and the node-runtime.json manifest consumed by
|
||||
# the app's Node.js runtime doctor check, so the version the wrapper
|
||||
# enforces at spawn time and the version the doctor reports at setup
|
||||
# time cannot disagree.
|
||||
acp_required_node_major() {
|
||||
local node_engine="$1"
|
||||
local major
|
||||
major="$(printf '%s\n' "$node_engine" | sed -n 's/^>=\([0-9][0-9]*\).*$/\1/p')"
|
||||
if [[ -z "$major" ]]; then
|
||||
major=22
|
||||
fi
|
||||
printf '%s\n' "$major"
|
||||
}
|
||||
|
||||
# write_node_wrapper <wrapper> <entrypoint> [node-engine]
|
||||
# Writes an executable bash shim at <wrapper> that verifies a Node.js
|
||||
# runtime satisfying <node-engine> (default ">=22") is on PATH, then
|
||||
# execs node on <entrypoint>. An absolute <entrypoint> is embedded
|
||||
# verbatim; a relative one is resolved against the wrapper's directory
|
||||
# at run time.
|
||||
|
||||
write_node_wrapper() {
|
||||
local wrapper="$1"
|
||||
local entrypoint="$2"
|
||||
local node_engine="${3:->=22}"
|
||||
local required_node_major
|
||||
required_node_major="$(acp_required_node_major "$node_engine")"
|
||||
|
||||
mkdir -p "$(dirname "$wrapper")"
|
||||
{
|
||||
printf '#!/usr/bin/env bash\n'
|
||||
printf 'set -euo pipefail\n'
|
||||
printf 'if ! command -v node >/dev/null 2>&1; then\n'
|
||||
printf ' echo "%s requires Node.js %s on PATH." >&2\n' "$(basename "$wrapper")" "$node_engine"
|
||||
printf ' exit 127\n'
|
||||
printf 'fi\n'
|
||||
printf 'required_node_major=%q\n' "$required_node_major"
|
||||
printf 'node_major="$(node -p '\''process.versions.node.split(".")[0]'\'' 2>/dev/null || true)"\n'
|
||||
printf 'if [[ -z "$node_major" || "$node_major" -lt "$required_node_major" ]]; then\n'
|
||||
printf ' echo "%s requires Node.js %s on PATH." >&2\n' "$(basename "$wrapper")" "$node_engine"
|
||||
printf ' exit 1\n'
|
||||
printf 'fi\n'
|
||||
if [[ "$entrypoint" == /* ]]; then
|
||||
printf 'entrypoint=%q\n' "$entrypoint"
|
||||
else
|
||||
printf 'wrapper_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\n'
|
||||
printf 'entrypoint="$wrapper_dir"/%q\n' "$entrypoint"
|
||||
fi
|
||||
printf 'exec node "$entrypoint" "$@"\n'
|
||||
} > "$wrapper"
|
||||
chmod +x "$wrapper"
|
||||
}
|
||||
128
desktop/scripts/prepare-acp-tools-resource.sh
Executable file
128
desktop/scripts/prepare-acp-tools-resource.sh
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
app_root="$(cd "$script_dir/.." && pwd)"
|
||||
lock_file="${ACP_TOOLS_LOCK_FILE:-$app_root/acp-tools.lock.json}"
|
||||
|
||||
# shellcheck source=lib/acp-node-wrapper.sh
|
||||
source "$script_dir/lib/acp-node-wrapper.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: desktop/scripts/prepare-acp-tools-resource.sh [target-triple]
|
||||
|
||||
Stages the locked ACP bridge tools into src-tauri/resources/acp so Tauri can
|
||||
bundle them as application resources: vendored npm package trees under
|
||||
resources/acp/node and executable wrappers under resources/acp/bin. The
|
||||
optional target triple defaults to the Rust host target.
|
||||
|
||||
Note: resources/acp/bin holds a single target at a time, so staging must stay
|
||||
tied to the build target.
|
||||
USAGE
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
target="${1:-}"
|
||||
ensure_args=()
|
||||
if [[ -n "$target" ]]; then
|
||||
ensure_args+=(--target "$target")
|
||||
else
|
||||
target="$(rustc -vV | sed -n 's|host: ||p')"
|
||||
fi
|
||||
if [[ -z "$target" ]]; then
|
||||
echo "Could not determine rust host target." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cache_bin_dir="$("$script_dir/ensure-acp-tools.sh" ${ensure_args[@]+"${ensure_args[@]}"} --print-bin-dir)"
|
||||
cache_root="$(dirname "$(dirname "$cache_bin_dir")")"
|
||||
resource_root="$app_root/src-tauri/resources/acp"
|
||||
resource_bin_dir="$resource_root/bin"
|
||||
resource_node_dir="$resource_root/node"
|
||||
mkdir -p "$resource_bin_dir"
|
||||
|
||||
# Keep .gitkeep but refresh any staged tools from the lock.
|
||||
find "$resource_bin_dir" -type f ! -name ".gitkeep" -delete
|
||||
rm -rf "$resource_node_dir"
|
||||
mkdir -p "$resource_node_dir"
|
||||
|
||||
# Manifest for the app's Node.js runtime doctor check, staged next to the
|
||||
# bin dir so the app can resolve it as the bin dir's parent. Removed up
|
||||
# front so locks with no npm-sourced tools ship no manifest and the doctor
|
||||
# check stays silent.
|
||||
node_runtime_manifest="$resource_root/node-runtime.json"
|
||||
rm -f "$node_runtime_manifest"
|
||||
node_runtime_entries=()
|
||||
|
||||
codesign_if_darwin() {
|
||||
local file="$1"
|
||||
if [[ "$(uname -s)" == "Darwin" ]] && command -v codesign >/dev/null 2>&1; then
|
||||
codesign --force --sign - "$file" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS=$'\t' read -r id binary package version node_engine; do
|
||||
[[ -n "$id" ]] || continue
|
||||
install_dir="$cache_root/$target/$id/$version/npm"
|
||||
entrypoint="$install_dir/node_modules/$package/dist/index.js"
|
||||
if [[ ! -f "$entrypoint" ]]; then
|
||||
echo "Locked npm ACP tool missing from cache: $package@$version" >&2
|
||||
exit 1
|
||||
fi
|
||||
resource_package_dir="$resource_node_dir/$id"
|
||||
mkdir -p "$resource_package_dir"
|
||||
cp -R "$install_dir/." "$resource_package_dir/"
|
||||
resource_entrypoint="$resource_package_dir/node_modules/$package/dist/index.js"
|
||||
if [[ ! -f "$resource_entrypoint" ]]; then
|
||||
echo "Failed to stage npm ACP tool: $package@$version" >&2
|
||||
exit 1
|
||||
fi
|
||||
write_node_wrapper "$resource_bin_dir/$binary" "../node/$id/node_modules/$package/dist/index.js" "$node_engine"
|
||||
node_runtime_entries+=("$id"$'\t'"$binary"$'\t'"$node_engine"$'\t'"$(acp_required_node_major "$node_engine")")
|
||||
# Ad-hoc sign every Mach-O in the staged package, not just the main CLIs:
|
||||
# the codex native package also vendors executables like rg and zsh, and
|
||||
# unsigned nested Mach-Os are killed by Gatekeeper. Darwin only, so Linux
|
||||
# staging skips the file(1) scan.
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
while IFS= read -r -d '' candidate; do
|
||||
if file -b "$candidate" | grep -q "Mach-O"; then
|
||||
codesign_if_darwin "$candidate"
|
||||
fi
|
||||
done < <(find "$resource_package_dir" -type f -print0)
|
||||
fi
|
||||
done < <(node - "$lock_file" "$target" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const [lockFile, target] = process.argv.slice(2);
|
||||
const data = JSON.parse(fs.readFileSync(lockFile, "utf8"));
|
||||
for (const entry of data.tools ?? []) {
|
||||
if (entry.target !== target || typeof entry.binary !== "string") continue;
|
||||
if (entry.source !== "npm") {
|
||||
throw new Error(`Unsupported ACP tool source: ${entry.source}`);
|
||||
}
|
||||
console.log([entry.id, entry.binary, entry.package, entry.version, entry.nodeEngine ?? ">=22"].join("\t"));
|
||||
}
|
||||
NODE
|
||||
)
|
||||
|
||||
# One manifest entry per npm-sourced bridge, each carrying its own required
|
||||
# Node major, so bridges with different engine ranges surface distinct
|
||||
# requirements in the doctor check.
|
||||
if ((${#node_runtime_entries[@]} > 0)); then
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const [manifestFile, ...entries] = process.argv.slice(1);
|
||||
const tools = entries.map((line) => {
|
||||
const [id, binary, nodeEngine, requiredNodeMajor] = line.split("\t");
|
||||
return { id, binary, nodeEngine, requiredNodeMajor: Number(requiredNodeMajor) };
|
||||
});
|
||||
fs.writeFileSync(manifestFile, `${JSON.stringify({ tools }, null, 2)}\n`);
|
||||
' "$node_runtime_manifest" ${node_runtime_entries[@]+"${node_runtime_entries[@]}"}
|
||||
echo "Wrote ACP Node runtime manifest: $node_runtime_manifest"
|
||||
fi
|
||||
|
||||
echo "Staged ACP tools resource: $resource_bin_dir"
|
||||
363
desktop/scripts/update-acp-tools-lock.mjs
Executable file
363
desktop/scripts/update-acp-tools-lock.mjs
Executable file
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const appRoot = path.resolve(import.meta.dirname, "..");
|
||||
const defaultLockFile = path.join(appRoot, "acp-tools.lock.json");
|
||||
|
||||
const SUPPORTED_TARGETS = [
|
||||
"aarch64-apple-darwin",
|
||||
"x86_64-apple-darwin",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
];
|
||||
|
||||
// The Codex ACP executable stays `codex-acp`, but bundled installs must come
|
||||
// from the maintained Agent Client Protocol package rather than the stale
|
||||
// Zed package.
|
||||
const CODEX_ACP_PACKAGE = "@agentclientprotocol/codex-acp";
|
||||
|
||||
const TOOL_SPECS = [
|
||||
{
|
||||
id: "claude-acp",
|
||||
binary: "claude-agent-acp",
|
||||
package: "@agentclientprotocol/claude-agent-acp",
|
||||
dependencyPackage: "@anthropic-ai/claude-agent-sdk",
|
||||
nativePackageKey: "claudeAgentSdk",
|
||||
includeClaudeCodeVersion: true,
|
||||
},
|
||||
{
|
||||
id: "codex-acp",
|
||||
binary: "codex-acp",
|
||||
package: CODEX_ACP_PACKAGE,
|
||||
dependencyPackage: "@openai/codex",
|
||||
nativePackageKey: "openaiCodex",
|
||||
},
|
||||
];
|
||||
|
||||
const NPM_TARGET_CONFIG = {
|
||||
"aarch64-apple-darwin": {
|
||||
npmOs: "darwin",
|
||||
npmCpu: "arm64",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-darwin-arm64",
|
||||
openaiCodex: "@openai/codex-darwin-arm64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude",
|
||||
openaiCodex: "vendor/aarch64-apple-darwin/bin/codex",
|
||||
},
|
||||
},
|
||||
"x86_64-apple-darwin": {
|
||||
npmOs: "darwin",
|
||||
npmCpu: "x64",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-darwin-x64",
|
||||
openaiCodex: "@openai/codex-darwin-x64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude",
|
||||
openaiCodex: "vendor/x86_64-apple-darwin/bin/codex",
|
||||
},
|
||||
},
|
||||
"aarch64-unknown-linux-gnu": {
|
||||
npmOs: "linux",
|
||||
npmCpu: "arm64",
|
||||
npmLibc: "glibc",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-linux-arm64",
|
||||
openaiCodex: "@openai/codex-linux-arm64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude",
|
||||
openaiCodex: "vendor/aarch64-unknown-linux-musl/bin/codex",
|
||||
},
|
||||
},
|
||||
"x86_64-unknown-linux-gnu": {
|
||||
npmOs: "linux",
|
||||
npmCpu: "x64",
|
||||
npmLibc: "glibc",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-linux-x64",
|
||||
openaiCodex: "@openai/codex-linux-x64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude",
|
||||
openaiCodex: "vendor/x86_64-unknown-linux-musl/bin/codex",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const npmViewCache = new Map();
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage: desktop/scripts/update-acp-tools-lock.mjs [--target <triple>]... [--lock-file <path>]
|
||||
|
||||
Queries npm for the latest release of each supported ACP bridge tool and
|
||||
writes acp-tools.lock.json. Fails loudly when a package or one of its
|
||||
per-target native dependencies cannot be resolved — never silently pins an
|
||||
older version.
|
||||
|
||||
Supported targets:
|
||||
${SUPPORTED_TARGETS.join("\n ")}
|
||||
|
||||
Environment:
|
||||
npm registry config used to resolve packages
|
||||
ACP_TOOLS_LOCK_FILE lockfile path override
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const targets = [];
|
||||
let lockFile = process.env.ACP_TOOLS_LOCK_FILE ?? defaultLockFile;
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === "-h" || arg === "--help") {
|
||||
usage();
|
||||
process.exit(0);
|
||||
}
|
||||
if (arg === "--target") {
|
||||
const value = argv[++i];
|
||||
if (!value) throw new Error("--target requires a value");
|
||||
targets.push(value);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--lock-file") {
|
||||
const value = argv[++i];
|
||||
if (!value) throw new Error("--lock-file requires a value");
|
||||
lockFile = path.resolve(value);
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
const selectedTargets = targets.length ? targets : SUPPORTED_TARGETS;
|
||||
for (const target of selectedTargets) {
|
||||
if (!SUPPORTED_TARGETS.includes(target)) {
|
||||
throw new Error(`Unsupported target '${target}'`);
|
||||
}
|
||||
}
|
||||
return { targets: selectedTargets, lockFile };
|
||||
}
|
||||
|
||||
async function npmView(spec, fields) {
|
||||
const cacheKey = `${spec}\0${fields.join("\0")}`;
|
||||
if (!npmViewCache.has(cacheKey)) {
|
||||
npmViewCache.set(
|
||||
cacheKey,
|
||||
execFileAsync("npm", ["view", spec, ...fields, "--json"], {
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
}).then(({ stdout }) => {
|
||||
try {
|
||||
return JSON.parse(stdout);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`npm view ${spec} returned invalid JSON: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
return npmViewCache.get(cacheKey);
|
||||
}
|
||||
|
||||
function requireString(value, label) {
|
||||
if (typeof value !== "string" || value.trim() === "") {
|
||||
throw new Error(`Missing ${label}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function packageDist(metadata, label) {
|
||||
const dist = metadata?.dist;
|
||||
return {
|
||||
tarball: requireString(dist?.tarball, `${label} dist.tarball`),
|
||||
integrity: requireString(dist?.integrity, `${label} dist.integrity`),
|
||||
};
|
||||
}
|
||||
|
||||
function compareSemver(left, right) {
|
||||
const leftCore = left.split("-", 1)[0].split(".").map(Number);
|
||||
const rightCore = right.split("-", 1)[0].split(".").map(Number);
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if ((leftCore[i] ?? 0) !== (rightCore[i] ?? 0)) {
|
||||
return (leftCore[i] ?? 0) - (rightCore[i] ?? 0);
|
||||
}
|
||||
}
|
||||
// A release outranks any prerelease of the same core version.
|
||||
return (left.includes("-") ? 0 : 1) - (right.includes("-") ? 0 : 1);
|
||||
}
|
||||
|
||||
// npm view returns a single object when a spec matches one version, but an
|
||||
// array of per-version objects when a range matches several. Pick the highest
|
||||
// matching version so a ranged dependency still pins the newest release.
|
||||
function pickLatestMatch(metadata, label) {
|
||||
if (!Array.isArray(metadata)) return metadata;
|
||||
if (metadata.length === 0) {
|
||||
throw new Error(`No versions match ${label}`);
|
||||
}
|
||||
return metadata.reduce((best, candidate) =>
|
||||
compareSemver(
|
||||
requireString(candidate?.version, `${label} version`),
|
||||
requireString(best?.version, `${label} version`),
|
||||
) > 0
|
||||
? candidate
|
||||
: best,
|
||||
);
|
||||
}
|
||||
|
||||
function parseNpmAliasSpec(spec, fallbackPackage) {
|
||||
if (!spec.startsWith("npm:")) {
|
||||
return { packageName: fallbackPackage, version: spec };
|
||||
}
|
||||
const aliased = spec.slice("npm:".length);
|
||||
const versionSeparator = aliased.lastIndexOf("@");
|
||||
if (versionSeparator <= 0) {
|
||||
throw new Error(`Unsupported npm alias spec: ${spec}`);
|
||||
}
|
||||
return {
|
||||
packageName: aliased.slice(0, versionSeparator),
|
||||
version: aliased.slice(versionSeparator + 1),
|
||||
};
|
||||
}
|
||||
|
||||
async function lockToolForTarget(tool, target) {
|
||||
const npmTarget = NPM_TARGET_CONFIG[target];
|
||||
if (!npmTarget) {
|
||||
throw new Error(`No npm target mapping for ${target}`);
|
||||
}
|
||||
|
||||
const packageName = tool.package;
|
||||
const packageMetadata = await npmView(`${packageName}@latest`, [
|
||||
"name",
|
||||
"version",
|
||||
"dist",
|
||||
"dependencies",
|
||||
"engines",
|
||||
"bin",
|
||||
]);
|
||||
|
||||
if (packageMetadata.name !== packageName) {
|
||||
throw new Error(
|
||||
`npm package ${packageName} resolved to ${packageMetadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const version = requireString(
|
||||
packageMetadata.version,
|
||||
`${packageName} version`,
|
||||
);
|
||||
const packageInfo = packageDist(packageMetadata, `${packageName}@${version}`);
|
||||
const entry = {
|
||||
id: tool.id,
|
||||
binary: tool.binary,
|
||||
source: "npm",
|
||||
package: packageName,
|
||||
version,
|
||||
integrity: packageInfo.integrity,
|
||||
tarball: packageInfo.tarball,
|
||||
target,
|
||||
npmOs: npmTarget.npmOs,
|
||||
npmCpu: npmTarget.npmCpu,
|
||||
...(npmTarget.npmLibc ? { npmLibc: npmTarget.npmLibc } : {}),
|
||||
nodeEngine: packageMetadata.engines?.node ?? ">=22",
|
||||
};
|
||||
|
||||
const dependencyRange = requireString(
|
||||
packageMetadata.dependencies?.[tool.dependencyPackage],
|
||||
`${packageName} dependency ${tool.dependencyPackage}`,
|
||||
);
|
||||
const dependencyMetadata = pickLatestMatch(
|
||||
await npmView(`${tool.dependencyPackage}@${dependencyRange}`, [
|
||||
"name",
|
||||
"version",
|
||||
"dist",
|
||||
"optionalDependencies",
|
||||
"claudeCodeVersion",
|
||||
]),
|
||||
`${tool.dependencyPackage}@${dependencyRange}`,
|
||||
);
|
||||
const dependencyVersion = requireString(
|
||||
dependencyMetadata.version,
|
||||
`${tool.dependencyPackage}@${dependencyRange} version`,
|
||||
);
|
||||
const dependencyInfo = packageDist(
|
||||
dependencyMetadata,
|
||||
`${tool.dependencyPackage}@${dependencyVersion}`,
|
||||
);
|
||||
entry.dependencyPackage = tool.dependencyPackage;
|
||||
entry.dependencyVersion = dependencyVersion;
|
||||
entry.dependencyIntegrity = dependencyInfo.integrity;
|
||||
entry.dependencyTarball = dependencyInfo.tarball;
|
||||
if (tool.includeClaudeCodeVersion) {
|
||||
entry.claudeCodeVersion = dependencyMetadata.claudeCodeVersion ?? null;
|
||||
}
|
||||
|
||||
const nativePackage = requireString(
|
||||
npmTarget.nativePackages?.[tool.nativePackageKey],
|
||||
`${target} native package for ${tool.nativePackageKey}`,
|
||||
);
|
||||
const nativeExecutable = requireString(
|
||||
npmTarget.nativeExecutables?.[tool.nativePackageKey],
|
||||
`${target} native executable for ${tool.nativePackageKey}`,
|
||||
);
|
||||
const nativeSpec = requireString(
|
||||
dependencyMetadata.optionalDependencies?.[nativePackage],
|
||||
`${tool.dependencyPackage}@${dependencyVersion} optional dependency ${nativePackage}`,
|
||||
);
|
||||
const nativeAlias = parseNpmAliasSpec(nativeSpec, nativePackage);
|
||||
const nativeMetadata = await npmView(
|
||||
`${nativeAlias.packageName}@${nativeAlias.version}`,
|
||||
["name", "version", "dist"],
|
||||
);
|
||||
const nativeVersion = requireString(
|
||||
nativeMetadata.version,
|
||||
`${nativeAlias.packageName}@${nativeAlias.version} version`,
|
||||
);
|
||||
if (nativeVersion !== nativeAlias.version) {
|
||||
throw new Error(
|
||||
`${nativeAlias.packageName}@${nativeAlias.version} resolved to ${nativeVersion}`,
|
||||
);
|
||||
}
|
||||
const nativeInfo = packageDist(
|
||||
nativeMetadata,
|
||||
`${nativeAlias.packageName}@${nativeVersion}`,
|
||||
);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
nativePackage,
|
||||
nativePackageName: nativeMetadata.name ?? nativePackage,
|
||||
nativeVersion,
|
||||
nativeIntegrity: nativeInfo.integrity,
|
||||
nativeTarball: nativeInfo.tarball,
|
||||
nativeExecutable,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { targets, lockFile } = parseArgs(process.argv.slice(2));
|
||||
const tools = [];
|
||||
for (const tool of TOOL_SPECS) {
|
||||
for (const target of targets) {
|
||||
tools.push(await lockToolForTarget(tool, target));
|
||||
}
|
||||
}
|
||||
tools.sort((left, right) =>
|
||||
`${left.id}:${left.target}`.localeCompare(`${right.id}:${right.target}`),
|
||||
);
|
||||
await mkdir(path.dirname(lockFile), { recursive: true });
|
||||
await writeFile(lockFile, `${JSON.stringify({ tools }, null, 2)}\n`);
|
||||
console.log(`Updated ${path.relative(process.cwd(), lockFile)}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
0
desktop/src-tauri/resources/acp/bin/.gitkeep
Normal file
0
desktop/src-tauri/resources/acp/bin/.gitkeep
Normal file
@@ -59,6 +59,7 @@
|
||||
"binaries/git-credential-nostr",
|
||||
"binaries/buzz"
|
||||
],
|
||||
"resources": ["resources/acp"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
Reference in New Issue
Block a user