Files
stack/scripts/commands/doctor.sh
Jason Woltje ab52827d9c
All checks were successful
ci/woodpecker/push/infra Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
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>
2026-02-14 11:04:36 -06:00

553 lines
18 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "$@"