#!/usr/bin/env bash # Coqui Installer # https://github.com/AgentCoqui/coqui # # Terminal AI agent with multi-model orchestration, persistent sessions, # and runtime extensibility via Composer. # # Install with: # curl -fsSL https://raw.githubusercontent.com/AgentCoqui/coqui-installer/main/install.sh | bash # # Windows: # irm https://raw.githubusercontent.com/AgentCoqui/coqui-installer/main/install.ps1 | iex set -eu # ─── Configuration (override via environment variables) ────────────────────── COQUI_REPO="${COQUI_REPO:-https://github.com/AgentCoqui/coqui.git}" COQUI_INSTALL_DIR="${COQUI_INSTALL_DIR:-$HOME/.coqui}" COQUI_VERSION="${COQUI_VERSION:-}" # GitHub release configuration COQUI_GITHUB_OWNER="AgentCoqui" COQUI_GITHUB_REPO="coqui" COQUI_API_URL="https://api.github.com/repos/${COQUI_GITHUB_OWNER}/${COQUI_GITHUB_REPO}/releases/latest" COQUI_DOWNLOAD_BASE="https://github.com/${COQUI_GITHUB_OWNER}/${COQUI_GITHUB_REPO}/releases/download" # Minimum PHP version required REQUIRED_PHP_MAJOR=8 REQUIRED_PHP_MINOR=4 # PHP extensions required by Coqui and php-agents REQUIRED_EXTENSIONS="curl mbstring openssl pdo_sqlite readline xml zip" # ─── Mode flags (set via CLI arguments) ────────────────────────────────────── INSTALL_PHP=false INSTALL_COMPOSER=false INSTALL_COQUI=false NON_INTERACTIVE=false QUIET_MODE=false # true when --quiet is passed (minimal output) SELECTIVE_MODE=false # true when any --install-* flag is passed DEV_MODE=false # true when --dev is passed (git clone instead of release) # Resolved at runtime LATEST_VERSION="" # ─── Argument parsing ──────────────────────────────────────────────────────── parse_args() { while [ $# -gt 0 ]; do case "$1" in --install-php) INSTALL_PHP=true; SELECTIVE_MODE=true; shift ;; --install-composer) INSTALL_COMPOSER=true; SELECTIVE_MODE=true; shift ;; --install-coqui) INSTALL_COQUI=true; SELECTIVE_MODE=true; shift ;; --dev) DEV_MODE=true; shift ;; --non-interactive) NON_INTERACTIVE=true; shift ;; --quiet|-q) QUIET_MODE=true; shift ;; --help|-h) show_usage; exit 0 ;; *) error "Unknown argument: $1" echo " Run '$0 --help' for usage." exit 1 ;; esac done # No --install-* flags → full install (backward compatible with curl | bash) if [ "$SELECTIVE_MODE" = false ]; then INSTALL_PHP=true INSTALL_COMPOSER=true INSTALL_COQUI=true fi } show_usage() { echo "Usage: $0 [flags]" echo "" echo "Flags:" echo " --install-php Install/check PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ and extensions" echo " --install-composer Install/check Composer" echo " --install-coqui Install/update Coqui and create symlink" echo " --dev Use git clone instead of release download (for development)" echo " --non-interactive Skip all confirmation prompts (assume yes)" echo " --quiet, -q Minimal output (milestones and errors only)" echo " --help, -h Show this help" echo "" echo "By default, the installer downloads the latest GitHub release." echo "Use --dev to clone the git repository instead (requires Git and Composer)." echo "" echo "When no --install-* flags are given, all components are installed (full setup)." echo "" echo "Environment variables:" echo " COQUI_REPO Git repo URL (default: ${COQUI_REPO})" echo " COQUI_INSTALL_DIR Install path (default: \$HOME/.coqui)" echo " COQUI_VERSION Release version or git branch/tag (default: latest)" echo "" echo "Examples:" echo " # Full install — latest release (default)" echo " curl -fsSL https://...install.sh | bash" echo "" echo " # Development install — git clone" echo " ./install.sh --dev" echo "" echo " # Specific version" echo " COQUI_VERSION=0.0.1 ./install.sh" echo "" echo " # PHP only, no prompts" echo " ./install.sh --install-php --non-interactive" echo "" echo " # PHP + Composer only" echo " ./install.sh --install-php --install-composer" echo "" echo " # Coqui only (user has PHP already)" echo " ./install.sh --install-coqui" } # ─── Output helpers ────────────────────────────────────────────────────────── BOLD="$( (tput bold 2>/dev/null) || echo '' )" RED="$( (tput setaf 1 2>/dev/null) || echo '' )" GREEN="$( (tput setaf 2 2>/dev/null) || echo '' )" YELLOW="$( (tput setaf 3 2>/dev/null) || echo '' )" CYAN="$( (tput setaf 6 2>/dev/null) || echo '' )" RESET="$( (tput sgr0 2>/dev/null) || echo '' )" TICK="${GREEN}✓${RESET}" CROSS="${RED}✗${RESET}" ARROW="${CYAN}▸${RESET}" status() { [ "$QUIET_MODE" = true ] && return; echo " ${ARROW} $*"; } success() { [ "$QUIET_MODE" = true ] && return; echo " ${TICK} $*"; } warn() { echo " ${YELLOW}! $*${RESET}"; } error() { echo " ${CROSS} ${RED}$*${RESET}" >&2; } fatal() { error "$@"; exit 1; } progress() { echo " ${ARROW} $*"; } # always prints, even in quiet mode # ─── Utility functions ─────────────────────────────────────────────────────── available() { command -v "$1" >/dev/null 2>&1; } # Prompt user for Y/n confirmation. Default is yes. confirm() { local prompt="${1:-Continue?}" local reply # Non-interactive mode — assume yes if [ "$NON_INTERACTIVE" = true ]; then return 0 fi # Piped input (curl | bash) — assume yes if [ ! -t 0 ]; then return 0 fi printf " %s %s [Y/n] " "${ARROW}" "${prompt}" read -r reply case "${reply}" in [nN]|[nN][oO]) return 1 ;; *) return 0 ;; esac } # Determine sudo requirement setup_sudo() { SUDO="" if [ "$(id -u)" -ne 0 ]; then if available sudo; then SUDO="sudo" fi fi } # Detect the best writable bin directory in PATH. # Never selects a directory that requires sudo — falls back to ~/.local/bin. detect_bin_dir() { # Apple Silicon Homebrew (/opt/homebrew/bin) — user-owned, no sudo needed if echo "$PATH" | tr ':' '\n' | grep -qx '/opt/homebrew/bin' && [ -w '/opt/homebrew/bin' ]; then BIN_DIR="/opt/homebrew/bin" return fi # Intel Homebrew / user-owned /usr/local/bin if echo "$PATH" | tr ':' '\n' | grep -qx '/usr/local/bin' && [ -w '/usr/local/bin' ]; then BIN_DIR="/usr/local/bin" return fi # Safe user-local fallback — always writable, no sudo required BIN_DIR="$HOME/.local/bin" } # ─── OS detection ──────────────────────────────────────────────────────────── # shellcheck disable=SC2034 detect_os() { OS="$(uname -s)" # shellcheck disable=SC2034 ARCH="$(uname -m)" DISTRO="" # shellcheck disable=SC2034 DISTRO_VERSION="" # shellcheck disable=SC2034 IS_WSL=false PKG_MANAGER="" case "$OS" in Linux) local kern kern="$(uname -r)" case "$kern" in *icrosoft*WSL2|*icrosoft*wsl2) IS_WSL=true ;; *icrosoft) fatal "WSL1 is not supported. Please upgrade to WSL2." ;; esac if [ -f /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release # shellcheck disable=SC2034 DISTRO="${ID:-unknown}" # shellcheck disable=SC2034 DISTRO_VERSION="${VERSION_ID:-}" fi if available apt-get; then PKG_MANAGER="apt" elif available dnf; then PKG_MANAGER="dnf" elif available yum; then PKG_MANAGER="yum" elif available pacman; then PKG_MANAGER="pacman" elif available apk; then PKG_MANAGER="apk" elif available nix-env || available nix; then PKG_MANAGER="nix" fi ;; Darwin) # shellcheck disable=SC2034 DISTRO="macos" if available brew; then PKG_MANAGER="brew" elif available nix-env || available nix; then PKG_MANAGER="nix" fi ;; *) fatal "Unsupported operating system: $OS. Coqui supports Linux and macOS." ;; esac } # ─── PHP checks ────────────────────────────────────────────────────────────── check_php() { status "Checking PHP..." if ! available php; then warn "PHP is not installed." install_php return fi local php_version php_version="$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')" local php_major php_minor php_major="$(echo "$php_version" | cut -d. -f1)" php_minor="$(echo "$php_version" | cut -d. -f2)" if [ "$php_major" -lt "$REQUIRED_PHP_MAJOR" ] || \ { [ "$php_major" -eq "$REQUIRED_PHP_MAJOR" ] && [ "$php_minor" -lt "$REQUIRED_PHP_MINOR" ]; }; then warn "PHP $php_version found, but PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ is required." install_php return fi success "PHP $php_version" } install_php() { case "$PKG_MANAGER" in apt) if confirm "Install PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR} via Ondrej PPA?"; then status "Adding Ondrej PHP repository..." $SUDO apt-get update -qq $SUDO apt-get install -y -qq software-properties-common ca-certificates lsb-release >/dev/null $SUDO add-apt-repository -y ppa:ondrej/php >/dev/null 2>&1 $SUDO apt-get update -qq local phpv="${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}" local packages="php${phpv}-cli php${phpv}-curl php${phpv}-mbstring php${phpv}-xml php${phpv}-zip php${phpv}-intl php${phpv}-readline php${phpv}-sqlite3" status "Installing PHP ${phpv} and extensions..." # shellcheck disable=SC2086 $SUDO apt-get install -y -qq $packages >/dev/null success "PHP ${phpv} installed" else fatal "PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ is required. Install it and re-run the installer." fi ;; brew) if confirm "Install PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR} via Homebrew?"; then status "Installing PHP via Homebrew..." brew install php@${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR} >/dev/null brew link --force --overwrite php@${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR} >/dev/null success "PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR} installed" else fatal "PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ is required." fi ;; dnf|yum) if confirm "Install PHP via ${PKG_MANAGER}?"; then status "Installing PHP via ${PKG_MANAGER}..." # shellcheck disable=SC2086 $SUDO ${PKG_MANAGER} install -y php-cli php-curl php-mbstring php-xml php-zip php-intl php-pdo >/dev/null success "PHP installed" else fatal "PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ is required." fi ;; pacman) if confirm "Install PHP via pacman?"; then status "Installing PHP via pacman..." $SUDO pacman -S --noconfirm php php-sqlite php-intl >/dev/null success "PHP installed" else fatal "PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ is required." fi ;; apk) if confirm "Install PHP via apk?"; then status "Installing PHP via apk..." local phpv="${REQUIRED_PHP_MAJOR}${REQUIRED_PHP_MINOR}" $SUDO apk add --no-cache php${phpv} php${phpv}-cli php${phpv}-curl php${phpv}-mbstring php${phpv}-xml php${phpv}-zip php${phpv}-intl php${phpv}-pdo_sqlite >/dev/null success "PHP installed" else fatal "PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ is required." fi ;; nix) if confirm "Install PHP via Nix?"; then status "Installing PHP via Nix..." local phpv="${REQUIRED_PHP_MAJOR}${REQUIRED_PHP_MINOR}" if available nix-env; then nix-env -iA nixpkgs.php${phpv} >/dev/null 2>&1 || nix-env -iA nixpkgs.php >/dev/null 2>&1 elif available nix; then nix profile install nixpkgs#php${phpv} >/dev/null 2>&1 || nix profile install nixpkgs#php >/dev/null 2>&1 fi success "PHP installed" else fatal "PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ is required." fi ;; *) echo "" echo " Please install PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ with the following extensions:" echo " curl, mbstring, pdo_sqlite, xml, zip, intl" echo "" echo " See: https://www.php.net/manual/en/install.php" echo "" fatal "PHP ${REQUIRED_PHP_MAJOR}.${REQUIRED_PHP_MINOR}+ is required." ;; esac } # ─── Extension checks ──────────────────────────────────────────────────────── check_extensions() { status "Checking PHP extensions..." local missing="" local loaded loaded="$(php -m 2>/dev/null)" for ext in $REQUIRED_EXTENSIONS; do if ! echo "$loaded" | grep -qi "^${ext}$"; then missing="${missing} ${ext}" fi done if [ -z "$missing" ]; then success "All required extensions available" return fi warn "Missing PHP extensions:${missing}" if [ "$PKG_MANAGER" = "apt" ]; then local phpv phpv="$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')" # Map extension names to apt package names local packages="" for ext in $missing; do case "$ext" in pdo_sqlite) packages="${packages} php${phpv}-sqlite3" ;; *) packages="${packages} php${phpv}-${ext}" ;; esac done if confirm "Install missing extensions via apt?"; then status "Installing:${packages}" # shellcheck disable=SC2086 $SUDO apt-get install -y -qq $packages >/dev/null success "Extensions installed" else fatal "Required extensions missing:${missing}" fi elif [ "$PKG_MANAGER" = "dnf" ] || [ "$PKG_MANAGER" = "yum" ]; then local packages="" for ext in $missing; do case "$ext" in pdo_sqlite) packages="${packages} php-pdo" ;; *) packages="${packages} php-${ext}" ;; esac done if confirm "Install missing extensions via ${PKG_MANAGER}?"; then status "Installing:${packages}" # shellcheck disable=SC2086 $SUDO ${PKG_MANAGER} install -y $packages >/dev/null success "Extensions installed" else fatal "Required extensions missing:${missing}" fi elif [ "$PKG_MANAGER" = "apk" ]; then local phpv="${REQUIRED_PHP_MAJOR}${REQUIRED_PHP_MINOR}" local packages="" for ext in $missing; do packages="${packages} php${phpv}-${ext}" done if confirm "Install missing extensions via apk?"; then status "Installing:${packages}" # shellcheck disable=SC2086 $SUDO apk add --no-cache $packages >/dev/null success "Extensions installed" else fatal "Required extensions missing:${missing}" fi elif [ "$PKG_MANAGER" = "nix" ]; then warn "Missing extensions:${missing}. Nix manages PHP extensions via its derivation configuration. Please ensure your Nix environment has these extensions enabled." if ! confirm "Ignore missing extensions warning and continue?"; then fatal "Required extensions missing:${missing}" fi else # pacman and brew generally bundle these extensions with their base php packages, # or require manual edits to php.ini to enable them. echo "" echo " Please enable or install the following PHP extensions:${missing}" echo "" fatal "Required PHP extensions missing." fi } # ─── Performance checks ───────────────────────────────────────────────────── check_opcache() { status "Checking OPcache / JIT..." local opcache_loaded opcache_loaded="$(php -r 'echo extension_loaded("Zend OPcache") ? "1" : "0";' 2>/dev/null)" if [ "$opcache_loaded" != "1" ]; then warn "OPcache extension not loaded — install php-opcache for faster performance" return fi local opcache_enabled opcache_enabled="$(php -r 'echo ini_get("opcache.enable_cli") ?: "0";' 2>/dev/null)" if [ "$opcache_enabled" != "1" ]; then warn "OPcache CLI not enabled — set opcache.enable_cli=1 in php.ini" else success "OPcache CLI enabled" fi local jit_buffer jit_buffer="$(php -r 'echo ini_get("opcache.jit_buffer_size") ?: "0";' 2>/dev/null)" if [ "$jit_buffer" = "0" ] || [ -z "$jit_buffer" ]; then warn "JIT disabled — set opcache.jit=1255 and opcache.jit_buffer_size=128M for improved loop performance" else success "JIT enabled (buffer: ${jit_buffer})" fi } # ─── Git check ─────────────────────────────────────────────────────────────── check_git() { status "Checking git..." if available git; then success "git $(git --version | awk '{print $3}')" return fi case "$PKG_MANAGER" in apt) if confirm "Install git via apt?"; then $SUDO apt-get install -y -qq git >/dev/null success "git installed" else fatal "git is required." fi ;; brew) if confirm "Install git via Homebrew?"; then brew install git >/dev/null success "git installed" else fatal "git is required. Install it with: brew install git" fi ;; dnf|yum) if confirm "Install git via ${PKG_MANAGER}?"; then $SUDO ${PKG_MANAGER} install -y git >/dev/null success "git installed" else fatal "git is required." fi ;; pacman) if confirm "Install git via pacman?"; then $SUDO pacman -S --noconfirm git >/dev/null success "git installed" else fatal "git is required." fi ;; apk) if confirm "Install git via apk?"; then $SUDO apk add --no-cache git >/dev/null success "git installed" else fatal "git is required." fi ;; nix) if confirm "Install git via Nix?"; then if available nix-env; then nix-env -iA nixpkgs.git >/dev/null 2>&1 elif available nix; then nix profile install nixpkgs#git >/dev/null 2>&1 fi success "git installed" else fatal "git is required." fi ;; *) fatal "git is required. Please install it and re-run the installer." ;; esac } # ─── Composer check ────────────────────────────────────────────────────────── check_composer() { status "Checking Composer..." if available composer; then success "Composer $(composer --version 2>/dev/null | awk '{print $NF}' | head -1)" return fi if confirm "Composer not found. Install it now?"; then install_composer else fatal "Composer is required." fi } install_composer() { status "Downloading Composer installer..." local expected_sig expected_sig="$(curl -fsSL https://composer.github.io/installer.sig)" php -r "copy('https://getcomposer.org/installer', '/tmp/composer-setup.php');" local actual_sig actual_sig="$(php -r "echo hash_file('sha384', '/tmp/composer-setup.php');")" if [ "$expected_sig" != "$actual_sig" ]; then rm -f /tmp/composer-setup.php fatal "Composer installer signature mismatch. Download may be corrupted." fi status "Installing Composer..." php /tmp/composer-setup.php --quiet rm -f /tmp/composer-setup.php # Move composer.phar to a directory in PATH detect_bin_dir if [ "$BIN_DIR" = "/usr/local/bin" ] && [ "$(id -u)" -ne 0 ]; then $SUDO mv composer.phar "$BIN_DIR/composer" else mkdir -p "$BIN_DIR" mv composer.phar "$BIN_DIR/composer" fi success "Composer installed to $BIN_DIR/composer" } # ─── Installation detection ───────────────────────────────────────────────── # Check for a git-based (dev) installation is_dev_installed() { [ -d "$COQUI_INSTALL_DIR" ] && [ -d "$COQUI_INSTALL_DIR/.git" ] } # Check for a release-based installation is_release_installed() { [ -d "$COQUI_INSTALL_DIR" ] && [ -f "$COQUI_INSTALL_DIR/.coqui-version" ] } # Check for any installation is_installed() { is_dev_installed || is_release_installed } # Read the currently installed release version get_installed_version() { if [ -f "$COQUI_INSTALL_DIR/.coqui-version" ]; then cat "$COQUI_INSTALL_DIR/.coqui-version" else echo "" fi } # ─── GitHub release functions ──────────────────────────────────────────────── # Fetch the latest release version from the GitHub API. # Sets LATEST_VERSION to the semver string (e.g. "0.0.1"). fetch_latest_version() { if [ -n "$COQUI_VERSION" ]; then LATEST_VERSION="$COQUI_VERSION" return fi status "Checking latest release..." local api_response api_response="$(curl -fsSL "$COQUI_API_URL" 2>/dev/null)" \ || fatal "Failed to fetch release info from GitHub. Check your internet connection or try --dev." # Parse tag_name from JSON without jq (works with grep + sed) local tag_name tag_name="$(echo "$api_response" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" if [ -z "$tag_name" ]; then fatal "Could not determine latest version from GitHub. Try: COQUI_VERSION=0.0.1 $0" fi # Strip leading 'v' if present LATEST_VERSION="${tag_name#v}" success "Latest release: v${LATEST_VERSION}" } # Verify SHA-256 checksum of a downloaded file. # Usage: verify_checksum verify_checksum() { local file_path="$1" local checksum_url="$2" local expected_checksum status "Verifying checksum..." expected_checksum="$(curl -fsSL "$checksum_url" 2>/dev/null)" || { warn "Could not download checksum file. Skipping verification." return 0 } # The .sha256 file format is: "hash filename" local expected_hash expected_hash="$(echo "$expected_checksum" | awk '{print $1}')" local actual_hash if available sha256sum; then actual_hash="$(sha256sum "$file_path" | awk '{print $1}')" elif available shasum; then actual_hash="$(shasum -a 256 "$file_path" | awk '{print $1}')" else warn "No sha256sum or shasum available. Skipping verification." return 0 fi if [ "$expected_hash" != "$actual_hash" ]; then fatal "Checksum verification failed. The download may be corrupted. Expected: ${expected_hash}, Got: ${actual_hash}" fi success "Checksum verified" } # ─── Release install / update ──────────────────────────────────────────────── install_release() { fetch_latest_version local archive_name="coqui-v${LATEST_VERSION}.tar.gz" local download_url="${COQUI_DOWNLOAD_BASE}/v${LATEST_VERSION}/${archive_name}" local checksum_url="${download_url}.sha256" local tmp_dir tmp_dir="$(mktemp -d)" # shellcheck disable=SC2064 trap "rm -rf '$tmp_dir'" EXIT status "Downloading Coqui v${LATEST_VERSION}..." curl -fsSL "$download_url" -o "${tmp_dir}/${archive_name}" \ || fatal "Failed to download release v${LATEST_VERSION}. URL: ${download_url}" verify_checksum "${tmp_dir}/${archive_name}" "$checksum_url" status "Installing to ${COQUI_INSTALL_DIR}..." mkdir -p "$COQUI_INSTALL_DIR" # Extract — the archive contains a top-level coqui/ directory tar -xzf "${tmp_dir}/${archive_name}" -C "$tmp_dir" # Copy contents from extracted directory into install dir cp -a "${tmp_dir}/coqui/." "$COQUI_INSTALL_DIR/" # Write version marker echo "$LATEST_VERSION" > "$COQUI_INSTALL_DIR/.coqui-version" # Ensure bin scripts are executable chmod +x "$COQUI_INSTALL_DIR/bin/coqui" 2>/dev/null || true chmod +x "$COQUI_INSTALL_DIR/bin/coqui-launcher" 2>/dev/null || true rm -rf "$tmp_dir" trap - EXIT success "Coqui v${LATEST_VERSION} installed" } update_release() { fetch_latest_version local current_version current_version="$(get_installed_version)" if [ "$current_version" = "$LATEST_VERSION" ]; then success "Coqui v${current_version} is already up to date" return fi if [ -n "$current_version" ]; then status "Update available: v${current_version} -> v${LATEST_VERSION}" fi if ! confirm "Update Coqui to v${LATEST_VERSION}?"; then success "Update skipped" return fi local archive_name="coqui-v${LATEST_VERSION}.tar.gz" local download_url="${COQUI_DOWNLOAD_BASE}/v${LATEST_VERSION}/${archive_name}" local checksum_url="${download_url}.sha256" local tmp_dir tmp_dir="$(mktemp -d)" # shellcheck disable=SC2064 trap "rm -rf '$tmp_dir'" EXIT status "Downloading Coqui v${LATEST_VERSION}..." curl -fsSL "$download_url" -o "${tmp_dir}/${archive_name}" \ || fatal "Failed to download release v${LATEST_VERSION}." verify_checksum "${tmp_dir}/${archive_name}" "$checksum_url" # Back up workspace directory if [ -d "$COQUI_INSTALL_DIR/.workspace" ]; then cp -a "$COQUI_INSTALL_DIR/.workspace" "${tmp_dir}/.workspace.bak" fi # Extract new release tar -xzf "${tmp_dir}/${archive_name}" -C "$tmp_dir" # Remove old files (except user data we already backed up) find "$COQUI_INSTALL_DIR" -mindepth 1 -maxdepth 1 \ ! -name '.workspace' \ -exec rm -rf {} + 2>/dev/null || true # Install new release cp -a "${tmp_dir}/coqui/." "$COQUI_INSTALL_DIR/" # Restore user data (overwrite any defaults from new release) if [ -d "${tmp_dir}/.workspace.bak" ]; then mkdir -p "$COQUI_INSTALL_DIR/.workspace" cp -a "${tmp_dir}/.workspace.bak/." "$COQUI_INSTALL_DIR/.workspace/" fi # Write version marker echo "$LATEST_VERSION" > "$COQUI_INSTALL_DIR/.coqui-version" # Ensure bin scripts are executable chmod +x "$COQUI_INSTALL_DIR/bin/coqui" 2>/dev/null || true chmod +x "$COQUI_INSTALL_DIR/bin/coqui-launcher" 2>/dev/null || true rm -rf "$tmp_dir" trap - EXIT success "Coqui updated to v${LATEST_VERSION}" } # ─── Dev (git) install / update ────────────────────────────────────────────── install_dev() { status "Cloning Coqui into ${COQUI_INSTALL_DIR}..." local clone_args="--depth 1" if [ -n "$COQUI_VERSION" ]; then clone_args="--branch $COQUI_VERSION --depth 1" fi # shellcheck disable=SC2086 git clone $clone_args "$COQUI_REPO" "$COQUI_INSTALL_DIR" 2>/dev/null \ || fatal "Failed to clone Coqui repository." success "Coqui cloned" run_composer_install } update_dev() { status "Checking for updates..." cd "$COQUI_INSTALL_DIR" # Stash any local changes (e.g. modified composer.lock) before pulling local stash_result stash_result="$(git stash --include-untracked 2>&1)" || true git fetch --quiet 2>/dev/null || fatal "Failed to fetch updates. Check your internet connection." local local_head remote_head local_head="$(git rev-parse HEAD)" remote_head="$(git rev-parse '@{u}' 2>/dev/null || echo "$local_head")" if [ "$local_head" = "$remote_head" ]; then success "Coqui is already up to date" # Restore stashed changes if echo "$stash_result" | grep -q "Saved working directory"; then git stash pop --quiet 2>/dev/null || true fi # Still run composer install in case dependencies changed locally run_composer_install return fi if confirm "A new version of Coqui is available. Update now?"; then status "Updating Coqui..." # Unshallow if needed (shallow clones can fail ff-only) if [ -f "$COQUI_INSTALL_DIR/.git/shallow" ]; then git fetch --unshallow --quiet 2>/dev/null || true fi if ! git pull --ff-only --quiet; then # Restore stash before reporting error if echo "$stash_result" | grep -q "Saved working directory"; then git stash pop --quiet 2>/dev/null || true fi fatal "Failed to update. Your local branch may have diverged. Try: rm -rf $COQUI_INSTALL_DIR && re-run the installer." fi success "Coqui updated" # Restore stashed changes — if pop fails, drop the stash since # the fresh pull state is authoritative and composer install will # regenerate composer.lock from the updated composer.json. if echo "$stash_result" | grep -q "Saved working directory"; then if ! git stash pop --quiet 2>/dev/null; then warn "Could not restore local changes from stash (likely a merge conflict)." warn "Dropping stash — composer install will regenerate lock file." git stash drop --quiet 2>/dev/null || true fi fi run_composer_install else # Restore stash if update was skipped if echo "$stash_result" | grep -q "Saved working directory"; then git stash pop --quiet 2>/dev/null || true fi success "Update skipped" fi } run_composer_install() { status "Installing dependencies..." cd "$COQUI_INSTALL_DIR" # Remove stale lock file if it conflicts with the current composer.json # (common after a git pull that changed composer.json while the old # composer.lock was stashed and couldn't be restored). if ! composer validate --no-check-all --no-check-publish --quiet 2>/dev/null; then warn "composer.lock is out of sync — regenerating..." rm -f composer.lock fi if ! composer install --no-dev --optimize-autoloader --no-interaction 2>&1; then fatal "Composer install failed. Run manually: cd $COQUI_INSTALL_DIR && composer install --no-dev" fi success "Dependencies installed" } # ─── Symlink ───────────────────────────────────────────────────────────────── create_symlink() { detect_bin_dir local target="${COQUI_INSTALL_DIR}/bin/coqui" # Ensure the bin script is executable chmod +x "$target" status "Creating symlink in ${BIN_DIR}..." mkdir -p "$BIN_DIR" ln -sf "$target" "$BIN_DIR/coqui" success "Symlink created: ${BIN_DIR}/coqui" # Warn if BIN_DIR is not in PATH if ! echo "$PATH" | tr ':' '\n' | grep -qx "$BIN_DIR"; then echo "" warn "${BIN_DIR} is not in your PATH." echo "" echo " Add it to your shell profile:" echo "" echo " echo 'export PATH=\"${BIN_DIR}:\$PATH\"' >> ~/.bashrc" echo "" fi } # ─── Banner ────────────────────────────────────────────────────────────────── show_banner() { if [ "$QUIET_MODE" = true ]; then return; fi echo "" echo " ${GREEN} ▄▄· .▄▄▄ ▄• ▄▌▪ ▄▄▄▄· ▄▄▄▄▄${RESET}" echo " ${GREEN}▐█ ▌▪▪ ▐▀•▀█ █▪██▌██ ▐█ ▀█▪▪ •██ ${RESET}" echo " ${GREEN}██ ▄▄ ▄█▀▄ █▌·.█▌█▌▐█▌▐█·▐█▀▀█▄ ▄█▀▄ ▐█.▪${RESET}" echo " ${GREEN}▐███▌▐█▌.▐▌▐█▪▄█·▐█▄█▌▐█▌██▄▪▐█▐█▌.▐▌ ▐█▌·${RESET}" echo " ${GREEN}·▀▀▀ ▀█▄▀▪·▀▀█. ▀▀▀ ▀▀▀·▀▀▀▀ ▀█▄▀▪ ▀▀▀ ${RESET}" echo "" echo " ${BOLD}Coqui Installer${RESET}" echo "" } # ─── Success message ───────────────────────────────────────────────────────── print_success() { local install_type="$1" local version_info="" if [ -n "$LATEST_VERSION" ]; then version_info=" v${LATEST_VERSION}" elif [ -f "$COQUI_INSTALL_DIR/.coqui-version" ]; then version_info=" v$(cat "$COQUI_INSTALL_DIR/.coqui-version")" fi if [ "$QUIET_MODE" = true ]; then progress "${install_type} complete!${version_info}" return fi echo "" echo " ──────────────────────────────────────────" echo " ${BOLD}${GREEN}${install_type} complete!${version_info}${RESET}" echo " ──────────────────────────────────────────" echo "" echo " ${BOLD}Get started:${RESET}" echo "" echo " coqui" echo "" echo " ${BOLD}Add cloud providers${RESET} (optional):" echo "" echo " export OPENAI_API_KEY=\"sk-...\"" echo " export ANTHROPIC_API_KEY=\"sk-ant-...\"" echo "" echo " ${BOLD}Prerequisites:${RESET}" echo "" echo " Make sure Ollama is running: ollama serve" echo " Pull a model: ollama pull glm-4.7-flash" echo "" echo " ${BOLD}Docs:${RESET} https://github.com/AgentCoqui/coqui" echo "" } # ─── Main ──────────────────────────────────────────────────────────────────── main() { parse_args "$@" show_banner detect_os setup_sudo # ── Selective mode: run only the requested components ── if [ "$SELECTIVE_MODE" = true ]; then if [ "$INSTALL_PHP" = true ]; then check_php check_extensions check_opcache fi if [ "$INSTALL_COMPOSER" = true ]; then # Composer needs PHP — verify it exists even if --install-php wasn't passed if ! available php; then fatal "PHP is required to install Composer. Re-run with --install-php or install PHP manually." fi check_composer fi if [ "$INSTALL_COQUI" = true ]; then if ! available php; then fatal "PHP is required to install Coqui. Re-run with --install-php or install PHP manually." fi if [ "$DEV_MODE" = true ]; then # Dev mode: requires Git + Composer if ! available composer; then fatal "Composer is required for --dev installs. Re-run with --install-composer or install Composer manually." fi check_git if is_dev_installed; then update_dev else install_dev fi else # Release mode: download pre-built archive (no Git/Composer needed) if is_dev_installed; then echo "" warn "A development (git) installation was found at ${COQUI_INSTALL_DIR}" echo " Use ${BOLD}--dev${RESET} to update it, or remove it first:" echo " rm -rf ${COQUI_INSTALL_DIR}" echo "" fatal "Cannot install release over a dev installation." fi if is_release_installed; then update_release else install_release fi fi create_symlink fi echo "" success "Done" echo "" return fi # ── Full install (no --install-* flags — backward compatible) ── if [ "$DEV_MODE" = true ]; then # Dev mode full install if is_dev_installed; then echo " ${ARROW} Existing dev installation found at ${COQUI_INSTALL_DIR}" echo "" check_php check_extensions check_opcache check_composer update_dev create_symlink print_success "Update" else if is_release_installed; then echo "" warn "A release installation was found at ${COQUI_INSTALL_DIR}" echo " Remove it first to switch to dev mode:" echo " rm -rf ${COQUI_INSTALL_DIR}" echo "" fatal "Cannot install dev over a release installation." fi check_php check_extensions check_opcache check_git check_composer install_dev create_symlink print_success "Installation" fi else # Release mode full install (default) if is_dev_installed; then echo "" warn "A development (git) installation was found at ${COQUI_INSTALL_DIR}" echo " To update it, re-run with ${BOLD}--dev${RESET}" echo " To switch to release mode, remove it first:" echo " rm -rf ${COQUI_INSTALL_DIR}" echo "" fatal "Cannot install release over a dev installation." fi if is_release_installed; then echo " ${ARROW} Existing installation found at ${COQUI_INSTALL_DIR}" echo "" check_php check_extensions check_opcache update_release create_symlink print_success "Update" else check_php check_extensions check_opcache install_release create_symlink print_success "Installation" fi fi } # Wrap in main() so partial curl downloads don't execute incomplete script main "$@"