chore: add install scripts, doctor command, and AGENTS.md
All checks were successful
ci/woodpecker/push/infra Pipeline was successful
ci/woodpecker/push/api Pipeline was successful

- 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:
2026-02-14 11:04:36 -06:00
parent 0ca3945061
commit ab52827d9c
8 changed files with 4290 additions and 4 deletions

552
scripts/commands/doctor.sh Executable file
View File

@@ -0,0 +1,552 @@
#!/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 "$@"