feat: Add comprehensive setup wizard foundation

Modeled after Calibr setup.sh pattern (~/src/calibr/scripts/setup.sh).

Implemented (Foundation):
- Platform detection (Ubuntu, Arch, macOS, Fedora)
- Dependency checking and installation
- Mode selection (Docker vs Native)
- Interactive + non-interactive modes
- Comprehensive logging (clean console + full trace to log file)
- Common utility functions library (450+ lines)

Features in common.sh:
- Output formatting (colors, headers, success/error/warning)
- User input (confirm, select_option)
- Platform detection
- Dependency checking (Docker, Node, pnpm, PostgreSQL)
- Package installation (apt, pacman, dnf, brew)
- Validation (URL, email, port, domain)
- Secret generation (cryptographically secure)
- .env file parsing and management
- Port conflict detection
- File backup with timestamps

To Be Implemented (See scripts/README.md):
- Complete configuration collection
- .env generation with smart preservation
- Port conflict detection
- Password/secret generation
- Authentik blueprint auto-configuration
- Docker deployment execution
- Post-install instructions

Usage:
  ./scripts/setup.sh                    # Interactive
  ./scripts/setup.sh --help             # Show options
  ./scripts/setup.sh --dry-run          # Preview
  ./scripts/setup.sh --non-interactive  # CI/CD

Refs: Setup wizard issue (created)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 16:44:25 -06:00
parent 0eb3abc12c
commit fd93be6032
26 changed files with 1255 additions and 7491 deletions

510
scripts/lib/common.sh Normal file
View File

