#!/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 }