#!/usr/bin/env bash # scripts/common.sh — Shared bash library for HomeAI setup scripts # Source this file: source "$(dirname "$0")/../scripts/common.sh" # Do NOT execute directly. set -euo pipefail # ─── Colours ─────────────────────────────────────────────────────────────────── if [[ -t 1 ]]; then RED='\033[0;31m'; YELLOW='\033[0;33m'; GREEN='\033[0;32m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' else RED=''; YELLOW=''; GREEN=''; BLUE=''; CYAN=''; BOLD=''; RESET='' fi # ─── Logging ─────────────────────────────────────────────────────────────────── log_info() { echo -e "${BLUE}[INFO]${RESET} $*"; } log_success() { echo -e "${GREEN}[OK]${RESET} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } log_error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } log_section() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}"; } log_step() { echo -e "${CYAN} →${RESET} $*"; } die() { log_error "$*"; exit 1; } # ─── OS & Architecture Detection ─────────────────────────────────────────────── detect_os() { case "$(uname -s)" in Linux*) OS_TYPE=linux ;; Darwin*) OS_TYPE=macos ;; *) die "Unsupported OS: $(uname -s)" ;; esac export OS_TYPE } detect_arch() { case "$(uname -m)" in x86_64|amd64) ARCH=amd64 ;; arm64|aarch64) ARCH=arm64 ;; *) ARCH=unknown ;; esac export ARCH } detect_distro() { DISTRO=unknown if [[ "$OS_TYPE" == "linux" ]]; then if [[ -f /etc/os-release ]]; then # shellcheck disable=SC1091 source /etc/os-release DISTRO="${ID:-unknown}" fi fi export DISTRO } detect_platform() { detect_os detect_arch detect_distro log_info "Platform: ${OS_TYPE}/${ARCH} (${DISTRO})" } # ─── GPU Detection ───────────────────────────────────────────────────────────── detect_gpu() { GPU_TYPE=none if [[ "$OS_TYPE" == "macos" && "$ARCH" == "arm64" ]]; then GPU_TYPE=metal elif command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then GPU_TYPE=cuda GPU_INFO=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || true) elif command -v rocm-smi &>/dev/null; then GPU_TYPE=rocm GPU_INFO=$(rocm-smi --showproductname 2>/dev/null | head -1 || true) fi export GPU_TYPE GPU_INFO log_info "GPU: ${GPU_TYPE}${GPU_INFO:+ — ${GPU_INFO}}" } # ─── Dependency Checking ─────────────────────────────────────────────────────── require_command() { local cmd="$1" local hint="${2:-install $cmd}" if ! command -v "$cmd" &>/dev/null; then die "'$cmd' is required but not found. Hint: $hint" fi } command_exists() { command -v "$1" &>/dev/null; } require_min_version() { local cmd="$1" required="$2" local actual actual="$("$cmd" --version 2>&1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)" if [[ "$(printf '%s\n' "$required" "$actual" | sort -V | head -1)" != "$required" ]]; then die "$cmd version $required+ required, found $actual" fi } # ─── Package Management ──────────────────────────────────────────────────────── install_package() { local pkg="$1" log_step "Installing $pkg..." case "$OS_TYPE" in linux) case "$DISTRO" in ubuntu|debian|linuxmint|pop) sudo apt-get install -y -qq "$pkg" ;; fedora|rhel|centos|rocky|almalinux) sudo dnf install -y -q "$pkg" ;; arch|manjaro|endeavouros) sudo pacman -S --noconfirm --quiet "$pkg" ;; opensuse*) sudo zypper install -y -q "$pkg" ;; *) die "Unknown distro '$DISTRO' — install $pkg manually" ;; esac ;; macos) if ! command_exists brew; then die "Homebrew not found. Install from https://brew.sh" fi brew install "$pkg" ;; esac } update_package_index() { if [[ "$OS_TYPE" == "linux" ]]; then case "$DISTRO" in ubuntu|debian|linuxmint|pop) sudo apt-get update -qq ;; fedora|rhel|centos|rocky|almalinux) sudo dnf check-update -q || true ;; esac fi } # ─── Docker ──────────────────────────────────────────────────────────────────── check_docker_installed() { command_exists docker } check_docker_running() { docker info &>/dev/null } install_docker() { if check_docker_installed; then log_success "Docker already installed: $(docker --version)" return fi log_info "Installing Docker..." if [[ "$OS_TYPE" == "macos" ]]; then die "On macOS, install Docker Desktop manually: https://www.docker.com/products/docker-desktop/" fi # Linux: use the official convenience script curl -fsSL https://get.docker.com -o /tmp/get-docker.sh sudo sh /tmp/get-docker.sh rm /tmp/get-docker.sh # Add current user to docker group sudo usermod -aG docker "$USER" log_warn "Added $USER to docker group. Log out and back in (or run 'newgrp docker') for it to take effect." # Start and enable Docker sudo systemctl enable --now docker log_success "Docker installed: $(docker --version)" } ensure_docker_running() { if ! check_docker_running; then if [[ "$OS_TYPE" == "linux" ]]; then log_info "Starting Docker..." sudo systemctl start docker sleep 2 else die "Docker Desktop is not running. Start it from your Applications folder." fi fi if ! check_docker_running; then die "Docker is not running. Start Docker and retry." fi } ensure_docker_network() { local network="${1:-homeai}" if ! docker network inspect "$network" &>/dev/null; then log_step "Creating Docker network '$network'..." docker network create "$network" log_success "Network '$network' created." else log_info "Docker network '$network' already exists." fi } # ─── Docker Compose ──────────────────────────────────────────────────────────── # Handles both `docker compose` (v2) and `docker-compose` (v1) docker_compose() { if docker compose version &>/dev/null 2>&1; then docker compose "$@" elif command_exists docker-compose; then docker-compose "$@" else die "docker compose not found. Ensure Docker is up to date." fi } # ─── Service Management ──────────────────────────────────────────────────────── install_service() { # Usage: install_service local name="$1" local systemd_file="$2" local launchd_file="$3" if [[ "$OS_TYPE" == "linux" ]]; then if [[ ! -f "$systemd_file" ]]; then log_warn "No systemd unit file at $systemd_file — skipping service install." return fi log_step "Installing systemd service: $name" sudo cp "$systemd_file" "/etc/systemd/system/${name}.service" sudo systemctl daemon-reload sudo systemctl enable --now "$name" log_success "Service '$name' enabled and started." elif [[ "$OS_TYPE" == "macos" ]]; then if [[ ! -f "$launchd_file" ]]; then log_warn "No launchd plist at $launchd_file — skipping service install." return fi local plist_dest="${HOME}/Library/LaunchAgents/$(basename "$launchd_file")" log_step "Installing launchd agent: $name" cp "$launchd_file" "$plist_dest" launchctl load -w "$plist_dest" log_success "LaunchAgent '$name' installed and loaded." fi } uninstall_service() { local name="$1" local plist_label="${2:-$name}" if [[ "$OS_TYPE" == "linux" ]]; then sudo systemctl disable --now "$name" 2>/dev/null || true sudo rm -f "/etc/systemd/system/${name}.service" sudo systemctl daemon-reload elif [[ "$OS_TYPE" == "macos" ]]; then local plist_path="${HOME}/Library/LaunchAgents/${plist_label}.plist" launchctl unload -w "$plist_path" 2>/dev/null || true rm -f "$plist_path" fi } # ─── Environment Files ───────────────────────────────────────────────────────── load_env() { local env_file="${1:-.env}" if [[ -f "$env_file" ]]; then # shellcheck disable=SC1090 set -a; source "$env_file"; set +a fi } load_env_services() { local services_file="${HOME}/.env.services" if [[ -f "$services_file" ]]; then set -a; source "$services_file"; set +a fi } write_env_service() { # Append or update KEY=VALUE in ~/.env.services local key="$1" value="$2" local file="${HOME}/.env.services" touch "$file" if grep -q "^${key}=" "$file" 2>/dev/null; then # Update existing sed -i "s|^${key}=.*|${key}=${value}|" "$file" else echo "${key}=${value}" >> "$file" fi } # Bootstrap .env from .env.example if not present bootstrap_env() { local dir="${1:-.}" if [[ ! -f "${dir}/.env" && -f "${dir}/.env.example" ]]; then cp "${dir}/.env.example" "${dir}/.env" log_warn "Created ${dir}/.env from .env.example — fill in secrets before continuing." fi } # ─── Network Helpers ─────────────────────────────────────────────────────────── wait_for_http() { local url="$1" name="${2:-service}" timeout="${3:-60}" log_step "Waiting for $name at $url (up to ${timeout}s)..." local elapsed=0 while ! curl -sf "$url" -o /dev/null; do sleep 3; elapsed=$((elapsed + 3)) if [[ $elapsed -ge $timeout ]]; then log_warn "$name did not respond within ${timeout}s. It may still be starting." return 1 fi done log_success "$name is up." } check_port_free() { local port="$1" if ss -tlnp "sport = :${port}" 2>/dev/null | grep -q ":${port}"; then return 1 # port in use fi return 0 } # ─── Utility ─────────────────────────────────────────────────────────────────── confirm() { local msg="${1:-Continue?}" read -rp "$(echo -e "${YELLOW}${msg} [y/N]${RESET} ")" response [[ "${response,,}" == "y" ]] } print_summary() { local title="$1"; shift echo "" log_section "$title" while [[ $# -gt 0 ]]; do local key="$1" val="$2"; shift 2 printf " ${BOLD}%-24s${RESET} %s\n" "$key" "$val" done echo "" }