#!/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='' 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[@]} # Output UI to /dev/tty so it's visible even when function output is captured echo "$prompt" >/dev/tty for i in "${!options[@]}"; do printf " %d) %s\n" "$((i + 1))" "${options[$i]}" >/dev/tty done echo "" >/dev/tty local selection while true; do read -r -p "Enter selection [1-$num_options]: " selection /dev/tty if [[ "$selection" =~ ^[0-9]+$ ]] && \ [ "$selection" -ge 1 ] && \ [ "$selection" -le "$num_options" ]; then # Only output the selected value to stdout (for capture) echo "${options[$((selection - 1))]}" return 0 else print_error "Invalid selection. Please enter a number between 1 and $num_options." >/dev/tty 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() { if ! check_command docker; then return 1 fi # Check if daemon is accessible if docker info >/dev/null 2>&1; then return 0 fi # Docker exists but daemon not accessible # This could be permission issue or daemon not running local error_msg error_msg=$(docker info 2>&1) if [[ "$error_msg" =~ "permission denied" ]]; then print_warning "Docker installed but permission denied" print_info "You may need to add your user to the docker group:" print_info " sudo usermod -aG docker \$USER" print_info " Then log out and back in" return 2 # Special code for permission issue elif [[ "$error_msg" =~ "Cannot connect to the Docker daemon" ]]; then print_warning "Docker installed but daemon not running" print_info "Start it with: sudo systemctl start docker" return 3 # Special code for daemon not running else print_warning "Docker installed but not accessible" return 4 # Unknown issue fi } 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) # Don't quote $package to allow multi-word package names 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" # Allow single-character subdomains and properly validate domain structure 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 } validate_ipv4() { local ip="$1" local IFS='.' local -a octets read -ra octets <<< "$ip" # Must have exactly 4 octets [[ ${#octets[@]} -eq 4 ]] || return 1 # Each octet must be 0-255 for octet in "${octets[@]}"; do # Must be numeric [[ "$octet" =~ ^[0-9]+$ ]] || return 1 # Must be in range 0-255 (( octet >= 0 && octet <= 255 )) || return 1 done return 0 } # ============================================================================ # 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!@#$%^&*()-_=+' $ ]] || \ [[ -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))" }