#!/usr/bin/env bash # Copyright 2026 Schelling Point Labs Inc # --- BUNDLED INSTALL SCRIPT --- # This is an auto-generated bundled version. For development, see install/ directory. # # Usage: curl -fsSL https://install.agent-harbor.com | bash set -euo pipefail # ============================================================================ # Build provenance (embedded at bundle time) # ============================================================================ BUILD_COMMIT_SHA="" BUILD_PR_URL="" BUILD_IS_PR="" # ============================================================================ # PART 1: Embedded lib/common.sh # ============================================================================ # Copyright 2026 Schelling Point Labs Inc # # Common utilities for Agent Harbor installation scripts # This library provides shared functions for platform detection, # command checking, HTTP fetching, logging, and error handling. # # Usage: # source "$(dirname "$0")/lib/common.sh" # Enable strict mode if not already enabled set -euo pipefail # ============================================================================ # Configuration # ============================================================================ # Binary cache configuration (optional). # # There is no public Agent Harbor binary cache: the old Cachix cache was # migrated to Attic and the public Attic buckets were then decommissioned # (see agent-harbor-infra Public-Attic-Cache-Decommission). Installs fall back # to cache.nixos.org + source builds by default. Set CACHIX_URL / CACHIX_KEY to # opt into a private or self-hosted substituter. CACHIX_URL="${CACHIX_URL:-}" CACHIX_KEY="${CACHIX_KEY:-}" NIXPKGS_KEY="${NIXPKGS_KEY:-cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=}" # Allow disabling colors NO_COLOR="${NO_COLOR:-false}" # ============================================================================ # Terminal colors # ============================================================================ setup_colors() { if [[ "${NO_COLOR}" == "true" ]] || ! [[ -t 1 ]]; then RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC='' else RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' fi } # Initialize colors setup_colors # ============================================================================ # Logging functions # ============================================================================ # Verbose mode flag - set to "true" to enable debug output VERBOSE="${VERBOSE:-false}" info() { printf "%b[INFO]%b %s\n" "${GREEN}" "${NC}" "$*" >&2 } warn() { printf "%b[WARN]%b %s\n" "${YELLOW}" "${NC}" "$*" >&2 } error() { printf "%b[ERROR]%b %s\n" "${RED}" "${NC}" "$*" >&2 } die() { error "$*" exit 1 } step() { printf "\n%b==>%b %b%s%b\n" "${BLUE}" "${NC}" "${BOLD}" "$*" "${NC}" >&2 } # Debug logging - only prints when VERBOSE is "true" debug() { if [[ "$VERBOSE" == "true" ]]; then printf "%b[DEBUG]%b %s\n" "${BLUE}" "${NC}" "$*" >&2 fi } # ============================================================================ # Confirmation for dangerous commands # ============================================================================ # Check if running in non-interactive mode (no terminal access) # Returns 0 if non-interactive, 1 if interactive is_non_interactive() { # If stdin is a TTY, we're definitely interactive if [[ -t 0 ]] && [[ -t 1 ]]; then return 1 fi # Even with piped stdin (curl | bash), we might have a controlling terminal # Check if /dev/tty is available for user interaction if [[ -r /dev/tty ]] && [[ -c /dev/tty ]]; then # We can read from the controlling terminal return 1 fi # No TTY access at all - truly non-interactive return 0 } # Confirm a dangerous action with the user # Usage: confirm_danger [default_answer] # intent_description: What we're trying to accomplish (e.g., "Install Nix package manager") # command_description: The actual command that will run (e.g., "curl ... | sh -s -- install") # default_answer: "y" or "n" (default: "y" if interactive, "n" if non-interactive) # Returns: 0 if confirmed, 1 if declined confirm_danger() { local intent="$1" local command="$2" local default_answer="${3:-}" # Auto-confirm if explicitly requested if [[ "${NO_CONFIRM:-false}" == "true" ]]; then return 0 fi # Set default based on interactivity if not provided if [[ -z "$default_answer" ]]; then if is_non_interactive; then default_answer="n" else default_answer="y" fi fi printf "\n" >&2 step "$intent" # Print the command in a visually distinct block printf "%b┃%b %s\n" "${BLUE}" "${NC}" "This will run the following command:" >&2 printf "%b┃%b\n" "${BLUE}" "${NC}" >&2 # Indent multi-line commands for readability echo "$command" | while IFS= read -r line; do printf "%b┃%b %s\n" "${BLUE}" "${NC}" "$line" >&2 done printf "%b┃%b\n" "${BLUE}" "${NC}" >&2 # In non-interactive mode, use default answer if is_non_interactive; then if [[ "$default_answer" == "y" ]]; then info "Non-interactive mode: proceeding (default: yes)" return 0 else warn "Non-interactive mode: skipping (default: no)" return 1 fi fi # Interactive prompt local prompt_text if [[ "$default_answer" == "y" ]]; then prompt_text="[Y/n]" else prompt_text="[y/N]" fi printf "%b[CONFIRM]%b Do you want to proceed? %s " "${YELLOW}" "${NC}" "$prompt_text" >&2 local response if ! read -r response /dev/null; then # Failed to read from terminal (race condition or terminal detached) warn "Could not read from terminal, using default: $default_answer" if [[ "$default_answer" == "y" ]]; then return 0 else return 1 fi fi # Normalize response response=$(echo "$response" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') if [[ -z "$response" ]]; then response="$default_answer" fi if [[ "$response" == "y" || "$response" == "yes" ]]; then return 0 fi warn "User declined: $intent" return 1 } # ============================================================================ # Command checking # ============================================================================ # Check if a command exists need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then die "Required command not found: $1" fi } # Check if a command exists, return 0/1 has_cmd() { command -v "$1" >/dev/null 2>&1 } # ============================================================================ # HTTP fetching # ============================================================================ # Fetch a URL to stdout using curl or wget # Usage: fetch [extra_curl_args...] fetch() { local url="$1" shift if has_cmd curl; then curl -fsSL "$@" "$url" elif has_cmd wget; then wget -q "$@" -O - "$url" else die "Neither curl nor wget is available. Please install one of them." fi } # Fetch a URL and save to file # Usage: fetch_to [extra_curl_args...] fetch_to() { local url="$1" local output="$2" shift 2 if has_cmd curl; then curl -fsSL "$@" -o "$output" "$url" elif has_cmd wget; then wget -q "$@" -O "$output" "$url" else die "Neither curl nor wget is available. Please install one of them." fi } # ============================================================================ # Platform detection # ============================================================================ # Detect platform in nix format: x86_64-linux, aarch64-linux, etc. detect_platform() { local arch os arch=$(uname -m) os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in linux) case "$arch" in x86_64 | amd64) echo "x86_64-linux" ;; aarch64 | arm64) echo "aarch64-linux" ;; *) die "Unsupported Linux architecture: $arch" ;; esac ;; darwin) case "$arch" in x86_64 | amd64) echo "x86_64-darwin" ;; aarch64 | arm64) echo "aarch64-darwin" ;; *) die "Unsupported macOS architecture: $arch" ;; esac ;; *) die "Unsupported operating system: $os" ;; esac } # Detect OS/distribution ID detect_os() { if [[ -f /etc/os-release ]]; then # shellcheck source=/dev/null source /etc/os-release echo "$ID" elif command -v uname >/dev/null 2>&1; then uname -s | tr '[:upper:]' '[:lower:]' else echo "unknown" fi } # Detect OS version detect_os_version() { if [[ -f /etc/os-release ]]; then # shellcheck source=/dev/null source /etc/os-release echo "$VERSION_ID" else echo "unknown" fi } # ============================================================================ # Privilege escalation # ============================================================================ # Check if running as root is_root() { [[ "$(id -u)" -eq 0 ]] } # Return "sudo" if not root, empty string if root # Usage: $(maybe_sudo) command... maybe_sudo() { if is_root; then echo "" else echo "sudo" fi } # Check if we have root privileges, set up sudo if needed ensure_privileges() { if is_root; then # Already root return 0 fi if ! has_cmd sudo; then die "This script requires root privileges, but sudo is not available." fi info "This script requires root privileges. You may be prompted for your password." # Validate sudo credentials if ! sudo -v >/dev/null 2>&1; then die "sudo authentication failed. Please ensure you have sudo access." fi } # ============================================================================ # Store paths (embedded at build time) # ============================================================================ # Store paths for each platform, embedded directly in the install script. # These are populated by `just generate-install-script` at build time. # When empty, installation will fall back to an error message. STORE_PATH_x86_64_linux="${STORE_PATH_x86_64_linux:-}" STORE_PATH_aarch64_linux="${STORE_PATH_aarch64_linux:-/nix/store/p1f7kyxdrsd2xm6mlfs05663s1jk4bvz-agent-harbor}" STORE_PATH_aarch64_darwin="${STORE_PATH_aarch64_darwin:-/nix/store/0kawir43nk2f81c6gxbaiwx699gvc6bh-agent-harbor-cli-0.1.0}" # Agent bundle store paths (populated by `just generate-install-script`) STORE_PATH_AGENTS_x86_64_linux="${STORE_PATH_AGENTS_x86_64_linux:-/nix/store/aaiir2pab73j1amkk7i2vyp70d1ng5z0-ah-agents}" STORE_PATH_AGENTS_aarch64_linux="${STORE_PATH_AGENTS_aarch64_linux:-}" STORE_PATH_AGENTS_aarch64_darwin="${STORE_PATH_AGENTS_aarch64_darwin:-/nix/store/n003axivks4nhs5a3qwkyav4kwah9kw9-ah-agents}" # Get the embedded store path for the current platform # Returns the store path on stdout, or returns 1 if not available get_store_path() { local platform platform=$(detect_platform) local store_path="" case "$platform" in x86_64-linux) store_path="$STORE_PATH_x86_64_linux" ;; aarch64-linux) store_path="$STORE_PATH_aarch64_linux" ;; aarch64-darwin) store_path="$STORE_PATH_aarch64_darwin" ;; *) die "Unsupported platform: $platform" ;; esac if [[ -z "$store_path" ]]; then warn "No store path embedded for platform: $platform" warn "The install script may not have been built with store paths." warn "Run 'just generate-install-script' to regenerate." return 1 fi info "Store path for $platform: $store_path" echo "$store_path" } # Get the embedded agent bundle store path for the current platform. # Returns the store path on stdout, or returns 1 if not available. get_agents_store_path() { local platform platform=$(detect_platform) local store_path="" case "$platform" in x86_64-linux) store_path="$STORE_PATH_AGENTS_x86_64_linux" ;; aarch64-linux) store_path="$STORE_PATH_AGENTS_aarch64_linux" ;; aarch64-darwin) store_path="$STORE_PATH_AGENTS_aarch64_darwin" ;; *) return 1 ;; esac if [[ -z "$store_path" ]]; then return 1 fi debug "Agent bundle store path for $platform: $store_path" echo "$store_path" } # ============================================================================ # AI coding agent installation # ============================================================================ # Agent installation mode: "yes", "no", or empty (prompt interactively) INSTALL_AGENTS="${INSTALL_AGENTS:-}" # Prompt user to install AI coding agents after the main CLI install. # Only called for Nix-based installations. Skipped in non-interactive mode # unless --with-agents is explicitly passed. prompt_agent_install() { if [[ "$INSTALL_AGENTS" == "no" ]]; then info "Skipping agent installation (--without-agents)" return 0 fi if [[ "$INSTALL_AGENTS" == "yes" ]]; then install_agents return $? fi # In non-interactive mode without explicit flag, skip if is_non_interactive; then info "Non-interactive mode: skipping agent installation" info "Use --with-agents to include AI coding agents" return 0 fi printf "\n" step "AI Coding Agents" printf "\n" printf "Agent Harbor can install AI coding agents into your PATH.\n" printf "The following agents are included in the bundle:\n" printf "\n" printf " • Claude Code (Anthropic)\n" printf " • Gemini CLI (Google)\n" printf " • OpenCode (open source)\n" printf " • Codex (OpenAI)\n" printf " • Goose (Block)\n" printf " • Amp (Sourcegraph)\n" printf " • Qwen Code (Alibaba)\n" printf " • Cursor CLI (Cursor)\n" printf " • Windsurf (Codeium) [Linux x86_64 only]\n" printf " • Copilot CLI, ccusage, crush, and more\n" printf "\n" printf "Install all AI coding agents? [Y/n]: " local response if ! read -r response /dev/null; then debug "Could not read from terminal, skipping agent installation" return 0 fi response=$(echo "$response" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') case "$response" in "" | y | yes) install_agents ;; n | no) info "Skipping AI agent installation" info "You can install agents later with:" info " nix profile add github:agent-harbor/agent-harbor#ah-agents" ;; *) warn "Invalid choice, skipping agent installation" info "You can install agents later with:" info " nix profile add github:agent-harbor/agent-harbor#ah-agents" ;; esac } # Install AI coding agents from the embedded store path. # Unlike the main CLI, there is no github flake fallback because the repo # may be private. Agents are only available in bundled install scripts that # have the store path pre-embedded. install_agents() { info "Installing AI coding agents..." local agents_store_path="" agents_store_path=$(get_agents_store_path 2>/dev/null) || true if [[ -z "$agents_store_path" ]]; then warn "Agent bundle not available in this install script" info "You can install agents manually after the repo is public:" info " nix profile add github:agent-harbor/agent-harbor#ah-agents" return 1 fi if install_from_store_path "$agents_store_path"; then info "AI coding agents installed successfully! 🤖" return 0 fi warn "Failed to install AI coding agents from binary cache" return 1 } # ============================================================================ # Package manager confirmation helpers # ============================================================================ # Confirm native package manager installation # This provides a consistent confirmation experience across all package managers # Usage: confirm_package_manager_install [commands...] confirm_package_manager_install() { local pm_name="$1" local pm_description="$2" shift 2 local intent="Install Agent Harbor via $pm_name This will use $pm_description to install Agent Harbor and set up our official repository for future updates." # Build command list from remaining arguments local commands="" for cmd in "$@"; do if [[ -n "$commands" ]]; then commands="$commands $cmd" else commands="$cmd" fi done confirm_danger "$intent" "$commands" "y" } # ============================================================================ # Interactive installation method selection # ============================================================================ # Return 0 when the current bundled installer represents a PR build. is_pr_build_context() { if [[ "${BUILD_IS_PR:-}" == "true" ]] || [[ "${BUILD_IS_PR:-}" == "1" ]]; then return 0 fi [[ -n "${BUILD_PR_URL:-}" ]] } # Display installation method comparison and prompt user for choice. # Runs system checks first (OS detection, Nix availability, native package # manager detection) and presents results alongside the options so the user # can make an informed decision. # Sets INSTALL_MODE to "nix" or "native" based on user input. # Returns 0 on success, 1 if cannot prompt (non-interactive) or user cancels. prompt_install_method() { # If we're in non-interactive mode without explicit choice, we can't prompt if is_non_interactive; then error "Cannot prompt for installation method in non-interactive mode." error "Please specify --nix or --native explicitly." error "" error "Example: curl -fsSL https://install.agent-harbor.com/install.sh | bash -s -- --nix" return 1 fi # ------------------------------------------------------------------ # Run system checks upfront # ------------------------------------------------------------------ local distro distro=$(detect_os) local nix_installed="no" ensure_nix_profile_in_path if check_nix_installed 2>/dev/null; then nix_installed="yes" fi local native_available="no" local pkg_manager="unknown" case "$distro" in debian | ubuntu | fedora | rhel | centos | rocky | almalinux | amzn | alpine | arch | manjaro | opensuse | darwin) native_available="yes" # detect_package_manager is defined in install-native.sh; guard in case # we're running inside the bundled script where it may not exist yet. if type -t detect_package_manager &>/dev/null; then pkg_manager=$(detect_package_manager "$distro") else # Inline fallback matching install-native.sh logic case "$distro" in debian | ubuntu) pkg_manager="apt" ;; fedora) pkg_manager="dnf" ;; rhel | centos | rocky | almalinux | amzn) pkg_manager="yum" ;; alpine) pkg_manager="apk" ;; arch | manjaro) pkg_manager="pacman" ;; opensuse) pkg_manager="zypper" ;; darwin) pkg_manager="macos" ;; esac fi ;; esac # ------------------------------------------------------------------ # Display detected system information # ------------------------------------------------------------------ printf "\n" step "Choose Installation Method" printf "\n" printf "%bSystem check results:%b\n" "${BOLD}" "${NC}" printf " OS/distro detected: %b%s%b\n" "${BOLD}" "$distro" "${NC}" if [[ "$nix_installed" == "yes" ]]; then printf " Nix package manager: %b✓ installed%b\n" "${GREEN}" "${NC}" else printf " Nix package manager: %b✗ not installed%b (can be set up automatically)\n" "${YELLOW}" "${NC}" fi if [[ "$native_available" == "yes" ]]; then printf " Native package manager: %b✓ %s%b\n" "${GREEN}" "$pkg_manager" "${NC}" else printf " Native package manager: %b✗ unsupported distro%b\n" "${RED}" "${NC}" fi printf "\n" # ------------------------------------------------------------------ # Check if this is a PR build where native packages are intentionally disabled. # Main/release installers still embed BUILD_COMMIT_SHA for provenance, so commit SHA # alone is not a reliable signal for gating native installation. # ------------------------------------------------------------------ local is_pr_build="no" if is_pr_build_context; then is_pr_build="yes" fi # ------------------------------------------------------------------ # Show option details # ------------------------------------------------------------------ PRO="${GREEN}+${NC}" CON="${RED}-${NC}" printf "%b1) Nix Installation%b" "${BOLD}" "${NC}" if [[ "$nix_installed" == "yes" ]]; then printf " %b★ Nix already installed — fast path!%b" "${GREEN}" "${NC}" fi printf "\n" printf " ${PRO} %b\n" \ "Fast binary cache install (seconds, not minutes)" \ "Option to install AI coding agents (Claude Code, Goose, Gemini, OpenCode, Codex, Amp, etc.)" \ "Node.js, npm, and all needed runtimes included" \ "Can install/update/delete packages atomically and securely" \ "Isolated in \`/nix\` — doesn't interfere with system" printf " ${CON} %b\n" \ "One-time Nix setup required (<1GB storage)" \ "Newer ecosystem, may be unfamiliar" printf "\n" printf "%b2) Native Installation%b" "${BOLD}" "${NC}" if [[ "$is_pr_build" == "yes" ]]; then printf " %b⚠ native packages are not available from arbitrary commits%b" "${RED}" "${NC}" elif [[ "$native_available" == "yes" ]]; then printf " %b(via %s)%b" "${GREEN}" "$pkg_manager" "${NC}" else printf " %b⚠ not available for %s%b" "${RED}" "$distro" "${NC}" fi printf "\n" printf " ${PRO} %b\n" \ "Uses system package manager (${pkg_manager})" \ "No extra package manager needed" \ "Familiar workflow for sysadmins" \ "Integrates with system updates" printf " ${CON} %b\n" \ "AI agents NOT in distro repos — must install separately" \ "Platform-specific, \`root\`/\`sudo\` often required" printf "\n" # ------------------------------------------------------------------ # Determine default choice # ------------------------------------------------------------------ local default_choice="1" if [[ "$is_pr_build" == "yes" ]]; then # Per-commit builds only have Nix packages available default_choice="1" elif [[ "$nix_installed" == "yes" ]]; then # Nix is already there — it's the obvious fast path default_choice="1" elif [[ "$native_available" == "no" ]]; then # Can't do native anyway default_choice="1" else # Both available, no Nix yet — default to native (no extra tooling) default_choice="2" fi local prompt_text="[1/2, default: ${default_choice}]" printf "Enter your choice %s: " "$prompt_text" local response if ! read -r response /dev/null; then warn "Could not read from terminal, using default: $default_choice" response="$default_choice" fi # Normalize and validate response response=$(echo "$response" | tr -d '[:space:]') if [[ -z "$response" ]]; then response="$default_choice" fi case "$response" in 1 | nix | Nix | NIX) INSTALL_MODE="nix" info "Selected: Nix installation" ;; 2 | native | Native | NATIVE) if [[ "$is_pr_build" == "yes" ]]; then warn "Native installation is not available for per-commit builds" info "Native packages are only published for tagged releases" info "Falling back to Nix installation" INSTALL_MODE="nix" elif [[ "$native_available" != "yes" ]]; then warn "Native installation is not available for $distro" info "Falling back to Nix installation" INSTALL_MODE="nix" else INSTALL_MODE="native" info "Selected: Native installation" fi ;; *) warn "Invalid choice: $response" info "Please enter '1' for Nix or '2' for Native" return 1 ;; esac printf "\n" return 0 } # ============================================================================ # Nix installation helpers # ============================================================================ # Check if Nix is already installed check_nix_installed() { debug "Checking for 'nix' command..." has_cmd nix } # Ensure Nix profile directories are in PATH # This is needed when running via "curl | bash" where the current shell # doesn't inherit the user's normal shell environment with .nix-profile/bin ensure_nix_profile_in_path() { # User profile (nix profile add installs here) if [[ -d "$HOME/.nix-profile/bin" ]] && [[ ":$PATH:" != *":$HOME/.nix-profile/bin:"* ]]; then export PATH="$HOME/.nix-profile/bin:$PATH" fi # System profile (daemon installs here) if [[ -d "/nix/var/nix/profiles/default/bin" ]] && [[ ":$PATH:" != *":/nix/var/nix/profiles/default/bin:"* ]]; then export PATH="/nix/var/nix/profiles/default/bin:$PATH" fi } # Check if Agent Harbor CLI is already installed # Returns 0 if installed, 1 otherwise is_ah_installed() { ensure_nix_profile_in_path command -v ah >/dev/null 2>&1 } # Handle already-installed scenario # Prints appropriate message and returns 0 (caller decides what to do) handle_already_installed() { info "Agent Harbor CLI is already installed" ah --version info "Run 'ah --help' to get started, or reinstall to update" return 0 } # Verify installation and print appropriate message # Returns 0 if ah is found, 1 otherwise (but doesn't exit) verify_installation() { # Ensure PATH is up to date before checking ensure_nix_profile_in_path # Detect if we can persist PATH changes: # - Sourced script: yes (runs in parent shell) # - Terminal stdin: no (script cannot modify parent environment) # - Pipe stdin: no (curl|bash, script cannot modify parent environment) local can_persist_path=false if [[ -t 0 ]] && [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then # Interactive terminal AND sourced (source script.sh) can_persist_path=true fi if is_ah_installed; then info "" info "✅ Agent Harbor CLI installed!" ah --version # If PATH changes won't persist, advise user if [[ "$can_persist_path" != true ]]; then info "" info "Note: Run 'exec \$SHELL' to use 'ah' in this terminal" info " (Scripts cannot modify parent shell environment)" fi return 0 else warn "'ah' not immediately available in current shell" info "" info "Installation complete! 🎉" info "To use 'ah', start a new shell session or run: exec \$SHELL" return 1 fi } # Unified entry point handler - checks if installed, returns 1 to signal "proceed with install" # Usage: check_existing_installation || return 0 check_existing_installation() { ensure_nix_profile_in_path if is_ah_installed; then handle_already_installed return 1 # Signal to caller: don't proceed with installation fi return 0 # Proceed with installation } # Install Nix package manager # Returns 0 on success, 1 on failure. install_nix() { if check_nix_installed; then info "Nix already installed: $(nix --version)" return 0 fi local os os=$(uname -s | tr '[:upper:]' '[:lower:]') # Build the command description based on platform local nix_installer_cmd local intent="Install Nix package manager" case "$os" in linux) nix_installer_cmd="curl -fsSL https://artifacts.nixos.org/nix-installer | sh -s -- install linux --no-confirm --init none" ;; darwin) nix_installer_cmd="curl -fsSL https://artifacts.nixos.org/nix-installer | sh -s -- install macos --no-confirm" ;; *) error "Unsupported platform for Nix installation: $os" return 1 ;; esac # Add context about what Nix is local full_intent full_intent="$intent This will install the Nix package manager from the official NixOS installer. Nix is a powerful package manager that enables reproducible builds and will allow Agent Harbor to be installed from our binary cache. After installation, Nix will: - Create /nix directory for storing packages - Configure the Nix daemon and nixbld users for multi-user builds - Add Cachix binary cache for faster Agent Harbor installs" # Confirm before installing Nix if ! confirm_danger "$full_intent" "$nix_installer_cmd" "y"; then warn "Nix installation declined by user" info "You can install Nix manually: https://nixos.org/download" return 1 fi info "Installing Nix package manager..." case "$os" in linux) if ! fetch "https://artifacts.nixos.org/nix-installer" | sh -s -- install linux --no-confirm --init none; then error "Nix installer failed on Linux" return 1 fi ;; darwin) if ! fetch "https://artifacts.nixos.org/nix-installer" | sh -s -- install macos --no-confirm; then error "Nix installer failed on macOS" return 1 fi ;; esac # Source the Nix environment (Linux only) if [[ "$os" == "linux" ]]; then # shellcheck source=/dev/null if [[ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]]; then . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh fi fi # Configure Nix mkdir -p ~/.config/nix if [[ -n "${GITHUB_TOKEN:-}" ]]; then echo "access-tokens = github.com=$GITHUB_TOKEN" >>~/.config/nix/nix.conf fi { echo "experimental-features = nix-command flakes" if [[ -n "$CACHIX_URL" ]]; then echo "extra-substituters = $CACHIX_URL" echo "extra-trusted-public-keys = $NIXPKGS_KEY $CACHIX_KEY" fi echo "narinfo-cache-negative-ttl = 0" } >>~/.config/nix/nix.conf info "Nix installed and configured" } # Install a package using nix profile (handles different nix versions) nix_profile_install() { local target="$1" local extra_args=("${@:2}") # Try nix profile add first (preferred, non-deprecated) if nix profile add --help >/dev/null 2>&1; then debug "Using 'nix profile add'" nix profile add "$target" "${extra_args[@]}" return $? fi # Fall back to nix profile install (deprecated alias) if nix profile install --help >/dev/null 2>&1; then debug "Using 'nix profile install' (deprecated)" nix profile install "$target" "${extra_args[@]}" return $? fi # Fall back to nix-env (legacy) debug "Using 'nix-env -i' (legacy)" nix-env -i "$target" } # Track whether we've already offered the Nix trust self-heal during this run. NIX_TRUST_PREFLIGHT_DONE="${NIX_TRUST_PREFLIGHT_DONE:-false}" # Run a command with sudo when needed. run_maybe_sudo() { if is_root; then "$@" else sudo "$@" fi } # Return 0 if current user appears in effective Nix trusted-users. nix_current_user_is_trusted() { local current_user trusted_users token group_name current_user="$(id -un 2>/dev/null || true)" if [[ -z "$current_user" ]]; then return 1 fi trusted_users="$(nix show-config trusted-users 2>/dev/null | sed -n 's/^trusted-users = //p' || true)" if [[ -z "$trusted_users" ]]; then return 1 fi for token in $trusted_users; do case "$token" in "*") return 0 ;; "$current_user") return 0 ;; @*) group_name="${token#@}" if id -nG "$current_user" 2>/dev/null | tr ' ' '\n' | grep -Fxq "$group_name"; then return 0 fi ;; esac done return 1 } # Choose the system-level Nix config file to update. nix_system_config_file() { if [[ -f /etc/nix/nix.custom.conf ]]; then echo "/etc/nix/nix.custom.conf" return 0 fi echo "/etc/nix/nix.conf" } # Return 0 if a Nix config file already trusts the specified user. nix_config_trusts_user() { local config_file="$1" local user_name="$2" # shellcheck disable=SC2016 run_maybe_sudo awk -F '=' -v user_name="$user_name" ' /^[[:space:]]*(trusted-users|extra-trusted-users)[[:space:]]*=/ { value = $2 gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) n = split(value, parts, /[[:space:]]+/) for (i = 1; i <= n; i++) { if (parts[i] == user_name) { found = 1 exit } } } END { exit(found ? 0 : 1) } ' "$config_file" } # Restart the Nix daemon after system-level config changes. restart_nix_daemon() { local os os="$(uname -s | tr '[:upper:]' '[:lower:]')" case "$os" in darwin) run_maybe_sudo launchctl kickstart -k system/org.nixos.nix-daemon ;; linux) if has_cmd systemctl; then run_maybe_sudo systemctl restart nix-daemon || run_maybe_sudo systemctl restart nix-daemon.service elif has_cmd service; then run_maybe_sudo service nix-daemon restart else warn "Could not find a service manager to restart nix-daemon" return 1 fi ;; *) warn "Unsupported platform for automatic nix-daemon restart: $os" return 1 ;; esac } # Try to self-heal Nix trusted-user config and daemon restart. configure_nix_trusted_user() { local current_user config_file attempt max_attempts current_user="$(id -un 2>/dev/null || true)" if [[ -z "$current_user" ]]; then warn "Could not determine current user for Nix trust configuration" return 1 fi if ! ensure_privileges; then return 1 fi config_file="$(nix_system_config_file)" info "Using Nix system config: $config_file" if ! run_maybe_sudo mkdir -p /etc/nix; then warn "Failed to create /etc/nix" return 1 fi if ! run_maybe_sudo touch "$config_file"; then warn "Failed to create Nix config file: $config_file" return 1 fi if nix_config_trusts_user "$config_file" "$current_user"; then info "Nix config already trusts '$current_user'" else if ! printf '\nextra-trusted-users = %s\n' "$current_user" | run_maybe_sudo tee -a "$config_file" >/dev/null; then warn "Failed to update Nix trusted users in $config_file" return 1 fi info "Added '$current_user' to extra-trusted-users in $config_file" fi info "Restarting nix-daemon..." if ! restart_nix_daemon; then warn "Failed to restart nix-daemon automatically" return 1 fi # launchd/systemd reload can be asynchronous on some hosts. # Poll briefly instead of using a single fixed sleep. max_attempts=10 attempt=1 while [[ $attempt -le $max_attempts ]]; do if nix_current_user_is_trusted; then info "Nix daemon now trusts '$current_user'" return 0 fi sleep 1 attempt=$((attempt + 1)) done warn "Nix daemon trust check still failing after automatic configuration" return 1 } # Offer an interactive self-heal when Nix daemon trust is missing. maybe_offer_nix_trust_self_heal() { local current_user os restart_cmd prompt_cmd prompt_intent config_file if [[ "${NIX_TRUST_PREFLIGHT_DONE}" == "true" ]]; then return 0 fi if ! check_nix_installed; then return 0 fi if is_root; then return 0 fi if nix_current_user_is_trusted; then return 0 fi NIX_TRUST_PREFLIGHT_DONE="true" current_user="$(id -un 2>/dev/null || echo "")" config_file="$(nix_system_config_file)" warn "Nix daemon does not trust user '$current_user'." warn "This can block Cachix and fail installation." if [[ "${NO_CONFIRM:-false}" == "true" ]]; then warn "Skipping automatic trust fix in --yes/--no-confirm mode." print_nix_untrusted_user_fix return 0 fi # Only attempt interactive self-heal when we can reliably access /dev/tty. if ! [[ -t 1 ]] || ! (: /dev/null; then warn "Skipping automatic trust fix: no interactive terminal available." print_nix_untrusted_user_fix return 0 fi os="$(uname -s | tr '[:upper:]' '[:lower:]')" case "$os" in darwin) restart_cmd="sudo launchctl kickstart -k system/org.nixos.nix-daemon" ;; linux) restart_cmd="sudo systemctl restart nix-daemon" ;; *) restart_cmd="# restart nix-daemon for your platform" ;; esac prompt_cmd="sudo sh -c 'echo \"extra-trusted-users = $current_user\" >> $config_file' $restart_cmd" prompt_intent="Enable Nix trusted-user access for this installer run This updates Nix daemon config to trust '$current_user' and restarts nix-daemon. Without this, binary-cache installation can fail on this machine." if ! confirm_danger "$prompt_intent" "$prompt_cmd" "y"; then warn "Skipped automatic Nix trust configuration by user choice." return 0 fi if ! configure_nix_trusted_user; then print_nix_untrusted_user_fix_warning fi return 0 } # Install tmux for Nix installs when it's missing. # Returns 0 on success, 1 on failure. ensure_tmux_installed() { local tmux_store_path nix_err_file nix_output if has_cmd tmux; then info "tmux already installed" return 0 fi info "Installing tmux terminal multiplexer..." nix_err_file="$(mktemp)" if ! tmux_store_path="$( nix build \ --print-out-paths \ --no-link \ "nixpkgs#tmux" \ --option extra-substituters "$CACHIX_URL" \ --option trusted-public-keys "$NIXPKGS_KEY $CACHIX_KEY" \ --option narinfo-cache-negative-ttl 0 \ --extra-experimental-features "nix-command flakes" \ 2> >(tee "$nix_err_file" >&2) )"; then nix_output="$(cat "$nix_err_file")" rm -f "$nix_err_file" if nix_output_indicates_untrusted_user "$nix_output"; then print_nix_untrusted_user_fix fi warn "Failed to resolve tmux from nixpkgs" return 1 fi rm -f "$nix_err_file" tmux_store_path="$(printf '%s\n' "$tmux_store_path" | tail -n1)" if [[ -z "$tmux_store_path" ]]; then warn "Could not determine tmux store path from nix build output" return 1 fi if ! install_from_store_path "$tmux_store_path"; then warn "Failed to install tmux from store path" return 1 fi if ! has_cmd tmux; then ensure_nix_profile_in_path fi if has_cmd tmux; then info "tmux installed successfully" return 0 fi warn "tmux installation finished but command is not in PATH yet" return 1 } # Detect Nix permission errors caused by untrusted users. nix_output_indicates_untrusted_user() { local output="$1" case "$output" in *"restricted setting and you are not a trusted user"* | *"ignoring untrusted substituter"* | *"you are not a trusted user"*) return 0 ;; *) return 1 ;; esac } # Print remediation steps for non-trusted-user Nix daemon setups. print_nix_untrusted_user_fix_inner() { local log_fn="${1:-error}" local current_user config_file current_user="$(id -un 2>/dev/null || echo "")" config_file="$(nix_system_config_file)" "$log_fn" "Nix rejected Cachix cache settings because '$current_user' is not a trusted user." "$log_fn" "Fix this once, then rerun the installer:" "$log_fn" " sudo sh -c 'echo \"extra-trusted-users = $current_user\" >> $config_file'" case "$(uname -s | tr '[:upper:]' '[:lower:]')" in darwin) "$log_fn" " sudo launchctl kickstart -k system/org.nixos.nix-daemon" ;; linux) "$log_fn" " sudo systemctl restart nix-daemon" ;; *) "$log_fn" " Restart the Nix daemon for your platform" ;; esac "$log_fn" "If needed, adjust your Nix daemon config file manually before rerunning." } # Print hard remediation errors after an actual Nix command fails due to trust. print_nix_untrusted_user_fix() { print_nix_untrusted_user_fix_inner error } # Print softer remediation guidance when preflight self-heal cannot be verified yet. print_nix_untrusted_user_fix_warning() { warn "Nix trust self-heal could not be verified immediately after restarting nix-daemon." warn "Continuing installation. If a later Nix command reports an untrusted user, run:" print_nix_untrusted_user_fix_inner warn } # Install from a store path using nix-store -r + nix profile add # Returns 0 on success, 1 on failure. install_from_store_path() { local store_path="$1" local nix_err_file="" local nix_output="" info "Installing from store path: $store_path" debug "Cachix URL: $CACHIX_URL" # Fetch from binary cache debug "Fetching from binary cache with nix-store -r..." nix_err_file="$(mktemp)" if ! nix-store -r "$store_path" \ --option extra-substituters "$CACHIX_URL" \ --option trusted-public-keys "$NIXPKGS_KEY $CACHIX_KEY" \ --option narinfo-cache-negative-ttl 0 2> >(tee "$nix_err_file" >&2); then nix_output="$(cat "$nix_err_file")" rm -f "$nix_err_file" if nix_output_indicates_untrusted_user "$nix_output"; then print_nix_untrusted_user_fix fi warn "Failed to fetch from binary cache" return 1 fi rm -f "$nix_err_file" # Add to user profile nix_err_file="$(mktemp)" if ! nix_profile_install "$store_path" \ --option extra-substituters "$CACHIX_URL" \ --option trusted-public-keys "$NIXPKGS_KEY $CACHIX_KEY" 2> >(tee "$nix_err_file" >&2); then nix_output="$(cat "$nix_err_file")" rm -f "$nix_err_file" if nix_output_indicates_untrusted_user "$nix_output"; then print_nix_untrusted_user_fix fi warn "Profile install failed, binary available at: $store_path/bin" return 1 fi rm -f "$nix_err_file" } # Install/refresh the systemd unit for ah-fs-snapshots-daemon when using # non-package Linux installs (Nix profile/store-path). Native packages already # ship this unit via package metadata. setup_snapshot_daemon_systemd_unit_for_nix_profile() { local store_path="${1:-}" local os os="$(uname -s | tr '[:upper:]' '[:lower:]')" if [[ "$os" != "linux" ]]; then return 0 fi local daemon_bin="" if [[ -n "$store_path" ]] && [[ -x "$store_path/bin/ah-fs-snapshots-daemon" ]]; then daemon_bin="$store_path/bin/ah-fs-snapshots-daemon" elif has_cmd ah-fs-snapshots-daemon; then daemon_bin="$(command -v ah-fs-snapshots-daemon)" else warn "Skipping snapshot daemon systemd setup: ah-fs-snapshots-daemon not found" return 0 fi local unit_path="/etc/systemd/system/ah-fs-snapshots-daemon.service" local tmp_unit if ! tmp_unit="$(mktemp)"; then warn "Skipping snapshot daemon systemd setup: failed to create temporary file" return 0 fi cat >"$tmp_unit" </dev/null 2>&1; then warn "Skipping snapshot daemon systemd setup: sudo authentication failed" rm -f "$tmp_unit" return 0 fi if ! sudo mkdir -p /etc/systemd/system; then warn "Failed to create /etc/systemd/system for snapshot daemon unit" rm -f "$tmp_unit" return 0 fi if ! sudo cp "$tmp_unit" "$unit_path"; then warn "Failed to install snapshot daemon unit at $unit_path" rm -f "$tmp_unit" return 0 fi if ! sudo chmod 0644 "$unit_path"; then warn "Failed to set permissions on $unit_path" rm -f "$tmp_unit" return 0 fi fi rm -f "$tmp_unit" info "Installed snapshot daemon systemd unit: $unit_path" info "Configured ExecStart=$daemon_bin" if has_cmd systemctl; then if is_root; then if ! systemctl daemon-reload; then warn "Failed to reload systemd daemon (unit file was still installed)" fi info "Enable and start it with: systemctl enable --now ah-fs-snapshots-daemon" else if ! sudo systemctl daemon-reload; then warn "Failed to reload systemd daemon (unit file was still installed)" fi info "Enable and start it with: sudo systemctl enable --now ah-fs-snapshots-daemon" fi else warn "systemctl not found; installed unit but could not run daemon-reload" fi return 0 } # Install ah-fs-snapshots-daemon as a LaunchDaemon on macOS. # Uses the daemon's built-in `install` subcommand which handles plist creation, # launchctl bootstrap, and socket verification. # This function follows the same pattern as setup_snapshot_daemon_systemd_unit_for_nix_profile # for Linux, leveraging existing sudo access from the install script. setup_snapshot_daemon_launchd_for_macos() { local store_path="${1:-}" local os os="$(uname -s | tr '[:upper:]' '[:lower:]')" if [[ "$os" != "darwin" ]]; then return 0 fi local daemon_bin="" if [[ -n "$store_path" ]] && [[ -x "$store_path/bin/ah-fs-snapshots-daemon" ]]; then daemon_bin="$store_path/bin/ah-fs-snapshots-daemon" elif has_cmd ah-fs-snapshots-daemon; then daemon_bin="$(command -v ah-fs-snapshots-daemon)" else warn "Skipping snapshot daemon LaunchDaemon setup: ah-fs-snapshots-daemon not found" return 0 fi info "Installing Agent Harbor daemon as LaunchDaemon..." # Use the daemon's built-in install subcommand # This handles plist creation, launchctl bootstrap, socket verification if is_root; then if "$daemon_bin" install; then info "✓ Daemon installed successfully" return 0 else warn "Failed to install daemon. You can install manually later with:" warn " sudo ah-fs-snapshots-daemon install" return 0 fi else if ! has_cmd sudo; then warn "Skipping snapshot daemon LaunchDaemon setup: sudo is required" return 0 fi if ! sudo -v >/dev/null 2>&1; then warn "Skipping snapshot daemon LaunchDaemon setup: sudo authentication failed" return 0 fi if sudo "$daemon_bin" install; then info "✓ Daemon installed successfully" return 0 else warn "Failed to install daemon. You can install manually later with:" warn " sudo ah-fs-snapshots-daemon install" return 0 fi fi } # Note: Git-based flake installation has been removed. # The installer only supports binary cache installation via nix-store. # This ensures fast, reproducible installs without requiring source builds. # To install from source, clone the repository and build manually: # git clone https://github.com/agent-harbor/agent-harbor-public # cd agent-harbor && nix build .#ah # ============================================================================ # PART 2: Embedded install-nix.sh (backend functions) # ============================================================================ # Nix installation backend - installs using Nix + Attic binary cache # Embed all functions, rename entry point to run_nix_backend # Copyright 2026 Schelling Point Labs Inc # # Agent Harbor Nix-based installation script # Installs Agent Harbor CLI using Nix + Attic binary cache # # Usage: # ./install-nix.sh [version] # VERSION=v1.2.3 ./install-nix.sh # Get the directory where this script is located # Source common utilities # ============================================================================ # Configuration # ============================================================================ # ============================================================================ # Internal installation function (can be called directly or used standalone) # ============================================================================ # Run the Nix installation process. Can be called directly when sourced, # or via main() when run standalone. # Returns 0 on success, 1 on failure. run_nix_backend() { local platform platform=$(detect_platform) info "Platform: $platform" # Ensure Nix is in PATH first (in case it was installed previously but PATH not set) ensure_nix_profile_in_path # Check/install Nix if ! check_nix_installed; then if ! install_nix; then error "Failed to install Nix package manager" return 1 fi # Ensure PATH is updated immediately after installation ensure_nix_profile_in_path fi # Offer to self-heal daemon trust before store-path install. maybe_offer_nix_trust_self_heal # Get embedded store path for current platform local store_path="" if ! store_path=$(get_store_path); then error "No store path available for this platform" error "" error "The installer requires pre-built binaries from the Attic binary cache." error "To build from source instead, clone the repository:" error " git clone https://github.com/agent-harbor/agent-harbor-public" error " cd agent-harbor && nix build .#ah" return 1 fi # Install from store path if [[ -n "$store_path" ]]; then if ! install_from_store_path "$store_path"; then error "Store path installation failed" return 1 fi setup_snapshot_daemon_systemd_unit_for_nix_profile "$store_path" setup_snapshot_daemon_launchd_for_macos "$store_path" # tmux is required for dashboard/session management. if ! ensure_tmux_installed; then error "tmux installation failed" return 1 fi else error "No store path available for installation" return 1 fi # Return success - verification is handled by caller (dispatcher or main) return 0 } # ============================================================================ # PART 3: Embedded install-native.sh (backend functions) # ============================================================================ # Native package manager backend - installs using APT/YUM/DNF/APK/etc # Embed all functions, rename entry point to run_native_backend # Copyright 2026 Schelling Point Labs Inc # # Agent Harbor native package installation script # Installs Agent Harbor using system package managers (APT, YUM, DNF, APK, etc.) # # Usage: # ./install-native.sh [options] # PACKAGE_VARIANT=agent-harbor-fs ./install-native.sh # Get the directory where this script is located # Source common utilities # ============================================================================ # Configuration # ============================================================================ # Repository URLs (can be overridden via environment) APT_REPO_URL="${APT_REPO_URL:-https://apt.agent-harbor.com}" YUM_REPO_URL="${YUM_REPO_URL:-https://yum.agent-harbor.com}" ARCH_REPO_URL="${ARCH_REPO_URL:-https://arch.agent-harbor.com}" APK_REPO_URL="${APK_REPO_URL:-https://apk.agent-harbor.com}" MACOS_DMG_BASE_URL="${MACOS_DMG_BASE_URL:-https://downloads.agent-harbor.com/macos}" # Package variant (agent-harbor, agent-harbor-fs) # M2: agent-harbor-cli has been merged into agent-harbor (unified package) PACKAGE_VARIANT="${PACKAGE_VARIANT:-agent-harbor}" VERSION="${VERSION:-latest}" # Testing options SKIP_GPG_CHECK="${SKIP_GPG_CHECK:-false}" # ============================================================================ # Package manager detection and installation # ============================================================================ # Detect the package manager based on OS/distro detect_package_manager() { local distro="$1" case "$distro" in debian | ubuntu) echo "apt" ;; fedora) echo "dnf" ;; rhel | centos | rocky | almalinux | amzn) echo "yum" ;; alpine) echo "apk" ;; arch | manjaro) echo "pacman" ;; opensuse) echo "zypper" ;; darwin) echo "macos" ;; *) echo "unknown" ;; esac } # Install using APT (Debian, Ubuntu) install_apt() { step "Installing via APT" local SUDO SUDO=$(maybe_sudo) # Check if gpg is available when needed if [[ "$SKIP_GPG_CHECK" != "true" ]] && ! has_cmd gpg; then warn "gpg command not found - required for GPG key verification" warn "Install gpg with: apt-get install gnupg" warn "Or set SKIP_GPG_CHECK=true to skip GPG verification (not recommended)" return 1 fi # Build command preview for confirmation local commands="" if [[ "$SKIP_GPG_CHECK" != "true" ]]; then commands="mkdir -p /etc/apt/keyrings fetch ${APT_REPO_URL}/gpg-key.asc | gpg --dearmor > /etc/apt/keyrings/agent-harbor.gpg" fi commands="${commands} echo \"deb ${APT_REPO_URL} stable main\" > /etc/apt/sources.list.d/agent-harbor.list apt-get update -qq apt-get install -y ${PACKAGE_VARIANT} tmux" if ! confirm_package_manager_install "APT" "Debian/Ubuntu package manager" "$commands"; then warn "APT installation declined by user" return 1 fi # GPG and repository configuration if [[ "$SKIP_GPG_CHECK" == "true" ]]; then warn "GPG check disabled - for testing only" local apt_options="[trusted=yes]" else info "Adding GPG key..." ensure_privileges mkdir -p /etc/apt/keyrings if ! fetch "${APT_REPO_URL}/gpg-key.asc" 2>/dev/null | ${SUDO} gpg --dearmor >/etc/apt/keyrings/agent-harbor.gpg; then warn "Failed to fetch or install GPG key from ${APT_REPO_URL}" warn "Repository may not exist yet, or there may be a network issue" return 1 fi local apt_options="[signed-by=/etc/apt/keyrings/agent-harbor.gpg]" fi info "Adding APT repository..." if ! echo "deb ${apt_options} ${APT_REPO_URL} stable main" | ${SUDO} tee /etc/apt/sources.list.d/agent-harbor.list >/dev/null; then warn "Failed to add APT repository" return 1 fi info "Updating package lists..." if ! ${SUDO} apt-get update -qq; then warn "Failed to update package lists - repository may be unavailable" return 1 fi info "Installing ${PACKAGE_VARIANT} and tmux..." if ! ${SUDO} apt-get install -y "$PACKAGE_VARIANT" tmux; then warn "Failed to install ${PACKAGE_VARIANT} and tmux" return 1 fi } # Install using DNF (Fedora) install_dnf() { step "Installing via DNF" local SUDO SUDO=$(maybe_sudo) # Build command preview for confirmation local gpg_cmd="" if [[ "$SKIP_GPG_CHECK" != "true" ]]; then gpg_cmd="rpm --import ${YUM_REPO_URL}/gpg-key.asc " fi local commands="tee /etc/yum.repos.d/agent-harbor.repo </dev/null </dev/null <> /etc/apk/repositories" if [[ "$SKIP_GPG_CHECK" != "true" ]]; then commands="${commands} fetch ${APK_REPO_URL}/gpg-key.asc > /etc/apk/keys/agent-harbor@gpg-key.asc" fi commands="${commands} apk update apk add ${PACKAGE_VARIANT} tmux" if ! confirm_package_manager_install "APK" "Alpine Linux package manager" "$commands"; then warn "APK installation declined by user" return 1 fi info "Adding APK repository..." echo "${APK_REPO_URL}/@agent-harbor" | ${SUDO} tee -a /etc/apk/repositories >/dev/null if [[ "$SKIP_GPG_CHECK" != "true" ]]; then info "Adding GPG key..." fetch "${APK_REPO_URL}/gpg-key.asc" | ${SUDO} tee /etc/apk/keys/agent-harbor@gpg-key.asc >/dev/null fi info "Updating package index..." ${SUDO} apk update info "Installing ${PACKAGE_VARIANT} and tmux..." ${SUDO} apk add "$PACKAGE_VARIANT" tmux } # Install using Pacman (Arch Linux) install_pacman() { step "Installing via Pacman" local SUDO SUDO=$(maybe_sudo) # Build command preview for confirmation # Note: -Syu performs full system upgrade to avoid partial upgrade conflicts local commands="echo \"[agent-harbor]\\nSigLevel = Optional TrustAll\\nServer = ${ARCH_REPO_URL}/\$arch/\$repo\" >> /etc/pacman.conf pacman -Syu pacman -S --noconfirm ${PACKAGE_VARIANT} tmux" if ! confirm_package_manager_install "Pacman" "Arch Linux package manager" "$commands"; then warn "Pacman installation declined by user" return 1 fi info "Adding repository configuration..." # Add to pacman.conf if not already present if ! grep -q "^\[agent-harbor\]" /etc/pacman.conf; then echo -e "\n[agent-harbor]\nSigLevel = Optional TrustAll\nServer = ${ARCH_REPO_URL}/\$arch/\$repo" | ${SUDO} tee -a /etc/pacman.conf >/dev/null fi info "Updating package database and upgrading system..." # Use -Syu to avoid partial upgrade conflicts (libgcc/libstdc++ vs gcc-libs) ${SUDO} pacman -Syu --noconfirm info "Installing ${PACKAGE_VARIANT} and tmux..." ${SUDO} pacman -S --noconfirm "$PACKAGE_VARIANT" tmux } # Install using Zypper (openSUSE) install_zypper() { step "Installing via Zypper" local SUDO SUDO=$(maybe_sudo) # Build command preview for confirmation local commands="zypper addrepo -f ${YUM_REPO_URL} agent-harbor zypper refresh zypper install -y ${PACKAGE_VARIANT} tmux" if ! confirm_package_manager_install "Zypper" "openSUSE package manager" "$commands"; then warn "Zypper installation declined by user" return 1 fi info "Adding repository..." ${SUDO} zypper addrepo -f "${YUM_REPO_URL}" agent-harbor info "Refreshing repositories..." ${SUDO} zypper refresh info "Installing ${PACKAGE_VARIANT} and tmux..." ${SUDO} zypper install -y "$PACKAGE_VARIANT" tmux } # Build the public macOS DMG URL for the selected architecture. get_macos_dmg_url() { local arch="$1" local base_url="${MACOS_DMG_BASE_URL%/}" if [[ "${VERSION}" == "latest" ]]; then echo "${base_url}/latest/AgentHarbor-latest-${arch}.dmg" else local v="${VERSION#v}" echo "${base_url}/v${v}/AgentHarbor-${v}-${arch}.dmg" fi } # Install on macOS by downloading the signed/notarized DMG and deploying the # .app bundle to /Applications. Homebrew is still used for tmux. install_macos() { step "Installing on macOS (DMG app bundle)" # Homebrew is still used to install tmux dependency. if ! has_cmd brew; then die "Homebrew is required to install tmux on macOS. Please install it first:\n /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" fi local arch arch=$(uname -m) case "$arch" in arm64 | aarch64) arch="arm64" ;; x86_64 | amd64) arch="x86_64" ;; *) die "Unsupported macOS architecture: ${arch}" ;; esac local dmg_url dmg_url=$(get_macos_dmg_url "$arch") local commands="brew install tmux curl -fsSL ${dmg_url} -o /tmp/AgentHarbor.dmg hdiutil attach /tmp/AgentHarbor.dmg cp -R 'Agent Harbor.app' '/Applications/Agent Harbor.app.new' mv '/Applications/Agent Harbor.app' '/Applications/Agent Harbor.app.previous.' || true mv '/Applications/Agent Harbor.app.new' '/Applications/Agent Harbor.app' ln -sf '/Applications/Agent Harbor.app/Contents/Resources/cli/ah-launcher' /usr/local/bin/ah" if ! confirm_package_manager_install "macOS" "macOS native installer (DMG app bundle + Homebrew tmux)" "$commands"; then warn "macOS installation declined by user" return 1 fi if brew list tmux >/dev/null 2>&1; then info "tmux already installed" else info "Installing tmux with Homebrew..." brew install tmux fi local tmpdir cleanup_cmd tmpdir=$(mktemp -d) cleanup_cmd=$(printf 'rm -rf %q' "$tmpdir") trap "$cleanup_cmd" RETURN local dmg_path="${tmpdir}/AgentHarbor.dmg" info "Downloading Agent Harbor DMG..." if ! fetch_to "$dmg_url" "$dmg_path"; then warn "Failed to download DMG: ${dmg_url}" return 1 fi info "Mounting DMG..." local mount_point mount_point=$(hdiutil attach "$dmg_path" -nobrowse -readonly 2>/dev/null | awk -F'\t' '/Apple_HFS/ {print $NF}') if [[ -z "$mount_point" ]]; then warn "Failed to mount DMG" return 1 fi # Find the .app bundle inside the mounted volume local app_path app_path=$(find "$mount_point" -maxdepth 1 -name "*.app" -type d | head -1) if [[ -z "$app_path" ]]; then hdiutil detach "$mount_point" -quiet || true warn "No .app bundle found in DMG" return 1 fi local app_name app_name=$(basename "$app_path") local dest="/Applications/${app_name}" local staged_dest="${dest}.new.$$" local previous_dest="${dest}.previous.$(date +%Y%m%d%H%M%S)" info "Installing ${app_name} to /Applications/..." [[ -d "$dest" ]] && info "Moving existing app bundle aside for deferred cleanup..." local SUDO SUDO=$(maybe_sudo) if [[ -w "/Applications" ]]; then rm -rf "$staged_dest" cp -R "$app_path" "$staged_dest" if [[ -d "$dest" ]]; then mv "$dest" "$previous_dest" fi mv "$staged_dest" "$dest" else ensure_privileges ${SUDO} rm -rf "$staged_dest" ${SUDO} cp -R "$app_path" "$staged_dest" if [[ -d "$dest" ]]; then ${SUDO} mv "$dest" "$previous_dest" fi ${SUDO} mv "$staged_dest" "$dest" fi info "Unmounting DMG..." hdiutil detach "$mount_point" -quiet || true # Create CLI symlink local cli_bin="${dest}/Contents/Resources/cli/ah-launcher" local symlink_dir="/usr/local/bin" if [[ -f "$cli_bin" ]]; then info "Creating CLI symlink..." if [[ ! -d "$symlink_dir" ]]; then if [[ -w "$(dirname "$symlink_dir")" ]]; then mkdir -p "$symlink_dir" else ensure_privileges ${SUDO} mkdir -p "$symlink_dir" fi fi if [[ -w "$symlink_dir" ]]; then ln -sf "$cli_bin" "${symlink_dir}/ah" else ensure_privileges ${SUDO} ln -sf "$cli_bin" "${symlink_dir}/ah" fi info "Symlinked ah to stable launcher at ${cli_bin}" else warn "CLI binary not found at ${cli_bin} — skipping symlink" fi info "Agent Harbor.app installed to /Applications/" # Print FSKit extension activation instructions echo "" info "To enable the AgentHarborFS filesystem extension:" info " 1. Open System Settings → General → Login Items & Extensions" info " 2. Click the ⓘ next to \"File System Extensions\"" info " 3. Enable \"AgentHarborFSKitExtension\"" echo "" # Offer to open System Settings if [[ -t 0 ]]; then printf ' Open System Settings now? [y/N] ' read -r answer if [[ "$answer" =~ ^[Yy] ]]; then open "x-apple.systempreferences:com.apple.LoginItems-Settings.extension" fi fi } # ============================================================================ # Main installation dispatcher # ============================================================================ install_native() { local distro distro=$(detect_os) case "$distro" in debian | ubuntu) install_apt ;; fedora) install_dnf ;; rhel | centos | rocky | almalinux | amzn) install_yum ;; alpine) install_apk ;; arch | manjaro) install_pacman ;; opensuse) install_zypper ;; darwin) install_macos ;; *) die "Unsupported distribution: $distro" ;; esac } # ============================================================================ # Internal installation function (can be called directly or used standalone) # ============================================================================ # Run the native installation process. Can be called directly when sourced, # or via main() when run standalone. # Returns 0 on success, 1 on failure. run_native_backend() { info "Agent Harbor Native Installer" info "=============================" local platform distro platform=$(detect_platform) distro=$(detect_os) debug "Detected platform: $platform" debug "Detected distribution: $distro" info "Platform: $platform" info "Distribution: $distro" info "Package variant: $PACKAGE_VARIANT" # macOS doesn't need sudo for brew if [[ "$distro" != "darwin" ]]; then debug "Ensuring root privileges for non-macOS system..." ensure_privileges fi # Install debug "Starting native package installation..." if ! install_native; then error "Native installation failed" return 1 fi # Return success - verification is handled by caller (dispatcher or main) return 0 } # ============================================================================ # PART 4: Dispatcher and main entry point # ============================================================================ VERSION="${VERSION:-latest}" PACKAGE_VARIANT="${PACKAGE_VARIANT:-agent-harbor}" INSTALL_MODE="${INSTALL_MODE:-}" VERBOSE="${VERBOSE:-false}" # Export NO_CONFIRM so it's picked up by confirm_danger() NO_CONFIRM="${NO_CONFIRM:-false}" export NO_CONFIRM # Agent installation: "yes", "no", or empty (prompt interactively) INSTALL_AGENTS="${INSTALL_AGENTS:-}" # Debug logging debug() { if [[ "$VERBOSE" == "true" ]]; then printf "%b[DEBUG]%b %s\n" "${BLUE}" "${NC}" "$*" >&2 fi } print_banner() { printf "%b" "${BOLD}" printf " _ _ _ _ _ \n" printf " / \\ __ _ ___ _ __ | |_ | | | | __ _ _ __| |__ ___ _ __ \n" printf " / _ \\ / _\` |/ _ \\ '_ \\| __| | |_| |/ _\` | '__| '_ \\ / _ \\| '__|\n" printf " / ___ \\ (_| | __/ | | | |_ | _ | (_| | | | |_) | (_) | | \n" printf "/_/ \\_\\__, |\\___|_| |_|\\__| |_| |_|\\__,_|_| |_.__/ \\___/|_| \n" printf " |___/ \n" printf "%b" "${NC}" echo "" echo "Agent Harbor Installation Script" echo "https://agent-harbor.com" echo "" # Print build provenance if available if [[ -n "$BUILD_COMMIT_SHA" ]] || [[ -n "$BUILD_PR_URL" ]]; then [[ -n "$BUILD_PR_URL" ]] && echo "* PR: $BUILD_PR_URL" [[ -n "$BUILD_COMMIT_SHA" ]] && echo "* Commit: $BUILD_COMMIT_SHA" echo "" fi echo "For installation options and troubleshooting, visit:" echo "https://docs.agent-harbor.com/getting-started/installation" echo "" } usage() { cat </dev/null; then return 0; fi fi debug "Native installation not available" # 3. Fallback to installing Nix warn "Native packages not available, will install via Nix" if try_nix_install; then return 0; fi die "Both installation methods failed" } main() { parse_args "$@" # Check if already installed (skips install if true) check_existing_installation || return 0 print_banner # If no install mode specified, prompt interactively or fall back to auto if [[ -z "$INSTALL_MODE" ]]; then if ! is_non_interactive; then # Interactive terminal: show system checks and let user choose if ! prompt_install_method; then die "Installation method selection failed" fi else # Non-interactive (CI, piped): silently auto-detect INSTALL_MODE="auto" fi fi local confirm_status="yes" [[ "$NO_CONFIRM" == "true" ]] && confirm_status="no" info "Mode: $INSTALL_MODE | Version: $VERSION | Variant: $PACKAGE_VARIANT | Confirm: $confirm_status" case "$INSTALL_MODE" in nix) try_nix_install || die "Nix install failed" # Offer AI coding agent installation (Nix only) prompt_agent_install || true ;; native) try_native_install || die "Native install failed" ;; auto) auto_install || die "Installation failed" # Offer agents if Nix was used (auto may have chosen Nix) if [[ "$INSTALL_MODE" == "nix" ]] || check_nix_installed 2>/dev/null; then prompt_agent_install || true fi ;; *) die "Unknown mode: $INSTALL_MODE" ;; esac # Verify installation using shared function (don't fail if just PATH issue) verify_installation || true } main "$@"