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:
37
AGENTS.md
Normal file
37
AGENTS.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Mosaic Stack — Agent Guidelines
|
||||||
|
|
||||||
|
> **Any AI model, coding assistant, or framework working in this codebase MUST read and follow `CLAUDE.md` in the project root.**
|
||||||
|
|
||||||
|
`CLAUDE.md` is the authoritative source for:
|
||||||
|
|
||||||
|
- Technology stack and versions
|
||||||
|
- TypeScript strict mode requirements
|
||||||
|
- ESLint Quality Rails (error-level enforcement)
|
||||||
|
- Prettier formatting rules
|
||||||
|
- Testing requirements (85% coverage, TDD)
|
||||||
|
- API conventions and database patterns
|
||||||
|
- Commit format and branch strategy
|
||||||
|
- PDA-friendly design principles
|
||||||
|
|
||||||
|
## Quick Rules (Read CLAUDE.md for Details)
|
||||||
|
|
||||||
|
- **No `any` types** — use `unknown`, generics, or proper types
|
||||||
|
- **Explicit return types** on all functions
|
||||||
|
- **Type-only imports** — `import type { Foo }` for types
|
||||||
|
- **Double quotes**, semicolons, 2-space indent, 100 char width
|
||||||
|
- **`??` not `||`** for defaults, **`?.`** not `&&` chains
|
||||||
|
- **All promises** must be awaited or returned
|
||||||
|
- **85% test coverage** minimum, tests before implementation
|
||||||
|
|
||||||
|
## Updating Conventions
|
||||||
|
|
||||||
|
If you discover new patterns, gotchas, or conventions while working in this codebase, **update `CLAUDE.md`** — not this file. This file exists solely to redirect agents that look for `AGENTS.md` to the canonical source.
|
||||||
|
|
||||||
|
## Per-App Context
|
||||||
|
|
||||||
|
Each app directory has its own `AGENTS.md` for app-specific patterns:
|
||||||
|
|
||||||
|
- `apps/api/AGENTS.md`
|
||||||
|
- `apps/web/AGENTS.md`
|
||||||
|
- `apps/coordinator/AGENTS.md`
|
||||||
|
- `apps/orchestrator/AGENTS.md`
|
||||||
60
README.md
60
README.md
@@ -35,13 +35,65 @@ Mosaic Stack is a modern, PDA-friendly platform designed to help users manage th
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
### One-Line Install (Recommended)
|
||||||
|
|
||||||
|
The fastest way to get Mosaic Stack running on macOS or Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://get.mosaicstack.dev | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
This installer:
|
||||||
|
|
||||||
|
- ✅ Detects your platform (macOS, Debian/Ubuntu, Arch, Fedora)
|
||||||
|
- ✅ Installs all required dependencies (Docker, Node.js, etc.)
|
||||||
|
- ✅ Generates secure secrets automatically
|
||||||
|
- ✅ Configures the environment for you
|
||||||
|
- ✅ Starts all services with Docker Compose
|
||||||
|
- ✅ Validates the installation with health checks
|
||||||
|
|
||||||
|
**Installer Options:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Non-interactive Docker deployment
|
||||||
|
curl -fsSL https://get.mosaicstack.dev | bash -s -- --non-interactive --mode docker
|
||||||
|
|
||||||
|
# Preview installation without making changes
|
||||||
|
curl -fsSL https://get.mosaicstack.dev | bash -s -- --dry-run
|
||||||
|
|
||||||
|
# With SSO and local Ollama
|
||||||
|
curl -fsSL https://get.mosaicstack.dev | bash -s -- \
|
||||||
|
--mode docker \
|
||||||
|
--enable-sso --bundled-authentik \
|
||||||
|
--ollama-mode local
|
||||||
|
|
||||||
|
# Skip dependency installation (if already installed)
|
||||||
|
curl -fsSL https://get.mosaicstack.dev | bash -s -- --skip-deps
|
||||||
|
```
|
||||||
|
|
||||||
|
**After Installation:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check system health
|
||||||
|
./scripts/commands/doctor.sh
|
||||||
|
|
||||||
|
# View service logs
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js 20+ and pnpm 9+
|
If you prefer manual installation, you'll need:
|
||||||
- PostgreSQL 17+ (or use Docker)
|
|
||||||
- Docker & Docker Compose (optional, for turnkey deployment)
|
|
||||||
|
|
||||||
### Installation
|
- **Docker mode:** Docker 24+ and Docker Compose
|
||||||
|
- **Native mode:** Node.js 22+, pnpm 10+, PostgreSQL 17+
|
||||||
|
|
||||||
|
The installer handles these automatically.
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
|
|||||||
552
scripts/commands/doctor.sh
Executable file
552
scripts/commands/doctor.sh
Executable 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 "$@"
|
||||||
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
|
||||||
908
scripts/lib/dependencies.sh
Normal file
908
scripts/lib/dependencies.sh
Normal file
@@ -0,0 +1,908 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Dependency management functions for Mosaic Stack installer
|
||||||
|
# Handles installation and verification of all required dependencies
|
||||||
|
|
||||||
|
# shellcheck source=lib/platform.sh
|
||||||
|
source "${BASH_SOURCE[0]%/*}/platform.sh"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Dependency Version Requirements
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
MIN_NODE_VERSION=22
|
||||||
|
MIN_DOCKER_VERSION=24
|
||||||
|
MIN_PNPM_VERSION=10
|
||||||
|
MIN_POSTGRES_VERSION=17
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Generic Command Checking
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check if a command exists
|
||||||
|
check_command() {
|
||||||
|
command -v "$1" &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get version of a command (generic)
|
||||||
|
get_command_version() {
|
||||||
|
local cmd="$1"
|
||||||
|
local flag="${2:---version}"
|
||||||
|
|
||||||
|
"$cmd" "$flag" 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract major version number from version string
|
||||||
|
extract_major_version() {
|
||||||
|
local version="$1"
|
||||||
|
echo "$version" | grep -oE '[0-9]+' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Git
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
check_git() {
|
||||||
|
if check_command git; then
|
||||||
|
local version
|
||||||
|
version=$(git --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
echo -e "${SUCCESS}✓${NC} Git: ${INFO}$version${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo -e "${WARN}→${NC} Git not found"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_git() {
|
||||||
|
local os pkg
|
||||||
|
os=$(detect_os)
|
||||||
|
pkg=$(detect_package_manager "$os")
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Installing Git..."
|
||||||
|
|
||||||
|
case "$pkg" in
|
||||||
|
brew)
|
||||||
|
brew install git
|
||||||
|
;;
|
||||||
|
apt)
|
||||||
|
maybe_sudo apt-get update -y
|
||||||
|
maybe_sudo apt-get install -y git
|
||||||
|
;;
|
||||||
|
pacman)
|
||||||
|
maybe_sudo pacman -Sy --noconfirm git
|
||||||
|
;;
|
||||||
|
dnf)
|
||||||
|
maybe_sudo dnf install -y git
|
||||||
|
;;
|
||||||
|
yum)
|
||||||
|
maybe_sudo yum install -y git
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${ERROR}Error: Unknown package manager for Git installation${NC}"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Git installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_git() {
|
||||||
|
if ! check_git; then
|
||||||
|
install_git
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Homebrew (macOS)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
check_homebrew() {
|
||||||
|
if check_command brew; then
|
||||||
|
local prefix
|
||||||
|
prefix=$(brew --prefix 2>/dev/null)
|
||||||
|
echo -e "${SUCCESS}✓${NC} Homebrew: ${INFO}$prefix${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_homebrew() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Homebrew..."
|
||||||
|
|
||||||
|
# Download and run the Homebrew installer
|
||||||
|
local tmp
|
||||||
|
tmp=$(create_temp_file)
|
||||||
|
|
||||||
|
if ! download_file "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" "$tmp"; then
|
||||||
|
echo -e "${ERROR}Error: Failed to download Homebrew installer${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NONINTERACTIVE=1 /bin/bash "$tmp"
|
||||||
|
local ret=$?
|
||||||
|
rm -f "$tmp"
|
||||||
|
|
||||||
|
if [[ $ret -ne 0 ]]; then
|
||||||
|
echo -e "${ERROR}Error: Homebrew installation failed${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add Homebrew to PATH for this session
|
||||||
|
if [[ -f "/opt/homebrew/bin/brew" ]]; then
|
||||||
|
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||||
|
elif [[ -f "/usr/local/bin/brew" ]]; then
|
||||||
|
eval "$(/usr/local/bin/brew shellenv)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Homebrew installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_homebrew() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
if [[ "$os" != "macos" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! check_homebrew; then
|
||||||
|
install_homebrew
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Node.js
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
check_node() {
|
||||||
|
local min_version="${1:-$MIN_NODE_VERSION}"
|
||||||
|
|
||||||
|
if ! check_command node; then
|
||||||
|
echo -e "${WARN}→${NC} Node.js not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local version
|
||||||
|
version=$(node --version 2>/dev/null | sed 's/v//')
|
||||||
|
local major
|
||||||
|
major=$(extract_major_version "$version")
|
||||||
|
|
||||||
|
if [[ "$major" -ge "$min_version" ]]; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} Node.js: ${INFO}v$version${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${WARN}→${NC} Node.js v$version found, but v${min_version}+ required"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_node_macos() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Node.js via Homebrew..."
|
||||||
|
|
||||||
|
ensure_homebrew
|
||||||
|
|
||||||
|
# Install node@22
|
||||||
|
brew install node@22
|
||||||
|
|
||||||
|
# Link it
|
||||||
|
brew link node@22 --overwrite --force 2>/dev/null || true
|
||||||
|
|
||||||
|
# Ensure it's on PATH
|
||||||
|
local prefix
|
||||||
|
prefix=$(brew --prefix node@22 2>/dev/null)
|
||||||
|
if [[ -n "$prefix" && -d "${prefix}/bin" ]]; then
|
||||||
|
export PATH="${prefix}/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Node.js installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_node_debian() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Node.js via NodeSource..."
|
||||||
|
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp=$(create_temp_file)
|
||||||
|
|
||||||
|
# Download NodeSource setup script for Node.js 22
|
||||||
|
if ! download_file "https://deb.nodesource.com/setup_22.x" "$tmp"; then
|
||||||
|
echo -e "${ERROR}Error: Failed to download NodeSource setup script${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
maybe_sudo -E bash "$tmp"
|
||||||
|
maybe_sudo apt-get install -y nodejs
|
||||||
|
|
||||||
|
rm -f "$tmp"
|
||||||
|
echo -e "${SUCCESS}✓${NC} Node.js installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_node_fedora() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Node.js via NodeSource..."
|
||||||
|
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp=$(create_temp_file)
|
||||||
|
|
||||||
|
if ! download_file "https://rpm.nodesource.com/setup_22.x" "$tmp"; then
|
||||||
|
echo -e "${ERROR}Error: Failed to download NodeSource setup script${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
maybe_sudo bash "$tmp"
|
||||||
|
|
||||||
|
if command -v dnf &>/dev/null; then
|
||||||
|
maybe_sudo dnf install -y nodejs
|
||||||
|
else
|
||||||
|
maybe_sudo yum install -y nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$tmp"
|
||||||
|
echo -e "${SUCCESS}✓${NC} Node.js installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_node_arch() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Node.js via pacman..."
|
||||||
|
|
||||||
|
maybe_sudo pacman -Sy --noconfirm nodejs npm
|
||||||
|
echo -e "${SUCCESS}✓${NC} Node.js installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_node() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
macos)
|
||||||
|
install_node_macos
|
||||||
|
;;
|
||||||
|
debian)
|
||||||
|
install_node_debian
|
||||||
|
;;
|
||||||
|
arch)
|
||||||
|
install_node_arch
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
install_node_fedora
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${ERROR}Error: Unsupported OS for Node.js installation: $os${NC}"
|
||||||
|
echo "Please install Node.js ${MIN_NODE_VERSION}+ manually: https://nodejs.org"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_node() {
|
||||||
|
if ! check_node; then
|
||||||
|
install_node
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# pnpm
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
check_pnpm() {
|
||||||
|
if check_command pnpm; then
|
||||||
|
local version
|
||||||
|
version=$(pnpm --version 2>/dev/null)
|
||||||
|
local major
|
||||||
|
major=$(extract_major_version "$version")
|
||||||
|
|
||||||
|
if [[ "$major" -ge "$MIN_PNPM_VERSION" ]]; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} pnpm: ${INFO}v$version${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${WARN}→${NC} pnpm v$version found, but v${MIN_PNPM_VERSION}+ recommended"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo -e "${WARN}→${NC} pnpm not found"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_pnpm() {
|
||||||
|
# Try corepack first (comes with Node.js 22+)
|
||||||
|
if check_command corepack; then
|
||||||
|
echo -e "${WARN}→${NC} Installing pnpm via Corepack..."
|
||||||
|
corepack enable 2>/dev/null || true
|
||||||
|
corepack prepare pnpm@${MIN_PNPM_VERSION} --activate
|
||||||
|
echo -e "${SUCCESS}✓${NC} pnpm installed via Corepack"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall back to npm
|
||||||
|
echo -e "${WARN}→${NC} Installing pnpm via npm..."
|
||||||
|
|
||||||
|
# Fix npm permissions on Linux first
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
if [[ "$os" != "macos" ]]; then
|
||||||
|
fix_npm_permissions
|
||||||
|
fi
|
||||||
|
|
||||||
|
npm install -g pnpm@${MIN_PNPM_VERSION}
|
||||||
|
echo -e "${SUCCESS}✓${NC} pnpm installed via npm"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_pnpm() {
|
||||||
|
if ! check_pnpm; then
|
||||||
|
install_pnpm
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# npm Permissions (Linux)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
fix_npm_permissions() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
if [[ "$os" == "macos" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local npm_prefix
|
||||||
|
npm_prefix=$(npm config get prefix 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -z "$npm_prefix" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we can write to the npm prefix
|
||||||
|
if [[ -w "$npm_prefix" || -w "${npm_prefix}/lib" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Configuring npm for user-local installs..."
|
||||||
|
|
||||||
|
# Create user-local npm directory
|
||||||
|
mkdir -p "$HOME/.npm-global"
|
||||||
|
|
||||||
|
# Configure npm to use it
|
||||||
|
npm config set prefix "$HOME/.npm-global"
|
||||||
|
|
||||||
|
# Add to shell config
|
||||||
|
local rc
|
||||||
|
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
||||||
|
if [[ -f "$rc" ]]; then
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
if ! grep -q ".npm-global" "$rc" 2>/dev/null; then
|
||||||
|
echo "" >> "$rc"
|
||||||
|
echo "# Added by Mosaic Stack installer" >> "$rc"
|
||||||
|
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$rc"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Update PATH for current session
|
||||||
|
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} npm configured for user installs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get npm global bin directory
|
||||||
|
npm_global_bin_dir() {
|
||||||
|
local prefix
|
||||||
|
prefix=$(npm prefix -g 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -n "$prefix" && "$prefix" == /* ]]; then
|
||||||
|
echo "${prefix%/}/bin"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
prefix=$(npm config get prefix 2>/dev/null || true)
|
||||||
|
if [[ -n "$prefix" && "$prefix" != "undefined" && "$prefix" != "null" && "$prefix" == /* ]]; then
|
||||||
|
echo "${prefix%/}/bin"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Docker
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
check_docker() {
|
||||||
|
if ! check_command docker; then
|
||||||
|
echo -e "${WARN}→${NC} Docker not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if daemon is accessible
|
||||||
|
if ! docker info &>/dev/null; then
|
||||||
|
local error_msg
|
||||||
|
error_msg=$(docker info 2>&1)
|
||||||
|
|
||||||
|
if [[ "$error_msg" =~ "permission denied" ]]; then
|
||||||
|
echo -e "${WARN}→${NC} Docker installed but permission denied"
|
||||||
|
echo -e " ${INFO}Fix: sudo usermod -aG docker \$USER${NC}"
|
||||||
|
echo -e " Then log out and back in"
|
||||||
|
return 2
|
||||||
|
elif [[ "$error_msg" =~ "Cannot connect to the Docker daemon" ]]; then
|
||||||
|
echo -e "${WARN}→${NC} Docker installed but daemon not running"
|
||||||
|
return 3
|
||||||
|
else
|
||||||
|
echo -e "${WARN}→${NC} Docker installed but not accessible"
|
||||||
|
return 4
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local version
|
||||||
|
version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
local major
|
||||||
|
major=$(extract_major_version "$version")
|
||||||
|
|
||||||
|
if [[ "$major" -ge "$MIN_DOCKER_VERSION" ]]; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker: ${INFO}$version${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${WARN}→${NC} Docker v$version found, but v${MIN_DOCKER_VERSION}+ recommended"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_docker_compose() {
|
||||||
|
# Check for docker compose plugin first
|
||||||
|
if docker compose version &>/dev/null; then
|
||||||
|
local version
|
||||||
|
version=$(docker compose version --short 2>/dev/null || docker compose version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker Compose: ${INFO}$version (plugin)${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for standalone docker-compose
|
||||||
|
if check_command docker-compose; then
|
||||||
|
local version
|
||||||
|
version=$(docker-compose --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker Compose: ${INFO}$version (standalone)${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Docker Compose not found"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_macos() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Docker Desktop for macOS..."
|
||||||
|
|
||||||
|
ensure_homebrew
|
||||||
|
|
||||||
|
brew install --cask docker
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker Desktop installed"
|
||||||
|
echo -e "${INFO}i${NC} Please open Docker Desktop to complete setup"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_debian() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Docker via docker.com apt repo..."
|
||||||
|
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
maybe_sudo apt-get update
|
||||||
|
maybe_sudo apt-get install -y ca-certificates curl gnupg
|
||||||
|
|
||||||
|
# Add Docker's official GPG key
|
||||||
|
local keyring="/usr/share/keyrings/docker-archive-keyring.gpg"
|
||||||
|
maybe_sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg | maybe_sudo gpg --dearmor -o "$keyring"
|
||||||
|
maybe_sudo chmod a+r "$keyring"
|
||||||
|
|
||||||
|
# Add Docker repository
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=$keyring] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | \
|
||||||
|
maybe_sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
|
# Install Docker
|
||||||
|
maybe_sudo apt-get update
|
||||||
|
maybe_sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
# Start Docker
|
||||||
|
maybe_sudo systemctl enable --now docker
|
||||||
|
|
||||||
|
# Add user to docker group
|
||||||
|
maybe_sudo usermod -aG docker "$USER"
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker installed"
|
||||||
|
echo -e "${INFO}i${NC} Run 'newgrp docker' or log out/in for group membership"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_ubuntu() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Docker via docker.com apt repo..."
|
||||||
|
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
maybe_sudo apt-get update
|
||||||
|
maybe_sudo apt-get install -y ca-certificates curl gnupg
|
||||||
|
|
||||||
|
# Add Docker's official GPG key
|
||||||
|
local keyring="/usr/share/keyrings/docker-archive-keyring.gpg"
|
||||||
|
maybe_sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | maybe_sudo gpg --dearmor -o "$keyring"
|
||||||
|
maybe_sudo chmod a+r "$keyring"
|
||||||
|
|
||||||
|
# Add Docker repository
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=$keyring] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
|
||||||
|
maybe_sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
|
# Install Docker
|
||||||
|
maybe_sudo apt-get update
|
||||||
|
maybe_sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
# Start Docker
|
||||||
|
maybe_sudo systemctl enable --now docker
|
||||||
|
|
||||||
|
# Add user to docker group
|
||||||
|
maybe_sudo usermod -aG docker "$USER"
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker installed"
|
||||||
|
echo -e "${INFO}i${NC} Run 'newgrp docker' or log out/in for group membership"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_fedora() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Docker via dnf..."
|
||||||
|
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
# Add Docker repository
|
||||||
|
maybe_sudo dnf -y install dnf-plugins-core
|
||||||
|
maybe_sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
|
||||||
|
|
||||||
|
# Install Docker
|
||||||
|
maybe_sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
|
||||||
|
# Start Docker
|
||||||
|
maybe_sudo systemctl enable --now docker
|
||||||
|
|
||||||
|
# Add user to docker group
|
||||||
|
maybe_sudo usermod -aG docker "$USER"
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker installed"
|
||||||
|
echo -e "${INFO}i${NC} Run 'newgrp docker' or log out/in for group membership"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_arch() {
|
||||||
|
echo -e "${WARN}→${NC} Installing Docker via pacman..."
|
||||||
|
|
||||||
|
maybe_sudo pacman -Sy --noconfirm docker docker-compose
|
||||||
|
|
||||||
|
# Start Docker
|
||||||
|
maybe_sudo systemctl enable --now docker
|
||||||
|
|
||||||
|
# Add user to docker group
|
||||||
|
maybe_sudo usermod -aG docker "$USER"
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker installed"
|
||||||
|
echo -e "${INFO}i${NC} Run 'newgrp docker' or log out/in for group membership"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
macos)
|
||||||
|
install_docker_macos
|
||||||
|
;;
|
||||||
|
debian)
|
||||||
|
# Check if Ubuntu specifically
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
source /etc/os-release
|
||||||
|
if [[ "$ID" == "ubuntu" ]]; then
|
||||||
|
install_docker_ubuntu
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
install_docker_debian
|
||||||
|
;;
|
||||||
|
arch)
|
||||||
|
install_docker_arch
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
install_docker_fedora
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${ERROR}Error: Unsupported OS for Docker installation: $os${NC}"
|
||||||
|
echo "Please install Docker manually: https://docs.docker.com/get-docker/"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_docker() {
|
||||||
|
local check_result
|
||||||
|
check_docker
|
||||||
|
check_result=$?
|
||||||
|
|
||||||
|
if [[ $check_result -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $check_result -eq 2 ]]; then
|
||||||
|
# Permission issue - try to fix
|
||||||
|
echo -e "${WARN}→${NC} Attempting to fix Docker permissions..."
|
||||||
|
maybe_sudo usermod -aG docker "$USER"
|
||||||
|
echo -e "${INFO}i${NC} Run 'newgrp docker' or log out/in for group membership"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $check_result -eq 3 ]]; then
|
||||||
|
# Daemon not running - try to start
|
||||||
|
echo -e "${WARN}→${NC} Starting Docker daemon..."
|
||||||
|
maybe_sudo systemctl start docker
|
||||||
|
sleep 3
|
||||||
|
check_docker
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker not installed
|
||||||
|
install_docker
|
||||||
|
}
|
||||||
|
|
||||||
|
start_docker() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
if [[ "$os" == "macos" ]]; then
|
||||||
|
if ! pgrep -x "Docker Desktop" &>/dev/null; then
|
||||||
|
echo -e "${WARN}→${NC} Starting Docker Desktop..."
|
||||||
|
open -a "Docker Desktop"
|
||||||
|
sleep 10
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if ! docker info &>/dev/null; then
|
||||||
|
echo -e "${WARN}→${NC} Starting Docker daemon..."
|
||||||
|
maybe_sudo systemctl start docker
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PostgreSQL
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
check_postgres() {
|
||||||
|
if check_command psql; then
|
||||||
|
local version
|
||||||
|
version=$(psql --version 2>/dev/null | grep -oE '[0-9]+')
|
||||||
|
echo -e "${SUCCESS}✓${NC} PostgreSQL: ${INFO}v$version${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo -e "${WARN}→${NC} PostgreSQL not found"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_postgres_macos() {
|
||||||
|
echo -e "${WARN}→${NC} Installing PostgreSQL via Homebrew..."
|
||||||
|
|
||||||
|
ensure_homebrew
|
||||||
|
|
||||||
|
brew install postgresql@17
|
||||||
|
brew link postgresql@17 --overwrite --force 2>/dev/null || true
|
||||||
|
|
||||||
|
# Start PostgreSQL
|
||||||
|
brew services start postgresql@17
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} PostgreSQL installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_postgres_debian() {
|
||||||
|
echo -e "${WARN}→${NC} Installing PostgreSQL via apt..."
|
||||||
|
|
||||||
|
require_sudo
|
||||||
|
|
||||||
|
# Add PostgreSQL APT repository
|
||||||
|
maybe_sudo apt-get install -y curl ca-certificates
|
||||||
|
maybe_sudo install -d /usr/share/postgresql-common/pgdg
|
||||||
|
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | maybe_sudo gpg --dearmor -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg
|
||||||
|
|
||||||
|
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | \
|
||||||
|
maybe_sudo tee /etc/apt/sources.list.d/pgdg.list > /dev/null
|
||||||
|
|
||||||
|
maybe_sudo apt-get update
|
||||||
|
maybe_sudo apt-get install -y postgresql-17 postgresql-17-pgvector
|
||||||
|
|
||||||
|
# Start PostgreSQL
|
||||||
|
maybe_sudo systemctl enable --now postgresql
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} PostgreSQL installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_postgres_arch() {
|
||||||
|
echo -e "${WARN}→${NC} Installing PostgreSQL via pacman..."
|
||||||
|
|
||||||
|
maybe_sudo pacman -Sy --noconfirm postgresql
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
maybe_sudo -u postgres initdb -D /var/lib/postgres/data 2>/dev/null || true
|
||||||
|
|
||||||
|
# Start PostgreSQL
|
||||||
|
maybe_sudo systemctl enable --now postgresql
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} PostgreSQL installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_postgres_fedora() {
|
||||||
|
echo -e "${WARN}→${NC} Installing PostgreSQL via dnf..."
|
||||||
|
|
||||||
|
maybe_sudo dnf install -y postgresql-server postgresql-contrib
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
maybe_sudo postgresql-setup --initdb 2>/dev/null || true
|
||||||
|
|
||||||
|
# Start PostgreSQL
|
||||||
|
maybe_sudo systemctl enable --now postgresql
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} PostgreSQL installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_postgres() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
macos)
|
||||||
|
install_postgres_macos
|
||||||
|
;;
|
||||||
|
debian)
|
||||||
|
install_postgres_debian
|
||||||
|
;;
|
||||||
|
arch)
|
||||||
|
install_postgres_arch
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
install_postgres_fedora
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${ERROR}Error: Unsupported OS for PostgreSQL installation: $os${NC}"
|
||||||
|
echo "Please install PostgreSQL ${MIN_POSTGRES_VERSION}+ manually"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Dependency Summary
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check all dependencies for Docker mode
|
||||||
|
check_docker_dependencies() {
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking Docker dependencies...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Git (optional but recommended)
|
||||||
|
check_git || ((errors++))
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
local docker_result
|
||||||
|
check_docker
|
||||||
|
docker_result=$?
|
||||||
|
if [[ $docker_result -ne 0 ]]; then
|
||||||
|
((errors++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker Compose
|
||||||
|
if [[ $docker_result -eq 0 ]]; then
|
||||||
|
check_docker_compose || ((errors++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check all dependencies for Native mode
|
||||||
|
check_native_dependencies() {
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking native dependencies...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_git || ((errors++))
|
||||||
|
check_node || ((errors++))
|
||||||
|
check_pnpm || ((errors++))
|
||||||
|
check_postgres || ((errors++))
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install missing dependencies based on mode
|
||||||
|
install_dependencies() {
|
||||||
|
local mode="$1"
|
||||||
|
|
||||||
|
echo -e "${BOLD}Installing dependencies...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ensure_git
|
||||||
|
|
||||||
|
if [[ "$mode" == "docker" ]]; then
|
||||||
|
ensure_docker
|
||||||
|
start_docker
|
||||||
|
else
|
||||||
|
ensure_node
|
||||||
|
ensure_pnpm
|
||||||
|
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
if [[ "$os" != "macos" ]]; then
|
||||||
|
fix_npm_permissions
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PostgreSQL is optional for native mode (can use Docker or external)
|
||||||
|
if ! check_postgres; then
|
||||||
|
echo -e "${INFO}i${NC} PostgreSQL not installed - you can use Docker or external database"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${SUCCESS}✓${NC} Dependencies installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Package Name Mapping
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get platform-specific package name
|
||||||
|
get_package_name() {
|
||||||
|
local pkg_manager="$1"
|
||||||
|
local package="$2"
|
||||||
|
|
||||||
|
case "$pkg_manager" in
|
||||||
|
apt)
|
||||||
|
case "$package" in
|
||||||
|
docker) echo "docker-ce" ;;
|
||||||
|
docker-compose) echo "docker-compose-plugin" ;;
|
||||||
|
node) echo "nodejs" ;;
|
||||||
|
postgres) echo "postgresql-17" ;;
|
||||||
|
*) echo "$package" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
pacman)
|
||||||
|
case "$package" in
|
||||||
|
docker) echo "docker" ;;
|
||||||
|
docker-compose) echo "docker-compose" ;;
|
||||||
|
node) echo "nodejs" ;;
|
||||||
|
postgres) echo "postgresql" ;;
|
||||||
|
*) echo "$package" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
dnf)
|
||||||
|
case "$package" in
|
||||||
|
docker) echo "docker-ce" ;;
|
||||||
|
docker-compose) echo "docker-compose-plugin" ;;
|
||||||
|
node) echo "nodejs" ;;
|
||||||
|
postgres) echo "postgresql-server" ;;
|
||||||
|
*) echo "$package" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
brew)
|
||||||
|
case "$package" in
|
||||||
|
docker) echo "docker" ;;
|
||||||
|
node) echo "node@22" ;;
|
||||||
|
postgres) echo "postgresql@17" ;;
|
||||||
|
*) echo "$package" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$package"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
491
scripts/lib/docker.sh
Normal file
491
scripts/lib/docker.sh
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Docker-specific functions for Mosaic Stack installer
|
||||||
|
# Handles Docker Compose operations, health checks, and service management
|
||||||
|
|
||||||
|
# shellcheck source=lib/platform.sh
|
||||||
|
source "${BASH_SOURCE[0]%/*}/platform.sh"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Docker Compose Helpers
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get the docker compose command (handles both plugin and standalone)
|
||||||
|
docker_compose_cmd() {
|
||||||
|
if docker compose version &>/dev/null; then
|
||||||
|
echo "docker compose"
|
||||||
|
elif command -v docker-compose &>/dev/null; then
|
||||||
|
echo "docker-compose"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run docker compose with all arguments
|
||||||
|
docker_compose() {
|
||||||
|
local cmd
|
||||||
|
cmd=$(docker_compose_cmd) || {
|
||||||
|
echo -e "${ERROR}Error: Docker Compose not available${NC}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
$cmd "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Service Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Pull all images defined in docker-compose.yml
|
||||||
|
docker_pull_images() {
|
||||||
|
local compose_file="${1:-docker-compose.yml}"
|
||||||
|
local env_file="${2:-.env}"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Pulling Docker images..."
|
||||||
|
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
docker_compose -f "$compose_file" --env-file "$env_file" pull
|
||||||
|
else
|
||||||
|
docker_compose -f "$compose_file" pull
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start services with Docker Compose
|
||||||
|
docker_compose_up() {
|
||||||
|
local compose_file="${1:-docker-compose.yml}"
|
||||||
|
local env_file="${2:-.env}"
|
||||||
|
local profiles="${3:-}"
|
||||||
|
local detached="${4:-true}"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Starting services..."
|
||||||
|
|
||||||
|
local args=("-f" "$compose_file")
|
||||||
|
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
args+=("--env-file" "$env_file")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$profiles" ]]; then
|
||||||
|
args+=("--profile" "$profiles")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$detached" == "true" ]]; then
|
||||||
|
args+=("up" "-d")
|
||||||
|
else
|
||||||
|
args+=("up")
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker_compose "${args[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker_compose_down() {
|
||||||
|
local compose_file="${1:-docker-compose.yml}"
|
||||||
|
local env_file="${2:-.env}"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Stopping services..."
|
||||||
|
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
docker_compose -f "$compose_file" --env-file "$env_file" down
|
||||||
|
else
|
||||||
|
docker_compose -f "$compose_file" down
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
docker_compose_restart() {
|
||||||
|
local compose_file="${1:-docker-compose.yml}"
|
||||||
|
local env_file="${2:-.env}"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Restarting services..."
|
||||||
|
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
docker_compose -f "$compose_file" --env-file "$env_file" restart
|
||||||
|
else
|
||||||
|
docker_compose -f "$compose_file" restart
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Health Checks
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Wait for a container to be healthy
|
||||||
|
wait_for_healthy_container() {
|
||||||
|
local container_name="$1"
|
||||||
|
local timeout="${2:-120}"
|
||||||
|
local interval="${3:-5}"
|
||||||
|
|
||||||
|
echo -e "${INFO}i${NC} Waiting for ${INFO}$container_name${NC} to be healthy..."
|
||||||
|
|
||||||
|
local elapsed=0
|
||||||
|
while [[ $elapsed -lt $timeout ]]; do
|
||||||
|
local status
|
||||||
|
status=$(docker inspect --format='{{.State.Health.Status}}' "$container_name" 2>/dev/null || echo "not_found")
|
||||||
|
|
||||||
|
case "$status" in
|
||||||
|
healthy)
|
||||||
|
echo -e "${SUCCESS}✓${NC} $container_name is healthy"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
unhealthy)
|
||||||
|
echo -e "${ERROR}✗${NC} $container_name is unhealthy"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
not_found)
|
||||||
|
echo -e "${WARN}→${NC} Container $container_name not found"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
sleep "$interval"
|
||||||
|
((elapsed += interval))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${ERROR}✗${NC} Timeout waiting for $container_name to be healthy"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for multiple containers to be healthy
|
||||||
|
wait_for_healthy_containers() {
|
||||||
|
local containers=("$@")
|
||||||
|
local timeout="${containers[-1]}"
|
||||||
|
unset 'containers[-1]'
|
||||||
|
|
||||||
|
for container in "${containers[@]}"; do
|
||||||
|
if ! wait_for_healthy_container "$container" "$timeout"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for a service to respond on a port
|
||||||
|
wait_for_service() {
|
||||||
|
local host="$1"
|
||||||
|
local port="$2"
|
||||||
|
local name="$3"
|
||||||
|
local timeout="${4:-60}"
|
||||||
|
|
||||||
|
echo -e "${INFO}i${NC} Waiting for ${INFO}$name${NC} at $host:$port..."
|
||||||
|
|
||||||
|
local elapsed=0
|
||||||
|
while [[ $elapsed -lt $timeout ]]; do
|
||||||
|
if docker run --rm --network host alpine:latest nc -z "$host" "$port" 2>/dev/null; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} $name is responding"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
((elapsed += 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${ERROR}✗${NC} Timeout waiting for $name"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Container Status
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get container status
|
||||||
|
get_container_status() {
|
||||||
|
local container_name="$1"
|
||||||
|
|
||||||
|
docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null || echo "not_found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if container is running
|
||||||
|
is_container_running() {
|
||||||
|
local container_name="$1"
|
||||||
|
local status
|
||||||
|
status=$(get_container_status "$container_name")
|
||||||
|
[[ "$status" == "running" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# List all Mosaic containers
|
||||||
|
list_mosaic_containers() {
|
||||||
|
docker ps -a --filter "name=mosaic-" --format "{{.Names}}\t{{.Status}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get container logs
|
||||||
|
get_container_logs() {
|
||||||
|
local container_name="$1"
|
||||||
|
local lines="${2:-100}"
|
||||||
|
|
||||||
|
docker logs --tail "$lines" "$container_name" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tail container logs
|
||||||
|
tail_container_logs() {
|
||||||
|
local container_name="$1"
|
||||||
|
|
||||||
|
docker logs -f "$container_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Database Operations
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
wait_for_postgres() {
|
||||||
|
local container_name="${1:-mosaic-postgres}"
|
||||||
|
local user="${2:-mosaic}"
|
||||||
|
local database="${3:-mosaic}"
|
||||||
|
local timeout="${4:-60}"
|
||||||
|
|
||||||
|
echo -e "${INFO}i${NC} Waiting for PostgreSQL to be ready..."
|
||||||
|
|
||||||
|
local elapsed=0
|
||||||
|
while [[ $elapsed -lt $timeout ]]; do
|
||||||
|
if docker exec "$container_name" pg_isready -U "$user" -d "$database" &>/dev/null; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} PostgreSQL is ready"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
((elapsed += 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${ERROR}✗${NC} Timeout waiting for PostgreSQL"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run database migrations
|
||||||
|
run_database_migrations() {
|
||||||
|
local api_container="${1:-mosaic-api}"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Running database migrations..."
|
||||||
|
|
||||||
|
if ! docker exec "$api_container" npx prisma migrate deploy &>/dev/null; then
|
||||||
|
echo -e "${WARN}→${NC} Could not run migrations via API container"
|
||||||
|
echo -e "${INFO}i${NC} Migrations will run automatically when API starts"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Database migrations complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Service URLs
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get the URL for a service
|
||||||
|
get_service_url() {
|
||||||
|
local service="$1"
|
||||||
|
local port="${2:-}"
|
||||||
|
|
||||||
|
local host="localhost"
|
||||||
|
|
||||||
|
# Check if we're in WSL and need to use Windows host
|
||||||
|
if is_wsl; then
|
||||||
|
host=$(cat /etc/resolv.conf 2>/dev/null | grep nameserver | awk '{print $2}' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$port" ]]; then
|
||||||
|
echo "http://${host}:${port}"
|
||||||
|
else
|
||||||
|
echo "http://${host}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all service URLs
|
||||||
|
get_all_service_urls() {
|
||||||
|
local env_file="${1:-.env}"
|
||||||
|
|
||||||
|
declare -A urls=()
|
||||||
|
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$env_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
urls[web]="http://localhost:${WEB_PORT:-3000}"
|
||||||
|
urls[api]="http://localhost:${API_PORT:-3001}"
|
||||||
|
urls[postgres]="localhost:${POSTGRES_PORT:-5432}"
|
||||||
|
urls[valkey]="localhost:${VALKEY_PORT:-6379}"
|
||||||
|
|
||||||
|
if [[ "${OIDC_ENABLED:-false}" == "true" ]]; then
|
||||||
|
urls[authentik]="http://localhost:${AUTHENTIK_PORT_HTTP:-9000}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${OLLAMA_MODE:-disabled}" != "disabled" ]]; then
|
||||||
|
urls[ollama]="http://localhost:${OLLAMA_PORT:-11434}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for service in "${!urls[@]}"; do
|
||||||
|
echo "$service: ${urls[$service]}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Docker Cleanup
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Remove unused Docker resources
|
||||||
|
docker_cleanup() {
|
||||||
|
echo -e "${WARN}→${NC} Cleaning up unused Docker resources..."
|
||||||
|
|
||||||
|
# Remove dangling images
|
||||||
|
docker image prune -f
|
||||||
|
|
||||||
|
# Remove unused networks
|
||||||
|
docker network prune -f
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Docker cleanup complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove all Mosaic containers and volumes
|
||||||
|
docker_remove_all() {
|
||||||
|
local compose_file="${1:-docker-compose.yml}"
|
||||||
|
local env_file="${2:-.env}"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Removing all Mosaic containers and volumes..."
|
||||||
|
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
docker_compose -f "$compose_file" --env-file "$env_file" down -v --remove-orphans
|
||||||
|
else
|
||||||
|
docker_compose -f "$compose_file" down -v --remove-orphans
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} All containers and volumes removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Docker Info
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Print Docker system info
|
||||||
|
print_docker_info() {
|
||||||
|
echo -e "${BOLD}Docker Information:${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e " Docker Version:"
|
||||||
|
docker --version 2>/dev/null | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " Docker Compose:"
|
||||||
|
docker_compose version 2>/dev/null | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " Docker Storage:"
|
||||||
|
docker system df 2>/dev/null | sed 's/^/ /'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " Running Containers:"
|
||||||
|
docker ps --format " {{.Names}}\t{{.Status}}" 2>/dev/null | head -10
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Volume Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# List all Mosaic volumes
|
||||||
|
list_mosaic_volumes() {
|
||||||
|
docker volume ls --filter "name=mosaic" --format "{{.Name}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backup a Docker volume
|
||||||
|
backup_volume() {
|
||||||
|
local volume_name="$1"
|
||||||
|
local backup_file="${2:-${volume_name}-backup-$(date +%Y%m%d-%H%M%S).tar.gz}"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Backing up volume ${INFO}$volume_name${NC}..."
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$volume_name":/source:ro \
|
||||||
|
-v "$(pwd)":/backup \
|
||||||
|
alpine:latest \
|
||||||
|
tar czf "/backup/$backup_file" -C /source .
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Backup created: $backup_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restore a Docker volume
|
||||||
|
restore_volume() {
|
||||||
|
local volume_name="$1"
|
||||||
|
local backup_file="$2"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Restoring volume ${INFO}$volume_name${NC} from $backup_file..."
|
||||||
|
|
||||||
|
# Create volume if it doesn't exist
|
||||||
|
docker volume create "$volume_name" &>/dev/null || true
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$volume_name":/target \
|
||||||
|
-v "$(pwd)":/backup \
|
||||||
|
alpine:latest \
|
||||||
|
tar xzf "/backup/$backup_file" -C /target
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} Volume restored"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Network Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Create a Docker network if it doesn't exist
|
||||||
|
ensure_network() {
|
||||||
|
local network_name="$1"
|
||||||
|
|
||||||
|
if ! docker network inspect "$network_name" &>/dev/null; then
|
||||||
|
echo -e "${WARN}→${NC} Creating network ${INFO}$network_name${NC}..."
|
||||||
|
docker network create "$network_name"
|
||||||
|
echo -e "${SUCCESS}✓${NC} Network created"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a network exists
|
||||||
|
network_exists() {
|
||||||
|
local network_name="$1"
|
||||||
|
docker network inspect "$network_name" &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Build Operations
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Build Docker images
|
||||||
|
docker_build() {
|
||||||
|
local compose_file="${1:-docker-compose.yml}"
|
||||||
|
local env_file="${2:-.env}"
|
||||||
|
local parallel="${3:-true}"
|
||||||
|
|
||||||
|
echo -e "${WARN}→${NC} Building Docker images..."
|
||||||
|
|
||||||
|
local args=("-f" "$compose_file")
|
||||||
|
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
args+=("--env-file" "$env_file")
|
||||||
|
fi
|
||||||
|
|
||||||
|
args+=("build")
|
||||||
|
|
||||||
|
if [[ "$parallel" == "true" ]]; then
|
||||||
|
args+=("--parallel")
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker_compose "${args[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if buildx is available
|
||||||
|
check_buildx() {
|
||||||
|
docker buildx version &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up buildx builder
|
||||||
|
setup_buildx() {
|
||||||
|
if ! check_buildx; then
|
||||||
|
echo -e "${WARN}→${NC} buildx not available"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create or use existing builder
|
||||||
|
if ! docker buildx inspect mosaic-builder &>/dev/null; then
|
||||||
|
echo -e "${WARN}→${NC} Creating buildx builder..."
|
||||||
|
docker buildx create --name mosaic-builder --use
|
||||||
|
else
|
||||||
|
docker buildx use mosaic-builder
|
||||||
|
fi
|
||||||
|
}
|
||||||
665
scripts/lib/platform.sh
Normal file
665
scripts/lib/platform.sh
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Platform detection functions for Mosaic Stack installer
|
||||||
|
# Provides OS, package manager, architecture, and environment detection
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Colors (if terminal supports them)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
BOLD='\033[1m'
|
||||||
|
ACCENT='\033[38;2;128;90;213m'
|
||||||
|
SUCCESS='\033[38;2;47;191;113m'
|
||||||
|
WARN='\033[38;2;255;176;32m'
|
||||||
|
ERROR='\033[38;2;226;61;45m'
|
||||||
|
INFO='\033[38;2;100;149;237m'
|
||||||
|
MUTED='\033[38;2;139;127;119m'
|
||||||
|
NC='\033[0m'
|
||||||
|
else
|
||||||
|
BOLD=''
|
||||||
|
ACCENT=''
|
||||||
|
SUCCESS=''
|
||||||
|
WARN=''
|
||||||
|
ERROR=''
|
||||||
|
INFO=''
|
||||||
|
MUTED=''
|
||||||
|
NC=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OS Detection
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Detect operating system type
|
||||||
|
# Returns: macos, debian, arch, fedora, linux, unknown
|
||||||
|
detect_os() {
|
||||||
|
case "$OSTYPE" in
|
||||||
|
darwin*)
|
||||||
|
echo "macos"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
linux-gnu*)
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source /etc/os-release
|
||||||
|
case "$ID" in
|
||||||
|
ubuntu|debian|linuxmint|pop|elementary)
|
||||||
|
echo "debian"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
arch|manjaro|endeavouros|garuda|arcolinux)
|
||||||
|
echo "arch"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
fedora|rhel|centos|rocky|almalinux|ol)
|
||||||
|
echo "fedora"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "linux"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "linux"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect if running under WSL (Windows Subsystem for Linux)
|
||||||
|
# Returns WSL_DISTRO_NAME if in WSL, empty string otherwise
|
||||||
|
detect_wsl() {
|
||||||
|
if [[ -n "${WSL_DISTRO_NAME:-}" ]]; then
|
||||||
|
echo "$WSL_DISTRO_NAME"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for WSL in /proc/version
|
||||||
|
if [[ -f /proc/version ]]; then
|
||||||
|
if grep -qi "microsoft\|wsl" /proc/version 2>/dev/null; then
|
||||||
|
# Try to get distro name from os-release
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source /etc/os-release
|
||||||
|
echo "${NAME:-WSL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "WSL"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if running in WSL
|
||||||
|
is_wsl() {
|
||||||
|
[[ -n "${WSL_DISTRO_NAME:-}" ]] && return 0
|
||||||
|
[[ -f /proc/version ]] && grep -qi "microsoft\|wsl" /proc/version 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get human-readable OS name
|
||||||
|
get_os_name() {
|
||||||
|
local os="$1"
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
macos)
|
||||||
|
echo "macOS"
|
||||||
|
;;
|
||||||
|
debian)
|
||||||
|
echo "Debian/Ubuntu"
|
||||||
|
;;
|
||||||
|
arch)
|
||||||
|
echo "Arch Linux"
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
echo "Fedora/RHEL"
|
||||||
|
;;
|
||||||
|
linux)
|
||||||
|
echo "Linux"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Package Manager Detection
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Detect the system's package manager
|
||||||
|
# Returns: brew, apt, pacman, dnf, yum, unknown
|
||||||
|
detect_package_manager() {
|
||||||
|
local os="$1"
|
||||||
|
|
||||||
|
# First check for Homebrew (available on macOS and Linux)
|
||||||
|
if command -v brew &>/dev/null; then
|
||||||
|
echo "brew"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall back to OS-specific package managers
|
||||||
|
case "$os" in
|
||||||
|
macos)
|
||||||
|
# macOS without Homebrew
|
||||||
|
echo "none"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
debian)
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
echo "apt"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
arch)
|
||||||
|
if command -v pacman &>/dev/null; then
|
||||||
|
echo "pacman"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
if command -v dnf &>/dev/null; then
|
||||||
|
echo "dnf"
|
||||||
|
return 0
|
||||||
|
elif command -v yum &>/dev/null; then
|
||||||
|
echo "yum"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "unknown"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Architecture Detection
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get system architecture
|
||||||
|
# Returns: x86_64, aarch64, armv7l, armv6l, unknown
|
||||||
|
get_arch() {
|
||||||
|
local arch
|
||||||
|
arch=$(uname -m)
|
||||||
|
|
||||||
|
case "$arch" in
|
||||||
|
x86_64|amd64)
|
||||||
|
echo "x86_64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
echo "aarch64"
|
||||||
|
;;
|
||||||
|
armv7l|armhf)
|
||||||
|
echo "armv7l"
|
||||||
|
;;
|
||||||
|
armv6l)
|
||||||
|
echo "armv6l"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if running on Apple Silicon
|
||||||
|
is_apple_silicon() {
|
||||||
|
[[ "$(detect_os)" == "macos" ]] && [[ "$(get_arch)" == "aarch64" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Init System Detection
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Detect the init system
|
||||||
|
# Returns: systemd, openrc, launchd, sysvinit, unknown
|
||||||
|
detect_init_system() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
macos)
|
||||||
|
echo "launchd"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check for systemd
|
||||||
|
if command -v systemctl &>/dev/null && pidof systemd &>/dev/null; then
|
||||||
|
echo "systemd"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for OpenRC
|
||||||
|
if command -v rc-status &>/dev/null; then
|
||||||
|
echo "openrc"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for SysVinit
|
||||||
|
if command -v service &>/dev/null; then
|
||||||
|
echo "sysvinit"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "unknown"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Privilege Helpers
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
is_root() {
|
||||||
|
[[ "$(id -u)" -eq 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run command with sudo only if not already root
|
||||||
|
maybe_sudo() {
|
||||||
|
if is_root; then
|
||||||
|
# Skip -E flag when root (env is already preserved)
|
||||||
|
if [[ "${1:-}" == "-E" ]]; then
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure sudo is available (Linux only)
|
||||||
|
require_sudo() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
if [[ "$os" == "macos" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_root; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sudo &>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${ERROR}Error: sudo is required for system installs on Linux${NC}"
|
||||||
|
echo "Install sudo or re-run as root."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate sudo credentials (cache them early)
|
||||||
|
validate_sudo() {
|
||||||
|
if is_root; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sudo &>/dev/null; then
|
||||||
|
sudo -v 2>/dev/null
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TTY and Interactive Detection
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check if running in an interactive terminal
|
||||||
|
is_interactive() {
|
||||||
|
[[ -t 0 && -t 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if we can prompt the user
|
||||||
|
is_promptable() {
|
||||||
|
[[ -r /dev/tty && -w /dev/tty ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read input from TTY (for prompts when stdin is piped)
|
||||||
|
read_from_tty() {
|
||||||
|
local prompt="$1"
|
||||||
|
local var_name="$2"
|
||||||
|
|
||||||
|
if is_promptable; then
|
||||||
|
echo -e "$prompt" > /dev/tty
|
||||||
|
read -r "$var_name" < /dev/tty
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Shell Detection
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get current shell name
|
||||||
|
get_shell() {
|
||||||
|
basename "${SHELL:-/bin/sh}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get shell configuration file
|
||||||
|
get_shell_rc() {
|
||||||
|
local shell
|
||||||
|
shell=$(get_shell)
|
||||||
|
|
||||||
|
case "$shell" in
|
||||||
|
zsh)
|
||||||
|
echo "$HOME/.zshrc"
|
||||||
|
;;
|
||||||
|
bash)
|
||||||
|
echo "$HOME/.bashrc"
|
||||||
|
;;
|
||||||
|
fish)
|
||||||
|
echo "$HOME/.config/fish/config.fish"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$HOME/.profile"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Network and Connectivity
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check if we have internet connectivity
|
||||||
|
has_internet() {
|
||||||
|
local timeout="${1:-5}"
|
||||||
|
|
||||||
|
# Try to reach common endpoints
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
curl -s --max-time "$timeout" https://api.github.com &>/dev/null
|
||||||
|
return $?
|
||||||
|
elif command -v wget &>/dev/null; then
|
||||||
|
wget -q --timeout="$timeout" --spider https://api.github.com
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall back to ping
|
||||||
|
ping -c 1 -W "$timeout" 8.8.8.8 &>/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get local IP address
|
||||||
|
get_local_ip() {
|
||||||
|
local ip
|
||||||
|
|
||||||
|
# Try hostname command first (macOS)
|
||||||
|
if command -v hostname &>/dev/null; then
|
||||||
|
ip=$(hostname -I 2>/dev/null | cut -d' ' -f1)
|
||||||
|
if [[ -n "$ip" ]]; then
|
||||||
|
echo "$ip"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try ip command (Linux)
|
||||||
|
if command -v ip &>/dev/null; then
|
||||||
|
ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}')
|
||||||
|
if [[ -n "$ip" ]]; then
|
||||||
|
echo "$ip"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try ifconfig
|
||||||
|
if command -v ifconfig &>/dev/null; then
|
||||||
|
ip=$(ifconfig 2>/dev/null | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1)
|
||||||
|
if [[ -n "$ip" ]]; then
|
||||||
|
echo "$ip"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# System Resources
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get total RAM in MB
|
||||||
|
get_total_ram() {
|
||||||
|
local os
|
||||||
|
os=$(detect_os)
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
macos)
|
||||||
|
sysctl -n hw.memsize 2>/dev/null | awk '{print int($1/1024/1024)}'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -f /proc/meminfo ]]; then
|
||||||
|
awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo
|
||||||
|
else
|
||||||
|
echo "0"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get available disk space in GB for a path
|
||||||
|
get_available_disk() {
|
||||||
|
local path="${1:-.}"
|
||||||
|
|
||||||
|
if command -v df &>/dev/null; then
|
||||||
|
df -BG "$path" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G'
|
||||||
|
else
|
||||||
|
echo "0"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if system meets minimum requirements
|
||||||
|
check_minimum_requirements() {
|
||||||
|
local min_ram="${1:-2048}" # MB
|
||||||
|
local min_disk="${2:-10}" # GB
|
||||||
|
|
||||||
|
local ram disk
|
||||||
|
ram=$(get_total_ram)
|
||||||
|
disk=$(get_available_disk "$HOME")
|
||||||
|
|
||||||
|
local errors=()
|
||||||
|
|
||||||
|
if [[ "$ram" -lt "$min_ram" ]]; then
|
||||||
|
errors+=("RAM: ${ram}MB (minimum: ${min_ram}MB)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$disk" -lt "$min_disk" ]]; then
|
||||||
|
errors+=("Disk: ${disk}GB (minimum: ${min_disk}GB)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#errors[@]} -gt 0 ]]; then
|
||||||
|
echo "System does not meet minimum requirements:"
|
||||||
|
printf ' - %s\n' "${errors[@]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Downloader Detection
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
DOWNLOADER=""
|
||||||
|
|
||||||
|
detect_downloader() {
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
DOWNLOADER="curl"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v wget &>/dev/null; then
|
||||||
|
DOWNLOADER="wget"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo -e "${ERROR}Error: Missing downloader (curl or wget required)${NC}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download a file securely
|
||||||
|
download_file() {
|
||||||
|
local url="$1"
|
||||||
|
local output="$2"
|
||||||
|
|
||||||
|
if [[ -z "$DOWNLOADER" ]]; then
|
||||||
|
detect_downloader || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DOWNLOADER" == "curl" ]]; then
|
||||||
|
curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download and execute a script
|
||||||
|
run_remote_script() {
|
||||||
|
local url="$1"
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
|
||||||
|
if ! download_file "$url" "$tmp"; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
/bin/bash "$tmp"
|
||||||
|
local ret=$?
|
||||||
|
rm -f "$tmp"
|
||||||
|
return $ret
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PATH Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Store original PATH for later comparison
|
||||||
|
ORIGINAL_PATH="${PATH:-}"
|
||||||
|
|
||||||
|
# Check if a directory is in PATH
|
||||||
|
path_has_dir() {
|
||||||
|
local path="$1"
|
||||||
|
local dir="${2%/}"
|
||||||
|
|
||||||
|
[[ -z "$dir" ]] && return 1
|
||||||
|
|
||||||
|
case ":${path}:" in
|
||||||
|
*":${dir}:"*) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add directory to PATH in shell config
|
||||||
|
add_to_path() {
|
||||||
|
local dir="$1"
|
||||||
|
local rc="${2:-$(get_shell_rc)}"
|
||||||
|
|
||||||
|
if [[ ! -f "$rc" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
local path_line="export PATH=\"${dir}:\$PATH\""
|
||||||
|
|
||||||
|
if ! grep -qF "$dir" "$rc" 2>/dev/null; then
|
||||||
|
echo "" >> "$rc"
|
||||||
|
echo "# Added by Mosaic Stack installer" >> "$rc"
|
||||||
|
echo "$path_line" >> "$rc"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Warn about missing PATH entries
|
||||||
|
warn_path_missing() {
|
||||||
|
local dir="$1"
|
||||||
|
local label="$2"
|
||||||
|
|
||||||
|
if [[ -z "$dir" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if path_has_dir "$ORIGINAL_PATH" "$dir"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${WARN}→${NC} PATH warning: missing ${label}: ${INFO}${dir}${NC}"
|
||||||
|
echo -e "This can make commands show as \"not found\" in new terminals."
|
||||||
|
echo -e "Fix by adding to your shell config:"
|
||||||
|
echo -e " export PATH=\"${dir}:\$PATH\""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Temp File Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
TMPFILES=()
|
||||||
|
|
||||||
|
# Create a tracked temp file
|
||||||
|
create_temp_file() {
|
||||||
|
local f
|
||||||
|
f=$(mktemp)
|
||||||
|
TMPFILES+=("$f")
|
||||||
|
echo "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a tracked temp directory
|
||||||
|
create_temp_dir() {
|
||||||
|
local d
|
||||||
|
d=$(mktemp -d)
|
||||||
|
TMPFILES+=("$d")
|
||||||
|
echo "$d"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup all temp files
|
||||||
|
cleanup_temp_files() {
|
||||||
|
local f
|
||||||
|
for f in "${TMPFILES[@]:-}"; do
|
||||||
|
rm -rf "$f" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up cleanup trap
|
||||||
|
setup_cleanup_trap() {
|
||||||
|
trap cleanup_temp_files EXIT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Platform Summary
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Print a summary of the detected platform
|
||||||
|
print_platform_summary() {
|
||||||
|
local os pkg arch init wsl
|
||||||
|
|
||||||
|
os=$(detect_os)
|
||||||
|
pkg=$(detect_package_manager "$os")
|
||||||
|
arch=$(get_arch)
|
||||||
|
init=$(detect_init_system)
|
||||||
|
wsl=$(detect_wsl)
|
||||||
|
|
||||||
|
echo -e "${BOLD}Platform Detection:${NC}"
|
||||||
|
echo -e " OS: ${INFO}$(get_os_name "$os")${NC}"
|
||||||
|
echo -e " Architecture: ${INFO}$arch${NC}"
|
||||||
|
echo -e " Package Mgr: ${INFO}$pkg${NC}"
|
||||||
|
echo -e " Init System: ${INFO}$init${NC}"
|
||||||
|
|
||||||
|
if [[ -n "$wsl" ]]; then
|
||||||
|
echo -e " WSL: ${INFO}$wsl${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e " Shell: ${INFO}$(get_shell)${NC}"
|
||||||
|
echo -e " RAM: ${INFO}$(get_total_ram)MB${NC}"
|
||||||
|
echo -e " Disk: ${INFO}$(get_available_disk)GB available${NC}"
|
||||||
|
}
|
||||||
747
scripts/lib/validation.sh
Normal file
747
scripts/lib/validation.sh
Normal file
@@ -0,0 +1,747 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Validation functions for Mosaic Stack installer
|
||||||
|
# Post-install validation and health checks
|
||||||
|
|
||||||
|
# shellcheck source=lib/platform.sh
|
||||||
|
source "${BASH_SOURCE[0]%/*}/platform.sh"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Validation Result Codes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
readonly CHECK_PASS=0
|
||||||
|
readonly CHECK_WARN=1
|
||||||
|
readonly CHECK_FAIL=2
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Port Validation
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check if a port is in use
|
||||||
|
check_port_in_use() {
|
||||||
|
local port="$1"
|
||||||
|
|
||||||
|
# Try ss first (most common on modern Linux)
|
||||||
|
if command -v ss &>/dev/null; then
|
||||||
|
ss -tuln 2>/dev/null | grep -q ":${port} "
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall back to netstat
|
||||||
|
if command -v netstat &>/dev/null; then
|
||||||
|
netstat -tuln 2>/dev/null | grep -q ":${port} "
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall back to lsof
|
||||||
|
if command -v lsof &>/dev/null; then
|
||||||
|
lsof -i ":$port" &>/dev/null
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Can't check, assume port is free
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get process using a port
|
||||||
|
get_process_on_port() {
|
||||||
|
local port="$1"
|
||||||
|
|
||||||
|
if command -v lsof &>/dev/null; then
|
||||||
|
lsof -i ":$port" -t 2>/dev/null | head -1
|
||||||
|
elif command -v ss &>/dev/null; then
|
||||||
|
ss -tulnp 2>/dev/null | grep ":${port} " | grep -oP 'pid=\K[0-9]+' | head -1
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate port number
|
||||||
|
validate_port() {
|
||||||
|
local port="$1"
|
||||||
|
|
||||||
|
if [[ "$port" =~ ^[0-9]+$ ]] && [[ "$port" -ge 1 ]] && [[ "$port" -le 65535 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check all configured ports
|
||||||
|
check_all_ports() {
|
||||||
|
local env_file="${1:-.env}"
|
||||||
|
local errors=0
|
||||||
|
local warnings=0
|
||||||
|
|
||||||
|
# Load env file if it exists
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$env_file" 2>/dev/null || true
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Default ports
|
||||||
|
declare -A default_ports=(
|
||||||
|
[WEB_PORT]=3000
|
||||||
|
[API_PORT]=3001
|
||||||
|
[POSTGRES_PORT]=5432
|
||||||
|
[VALKEY_PORT]=6379
|
||||||
|
[AUTHENTIK_PORT_HTTP]=9000
|
||||||
|
[AUTHENTIK_PORT_HTTPS]=9443
|
||||||
|
[OLLAMA_PORT]=11434
|
||||||
|
[TRAEFIK_HTTP_PORT]=80
|
||||||
|
[TRAEFIK_HTTPS_PORT]=443
|
||||||
|
[TRAEFIK_DASHBOARD_PORT]=8080
|
||||||
|
)
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking ports...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for port_var in "${!default_ports[@]}"; do
|
||||||
|
local port="${!port_var:-${default_ports[$port_var]}}"
|
||||||
|
|
||||||
|
if check_port_in_use "$port"; then
|
||||||
|
local process
|
||||||
|
process=$(get_process_on_port "$port")
|
||||||
|
echo -e "${WARN}⚠${NC} $port_var: Port $port is in use (PID: $process)"
|
||||||
|
((warnings++))
|
||||||
|
else
|
||||||
|
echo -e "${SUCCESS}✓${NC} $port_var: Port $port available"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $warnings -gt 0 ]]; then
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
return $CHECK_PASS
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Environment Validation
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Required environment variables
|
||||||
|
REQUIRED_ENV_VARS=(
|
||||||
|
"DATABASE_URL"
|
||||||
|
"JWT_SECRET"
|
||||||
|
"BETTER_AUTH_SECRET"
|
||||||
|
"ENCRYPTION_KEY"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional but recommended environment variables
|
||||||
|
RECOMMENDED_ENV_VARS=(
|
||||||
|
"POSTGRES_PASSWORD"
|
||||||
|
"VALKEY_URL"
|
||||||
|
"NEXT_PUBLIC_API_URL"
|
||||||
|
"NEXT_PUBLIC_APP_URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if env file exists
|
||||||
|
check_env_file() {
|
||||||
|
local env_file="${1:-.env}"
|
||||||
|
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} .env file exists"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${ERROR}✗${NC} .env file not found"
|
||||||
|
return $CHECK_FAIL
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check required environment variables
|
||||||
|
check_required_env() {
|
||||||
|
local env_file="${1:-.env}"
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking required environment variables...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Load env file
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$env_file" 2>/dev/null || true
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
for var in "${REQUIRED_ENV_VARS[@]}"; do
|
||||||
|
local value="${!var:-}"
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
echo -e "${ERROR}✗${NC} $var: Not set"
|
||||||
|
((errors++))
|
||||||
|
elif is_placeholder "$value"; then
|
||||||
|
echo -e "${WARN}⚠${NC} $var: Contains placeholder value"
|
||||||
|
((errors++))
|
||||||
|
else
|
||||||
|
echo -e "${SUCCESS}✓${NC} $var: Set"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
return $CHECK_FAIL
|
||||||
|
fi
|
||||||
|
return $CHECK_PASS
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check recommended environment variables
|
||||||
|
check_recommended_env() {
|
||||||
|
local env_file="${1:-.env}"
|
||||||
|
local warnings=0
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking recommended environment variables...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Load env file
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$env_file" 2>/dev/null || true
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
for var in "${RECOMMENDED_ENV_VARS[@]}"; do
|
||||||
|
local value="${!var:-}"
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
echo -e "${WARN}⚠${NC} $var: Not set (using default)"
|
||||||
|
((warnings++))
|
||||||
|
elif is_placeholder "$value"; then
|
||||||
|
echo -e "${WARN}⚠${NC} $var: Contains placeholder value"
|
||||||
|
((warnings++))
|
||||||
|
else
|
||||||
|
echo -e "${SUCCESS}✓${NC} $var: Set"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $warnings -gt 0 ]]; then
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
return $CHECK_PASS
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a value is a placeholder
|
||||||
|
is_placeholder() {
|
||||||
|
local value="$1"
|
||||||
|
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Common placeholder patterns
|
||||||
|
case "$value" in
|
||||||
|
*"REPLACE_WITH"*|*"CHANGE_ME"*|*"changeme"*|*"your-"*|*"example"*|*"placeholder"*|*"TODO"*|*"FIXME"*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*"xxx"*|*"<"*">"*|*"\${"*|*"$${"*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Secret Validation
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Minimum secret lengths
|
||||||
|
declare -A MIN_SECRET_LENGTHS=(
|
||||||
|
[JWT_SECRET]=32
|
||||||
|
[BETTER_AUTH_SECRET]=32
|
||||||
|
[ENCRYPTION_KEY]=64
|
||||||
|
[AUTHENTIK_SECRET_KEY]=50
|
||||||
|
[COORDINATOR_API_KEY]=32
|
||||||
|
[ORCHESTRATOR_API_KEY]=32
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check secret strength
|
||||||
|
check_secrets() {
|
||||||
|
local env_file="${1:-.env}"
|
||||||
|
local errors=0
|
||||||
|
local warnings=0
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking secret strength...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Load env file
|
||||||
|
if [[ -f "$env_file" ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$env_file" 2>/dev/null || true
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
for secret_var in "${!MIN_SECRET_LENGTHS[@]}"; do
|
||||||
|
local value="${!secret_var:-}"
|
||||||
|
local min_len="${MIN_SECRET_LENGTHS[$secret_var]}"
|
||||||
|
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
echo -e "${WARN}⚠${NC} $secret_var: Not set"
|
||||||
|
((warnings++))
|
||||||
|
elif is_placeholder "$value"; then
|
||||||
|
echo -e "${ERROR}✗${NC} $secret_var: Contains placeholder (MUST change)"
|
||||||
|
((errors++))
|
||||||
|
elif [[ ${#value} -lt $min_len ]]; then
|
||||||
|
echo -e "${WARN}⚠${NC} $secret_var: Too short (${#value} chars, minimum $min_len)"
|
||||||
|
((warnings++))
|
||||||
|
else
|
||||||
|
echo -e "${SUCCESS}✓${NC} $secret_var: Strong (${#value} chars)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
return $CHECK_FAIL
|
||||||
|
fi
|
||||||
|
if [[ $warnings -gt 0 ]]; then
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
return $CHECK_PASS
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Docker Validation
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check Docker containers are running
|
||||||
|
check_docker_containers() {
|
||||||
|
local compose_file="${1:-docker-compose.yml}"
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking Docker containers...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Expected container names
|
||||||
|
local containers=("mosaic-postgres" "mosaic-valkey" "mosaic-api" "mosaic-web")
|
||||||
|
|
||||||
|
for container in "${containers[@]}"; do
|
||||||
|
local status
|
||||||
|
status=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null || echo "not_found")
|
||||||
|
|
||||||
|
case "$status" in
|
||||||
|
running)
|
||||||
|
echo -e "${SUCCESS}✓${NC} $container: Running"
|
||||||
|
;;
|
||||||
|
exited)
|
||||||
|
echo -e "${ERROR}✗${NC} $container: Exited"
|
||||||
|
((errors++))
|
||||||
|
;;
|
||||||
|
not_found)
|
||||||
|
# Container might not be in current profile
|
||||||
|
echo -e "${MUTED}○${NC} $container: Not found (may not be in profile)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${WARN}⚠${NC} $container: $status"
|
||||||
|
((errors++))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
return $CHECK_FAIL
|
||||||
|
fi
|
||||||
|
return $CHECK_PASS
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check container health
|
||||||
|
check_container_health() {
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking container health...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get all mosaic containers
|
||||||
|
local containers
|
||||||
|
containers=$(docker ps --filter "name=mosaic-" --format "{{.Names}}" 2>/dev/null)
|
||||||
|
|
||||||
|
for container in $containers; do
|
||||||
|
local health
|
||||||
|
health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "no_healthcheck")
|
||||||
|
|
||||||
|
case "$health" in
|
||||||
|
healthy)
|
||||||
|
echo -e "${SUCCESS}✓${NC} $container: Healthy"
|
||||||
|
;;
|
||||||
|
unhealthy)
|
||||||
|
echo -e "${ERROR}✗${NC} $container: Unhealthy"
|
||||||
|
((errors++))
|
||||||
|
;;
|
||||||
|
starting)
|
||||||
|
echo -e "${WARN}⚠${NC} $container: Starting..."
|
||||||
|
;;
|
||||||
|
no_healthcheck)
|
||||||
|
echo -e "${INFO}ℹ${NC} $container: No health check"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${WARN}⚠${NC} $container: $health"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
return $CHECK_FAIL
|
||||||
|
fi
|
||||||
|
return $CHECK_PASS
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Service Connectivity
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check if a URL responds
|
||||||
|
check_url_responds() {
|
||||||
|
local url="$1"
|
||||||
|
local expected_status="${2:-200}"
|
||||||
|
local timeout="${3:-10}"
|
||||||
|
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
local status
|
||||||
|
status=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$timeout" "$url" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ "$status" == "$expected_status" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check API health endpoint
|
||||||
|
check_api_health() {
|
||||||
|
local api_url="${1:-http://localhost:3001}"
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking API health...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if check_url_responds "${api_url}/health" 200 10; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} API health check passed"
|
||||||
|
return $CHECK_PASS
|
||||||
|
else
|
||||||
|
echo -e "${ERROR}✗${NC} API health check failed"
|
||||||
|
return $CHECK_FAIL
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Web frontend
|
||||||
|
check_web_health() {
|
||||||
|
local web_url="${1:-http://localhost:3000}"
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking Web frontend...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if check_url_responds "$web_url" 200 10; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} Web frontend responding"
|
||||||
|
return $CHECK_PASS
|
||||||
|
else
|
||||||
|
echo -e "${WARN}⚠${NC} Web frontend not responding (may still be starting)"
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check database connectivity
|
||||||
|
check_database_connection() {
|
||||||
|
local host="${1:-localhost}"
|
||||||
|
local port="${2:-5432}"
|
||||||
|
local user="${3:-mosaic}"
|
||||||
|
local database="${4:-mosaic}"
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking database connection...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Try via Docker if postgres container exists
|
||||||
|
if docker exec mosaic-postgres pg_isready -U "$user" -d "$database" &>/dev/null; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} Database connection successful"
|
||||||
|
return $CHECK_PASS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try via psql if available
|
||||||
|
if command -v psql &>/dev/null; then
|
||||||
|
if PGPASSWORD="${POSTGRES_PASSWORD:-}" psql -h "$host" -p "$port" -U "$user" -d "$database" -c "SELECT 1" &>/dev/null; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} Database connection successful"
|
||||||
|
return $CHECK_PASS
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try via TCP
|
||||||
|
if command -v nc &>/dev/null; then
|
||||||
|
if nc -z "$host" "$port" 2>/dev/null; then
|
||||||
|
echo -e "${WARN}⚠${NC} Database port open but could not verify connection"
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${ERROR}✗${NC} Database connection failed"
|
||||||
|
return $CHECK_FAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Valkey/Redis connectivity
|
||||||
|
check_valkey_connection() {
|
||||||
|
local host="${1:-localhost}"
|
||||||
|
local port="${2:-6379}"
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking Valkey/Redis connection...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Try via Docker if valkey container exists
|
||||||
|
if docker exec mosaic-valkey valkey-cli ping 2>/dev/null | grep -q PONG; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} Valkey connection successful"
|
||||||
|
return $CHECK_PASS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try via redis-cli if available
|
||||||
|
if command -v redis-cli &>/dev/null; then
|
||||||
|
if redis-cli -h "$host" -p "$port" ping 2>/dev/null | grep -q PONG; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} Valkey/Redis connection successful"
|
||||||
|
return $CHECK_PASS
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try via TCP
|
||||||
|
if command -v nc &>/dev/null; then
|
||||||
|
if nc -z "$host" "$port" 2>/dev/null; then
|
||||||
|
echo -e "${WARN}⚠${NC} Valkey port open but could not verify connection"
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${ERROR}✗${NC} Valkey/Redis connection failed"
|
||||||
|
return $CHECK_FAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# System Requirements
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check minimum system requirements
|
||||||
|
check_system_requirements() {
|
||||||
|
local min_ram="${1:-2048}"
|
||||||
|
local min_disk="${2:-10}"
|
||||||
|
local errors=0
|
||||||
|
local warnings=0
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking system requirements...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# RAM check
|
||||||
|
local ram
|
||||||
|
ram=$(get_total_ram)
|
||||||
|
if [[ "$ram" -lt "$min_ram" ]]; then
|
||||||
|
echo -e "${ERROR}✗${NC} RAM: ${ram}MB (minimum: ${min_ram}MB)"
|
||||||
|
((errors++))
|
||||||
|
else
|
||||||
|
echo -e "${SUCCESS}✓${NC} RAM: ${ram}MB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disk check
|
||||||
|
local disk
|
||||||
|
disk=$(get_available_disk "$HOME")
|
||||||
|
if [[ "$disk" -lt "$min_disk" ]]; then
|
||||||
|
echo -e "${WARN}⚠${NC} Disk: ${disk}GB available (recommended: ${min_disk}GB+)"
|
||||||
|
((warnings++))
|
||||||
|
else
|
||||||
|
echo -e "${SUCCESS}✓${NC} Disk: ${disk}GB available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker disk (if using Docker)
|
||||||
|
if command -v docker &>/dev/null && docker info &>/dev/null; then
|
||||||
|
local docker_disk
|
||||||
|
docker_disk=$(docker system df --format "{{.Total}}" 2>/dev/null | head -1 || echo "unknown")
|
||||||
|
echo -e "${INFO}ℹ${NC} Docker storage: $docker_disk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
return $CHECK_FAIL
|
||||||
|
fi
|
||||||
|
if [[ $warnings -gt 0 ]]; then
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
return $CHECK_PASS
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# File Permissions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check .env file permissions
|
||||||
|
check_env_permissions() {
|
||||||
|
local env_file="${1:-.env}"
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking file permissions...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ ! -f "$env_file" ]]; then
|
||||||
|
echo -e "${WARN}⚠${NC} .env file not found"
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
|
||||||
|
local perms
|
||||||
|
perms=$(stat -c "%a" "$env_file" 2>/dev/null || stat -f "%OLp" "$env_file" 2>/dev/null)
|
||||||
|
|
||||||
|
# Check if world-readable
|
||||||
|
if [[ "$perms" =~ [0-7][0-7][4-7]$ ]]; then
|
||||||
|
echo -e "${WARN}⚠${NC} .env is world-readable (permissions: $perms)"
|
||||||
|
echo -e " ${INFO}Fix: chmod 600 $env_file${NC}"
|
||||||
|
return $CHECK_WARN
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${SUCCESS}✓${NC} .env permissions: $perms"
|
||||||
|
return $CHECK_PASS
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Comprehensive Doctor Check
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Run all checks and report results
|
||||||
|
run_doctor() {
|
||||||
|
local env_file="${1:-.env}"
|
||||||
|
local compose_file="${2:-docker-compose.yml}"
|
||||||
|
local mode="${3:-docker}"
|
||||||
|
|
||||||
|
local errors=0
|
||||||
|
local warnings=0
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${BOLD} Mosaic Stack Doctor${NC}"
|
||||||
|
echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# System requirements
|
||||||
|
run_doctor_check "System Requirements" check_system_requirements 2048 10
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# Environment file
|
||||||
|
run_doctor_check "Environment File" check_env_file "$env_file"
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# Required environment variables
|
||||||
|
run_doctor_check "Required Variables" check_required_env "$env_file"
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# Secret strength
|
||||||
|
run_doctor_check "Secret Strength" check_secrets "$env_file"
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# File permissions
|
||||||
|
run_doctor_check "File Permissions" check_env_permissions "$env_file"
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
if [[ "$mode" == "docker" ]]; then
|
||||||
|
# Docker containers
|
||||||
|
run_doctor_check "Docker Containers" check_docker_containers "$compose_file"
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# Container health
|
||||||
|
run_doctor_check "Container Health" check_container_health
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
run_doctor_check "Database" check_database_connection
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# Valkey connection
|
||||||
|
run_doctor_check "Cache (Valkey)" check_valkey_connection
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# API health
|
||||||
|
run_doctor_check "API" check_api_health
|
||||||
|
collect_result $?
|
||||||
|
|
||||||
|
# Web frontend
|
||||||
|
run_doctor_check "Web Frontend" check_web_health
|
||||||
|
collect_result $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}════════════════════════════════════════════════════════════${NC}"
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
echo -e "${ERROR}✗${NC} ${BOLD}Failed${NC}: $errors errors, $warnings warnings"
|
||||||
|
echo ""
|
||||||
|
echo "Fix the errors above and run doctor again."
|
||||||
|
return $CHECK_FAIL
|
||||||
|
elif [[ $warnings -gt 0 ]]; then
|
||||||
|
echo -e "${WARN}⚠${NC} ${BOLD}Warnings${NC}: $warnings warnings"
|
||||||
|
echo ""
|
||||||
|
echo "System is operational but some optimizations are recommended."
|
||||||
|
return $CHECK_WARN
|
||||||
|
else
|
||||||
|
echo -e "${SUCCESS}✓${NC} ${BOLD}All checks passed${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Mosaic Stack is healthy and ready to use."
|
||||||
|
return $CHECK_PASS
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper to run a check and print result
|
||||||
|
run_doctor_check() {
|
||||||
|
local name="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
echo -e "${BOLD}Checking: $name${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
"$@"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper to collect check results
|
||||||
|
collect_result() {
|
||||||
|
local result=$1
|
||||||
|
|
||||||
|
case $result in
|
||||||
|
$CHECK_PASS) ;;
|
||||||
|
$CHECK_WARN) ((warnings++)) ;;
|
||||||
|
$CHECK_FAIL) ((errors++)) ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Quick Health Check
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Quick check for CI/CD or scripts
|
||||||
|
quick_health_check() {
|
||||||
|
local api_url="${1:-http://localhost:3001}"
|
||||||
|
|
||||||
|
check_url_responds "${api_url}/health" 200 5
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for healthy state
|
||||||
|
wait_for_healthy() {
|
||||||
|
local timeout="${1:-120}"
|
||||||
|
local interval="${2:-5}"
|
||||||
|
|
||||||
|
echo -e "${INFO}ℹ${NC} Waiting for healthy state..."
|
||||||
|
|
||||||
|
local elapsed=0
|
||||||
|
while [[ $elapsed -lt $timeout ]]; do
|
||||||
|
if quick_health_check &>/dev/null; then
|
||||||
|
echo -e "${SUCCESS}✓${NC} System is healthy"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$interval"
|
||||||
|
((elapsed += interval))
|
||||||
|
echo -n "."
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${ERROR}✗${NC} Timeout waiting for healthy state"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user