diff --git a/.gitignore b/.gitignore index ed2d1a9413..bbf5358839 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/Justfile b/Justfile index d49369b2fa..ea50e8a2df 100644 --- a/Justfile +++ b/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) diff --git a/desktop/acp-tools.lock.json b/desktop/acp-tools.lock.json new file mode 100644 index 0000000000..d2361a3b6b --- /dev/null +++ b/desktop/acp-tools.lock.json @@ -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" + } + ] +} diff --git a/desktop/scripts/ensure-acp-tools.sh b/desktop/scripts/ensure-acp-tools.sh new file mode 100755 index 0000000000..ddee056ca4 --- /dev/null +++ b/desktop/scripts/ensure-acp-tools.sh @@ -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 ] [--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 diff --git a/desktop/scripts/lib/acp-node-wrapper.sh b/desktop/scripts/lib/acp-node-wrapper.sh new file mode 100644 index 0000000000..7d99839ee1 --- /dev/null +++ b/desktop/scripts/lib/acp-node-wrapper.sh @@ -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 +# 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 [node-engine] +# Writes an executable bash shim at that verifies a Node.js +# runtime satisfying (default ">=22") is on PATH, then +# execs node on . An absolute 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" +} diff --git a/desktop/scripts/prepare-acp-tools-resource.sh b/desktop/scripts/prepare-acp-tools-resource.sh new file mode 100755 index 0000000000..df7f840472 --- /dev/null +++ b/desktop/scripts/prepare-acp-tools-resource.sh @@ -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" diff --git a/desktop/scripts/update-acp-tools-lock.mjs b/desktop/scripts/update-acp-tools-lock.mjs new file mode 100755 index 0000000000..bffcf0c320 --- /dev/null +++ b/desktop/scripts/update-acp-tools-lock.mjs @@ -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 ]... [--lock-file ] + +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); +}); diff --git a/desktop/src-tauri/resources/acp/bin/.gitkeep b/desktop/src-tauri/resources/acp/bin/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index a8600f3bc5..fbda81d656 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -59,6 +59,7 @@ "binaries/git-credential-nostr", "binaries/buzz" ], + "resources": ["resources/acp"], "icon": [ "icons/32x32.png", "icons/128x128.png",