#!/bin/bash # Kaboom - The Ultimate One-liner Installer # https://github.com/brennhill/Kaboom-Browser-AI-Devtools-MCP # # PURPOSE: # This script provides a zero-dependency, platform-aware installation flow for Kaboom. # It handles binary acquisition, extension staging, and native configuration in one go. # # USAGE: # curl -sSL https://raw.githubusercontent.com/brennhill/Kaboom-Browser-AI-Devtools-MCP/STABLE/scripts/install.sh | bash # curl -sSL ... | sh -s -- --hooks-only # Install only the hooks binary # Fail immediately if a command fails (-e), an unset variable is used (-u), # or a command in a pipeline fails (-o pipefail). This is critical for installer safety. set -euo pipefail # ───────────────────────────────────────────────────────────── # CLI flag parsing # ───────────────────────────────────────────────────────────── HOOKS_ONLY="${KABOOM_HOOKS_ONLY:-0}" for arg in "$@"; do case "$arg" in --hooks-only) HOOKS_ONLY=1 ;; esac done # Configuration: Define the single source of truth for paths and repository metadata. REPO="brennhill/Kaboom-Browser-AI-Devtools-MCP" INSTALL_DIR="$HOME/.kaboom" BIN_DIR="$INSTALL_DIR/bin" EXT_DIR="${KABOOM_EXTENSION_DIR:-$HOME/KaboomAgenticDevtoolExtension}" STAGE_EXT_DIR="$INSTALL_DIR/.extension-stage-$$" BACKUP_EXT_DIR="$INSTALL_DIR/.extension-backup-$$" # The VERSION file on the STABLE branch is the source of truth for the latest release. VERSION_URL="https://raw.githubusercontent.com/$REPO/STABLE/VERSION" STRICT_CHECKSUM="${KABOOM_INSTALL_STRICT:-0}" # Minimum plausible binary sizes. Catches truncated downloads and HTML error pages. MIN_BINARY_BYTES=5000000 MIN_HOOKS_BINARY_BYTES=2000000 # UI: Define colors for high-visibility terminal output. RED='\033[0;31m' GREEN='\033[0;32m' BLUE='\033[0;34m' YELLOW='\033[1;33m' ORANGE='\033[38;5;208m' BOLD='\033[1m' NC='\033[0m' # No Color (Reset) # Anonymous install error beacon (disable: KABOOM_TELEMETRY=off). # Fire-and-forget, never blocks, never fails the install. beacon_error() { local step="${1:-unknown}" if [ "${KABOOM_TELEMETRY:-}" = "off" ]; then return; fi curl -s --max-time 2 -X POST "https://t.gokaboom.dev/v1/event" \ -H "Content-Type: application/json" \ -d "{\"event\":\"install_error\",\"v\":\"${VERSION:-unknown}\",\"os\":\"$(uname -s)-$(uname -m)\",\"props\":{\"step\":\"${step}\",\"method\":\"curl\"}}" \ > /dev/null 2>&1 || true } # Cleanup: Ensure temporary files are removed even if the script crashes or is interrupted. # Uses mktemp to prevent predictable filename attacks. TEMP_ROOT=$(mktemp -d) cleanup() { rm -rf "$STAGE_EXT_DIR" "$BACKUP_EXT_DIR" rm -rf "$TEMP_ROOT" } trap cleanup EXIT echo -e "${ORANGE}${BOLD}" cat <<'EOF' _ __ ____ ___ ___ __ __ _ | |/ /__ _| __ ) / _ \ / _ \| \/ | | | ' // _` | _ \| | | | | | | |\/| | | | . \ (_| | |_) | |_| | |_| | | | |_| |_|\_\__,_|____/ \___/ \___/|_| |_(_) EOF echo -e "${NC}" if [ "$HOOKS_ONLY" = "1" ]; then echo -e "${ORANGE}${BOLD}KaBOOM! Hooks Installer${NC} (hooks-only mode)" else echo -e "${ORANGE}${BOLD}KaBOOM! Installer${NC}" fi echo -e "${BLUE}--------------------------------------------------${NC}" if [ "$STRICT_CHECKSUM" = "1" ]; then echo -e "Strict checksum mode enabled (KABOOM_INSTALL_STRICT=1)" fi # ───────────────────────────────────────────────────────────── # Prerequisite Checks # ───────────────────────────────────────────────────────────── check_prerequisites() { local missing="" if ! command -v curl >/dev/null 2>&1; then missing="${missing} - curl (required for downloads)\n" fi if [ "$HOOKS_ONLY" != "1" ] && ! command -v unzip >/dev/null 2>&1; then missing="${missing} - unzip (required for extension extraction)\n" fi if [ -n "$missing" ]; then echo -e "${RED}Missing required tools:${NC}" echo -e "$missing" echo -e "Install them with your package manager and re-run." exit 1 fi } check_disk_space() { # Full install: ~50 MB (binary + extension + temp files). # Hooks only: ~15 MB (hooks binary + temp files). local required_mb=50 if [ "$HOOKS_ONLY" = "1" ]; then required_mb=15 fi local available_mb=0 if command -v df >/dev/null 2>&1; then # df -Pm gives POSIX-portable megabyte output; grab the mount containing $HOME. available_mb=$(df -Pm "$HOME" 2>/dev/null | awk 'NR==2 {print $4}' || echo "0") fi if [ "${available_mb:-0}" -gt 0 ] && [ "$available_mb" -lt "$required_mb" ]; then echo -e "${RED}Insufficient disk space: ${available_mb} MB available, need ${required_mb} MB.${NC}" echo -e "Free up space in $HOME and re-run." exit 1 fi } check_network_connectivity() { # Quick connectivity probe — catch proxy/firewall/offline errors early # with a clear message instead of a raw curl failure later. if ! curl -fsSL --max-time 10 -o /dev/null "https://github.com" 2>/dev/null; then echo -e "${YELLOW}Cannot reach github.com — check your network connection or proxy settings.${NC}" echo -e "If you are behind a corporate proxy, set https_proxy before running." exit 1 fi } check_prerequisites check_disk_space check_network_connectivity # ───────────────────────────────────────────────────────────── # Retry-capable download helper # ───────────────────────────────────────────────────────────── # curl_retry wraps curl with automatic retry for transient network errors. # Usage: curl_retry [extra_curl_flags...] # Retries up to 3 times with exponential backoff (2s, 4s, 8s). curl_retry() { local output="$1" local url="$2" shift 2 local max_attempts=3 local attempt=1 local delay=2 while [ $attempt -le $max_attempts ]; do if curl -fsSL --connect-timeout 15 --max-time 120 "$@" "$url" -o "$output" 2>/dev/null; then return 0 fi if [ $attempt -lt $max_attempts ]; then echo -e "${YELLOW} Download attempt $attempt/$max_attempts failed; retrying in ${delay}s...${NC}" sleep $delay delay=$((delay * 2)) fi attempt=$((attempt + 1)) done return 1 } # ───────────────────────────────────────────────────────────── # Extension staging helpers # ───────────────────────────────────────────────────────────── prepare_extension_stage() { rm -rf "$STAGE_EXT_DIR" mkdir -p "$STAGE_EXT_DIR" } validate_extension_stage() { local base_dir="${1:-$EXT_DIR}" [ -f "$base_dir/manifest.json" ] || return 1 # Support both modern bundled extension layout and legacy modular layout. local has_background=0 local has_content=0 local has_inject=0 local has_bootstrap=0 [ -f "$base_dir/background.js" ] || [ -f "$base_dir/background/init.js" ] && has_background=1 [ -f "$base_dir/content.bundled.js" ] || [ -f "$base_dir/content/script-injection.js" ] && has_content=1 [ -f "$base_dir/inject.bundled.js" ] || [ -f "$base_dir/inject/index.js" ] && has_inject=1 [ -f "$base_dir/early-patch.bundled.js" ] || [ -f "$base_dir/theme-bootstrap.js" ] && has_bootstrap=1 [ "$has_background" -eq 1 ] && [ "$has_content" -eq 1 ] && [ "$has_inject" -eq 1 ] && [ "$has_bootstrap" -eq 1 ] } promote_extension_stage() { if ! validate_extension_stage "$STAGE_EXT_DIR"; then echo -e "${RED}Extension staging failed: required module files are missing from staging.${NC}" exit 1 fi rm -rf "$BACKUP_EXT_DIR" if [ -d "$EXT_DIR" ]; then mv "$EXT_DIR" "$BACKUP_EXT_DIR" fi mkdir -p "$(dirname "$EXT_DIR")" if ! mv "$STAGE_EXT_DIR" "$EXT_DIR"; then echo -e "${RED}Failed to promote staged extension directory.${NC}" if [ -d "$BACKUP_EXT_DIR" ]; then mv "$BACKUP_EXT_DIR" "$EXT_DIR" || true fi exit 1 fi if ! validate_extension_stage "$EXT_DIR"; then echo -e "${RED}Promoted extension failed validation; restoring previous extension.${NC}" rm -rf "$EXT_DIR" if [ -d "$BACKUP_EXT_DIR" ]; then mv "$BACKUP_EXT_DIR" "$EXT_DIR" || true fi exit 1 fi rm -rf "$BACKUP_EXT_DIR" } stage_extension_from_source_zip() { local source_zip_url="$1" local temp_extract="$TEMP_ROOT/ext_extract" rm -rf "$temp_extract" mkdir -p "$temp_extract" if ! curl_retry "$TEMP_ZIP" "$source_zip_url"; then return 1 fi prepare_extension_stage if ! unzip -q "$TEMP_ZIP" -d "$temp_extract"; then return 1 fi # The source zip root folder is typically 'repo-branch'. local extract_root extract_root=$(find "$temp_extract" -mindepth 1 -maxdepth 1 -type d | head -n 1) if [ -z "$extract_root" ] || [ ! -d "$extract_root/extension" ]; then return 1 fi if ! cp -r "$extract_root/extension/." "$STAGE_EXT_DIR/"; then return 1 fi if ! validate_extension_stage "$STAGE_EXT_DIR"; then return 1 fi return 0 } purge_legacy_install_artifacts() { local legacy_path="" for legacy_path in \ "$BIN_DIR/kaboom$BINARY_EXT" \ "$BIN_DIR/kaboom-agentic-browser$BINARY_EXT" \ "$BIN_DIR/kaboom-agentic-devtools$BINARY_EXT" \ "$BIN_DIR/kaboom-hooks$BINARY_EXT" \ "$BIN_DIR/gasoline$BINARY_EXT" \ "$BIN_DIR/gasoline-agentic-browser$BINARY_EXT" \ "$BIN_DIR/gasoline-agentic-devtools$BINARY_EXT" \ "$BIN_DIR/gasoline-hooks$BINARY_EXT" \ "$BIN_DIR/strum$BINARY_EXT" \ "$BIN_DIR/strum-hooks$BINARY_EXT" do rm -f "$legacy_path" 2>/dev/null || true done } # ───────────────────────────────────────────────────────────── # Stale process cleanup (pre-install) # ───────────────────────────────────────────────────────────── kill_stale_kaboom_processes() { # Kill any running Kaboom daemons before replacing the binary. # This avoids "text file busy" on Linux and ensures a clean upgrade. local killed=0 local pids="" if command -v pgrep >/dev/null 2>&1; then pids=$(pgrep -f 'kaboom-agentic-browser|kaboom.*--daemon|kaboom-agentic-devtools|gasoline-agentic-browser|gasoline.*--daemon|gasoline-agentic-devtools|strum(\.exe)?|strum.*--daemon' 2>/dev/null || true) elif command -v pkill >/dev/null 2>&1; then # pgrep not available but pkill is — just send TERM directly. pkill -f 'kaboom-agentic-browser|kaboom-agentic-devtools|gasoline-agentic-browser|gasoline-agentic-devtools|gasoline|strum' 2>/dev/null || true sleep 0.5 pkill -9 -f 'kaboom-agentic-browser|kaboom-agentic-devtools|gasoline-agentic-browser|gasoline-agentic-devtools|gasoline|strum' 2>/dev/null || true return 0 fi if [ -n "$pids" ]; then echo -e " Stopping running KaBOOM!/legacy processes..." for pid in $pids; do # Don't kill ourselves. if [ "$pid" != "$$" ]; then kill "$pid" 2>/dev/null && killed=$((killed + 1)) || true fi done if [ $killed -gt 0 ]; then sleep 1 # Force-kill any survivors. for pid in $pids; do if [ "$pid" != "$$" ] && kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null || true fi done fi fi } # ───────────────────────────────────────────────────────────── # 1. Platform Detection # ───────────────────────────────────────────────────────────── OS="$(uname -s | tr '[:upper:]' '[:lower:]')" ARCH="$(uname -m)" case "$OS" in darwin) PLATFORM="darwin" ;; # macOS linux) PLATFORM="linux" ;; # Linux mingw*|cygwin*) PLATFORM="win32" ;; # Windows (Git Bash/Cygwin) *) echo -e "${RED}Unsupported OS: $OS${NC}"; exit 1 ;; esac # Normalize architecture strings to match release asset naming conventions (x64 vs arm64). case "$ARCH" in x86_64|amd64) E_ARCH="x64" ;; arm64|aarch64) E_ARCH="arm64" ;; *) echo -e "${RED}Unsupported architecture: $ARCH${NC}"; exit 1 ;; esac # Windows-specific binary suffix and architecture enforcement. if [ "$PLATFORM" == "win32" ]; then E_ARCH="x64" BINARY_EXT=".exe" else BINARY_EXT="" fi # ───────────────────────────────────────────────────────────── # 2. Version Check # ───────────────────────────────────────────────────────────── echo -e "Checking for updates..." VERSION=$(curl -sSL --fail --max-time 15 "$VERSION_URL" | tr -d '[:space:]' || true) if [ -z "$VERSION" ]; then echo -e "${RED}Failed to fetch latest version info from $VERSION_URL${NC}" echo -e "Check your network connection and try again." exit 1 fi # ───────────────────────────────────────────────────────────── # 3. Detect install vs upgrade # ───────────────────────────────────────────────────────────── CANONICAL_KABOOM_BIN="$BIN_DIR/kaboom-agentic-browser$BINARY_EXT" KABOOM_HOOKS_BIN="$BIN_DIR/kaboom-hooks$BINARY_EXT" IS_UPGRADE=0 PREVIOUS_VERSION="" if [ "$HOOKS_ONLY" = "1" ]; then if [ -x "$KABOOM_HOOKS_BIN" ]; then PREVIOUS_VERSION=$("$KABOOM_HOOKS_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) IS_UPGRADE=1 fi else if [ -x "$CANONICAL_KABOOM_BIN" ]; then PREVIOUS_VERSION=$("$CANONICAL_KABOOM_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) IS_UPGRADE=1 fi fi if [ "$IS_UPGRADE" = "1" ] && [ -n "$PREVIOUS_VERSION" ]; then echo -e "Upgrading: v$PREVIOUS_VERSION -> v$VERSION ($PLATFORM-$E_ARCH)" else echo -e "Installing: v$VERSION ($PLATFORM-$E_ARCH)" fi # ───────────────────────────────────────────────────────────── # 4. Directory Setup # ───────────────────────────────────────────────────────────── mkdir -p "$BIN_DIR" mkdir -p "$INSTALL_DIR" # Verify the directory is writable (catches permission issues early). if ! touch "$INSTALL_DIR/.write-test" 2>/dev/null; then echo -e "${RED}Cannot write to $INSTALL_DIR — check directory permissions.${NC}" echo -e "If this was installed with sudo previously, run: sudo chown -R \$USER $INSTALL_DIR" exit 1 fi rm -f "$INSTALL_DIR/.write-test" echo -e "Install root: $INSTALL_DIR" # ───────────────────────────────────────────────────────────── # 5. Stop stale processes before binary replacement # ───────────────────────────────────────────────────────────── # Hooks-only installs don't run a daemon — no processes to stop. if [ "$HOOKS_ONLY" != "1" ]; then kill_stale_kaboom_processes fi purge_legacy_install_artifacts # ───────────────────────────────────────────────────────────── # 6. Binary Installation # ───────────────────────────────────────────────────────────── CHECKSUM_URL="https://github.com/$REPO/releases/download/v$VERSION/checksums.txt" # download_and_verify fetches a binary, validates size, verifies checksum, and installs it. # Usage: download_and_verify