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>
This commit is contained in:
665
scripts/lib/platform.sh
Normal file
665
scripts/lib/platform.sh
Normal file
@@ -0,0 +1,665 @@
|
||||
#!/bin/bash
|
||||
# Platform detection functions for Mosaic Stack installer
|
||||
# Provides OS, package manager, architecture, and environment detection
|
||||
|
||||
# ============================================================================
|
||||
# Colors (if terminal supports them)
|
||||
# ============================================================================
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
BOLD='\033[1m'
|
||||
ACCENT='\033[38;2;128;90;213m'
|
||||
SUCCESS='\033[38;2;47;191;113m'
|
||||
WARN='\033[38;2;255;176;32m'
|
||||
ERROR='\033[38;2;226;61;45m'
|
||||
INFO='\033[38;2;100;149;237m'
|
||||
MUTED='\033[38;2;139;127;119m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
BOLD=''
|
||||
ACCENT=''
|
||||
SUCCESS=''
|
||||
WARN=''
|
||||
ERROR=''
|
||||
INFO=''
|
||||
MUTED=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# OS Detection
|
||||
# ============================================================================
|
||||
|
||||
# Detect operating system type
|
||||
# Returns: macos, debian, arch, fedora, linux, unknown
|
||||
detect_os() {
|
||||
case "$OSTYPE" in
|
||||
darwin*)
|
||||
echo "macos"
|
||||
return 0
|
||||
;;
|
||||
linux-gnu*)
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|debian|linuxmint|pop|elementary)
|
||||
echo "debian"
|
||||
return 0
|
||||
;;
|
||||
arch|manjaro|endeavouros|garuda|arcolinux)
|
||||
echo "arch"
|
||||
return 0
|
||||
;;
|
||||
fedora|rhel|centos|rocky|almalinux|ol)
|
||||
echo "fedora"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "linux"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "linux"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "unknown"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Detect if running under WSL (Windows Subsystem for Linux)
|
||||
# Returns WSL_DISTRO_NAME if in WSL, empty string otherwise
|
||||
detect_wsl() {
|
||||
if [[ -n "${WSL_DISTRO_NAME:-}" ]]; then
|
||||
echo "$WSL_DISTRO_NAME"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for WSL in /proc/version
|
||||
if [[ -f /proc/version ]]; then
|
||||
if grep -qi "microsoft\|wsl" /proc/version 2>/dev/null; then
|
||||
# Try to get distro name from os-release
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/os-release
|
||||
echo "${NAME:-WSL}"
|
||||
return 0
|
||||
fi
|
||||
echo "WSL"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if running in WSL
|
||||
is_wsl() {
|
||||
[[ -n "${WSL_DISTRO_NAME:-}" ]] && return 0
|
||||
[[ -f /proc/version ]] && grep -qi "microsoft\|wsl" /proc/version 2>/dev/null
|
||||
}
|
||||
|
||||
# Get human-readable OS name
|
||||
get_os_name() {
|
||||
local os="$1"
|
||||
|
||||
case "$os" in
|
||||
macos)
|
||||
echo "macOS"
|
||||
;;
|
||||
debian)
|
||||
echo "Debian/Ubuntu"
|
||||
;;
|
||||
arch)
|
||||
echo "Arch Linux"
|
||||
;;
|
||||
fedora)
|
||||
echo "Fedora/RHEL"
|
||||
;;
|
||||
linux)
|
||||
echo "Linux"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Package Manager Detection
|
||||
# ============================================================================
|
||||
|
||||
# Detect the system's package manager
|
||||
# Returns: brew, apt, pacman, dnf, yum, unknown
|
||||
detect_package_manager() {
|
||||
local os="$1"
|
||||
|
||||
# First check for Homebrew (available on macOS and Linux)
|
||||
if command -v brew &>/dev/null; then
|
||||
echo "brew"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fall back to OS-specific package managers
|
||||
case "$os" in
|
||||
macos)
|
||||
# macOS without Homebrew
|
||||
echo "none"
|
||||
return 1
|
||||
;;
|
||||
debian)
|
||||
if command -v apt-get &>/dev/null; then
|
||||
echo "apt"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
arch)
|
||||
if command -v pacman &>/dev/null; then
|
||||
echo "pacman"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
fedora)
|
||||
if command -v dnf &>/dev/null; then
|
||||
echo "dnf"
|
||||
return 0
|
||||
elif command -v yum &>/dev/null; then
|
||||
echo "yum"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "unknown"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Architecture Detection
|
||||
# ============================================================================
|
||||
|
||||
# Get system architecture
|
||||
# Returns: x86_64, aarch64, armv7l, armv6l, unknown
|
||||
get_arch() {
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
|
||||
case "$arch" in
|
||||
x86_64|amd64)
|
||||
echo "x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "aarch64"
|
||||
;;
|
||||
armv7l|armhf)
|
||||
echo "armv7l"
|
||||
;;
|
||||
armv6l)
|
||||
echo "armv6l"
|
||||
;;
|
||||
*)
|
||||
echo "unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Check if running on Apple Silicon
|
||||
is_apple_silicon() {
|
||||
[[ "$(detect_os)" == "macos" ]] && [[ "$(get_arch)" == "aarch64" ]]
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Init System Detection
|
||||
# ============================================================================
|
||||
|
||||
# Detect the init system
|
||||
# Returns: systemd, openrc, launchd, sysvinit, unknown
|
||||
detect_init_system() {
|
||||
local os
|
||||
os=$(detect_os)
|
||||
|
||||
case "$os" in
|
||||
macos)
|
||||
echo "launchd"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check for systemd
|
||||
if command -v systemctl &>/dev/null && pidof systemd &>/dev/null; then
|
||||
echo "systemd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for OpenRC
|
||||
if command -v rc-status &>/dev/null; then
|
||||
echo "openrc"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for SysVinit
|
||||
if command -v service &>/dev/null; then
|
||||
echo "sysvinit"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "unknown"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Privilege Helpers
|
||||
# ============================================================================
|
||||
|
||||
# Check if running as root
|
||||
is_root() {
|
||||
[[ "$(id -u)" -eq 0 ]]
|
||||
}
|
||||
|
||||
# Run command with sudo only if not already root
|
||||
maybe_sudo() {
|
||||
if is_root; then
|
||||
# Skip -E flag when root (env is already preserved)
|
||||
if [[ "${1:-}" == "-E" ]]; then
|
||||
shift
|
||||
fi
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure sudo is available (Linux only)
|
||||
require_sudo() {
|
||||
local os
|
||||
os=$(detect_os)
|
||||
|
||||
if [[ "$os" == "macos" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if is_root; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v sudo &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${ERROR}Error: sudo is required for system installs on Linux${NC}"
|
||||
echo "Install sudo or re-run as root."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Validate sudo credentials (cache them early)
|
||||
validate_sudo() {
|
||||
if is_root; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v sudo &>/dev/null; then
|
||||
sudo -v 2>/dev/null
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# TTY and Interactive Detection
|
||||
# ============================================================================
|
||||
|
||||
# Check if running in an interactive terminal
|
||||
is_interactive() {
|
||||
[[ -t 0 && -t 1 ]]
|
||||
}
|
||||
|
||||
# Check if we can prompt the user
|
||||
is_promptable() {
|
||||
[[ -r /dev/tty && -w /dev/tty ]]
|
||||
}
|
||||
|
||||
# Read input from TTY (for prompts when stdin is piped)
|
||||
read_from_tty() {
|
||||
local prompt="$1"
|
||||
local var_name="$2"
|
||||
|
||||
if is_promptable; then
|
||||
echo -e "$prompt" > /dev/tty
|
||||
read -r "$var_name" < /dev/tty
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Shell Detection
|
||||
# ============================================================================
|
||||
|
||||
# Get current shell name
|
||||
get_shell() {
|
||||
basename "${SHELL:-/bin/sh}"
|
||||
}
|
||||
|
||||
# Get shell configuration file
|
||||
get_shell_rc() {
|
||||
local shell
|
||||
shell=$(get_shell)
|
||||
|
||||
case "$shell" in
|
||||
zsh)
|
||||
echo "$HOME/.zshrc"
|
||||
;;
|
||||
bash)
|
||||
echo "$HOME/.bashrc"
|
||||
;;
|
||||
fish)
|
||||
echo "$HOME/.config/fish/config.fish"
|
||||
;;
|
||||
*)
|
||||
echo "$HOME/.profile"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Network and Connectivity
|
||||
# ============================================================================
|
||||
|
||||
# Check if we have internet connectivity
|
||||
has_internet() {
|
||||
local timeout="${1:-5}"
|
||||
|
||||
# Try to reach common endpoints
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -s --max-time "$timeout" https://api.github.com &>/dev/null
|
||||
return $?
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -q --timeout="$timeout" --spider https://api.github.com
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Fall back to ping
|
||||
ping -c 1 -W "$timeout" 8.8.8.8 &>/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Get local IP address
|
||||
get_local_ip() {
|
||||
local ip
|
||||
|
||||
# Try hostname command first (macOS)
|
||||
if command -v hostname &>/dev/null; then
|
||||
ip=$(hostname -I 2>/dev/null | cut -d' ' -f1)
|
||||
if [[ -n "$ip" ]]; then
|
||||
echo "$ip"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try ip command (Linux)
|
||||
if command -v ip &>/dev/null; then
|
||||
ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}')
|
||||
if [[ -n "$ip" ]]; then
|
||||
echo "$ip"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try ifconfig
|
||||
if command -v ifconfig &>/dev/null; then
|
||||
ip=$(ifconfig 2>/dev/null | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1)
|
||||
if [[ -n "$ip" ]]; then
|
||||
echo "$ip"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# System Resources
|
||||
# ============================================================================
|
||||
|
||||
# Get total RAM in MB
|
||||
get_total_ram() {
|
||||
local os
|
||||
os=$(detect_os)
|
||||
|
||||
case "$os" in
|
||||
macos)
|
||||
sysctl -n hw.memsize 2>/dev/null | awk '{print int($1/1024/1024)}'
|
||||
;;
|
||||
*)
|
||||
if [[ -f /proc/meminfo ]]; then
|
||||
awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Get available disk space in GB for a path
|
||||
get_available_disk() {
|
||||
local path="${1:-.}"
|
||||
|
||||
if command -v df &>/dev/null; then
|
||||
df -BG "$path" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G'
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if system meets minimum requirements
|
||||
check_minimum_requirements() {
|
||||
local min_ram="${1:-2048}" # MB
|
||||
local min_disk="${2:-10}" # GB
|
||||
|
||||
local ram disk
|
||||
ram=$(get_total_ram)
|
||||
disk=$(get_available_disk "$HOME")
|
||||
|
||||
local errors=()
|
||||
|
||||
if [[ "$ram" -lt "$min_ram" ]]; then
|
||||
errors+=("RAM: ${ram}MB (minimum: ${min_ram}MB)")
|
||||
fi
|
||||
|
||||
if [[ "$disk" -lt "$min_disk" ]]; then
|
||||
errors+=("Disk: ${disk}GB (minimum: ${min_disk}GB)")
|
||||
fi
|
||||
|
||||
if [[ ${#errors[@]} -gt 0 ]]; then
|
||||
echo "System does not meet minimum requirements:"
|
||||
printf ' - %s\n' "${errors[@]}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Downloader Detection
|
||||
# ============================================================================
|
||||
|
||||
DOWNLOADER=""
|
||||
|
||||
detect_downloader() {
|
||||
if command -v curl &>/dev/null; then
|
||||
DOWNLOADER="curl"
|
||||
return 0
|
||||
fi
|
||||
if command -v wget &>/dev/null; then
|
||||
DOWNLOADER="wget"
|
||||
return 0
|
||||
fi
|
||||
echo -e "${ERROR}Error: Missing downloader (curl or wget required)${NC}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Download a file securely
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
|
||||
if [[ -z "$DOWNLOADER" ]]; then
|
||||
detect_downloader || return 1
|
||||
fi
|
||||
|
||||
if [[ "$DOWNLOADER" == "curl" ]]; then
|
||||
curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url"
|
||||
return $?
|
||||
fi
|
||||
|
||||
wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Download and execute a script
|
||||
run_remote_script() {
|
||||
local url="$1"
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
|
||||
if ! download_file "$url" "$tmp"; then
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
/bin/bash "$tmp"
|
||||
local ret=$?
|
||||
rm -f "$tmp"
|
||||
return $ret
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# PATH Management
|
||||
# ============================================================================
|
||||
|
||||
# Store original PATH for later comparison
|
||||
ORIGINAL_PATH="${PATH:-}"
|
||||
|
||||
# Check if a directory is in PATH
|
||||
path_has_dir() {
|
||||
local path="$1"
|
||||
local dir="${2%/}"
|
||||
|
||||
[[ -z "$dir" ]] && return 1
|
||||
|
||||
case ":${path}:" in
|
||||
*":${dir}:"*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Add directory to PATH in shell config
|
||||
add_to_path() {
|
||||
local dir="$1"
|
||||
local rc="${2:-$(get_shell_rc)}"
|
||||
|
||||
if [[ ! -f "$rc" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
local path_line="export PATH=\"${dir}:\$PATH\""
|
||||
|
||||
if ! grep -qF "$dir" "$rc" 2>/dev/null; then
|
||||
echo "" >> "$rc"
|
||||
echo "# Added by Mosaic Stack installer" >> "$rc"
|
||||
echo "$path_line" >> "$rc"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Warn about missing PATH entries
|
||||
warn_path_missing() {
|
||||
local dir="$1"
|
||||
local label="$2"
|
||||
|
||||
if [[ -z "$dir" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if path_has_dir "$ORIGINAL_PATH" "$dir"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${WARN}→${NC} PATH warning: missing ${label}: ${INFO}${dir}${NC}"
|
||||
echo -e "This can make commands show as \"not found\" in new terminals."
|
||||
echo -e "Fix by adding to your shell config:"
|
||||
echo -e " export PATH=\"${dir}:\$PATH\""
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Temp File Management
|
||||
# ============================================================================
|
||||
|
||||
TMPFILES=()
|
||||
|
||||
# Create a tracked temp file
|
||||
create_temp_file() {
|
||||
local f
|
||||
f=$(mktemp)
|
||||
TMPFILES+=("$f")
|
||||
echo "$f"
|
||||
}
|
||||
|
||||
# Create a tracked temp directory
|
||||
create_temp_dir() {
|
||||
local d
|
||||
d=$(mktemp -d)
|
||||
TMPFILES+=("$d")
|
||||
echo "$d"
|
||||
}
|
||||
|
||||
# Cleanup all temp files
|
||||
cleanup_temp_files() {
|
||||
local f
|
||||
for f in "${TMPFILES[@]:-}"; do
|
||||
rm -rf "$f" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
# Set up cleanup trap
|
||||
setup_cleanup_trap() {
|
||||
trap cleanup_temp_files EXIT
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Platform Summary
|
||||
# ============================================================================
|
||||
|
||||
# Print a summary of the detected platform
|
||||
print_platform_summary() {
|
||||
local os pkg arch init wsl
|
||||
|
||||
os=$(detect_os)
|
||||
pkg=$(detect_package_manager "$os")
|
||||
arch=$(get_arch)
|
||||
init=$(detect_init_system)
|
||||
wsl=$(detect_wsl)
|
||||
|
||||
echo -e "${BOLD}Platform Detection:${NC}"
|
||||
echo -e " OS: ${INFO}$(get_os_name "$os")${NC}"
|
||||
echo -e " Architecture: ${INFO}$arch${NC}"
|
||||
echo -e " Package Mgr: ${INFO}$pkg${NC}"
|
||||
echo -e " Init System: ${INFO}$init${NC}"
|
||||
|
||||
if [[ -n "$wsl" ]]; then
|
||||
echo -e " WSL: ${INFO}$wsl${NC}"
|
||||
fi
|
||||
|
||||
echo -e " Shell: ${INFO}$(get_shell)${NC}"
|
||||
echo -e " RAM: ${INFO}$(get_total_ram)MB${NC}"
|
||||
echo -e " Disk: ${INFO}$(get_available_disk)GB available${NC}"
|
||||
}
|
||||
Reference in New Issue
Block a user