@@ -0,0 +1,510 @@
#!/bin/bash
# Common utility functions for Mosaic Stack setup
# Adapted from Calibr setup pattern
# ============================================================================
# Output Formatting
# ============================================================================
# Colors
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
CYAN='\033[0;36m'
BOLD=''
NC=''
fi
print_header() {
echo ""
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BOLD}${CYAN} $1${NC}"
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
}
print_success() {
echo -e "${GREEN}${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1" >&2
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
}
print_info() {
echo -e "${BLUE}${NC} $1"
}
print_step() {
echo -e "${CYAN}${NC} $1"
}
# ============================================================================
# User Input
# ============================================================================
confirm() {
local prompt="$1"
local default="${2:-n}"
local response
if [[ "$default" == "y" ]]; then
prompt="$prompt [Y/n]: "
else
prompt="$prompt [y/N]: "
fi
while true; do
read -r -p "$prompt" response
response=${response:-$default}
case "$response" in
[Yy]|[Yy][Ee][Ss])
return 0
;;
[Nn]|[Nn][Oo])
return 1
;;
*)
echo "Please answer yes or no."
;;
esac
done
}
select_option() {
local prompt="$1"
shift
local options=("$@")
local num_options=${#options[@]}
echo "$prompt"
for i in "${!options[@]}"; do
printf " %d) %s\n" "$((i + 1))" "${options[$i]}"
done
echo ""
local selection
while true; do
read -r -p "Enter selection [1-$num_options]: " selection
if [[ "$selection" =~ ^[0-9]+$ ]] && \
[ "$selection" -ge 1 ] && \
[ "$selection" -le "$num_options" ]; then
echo "${options[$((selection - 1))]}"
return 0
else
print_error "Invalid selection. Please enter a number between 1 and $num_options."
fi
done
}
# ============================================================================
# Platform Detection
# ============================================================================
detect_os() {
case "$OSTYPE" in
linux-gnu*)
if [[ -f /etc/os-release ]]; then
source /etc/os-release
case "$ID" in
ubuntu|debian)
echo "debian"
;;
arch|manjaro|endeavouros)
echo "arch"
;;
fedora|rhel|centos)
echo "fedora"
;;
*)
echo "linux"
;;
esac
else
echo "linux"
fi
;;
darwin*)
echo "macos"
;;
*)
echo "unknown"
;;
esac
}
detect_package_manager() {
local os="$1"
case "$os" in
debian)
echo "apt"
;;
arch)
echo "pacman"
;;
fedora)
echo "dnf"
;;
macos)
if command -v brew >/dev/null 2>&1; then
echo "brew"
else
echo "none"
fi
;;
*)
echo "unknown"
;;
esac
}
get_os_name() {
local os="$1"
case "$os" in
debian)
echo "Debian/Ubuntu"
;;
arch)
echo "Arch Linux"
;;
fedora)
echo "Fedora/RHEL"
;;
macos)
echo "macOS"
;;
*)
echo "Unknown OS"
;;
esac
}
# ============================================================================
# Command and Dependency Checking
# ============================================================================
check_command() {
command -v "$1" >/dev/null 2>&1
}
check_docker() {
check_command docker && docker info >/dev/null 2>&1
}
check_docker_compose() {
if docker compose version >/dev/null 2>&1; then
return 0
elif check_command docker-compose; then
return 0
else
return 1
fi
}
check_docker_buildx() {
docker buildx version >/dev/null 2>&1
}
check_node() {
local min_version="${1:-18}"
if ! check_command node; then
return 1
fi
local node_version
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
if [[ "$node_version" -ge "$min_version" ]]; then
return 0
else
return 1
fi
}
check_pnpm() {
check_command pnpm
}
check_postgres() {
check_command psql
}
check_sudo() {
if ! command -v sudo >/dev/null 2>&1; then
print_error "sudo is not installed"
return 1
fi
if ! sudo -n true 2>/dev/null; then
print_warning "This script may need elevated privileges"
print_info "You may be prompted for your password"
sudo -v
fi
}
# ============================================================================
# Package Installation
# ============================================================================
get_package_name() {
local pkg_manager="$1"
local package="$2"
case "$pkg_manager" in
apt)
case "$package" in
docker) echo "docker.io" ;;
docker-compose) echo "docker-compose" ;;
node) echo "nodejs" ;;
postgres) echo "postgresql postgresql-contrib" ;;
*) 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" ;;
docker-compose) echo "docker-compose" ;;
node) echo "nodejs" ;;
postgres) echo "postgresql-server" ;;
*) echo "$package" ;;
esac
;;
brew)
case "$package" in
docker) echo "docker" ;;
node) echo "node" ;;
postgres) echo "postgresql@17" ;;
*) echo "$package" ;;
esac
;;
*)
echo "$package"
;;
esac
}
install_package() {
local pkg_manager="$1"
local package="$2"
print_step "Installing $package..."
case "$pkg_manager" in
apt)
sudo apt update && sudo apt install -y "$package"
;;
pacman)
sudo pacman -Sy --noconfirm "$package"
;;
dnf)
sudo dnf install -y "$package"
;;
brew)
brew install "$package"
;;
*)
print_error "Unknown package manager: $pkg_manager"
return 1
;;
esac
}
# ============================================================================
# Validation Functions
# ============================================================================
validate_url() {
local url="$1"
if [[ "$url" =~ ^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?$ ]]; then
return 0
else
return 1
fi
}
validate_email() {
local email="$1"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
validate_port() {
local port="$1"
if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
return 0
else
return 1
fi
}
validate_domain() {
local domain="$1"
if [[ "$domain" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]{0,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
# ============================================================================
# Secret and Password Generation
# ============================================================================
generate_secret() {
local length="${1:-32}"
# Use /dev/urandom for cryptographically secure random
LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*()-_=+' </dev/urandom | head -c "$length"
}
generate_password() {
local length="${1:-16}"
# Passwords without special chars for easier manual entry
LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c "$length"
}
mask_value() {
local value="$1"
local length=${#value}
if [ "$length" -le 8 ]; then
echo "***${value: -2}"
else
echo "${value:0:3}...${value: -3}"
fi
}
is_placeholder() {
local value="$1"
# Check for common placeholder patterns
if [[ "$value" =~ ^\$\{.*\}$ ]] || \
[[ "$value" =~ ^(change-me|changeme|your-.*|example|placeholder|TODO|FIXME|xxx+)$ ]] || \
[[ "$value" =~ ^<.*>$ ]] || \
[[ -z "$value" ]]; then
return 0
else
return 1
fi
}
# ============================================================================
# .env File Management
# ============================================================================
# Global associative array to store env values
declare -gA ENV_VALUES
parse_env_file() {
local env_file="$1"
if [[ ! -f "$env_file" ]]; then
return 1
fi
# Clear existing values
ENV_VALUES=()
while IFS='=' read -r key value || [[ -n "$key" ]]; do
# Skip comments and empty lines
[[ "$key" =~ ^#.*$ ]] && continue
[[ -z "$key" ]] && continue
# Remove leading/trailing whitespace
key=$(echo "$key" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
value=$(echo "$value" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
# Remove quotes if present
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
ENV_VALUES["$key"]="$value"
done < "$env_file"
}
get_env_value() {
local key="$1"
echo "${ENV_VALUES[$key]:-}"
}
set_env_value() {
local key="$1"
local value="$2"
ENV_VALUES["$key"]="$value"
}
# ============================================================================
# File Operations
# ============================================================================
backup_file() {
local file="$1"
if [[ -f "$file" ]]; then
local backup="${file}.bak.$(date +%Y%m%d_%H%M%S)"
cp "$file" "$backup"
print_success "Backed up $file to $backup"
fi
}
# ============================================================================
# Port Checking
# ============================================================================
check_port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -tuln | grep -q ":${port} "
elif command -v netstat >/dev/null 2>&1; then
netstat -tuln | grep -q ":${port} "
elif command -v lsof >/dev/null 2>&1; then
lsof -i ":${port}" >/dev/null 2>&1
else
# Can't check, assume available
return 1
fi
}
suggest_alternative_port() {
local base_port="$1"
local offset=100
for i in {1..10}; do
local alt_port=$((base_port + offset * i))
if ! check_port_in_use "$alt_port"; then
echo "$alt_port"
return 0
fi
done
echo "$((base_port + 10000))"
}