#!/bin/bash set -e REPO="maxritter/pilot-shell" VERSION="${VERSION:-}" VERSION="${VERSION#v}" INSTALLER_ARGS="" RESTART_PILOT=false SKIP_VERSION_CHECK=false USE_LOCAL_INSTALLER=false while [ $# -gt 0 ]; do case "$1" in --restart-pilot) RESTART_PILOT=true shift ;; --skip-version-check) SKIP_VERSION_CHECK=true shift ;; --local) USE_LOCAL_INSTALLER=true SKIP_VERSION_CHECK=true shift ;; *) if [ -z "$INSTALLER_ARGS" ]; then INSTALLER_ARGS="$1" else INSTALLER_ARGS="$INSTALLER_ARGS $1" fi shift ;; esac done get_latest_release() { local redirect_url="https://github.com/${REPO}/releases/latest" local api_url="https://api.github.com/repos/${REPO}/releases/latest" local version="" if command -v curl >/dev/null 2>&1; then local redirect_location redirect_location=$(curl -sIo /dev/null -w '%{redirect_url}' "$redirect_url" 2>/dev/null | tr -d '\r') || true if [ -n "$redirect_location" ] && [ "$redirect_location" != "%{redirect_url}" ]; then version=$(echo "$redirect_location" | sed -n 's|.*/releases/tag/v\([^/]*\).*|\1|p') || true fi elif command -v wget >/dev/null 2>&1; then local redirect_location redirect_location=$(wget --spider -S "$redirect_url" 2>&1 | grep -i 'location:' | tail -1 | sed 's/.*location: *//I' | tr -d '\r') || true if [ -n "$redirect_location" ]; then version=$(echo "$redirect_location" | sed -n 's|.*/releases/tag/v\([^/]*\).*|\1|p') || true fi fi if [ -n "$version" ]; then echo "$version" return 0 fi if command -v curl >/dev/null 2>&1; then version=$(curl -fsSL "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') || true elif command -v wget >/dev/null 2>&1; then version=$(wget -qO- "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') || true fi if [ -n "$version" ]; then echo "$version" return 0 fi return 1 } if [ -z "$VERSION" ]; then echo " [..] Fetching latest version..." VERSION=$(get_latest_release) || true if [ -z "$VERSION" ]; then echo " [!!] Failed to fetch latest version from GitHub." echo " [!!] Please specify a version: VERSION=6.0.0 curl ... | bash" exit 1 fi echo " [OK] Latest version: $VERSION" else echo " Using specified version: $VERSION" if [ "$SKIP_VERSION_CHECK" = true ]; then echo " [..] Skipping version check (--skip-version-check)" fi fi case "$VERSION" in dev-*) REPO_RAW="https://raw.githubusercontent.com/${REPO}/${VERSION}" ;; *) REPO_RAW="https://raw.githubusercontent.com/${REPO}/v${VERSION}" ;; esac is_in_container() { [ -f "/.dockerenv" ] || [ -f "/run/.containerenv" ] } download_file() { local path="$1" local dest="$2" local url="${REPO_RAW}/${path}" mkdir -p "$(dirname "$dest")" if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" -o "$dest" elif command -v wget >/dev/null 2>&1; then wget -q "$url" -O "$dest" else echo "Error: Neither curl nor wget found." exit 1 fi } check_uv() { command -v uv >/dev/null 2>&1 } install_uv() { echo " [..] Installing uv..." if command -v curl >/dev/null 2>&1; then curl -LsSf https://astral.sh/uv/install.sh | sh elif command -v wget >/dev/null 2>&1; then wget -qO- https://astral.sh/uv/install.sh | sh fi export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" if ! check_uv; then echo " [!!] Failed to install uv" exit 1 fi echo " [OK] uv installed" } show_macos_gatekeeper_warning() { echo "" echo " ⚠️ macOS Gatekeeper is blocking the pilot binary" echo "" echo " The installer requires pilot to verify your license." echo " Please follow these steps to unblock it:" echo "" echo " 1. Open System Settings → Privacy & Security" echo " 2. Scroll down to find a message about 'pilot' being blocked" echo " 3. Click 'Allow Anyway'" echo " 4. Re-run this installer" echo "" echo " Or run this command to remove the quarantine flag:" echo " xattr -cr $HOME/.pilot/bin" echo "" } confirm_local_install() { echo "" echo " Local installation will:" echo " • Add 'pilot' and 'ccp' command to your favorite shell config (~/.bashrc, ~/.zshrc, fish)" echo " • Configure Claude Code to Pilot best-practices (~/.claude.json, ~/.claude/settings.json)" echo " • Install additional tool dependencies via Homebrew or NPM on your system" echo "" confirm="" if [ -t 0 ]; then printf " Continue? [Y/n]: " read -r confirm elif [ -e /dev/tty ]; then printf " Continue? [Y/n]: " read -r confirm /dev/null 2>&1; then tree_json=$(curl -fsSL "$tree_url" 2>/dev/null) || true elif command -v wget >/dev/null 2>&1; then tree_json=$(wget -qO- "$tree_url" 2>/dev/null) || true fi if [ -z "$tree_json" ]; then if command -v curl >/dev/null 2>&1; then tree_json=$(curl -fsSL "$api_url" 2>/dev/null) || true elif command -v wget >/dev/null 2>&1; then tree_json=$(wget -qO- "$api_url" 2>/dev/null) || true fi fi if [ -z "$tree_json" ]; then echo " [!!] Failed to fetch file list from GitHub API" exit 1 fi echo "$tree_json" | grep -oE '"path": ?"installer/[^"]*\.py"' | sed 's/"path": *"//g; s/"$//g' | while IFS= read -r file_path; do case "$file_path" in *__pycache__* | *dist/* | *build/* | *tests/*) continue ;; esac local dest_file="$installer_dir/$file_path" mkdir -p "$(dirname "$dest_file")" download_file "$file_path" "$dest_file" done download_file "pyproject.toml" "$installer_dir/pyproject.toml" echo " [OK] Installer downloaded" } get_platform_suffix() { local os_name="" local arch="" case "$(uname -s)" in Linux) os_name="linux" ;; Darwin) os_name="darwin" ;; *) return 1 ;; esac case "$(uname -m)" in x86_64 | amd64) arch="x86_64" ;; arm64 | aarch64) arch="arm64" ;; *) return 1 ;; esac echo "${os_name}-${arch}" } get_local_so_name() { local platform_tag="" case "$(uname -s)" in Linux) case "$(uname -m)" in x86_64 | amd64) platform_tag="x86_64-linux-gnu" ;; arm64 | aarch64) platform_tag="aarch64-linux-gnu" ;; esac ;; Darwin) platform_tag="darwin" ;; esac echo "pilot.cpython-312-${platform_tag}.so" } download_pilot_binary() { local bin_dir="$HOME/.pilot/bin" local platform_suffix local so_name local base_url platform_suffix=$(get_platform_suffix) || { echo " [!!] Unsupported platform for Pilot binary" return 1 } so_name=$(get_local_so_name) case "$VERSION" in dev-*) base_url="https://github.com/${REPO}/releases/download/${VERSION}" ;; *) base_url="https://github.com/${REPO}/releases/download/v${VERSION}" ;; esac if [ -d "$bin_dir" ]; then rm -rf "$bin_dir" fi mkdir -p "$bin_dir" echo " [..] Downloading Pilot binary (${platform_suffix})..." local so_url="${base_url}/pilot-${platform_suffix}.so" local so_path="${bin_dir}/${so_name}" if command -v curl >/dev/null 2>&1; then if ! curl -fsSL "$so_url" -o "$so_path" 2>/dev/null; then echo " [!!] Failed to download pilot module" return 1 fi elif command -v wget >/dev/null 2>&1; then if ! wget -q "$so_url" -O "$so_path" 2>/dev/null; then echo " [!!] Failed to download pilot module" return 1 fi fi chmod +x "$so_path" local wrapper_url="${base_url}/pilot" local wrapper_path="${bin_dir}/pilot" if command -v curl >/dev/null 2>&1; then if ! curl -fsSL "$wrapper_url" -o "$wrapper_path" 2>/dev/null; then echo " [!!] Failed to download pilot wrapper" rm -f "$so_path" return 1 fi elif command -v wget >/dev/null 2>&1; then if ! wget -q "$wrapper_url" -O "$wrapper_path" 2>/dev/null; then echo " [!!] Failed to download pilot wrapper" rm -f "$so_path" return 1 fi fi chmod +x "$wrapper_path" echo " [..] Verifying pilot binary..." local pilot_version pilot_version=$("$wrapper_path" --version 2>/dev/null) || true if [ -z "$pilot_version" ] && [ "$(uname -s)" = "Darwin" ]; then echo " [..] Removing macOS quarantine attributes..." xattr -cr "$bin_dir" 2>/dev/null || true spctl --add "$wrapper_path" 2>/dev/null || true spctl --add "$so_path" 2>/dev/null || true pilot_version=$("$wrapper_path" --version 2>/dev/null) || true fi if [ -z "$pilot_version" ]; then if [ "$(uname -s)" = "Darwin" ]; then show_macos_gatekeeper_warning exit 1 else echo " [!!] Pilot binary failed to execute" return 1 fi fi local installed_version installed_version=$(echo "$pilot_version" | sed -n 's/.* v\([^ ]*\).*/\1/p') if [ -z "$installed_version" ]; then echo " [!!] Could not determine pilot version" return 1 fi echo " [OK] Pilot binary ready (v${installed_version})" } run_installer() { local installer_dir="$HOME/.pilot/installer" echo "" export PYTHONPATH="$installer_dir:${PYTHONPATH:-}" local version_arg="--target-version $VERSION" local local_arg="" if [ "$USE_LOCAL_INSTALLER" = true ]; then local_arg="--local --local-repo-dir $(pwd)" fi local system_arg="" if ! is_in_container; then system_arg="--local-system" fi uv run --python 3.12 --no-project --with rich --with certifi \ python -m installer install $system_arg $version_arg $local_arg "$@" } is_native_windows() { case "$(uname -s)" in MINGW* | MSYS* | CYGWIN*) return 0 ;; *) return 1 ;; esac } if is_native_windows; then echo "" echo "======================================================================" echo " Pilot Shell — Windows Detected" echo "======================================================================" echo "" echo " Pilot Shell requires a Unix environment (macOS, Linux, or WSL2)." echo "" echo " Install WSL2 first (PowerShell as admin):" echo " wsl --install -d Ubuntu" echo "" echo " Then open Ubuntu and re-run this installer." echo "" exit 1 fi echo "" echo "======================================================================" echo " Pilot Shell Installer (v${VERSION})" echo "======================================================================" echo "" if is_in_container; then echo " Running inside container — skipping system dependencies" echo "" elif [ "$RESTART_PILOT" = true ]; then echo " Updating local installation..." echo "" elif [ "$USE_LOCAL_INSTALLER" = true ]; then echo " Local installation selected (--local)" echo "" confirm_local_install else confirm_local_install fi echo "" echo "Downloading Pilot Shell (v${VERSION})..." echo "" if check_uv; then echo " [OK] uv already installed" else install_uv fi if ! command -v git >/dev/null 2>&1; then case "$(uname -s)" in Linux) if command -v dnf >/dev/null 2>&1; then echo " [..] Installing git (required by Homebrew)..." sudo dnf install -y git && echo " [OK] git installed" || echo " [!!] Failed to install git via dnf" elif command -v yum >/dev/null 2>&1; then echo " [..] Installing git (required by Homebrew)..." sudo yum install -y git && echo " [OK] git installed" || echo " [!!] Failed to install git via yum" elif command -v apt-get >/dev/null 2>&1; then echo " [..] Installing git (required by Homebrew)..." sudo apt-get update -qq && sudo apt-get install -y git && echo " [OK] git installed" || echo " [!!] Failed to install git via apt" fi ;; esac fi if [ "$USE_LOCAL_INSTALLER" = true ]; then if [ -d "installer" ] && [ -f "pyproject.toml" ]; then echo " [OK] Using local installer from current directory" rm -rf "$HOME/.pilot/installer" mkdir -p "$HOME/.pilot/installer" ln -sf "$(pwd)/installer" "$HOME/.pilot/installer/installer" ln -sf "$(pwd)/pyproject.toml" "$HOME/.pilot/installer/pyproject.toml" else echo " [!!] --local requires running from pilot-shell repo root" echo " [!!] Missing: installer/ directory or pyproject.toml" exit 1 fi else download_installer fi download_pilot_binary run_installer $INSTALLER_ARGS if [ "$RESTART_PILOT" = true ]; then PILOT_BIN="$HOME/.pilot/bin/pilot" if [ -x "$PILOT_BIN" ]; then echo "" echo " Restarting Pilot Shell..." echo "" exec "$PILOT_BIN" --skip-update-check fi fi