#!/bin/sh # # Lovelace CLI Installer # # Complete, self-contained installer that works with any POSIX shell. # Automatically adapts to shell capabilities without additional downloads. # # Usage: # curl -fsSL https://d.uselovelace.com/install | sh # # Specific version: # curl -fsSL https://d.uselovelace.com/install | sh -s -- --version 1.2.3 # # Environment Variables: # LOVELACE_VERSION Version to install (default: latest) # LOVELACE_INSTALL_DIR Custom install path (default: ~/.lovelace) # LOVELACE_NO_TELEMETRY Set to 1 to disable anonymous install telemetry # LOVELACE_CI Set to 1 to force CI mode # GITHUB_TOKEN Authenticated GitHub API calls (avoids rate limits) # # CI Usage (GitHub Actions): # - run: curl -fsSL https://d.uselovelace.com/install | sh # env: # LOVELACE_VERSION: latest # - run: lovelace --version # env: # LOVELACE_API_KEY: ${{ secrets.LOVELACE_API_KEY }} # set -eu # ============================================================================ # Configuration and Constants # ============================================================================ readonly INSTALLER_VERSION="1.0.0" readonly DEFAULT_VERSION="latest" readonly DEFAULT_BASE_URL="https://github.com/ReasonableTech/lovelace/releases/download" # Environment variables with defaults LOVELACE_VERSION="${LOVELACE_VERSION:-$DEFAULT_VERSION}" LOVELACE_INSTALL_DIR="${LOVELACE_INSTALL_DIR:-}" LOVELACE_BASE_URL="${LOVELACE_BASE_URL:-$DEFAULT_BASE_URL}" LOVELACE_COMPONENTS="${LOVELACE_COMPONENTS:-cli}" LOVELACE_SKIP_VERIFY="${LOVELACE_SKIP_VERIFY:-0}" LOVELACE_NO_TELEMETRY="${LOVELACE_NO_TELEMETRY:-}" # Global state variables CI_MODE=0 PLATFORM="" ARCH="" INSTALL_DIR="" BIN_DIR="" TEMP_DIR="" SHELL_TYPE="" # ============================================================================ # Shell Detection and Capability Testing # ============================================================================ detect_shell_capabilities() { # Test for advanced shell features if [ -n "${BASH_VERSION:-}" ] && [ "${BASH_VERSINFO[0]:-0}" -ge 4 ] 2>/dev/null; then SHELL_TYPE="bash" return 0 fi if [ -n "${ZSH_VERSION:-}" ]; then SHELL_TYPE="zsh" return 0 fi # Test for specific shell features without relying on version variables if command -v bash >/dev/null 2>&1; then # Test if bash supports our required features if bash -c 'set -euo pipefail; [[ "test" == "test" ]] 2>/dev/null && exit 0 || exit 1' 2>/dev/null; then SHELL_TYPE="bash" return 0 fi fi # Default to POSIX mode SHELL_TYPE="posix" } # ============================================================================ # Color and Output Functions (Shell-Adaptive) # ============================================================================ setup_colors() { if [ -t 1 ] && command -v tput >/dev/null 2>&1; then RED=$(tput setaf 1 2>/dev/null || echo "") GREEN=$(tput setaf 2 2>/dev/null || echo "") YELLOW=$(tput setaf 3 2>/dev/null || echo "") BLUE=$(tput setaf 4 2>/dev/null || echo "") MAGENTA=$(tput setaf 5 2>/dev/null || echo "") CYAN=$(tput setaf 6 2>/dev/null || echo "") BOLD=$(tput bold 2>/dev/null || echo "") RESET=$(tput sgr0 2>/dev/null || echo "") else RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" RESET="" fi } log() { level="$1" shift message="$*" case "$level" in INFO) if [ "$CI_MODE" = "1" ]; then echo "::notice::$message" else printf "%sšŸ”%s %s\n" "$BLUE" "$RESET" "$message" fi ;; SUCCESS) if [ "$CI_MODE" = "1" ]; then echo "::notice::āœ… $message" else printf "%sāœ…%s %s\n" "$GREEN" "$RESET" "$message" fi ;; WARN) if [ "$CI_MODE" = "1" ]; then echo "::warning::$message" else printf "%sāš ļø%s %s\n" "$YELLOW" "$RESET" "$message" >&2 fi ;; ERROR) if [ "$CI_MODE" = "1" ]; then echo "::error::$message" else printf "%sāŒ%s %s\n" "$RED" "$RESET" "$message" >&2 fi ;; DEBUG) if [ "${LOVELACE_DEBUG:-0}" = "1" ]; then printf "%sšŸ”%s [DEBUG] %s\n" "$MAGENTA" "$RESET" "$message" >&2 fi ;; esac } # ============================================================================ # Error Handling and Cleanup # ============================================================================ cleanup() { if [ -n "${TEMP_DIR:-}" ] && [ -d "$TEMP_DIR" ]; then log DEBUG "Cleaning up temporary directory: $TEMP_DIR" rm -rf "$TEMP_DIR" fi } trap cleanup EXIT error_exit() { message="$1" exit_code="${2:-1}" log ERROR "$message" if [ "$CI_MODE" = "1" ]; then echo "::set-output name=success::false" echo "::set-output name=error::$message" fi exit "$exit_code" } # ============================================================================ # CI Environment Detection # ============================================================================ detect_ci_environment() { # Check for common CI environment variables if [ -n "${CI:-}" ] || [ -n "${CONTINUOUS_INTEGRATION:-}" ]; then CI_MODE=1 export LOVELACE_CI=1 log DEBUG "CI environment detected (LOVELACE_CI=1 exported)" # GitHub Actions specific handling if [ -n "${GITHUB_ACTIONS:-}" ]; then log DEBUG "GitHub Actions detected" echo "::group::Installing Lovelace $LOVELACE_VERSION" return fi # Other CI platforms if [ -n "${GITLAB_CI:-}" ]; then log DEBUG "GitLab CI detected" elif [ -n "${CIRCLECI:-}" ]; then log DEBUG "CircleCI detected" elif [ -n "${JENKINS_URL:-}" ]; then log DEBUG "Jenkins detected" elif [ -n "${TRAVIS:-}" ]; then log DEBUG "Travis CI detected" else log DEBUG "Generic CI environment detected" fi else log DEBUG "Interactive installation mode" fi } # ============================================================================ # Platform and Architecture Detection # ============================================================================ detect_platform() { os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in linux*) PLATFORM="linux" ;; darwin*) PLATFORM="darwin" ;; mingw*|msys*|cygwin*) PLATFORM="win32" ;; *) error_exit "Unsupported operating system: $os" ;; esac log DEBUG "Detected platform: $PLATFORM" } detect_architecture() { arch=$(uname -m) case "$arch" in x86_64|amd64) ARCH="x64" ;; aarch64|arm64) ARCH="arm64" ;; armv7*|armv6*) ARCH="arm" ;; *) error_exit "Unsupported architecture: $arch" ;; esac log DEBUG "Detected architecture: $ARCH" } # ============================================================================ # Installation Directory Management # ============================================================================ determine_install_dir() { if [ -n "$LOVELACE_INSTALL_DIR" ]; then INSTALL_DIR="$LOVELACE_INSTALL_DIR" log DEBUG "Using custom install directory: $INSTALL_DIR" elif [ "$CI_MODE" = "1" ]; then # CI-specific installation paths if [ -n "${RUNNER_TOOL_CACHE:-}" ]; then # GitHub Actions INSTALL_DIR="$RUNNER_TOOL_CACHE/lovelace/$LOVELACE_VERSION" else # Generic CI - system-wide installation INSTALL_DIR="/usr/local" fi log DEBUG "Using CI install directory: $INSTALL_DIR" else # Interactive installation - user directory INSTALL_DIR="$HOME/.lovelace" log DEBUG "Using default install directory: $INSTALL_DIR" fi BIN_DIR="$INSTALL_DIR/bin" } # ============================================================================ # Version Resolution # ============================================================================ resolve_version() { version="$LOVELACE_VERSION" if [ "$version" = "latest" ]; then log INFO "Resolving latest version..." # Try to fetch latest version from GitHub releases if command -v curl >/dev/null 2>&1; then latest_url="https://api.github.com/repos/ReasonableTech/lovelace/releases/latest" log DEBUG "Fetching latest version from: $latest_url" auth_header="" if [ -n "${GITHUB_TOKEN:-}" ]; then auth_header="-H \"Authorization: Bearer $GITHUB_TOKEN\"" log DEBUG "Using authenticated GitHub API request" fi if latest=$(eval curl -fsSL $auth_header "$latest_url" 2>/dev/null); then tag=$(echo "$latest" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') tag="${tag#v}" if [ -n "$tag" ]; then version="$tag" log SUCCESS "Latest version resolved: $version" else log WARN "Could not parse version from response, using fallback" version="0.1.0" fi else log WARN "Could not resolve latest version, using fallback" version="0.1.0" fi else log WARN "curl not available, using fallback version" version="0.1.0" fi fi echo "$version" } # ============================================================================ # Binary Download and Installation # ============================================================================ get_binary_name() { component="$1" version="$2" platform="$3" arch="$4" extension="" if [ "$platform" = "win32" ]; then extension=".exe" fi echo "${component}-v${version}-${platform}-${arch}${extension}" } download_with_progress() { url="$1" output_path="$2" # Build auth header if GITHUB_TOKEN is available auth_args="" if [ -n "${GITHUB_TOKEN:-}" ]; then auth_args="-H \"Authorization: Bearer $GITHUB_TOKEN\"" fi # Use curl with progress bar in interactive mode, silent in CI if [ "$CI_MODE" = "1" ]; then eval curl -fsSL $auth_args -o "$output_path" "$url" else eval curl -fSL --progress-bar $auth_args -o "$output_path" "$url" fi } download_with_retry() { url="$1" output_path="$2" max_attempts="${3:-3}" attempt=1 while [ "$attempt" -le "$max_attempts" ]; do if [ "$attempt" -gt 1 ]; then # Exponential backoff: 2, 4, 8 seconds delay=$((1 << attempt)) log INFO "Retry $attempt/$max_attempts in ${delay}s..." sleep "$delay" fi # Attempt the download if download_with_progress "$url" "$output_path" 2>/dev/null; then if [ -f "$output_path" ] && [ -s "$output_path" ]; then return 0 fi fi # Check for rate limiting by re-fetching headers if command -v curl >/dev/null 2>&1; then http_code=$(curl -sS -o /dev/null -w "%{http_code}" "$url" 2>/dev/null) || true if [ "$http_code" = "403" ] || [ "$http_code" = "429" ]; then log WARN "GitHub API rate limit hit (HTTP $http_code)" if [ "$attempt" -eq "$max_attempts" ]; then log ERROR "Rate limit exceeded after $max_attempts attempts." log ERROR "Set GITHUB_TOKEN to authenticate and raise your rate limit:" log ERROR " export GITHUB_TOKEN=ghp_your_token_here" return 1 fi fi fi log WARN "Download attempt $attempt/$max_attempts failed" attempt=$((attempt + 1)) done log ERROR "Download failed after $max_attempts attempts: $url" return 1 } verify_binary() { binary_path="$1" if [ "$LOVELACE_SKIP_VERIFY" = "1" ]; then log DEBUG "Skipping binary verification" return 0 fi # Basic verification - check if binary exists and is executable if [ ! -f "$binary_path" ]; then log ERROR "Binary not found: $binary_path" return 1 fi if [ "$PLATFORM" != "win32" ] && [ ! -x "$binary_path" ]; then log ERROR "Binary is not executable: $binary_path" return 1 fi # Try to run --version to verify it works log DEBUG "Verifying binary functionality..." if "$binary_path" --version >/dev/null 2>&1; then log SUCCESS "Binary verification successful" return 0 else log WARN "Binary verification failed, but continuing..." return 0 # Don't fail installation for this fi } # ============================================================================ # SHA256 Checksum Verification # ============================================================================ verify_checksum() { binary_path="$1" checksum_url="$2" if [ "$LOVELACE_SKIP_VERIFY" = "1" ]; then log DEBUG "Skipping checksum verification" return 0 fi log INFO "Verifying SHA256 checksum..." # Download the checksum file checksum_file="$TEMP_DIR/checksum.sha256" if ! curl -fsSL -o "$checksum_file" "$checksum_url" 2>/dev/null; then log WARN "Could not download checksum file, skipping verification" return 0 fi expected_hash=$(cat "$checksum_file" | tr -d '[:space:]') # Compute actual hash — prefer sha256sum (Linux), fall back to shasum (macOS) if command -v sha256sum >/dev/null 2>&1; then actual_hash=$(sha256sum "$binary_path" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then actual_hash=$(shasum -a 256 "$binary_path" | awk '{print $1}') else log WARN "No SHA256 tool available, skipping checksum verification" return 0 fi if [ "$expected_hash" = "$actual_hash" ]; then log SUCCESS "Checksum verified" return 0 else log ERROR "Checksum mismatch!" log ERROR " Expected: $expected_hash" log ERROR " Actual: $actual_hash" log ERROR "The downloaded binary may be corrupted or tampered with." return 1 fi } download_binary() { component="$1" version="$2" platform="$3" arch="$4" binary_name=$(get_binary_name "$component" "$version" "$platform" "$arch") download_url="$LOVELACE_BASE_URL/v$version/$binary_name" local_binary_name="$component" # Add .exe extension for Windows if [ "$platform" = "win32" ]; then local_binary_name="$component.exe" fi dest_path="$BIN_DIR/$local_binary_name" log INFO "Downloading $component from $download_url" # Create bin directory if it doesn't exist mkdir -p "$BIN_DIR" # Download binary with retry if download_with_retry "$download_url" "$dest_path" 3; then # Make executable on Unix-like systems if [ "$platform" != "win32" ]; then chmod +x "$dest_path" fi # Verify checksum against published .sha256 file checksum_url="$download_url.sha256" if ! verify_checksum "$dest_path" "$checksum_url"; then log ERROR "Checksum verification failed for $component" rm -f "$dest_path" return 1 fi # Verify binary runs if verify_binary "$dest_path"; then log SUCCESS "Downloaded and verified $component" return 0 else log ERROR "$component verification failed" return 1 fi else log ERROR "Failed to download $component" return 1 fi } # ============================================================================ # PATH Management # ============================================================================ add_to_path() { if [ "$CI_MODE" = "1" ]; then # CI mode: Add to PATH for current session only. # Shell profile updates are explicitly suppressed in CI — # runners are ephemeral and profile modifications are irrelevant. if [ -n "${GITHUB_PATH:-}" ]; then echo "$BIN_DIR" >> "$GITHUB_PATH" fi export PATH="$BIN_DIR:$PATH" log DEBUG "Added $BIN_DIR to PATH for CI session" return fi # Interactive installation - update shell profiles shell_profile="" # Detect shell and appropriate profile file case "${SHELL:-}" in */bash) if [ -f "$HOME/.bashrc" ]; then shell_profile="$HOME/.bashrc" elif [ -f "$HOME/.bash_profile" ]; then shell_profile="$HOME/.bash_profile" fi ;; */zsh) shell_profile="$HOME/.zshrc" ;; */fish) shell_profile="$HOME/.config/fish/config.fish" ;; *) # Default to .profile for maximum compatibility shell_profile="$HOME/.profile" ;; esac if [ -n "$shell_profile" ]; then path_export="export PATH=\"$BIN_DIR:\$PATH\"" # Check if already in PATH if ! grep -q "$BIN_DIR" "$shell_profile" 2>/dev/null; then echo "" >> "$shell_profile" echo "# Added by Lovelace installer" >> "$shell_profile" echo "$path_export" >> "$shell_profile" log SUCCESS "Added $BIN_DIR to PATH in $shell_profile" log INFO "Please restart your shell or run: . $shell_profile" else log DEBUG "PATH already contains $BIN_DIR" fi else log WARN "Could not detect shell profile, please manually add $BIN_DIR to your PATH" fi # Add to current session export PATH="$BIN_DIR:$PATH" } # ============================================================================ # Installation Failure Guidance # ============================================================================ show_manual_installation_help() { local failed_components="$1" log ERROR "Failed to install components: $failed_components" echo "" echo "Manual Installation Options:" echo "" echo "1. Download directly from releases:" echo " https://github.com/ReasonableTech/lovelace/releases" echo "" echo "2. Native install instructions:" echo " https://uselovelace.com/downloads" echo "" echo "3. Documentation and support:" echo " https://docs.uselovelace.com/support" echo " Email: support@uselovelace.com" echo "" echo "For troubleshooting, include this information:" echo " - Platform: $PLATFORM-$ARCH" echo " - Version requested: $LOVELACE_VERSION" echo " - Base URL: $LOVELACE_BASE_URL" echo "" } # ============================================================================ # Component Installation # ============================================================================ install_component() { component="$1" version="$2" log INFO "Installing $component $version for $PLATFORM-$ARCH" # Create temporary directory TEMP_DIR=$(mktemp -d) log DEBUG "Created temporary directory: $TEMP_DIR" if [ "$component" = "cli" ]; then components_to_install="ada lovelace" else components_to_install="$component" fi for binary in $components_to_install; do if download_binary "$binary" "$version" "$PLATFORM" "$ARCH"; then log SUCCESS "$binary installation completed" else log ERROR "Failed to install $binary" return 1 fi done return 0 } # ============================================================================ # Installation Summary # ============================================================================ print_installation_summary() { version="$1" echo "" log SUCCESS "Lovelace $version installed successfully!" echo "" if [ "$CI_MODE" != "1" ]; then echo "${BOLD}Installation Details:${RESET}" echo " šŸ“ Location: $INSTALL_DIR" echo " šŸ”§ Components: $LOVELACE_COMPONENTS" echo " šŸ’» Platform: $PLATFORM-$ARCH" echo " 🐚 Shell: $SHELL_TYPE" echo "" echo "${BOLD}Next Steps:${RESET}" echo " 1. Restart your shell or source your profile" echo " 2. Verify installation: ${CYAN}ada --version${RESET}" echo " 3. Get started: ${CYAN}ada --help${RESET}" echo "" echo "For more information, visit: ${BLUE}https://docs.uselovelace.com${RESET}" else # CI output echo "::set-output name=success::true" echo "::set-output name=version::$version" echo "::set-output name=install-path::$INSTALL_DIR" echo "::set-output name=shell-type::$SHELL_TYPE" if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::endgroup::" fi fi } # ============================================================================ # Install Telemetry (Optional, Anonymous) # ============================================================================ send_telemetry() { status="$1" version="$2" # Determine default: off in CI, on in interactive if [ -z "$LOVELACE_NO_TELEMETRY" ]; then if [ "$CI_MODE" = "1" ]; then # Off by default in CI unless explicitly opted in if [ "${LOVELACE_TELEMETRY:-0}" != "1" ]; then return 0 fi fi elif [ "$LOVELACE_NO_TELEMETRY" = "1" ]; then return 0 fi # Fire-and-forget: never block installation on telemetry telemetry_url="https://d.uselovelace.com/telemetry" payload="{\"os\":\"$PLATFORM\",\"arch\":\"$ARCH\",\"shell\":\"$SHELL_TYPE\",\"version\":\"$version\",\"ci\":$CI_MODE,\"status\":\"$status\",\"installer_version\":\"$INSTALLER_VERSION\"}" # Best-effort POST, timeout 2 seconds, discard all output curl -fsSL -X POST \ -H "Content-Type: application/json" \ -d "$payload" \ --max-time 2 \ "$telemetry_url" >/dev/null 2>&1 || true log DEBUG "Telemetry sent: $status" } # ============================================================================ # Main Installation Process # ============================================================================ main() { # Initialize setup_colors detect_shell_capabilities log INFO "Starting Lovelace installation (installer v$INSTALLER_VERSION)" log DEBUG "Shell type: $SHELL_TYPE" # Parse version from @version syntax (used by component-specific installers) for arg in "$@"; do case "$arg" in @*) LOVELACE_VERSION="${arg#@}" ;; esac done # Parse command line arguments while [ $# -gt 0 ]; do case $1 in @*) # Skip @version args as they're handled above shift ;; --version) LOVELACE_VERSION="$2" shift 2 ;; --install-dir) LOVELACE_INSTALL_DIR="$2" shift 2 ;; --components) LOVELACE_COMPONENTS="$2" shift 2 ;; --help) echo "${BOLD}Lovelace Standalone Installer${RESET}" echo "" echo "A self-contained installer that works with any POSIX shell" echo "and automatically adapts to your environment capabilities." echo "" echo "Usage: $0 [options]" echo "" echo "Options:" echo " --version Version to install (default: latest)" echo " --install-dir Installation directory" echo " --components Components to install (default: cli)" echo " --help Show this help message" echo "" echo "Environment Variables:" echo " LOVELACE_VERSION Version to install" echo " LOVELACE_INSTALL_DIR Installation directory" echo " LOVELACE_BASE_URL Base URL for downloads (default: https://github.com/ReasonableTech/lovelace/releases/download)" echo " LOVELACE_COMPONENTS Components to install" echo " LOVELACE_CI Force CI mode (auto-detected from \$CI, \$GITHUB_ACTIONS, etc.)" echo " LOVELACE_NO_TELEMETRY Set to 1 to disable anonymous install telemetry" echo " LOVELACE_TELEMETRY Set to 1 to enable telemetry in CI (off by default)" echo " LOVELACE_SKIP_VERIFY Skip binary verification" echo " LOVELACE_DEBUG Enable debug output" echo " GITHUB_TOKEN Authenticated GitHub API calls (avoids rate limits)" echo "" echo "Examples:" echo " # Production (default)" echo " curl -fsSL https://d.uselovelace.com/install | sh" echo "" echo " # Testing with custom base URL" echo " LOVELACE_BASE_URL=http://localhost:8080 curl -fsSL https://d.uselovelace.com/install | sh" echo "" echo " # Specific version" echo " curl -fsSL https://d.uselovelace.com/install | sh -s -- --version 1.2.3" echo "" echo " # Custom install directory" echo " LOVELACE_INSTALL_DIR=/opt/lovelace curl -fsSL https://d.uselovelace.com/install | sh" exit 0 ;; *) log WARN "Unknown option: $1" shift ;; esac done # Environment detection detect_ci_environment detect_platform detect_architecture determine_install_dir # Resolve version resolved_version=$(resolve_version) log INFO "Installing Lovelace $resolved_version" log DEBUG "Platform: $PLATFORM-$ARCH" log DEBUG "Install directory: $INSTALL_DIR" log DEBUG "Components: $LOVELACE_COMPONENTS" # Install each component # Split components by comma (POSIX-compatible) saved_IFS="$IFS" IFS=',' set -- $LOVELACE_COMPONENTS IFS="$saved_IFS" failed_components="" successful_installs=0 for component in "$@"; do # Trim whitespace component=$(echo "$component" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') log INFO "Installing component: $component" if install_component "$component" "$resolved_version"; then successful_installs=$((successful_installs + 1)) else if [ -n "$failed_components" ]; then failed_components="$failed_components,$component" else failed_components="$component" fi fi done # Handle failures if [ -n "$failed_components" ]; then if [ $successful_installs -eq 0 ]; then send_telemetry "failure" "$resolved_version" show_manual_installation_help "$failed_components" exit 1 else log WARN "Some components failed to install: $failed_components" log INFO "Continuing with successfully installed components" fi fi # Add to PATH add_to_path # Report telemetry send_telemetry "success" "$resolved_version" # Print summary print_installation_summary "$resolved_version" } # ============================================================================ # Script Entry Point # ============================================================================ # Run main function if script is executed directly or piped case "${0##*/}" in install.sh|install) # Direct execution: ./install.sh or ./install (CDN copy) main "$@" ;; sh|-sh|bash|-bash|/bin/sh|/bin/bash) # Piped execution: curl ... | sh main "$@" ;; esac