#!/usr/bin/env bash set -euo pipefail SITE_HOST="${AGENTC_SITE_HOST:-agentc.sh}" PACKAGE_NAME="agentc" BINARY_NAME="agentc" COSIGN_PUBLIC_KEY_B64="LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFR1lPcXhOcU80bjVtdHNObkc2KzdyNHdseWxtMwo0citMQzJWSy94UGhUUXA3bC9lbnlXY0taTmRlbWRxbE02cjhRSlFiN1AzbW9wcXlYQXdUUWQvSEd3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" API_BASE="https://${SITE_HOST}/api/binary" TMP_DIR="" ARG_VERSION="" ARG_PRE=false ARG_INSTALL_DIR="" ARG_TOKEN="" ARG_NO_VERIFY=false ARG_NO_CHECKSUM=false ARG_DRY_RUN=false ARG_UPGRADE=false ARG_QUIET=false # ============================================================================== # COLOURS # ============================================================================== if [ -t 1 ]; then COL_RED="\033[0;31m" COL_YELLOW="\033[0;33m" COL_GREEN="\033[0;32m" COL_CYAN="\033[0;36m" COL_BOLD="\033[1m" COL_RESET="\033[0m" else COL_RED="" COL_YELLOW="" COL_GREEN="" COL_CYAN="" COL_BOLD="" COL_RESET="" fi # ============================================================================== # HELPERS # ============================================================================== log() { [[ "$ARG_QUIET" == "true" ]] || printf "%b\n" "$*"; } info() { log "${COL_CYAN}==>${COL_RESET} $*"; } ok() { log "${COL_GREEN}✔${COL_RESET} $*"; } warn() { printf "%b\n" "${COL_YELLOW}warn:${COL_RESET} $*" >&2; } die() { printf "%b\n" "${COL_RED}error:${COL_RESET} $*" >&2; exit 1; } dry_log() { printf "%b\n" "${COL_YELLOW}dry-run:${COL_RESET} $*"; } need() { command -v "$1" &>/dev/null || die "Required tool not found: $1. Please install it and retry." } # ============================================================================== # VALIDATION # ============================================================================== validate_args() { [[ -n "$ARG_INSTALL_DIR" ]] || die "--install-dir is required." [[ -n "$ARG_TOKEN" ]] || die "--token is required. Provide your agentc.sh early access token." } usage() { cat < Version to install (default: latest stable) --pre Allow pre-release / pipeline snapshot versions -d, --install-dir Directory to install binary into (required) -t, --token agentc.sh early access token (required) --no-verify Skip cosign signature verification --no-checksum Skip SHA256 checksum verification --dry-run Print what would be done without downloading or installing --upgrade Replace existing installation if present -q, --quiet Suppress non-error output -h, --help Show this help message Examples: # Install latest stable curl -sSfL https://install.${SITE_HOST} | bash -s -- \\ --install-dir /usr/local/bin \\ --token # Install a specific version curl -sSfL https://install.${SITE_HOST} | bash -s -- \\ --install-dir /usr/local/bin \\ --token \\ --version 1.2.3 # Install latest pre-release curl -sSfL https://install.${SITE_HOST} | bash -s -- \\ --install-dir /usr/local/bin \\ --token \\ --pre # Upgrade an existing installation curl -sSfL https://install.${SITE_HOST} | bash -s -- \\ --install-dir /usr/local/bin \\ --token \\ --upgrade # Dry run — see what would happen without installing curl -sSfL https://install.${SITE_HOST} | bash -s -- \\ --install-dir /usr/local/bin \\ --token \\ --dry-run EOF exit 0 } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -v|--version) ARG_VERSION="${2:?--version requires a value}"; shift 2 ;; --pre) ARG_PRE=true; shift ;; -d|--install-dir) ARG_INSTALL_DIR="${2:?--install-dir requires a value}"; shift 2 ;; -t|--token) ARG_TOKEN="${2:?--token requires a value}"; shift 2 ;; --no-verify) ARG_NO_VERIFY=true; shift ;; --no-checksum) ARG_NO_CHECKSUM=true; shift ;; --dry-run) ARG_DRY_RUN=true; shift ;; --upgrade) ARG_UPGRADE=true; shift ;; -q|--quiet) ARG_QUIET=true; shift ;; -h|--help) usage ;; *) echo "error: unknown option: $1" >&2; exit 1 ;; esac done } # ============================================================================== # PLATFORM DETECTION # ============================================================================== detect_platform() { local os arch os="$(uname -s)" arch="$(uname -m)" case "$os" in Linux) OS="linux" ;; MINGW*|MSYS*|CYGWIN*) OS="windows" ;; *) die "Unsupported operating system: $os" ;; esac case "$arch" in x86_64|amd64) ARCH="x86_64" ;; *) die "Unsupported architecture: $arch (only x86_64 is supported)" ;; esac if [[ "$OS" == "windows" ]]; then BINARY_FILENAME="${BINARY_NAME}-windows-x86_64.exe" INSTALL_BINARY="${BINARY_NAME}.exe" else BINARY_FILENAME="${BINARY_NAME}-linux-x86_64" INSTALL_BINARY="${BINARY_NAME}" fi CHECKSUM_FILENAME="${BINARY_FILENAME}.sha256" SIG_BUNDLE_FILENAME="${BINARY_FILENAME}.sig.sigstore" log "${COL_BOLD}Platform:${COL_RESET} ${OS}/${ARCH}" } # ============================================================================== # VERSION RESOLUTION # ============================================================================== resolve_version() { if [[ -n "$ARG_VERSION" ]]; then RESOLVED_VERSION="$ARG_VERSION" log "${COL_BOLD}Version:${COL_RESET} ${RESOLVED_VERSION} (pinned)" return fi info "Fetching available versions..." local url="${API_BASE}/versions" [[ "$ARG_PRE" == "true" ]] && url="${url}?pre=true" local raw raw=$(curl \ --fail --silent --show-error --location \ --header "Authorization: Bearer ${ARG_TOKEN}" \ "$url" \ ) || die "Failed to fetch version list.\nCheck your token and ensure you have access." local versions versions=$(printf '%s' "$raw" | grep -o '"[^"]*"' | tr -d '"') [[ -n "$versions" ]] || die "No versions found." RESOLVED_VERSION=$(printf '%s\n' $versions | sort -V | tail -n1) log "${COL_BOLD}Version:${COL_RESET} ${RESOLVED_VERSION}" } # ============================================================================== # DOWNLOAD # ============================================================================== download_file() { local file="$1" dest="$2" label="$3" local url="${API_BASE}/download?version=${RESOLVED_VERSION}&file=${file}" info "Downloading ${label}..." curl \ --fail --silent --show-error --location \ --progress-bar \ --header "Authorization: Bearer ${ARG_TOKEN}" \ --output "$dest" \ "$url" || die "Failed to download ${label}." } # ============================================================================== # VERIFICATION # ============================================================================== verify_checksum() { local binary="$1" checksum_file="$2" info "Verifying checksum..." local expected actual expected=$(tr -d '[:space:]' < "$checksum_file") actual=$(sha256sum "$binary" | awk '{print $1}') [[ "$expected" == "$actual" ]] || die "Checksum mismatch!\n Expected: ${expected}\n Got: ${actual}" ok "Checksum verified." } verify_signature() { local binary="$1" bundle="$2" info "Verifying cosign signature..." local key_file key_file=$(mktemp) trap "rm -f '${key_file}'" RETURN if ! printf '%s' "$COSIGN_PUBLIC_KEY_B64" | base64 -d > "$key_file" 2>/dev/null; then warn "Failed to decode embedded cosign public key — skipping signature verification." return fi if ! command -v cosign &>/dev/null; then warn "cosign not found on PATH — skipping signature verification." warn "Install cosign from https://docs.sigstore.dev/cosign/system_config/installation/" warn "then verify manually with:" warn " cosign verify-blob --key --bundle ${bundle} ${binary}" return fi if cosign verify-blob \ --key "$key_file" \ --bundle "$bundle" \ "$binary" 2>/dev/null; then ok "Signature verified." else die "Signature verification failed. The binary may have been tampered with." fi } # ============================================================================== # INSTALLATION # ============================================================================== install_binary() { local src="$1" dest_dir="$2" dest_name="$3" local dest="${dest_dir}/${dest_name}" info "Installing to ${dest}..." if [[ ! -d "$dest_dir" ]]; then mkdir -p "$dest_dir" || die "Failed to create install directory: ${dest_dir}" fi if [[ -e "$dest" ]]; then info "Upgrading existing installation at ${dest}..." fi cp "$src" "$dest" || die "Failed to copy binary to ${dest}.\nYou may need elevated permissions." chmod +x "$dest" || die "Failed to set executable permission on ${dest}." ok "Installed ${dest_name} to ${dest}." } check_path() { local install_dir="$1" if ! echo ":${PATH}:" | grep -q ":${install_dir}:"; then warn "${install_dir} is not on your PATH." warn "Add it to your shell profile:" warn " export PATH=\"${install_dir}:\$PATH\"" fi } # ============================================================================== # DRY RUN # ============================================================================== print_dry_run_summary() { log "" log "${COL_BOLD}${COL_YELLOW}Dry run — no files will be downloaded or installed.${COL_RESET}" log "────────────────────────────────────────────────────" dry_log "Binary: ${BINARY_FILENAME}" dry_log "Version: ${RESOLVED_VERSION}" dry_log "Platform: ${OS}/${ARCH}" dry_log "Install path: ${ARG_INSTALL_DIR}/${INSTALL_BINARY}" log "" dry_log "Would download via ${API_BASE}/download?version=${RESOLVED_VERSION}&file=:" dry_log " ${BINARY_FILENAME}" if [[ "$ARG_NO_CHECKSUM" != "true" ]]; then dry_log " ${CHECKSUM_FILENAME}" fi if [[ "$ARG_NO_VERIFY" != "true" ]]; then dry_log " ${SIG_BUNDLE_FILENAME}" fi log "" dry_log "Would verify:" if [[ "$ARG_NO_CHECKSUM" == "true" ]]; then dry_log " checksum: skipped (--no-checksum)" else dry_log " checksum: sha256sum ${BINARY_FILENAME} == ${CHECKSUM_FILENAME}" fi if [[ "$ARG_NO_VERIFY" == "true" ]]; then dry_log " signature: skipped (--no-verify)" elif command -v cosign &>/dev/null; then dry_log " signature: cosign verify-blob --key --bundle ${SIG_BUNDLE_FILENAME} ${BINARY_FILENAME}" else dry_log " signature: would be skipped (cosign not found on PATH)" fi log "" dry_log "Would install:" if [[ -e "${ARG_INSTALL_DIR}/${INSTALL_BINARY}" ]]; then if [[ "$ARG_UPGRADE" == "true" ]]; then dry_log " (upgrading existing installation)" else dry_log " (would fail — ${ARG_INSTALL_DIR}/${INSTALL_BINARY} already exists, use --upgrade)" fi fi dry_log " cp ${BINARY_FILENAME} ${ARG_INSTALL_DIR}/${INSTALL_BINARY}" dry_log " chmod +x ${ARG_INSTALL_DIR}/${INSTALL_BINARY}" if ! echo ":${PATH}:" | grep -q ":${ARG_INSTALL_DIR}:"; then log "" dry_log "Note: ${ARG_INSTALL_DIR} is not currently on your PATH." fi log "" log "${COL_YELLOW}Dry run complete. Re-run without --dry-run to install.${COL_RESET}" log "" } # ============================================================================== # MAIN # ============================================================================== main() { parse_args "$@" validate_args log "" log "${COL_BOLD}agentc installer${COL_RESET}" log "────────────────────────────────" need curl need sha256sum need sort detect_platform # Check for existing installation before hitting the network at all if [[ -e "${ARG_INSTALL_DIR}/${INSTALL_BINARY}" ]] && [[ "$ARG_UPGRADE" != "true" ]]; then die "${ARG_INSTALL_DIR}/${INSTALL_BINARY} already exists. Use --upgrade to replace it." fi resolve_version if [[ "$ARG_DRY_RUN" == "true" ]]; then print_dry_run_summary exit 0 fi TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT local tmp_binary="${TMP_DIR}/${BINARY_FILENAME}" local tmp_checksum="${TMP_DIR}/${CHECKSUM_FILENAME}" local tmp_bundle="${TMP_DIR}/${SIG_BUNDLE_FILENAME}" download_file "$BINARY_FILENAME" "$tmp_binary" "$BINARY_FILENAME" if [[ "$ARG_NO_CHECKSUM" == "true" ]]; then warn "Checksum verification skipped (--no-checksum)." else download_file "$CHECKSUM_FILENAME" "$tmp_checksum" "$CHECKSUM_FILENAME" verify_checksum "$tmp_binary" "$tmp_checksum" fi if [[ "$ARG_NO_VERIFY" == "true" ]]; then warn "Signature verification skipped (--no-verify)." else download_file "$SIG_BUNDLE_FILENAME" "$tmp_bundle" "$SIG_BUNDLE_FILENAME" verify_signature "$tmp_binary" "$tmp_bundle" fi install_binary "$tmp_binary" "$ARG_INSTALL_DIR" "$INSTALL_BINARY" check_path "$ARG_INSTALL_DIR" log "" log "${COL_GREEN}${COL_BOLD}Done!${COL_RESET} ${BINARY_NAME} ${RESOLVED_VERSION} installed successfully." log "" if command -v "${ARG_INSTALL_DIR}/${INSTALL_BINARY}" &>/dev/null; then "${ARG_INSTALL_DIR}/${INSTALL_BINARY}" --version 2>/dev/null || true fi } main "$@"