chore: add install scripts, doctor command, and AGENTS.md
- Add one-line installer (scripts/install.sh) with platform detection - Add doctor command (scripts/commands/doctor.sh) for environment diagnostics - Add shared libraries: dependencies, docker, platform, validation - Update README with quick-start installer instructions - Add AGENTS.md with codebase patterns for AI agent context Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
834
scripts/install.sh
Executable file
834
scripts/install.sh
Executable file
@@ -0,0 +1,834 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user