From 5ae90f95fa7c478bb05db4e416cc467bde625d2f Mon Sep 17 00:00:00 2001 From: Derek Wang Date: Tue, 3 Nov 2026 15:51:27 -0800 Subject: [PATCH 1/3] feat: add install script for macOS and Linux Signed-off-by: Derek Wang --- README.md | 8 +++ install.sh | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100755 install.sh diff --git a/README.md b/README.md index 21679f7..6bdc492 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,14 @@ brew install a2a winget install a2aproject.a2acli ``` +**Install script (macOS / Linux)** + +```bash +curl -fsSL https://raw.githubusercontent.com/a2aproject/a2a-cli/main/install.sh | bash +``` + +Set `A2A_VERSION` to pin a release and `INSTALL_DIR` to change the destination (default `/usr/local/bin`). + **From Source (Linux / Windows / macOS)** ```bash diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..6cbd59c --- /dev/null +++ b/install.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# +# a2a installer +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/a2aproject/a2a-cli/main/install.sh | bash +# +# Environment variables: +# A2A_VERSION Specific version to install (e.g. v0.3.1 or 0.3.1). Defaults +# to the latest published release. +# INSTALL_DIR Directory to install the binary into. Defaults to +# /usr/local/bin (falls back to $HOME/.local/bin if not +# writable and sudo is unavailable). +# BINARY_NAME Name of the installed binary. Defaults to a2a. +# +set -euo pipefail + +REPO="a2aproject/a2a-cli" +BINARY_NAME="${BINARY_NAME:-a2a}" +INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" + +tmpdir="" +trap 'rm -rf "${tmpdir:-}"' EXIT + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m==>\033[0m %s\n' "$*" >&2; } +err() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; exit 1; } + +require() { + command -v "$1" >/dev/null 2>&1 || err "required command not found: $1" +} + +require uname +require tar +require install +require mktemp + +if command -v curl >/dev/null 2>&1; then + DOWNLOADER="curl" +elif command -v wget >/dev/null 2>&1; then + DOWNLOADER="wget" +else + err "neither curl nor wget is installed" +fi + +if command -v sha256sum >/dev/null 2>&1; then + SHA256=(sha256sum) +elif command -v shasum >/dev/null 2>&1; then + SHA256=(shasum -a 256) +else + err "neither sha256sum nor shasum is installed" +fi + +fetch() { + local url="$1" out="$2" + if [ "$DOWNLOADER" = "curl" ]; then + curl -fsSL "$url" -o "$out" + else + wget -qO "$out" "$url" + fi +} + +fetch_stdout() { + local url="$1" + if [ "$DOWNLOADER" = "curl" ]; then + curl -fsSL "$url" + else + wget -qO- "$url" + fi +} + +detect_os() { + local os + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + case "$os" in + linux) echo "linux" ;; + darwin) echo "darwin" ;; + *) err "unsupported OS: $os (see https://github.com/${REPO}/releases for Windows builds)" ;; + esac +} + +detect_arch() { + local arch + arch="$(uname -m)" + case "$arch" in + x86_64|amd64) echo "amd64" ;; + aarch64|arm64) echo "arm64" ;; + *) err "unsupported architecture: $arch" ;; + esac +} + +resolve_tag() { + if [ -n "${A2A_VERSION:-}" ]; then + echo "v${A2A_VERSION#v}" + return + fi + local api_url="https://api.github.com/repos/${REPO}/releases/latest" + local tag + tag="$(fetch_stdout "$api_url" \ + | grep -E '"tag_name"' \ + | head -n 1 \ + | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')" + [ -n "$tag" ] || err "failed to detect latest version from $api_url" + echo "$tag" +} + +verify_checksum() { + local archive="$1" checksums="$2" name expected actual + name="$(basename "$archive")" + expected="$(awk -v n="$name" '$2 == n { print $1 }' "$checksums")" + [ -n "$expected" ] || err "no checksum found for $name" + actual="$("${SHA256[@]}" "$archive" | awk '{ print $1 }')" + [ "$expected" = "$actual" ] || err "checksum mismatch for $name (want $expected, got $actual)" +} + +choose_install_dir() { + local dir="$INSTALL_DIR" + if [ -d "$dir" ] && [ -w "$dir" ]; then + echo "$dir" + return + fi + if [ ! -d "$dir" ] && mkdir -p "$dir" 2>/dev/null; then + echo "$dir" + return + fi + if command -v sudo >/dev/null 2>&1; then + echo "$dir" + return + fi + warn "$dir is not writable and sudo is unavailable; falling back to \$HOME/.local/bin" + mkdir -p "$HOME/.local/bin" + echo "$HOME/.local/bin" +} + +install_binary() { + local src="$1" dest_dir="$2" dest="$2/$BINARY_NAME" + if [ -w "$dest_dir" ]; then + install -m 0755 "$src" "$dest" + return + fi + log "Installing to $dest (requires sudo)" + sudo mkdir -p "$dest_dir" + sudo install -m 0755 "$src" "$dest" +} + +main() { + local os arch tag version base_url archive_name dest_dir + os="$(detect_os)" + arch="$(detect_arch)" + tag="$(resolve_tag)" + version="${tag#v}" + archive_name="a2a_${version}_${os}_${arch}.tar.gz" + base_url="https://github.com/${REPO}/releases/download/${tag}" + + log "Installing a2a ${tag} for ${os}/${arch}" + log "Asset: ${base_url}/${archive_name}" + + tmpdir="$(mktemp -d)" + + fetch "${base_url}/${archive_name}" "${tmpdir}/${archive_name}" \ + || err "failed to download ${base_url}/${archive_name}" + fetch "${base_url}/checksums.txt" "${tmpdir}/checksums.txt" \ + || err "failed to download ${base_url}/checksums.txt" + verify_checksum "${tmpdir}/${archive_name}" "${tmpdir}/checksums.txt" + + tar -xzf "${tmpdir}/${archive_name}" -C "$tmpdir" a2a \ + || err "failed to extract a2a from ${archive_name}" + + dest_dir="$(choose_install_dir)" + install_binary "${tmpdir}/a2a" "$dest_dir" + + log "Installed ${BINARY_NAME} to ${dest_dir}/${BINARY_NAME}" + + case ":$PATH:" in + *":${dest_dir}:"*) ;; + *) warn "${dest_dir} is not in your PATH; add it to use ${BINARY_NAME} directly" ;; + esac + + "${dest_dir}/${BINARY_NAME}" version || warn "installed binary failed to report its version" +} + +main "$@" From 0eb3f7a803ad81ca68bb1931f295b3d99e55dfbe Mon Sep 17 00:00:00 2001 From: Derek Wang Date: Wed, 4 Nov 2026 09:33:07 -0800 Subject: [PATCH 2/3] chore: variable name change Signed-off-by: Derek Wang --- README.md | 6 +++++- install.sh | 26 +++++++++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6bdc492..8fa586b 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,11 @@ winget install a2aproject.a2acli curl -fsSL https://raw.githubusercontent.com/a2aproject/a2a-cli/main/install.sh | bash ``` -Set `A2A_VERSION` to pin a release and `INSTALL_DIR` to change the destination (default `/usr/local/bin`). +This installs the latest release into `/usr/local/bin`. Set `A2A_INSTALL_DIR` to change the destination, or `A2A_VERSION` to pin a version: + +```bash +curl -fsSL https://raw.githubusercontent.com/a2aproject/a2a-cli/main/install.sh | A2A_VERSION=0.3.1 bash +``` **From Source (Linux / Windows / macOS)** diff --git a/install.sh b/install.sh index 6cbd59c..ddac2cb 100755 --- a/install.sh +++ b/install.sh @@ -6,18 +6,18 @@ # curl -fsSL https://raw.githubusercontent.com/a2aproject/a2a-cli/main/install.sh | bash # # Environment variables: -# A2A_VERSION Specific version to install (e.g. v0.3.1 or 0.3.1). Defaults -# to the latest published release. -# INSTALL_DIR Directory to install the binary into. Defaults to -# /usr/local/bin (falls back to $HOME/.local/bin if not -# writable and sudo is unavailable). -# BINARY_NAME Name of the installed binary. Defaults to a2a. +# A2A_VERSION Specific version to install (e.g. v0.3.1 or 0.3.1). +# Defaults to the latest published release. +# A2A_INSTALL_DIR Directory to install the binary into. Defaults to +# /usr/local/bin (falls back to $HOME/.local/bin if not +# writable and sudo is unavailable). +# A2A_BINARY_NAME Name of the installed binary. Defaults to a2a. # set -euo pipefail REPO="a2aproject/a2a-cli" -BINARY_NAME="${BINARY_NAME:-a2a}" -INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" +A2A_BINARY_NAME="${A2A_BINARY_NAME:-a2a}" +A2A_INSTALL_DIR="${A2A_INSTALL_DIR:-/usr/local/bin}" tmpdir="" trap 'rm -rf "${tmpdir:-}"' EXIT @@ -114,7 +114,7 @@ verify_checksum() { } choose_install_dir() { - local dir="$INSTALL_DIR" + local dir="$A2A_INSTALL_DIR" if [ -d "$dir" ] && [ -w "$dir" ]; then echo "$dir" return @@ -133,7 +133,7 @@ choose_install_dir() { } install_binary() { - local src="$1" dest_dir="$2" dest="$2/$BINARY_NAME" + local src="$1" dest_dir="$2" dest="$2/$A2A_BINARY_NAME" if [ -w "$dest_dir" ]; then install -m 0755 "$src" "$dest" return @@ -169,14 +169,14 @@ main() { dest_dir="$(choose_install_dir)" install_binary "${tmpdir}/a2a" "$dest_dir" - log "Installed ${BINARY_NAME} to ${dest_dir}/${BINARY_NAME}" + log "Installed ${A2A_BINARY_NAME} to ${dest_dir}/${A2A_BINARY_NAME}" case ":$PATH:" in *":${dest_dir}:"*) ;; - *) warn "${dest_dir} is not in your PATH; add it to use ${BINARY_NAME} directly" ;; + *) warn "${dest_dir} is not in your PATH; add it to use ${A2A_BINARY_NAME} directly" ;; esac - "${dest_dir}/${BINARY_NAME}" version || warn "installed binary failed to report its version" + "${dest_dir}/${A2A_BINARY_NAME}" version || warn "installed binary failed to report its version" } main "$@" From 92e692fd7a70b06a435e76a41165fbe43b1d7611 Mon Sep 17 00:00:00 2001 From: Yaroslav Shevchuk Date: Wed, 4 Nov 2026 21:04:00 +0000 Subject: [PATCH 3/3] fix: harden install script sudo, latest-tag, and Rosetta handling - Fall back to $HOME/.local/bin when sudo needs a password and no terminal is available. - Resolve the latest tag via the releases/latest redirect to avoid GitHub API rate limits. - Install the arm64 build when the shell runs under Rosetta on Apple Silicon. --- install.sh | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/install.sh b/install.sh index ddac2cb..172b031 100755 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ # Defaults to the latest published release. # A2A_INSTALL_DIR Directory to install the binary into. Defaults to # /usr/local/bin (falls back to $HOME/.local/bin if not -# writable and sudo is unavailable). +# writable and sudo is unusable). # A2A_BINARY_NAME Name of the installed binary. Defaults to a2a. # set -euo pipefail @@ -60,15 +60,23 @@ fetch() { fi } -fetch_stdout() { +resolve_redirect() { local url="$1" if [ "$DOWNLOADER" = "curl" ]; then - curl -fsSL "$url" + curl -fsSLI -o /dev/null -w '%{url_effective}' "$url" else - wget -qO- "$url" + wget -S --spider "$url" 2>&1 \ + | awk '/^ Location: / { loc = $2 } END { sub(/\r$/, "", loc); if (loc == "") exit 1; print loc }' fi } +# sudo is only usable if it doesn't need a password or can prompt for one. +can_sudo() { + command -v sudo >/dev/null 2>&1 || return 1 + sudo -n true 2>/dev/null && return 0 + { : /dev/null +} + detect_os() { local os os="$(uname -s | tr '[:upper:]' '[:lower:]')" @@ -82,6 +90,10 @@ detect_os() { detect_arch() { local arch arch="$(uname -m)" + # A shell running under Rosetta reports x86_64 on Apple Silicon. + if [ "$arch" = "x86_64" ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then + arch="arm64" + fi case "$arch" in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; @@ -94,14 +106,14 @@ resolve_tag() { echo "v${A2A_VERSION#v}" return fi - local api_url="https://api.github.com/repos/${REPO}/releases/latest" - local tag - tag="$(fetch_stdout "$api_url" \ - | grep -E '"tag_name"' \ - | head -n 1 \ - | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')" - [ -n "$tag" ] || err "failed to detect latest version from $api_url" - echo "$tag" + # Follow the releases/latest redirect to avoid GitHub API rate limits. + local latest_url="https://github.com/${REPO}/releases/latest" + local url + url="$(resolve_redirect "$latest_url")" || err "failed to detect latest version from $latest_url" + case "$url" in + */releases/tag/*) echo "${url##*/}" ;; + *) err "failed to detect latest version from $latest_url (resolved to $url)" ;; + esac } verify_checksum() { @@ -123,11 +135,11 @@ choose_install_dir() { echo "$dir" return fi - if command -v sudo >/dev/null 2>&1; then + if can_sudo; then echo "$dir" return fi - warn "$dir is not writable and sudo is unavailable; falling back to \$HOME/.local/bin" + warn "$dir is not writable and sudo is unusable; falling back to \$HOME/.local/bin" mkdir -p "$HOME/.local/bin" echo "$HOME/.local/bin" }