#!/bin/bash set -euo pipefail # ============================================================================ # Mosaic Stack Doctor # ============================================================================ # Diagnostic and repair tool for Mosaic Stack installations. # Run without arguments for interactive mode, or use flags for CI/CD. 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" # ============================================================================ # Configuration # ============================================================================ FIX_MODE=false JSON_OUTPUT=false VERBOSE=false ENV_FILE="$PROJECT_ROOT/.env" COMPOSE_FILE="$PROJECT_ROOT/docker-compose.yml" MODE="" # ============================================================================ # Help # ============================================================================ print_usage() { cat << EOF Mosaic Stack Doctor - Diagnostic and repair tool USAGE: ./scripts/commands/doctor.sh [OPTIONS] OPTIONS: -h, --help Show this help message --fix Attempt to automatically fix issues --json Output results in JSON format --verbose Show detailed output --env FILE Path to .env file (default: .env) --compose FILE Path to docker-compose.yml (default: docker-compose.yml) --mode MODE Deployment mode: docker or native (auto-detected) EXIT CODES: 0 All checks passed 1 Some checks failed 2 Critical failure EXAMPLES: # Run all checks ./scripts/commands/doctor.sh # Attempt automatic fixes ./scripts/commands/doctor.sh --fix # JSON output for CI/CD ./scripts/commands/doctor.sh --json # Verbose output ./scripts/commands/doctor.sh --verbose EOF } # ============================================================================ # Argument Parsing # ============================================================================ parse_arguments() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) print_usage exit 0 ;; --fix) FIX_MODE=true shift ;; --json) JSON_OUTPUT=true shift ;; --verbose) VERBOSE=true shift ;; --env) ENV_FILE="$2" shift 2 ;; --compose) COMPOSE_FILE="$2" shift 2 ;; --mode) MODE="$2" shift 2 ;; *) echo "Unknown option: $1" exit 1 ;; esac done # Auto-detect mode if not specified if [[ -z "$MODE" ]]; then if [[ -f "$COMPOSE_FILE" ]] && command -v docker &>/dev/null && docker info &>/dev/null; then MODE="docker" else MODE="native" fi fi } # ============================================================================ # JSON Output Helpers # ============================================================================ json_start() { if [[ "$JSON_OUTPUT" == true ]]; then echo "{" echo ' "timestamp": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'",' echo ' "version": "1.0.0",' echo ' "mode": "'$MODE'",' echo ' "checks": [' fi } json_end() { local errors="$1" local warnings="$2" if [[ "$JSON_OUTPUT" == true ]]; then echo "" echo " ]," echo ' "summary": {' echo ' "errors": '$errors',' echo ' "warnings": '$warnings',' echo ' "status": "'$([ "$errors" -gt 0 ] && echo "failed" || ([ "$warnings" -gt 0 ] && echo "warning" || echo "passed"))'"' echo ' }' echo "}" fi } json_check() { local name="$1" local status="$2" local message="$3" local first="${4:-true}" if [[ "$JSON_OUTPUT" == true ]]; then [[ "$first" != "true" ]] && echo "," echo -n ' {"name": "'$name'", "status": "'$status'", "message": "'$message'"}' fi } # ============================================================================ # Fix Functions # ============================================================================ fix_env_permissions() { echo -e "${WARN}→${NC} Fixing .env permissions..." chmod 600 "$ENV_FILE" echo -e "${SUCCESS}✓${NC} Fixed" } fix_docker_permissions() { echo -e "${WARN}→${NC} Adding user to docker group..." maybe_sudo usermod -aG docker "$USER" echo -e "${SUCCESS}✓${NC} User added to docker group" echo -e "${INFO}ℹ${NC} Run 'newgrp docker' or log out/in for changes to take effect" } start_docker_daemon() { echo -e "${WARN}→${NC} Starting Docker daemon..." maybe_sudo systemctl start docker sleep 3 if docker info &>/dev/null; then echo -e "${SUCCESS}✓${NC} Docker started" else echo -e "${ERROR}✗${NC} Failed to start Docker" return 1 fi } restart_containers() { echo -e "${WARN}→${NC} Restarting containers..." docker_compose_down "$COMPOSE_FILE" "$ENV_FILE" docker_compose_up "$COMPOSE_FILE" "$ENV_FILE" echo -e "${SUCCESS}✓${NC} Containers restarted" } generate_missing_secrets() { echo -e "${WARN}→${NC} Generating missing secrets..." # Load existing env if [[ -f "$ENV_FILE" ]]; then set -a # shellcheck source=/dev/null source "$ENV_FILE" 2>/dev/null || true set +a fi local updated=false # Check each secret if [[ -z "${JWT_SECRET:-}" ]] || is_placeholder "${JWT_SECRET:-}"; then JWT_SECRET=$(openssl rand -base64 32) echo "JWT_SECRET=$JWT_SECRET" >> "$ENV_FILE" updated=true fi if [[ -z "${BETTER_AUTH_SECRET:-}" ]] || is_placeholder "${BETTER_AUTH_SECRET:-}"; then BETTER_AUTH_SECRET=$(openssl rand -base64 32) echo "BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET" >> "$ENV_FILE" updated=true fi if [[ -z "${ENCRYPTION_KEY:-}" ]] || is_placeholder "${ENCRYPTION_KEY:-}"; then ENCRYPTION_KEY=$(openssl rand -hex 32) echo "ENCRYPTION_KEY=$ENCRYPTION_KEY" >> "$ENV_FILE" updated=true fi if [[ -z "${POSTGRES_PASSWORD:-}" ]] || is_placeholder "${POSTGRES_PASSWORD:-}"; then POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 32) # Update DATABASE_URL too DATABASE_URL="postgresql://mosaic:${POSTGRES_PASSWORD}@postgres:5432/mosaic" sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$POSTGRES_PASSWORD|" "$ENV_FILE" 2>/dev/null || \ echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" >> "$ENV_FILE" sed -i "s|^DATABASE_URL=.*|DATABASE_URL=$DATABASE_URL|" "$ENV_FILE" 2>/dev/null || \ echo "DATABASE_URL=$DATABASE_URL" >> "$ENV_FILE" updated=true fi if [[ "$updated" == true ]]; then echo -e "${SUCCESS}✓${NC} Secrets generated" else echo -e "${INFO}ℹ${NC} All secrets already set" fi } # ============================================================================ # Check Functions # ============================================================================ run_checks() { local errors=0 local warnings=0 local first=true json_start # System requirements echo -e "${BOLD}━━━ System Requirements ━━━${NC}" check_system_requirements 2048 10 local result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "system_requirements" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") )" "RAM and disk check" "$first" first=false echo "" # Environment file echo -e "${BOLD}━━━ Environment File ━━━${NC}" check_env_file "$ENV_FILE" result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "env_file" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") )" ".env file exists" "$first" echo "" # Required variables check_required_env "$ENV_FILE" result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "required_env" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") )" "Required environment variables" "$first" echo "" # Secret strength check_secrets "$ENV_FILE" result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "secrets" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") ")" "Secret strength and validity" "$first" echo "" # File permissions check_env_permissions "$ENV_FILE" result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "env_permissions" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") ")" ".env file permissions" "$first" echo "" if [[ "$MODE" == "docker" ]]; then # Docker checks echo -e "${BOLD}━━━ Docker ━━━${NC}" check_docker result=$? [[ $result -ne 0 ]] && ((errors++)) json_check "docker" "$( [[ $result -eq 0 ]] && echo "pass" || "fail")" "Docker availability" "$first" echo "" if [[ $result -eq 0 ]]; then check_docker_compose result=$? [[ $result -ne 0 ]] && ((errors++)) json_check "docker_compose" "$( [[ $result -eq 0 ]] && echo "pass" || "fail")" "Docker Compose availability" "$first" echo "" # Container status check_docker_containers "$COMPOSE_FILE" result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "containers" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") ")" "Container status" "$first" echo "" # Container health check_container_health result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "container_health" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") ")" "Container health checks" "$first" echo "" # Database check_database_connection result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "database" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") ")" "Database connectivity" "$first" echo "" # Valkey check_valkey_connection result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "cache" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") ")" "Valkey/Redis connectivity" "$first" echo "" # API check_api_health result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "api" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") ")" "API health endpoint" "$first" echo "" # Web check_web_health result=$? [[ $result -eq $CHECK_FAIL ]] && ((errors++)) [[ $result -eq $CHECK_WARN ]] && ((warnings++)) json_check "web" "$( [[ $result -eq 0 ]] && echo "pass" || ([[ $result -eq 1 ]] && echo "warn" || echo "fail") ")" "Web frontend" "$first" echo "" fi else # Native mode checks echo -e "${BOLD}━━━ Native Dependencies ━━━${NC}" check_node result=$? [[ $result -ne 0 ]] && ((errors++)) json_check "nodejs" "$( [[ $result -eq 0 ]] && echo "pass" || "fail")" "Node.js" "$first" echo "" check_pnpm result=$? [[ $result -ne 0 ]] && ((errors++)) json_check "pnpm" "$( [[ $result -eq 0 ]] && echo "pass" || "fail")" "pnpm" "$first" echo "" check_postgres result=$? [[ $result -ne 0 ]] && ((warnings++)) json_check "postgresql" "$( [[ $result -eq 0 ]] && echo "pass" || "warn")" "PostgreSQL" "$first" echo "" fi json_end $errors $warnings return $([ $errors -eq 0 ] && echo 0 || echo 1) } # ============================================================================ # Interactive Fix Mode # ============================================================================ interactive_fix() { local issues=() # Collect issues if [[ ! -f "$ENV_FILE" ]]; then issues+=("env_missing:Missing .env file") fi if [[ -f "$ENV_FILE" ]]; then # Check permissions local perms perms=$(stat -c "%a" "$ENV_FILE" 2>/dev/null || stat -f "%OLp" "$ENV_FILE" 2>/dev/null) if [[ "$perms" =~ [0-7][0-7][4-7]$ ]]; then issues+=("env_perms:.env is world-readable") fi # Check secrets set -a # shellcheck source=/dev/null source "$ENV_FILE" 2>/dev/null || true set +a if [[ -z "${JWT_SECRET:-}" ]] || is_placeholder "${JWT_SECRET:-}"; then issues+=("secrets:Missing or invalid secrets") fi fi if [[ "$MODE" == "docker" ]]; then if ! docker info &>/dev/null; then local docker_result docker_result=$(docker info 2>&1) if [[ "$docker_result" =~ "permission denied" ]]; then issues+=("docker_perms:Docker permission denied") elif [[ "$docker_result" =~ "Cannot connect to the Docker daemon" ]]; then issues+=("docker_daemon:Docker daemon not running") fi fi fi if [[ ${#issues[@]} -eq 0 ]]; then echo -e "${SUCCESS}✓${NC} No fixable issues found" return 0 fi echo -e "${BOLD}Found ${#issues[@]} fixable issue(s):${NC}" echo "" for issue in "${issues[@]}"; do local code="${issue%%:*}" local desc="${issue#*:}" echo " - $desc" done echo "" read -r -p "Fix these issues? [Y/n]: " fix_them case "$fix_them" in n|N) echo -e "${INFO}ℹ${NC} Skipping fixes" return 0 ;; esac # Apply fixes for issue in "${issues[@]}"; do local code="${issue%%:*}" case "$code" in env_perms) fix_env_permissions ;; docker_perms) fix_docker_permissions ;; docker_daemon) start_docker_daemon ;; secrets) generate_missing_secrets ;; *) echo -e "${WARN}⚠${NC} Unknown issue: $code" ;; esac done echo "" echo -e "${SUCCESS}✓${NC} Fixes applied" } # ============================================================================ # Main # ============================================================================ main() { parse_arguments "$@" # Configure verbose mode if [[ "$VERBOSE" == true ]]; then set -x fi # Show banner (unless JSON mode) if [[ "$JSON_OUTPUT" != true ]]; then echo "" echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}" echo -e "${BOLD} Mosaic Stack Doctor${NC}" echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}" echo "" echo -e " Mode: ${INFO}$MODE${NC}" echo -e " Env: ${INFO}$ENV_FILE${NC}" echo -e " Compose: ${INFO}$COMPOSE_FILE${NC}" echo "" fi # Run checks run_checks local check_result=$? # Fix mode if [[ "$FIX_MODE" == true && "$JSON_OUTPUT" != true && $check_result -ne 0 ]]; then echo "" echo -e "${BOLD}━━━ Fix Mode ━━━${NC}" echo "" interactive_fix fi # Summary (unless JSON mode) if [[ "$JSON_OUTPUT" != true ]]; then echo "" echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}" if [[ $check_result -eq 0 ]]; then echo -e "${SUCCESS}✓ All checks passed${NC}" else echo -e "${WARN}⚠ Some checks failed${NC}" echo "" echo "Run with --fix to attempt automatic repairs" fi echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}" fi exit $([ $check_result -eq 0 ] && echo 0 || echo 1) } # Run main "$@"