Add self-deploying setup scripts for all sub-projects (P1-P8)

- Root setup.sh orchestrator with per-phase dispatch (./setup.sh p1..p8 | all | status)
- Makefile convenience targets (make infra, make llm, make status, etc.)
- scripts/common.sh: shared bash library for OS detection, Docker helpers,
  service management (launchd/systemd), package install, env management
- .env.example + .gitignore: shared config template and secret exclusions

P1 (homeai-infra): full implementation
- docker-compose.yml: Uptime Kuma, code-server, n8n
- Note: Home Assistant, Portainer, Gitea are pre-existing instances
- setup.sh: Docker install, homeai network, container health checks

P2 (homeai-llm): full implementation
- Ollama native install with CUDA/ROCm/Metal auto-detection
- launchd plist (macOS) + systemd service (Linux) for auto-start
- scripts/pull-models.sh: idempotent model puller from manifest
- scripts/benchmark.sh: tokens/sec measurement per model
- Open WebUI on port 3030 (avoids Gitea :3000 conflict)

P3-P8: working stubs with prerequisite checks and TODO sections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-04 21:10:53 +00:00
parent 38247d7cc4
commit 7978eaea14
23 changed files with 2525 additions and 0 deletions

331
scripts/common.sh Normal file
View File

@@ -0,0 +1,331 @@
#!/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 <name> <systemd_unit_file> <launchd_plist_file>
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 ""
}