#!/bin/bash set -euo pipefail # ============================================================================ # Mosaic Stack Installer # ============================================================================ # Usage: curl -fsSL https://get.mosaicstack.dev | bash # # A comprehensive installer that "just works" across platforms. # Automatically detects the OS, installs dependencies, and configures # the system for running Mosaic Stack. # Script version INSTALLER_VERSION="1.0.0" # Get script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Source library files # shellcheck source=lib/platform.sh source "$SCRIPT_DIR/lib/platform.sh" # shellcheck source=lib/dependencies.sh source "$SCRIPT_DIR/lib/dependencies.sh" # shellcheck source=lib/docker.sh source "$SCRIPT_DIR/lib/docker.sh" # shellcheck source=lib/validation.sh source "$SCRIPT_DIR/lib/validation.sh" # Set up cleanup trap setup_cleanup_trap # ============================================================================ # Configuration # ============================================================================ # Default values NON_INTERACTIVE=false DRY_RUN=false VERBOSE=false MODE="" ENABLE_SSO=false USE_BUNDLED_AUTHENTIK=false EXTERNAL_AUTHENTIK_URL="" OLLAMA_MODE="disabled" OLLAMA_URL="" MOSAIC_BASE_URL="" COMPOSE_PROFILES="full" SKIP_DEPS=false NO_PORT_CHECK=false # Ports (defaults, can be overridden) WEB_PORT="${WEB_PORT:-3000}" API_PORT="${API_PORT:-3001}" POSTGRES_PORT="${POSTGRES_PORT:-5432}" VALKEY_PORT="${VALKEY_PORT:-6379}" # ============================================================================ # Taglines # ============================================================================ TAGLINES=( "Claws out, configs in — let's ship a calm, clean stack." "Less yak-shaving, more uptime." "Turnkey today, productive tonight." "Ports resolved. Secrets sealed. Stack ready." "All signal, no ceremony." "Your .env is safe with me." "One curl away from your personal AI assistant." "Infrastructure that stays out of your way." "From zero to AI assistant in under 5 minutes." "Because you have better things to do than configure Docker." ) pick_tagline() { local count=${#TAGLINES[@]} local idx=$((RANDOM % count)) echo "${TAGLINES[$idx]}" } TAGLINE=$(pick_tagline) # ============================================================================ # Help and Usage # ============================================================================ print_usage() { cat << EOF Mosaic Stack Installer v${INSTALLER_VERSION} USAGE: curl -fsSL https://get.mosaicstack.dev | bash ./install.sh [OPTIONS] OPTIONS: -h, --help Show this help message --non-interactive Run without prompts (requires --mode) --dry-run Preview changes without executing --verbose Enable debug output --mode MODE Deployment mode: docker or native --enable-sso Enable Authentik SSO (Docker only) --bundled-authentik Use bundled Authentik server --external-authentik URL Use external Authentik server --ollama-mode MODE Ollama: local, remote, disabled --ollama-url URL Remote Ollama server URL --base-url URL Mosaic base URL --profiles PROFILES Docker Compose profiles (default: full) --skip-deps Skip dependency installation --no-port-check Skip port conflict detection ENVIRONMENT VARIABLES: All options can be set via environment variables: MOSAIC_MODE, MOSAIC_ENABLE_SSO, MOSAIC_OLLAMA_MODE, etc. EXAMPLES: # Interactive installation (recommended) curl -fsSL https://get.mosaicstack.dev | bash # Non-interactive Docker deployment curl -fsSL https://get.mosaicstack.dev | bash -s -- --non-interactive --mode docker # With SSO and local Ollama curl -fsSL https://get.mosaicstack.dev | bash -s -- \\ --mode docker \\ --enable-sso --bundled-authentik \\ --ollama-mode local # Preview installation curl -fsSL https://get.mosaicstack.dev | bash -s -- --dry-run DOCUMENTATION: https://docs.mosaicstack.dev EOF } # ============================================================================ # Argument Parsing # ============================================================================ parse_arguments() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) print_usage exit 0 ;; --non-interactive) NON_INTERACTIVE=true shift ;; --dry-run) DRY_RUN=true shift ;; --verbose) VERBOSE=true shift ;; --mode) if [[ -z "${2:-}" || "$2" == --* ]]; then echo -e "${ERROR}Error: --mode requires a value (docker or native)${NC}" exit 1 fi MODE="$2" shift 2 ;; --enable-sso) ENABLE_SSO=true shift ;; --bundled-authentik) USE_BUNDLED_AUTHENTIK=true shift ;; --external-authentik) if [[ -z "${2:-}" || "$2" == --* ]]; then echo -e "${ERROR}Error: --external-authentik requires a URL${NC}" exit 1 fi EXTERNAL_AUTHENTIK_URL="$2" shift 2 ;; --ollama-mode) if [[ -z "${2:-}" || "$2" == --* ]]; then echo -e "${ERROR}Error: --ollama-mode requires a value (local, remote, disabled)${NC}" exit 1 fi OLLAMA_MODE="$2" shift 2 ;; --ollama-url) if [[ -z "${2:-}" || "$2" == --* ]]; then echo -e "${ERROR}Error: --ollama-url requires a URL${NC}" exit 1 fi OLLAMA_URL="$2" shift 2 ;; --base-url) if [[ -z "${2:-}" || "$2" == --* ]]; then echo -e "${ERROR}Error: --base-url requires a URL${NC}" exit 1 fi MOSAIC_BASE_URL="$2" shift 2 ;; --profiles) if [[ -z "${2:-}" || "$2" == --* ]]; then echo -e "${ERROR}Error: --profiles requires a value${NC}" exit 1 fi COMPOSE_PROFILES="$2" shift 2 ;; --skip-deps) SKIP_DEPS=true shift ;; --no-port-check) NO_PORT_CHECK=true shift ;; *) echo -e "${ERROR}Error: Unknown option: $1${NC}" echo "Use --help for usage information" exit 1 ;; esac done # Validate non-interactive mode if [[ "$NON_INTERACTIVE" == true ]]; then if [[ -z "$MODE" ]]; then echo -e "${ERROR}Error: Non-interactive mode requires --mode${NC}" exit 1 fi if [[ "$MODE" != "native" && "$MODE" != "docker" ]]; then echo -e "${ERROR}Error: Invalid mode: $MODE (must be 'docker' or 'native')${NC}" exit 1 fi if [[ "$OLLAMA_MODE" == "remote" && -z "$OLLAMA_URL" ]]; then echo -e "${ERROR}Error: Remote Ollama mode requires --ollama-url${NC}" exit 1 fi if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" != true && -z "$EXTERNAL_AUTHENTIK_URL" ]]; then echo -e "${ERROR}Error: SSO enabled but no Authentik configuration provided${NC}" echo "Use --bundled-authentik or --external-authentik URL" exit 1 fi fi } # ============================================================================ # Banner # ============================================================================ show_banner() { echo "" echo -e "${ACCENT}${BOLD}" cat << "EOF" __ __ _ ____ _ _ | \/ | ___ ___ __ _(_) ___ / ___| |_ __ _ ___| | __ | |\/| |/ _ \/ __|/ _` | |/ __|\___ | __/ _` |/ __| |/ / | | | | (_) \__ \ (_| | | (__ ___/ | || (_| | (__| < |_| |_|\___/|___/\__,_|_|\___|____/ \__\__,_|\___|_|\_\ EOF echo -e "${NC}${MUTED} ${TAGLINE}${NC}" echo "" } # ============================================================================ # Mode Selection # ============================================================================ select_mode() { if [[ -n "$MODE" ]]; then return fi if [[ "$NON_INTERACTIVE" == true ]]; then MODE="docker" return fi echo -e "${BOLD}How would you like to run Mosaic Stack?${NC}" echo "" echo " 1) Docker (Recommended)" echo " - Best for production deployment" echo " - Isolated environment with all dependencies" echo " - Includes PostgreSQL, Valkey, all services" echo "" echo " 2) Native" echo " - Best for development" echo " - Runs directly on your system" echo " - Requires manual dependency installation" echo "" local selection read -r -p "Select deployment mode [1-2]: " selection case "$selection" in 1) MODE="docker" ;; 2) MODE="native" ;; *) echo -e "${INFO}i${NC} Defaulting to Docker mode" MODE="docker" ;; esac echo "" } # ============================================================================ # Configuration Collection # ============================================================================ collect_configuration() { echo -e "${BOLD}Configuration${NC}" echo "" # Check for existing .env if [[ -f "$PROJECT_ROOT/.env" ]]; then echo -e "${SUCCESS}✓${NC} Found existing .env file" if [[ "$NON_INTERACTIVE" != true ]]; then read -r -p "Use existing configuration? [Y/n]: " use_existing case "$use_existing" in n|N) echo -e "${INFO}i${NC} Will reconfigure..." ;; *) echo -e "${INFO}i${NC} Using existing configuration" return ;; esac fi fi # Base URL if [[ -z "$MOSAIC_BASE_URL" ]]; then if [[ "$NON_INTERACTIVE" == true ]]; then MOSAIC_BASE_URL="http://localhost:${WEB_PORT}" else echo -e "${INFO}i${NC} Base URL configuration" echo " - Localhost: http://localhost:${WEB_PORT}" echo " - Custom: Enter your domain URL" read -r -p "Base URL [http://localhost:${WEB_PORT}]: " MOSAIC_BASE_URL MOSAIC_BASE_URL="${MOSAIC_BASE_URL:-http://localhost:${WEB_PORT}}" fi fi echo -e "${SUCCESS}✓${NC} Base URL: ${INFO}$MOSAIC_BASE_URL${NC}" # SSO Configuration (Docker mode only) if [[ "$MODE" == "docker" && "$ENABLE_SSO" != true ]]; then if [[ "$NON_INTERACTIVE" != true ]]; then echo "" read -r -p "Enable Authentik SSO? [y/N]: " enable_sso case "$enable_sso" in y|Y) ENABLE_SSO=true read -r -p "Use bundled Authentik? [Y/n]: " bundled case "$bundled" in n|N) read -r -p "External Authentik URL: " EXTERNAL_AUTHENTIK_URL ;; *) USE_BUNDLED_AUTHENTIK=true ;; esac ;; esac fi fi # Ollama Configuration if [[ "$OLLAMA_MODE" == "disabled" ]]; then if [[ "$NON_INTERACTIVE" != true ]]; then echo "" echo -e "${INFO}i${NC} Ollama Configuration" echo " 1) Local (bundled Ollama service)" echo " 2) Remote (connect to existing Ollama)" echo " 3) Disabled" read -r -p "Ollama mode [1-3]: " ollama_choice case "$ollama_choice" in 1) OLLAMA_MODE="local" ;; 2) OLLAMA_MODE="remote" read -r -p "Ollama URL: " OLLAMA_URL ;; *) OLLAMA_MODE="disabled" ;; esac fi fi echo "" } # ============================================================================ # Environment File Generation # ============================================================================ generate_secrets() { echo -e "${WARN}→${NC} Generating secrets..." # Generate all required secrets POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 32) JWT_SECRET=$(openssl rand -base64 32) BETTER_AUTH_SECRET=$(openssl rand -base64 32) ENCRYPTION_KEY=$(openssl rand -hex 32) AUTHENTIK_SECRET_KEY=$(openssl rand -base64 50) AUTHENTIK_BOOTSTRAP_PASSWORD=$(openssl rand -base64 16 | tr -d '/+=' | head -c 16) COORDINATOR_API_KEY=$(openssl rand -base64 32) ORCHESTRATOR_API_KEY=$(openssl rand -base64 32) GITEA_WEBHOOK_SECRET=$(openssl rand -hex 32) echo -e "${SUCCESS}✓${NC} Secrets generated" } generate_env_file() { local env_file="$PROJECT_ROOT/.env" echo -e "${WARN}→${NC} Generating .env file..." # Parse base URL local scheme="http" local host="localhost" local port="$WEB_PORT" if [[ "$MOSAIC_BASE_URL" =~ ^(https?)://([^/:]+)(:([0-9]+))? ]]; then scheme="${BASH_REMATCH[1]}" host="${BASH_REMATCH[2]}" port="${BASH_REMATCH[4]:-$WEB_PORT}" fi # Determine profiles local profiles="$COMPOSE_PROFILES" # Start with example file if it exists if [[ -f "$PROJECT_ROOT/.env.example" ]]; then cp "$PROJECT_ROOT/.env.example" "$env_file" fi # Write configuration cat >> "$env_file" << EOF # ============================================== # Generated by Mosaic Stack Installer v${INSTALLER_VERSION} # Generated at: $(date -u +"%Y-%m-%dT%H:%M:%SZ") # ============================================== # Application Ports WEB_PORT=$port API_PORT=$API_PORT POSTGRES_PORT=$POSTGRES_PORT VALKEY_PORT=$VALKEY_PORT # Web Configuration NEXT_PUBLIC_APP_URL=$MOSAIC_BASE_URL NEXT_PUBLIC_API_URL=${scheme}://${host}:${API_PORT} # Database DATABASE_URL=postgresql://mosaic:${POSTGRES_PASSWORD}@postgres:5432/mosaic POSTGRES_PASSWORD=$POSTGRES_PASSWORD # Authentication JWT_SECRET=$JWT_SECRET BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET # Encryption ENCRYPTION_KEY=$ENCRYPTION_KEY # Compose Profiles COMPOSE_PROFILES=$profiles EOF # Add SSO configuration if enabled if [[ "$ENABLE_SSO" == true ]]; then cat >> "$env_file" << EOF # Authentik SSO OIDC_ENABLED=true AUTHENTIK_SECRET_KEY=$AUTHENTIK_SECRET_KEY AUTHENTIK_BOOTSTRAP_PASSWORD=$AUTHENTIK_BOOTSTRAP_PASSWORD EOF if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then echo "AUTHENTIK_PUBLIC_URL=http://localhost:\${AUTHENTIK_PORT_HTTP:-9000}" >> "$env_file" else echo "AUTHENTIK_PUBLIC_URL=$EXTERNAL_AUTHENTIK_URL" >> "$env_file" fi fi # Add Ollama configuration if enabled if [[ "$OLLAMA_MODE" != "disabled" ]]; then cat >> "$env_file" << EOF # Ollama OLLAMA_MODE=$OLLAMA_MODE EOF if [[ "$OLLAMA_MODE" == "local" ]]; then echo "OLLAMA_ENDPOINT=http://ollama:11434" >> "$env_file" else echo "OLLAMA_ENDPOINT=$OLLAMA_URL" >> "$env_file" fi fi # Add API keys cat >> "$env_file" << EOF # API Keys COORDINATOR_API_KEY=$COORDINATOR_API_KEY ORCHESTRATOR_API_KEY=$ORCHESTRATOR_API_KEY GITEA_WEBHOOK_SECRET=$GITEA_WEBHOOK_SECRET EOF # Set restrictive permissions chmod 600 "$env_file" echo -e "${SUCCESS}✓${NC} .env file generated at ${INFO}$env_file${NC}" } # ============================================================================ # Port Conflict Resolution # ============================================================================ check_port_conflicts() { if [[ "$NO_PORT_CHECK" == true ]]; then return fi echo -e "${BOLD}Checking for port conflicts...${NC}" echo "" local conflicts=() local ports_to_check=("WEB_PORT:$WEB_PORT" "API_PORT:$API_PORT" "POSTGRES_PORT:$POSTGRES_PORT" "VALKEY_PORT:$VALKEY_PORT") for entry in "${ports_to_check[@]}"; do local name="${entry%%:*}" local port="${entry#*:}" if check_port_in_use "$port"; then conflicts+=("$name:$port") fi done if [[ ${#conflicts[@]} -eq 0 ]]; then echo -e "${SUCCESS}✓${NC} No port conflicts detected" return fi echo -e "${WARN}⚠${NC} Port conflicts detected:" for conflict in "${conflicts[@]}"; do local name="${conflict%%:*}" local port="${conflict#*:}" local process process=$(get_process_on_port "$port") echo " - $name: Port $port is in use (PID: $process)" done if [[ "$NON_INTERACTIVE" == true ]]; then echo -e "${INFO}i${NC} Non-interactive mode: Please free the ports and try again" exit 1 fi echo "" read -r -p "Continue anyway? [y/N]: " continue_anyway case "$continue_anyway" in y|Y) echo -e "${WARN}⚠${NC} Continuing with port conflicts - services may fail to start" ;; *) echo -e "${ERROR}Error: Port conflicts must be resolved${NC}" exit 1 ;; esac } # ============================================================================ # Installation Steps # ============================================================================ install_docker_mode() { echo -e "${BOLD}Installing Mosaic Stack (Docker mode)${NC}" echo "" # Check and install dependencies if [[ "$SKIP_DEPS" != true ]]; then if ! check_docker_dependencies; then echo "" if [[ "$NON_INTERACTIVE" == true ]] || \ confirm "Install missing dependencies?" "y"; then install_dependencies "docker" else echo -e "${ERROR}Error: Cannot proceed without dependencies${NC}" exit 1 fi fi fi # Ensure Docker is running start_docker # Check port conflicts check_port_conflicts # Generate secrets and .env generate_secrets generate_env_file # Pull images if [[ "$DRY_RUN" != true ]]; then echo "" docker_pull_images "$PROJECT_ROOT/docker-compose.yml" "$PROJECT_ROOT/.env" fi # Start services if [[ "$DRY_RUN" != true ]]; then echo "" docker_compose_up "$PROJECT_ROOT/docker-compose.yml" "$PROJECT_ROOT/.env" "$COMPOSE_PROFILES" # Wait for services to be healthy echo "" echo -e "${INFO}ℹ${NC} Waiting for services to start..." sleep 10 # Run health checks wait_for_healthy_container "mosaic-postgres" 60 || true wait_for_healthy_container "mosaic-valkey" 30 || true fi } install_native_mode() { echo -e "${BOLD}Installing Mosaic Stack (Native mode)${NC}" echo "" # Check and install dependencies if [[ "$SKIP_DEPS" != true ]]; then if ! check_native_dependencies; then echo "" if [[ "$NON_INTERACTIVE" == true ]] || \ confirm "Install missing dependencies?" "y"; then install_dependencies "native" else echo -e "${ERROR}Error: Cannot proceed without dependencies${NC}" exit 1 fi fi fi # Generate secrets and .env generate_secrets generate_env_file # Install npm dependencies if [[ "$DRY_RUN" != true ]]; then echo "" echo -e "${WARN}→${NC} Installing npm dependencies..." pnpm install # Run database migrations echo "" echo -e "${WARN}→${NC} Running database setup..." echo -e "${INFO}ℹ${NC} Make sure PostgreSQL is running and accessible" fi } # ============================================================================ # Post-Install # ============================================================================ run_post_install_checks() { echo "" echo -e "${BOLD}Post-Installation Checks${NC}" echo "" if [[ "$DRY_RUN" == true ]]; then echo -e "${INFO}ℹ${NC} Dry run - skipping checks" return fi # Run doctor run_doctor "$PROJECT_ROOT/.env" "$PROJECT_ROOT/docker-compose.yml" "$MODE" local doctor_result=$? if [[ $doctor_result -eq $CHECK_FAIL ]]; then echo "" echo -e "${WARN}⚠${NC} Some checks failed. Review the output above." fi } show_success_message() { local web_url="$MOSAIC_BASE_URL" local api_url="${MOSAIC_BASE_URL/http:\/\//http:\/\/}:${API_PORT}" # If using Traefik, adjust URLs if [[ "$COMPOSE_PROFILES" == *"traefik"* ]]; then api_url="${web_url/api./}" fi echo "" echo -e "${BOLD}${SUCCESS}════════════════════════════════════════════════════════════${NC}" echo -e "${BOLD}${SUCCESS} Mosaic Stack is ready!${NC}" echo -e "${BOLD}${SUCCESS}════════════════════════════════════════════════════════════${NC}" echo "" echo -e " ${INFO}Web UI:${NC} $web_url" echo -e " ${INFO}API:${NC} $api_url" echo -e " ${INFO}Database:${NC} localhost:$POSTGRES_PORT" echo "" echo -e " ${BOLD}Next steps:${NC}" echo " 1. Open $web_url in your browser" echo " 2. Create your first workspace" echo " 3. Configure AI providers in Settings" echo "" echo -e " ${BOLD}Useful commands:${NC}" if [[ "$MODE" == "docker" ]]; then echo " To stop: docker compose down" echo " To restart: docker compose restart" echo " To view logs: docker compose logs -f" else echo " Start API: pnpm --filter api dev" echo " Start Web: pnpm --filter web dev" fi echo "" echo -e " ${INFO}Documentation:${NC} https://docs.mosaicstack.dev" echo -e " ${INFO}Support:${NC} https://github.com/mosaicstack/stack/issues" echo "" } # ============================================================================ # Dry Run # ============================================================================ show_dry_run_summary() { echo "" echo -e "${BOLD}${INFO}════════════════════════════════════════════════════════════${NC}" echo -e "${BOLD}${INFO} Dry Run Summary${NC}" echo -e "${BOLD}${INFO}════════════════════════════════════════════════════════════${NC}" echo "" echo -e " ${INFO}Mode:${NC} $MODE" echo -e " ${INFO}Base URL:${NC} $MOSAIC_BASE_URL" echo -e " ${INFO}Profiles:${NC} $COMPOSE_PROFILES" echo "" echo -e " ${INFO}SSO:${NC} $([ "$ENABLE_SSO" == true ] && echo "Enabled" || echo "Disabled")" echo -e " ${INFO}Ollama:${NC} $OLLAMA_MODE" echo "" echo -e " ${MUTED}This was a dry run. No changes were made.${NC}" echo -e " ${MUTED}Run without --dry-run to perform installation.${NC}" echo "" } # ============================================================================ # Main # ============================================================================ main() { # Configure verbose mode if [[ "$VERBOSE" == true ]]; then set -x fi # Show banner show_banner # Detect platform print_platform_summary echo "" # Select deployment mode select_mode echo -e "${SUCCESS}✓${NC} Selected: ${INFO}$MODE${NC} mode" echo "" # Dry run check if [[ "$DRY_RUN" == true ]]; then collect_configuration show_dry_run_summary exit 0 fi # Collect configuration collect_configuration # Install based on mode case "$MODE" in docker) install_docker_mode ;; native) install_native_mode ;; esac # Post-installation checks run_post_install_checks # Show success message show_success_message } # Confirm helper confirm() { local prompt="$1" local default="${2:-n}" local response if [[ "$default" == "y" ]]; then prompt="$prompt [Y/n]: " else prompt="$prompt [y/N]: " fi read -r -p "$prompt" response response=${response:-$default} case "$response" in [Yy]|[Yy][Ee][Ss]) return 0 ;; *) return 1 ;; esac } # Run if not being sourced if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then parse_arguments "$@" main fi