#!/bin/bash # Mosaic Stack Setup Wizard # Interactive installer for Mosaic Stack personal assistant platform # Supports Docker and native deployments set -e # ============================================================================ # Configuration # ============================================================================ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Set up logging LOG_DIR="$PROJECT_ROOT/logs" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/setup-$(date +%Y%m%d_%H%M%S).log" # Redirect stdout/stderr to both console and log file exec > >(tee -a "$LOG_FILE") 2>&1 # Send trace output (set -x) ONLY to log file on fd 3 exec 3>>"$LOG_FILE" export BASH_XTRACEFD=3 # Enable verbose command tracing (only goes to log file now) set -x echo "===================================================================" echo "Mosaic Stack Setup Wizard" echo "Started: $(date)" echo "Full log: $LOG_FILE" echo "===================================================================" echo "" # Source common functions # shellcheck source=lib/common.sh source "$SCRIPT_DIR/lib/common.sh" # ============================================================================ # Global Variables # ============================================================================ NON_INTERACTIVE=false DRY_RUN=false MODE="" ENABLE_SSO=false USE_BUNDLED_AUTHENTIK=false EXTERNAL_AUTHENTIK_URL="" OLLAMA_MODE="disabled" OLLAMA_URL="" ENABLE_MOLTBOT=false MOSAIC_BASE_URL="" API_BASE_URL="" AUTHENTIK_PUBLIC_URL="" BASE_URL_MODE="" BASE_URL_SCHEME="http" BASE_URL_HOST="" BASE_URL_PORT="" TRAEFIK_MODE="none" TRAEFIK_ENABLE=false TRAEFIK_TLS_ENABLED=true TRAEFIK_ENTRYPOINT="websecure" TRAEFIK_NETWORK="" TRAEFIK_DOCKER_NETWORK="mosaic-public" TRAEFIK_ACME_EMAIL="" TRAEFIK_CERTRESOLVER="" TRAEFIK_DASHBOARD_ENABLED=true TRAEFIK_DASHBOARD_PORT="" MOSAIC_WEB_DOMAIN="" MOSAIC_API_DOMAIN="" MOSAIC_AUTH_DOMAIN="" WEB_PORT="" API_PORT="" POSTGRES_PORT="" VALKEY_PORT="" AUTHENTIK_PORT_HTTP="" AUTHENTIK_PORT_HTTPS="" OLLAMA_PORT="" TRAEFIK_HTTP_PORT="" TRAEFIK_HTTPS_PORT="" DETECTED_OS="" DETECTED_PKG_MANAGER="" PORT_OVERRIDES=() # ============================================================================ # Taglines (OpenClaw-inspired flavor) # ============================================================================ 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." ) pick_tagline() { local idx=$((RANDOM % ${#TAGLINES[@]})) echo "${TAGLINES[$idx]}" } # ============================================================================ # Help and Usage # ============================================================================ show_help() { cat << EOF Mosaic Stack Setup Wizard USAGE: $0 [OPTIONS] OPTIONS: -h, --help Show this help message --non-interactive Run in non-interactive mode (requires all options) --dry-run Show what would happen without executing --mode MODE Deployment mode: docker or native --enable-sso Enable Authentik SSO (Docker mode only) --bundled-authentik Use bundled Authentik server (with --enable-sso) --external-authentik URL Use external Authentik server URL (with --enable-sso) --ollama-mode MODE Ollama mode: local, remote, disabled (default: disabled) --ollama-url URL Ollama server URL (if --ollama-mode remote) --enable-moltbot Enable MoltBot integration --base-url URL Mosaic base URL (e.g., https://mosaic.example.com) EXAMPLES: # Interactive mode (recommended for first-time setup) $0 # Non-interactive Docker deployment with bundled SSO $0 --non-interactive --mode docker --enable-sso --bundled-authentik # Non-interactive Docker with external Authentik and local Ollama $0 --non-interactive --mode docker --enable-sso --external-authentik "https://auth.example.com" --ollama-mode local # Dry run to see what would happen $0 --dry-run --mode docker --enable-sso --bundled-authentik EOF } # ============================================================================ # Argument Parsing # ============================================================================ parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in -h|--help) show_help exit 0 ;; --non-interactive) NON_INTERACTIVE=true ;; --dry-run) DRY_RUN=true ;; --mode) if [[ -z "${2:-}" || "$2" == --* ]]; then print_error "--mode requires a value (docker or native)" exit 1 fi MODE="$2" shift ;; --enable-sso) ENABLE_SSO=true ;; --bundled-authentik) USE_BUNDLED_AUTHENTIK=true ;; --external-authentik) if [[ -z "${2:-}" || "$2" == --* ]]; then print_error "--external-authentik requires a URL" exit 1 fi EXTERNAL_AUTHENTIK_URL="$2" shift ;; --ollama-mode) if [[ -z "${2:-}" || "$2" == --* ]]; then print_error "--ollama-mode requires a value (local, remote, or disabled)" exit 1 fi OLLAMA_MODE="$2" shift ;; --ollama-url) if [[ -z "${2:-}" || "$2" == --* ]]; then print_error "--ollama-url requires a URL" exit 1 fi OLLAMA_URL="$2" shift ;; --enable-moltbot) ENABLE_MOLTBOT=true ;; --base-url) if [[ -z "${2:-}" || "$2" == --* ]]; then print_error "--base-url requires a URL" exit 1 fi MOSAIC_BASE_URL="$2" shift ;; *) print_error "Unknown option: $1" echo "Use --help for usage information" exit 1 ;; esac shift done # Validate non-interactive mode if [[ "$NON_INTERACTIVE" == true ]]; then if [[ -z "$MODE" ]]; then print_error "Non-interactive mode requires --mode" exit 1 fi if [[ "$MODE" != "native" && "$MODE" != "docker" ]]; then print_error "Invalid mode: $MODE (must be 'native' or 'docker')" exit 1 fi if [[ "$OLLAMA_MODE" == "remote" && -z "$OLLAMA_URL" ]]; then print_error "Remote Ollama mode requires --ollama-url" exit 1 fi if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" != true && -z "$EXTERNAL_AUTHENTIK_URL" ]]; then print_error "SSO enabled in non-interactive mode requires --bundled-authentik or --external-authentik URL" exit 1 fi fi } # ============================================================================ # Welcome Banner # ============================================================================ show_banner() { if [[ "$NON_INTERACTIVE" == true ]]; then return fi cat << "EOF" __ __ _ ____ _ _ | \/ | ___ ___ __ _(_) ___ / ___| |_ __ _ ___| | __ | |\/| |/ _ \/ __|/ _` | |/ __|\___ | __/ _` |/ __| |/ / | | | | (_) \__ \ (_| | | (__ ___/ | || (_| | (__| < |_| |_|\___/|___/\__,_|_|\___|____/ \__\__,_|\___|_|\_\ Multi-Tenant Personal Assistant Platform EOF local tagline tagline=$(pick_tagline) echo " $tagline" echo "" echo "Welcome to the Mosaic Stack Setup Wizard!" echo "" echo "This wizard will guide you through setting up Mosaic Stack on your system." echo "You can choose between Docker (production) or native (development) deployment," echo "and configure optional features like Authentik SSO and Ollama LLM integration." echo "" } # ============================================================================ # Platform Detection # ============================================================================ detect_platform() { print_header "Detecting Platform" DETECTED_OS=$(detect_os) DETECTED_PKG_MANAGER=$(detect_package_manager "$DETECTED_OS") local os_name os_name=$(get_os_name "$DETECTED_OS") if [[ "$DETECTED_OS" == "unknown" ]]; then print_warning "Could not detect operating system" print_info "Detected OS type: $OSTYPE" echo "" if [[ "$NON_INTERACTIVE" == true ]]; then print_error "Cannot proceed in non-interactive mode on unknown OS" exit 1 fi if ! confirm "Continue anyway?"; then exit 1 fi else print_success "Detected: $os_name ($DETECTED_PKG_MANAGER)" fi } # ============================================================================ # Mode Selection # ============================================================================ select_deployment_mode() { if [[ -n "$MODE" ]]; then print_header "Deployment Mode" print_info "Using mode: $MODE" return fi print_header "Deployment Mode" echo "" echo "How would you like to run Mosaic Stack?" echo "" echo " 1) Docker (Recommended)" echo " - Best for production deployment" echo " - Isolated environment with all dependencies" echo " - Includes PostgreSQL, Valkey, all services" echo " - Optional Authentik SSO integration" echo " - Turnkey deployment" echo "" echo " 2) Native" echo " - Best for development" echo " - Runs directly on your system" echo " - Requires manual dependency installation" echo " - Easier to debug and modify" echo "" local selection selection=$(select_option "Select deployment mode:" \ "Docker (production, recommended)" \ "Native (development)") if [[ "$selection" == *"Docker"* ]]; then MODE="docker" else MODE="native" fi print_success "Selected: $MODE mode" } # ============================================================================ # Dependency Checking # ============================================================================ check_and_install_dependencies() { print_header "Checking Dependencies" local missing_deps=() local docker_status=0 if [[ "$MODE" == "docker" ]]; then check_docker docker_status=$? if [[ $docker_status -eq 1 ]]; then # Docker not installed missing_deps+=("Docker") elif [[ $docker_status -ne 0 ]]; then # Docker installed but not accessible (permission/daemon issue) echo "" print_error "Docker is installed but not accessible" print_info "Please fix the issue above and run the setup script again" exit 1 else print_success "Docker: OK" fi if ! check_docker_compose; then missing_deps+=("Docker Compose") else print_success "Docker Compose: OK" fi else if ! check_node 18; then missing_deps+=("Node.js 18+") else print_success "Node.js: OK" fi if ! check_pnpm; then missing_deps+=("pnpm") else print_success "pnpm: OK" fi if ! check_postgres; then missing_deps+=("PostgreSQL") else print_success "PostgreSQL: OK" fi fi if [[ ${#missing_deps[@]} -eq 0 ]]; then echo "" print_success "All dependencies satisfied" return 0 fi echo "" print_warning "Missing dependencies:" for dep in "${missing_deps[@]}"; do echo " - $dep" done echo "" if [[ "$NON_INTERACTIVE" == true ]]; then print_error "Dependency check failed in non-interactive mode" exit 1 fi if confirm "Would you like to install missing dependencies?"; then install_missing_dependencies else print_error "Cannot proceed without required dependencies" exit 1 fi } install_missing_dependencies() { print_header "Installing Dependencies" check_sudo if [[ "$MODE" == "docker" ]]; then # Install Docker if ! check_docker; then local docker_pkg docker_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "docker") if [[ -n "$docker_pkg" ]]; then install_package "$DETECTED_PKG_MANAGER" "$docker_pkg" # Start Docker service case "$DETECTED_PKG_MANAGER" in pacman|dnf) print_step "Starting Docker service..." sudo systemctl enable --now docker ;; esac # Add user to docker group print_step "Adding user to docker group..." sudo usermod -aG docker "$USER" print_warning "You may need to log out and back in for docker group membership to take effect" print_info "Or run: newgrp docker" fi fi # Install Docker Compose if ! check_docker_compose; then local compose_pkg compose_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "docker-compose") if [[ -n "$compose_pkg" ]]; then install_package "$DETECTED_PKG_MANAGER" "$compose_pkg" fi fi else # Install Node.js if ! check_node 18; then local node_pkg node_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "node") if [[ -n "$node_pkg" ]]; then install_package "$DETECTED_PKG_MANAGER" "$node_pkg" fi fi # Install pnpm if ! check_pnpm; then print_step "Installing pnpm via npm..." npm install -g pnpm fi # Install PostgreSQL if ! check_postgres; then local postgres_pkg postgres_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "postgres") if [[ -n "$postgres_pkg" ]]; then install_package "$DETECTED_PKG_MANAGER" "$postgres_pkg" fi fi fi # Re-check dependencies echo "" local still_missing=false if [[ "$MODE" == "docker" ]]; then if ! check_docker || ! check_docker_compose; then still_missing=true fi else if ! check_node 18 || ! check_pnpm; then still_missing=true fi fi if [[ "$still_missing" == true ]]; then print_error "Some dependencies are still missing after installation" print_info "You may need to install them manually" exit 1 else print_success "All dependencies installed successfully" fi } # ============================================================================ # Configuration Collection # ============================================================================ load_existing_env() { if [[ -f "$PROJECT_ROOT/.env" ]]; then print_header "Detecting Existing Configuration" parse_env_file "$PROJECT_ROOT/.env" print_success "Found existing .env file" return 0 fi return 1 } # ============================================================================ # URL and Port Helpers # ============================================================================ strip_trailing_slash() { local value="$1" echo "${value%/}" } format_url() { local scheme="$1" local host="$2" local port="$3" local always_port="${4:-false}" if [[ -z "$host" ]]; then echo "" return fi if [[ -z "$port" ]]; then echo "${scheme}://${host}" return fi if [[ "$always_port" == true ]]; then echo "${scheme}://${host}:${port}" return fi if [[ "$scheme" == "http" && "$port" == "80" ]] || [[ "$scheme" == "https" && "$port" == "443" ]]; then echo "${scheme}://${host}" else echo "${scheme}://${host}:${port}" fi } bool_str() { if [[ "$1" == true ]]; then echo "true" else echo "false" fi } resolve_env_value() { local key="$1" local fallback="$2" local value value=$(get_env_value "$key") if [[ -n "$value" ]] && ! is_placeholder "$value"; then echo "$value" else echo "$fallback" fi } resolve_secret_value() { local key="$1" local generator="$2" local length="$3" local value value=$(get_env_value "$key") if [[ -n "$value" ]] && ! is_placeholder "$value"; then echo "$value" else "$generator" "$length" fi } derive_cookie_domain() { local domain="$1" if [[ "$domain" == *.* ]]; then echo ".${domain#*.}" else echo ".${domain}" fi } resolve_port_value() { local key="$1" local fallback="$2" local value value=$(get_env_value "$key") if validate_port "$value"; then echo "$value" else echo "$fallback" fi } initialize_ports() { WEB_PORT=$(resolve_port_value "WEB_PORT" "3000") API_PORT=$(resolve_port_value "API_PORT" "3001") POSTGRES_PORT=$(resolve_port_value "POSTGRES_PORT" "5432") VALKEY_PORT=$(resolve_port_value "VALKEY_PORT" "6379") AUTHENTIK_PORT_HTTP=$(resolve_port_value "AUTHENTIK_PORT_HTTP" "9000") AUTHENTIK_PORT_HTTPS=$(resolve_port_value "AUTHENTIK_PORT_HTTPS" "9443") OLLAMA_PORT=$(resolve_port_value "OLLAMA_PORT" "11434") TRAEFIK_HTTP_PORT=$(resolve_port_value "TRAEFIK_HTTP_PORT" "80") TRAEFIK_HTTPS_PORT=$(resolve_port_value "TRAEFIK_HTTPS_PORT" "443") TRAEFIK_DASHBOARD_PORT=$(resolve_port_value "TRAEFIK_DASHBOARD_PORT" "8080") } parse_base_url() { local url="$1" if [[ "$url" =~ ^(https?)://([^/:]+)(:([0-9]+))?(/.*)?$ ]]; then BASE_URL_SCHEME="${BASH_REMATCH[1]}" BASE_URL_HOST="${BASH_REMATCH[2]}" BASE_URL_PORT="${BASH_REMATCH[4]}" return 0 fi return 1 } recalculate_urls() { if [[ "$BASE_URL_MODE" == "traefik" ]]; then local traefik_port="" if [[ "$TRAEFIK_MODE" == "bundled" ]]; then if [[ "$BASE_URL_SCHEME" == "https" ]]; then traefik_port="$TRAEFIK_HTTPS_PORT" else traefik_port="$TRAEFIK_HTTP_PORT" fi fi MOSAIC_BASE_URL=$(format_url "$BASE_URL_SCHEME" "$MOSAIC_WEB_DOMAIN" "$traefik_port") API_BASE_URL=$(format_url "$BASE_URL_SCHEME" "$MOSAIC_API_DOMAIN" "$traefik_port") else local always_port=false if [[ "$BASE_URL_MODE" == "localhost" || "$BASE_URL_MODE" == "ip" ]]; then always_port=true fi MOSAIC_BASE_URL=$(format_url "$BASE_URL_SCHEME" "$BASE_URL_HOST" "$WEB_PORT" "$always_port") API_BASE_URL=$(format_url "$BASE_URL_SCHEME" "$BASE_URL_HOST" "$API_PORT" true) fi if [[ "$ENABLE_SSO" == true ]]; then if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then if [[ "$BASE_URL_MODE" == "traefik" && -n "$MOSAIC_AUTH_DOMAIN" ]]; then local auth_port="" if [[ "$TRAEFIK_MODE" == "bundled" ]]; then if [[ "$BASE_URL_SCHEME" == "https" ]]; then auth_port="$TRAEFIK_HTTPS_PORT" else auth_port="$TRAEFIK_HTTP_PORT" fi fi AUTHENTIK_PUBLIC_URL=$(format_url "$BASE_URL_SCHEME" "$MOSAIC_AUTH_DOMAIN" "$auth_port") else AUTHENTIK_PUBLIC_URL="http://localhost:${AUTHENTIK_PORT_HTTP}" fi else AUTHENTIK_PUBLIC_URL="$EXTERNAL_AUTHENTIK_URL" fi fi } port_label() { local key="$1" case "$key" in WEB_PORT) echo "Web UI" ;; API_PORT) echo "API" ;; POSTGRES_PORT) echo "PostgreSQL" ;; VALKEY_PORT) echo "Valkey" ;; AUTHENTIK_PORT_HTTP) echo "Authentik HTTP" ;; AUTHENTIK_PORT_HTTPS) echo "Authentik HTTPS" ;; OLLAMA_PORT) echo "Ollama" ;; TRAEFIK_HTTP_PORT) echo "Traefik HTTP" ;; TRAEFIK_HTTPS_PORT) echo "Traefik HTTPS" ;; TRAEFIK_DASHBOARD_PORT) echo "Traefik Dashboard" ;; *) echo "$key" ;; esac } suggest_port_for_key() { local key="$1" local port="$2" case "$key" in TRAEFIK_HTTP_PORT) if ! check_port_in_use 8080; then echo "8080" return 0 fi ;; TRAEFIK_HTTPS_PORT) if ! check_port_in_use 8443; then echo "8443" return 0 fi ;; esac suggest_alternative_port "$port" } apply_port_override() { local key="$1" local value="$2" case "$key" in WEB_PORT) WEB_PORT="$value" ;; API_PORT) API_PORT="$value" ;; POSTGRES_PORT) POSTGRES_PORT="$value" ;; VALKEY_PORT) VALKEY_PORT="$value" ;; AUTHENTIK_PORT_HTTP) AUTHENTIK_PORT_HTTP="$value" ;; AUTHENTIK_PORT_HTTPS) AUTHENTIK_PORT_HTTPS="$value" ;; OLLAMA_PORT) OLLAMA_PORT="$value" ;; TRAEFIK_HTTP_PORT) TRAEFIK_HTTP_PORT="$value" ;; TRAEFIK_HTTPS_PORT) TRAEFIK_HTTPS_PORT="$value" ;; TRAEFIK_DASHBOARD_PORT) TRAEFIK_DASHBOARD_PORT="$value" ;; esac set_env_value "$key" "$value" } apply_port_overrides() { if [[ ${#PORT_OVERRIDES[@]} -eq 0 ]]; then return fi for override in "${PORT_OVERRIDES[@]}"; do local key="${override%%=*}" local value="${override#*=}" apply_port_override "$key" "$value" done recalculate_urls } resolve_port_conflicts() { if [[ "$MODE" != "docker" ]]; then return fi print_header "Port Check" local port_entries=() port_entries+=("WEB_PORT=$WEB_PORT") port_entries+=("API_PORT=$API_PORT") port_entries+=("POSTGRES_PORT=$POSTGRES_PORT") port_entries+=("VALKEY_PORT=$VALKEY_PORT") if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then port_entries+=("AUTHENTIK_PORT_HTTP=$AUTHENTIK_PORT_HTTP") port_entries+=("AUTHENTIK_PORT_HTTPS=$AUTHENTIK_PORT_HTTPS") fi if [[ "$OLLAMA_MODE" == "local" ]]; then port_entries+=("OLLAMA_PORT=$OLLAMA_PORT") fi if [[ "$TRAEFIK_MODE" == "bundled" ]]; then port_entries+=("TRAEFIK_HTTP_PORT=$TRAEFIK_HTTP_PORT") port_entries+=("TRAEFIK_HTTPS_PORT=$TRAEFIK_HTTPS_PORT") if [[ "$TRAEFIK_DASHBOARD_ENABLED" == true ]]; then port_entries+=("TRAEFIK_DASHBOARD_PORT=$TRAEFIK_DASHBOARD_PORT") fi fi local conflicts=() local suggestions=() for entry in "${port_entries[@]}"; do local key="${entry%%=*}" local port="${entry#*=}" if check_port_in_use "$port"; then local suggested suggested=$(suggest_port_for_key "$key" "$port") conflicts+=("${key}=${port}") suggestions+=("${key}=${suggested}") fi done if [[ ${#conflicts[@]} -eq 0 ]]; then print_success "No port conflicts detected" return fi echo "" print_warning "Port conflicts detected:" for conflict in "${conflicts[@]}"; do local key="${conflict%%=*}" local port="${conflict#*=}" echo " - $(port_label "$key"): $port" done echo "" print_info "Suggested alternatives:" for suggestion in "${suggestions[@]}"; do local key="${suggestion%%=*}" local port="${suggestion#*=}" echo " - $(port_label "$key"): $port" done echo "" if [[ "$NON_INTERACTIVE" == true ]]; then PORT_OVERRIDES=("${suggestions[@]}") print_success "Applying alternative ports automatically (non-interactive)" apply_port_overrides return fi if confirm "Apply suggested ports automatically?" "y"; then PORT_OVERRIDES=("${suggestions[@]}") apply_port_overrides print_success "Alternative ports applied" else print_error "Port conflicts must be resolved to continue" exit 1 fi } collect_configuration() { print_header "Configuration" # Try to load existing .env local has_existing_env=false if load_existing_env; then has_existing_env=true echo "" print_info "Found existing configuration. I'll ask about each setting." echo "" fi initialize_ports # Base URL Configuration configure_base_url # SSO Configuration (Docker mode only) if [[ "$MODE" == "docker" ]]; then configure_sso fi # Ollama Configuration configure_ollama # MoltBot Configuration configure_moltbot # Generate secrets if needed configure_secrets } configure_base_url() { echo "" print_step "Base URL Configuration" echo "" local current_url current_url=$(get_env_value "MOSAIC_BASE_URL") if [[ -n "$current_url" ]] && ! is_placeholder "$current_url"; then echo "Current base URL: $current_url" if [[ "$NON_INTERACTIVE" == true ]] || ! confirm "Change base URL?" "n"; then if parse_base_url "$current_url"; then BASE_URL_MODE="custom" if [[ -n "$BASE_URL_PORT" ]]; then WEB_PORT="$BASE_URL_PORT" else if [[ "$BASE_URL_SCHEME" == "https" ]]; then WEB_PORT="443" else WEB_PORT="80" fi fi recalculate_urls return fi fi fi if [[ -n "$MOSAIC_BASE_URL" ]]; then # Already set via CLI argument print_info "Using base URL from command line: $MOSAIC_BASE_URL" if ! parse_base_url "$MOSAIC_BASE_URL"; then print_error "Invalid base URL format. Expected: http(s)://host[:port]" exit 1 fi BASE_URL_MODE="custom" if [[ -n "$BASE_URL_PORT" ]]; then WEB_PORT="$BASE_URL_PORT" else if [[ "$BASE_URL_SCHEME" == "https" ]]; then WEB_PORT="443" else WEB_PORT="80" fi fi recalculate_urls return fi if [[ "$NON_INTERACTIVE" == true ]]; then BASE_URL_MODE="localhost" BASE_URL_SCHEME="http" BASE_URL_HOST="localhost" BASE_URL_PORT="$WEB_PORT" recalculate_urls print_info "Non-interactive mode: using $MOSAIC_BASE_URL" return fi echo "How will Mosaic Stack be accessed?" echo "" local options=( "Localhost (recommended for local install)" "Local network IP" "Custom domain (direct ports)" ) if [[ "$MODE" == "docker" ]]; then options+=("Traefik reverse proxy (bundled or existing)") fi local selection selection=$(select_option "Select access method:" "${options[@]}") case "$selection" in *"Localhost"*) configure_localhost_url ;; *"Local network IP"*) configure_ip_url ;; *"Custom domain"*) configure_dns_url ;; *"Traefik"*) configure_traefik_url ;; esac } configure_dns_url() { echo "" BASE_URL_MODE="domain" local domain while true; do read -r -p "Enter your domain (e.g., mosaic.example.com): " domain if validate_domain "$domain"; then break else print_error "Invalid domain format" fi done # Ask about SSL local use_https=true if ! confirm "Use HTTPS?" "y"; then use_https=false fi if [[ "$use_https" == true ]]; then BASE_URL_SCHEME="https" print_warning "Consider using a reverse proxy for TLS certificates" else BASE_URL_SCHEME="http" fi BASE_URL_HOST="$domain" local default_port="$WEB_PORT" local port while true; do read -r -p "Enter web port [$default_port]: " port port=${port:-$default_port} if validate_port "$port"; then break else print_error "Invalid port number" fi done WEB_PORT="$port" BASE_URL_PORT="$WEB_PORT" recalculate_urls print_success "Base URL set to: $MOSAIC_BASE_URL" } configure_localhost_url() { echo "" BASE_URL_MODE="localhost" BASE_URL_SCHEME="http" BASE_URL_HOST="localhost" local default_port="$WEB_PORT" local port while true; do read -r -p "Enter web port [$default_port]: " port port=${port:-$default_port} if validate_port "$port"; then break else print_error "Invalid port number" fi done WEB_PORT="$port" BASE_URL_PORT="$WEB_PORT" recalculate_urls print_success "Base URL set to: $MOSAIC_BASE_URL" } configure_ip_url() { echo "" BASE_URL_MODE="ip" BASE_URL_SCHEME="http" # Try to detect local IP local detected_ip if command -v ip >/dev/null 2>&1; then detected_ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}') elif command -v ifconfig >/dev/null 2>&1; then detected_ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -n1) fi if [[ -n "$detected_ip" ]]; then print_info "Detected IP: $detected_ip" fi local ip_addr while true; do read -r -p "Enter IP address${detected_ip:+ [$detected_ip]}: " ip_addr ip_addr=${ip_addr:-$detected_ip} if validate_ipv4 "$ip_addr"; then break else print_error "Invalid IP address (must be 0.0.0.0 - 255.255.255.255)" fi done BASE_URL_HOST="$ip_addr" local default_port="$WEB_PORT" local port while true; do read -r -p "Enter web port [$default_port]: " port port=${port:-$default_port} if validate_port "$port"; then break else print_error "Invalid port number" fi done WEB_PORT="$port" BASE_URL_PORT="$WEB_PORT" recalculate_urls print_success "Base URL set to: $MOSAIC_BASE_URL" } configure_traefik_url() { echo "" print_info "Configuring Traefik integration" echo "" local mode_selection mode_selection=$(select_option "Traefik mode:" \ "Bundled Traefik (run inside this stack)" \ "Upstream Traefik (existing instance)") if [[ "$mode_selection" == *"Bundled"* ]]; then TRAEFIK_MODE="bundled" TRAEFIK_ENABLE=true else TRAEFIK_MODE="upstream" TRAEFIK_ENABLE=true fi if confirm "Enable TLS/HTTPS for Traefik?" "y"; then TRAEFIK_TLS_ENABLED=true TRAEFIK_ENTRYPOINT="websecure" BASE_URL_SCHEME="https" else TRAEFIK_TLS_ENABLED=false TRAEFIK_ENTRYPOINT="web" BASE_URL_SCHEME="http" fi BASE_URL_MODE="traefik" local default_web_domain="mosaic.local" while true; do read -r -p "Web domain [$default_web_domain]: " MOSAIC_WEB_DOMAIN MOSAIC_WEB_DOMAIN=${MOSAIC_WEB_DOMAIN:-$default_web_domain} if validate_domain "$MOSAIC_WEB_DOMAIN"; then break fi print_error "Invalid domain format" done local default_api_domain="api.${MOSAIC_WEB_DOMAIN}" while true; do read -r -p "API domain [$default_api_domain]: " MOSAIC_API_DOMAIN MOSAIC_API_DOMAIN=${MOSAIC_API_DOMAIN:-$default_api_domain} if validate_domain "$MOSAIC_API_DOMAIN"; then break fi print_error "Invalid domain format" done if [[ "$ENABLE_SSO" == true ]]; then local default_auth_domain="auth.${MOSAIC_WEB_DOMAIN}" while true; do read -r -p "Auth domain [$default_auth_domain]: " MOSAIC_AUTH_DOMAIN MOSAIC_AUTH_DOMAIN=${MOSAIC_AUTH_DOMAIN:-$default_auth_domain} if validate_domain "$MOSAIC_AUTH_DOMAIN"; then break fi print_error "Invalid domain format" done fi if [[ "$TRAEFIK_MODE" == "upstream" ]]; then local default_network="traefik-public" read -r -p "External Traefik network name [$default_network]: " TRAEFIK_NETWORK TRAEFIK_NETWORK=${TRAEFIK_NETWORK:-$default_network} TRAEFIK_DOCKER_NETWORK="$TRAEFIK_NETWORK" fi if [[ "$TRAEFIK_MODE" == "bundled" ]]; then if confirm "Enable Traefik dashboard?" "y"; then TRAEFIK_DASHBOARD_ENABLED=true local default_dash_port="$TRAEFIK_DASHBOARD_PORT" local dash_port while true; do read -r -p "Traefik dashboard port [$default_dash_port]: " dash_port dash_port=${dash_port:-$default_dash_port} if validate_port "$dash_port"; then break else print_error "Invalid port number" fi done TRAEFIK_DASHBOARD_PORT="$dash_port" else TRAEFIK_DASHBOARD_ENABLED=false fi fi recalculate_urls print_success "Base URL set to: $MOSAIC_BASE_URL" print_info "Traefik domains configured for web and API" } configure_sso() { echo "" print_step "SSO Configuration (Authentik)" echo "" # Check if SSO already configured local current_sso_enabled current_sso_enabled=$(get_env_value "ENABLE_SSO") if [[ -n "$ENABLE_SSO" ]]; then # Already set via CLI print_info "SSO enabled: $ENABLE_SSO" elif [[ "$current_sso_enabled" == "true" ]]; then echo "SSO is currently enabled" if [[ "$NON_INTERACTIVE" == false ]] && ! confirm "Keep SSO enabled?" "y"; then ENABLE_SSO=false return fi ENABLE_SSO=true else if [[ "$NON_INTERACTIVE" == true ]]; then ENABLE_SSO=false return fi if ! confirm "Enable Authentik SSO?"; then ENABLE_SSO=false return fi ENABLE_SSO=true fi if [[ "$ENABLE_SSO" != true ]]; then return fi # Bundled vs External Authentik local current_bundled current_bundled=$(get_env_value "USE_BUNDLED_AUTHENTIK") if [[ -n "$USE_BUNDLED_AUTHENTIK" ]]; then # Already set via CLI print_info "Using bundled Authentik: $USE_BUNDLED_AUTHENTIK" elif [[ "$current_bundled" == "true" ]]; then echo "Currently using bundled Authentik" if [[ "$NON_INTERACTIVE" == false ]] && ! confirm "Keep using bundled Authentik?" "y"; then USE_BUNDLED_AUTHENTIK=false else USE_BUNDLED_AUTHENTIK=true fi else if [[ "$NON_INTERACTIVE" == true ]]; then USE_BUNDLED_AUTHENTIK=false elif confirm "Use bundled Authentik server?" "y"; then USE_BUNDLED_AUTHENTIK=true else USE_BUNDLED_AUTHENTIK=false fi fi # If external, get URL if [[ "$USE_BUNDLED_AUTHENTIK" != true ]]; then local current_auth_url current_auth_url=$(get_env_value "AUTHENTIK_BASE_URL") if [[ -n "$EXTERNAL_AUTHENTIK_URL" ]]; then EXTERNAL_AUTHENTIK_URL=$(strip_trailing_slash "$EXTERNAL_AUTHENTIK_URL") elif [[ -n "$current_auth_url" ]] && ! is_placeholder "$current_auth_url"; then echo "Current Authentik URL: $current_auth_url" if [[ "$NON_INTERACTIVE" == true ]] || ! confirm "Change Authentik URL?" "n"; then EXTERNAL_AUTHENTIK_URL=$(strip_trailing_slash "$current_auth_url") else read_authentik_url fi else read_authentik_url fi if [[ -z "$EXTERNAL_AUTHENTIK_URL" ]]; then print_error "External Authentik URL is required when not using bundled Authentik" exit 1 fi fi if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then if [[ "$BASE_URL_MODE" == "traefik" && -z "$MOSAIC_AUTH_DOMAIN" ]]; then if [[ "$NON_INTERACTIVE" == true ]]; then MOSAIC_AUTH_DOMAIN="auth.${MOSAIC_WEB_DOMAIN:-mosaic.local}" else local default_auth_domain="auth.${MOSAIC_WEB_DOMAIN:-mosaic.local}" read -r -p "Auth domain [$default_auth_domain]: " MOSAIC_AUTH_DOMAIN MOSAIC_AUTH_DOMAIN=${MOSAIC_AUTH_DOMAIN:-$default_auth_domain} fi fi fi recalculate_urls } read_authentik_url() { while true; do read -r -p "Enter external Authentik URL: " EXTERNAL_AUTHENTIK_URL if validate_url "$EXTERNAL_AUTHENTIK_URL"; then EXTERNAL_AUTHENTIK_URL=$(strip_trailing_slash "$EXTERNAL_AUTHENTIK_URL") break else print_error "Invalid URL format" fi done } configure_ollama() { echo "" print_step "Ollama Configuration" echo "" local current_mode current_mode=$(get_env_value "OLLAMA_MODE") if [[ -n "$OLLAMA_MODE" ]] && [[ "$OLLAMA_MODE" != "disabled" ]]; then # Already set via CLI print_info "Ollama mode: $OLLAMA_MODE" elif [[ -n "$current_mode" ]] && [[ "$current_mode" != "disabled" ]]; then echo "Current Ollama mode: $current_mode" if [[ "$NON_INTERACTIVE" == false ]] && confirm "Change Ollama configuration?" "n"; then select_ollama_mode else OLLAMA_MODE="$current_mode" fi else select_ollama_mode fi # Get Ollama URL if remote mode if [[ "$OLLAMA_MODE" == "remote" ]]; then local current_url current_url=$(get_env_value "OLLAMA_ENDPOINT") if [[ -n "$OLLAMA_URL" ]]; then # Already set via CLI print_info "Ollama URL: $OLLAMA_URL" elif [[ -n "$current_url" ]] && ! is_placeholder "$current_url"; then echo "Current Ollama URL: $current_url" if [[ "$NON_INTERACTIVE" == false ]] && confirm "Change Ollama URL?" "n"; then read_ollama_url else OLLAMA_URL="$current_url" fi else read_ollama_url fi fi } select_ollama_mode() { if [[ "$NON_INTERACTIVE" == true ]]; then OLLAMA_MODE="disabled" return fi local selection selection=$(select_option "Select Ollama mode:" \ "Disabled (no local LLM)" \ "Local (run Ollama in Docker)" \ "Remote (connect to existing Ollama server)") case "$selection" in *"Disabled"*) OLLAMA_MODE="disabled" ;; *"Local"*) OLLAMA_MODE="local" ;; *"Remote"*) OLLAMA_MODE="remote" ;; esac } read_ollama_url() { while true; do read -r -p "Enter Ollama server URL: " OLLAMA_URL if validate_url "$OLLAMA_URL"; then break else print_error "Invalid URL format" fi done } configure_moltbot() { echo "" print_step "MoltBot Configuration" echo "" local current_enabled current_enabled=$(get_env_value "ENABLE_MOLTBOT") if [[ -n "$ENABLE_MOLTBOT" ]]; then # Already set via CLI print_info "MoltBot enabled: $ENABLE_MOLTBOT" elif [[ "$current_enabled" == "true" ]]; then echo "MoltBot is currently enabled" if [[ "$NON_INTERACTIVE" == false ]] && ! confirm "Keep MoltBot enabled?" "y"; then ENABLE_MOLTBOT=false else ENABLE_MOLTBOT=true fi else if [[ "$NON_INTERACTIVE" == true ]]; then ENABLE_MOLTBOT=false elif confirm "Enable MoltBot integration?"; then ENABLE_MOLTBOT=true else ENABLE_MOLTBOT=false fi fi } configure_secrets() { echo "" print_step "Secret Generation" echo "" # Check if secrets exist and are not placeholders local db_password local jwt_secret local auth_db_password local auth_secret_key local auth_bootstrap_password db_password=$(get_env_value "POSTGRES_PASSWORD") jwt_secret=$(get_env_value "JWT_SECRET") auth_db_password=$(get_env_value "AUTHENTIK_POSTGRES_PASSWORD") auth_secret_key=$(get_env_value "AUTHENTIK_SECRET_KEY") auth_bootstrap_password=$(get_env_value "AUTHENTIK_BOOTSTRAP_PASSWORD") local needs_generation=false if is_placeholder "$db_password"; then print_info "Will generate database password" needs_generation=true fi if is_placeholder "$jwt_secret"; then print_info "Will generate JWT secret" needs_generation=true fi if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then if is_placeholder "$auth_db_password"; then print_info "Will generate Authentik database password" needs_generation=true fi if is_placeholder "$auth_secret_key"; then print_info "Will generate Authentik secret key" needs_generation=true fi if is_placeholder "$auth_bootstrap_password"; then print_info "Will generate Authentik bootstrap password" needs_generation=true fi fi if [[ "$needs_generation" == true ]]; then print_success "Secrets will be generated during .env creation" else print_success "Existing secrets will be preserved" fi } # ============================================================================ # .env File Generation # ============================================================================ declare -gA ENV_OVERRIDES env_override_set() { local key="$1" local value="$2" ENV_OVERRIDES["$key"]="$value" } write_env_with_overrides() { local template="$1" local output="$2" local overrides_file overrides_file=$(mktemp) for key in "${!ENV_OVERRIDES[@]}"; do printf '%s=%s\n' "$key" "${ENV_OVERRIDES[$key]}" >> "$overrides_file" done awk -v overrides_file="$overrides_file" ' BEGIN { while ((getline < overrides_file) > 0) { line = $0 if (line ~ /^[A-Za-z0-9_]+=*/) { key = substr(line, 1, index(line, "=") - 1) value = substr(line, index(line, "=") + 1) map[key] = value } } close(overrides_file) } { if (match($0, /^([A-Za-z0-9_]+)=/, m)) { key = m[1] if (key in map) { print key "=" map[key] used[key] = 1 next } } print } END { for (key in map) { if (!(key in used)) { print key "=" map[key] } } } ' "$template" > "$output" rm -f "$overrides_file" } generate_env_file() { print_header ".env File Generation" # Backup existing .env if it exists if [[ -f "$PROJECT_ROOT/.env" ]]; then backup_file "$PROJECT_ROOT/.env" fi echo "" print_step "Generating .env file with your configuration..." echo "" # Start with .env.example template local template="$PROJECT_ROOT/.env.example" local output="$PROJECT_ROOT/.env" if [[ ! -f "$template" ]]; then print_error ".env.example template not found" exit 1 fi ENV_OVERRIDES=() for key in "${!ENV_VALUES[@]}"; do local value="${ENV_VALUES[$key]}" if [[ -n "$value" ]] && ! is_placeholder "$value"; then env_override_set "$key" "$value" fi done local xtrace_was_on=false if [[ "$-" == *x* ]]; then xtrace_was_on=true set +x fi local postgres_user local postgres_db local db_password local jwt_secret local jwt_expiration local api_host local valkey_url local database_url postgres_user=$(resolve_env_value "POSTGRES_USER" "mosaic") postgres_db=$(resolve_env_value "POSTGRES_DB" "mosaic") api_host=$(resolve_env_value "API_HOST" "0.0.0.0") jwt_expiration=$(resolve_env_value "JWT_EXPIRATION" "24h") local existing_db_password existing_db_password=$(get_env_value "POSTGRES_PASSWORD") db_password=$(resolve_secret_value "POSTGRES_PASSWORD" generate_password 32) if [[ -n "$existing_db_password" ]] && ! is_placeholder "$existing_db_password"; then print_info "Preserving existing database password" else print_info "Generated database password" fi local existing_jwt_secret existing_jwt_secret=$(get_env_value "JWT_SECRET") jwt_secret=$(resolve_secret_value "JWT_SECRET" generate_secret 50) if [[ -n "$existing_jwt_secret" ]] && ! is_placeholder "$existing_jwt_secret"; then print_info "Preserving existing JWT secret" else print_info "Generated JWT secret" fi if [[ "$MODE" == "docker" ]]; then database_url="postgresql://${postgres_user}:${db_password}@postgres:5432/${postgres_db}" valkey_url="redis://valkey:6379" else database_url="postgresql://${postgres_user}:${db_password}@localhost:${POSTGRES_PORT}/${postgres_db}" valkey_url="redis://localhost:${VALKEY_PORT}" fi local authentik_db_password="" local authentik_secret_key="" local authentik_bootstrap_password="" local authentik_postgres_user="" local authentik_postgres_db="" local authentik_bootstrap_email="" local authentik_cookie_domain="" if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then authentik_postgres_user=$(resolve_env_value "AUTHENTIK_POSTGRES_USER" "authentik") authentik_postgres_db=$(resolve_env_value "AUTHENTIK_POSTGRES_DB" "authentik") authentik_bootstrap_email=$(resolve_env_value "AUTHENTIK_BOOTSTRAP_EMAIL" "admin@localhost") local existing_auth_db_password existing_auth_db_password=$(get_env_value "AUTHENTIK_POSTGRES_PASSWORD") authentik_db_password=$(resolve_secret_value "AUTHENTIK_POSTGRES_PASSWORD" generate_password 32) if [[ -n "$existing_auth_db_password" ]] && ! is_placeholder "$existing_auth_db_password"; then print_info "Preserving existing Authentik database password" else print_info "Generated Authentik database password" fi local existing_auth_secret existing_auth_secret=$(get_env_value "AUTHENTIK_SECRET_KEY") authentik_secret_key=$(resolve_secret_value "AUTHENTIK_SECRET_KEY" generate_secret 64) if [[ -n "$existing_auth_secret" ]] && ! is_placeholder "$existing_auth_secret"; then print_info "Preserving existing Authentik secret key" else print_info "Generated Authentik secret key" fi local existing_auth_bootstrap existing_auth_bootstrap=$(get_env_value "AUTHENTIK_BOOTSTRAP_PASSWORD") authentik_bootstrap_password=$(resolve_secret_value "AUTHENTIK_BOOTSTRAP_PASSWORD" generate_password 24) if [[ -n "$existing_auth_bootstrap" ]] && ! is_placeholder "$existing_auth_bootstrap"; then print_info "Preserving existing Authentik bootstrap password" else print_info "Generated Authentik bootstrap password" fi authentik_cookie_domain=$(resolve_env_value "AUTHENTIK_COOKIE_DOMAIN" "") if [[ -z "$authentik_cookie_domain" ]]; then if [[ "$BASE_URL_MODE" == "traefik" && -n "$MOSAIC_AUTH_DOMAIN" ]]; then authentik_cookie_domain=$(derive_cookie_domain "$MOSAIC_AUTH_DOMAIN") else authentik_cookie_domain=".localhost" fi fi fi local oidc_issuer="" local oidc_client_id="" local oidc_client_secret="" local oidc_redirect_uri="" if [[ "$ENABLE_SSO" == true ]]; then local auth_url if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then auth_url=$(strip_trailing_slash "$AUTHENTIK_PUBLIC_URL") else auth_url=$(strip_trailing_slash "$EXTERNAL_AUTHENTIK_URL") fi oidc_issuer="${auth_url}/application/o/mosaic-stack/" oidc_client_id=$(resolve_env_value "OIDC_CLIENT_ID" "mosaic-stack") oidc_client_secret=$(resolve_env_value "OIDC_CLIENT_SECRET" "change-after-authentik-setup") oidc_redirect_uri="${API_BASE_URL}/auth/callback" fi local ollama_mode_env="" local ollama_endpoint="" if [[ "$OLLAMA_MODE" == "local" ]]; then ollama_mode_env="local" ollama_endpoint="http://ollama:11434" elif [[ "$OLLAMA_MODE" == "remote" ]]; then ollama_mode_env="remote" ollama_endpoint="$OLLAMA_URL" else ollama_mode_env="remote" ollama_endpoint=$(resolve_env_value "OLLAMA_ENDPOINT" "http://localhost:11434") fi local compose_profiles=() if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then compose_profiles+=("authentik") fi if [[ "$OLLAMA_MODE" == "local" ]]; then compose_profiles+=("ollama") fi if [[ "$TRAEFIK_MODE" == "bundled" ]]; then compose_profiles+=("traefik-bundled") fi local profiles_string="" if [[ ${#compose_profiles[@]} -gt 0 ]]; then profiles_string=$(IFS=,; echo "${compose_profiles[*]}") fi env_override_set "API_PORT" "$API_PORT" env_override_set "API_HOST" "$api_host" env_override_set "WEB_PORT" "$WEB_PORT" env_override_set "POSTGRES_USER" "$postgres_user" env_override_set "POSTGRES_PASSWORD" "$db_password" env_override_set "POSTGRES_DB" "$postgres_db" env_override_set "POSTGRES_PORT" "$POSTGRES_PORT" env_override_set "DATABASE_URL" "$database_url" env_override_set "VALKEY_URL" "$valkey_url" env_override_set "VALKEY_PORT" "$VALKEY_PORT" env_override_set "NEXT_PUBLIC_API_URL" "$API_BASE_URL" if [[ -n "$MOSAIC_BASE_URL" ]]; then env_override_set "MOSAIC_BASE_URL" "$MOSAIC_BASE_URL" fi env_override_set "JWT_SECRET" "$jwt_secret" env_override_set "JWT_EXPIRATION" "$jwt_expiration" env_override_set "OLLAMA_MODE" "$ollama_mode_env" env_override_set "OLLAMA_ENDPOINT" "$ollama_endpoint" if [[ -n "$profiles_string" ]]; then env_override_set "COMPOSE_PROFILES" "$profiles_string" else env_override_set "COMPOSE_PROFILES" "" fi env_override_set "TRAEFIK_MODE" "$TRAEFIK_MODE" env_override_set "TRAEFIK_ENABLE" "$(bool_str "$TRAEFIK_ENABLE")" env_override_set "TRAEFIK_TLS_ENABLED" "$(bool_str "$TRAEFIK_TLS_ENABLED")" env_override_set "TRAEFIK_ENTRYPOINT" "$TRAEFIK_ENTRYPOINT" if [[ -n "$TRAEFIK_NETWORK" ]]; then env_override_set "TRAEFIK_NETWORK" "$TRAEFIK_NETWORK" fi if [[ -n "$TRAEFIK_DOCKER_NETWORK" ]]; then env_override_set "TRAEFIK_DOCKER_NETWORK" "$TRAEFIK_DOCKER_NETWORK" fi if [[ -n "$MOSAIC_WEB_DOMAIN" ]]; then env_override_set "MOSAIC_WEB_DOMAIN" "$MOSAIC_WEB_DOMAIN" fi if [[ -n "$MOSAIC_API_DOMAIN" ]]; then env_override_set "MOSAIC_API_DOMAIN" "$MOSAIC_API_DOMAIN" fi if [[ -n "$MOSAIC_AUTH_DOMAIN" ]]; then env_override_set "MOSAIC_AUTH_DOMAIN" "$MOSAIC_AUTH_DOMAIN" fi if [[ "$TRAEFIK_MODE" == "bundled" ]]; then env_override_set "TRAEFIK_HTTP_PORT" "$TRAEFIK_HTTP_PORT" env_override_set "TRAEFIK_HTTPS_PORT" "$TRAEFIK_HTTPS_PORT" env_override_set "TRAEFIK_DASHBOARD_ENABLED" "$(bool_str "$TRAEFIK_DASHBOARD_ENABLED")" env_override_set "TRAEFIK_DASHBOARD_PORT" "$TRAEFIK_DASHBOARD_PORT" if [[ -n "$TRAEFIK_ACME_EMAIL" ]]; then env_override_set "TRAEFIK_ACME_EMAIL" "$TRAEFIK_ACME_EMAIL" fi if [[ -n "$TRAEFIK_CERTRESOLVER" ]]; then env_override_set "TRAEFIK_CERTRESOLVER" "$TRAEFIK_CERTRESOLVER" fi fi if [[ "$ENABLE_SSO" == true ]]; then env_override_set "OIDC_ISSUER" "$oidc_issuer" env_override_set "OIDC_CLIENT_ID" "$oidc_client_id" env_override_set "OIDC_CLIENT_SECRET" "$oidc_client_secret" env_override_set "OIDC_REDIRECT_URI" "$oidc_redirect_uri" fi if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then env_override_set "AUTHENTIK_POSTGRES_USER" "$authentik_postgres_user" env_override_set "AUTHENTIK_POSTGRES_PASSWORD" "$authentik_db_password" env_override_set "AUTHENTIK_POSTGRES_DB" "$authentik_postgres_db" env_override_set "AUTHENTIK_SECRET_KEY" "$authentik_secret_key" env_override_set "AUTHENTIK_BOOTSTRAP_PASSWORD" "$authentik_bootstrap_password" env_override_set "AUTHENTIK_BOOTSTRAP_EMAIL" "$authentik_bootstrap_email" env_override_set "AUTHENTIK_COOKIE_DOMAIN" "$authentik_cookie_domain" env_override_set "AUTHENTIK_PORT_HTTP" "$AUTHENTIK_PORT_HTTP" env_override_set "AUTHENTIK_PORT_HTTPS" "$AUTHENTIK_PORT_HTTPS" fi if [[ "$OLLAMA_MODE" == "local" ]]; then env_override_set "OLLAMA_PORT" "$OLLAMA_PORT" fi write_env_with_overrides "$template" "$output" chmod 600 "$output" print_success "Generated .env file" # Write credentials file write_credentials_file "$db_password" "$authentik_bootstrap_password" if [[ "$xtrace_was_on" == true ]]; then set -x fi } write_credentials_file() { local db_password="$1" local authentik_password="$2" local creds_file="$PROJECT_ROOT/.admin-credentials" local db_user db_user=$(resolve_env_value "POSTGRES_USER" "mosaic") cat > "$creds_file" << EOF # Mosaic Stack Admin Credentials # Generated: $(date) # KEEP THIS FILE SECURE AND DO NOT COMMIT TO VERSION CONTROL Database (PostgreSQL): Username: ${db_user} Password: $(mask_value "$db_password") Full password available in .env as POSTGRES_PASSWORD JWT Secret: Available in .env as JWT_SECRET EOF if [[ "$ENABLE_SSO" == true && -n "$authentik_password" ]]; then cat >> "$creds_file" << EOF Authentik Admin: Email: admin@localhost Password: $(mask_value "$authentik_password") Full password available in .env as AUTHENTIK_BOOTSTRAP_PASSWORD URL: ${AUTHENTIK_PUBLIC_URL:-http://localhost:9000} IMPORTANT: Change the bootstrap password after first login! EOF fi chmod 600 "$creds_file" print_success "Saved credentials to .admin-credentials" } # ============================================================================ # Deployment # ============================================================================ run_deployment() { print_header "Deployment" if [[ "$MODE" == "docker" ]]; then deploy_docker else deploy_native fi } prepare_traefik_upstream() { if [[ "$TRAEFIK_MODE" != "upstream" ]]; then return fi print_header "Traefik Upstream Setup" local network="${TRAEFIK_NETWORK:-traefik-public}" if ! docker network ls --format '{{.Name}}' | grep -q "^${network}$"; then print_warning "Traefik network '${network}' not found" if [[ "$NON_INTERACTIVE" == true ]] || confirm "Create network '${network}' now?" "y"; then docker network create "$network" print_success "Created network: ${network}" else print_warning "Continuing without creating network" fi fi local override_file="$PROJECT_ROOT/docker-compose.override.yml" if [[ -f "$override_file" ]]; then print_info "docker-compose.override.yml already exists. Leaving it unchanged." return fi cat > "$override_file" << 'EOF' version: '3.9' services: api: networks: - traefik-public web: networks: - traefik-public authentik-server: networks: - traefik-public networks: traefik-public: external: true name: ${TRAEFIK_NETWORK:-traefik-public} EOF print_success "Created docker-compose.override.yml for upstream Traefik" } deploy_docker() { echo "" print_step "Starting Docker deployment..." echo "" prepare_traefik_upstream # Check if docker-compose.yml exists if [[ ! -f "$PROJECT_ROOT/docker-compose.yml" ]]; then print_error "docker-compose.yml not found" exit 1 fi cd "$PROJECT_ROOT" || exit 1 local compose_files=("-f" "docker-compose.yml") if [[ -f "docker-compose.override.yml" ]]; then compose_files+=("-f" "docker-compose.override.yml") fi # Set up error trap for rollback local deployment_started=false rollback_deployment() { if [[ "$deployment_started" == true ]]; then print_warning "Deployment failed, rolling back..." docker compose "${compose_files[@]}" down --remove-orphans 2>/dev/null || true print_info "Rollback complete" fi } trap rollback_deployment ERR # Pull images print_step "Pulling Docker images..." if ! docker compose "${compose_files[@]}" pull; then print_warning "Failed to pull some images (will build locally)" fi # Build services print_step "Building services..." if ! docker compose "${compose_files[@]}" build; then print_error "Build failed" trap - ERR return 1 fi # Start services print_step "Starting services..." deployment_started=true if ! docker compose "${compose_files[@]}" up -d; then print_error "Failed to start services" trap - ERR rollback_deployment return 1 fi # Clear trap on success trap - ERR # Wait for services to be healthy echo "" print_step "Waiting for services to start..." sleep 5 # Check service health local all_healthy=true local services=("postgres" "valkey" "api" "web") for service in "${services[@]}"; do if docker compose "${compose_files[@]}" ps "$service" 2>/dev/null | grep -q "Up"; then print_success "$service: Running" else print_warning "$service: Not running" all_healthy=false fi done if [[ "$all_healthy" == true ]]; then echo "" print_success "All services started successfully" else echo "" print_warning "Some services may not be running correctly" print_info "Check logs with: docker compose logs" fi } deploy_native() { echo "" print_step "Native deployment setup..." echo "" cd "$PROJECT_ROOT" || exit 1 # Install dependencies print_step "Installing Node.js dependencies..." if ! pnpm install; then print_error "Failed to install dependencies" exit 1 fi # Database setup print_step "Setting up database..." print_info "Make sure PostgreSQL is running and accessible" # Run migrations print_step "Running database migrations..." if ! pnpm -F api run prisma:migrate; then print_warning "Database migration failed - you may need to set this up manually" fi # Build print_step "Building application..." pnpm run build print_success "Native setup complete" echo "" print_info "To start the application:" print_info " pnpm dev # Development mode" print_info " pnpm start # Production mode" } # ============================================================================ # Post-Install Information # ============================================================================ show_post_install_info() { if [[ "$DRY_RUN" == true ]]; then print_header "Dry Run Summary" echo "" print_info "No files were written and no containers were started." echo "" print_step "Planned URLs:" echo "" if [[ "$MODE" == "docker" ]]; then echo " Web Interface: $MOSAIC_BASE_URL" echo " API: $API_BASE_URL" if [[ "$ENABLE_SSO" == true ]]; then echo " Authentik SSO: ${AUTHENTIK_PUBLIC_URL:-http://localhost:9000}" fi fi echo "" return fi print_header "Installation Complete" echo "" echo "🎉 Mosaic Stack has been set up successfully!" echo "" # Show URLs print_step "Access URLs:" echo "" if [[ "$MODE" == "docker" ]]; then local web_url="$MOSAIC_BASE_URL" local api_url="$API_BASE_URL" echo " Web Interface: $web_url" echo " API: $api_url" if [[ "$ENABLE_SSO" == true ]]; then local auth_url="${AUTHENTIK_PUBLIC_URL:-http://localhost:9000}" echo " Authentik SSO: $auth_url" fi if [[ "$OLLAMA_MODE" == "local" ]]; then echo " Ollama: http://localhost:${OLLAMA_PORT}" fi else echo " Run 'pnpm dev' to start the development server" fi echo "" print_step "Credentials:" echo "" echo " Saved to: .admin-credentials" print_warning "Keep this file secure!" echo "" print_step "Next Steps:" echo "" if [[ "$ENABLE_SSO" == true ]]; then echo " 1. Access Authentik at ${AUTHENTIK_PUBLIC_URL:-http://localhost:9000}" echo " 2. Log in with credentials from .admin-credentials" echo " 3. Complete SSO setup (create application, get client secret)" echo " 4. Update OIDC_CLIENT_SECRET in .env" echo " 5. Access Mosaic Stack at $MOSAIC_BASE_URL" else echo " 1. Access Mosaic Stack at $MOSAIC_BASE_URL" echo " 2. Create your first user account" fi if [[ "$TRAEFIK_MODE" == "upstream" ]]; then echo "" print_info "Upstream Traefik: ensure your Traefik is attached to '${TRAEFIK_NETWORK:-traefik-public}'" elif [[ "$TRAEFIK_MODE" == "bundled" && "$TRAEFIK_DASHBOARD_ENABLED" == true ]]; then echo "" print_info "Traefik dashboard: http://localhost:${TRAEFIK_DASHBOARD_PORT}/dashboard/" fi echo "" print_step "Useful Commands:" echo "" if [[ "$MODE" == "docker" ]]; then echo " View logs: docker compose logs -f" echo " Stop services: docker compose down" echo " Restart: docker compose restart" echo " View status: docker compose ps" else echo " Development: pnpm dev" echo " Production: pnpm start" echo " Database: pnpm -F api run prisma:migrate" fi echo "" print_step "Documentation:" echo "" echo " Setup docs: /home/jwoltje/src/mosaic-stack/scripts/README.md" echo " Architecture: /home/jwoltje/src/mosaic-stack/docs/" echo "" } # ============================================================================ # Main Execution # ============================================================================ main() { parse_arguments "$@" show_banner detect_platform select_deployment_mode check_and_install_dependencies collect_configuration resolve_port_conflicts if [[ "$DRY_RUN" != true ]]; then generate_env_file run_deployment else echo "" print_warning "Dry run mode - skipping .env generation and deployment" echo "" print_info "Configuration collected but not applied" fi show_post_install_info echo "" print_success "Setup complete!" echo "" echo "===================================================================" echo "Setup completed: $(date)" echo "Full log saved to: $LOG_FILE" echo "===================================================================" echo "" # Reset terminal colors before exiting printf "${NC}" } # Run main function main "$@"