#!/bin/bash set -euo pipefail # ============================================================================= # XHawk CLI Installation Script # Install: curl -fsSL https://xhawk.ai/install | bash # # Installs the full CLI (self-contained binary) via tarball. # No runtime dependencies required (Node.js, Bun, etc.). # # @author puneet (with help from claude AGI) # ============================================================================= # Configuration BINARY_NAME="xhawk" CLI_NAME="XHawk CLI" BLOB_BASE="https://rpvhid8n6jvov4s6.public.blob.vercel-storage.com" MANIFEST_URL="${BLOB_BASE}/releases/latest.json" INSTALL_DIR="${XHAWK_INSTALL_DIR:-$HOME/.local/bin}" CONFIG_DIR="$HOME/.xhawk" DOWNLOAD_DIR="" # Minisign public key for signature verification (Ed25519) MINISIGN_PUBLIC_KEY="RWRsKwjZGs3/e3bXEjrP1+/RA9YdJ5DQTZWChCeZoMNb+Wj9kUjh4oNt" # Validate INSTALL_DIR is under HOME (prevent writing to system paths) case "$INSTALL_DIR" in "$HOME"/*) ;; # ok *) echo " ✗ XHAWK_INSTALL_DIR must be under \$HOME (got: $INSTALL_DIR)" >&2; exit 1 ;; esac # State SUCCESS=false token="" pin_version="" force_install=false is_wsl=false tarball_sig_url="" # Colors (only when outputting to a terminal) GREEN='' BOLD='' CYAN='' ORANGE='' DIM='' NC='' WHITE='' TERRA='' G0='' G1='' G2='' G3='' G4='' G5='' if [[ -t 2 ]]; then GREEN=$(printf '\033[0;32m') BOLD=$(printf '\033[1m') CYAN=$(printf '\033[0;36m') ORANGE=$(printf '\033[38;5;214m') TERRA=$(printf '\033[38;5;173m') WHITE=$(printf '\033[1;37m') DIM=$(printf '\033[2m') NC=$(printf '\033[0m') # Logo gradient: yellow → amber → orange → terracotta (matches CLI logo.ts) G0=$(printf '\033[38;5;227m') # #fde047 light yellow G1=$(printf '\033[38;5;220m') # #fbbf24 amber/gold G2=$(printf '\033[38;5;214m') # #f59e0b orange-amber G3=$(printf '\033[38;5;208m') # #f0944a orange G4=$(printf '\033[38;5;209m') # #e8855a salmon G5=$(printf '\033[38;5;173m') # #E07A5F terracotta fi # ============================================================================= # Utility Functions # ============================================================================= log_info() { printf " %s\n" "$1" >&2 } log() { printf " %s✓%s %s\n" "$GREEN" "$NC" "$1" >&2 } error() { printf "\n %s✗ %s%s\n\n" "$ORANGE" "$1" "$NC" >&2 exit 1 } warning() { printf " ⚠ %s\n" "$1" >&2 } print_header() { echo "" >&2 printf "%s ██╗ ██╗██╗ ██╗ █████╗ ██╗ ██╗██╗ ██╗%s\n" "$G0" "$NC" >&2 printf "%s ╚██╗██╔╝██║ ██║██╔══██╗██║ ██║██║ ██╔╝%s\n" "$G1" "$NC" >&2 printf "%s ╚███╔╝ ███████║███████║██║ █╗ ██║█████╔╝%s\n" "$G2" "$NC" >&2 printf "%s ██╔██╗ ██╔══██║██╔══██║██║███╗██║██╔═██╗%s\n" "$G3" "$NC" >&2 printf "%s ██╔╝ ██╗██║ ██║██║ ██║╚███╔███╔╝██║ ██╗%s\n" "$G4" "$NC" >&2 printf "%s ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═╝%s\n" "$G5" "$NC" >&2 echo "" >&2 printf " %s│%s Context Infrastructure for AI-Native Development %s│%s\n" "$DIM" "$NC" "$DIM" "$NC" >&2 echo "" >&2 } parse_args() { while [[ $# -gt 0 ]]; do case $1 in --help|-h) show_help exit 0 ;; --token|-t) if [[ $# -lt 2 ]]; then error "--token requires a value" fi token="$2" shift 2 ;; --version|-v) if [[ $# -lt 2 ]]; then error "--version requires a value (e.g., 1.0.84)" fi pin_version="$2" MANIFEST_URL="${BLOB_BASE}/releases/v${pin_version}/manifest.json" shift 2 ;; --force|-f) force_install=true shift ;; *) error "Unknown option: $1. Use --help for usage." ;; esac done } show_help() { cat << 'EOF' XHawk CLI Installation Script Supported platforms: macOS, Linux, Windows (via WSL) Usage: curl -fsSL https://xhawk.ai/install | bash curl -fsSL https://xhawk.ai/install | bash -s -- [OPTIONS] Options: --help, -h Show this help message --version, -v VERSION Install a specific version (e.g., 1.0.84) --token, -t TOKEN Configure API token after installation --force, -f Force reinstall even if already installed Examples: # macOS / Linux curl -fsSL https://xhawk.ai/install | bash # Windows (WSL) wsl curl -fsSL https://xhawk.ai/install | bash # Install specific version curl -fsSL https://xhawk.ai/install | bash -s -- --version 1.0.84 # Install with token curl -fsSL https://xhawk.ai/install | bash -s -- --token agt_abc123xyz # Force reinstall curl -fsSL https://xhawk.ai/install | bash -s -- --force Environment Variables: XHAWK_INSTALL_DIR Override install directory (default: ~/.local/bin) For more information: https://xhawk.ai EOF } # ============================================================================= # Download Helpers # ============================================================================= download_file() { local url="$1" local output="${2:-}" # Enforce HTTPS — reject http:// and other protocols case "$url" in https://*) ;; # ok *) error "Refusing non-HTTPS download URL: $url" ;; esac if command -v curl >/dev/null 2>&1; then if [[ -n "$output" ]]; then curl -fsSL -o "$output" "$url" || error "Failed to download: $url" else curl -fsSL "$url" || error "Failed to download: $url" fi elif command -v wget >/dev/null 2>&1; then if [[ -n "$output" ]]; then wget -q -O "$output" "$url" || error "Failed to download: $url" else wget -q -O - "$url" || error "Failed to download: $url" fi else error "curl or wget is required" fi } check_dependencies() { local missing=() if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then missing+=("curl or wget") fi if [[ ${#missing[@]} -gt 0 ]]; then error "Missing required dependencies: ${missing[*]}" fi } # Compute SHA256 checksum — errors out if no hash tool is available compute_sha256() { local file="$1" if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" | cut -d' ' -f1 elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$file" | cut -d' ' -f1 elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$file" | awk '{print $NF}' else error "No SHA-256 tool found (need shasum, sha256sum, or openssl). Cannot verify download integrity." fi } # Verify Minisign signature (optional — advisory if minisign not installed) verify_signature() { local file="$1" local sig_url="$2" if [[ -z "$sig_url" || "$sig_url" == "null" ]]; then return 0 # No signature URL in manifest — skip silently fi if ! command -v minisign >/dev/null 2>&1; then log_info "${DIM}Tip: install minisign for cryptographic signature verification${NC}" return 0 fi local sig_file="$DOWNLOAD_DIR/artifact.minisig" if download_file "$sig_url" "$sig_file" 2>/dev/null; then if minisign -V -P "$MINISIGN_PUBLIC_KEY" -m "$file" -x "$sig_file" >/dev/null 2>&1; then log_info "Signature verified (minisign Ed25519)" else warning "Minisign signature verification failed — continuing (checksum already verified)" fi else warning "Could not download signature file — skipping signature check" fi } # ============================================================================= # Platform Detection # ============================================================================= detect_wsl() { if [[ -f /proc/version ]] && grep -qi "microsoft\|wsl" /proc/version 2>/dev/null; then is_wsl=true elif [[ -n "${WSL_DISTRO_NAME:-}" ]]; then is_wsl=true elif [[ -n "${WSL_INTEROP:-}" ]]; then is_wsl=true fi } detect_platform() { # 1. Check for Windows environment BEFORE uname (catches Git Bash, MSYS2, # Busybox-bash, and other Windows shells where uname may lie) if [[ "${OS:-}" == "Windows_NT" || -n "${MSYSTEM:-}" ]]; then # WSL sets OS=Windows_NT in interop mode but also sets WSL_DISTRO_NAME if [[ -n "${WSL_DISTRO_NAME:-}" || -n "${WSL_INTEROP:-}" ]]; then os="linux" is_wsl=true else echo "" >&2 echo " Windows detected (${MSYSTEM:-native})." >&2 echo "" >&2 echo " XHawk CLI requires WSL (Windows Subsystem for Linux)." >&2 echo " Install WSL from PowerShell: wsl --install" >&2 echo " Then run: wsl curl -fsSL https://xhawk.ai/install | bash" >&2 echo "" >&2 exit 1 fi else case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" detect_wsl ;; MINGW*|MSYS*|CYGWIN*) echo "" >&2 echo " Git Bash / MSYS2 / Cygwin detected." >&2 echo "" >&2 echo " XHawk CLI requires WSL (Windows Subsystem for Linux)." >&2 echo " Install WSL from PowerShell: wsl --install" >&2 echo " Then run: wsl curl -fsSL https://xhawk.ai/install | bash" >&2 echo "" >&2 exit 1 ;; *) error "Unsupported operating system: $(uname -s)" ;; esac fi # 2. Detect architecture case "$(uname -m)" in x86_64|amd64) arch="amd64" ;; arm64|aarch64) arch="arm64" ;; i686|i386) error "32-bit systems are not supported. XHawk requires a 64-bit OS." ;; *) error "Unsupported architecture: $(uname -m). Supported: x86_64, arm64" ;; esac platform_key="${os}-${arch}" # 3. Log WSL clearly so users know what's happening if [[ "$is_wsl" == "true" ]]; then log_info "Windows (WSL) detected — installing Linux binary for WSL" fi } # ============================================================================= # Existing Installation Check # ============================================================================= check_existing_installation() { if [[ "$force_install" == "true" ]]; then return 0 fi # Only check known install location — never execute arbitrary PATH binaries if [[ ! -x "$INSTALL_DIR/xhawk" ]]; then return 0 fi local existing_version existing_version=$("$INSTALL_DIR/xhawk" version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "unknown") log_info "Found existing installation: v${existing_version} at ${INSTALL_DIR}/xhawk" log_info "Upgrading to latest version..." } # ============================================================================= # Manifest & Download # ============================================================================= get_manifest() { manifest_json=$(download_file "$MANIFEST_URL") if command -v jq >/dev/null 2>&1; then version=$(echo "$manifest_json" | jq -r '.version // empty') tarball_url=$(echo "$manifest_json" | jq -r ".tarballs[\"${platform_key}\"].url // empty") tarball_sha256=$(echo "$manifest_json" | jq -r ".tarballs[\"${platform_key}\"].sha256 // empty") tarball_sig_url=$(echo "$manifest_json" | jq -r ".tarballs[\"${platform_key}\"].signature_url // empty") else version=$(echo "$manifest_json" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/') tarball_url=$(echo "$manifest_json" | tr '\n' ' ' | sed -n "s/.*\"tarballs\"[[:space:]]*:[[:space:]]*{[^}]*\"${platform_key}\"[[:space:]]*:[[:space:]]*{[^}]*\"url\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p") tarball_sha256=$(echo "$manifest_json" | tr '\n' ' ' | sed -n "s/.*\"tarballs\"[[:space:]]*:[[:space:]]*{[^}]*\"${platform_key}\"[[:space:]]*:[[:space:]]*{[^}]*\"sha256\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p") tarball_sig_url=$(echo "$manifest_json" | tr '\n' ' ' | sed -n "s/.*\"tarballs\"[[:space:]]*:[[:space:]]*{[^}]*\"${platform_key}\"[[:space:]]*:[[:space:]]*{[^}]*\"signature_url\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p") fi if [[ -z "$version" ]]; then error "Failed to parse version from release manifest" fi } verify_checksum() { local file="$1" local expected="$2" local label="$3" # Require a valid checksum — refuse to install unverified binaries if [[ -z "$expected" || "$expected" == "null" || "$expected" == "$manifest_json" ]]; then rm -f "$file" error "No SHA-256 checksum in manifest for ${label}. Refusing to install unverified binary." fi local actual actual=$(compute_sha256 "$file") if [[ "$actual" != "$expected" ]]; then rm -f "$file" error "Checksum verification failed for ${label}. Expected: ${expected}, Got: ${actual}" fi } # ============================================================================= # Install: Full (standalone tarball) # ============================================================================= install_full() { if [[ -z "$tarball_url" || "$tarball_url" == "null" || "$tarball_url" == "$manifest_json" ]]; then error "No tarball available for ${platform_key}. Please report this at https://xhawk.ai" fi local tarball_file="$DOWNLOAD_DIR/xhawk.tar.gz" download_file "$tarball_url" "$tarball_file" verify_checksum "$tarball_file" "$tarball_sha256" "tarball" # Validate tarball entries — reject path traversal (Zip Slip prevention) if tar -tzf "$tarball_file" | grep -qE '(^|/)\.\.(/|$)'; then rm -f "$tarball_file" error "Tarball contains path traversal entries. Aborting." fi # Extract to ~/.xhawk/ (tarball contains xhawk/ directory with bin/xh) rm -rf "$CONFIG_DIR/bin" "$CONFIG_DIR/dist" "$CONFIG_DIR/xh" mkdir -p "$CONFIG_DIR" tar -xzf "$tarball_file" -C "$CONFIG_DIR" --strip-components=1 chmod +x "$CONFIG_DIR/bin/xh" # Create symlinks in ~/.local/bin/ (xhawk is an alias for xh) mkdir -p "$INSTALL_DIR" ln -sf "$CONFIG_DIR/bin/xh" "$INSTALL_DIR/xh" ln -sf "$CONFIG_DIR/bin/xh" "$INSTALL_DIR/xhawk" } # ============================================================================= # Post-Install Configuration # ============================================================================= setup_config_dir() { mkdir -p "$CONFIG_DIR" && chmod 700 "$CONFIG_DIR" mkdir -p "$CONFIG_DIR/projects" if [[ -n "$token" ]]; then # Validate token format (alphanumeric, underscores, hyphens only) if [[ ! "$token" =~ ^[a-zA-Z0-9_-]+$ ]]; then error "Invalid token format. Must contain only alphanumeric characters, underscores, and hyphens." fi # Quoted heredoc prevents shell expansion; umask 077 restricts file perms ( umask 077 cat > "$CONFIG_DIR/config.json" << 'CONFIGEOF' { "token": "TOKEN_PLACEHOLDER", "cloud_url": "wss://api.xhawk.ai" } CONFIGEOF ) # Replace placeholder safely (token is validated alphanumeric above) # Use temp file for portability across macOS and Linux sed sed "s/TOKEN_PLACEHOLDER/${token}/" "$CONFIG_DIR/config.json" > "$CONFIG_DIR/config.json.tmp" mv "$CONFIG_DIR/config.json.tmp" "$CONFIG_DIR/config.json" log "Agent token configured" fi } add_to_shell_config() { if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then return 0 fi local shell_config="" local current_shell current_shell=$(basename "${SHELL:-/bin/bash}") case "$current_shell" in zsh) shell_config="$HOME/.zshrc" ;; bash) if [[ -f "$HOME/.bashrc" ]]; then shell_config="$HOME/.bashrc" elif [[ -f "$HOME/.bash_profile" ]]; then shell_config="$HOME/.bash_profile" fi ;; fish) shell_config="$HOME/.config/fish/config.fish" ;; esac echo "" >&2 printf " %sIMPORTANT:%s Add %s to your PATH:\n" "$ORANGE" "$NC" "$INSTALL_DIR" >&2 echo "" >&2 if [[ "$current_shell" == "fish" ]]; then echo " fish_add_path $INSTALL_DIR" >&2 else echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" >&2 fi if [[ -n "$shell_config" ]]; then echo "" >&2 printf " Add the line above to %s%s%s, then restart your terminal.\n" "$DIM" "$shell_config" "$NC" >&2 fi if [[ "$is_wsl" == "true" ]]; then echo "" >&2 log_info "Note: This binary runs in your WSL terminal, not in" log_info "PowerShell or CMD. Always use WSL to run xhawk." fi } # ============================================================================= # Cleanup # ============================================================================= cleanup() { if [[ "$SUCCESS" == "false" ]]; then echo "" >&2 echo " Installation failed." >&2 echo "" >&2 fi if [[ -n "$DOWNLOAD_DIR" ]] && [[ -d "$DOWNLOAD_DIR" ]]; then rm -rf "$DOWNLOAD_DIR" fi } # ============================================================================= # Main # ============================================================================= main() { parse_args "$@" print_header trap cleanup EXIT DOWNLOAD_DIR=$(mktemp -d "${TMPDIR:-/tmp}/xhawk-install-XXXXXX") log_info "Starting XHawk CLI installation..." detect_platform log_info "Detected platform: ${platform_key}" check_dependencies log_info "All required tools are available" log_info "Install directory: ${INSTALL_DIR}" check_existing_installation get_manifest log_info "Downloading xhawk v${version}..." install_full log_info "Checksum verified" local tarball_file="$DOWNLOAD_DIR/xhawk.tar.gz" verify_signature "$tarball_file" "${tarball_sig_url:-}" setup_config_dir log_info "Done!" SUCCESS=true # Telemetry: record install (best-effort, non-blocking) (curl -s -X POST "https://api.xhawk.ai/api/v1/telemetry/install" \ -H "Content-Type: application/json" \ -d "{\"platform\":\"${platform_key}\",\"version\":\"${version}\"}" \ 2>/dev/null &) || true local sig_status="unsigned" if command -v minisign >/dev/null 2>&1 && [[ -n "${tarball_sig_url:-}" && "${tarball_sig_url}" != "null" ]]; then sig_status="signed" fi echo "" >&2 printf " %s%s✓ XHawk CLI v${version}%s installed to %s~/.local/bin/xhawk%s\n" "$BOLD" "$GREEN" "$NC" "$DIM" "$NC" >&2 printf " %sPlatform: %s • Checksum: verified • Signature: %s%s\n" "$DIM" "$platform_key" "$sig_status" "$NC" >&2 # Ensure binary is on PATH for setup if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then export PATH="$INSTALL_DIR:$PATH" add_to_shell_config fi echo "" >&2 printf " %scd into your project directory and run %s%sxh init%s%s to get started%s\n" \ "$ORANGE" "$BOLD" "$ORANGE" "$NC" "$ORANGE" "$NC" >&2 echo "" >&2 } main "$@"