Files
stack/scripts/lib/common.sh
Jason Woltje fd93be6032 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>
2026-01-31 16:45:56 -06:00

511 lines
12 KiB
Bash
Raw Blame History

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