Files
stack/scripts/lib/dependencies.sh
Jason Woltje ab52827d9c
All checks were successful
ci/woodpecker/push/infra Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
chore: add install scripts, doctor command, and AGENTS.md
- Add one-line installer (scripts/install.sh) with platform detection
- Add doctor command (scripts/commands/doctor.sh) for environment diagnostics
- Add shared libraries: dependencies, docker, platform, validation
- Update README with quick-start installer instructions
- Add AGENTS.md with codebase patterns for AI agent context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:04:36 -06:00

909 lines
25 KiB
Bash

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