diff options
| author | srdusr <trevorgray@srdusr.com> | 2025-09-09 19:37:46 +0200 |
|---|---|---|
| committer | srdusr <trevorgray@srdusr.com> | 2025-09-09 19:37:46 +0200 |
| commit | 2af67d4a7bb2515472212d9bf4c19e6dd571007c (patch) | |
| tree | fca34988b2d7897cbbd667e1c9c12b71512bf612 /common | |
| parent | 90b21c97d1cc13aa32588366c2044282aae313e3 (diff) | |
| download | dotfiles-2af67d4a7bb2515472212d9bf4c19e6dd571007c.tar.gz dotfiles-2af67d4a7bb2515472212d9bf4c19e6dd571007c.zip | |
Install/packages script
Diffstat (limited to 'common')
| -rwxr-xr-x | common/install.sh | 2462 |
1 files changed, 2462 insertions, 0 deletions
diff --git a/common/install.sh b/common/install.sh new file mode 100755 index 0000000..60abd0b --- /dev/null +++ b/common/install.sh @@ -0,0 +1,2462 @@ +#!/usr/bin/env bash + +# Created By: srdusr +# Created On: Tue 06 Sep 2025 16:20:52 PM CAT +# Project: Dotfiles installation script + +# Dependencies: git, curl + +set -euo pipefail # Exit on error, undefined vars, pipe failures + +#====================================== +# Variables & Configuration +#====================================== + +# Color definitions for pretty UI +NOCOLOR='\033[0m' +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +WHITE='\033[0;37m' +BOLD='\033[1m' + +# Dotfiles configuration +DOTFILES_URL='https://github.com/srdusr/dotfiles.git' +DOTFILES_DIR="$HOME/.cfg" +LOG_FILE="$HOME/.local/share/dotfiles_install.log" +STATE_FILE="$HOME/.local/share/dotfiles_install_state" +BACKUP_DIR="$HOME/.dotfiles-backup-$(date +%Y%m%d-%H%M%S)" +PACKAGES_FILE="packages.yml" + +# Network connectivity check +CONNECTIVITY_CHECKED=false +INTERNET_AVAILABLE=false + +# Installation tracking +INSTALL_SUMMARY=() +FAILED_ITEMS=() +SKIPPED_ITEMS=() +COMPLETED_STEPS=() + +# Script options +RESUME_MODE=false +UPDATE_MODE=false +VERBOSE_MODE=false +DRY_RUN=false +FORCE_MODE=false +INSTALL_MODE="ask" # ask, essentials, full, profile + +# Global variables for system detection +CFG_OS="" +DISTRO="" +PACKAGE_MANAGER="" +PRIVILEGE_TOOL="" +PRIVILEGE_CACHED=false + +# Essential tools needed by this script +ESSENTIAL_TOOLS=("git" "curl" "wget") +PACKAGE_TOOLS=("yq" "jq") + +# Installation profiles +declare -A INSTALLATION_PROFILES=( + ["essentials"]="Essential packages only (git, curl, wget, vim, zsh)" + ["minimal"]="Minimal setup for basic development" + ["dev"]="Full development environment" + ["server"]="Server configuration" + ["full"]="Complete installation with all packages" +) + +# Installation steps configuration +declare -A INSTALLATION_STEPS=( + ["setup_environment"]="Setup installation environment" + ["check_connectivity"]="Check internet connectivity" + ["install_dependencies"]="Install dependencies" + ["install_dotfiles"]="Install dotfiles repository" + ["setup_user_dirs"]="Setup user directories" + ["install_essentials"]="Install essential tools" + ["install_packages"]="Install system packages" + ["setup_shell"]="Setup shell environment" + ["setup_ssh"]="Setup SSH configuration" + ["configure_services"]="Configure system services" + ["setup_development"]="Setup development environment" + ["apply_tweaks"]="Apply system tweaks" + ["deploy_config"]="Deploy config command and dotfiles" +) + +# Step order (important for dependencies) +STEP_ORDER=( + "setup_environment" + "check_connectivity" + "install_dependencies" + "install_dotfiles" + "setup_user_dirs" + "install_essentials" + "install_packages" + "setup_shell" + "setup_ssh" + "configure_services" + "setup_development" + "apply_tweaks" + "deploy_config" +) + +#====================================== +# State Management Functions +#====================================== + +save_state() { + local current_step="$1" + local status="$2" + + mkdir -p "$(dirname "$STATE_FILE")" + + { + echo "LAST_STEP=$current_step" + echo "STEP_STATUS=$status" + echo "TIMESTAMP=$(date +%s)" + echo "RESUME_AVAILABLE=true" + echo "PRIVILEGE_CACHED=$PRIVILEGE_CACHED" + echo "INSTALL_MODE=$INSTALL_MODE" + echo "COMPLETED_STEPS=(${COMPLETED_STEPS[*]})" + echo "CFG_OS=$CFG_OS" + echo "DISTRO=${DISTRO:-}" + echo "PACKAGE_MANAGER=${PACKAGE_MANAGER:-}" + echo "PRIVILEGE_TOOL=${PRIVILEGE_TOOL:-}" + echo "CONNECTIVITY_CHECKED=$CONNECTIVITY_CHECKED" + echo "INTERNET_AVAILABLE=$INTERNET_AVAILABLE" + } > "$STATE_FILE" +} + +load_state() { + if [[ -f "$STATE_FILE" ]]; then + source "$STATE_FILE" + return 0 + else + return 1 + fi +} + +clear_state() { + [[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE" +} + +is_step_completed() { + local step="$1" + [[ " ${COMPLETED_STEPS[*]} " =~ " ${step} " ]] +} + +mark_step_completed() { + local step="$1" + if ! is_step_completed "$step"; then + COMPLETED_STEPS+=("$step") + fi + save_state "$step" "completed" +} + +mark_step_failed() { + local step="$1" + save_state "$step" "failed" +} + +#====================================== +# UI Functions +#====================================== + +print_color() { + local color="$1" + local message="$2" + echo -e "${color}${message}${NOCOLOR}" + + if [[ -n "${LOG_FILE:-}" && -f "$LOG_FILE" ]]; then + echo "$(date +'%Y-%m-%d %H:%M:%S') - $message" >> "$LOG_FILE" + fi +} + +print_header() { + local title="$1" + local border_char="=" + local border_length=60 + + echo + print_color "$CYAN" "$(printf '%*s' $border_length '' | tr ' ' "$border_char")" + print_color "$CYAN$BOLD" "$(printf '%*s' $(((border_length + ${#title}) / 2)) "$title")" + print_color "$CYAN" "$(printf '%*s' $border_length '' | tr ' ' "$border_char")" + echo +} + +print_section() { + local title="$1" + echo + print_color "$BLUE$BOLD" "▶ $title" + print_color "$BLUE" "$(printf '%*s' $((${#title} + 2)) '' | tr ' ' '-')" +} + +print_success() { + local message="$1" + print_color "$GREEN" "✓ $message" + INSTALL_SUMMARY+=("✓ $message") +} + +print_error() { + local message="$1" + print_color "$RED" "✗ $message" >&2 + FAILED_ITEMS+=("✗ $message") +} + +print_warning() { + local message="$1" + print_color "$YELLOW" "⚠ $message" +} + +print_info() { + local message="$1" + if [[ "$VERBOSE_MODE" == true ]] || [[ "${2:-}" == "always" ]]; then + print_color "$CYAN" "ℹ $message" + fi +} + +print_skip() { + local message="$1" + print_color "$YELLOW" "⏭ $message" + SKIPPED_ITEMS+=("⏭ $message") +} + +print_dry_run() { + local message="$1" + print_color "$MAGENTA" "[DRY RUN] $message" +} + +#====================================== +# Network Connectivity Functions +#====================================== + +check_internet_connectivity() { + if [[ "$CONNECTIVITY_CHECKED" == true ]]; then + return $([[ "$INTERNET_AVAILABLE" == true ]] && echo 0 || echo 1) + fi + + print_section "Checking Internet Connectivity" + + local test_urls=("8.8.8.8" "1.1.1.1" "google.com" "github.com") + + for url in "${test_urls[@]}"; do + if ping -c 1 -W 2 "$url" &>/dev/null || curl -s --connect-timeout 5 "https://$url" &>/dev/null; then + INTERNET_AVAILABLE=true + CONNECTIVITY_CHECKED=true + print_success "Internet connectivity confirmed" + return 0 + fi + done + + INTERNET_AVAILABLE=false + CONNECTIVITY_CHECKED=true + print_error "No internet connectivity detected" + + # Try to connect to WiFi or prompt user + attempt_network_connection + + return 1 +} + +attempt_network_connection() { + print_warning "Attempting to establish network connection..." + + # Try NetworkManager + if command_exists nmcli; then + print_info "Available WiFi networks:" + nmcli device wifi list 2>/dev/null || print_warning "Could not list WiFi networks" + + if prompt_user "Would you like to connect to a WiFi network?"; then + print_color "$YELLOW" "Enter WiFi network name (SSID): " + read -r wifi_ssid + if [[ -n "$wifi_ssid" ]]; then + print_color "$YELLOW" "Enter WiFi password: " + read -rs wifi_password + echo + + if execute_with_privilege "nmcli device wifi connect '$wifi_ssid' password '$wifi_password'"; then + print_success "Connected to WiFi network: $wifi_ssid" + # Re-check connectivity + CONNECTIVITY_CHECKED=false + check_internet_connectivity + return $? + else + print_error "Failed to connect to WiFi network" + fi + fi + fi + fi + + # Try other connection methods + if command_exists iwctl; then + print_info "You can also connect manually using iwctl" + fi + + return 1 +} + +#====================================== +# System Detection Functions +#====================================== + +detect_os() { + case "$(uname -s)" in + Linux) CFG_OS="linux" ;; + Darwin) CFG_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;; + *) CFG_OS="unknown" ;; + esac + + print_info "Detected OS: $CFG_OS" "always" +} + +detect_privilege_tools() { + if [[ "$(id -u)" -eq 0 ]]; then + PRIVILEGE_TOOL="" + print_info "Running as root, no privilege escalation needed" + return 0 + fi + + for tool in sudo doas pkexec; do + if command -v "$tool" &>/dev/null; then + PRIVILEGE_TOOL="$tool" + print_success "Using privilege escalation tool: $PRIVILEGE_TOOL" + return 0 + fi + done + + print_warning "No privilege escalation tool found (sudo, doas, pkexec)" + PRIVILEGE_TOOL="" + return 1 +} + +test_privilege_access() { + if [[ "$PRIVILEGE_CACHED" == true ]]; then + return 0 + fi + + if [[ -z "$PRIVILEGE_TOOL" ]]; then + return 0 # Running as root or no privilege needed + fi + + print_info "Testing privilege access..." + if "$PRIVILEGE_TOOL" -v &>/dev/null || echo "test" | "$PRIVILEGE_TOOL" -S true &>/dev/null; then + PRIVILEGE_CACHED=true + print_success "Privilege access confirmed" + return 0 + else + print_error "Failed to obtain privilege access" + return 1 + fi +} + +detect_package_manager() { + # First try to detect from OS release files + if [[ "$CFG_OS" == "linux" && -f /etc/os-release ]]; then + source /etc/os-release + case "$ID" in + arch|manjaro|endeavouros|artix) + DISTRO="$ID" + PACKAGE_MANAGER="pacman" ;; + debian|ubuntu|mint|pop|elementary|zorin) + DISTRO="$ID" + PACKAGE_MANAGER="apt" ;; + fedora|rhel|centos|rocky|almalinux) + DISTRO="$ID" + PACKAGE_MANAGER="dnf" ;; + opensuse*|sles) + DISTRO="$ID" + PACKAGE_MANAGER="zypper" ;; + gentoo|funtoo) + DISTRO="$ID" + PACKAGE_MANAGER="portage" ;; + alpine) + DISTRO="$ID" + PACKAGE_MANAGER="apk" ;; + void) + DISTRO="$ID" + PACKAGE_MANAGER="xbps" ;; + nixos) + DISTRO="$ID" + PACKAGE_MANAGER="nix" ;; + *) + print_warning "Unknown distribution: $ID, trying to detect package manager directly" + ;; + esac + elif [[ "$CFG_OS" == "macos" ]]; then + DISTRO="macos" + if command -v brew &>/dev/null; then + PACKAGE_MANAGER="brew" + else + PACKAGE_MANAGER="brew-install" # Will install homebrew + fi + fi + + # Fallback: detect by available commands + if [[ -z "$PACKAGE_MANAGER" ]]; then + local managers=( + "pacman:pacman" + "apt:apt" + "dnf:dnf" + "yum:yum" + "zypper:zypper" + "emerge:portage" + "apk:apk" + "xbps-install:xbps" + "nix-env:nix" + "pkg:pkg" + "brew:brew" + ) + + for manager in "${managers[@]}"; do + local cmd="${manager%:*}" + local name="${manager#*:}" + if command -v "$cmd" &>/dev/null; then + PACKAGE_MANAGER="$name" + break + fi + done + fi + + if [[ -n "$PACKAGE_MANAGER" ]]; then + print_success "Detected package manager: $PACKAGE_MANAGER" + [[ -n "$DISTRO" ]] && print_info "Distribution: $DISTRO" + return 0 + else + print_error "Could not detect package manager" + return 1 + fi +} + +#====================================== +# Utility Functions +#====================================== + +command_exists() { + command -v "$1" &>/dev/null +} + +execute_command() { + local cmd="$*" + + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "$cmd" + return 0 + fi + + if [[ "$VERBOSE_MODE" == true ]]; then + print_info "Running: $cmd" + fi + + eval "$cmd" +} + +execute_with_privilege() { + local cmd="$*" + + if [[ "$DRY_RUN" == true ]]; then + if [[ -n "$PRIVILEGE_TOOL" ]]; then + print_dry_run "$PRIVILEGE_TOOL $cmd" + else + print_dry_run "$cmd" + fi + return 0 + fi + + if [[ -n "$PRIVILEGE_TOOL" ]]; then + if [[ "$PRIVILEGE_CACHED" != true ]]; then + test_privilege_access || return 1 + fi + eval "$PRIVILEGE_TOOL $cmd" + else + eval "$cmd" + fi +} + +prompt_user() { + local question="$1" + local default="${2:-Y}" + local response + + if [[ "$FORCE_MODE" == true ]]; then + print_info "Auto-answering '$question' with: $default" + [[ "$default" =~ ^[Yy] ]] && return 0 || return 1 + fi + + while true; do + if [[ "$default" == "Y" ]]; then + print_color "$YELLOW" "$question [Y/n]: " + else + print_color "$YELLOW" "$question [y/N]: " + fi + + read -r response + + if [[ -z "$response" ]]; then + response="$default" + fi + + case "${response^^}" in + Y|YES) return 0 ;; + N|NO) return 1 ;; + *) print_warning "Please answer Y/yes or N/no" ;; + esac + done +} + +create_dir() { + local dir="$1" + local permissions="${2:-755}" + + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "Create directory: $dir (mode: $permissions)" + return 0 + fi + + if [[ ! -d "$dir" ]]; then + mkdir -p "$dir" || { + print_error "Failed to create directory: $dir" + return 1 + } + chmod "$permissions" "$dir" + print_success "Created directory: $dir" + else + print_info "Directory already exists: $dir" + fi +} + +setup_logging() { + local log_dir + log_dir="$(dirname "$LOG_FILE")" + + if [[ ! -d "$log_dir" ]]; then + mkdir -p "$log_dir" || { + print_error "Failed to create log directory: $log_dir" + exit 1 + } + fi + + { + echo "=======================================" + echo "Dotfiles Installation Log" + echo "Date: $(date)" + echo "User: $USER" + echo "Host: ${HOSTNAME:-$(hostname)}" + echo "OS: $(uname -s)" + echo "Install Mode: $INSTALL_MODE" + echo "=======================================" + echo + } > "$LOG_FILE" + + print_info "Log file initialized: $LOG_FILE" "always" +} + +get_package_name() { + local package="$1" + local packages_file="${2:-}" + + # If packages.yml is available, check for distribution-specific mappings + if [[ -n "$packages_file" ]] && [[ -f "$packages_file" ]] && command_exists yq; then + local distro_package="" + + # Try to get package name for current distribution + case "$DISTRO" in + arch|manjaro|endeavouros|artix) + distro_package=$(yq eval ".arch.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + debian|ubuntu|mint|pop|elementary|zorin) + distro_package=$(yq eval ".debian.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + fedora|rhel|centos|rocky|almalinux) + distro_package=$(yq eval ".rhel.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + opensuse*|sles) + distro_package=$(yq eval ".opensuse.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + gentoo|funtoo) + distro_package=$(yq eval ".gentoo.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + ;; + macos) + distro_package=$(yq eval ".macos[]" "$packages_file" 2>/dev/null | grep "^$package$" || echo "") + ;; + esac + + # Return the distribution-specific package name if found + if [[ -n "$distro_package" ]]; then + echo "$distro_package" + return 0 + fi + fi + + # Fallback to original package name + echo "$package" +} + +get_package_use_flags() { + local package="$1" + local packages_file="${2:-}" + + # Only relevant for Gentoo/Portage + if [[ "$PACKAGE_MANAGER" != "portage" ]]; then + echo "" + return 0 + fi + + if [[ -n "$packages_file" ]] && [[ -f "$packages_file" ]] && command_exists yq; then + local use_flags + use_flags=$(yq eval ".gentoo_use_flags.$package" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") + echo "$use_flags" + else + echo "" + fi +} + +#====================================== +# Dependency Installation Functions +#====================================== + +install_dependencies_if_missing() { + print_section "Checking for dependencies git, wget/curl" + save_state "install_dependencies" "started" + + local missing_deps=() + local failed_deps=() + + # Check for missing essential tools + for tool in "${ESSENTIAL_TOOLS[@]}"; do + if ! command_exists "$tool"; then + missing_deps+=("$tool") + fi + done + + # If no internet and dependencies are missing, try offline packages + if [[ "$INTERNET_AVAILABLE" != true ]] && [[ ${#missing_deps[@]} -gt 0 ]]; then + print_warning "No internet connection available" + print_info "Attempting to install dependencies from local packages..." + + # Try to install from local package cache + for tool in "${missing_deps[@]}"; do + if install_package_offline "$tool"; then + print_success "Installed $tool from local cache" + else + failed_deps+=("$tool") + fi + done + elif [[ ${#missing_deps[@]} -gt 0 ]]; then + # Online installation + print_info "Installing missing dependencies: ${missing_deps[*]}" + update_package_database + + for tool in "${missing_deps[@]}"; do + if install_single_package "$tool" "dependency"; then + print_success "Installed dependency: $tool" + else + failed_deps+=("$tool") + fi + done + fi + + if [[ ${#failed_deps[@]} -gt 0 ]]; then + print_error "Failed to install dependencies: ${failed_deps[*]}" + mark_step_failed "install_dependencies" + return 1 + else + mark_step_completed "install_dependencies" + return 0 + fi +} + +install_package_offline() { + local package="$1" + + case "$PACKAGE_MANAGER" in + pacman) + # Check if package is in cache + if execute_with_privilege "pacman -U /var/cache/pacman/pkg/${package}-*.pkg.tar.*" 2>/dev/null; then + return 0 + fi + ;; + apt) + # Try from local cache + if execute_with_privilege "apt-get install --no-download '$package'" 2>/dev/null; then + return 0 + fi + ;; + esac + + return 1 +} + +#====================================== +# Package Management Functions +#====================================== + +install_single_package() { + local package="$1" + local package_type="${2:-system}" + local packages_file="${3:-}" + + # Get the correct package name for this distro + local pkg_name + pkg_name=$(get_package_name "$package" "$packages_file") + + # Get USE flags for Gentoo + local use_flags + use_flags=$(get_package_use_flags "$package" "$packages_file") + + print_info "Installing $package_type package: $pkg_name" + + case "$PACKAGE_MANAGER" in + pacman) + execute_with_privilege "pacman -S --noconfirm '$pkg_name'" ;; + apt) + execute_with_privilege "apt-get install -y '$pkg_name'" ;; + dnf) + execute_with_privilege "dnf install -y '$pkg_name'" ;; + yum) + execute_with_privilege "yum install -y '$pkg_name'" ;; + zypper) + execute_with_privilege "zypper install -y '$pkg_name'" ;; + portage) + local emerge_cmd="emerge" + if [[ -n "$use_flags" ]]; then + emerge_cmd="USE='$use_flags' emerge" + print_info "Using USE flags for $pkg_name: $use_flags" + fi + execute_with_privilege "$emerge_cmd '$pkg_name'" ;; + apk) + execute_with_privilege "apk add '$pkg_name'" ;; + xbps) + execute_with_privilege "xbps-install -y '$pkg_name'" ;; + nix) + execute_command "nix-env -iA nixpkgs.$pkg_name" ;; + brew) + execute_command "brew install '$pkg_name'" ;; + brew-install) + print_error "Homebrew not installed. Please install it first." + return 1 ;; + *) + print_error "Package manager '$PACKAGE_MANAGER' not supported" + return 1 ;; + esac +} + +update_package_database() { + print_info "Updating package database..." + + case "$PACKAGE_MANAGER" in + pacman) + execute_with_privilege "pacman -Sy" ;; + apt) + execute_with_privilege "apt-get update" ;; + dnf) + execute_with_privilege "dnf check-update" || true ;; + yum) + execute_with_privilege "yum check-update" || true ;; + zypper) + execute_with_privilege "zypper refresh" ;; + portage) + execute_with_privilege "emerge --sync" ;; + apk) + execute_with_privilege "apk update" ;; + xbps) + execute_with_privilege "xbps-install -S" ;; + brew) + execute_command "brew update" ;; + *) + print_info "Package database update not needed for $PACKAGE_MANAGER" ;; + esac +} + +install_homebrew() { + if command_exists brew; then + print_info "Homebrew already installed" + return 0 + fi + + print_info "Installing Homebrew..." + if execute_command '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'; then + print_success "Homebrew installed" + PACKAGE_MANAGER="brew" + + # Add to PATH for current 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 + return 0 + else + print_error "Failed to install Homebrew" + return 1 + fi +} + +install_yq() { + if command_exists yq; then + print_info "yq already installed" + return 0 + fi + + print_info "Installing yq..." + + local bin_dir="$HOME/.local/bin" + create_dir "$bin_dir" + + local yq_path="$bin_dir/yq" + local yq_url="" + + case "$(uname -m)" in + x86_64|amd64) + yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" ;; + aarch64|arm64) + yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64" ;; + armv7l) + yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm" ;; + *) + print_error "Unsupported architecture: $(uname -m)" + return 1 ;; + esac + + if execute_command "curl -L '$yq_url' -o '$yq_path'"; then + execute_command "chmod +x '$yq_path'" + + # Add to PATH if not already there + if [[ ":$PATH:" != *":$bin_dir:"* ]]; then + export PATH="$bin_dir:$PATH" + fi + + print_success "yq installed successfully" + return 0 + else + print_error "Failed to install yq" + return 1 + fi +} + +parse_packages_from_yaml() { + local packages_file="$1" + local section="$2" + local packages=() + + if [[ ! -f "$packages_file" ]]; then + print_warning "Package file not found: $packages_file" + return 1 + fi + + if ! command_exists yq; then + print_error "yq not available for parsing packages.yml" + return 1 + fi + + # Try to parse packages from the specified section + if yq eval ".$section" "$packages_file" &>/dev/null; then + mapfile -t packages < <(yq eval ".$section[]" "$packages_file" 2>/dev/null | grep -v "^null$" || true) + fi + + # Output packages + printf '%s\n' "${packages[@]}" +} + +install_packages_from_yaml() { + local packages_file="$1" + local profile="${2:-essentials}" + local failed_packages=() + local installed_count=0 + + print_section "Installing Packages (Profile: $profile)" + + if [[ ! -f "$packages_file" ]]; then + print_warning "Package file not found: $packages_file, skipping package installation" + return 0 + fi + + # Define sections to install based on profile + local sections=() + case "$profile" in + essentials) + sections=("common" "essentials") ;; + minimal) + sections=("common" "essentials" "minimal") ;; + dev) + sections=("common" "essentials" "dev") ;; + server) + sections=("common" "essentials" "server") ;; + full) + sections=("common" "essentials" "dev" "server" "desktop") ;; + *) + if [[ -f "profiles/$profile.yml" ]]; then + packages_file="profiles/$profile.yml" + sections=("packages") + else + print_error "Unknown profile: $profile" + return 1 + fi + ;; + esac + + # Install packages from each section + for section in "${sections[@]}"; do + print_info "Installing packages from section: $section" + + local packages + mapfile -t packages < <(parse_packages_from_yaml "$packages_file" "$section") + + if [[ ${#packages[@]} -eq 0 ]]; then + print_info "No packages found in section: $section" + continue + fi + + print_info "Found ${#packages[@]} packages in section $section" + + for package in "${packages[@]}"; do + [[ -z "$package" ]] && continue + + if install_single_package "$package" "$section" "$packages_file"; then + print_success "Installed: $package" + ((installed_count++)) + else + print_error "Failed to install: $package" + failed_packages+=("$package") + fi + done + done + + print_info "Package installation summary:" + print_color "$GREEN" " Installed: $installed_count" + print_color "$RED" " Failed: ${#failed_packages[@]}" + + if [[ ${#failed_packages[@]} -gt 0 ]]; then + print_warning "Failed packages: ${failed_packages[*]}" + print_info "Failed packages will be listed in the final summary" + return 0 + else + print_success "All packages installed successfully" + return 0 + fi +} + +#====================================== +# Dotfiles Management System (Config Command) +#====================================== + +install_config_command() { + print_info "Installing config command for dotfiles management" + + # Known function files where cfg might already be defined + local function_files=( + "$HOME/.config/zsh/user/functions.zsh" + "$HOME/.bashrc" + ) + + # Check if cfg is already defined + local cfg_defined=false + for f in "${function_files[@]}"; do + if [[ -f "$f" ]] && grep -q '^\s*cfg\s*()' "$f"; then + cfg_defined=true + # Source the file to make cfg available in current session + # Only source if not already sourced + if ! type cfg >/dev/null 2>&1; then + # shellcheck disable=SC1090 + source "$f" + print_info "Sourced cfg from $f" + fi + break + fi + done + + if [[ "$cfg_defined" == true ]]; then + print_info "cfg function already defined, no need to append" + return + fi + + # Determine current shell + local current_shell + current_shell=$(basename "$SHELL") + + local profile_files=() + + case "$current_shell" in + bash) + profile_files+=("$HOME/.bashrc") + [[ -f "$HOME/.profile" ]] && profile_files+=("$HOME/.profile") + ;; + zsh) + profile_files+=("$HOME/.zshrc") + [[ -f "$HOME/.config/zsh/.zshrc" ]] && profile_files+=("$HOME/.config/zsh/.zshrc") + [[ -f "$HOME/.profile" ]] && profile_files+=("$HOME/.profile") + ;; + *) + [[ -f "$HOME/.profile" ]] && profile_files+=("$HOME/.profile") + ;; + esac + + # If no profile files exist, create .bashrc + if [[ ${#profile_files[@]} -eq 0 ]]; then + profile_files+=("$HOME/.bashrc") + touch "$HOME/.bashrc" + fi + + # Append cfg function to profiles if not already present + for profile in "${profile_files[@]}"; do + if [[ -w "$profile" ]] && ! grep -q "# Dotfiles config function" "$profile" 2>/dev/null; then + cat >> "$profile" << 'EOF' + +# Dotfiles Management System +if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then + # Core git wrapper with repository as work-tree + _config() { + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" "$@" + } + + # Detect OS + case "$(uname -s)" in + Linux) CFG_OS="linux" ;; + Darwin) CFG_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;; + *) CFG_OS="other" ;; + esac + + # Map system path to repository path + _repo_path() { + local f="$1" + + # If it's an absolute path that's not in HOME, handle it specially + if [[ "$f" == /* && "$f" != "$HOME/"* ]]; then + echo "$CFG_OS/${f#/}" + return + fi + + # Check for paths that should go to the repository root + case "$f" in + common/*|linux/*|macos/*|windows/*|profile/*|README.md) + echo "$f" + return + ;; + "$HOME/"*) + f="${f#$HOME/}" + ;; + esac + + # Default: put under OS-specific home + echo "$CFG_OS/home/$f" + } + + _sys_path() { + local repo_path="$1" + local os_path_pattern="$CFG_OS/" + + # Handle OS-specific files that are not in the home subdirectory + if [[ "$repo_path" == "$os_path_pattern"* && "$repo_path" != */home/* ]]; then + echo "/${repo_path#$os_path_pattern}" + return + fi + + case "$repo_path" in + # Common configs → OS-specific config dirs + common/config/*) + case "$CFG_OS" in + linux) + local base="${XDG_CONFIG_HOME:-$HOME/.config}" + echo "$base/${repo_path#common/config/}" + ;; + macos) + echo "$HOME/Library/Application Support/${repo_path#common/config/}" + ;; + windows) + echo "$LOCALAPPDATA\\${repo_path#common/config/}" + ;; + *) + echo "$HOME/.config/${repo_path#common/config/}" + ;; + esac + ;; + + # Common assets → stay in repo + common/assets/*) + echo "$HOME/.cfg/$repo_path" + ;; + + # Other common files (dotfiles like .bashrc, .gitconfig, etc.) → $HOME + common/*) + echo "$HOME/${repo_path#common/}" + ;; + + # OS-specific home + */home/*) + echo "$HOME/${repo_path#*/home/}" + ;; + + # Profile configs and README → stay in repo + profile/*|README.md) + echo "$HOME/.cfg/$repo_path" + ;; + + # Default fallback + *) + echo "$HOME/.cfg/$repo_path" + ;; + + esac + } + + # Prompts for sudo if needed and runs the command + _sudo_prompt() { + if [[ $EUID -eq 0 ]]; then + "$@" + else + if command -v sudo >/dev/null; then + sudo "$@" + elif command -v doas >/dev/null; then + doas "$@" + elif command -v pkexec >/dev/null; then + pkexec "$@" + else + echo "Error: No privilege escalation tool found." + return 1 + fi + fi + } + + # Main config command + config() { + local cmd="$1"; shift + local target_dir="" + # Parse optional --target flag for add + if [[ "$cmd" == "add" ]]; then + while [[ "$1" == --* ]]; do + case "$1" in + --target|-t) + target_dir="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + return 1 + ;; + esac + done + fi + + case "$cmd" in + add) + local file_path + for file_path in "$@"; do + local repo_path + if [[ -n "$target_dir" ]]; then + local rel_path + if [[ "$file_path" == /* ]]; then + rel_path="$(basename "$file_path")" + else + rel_path="$file_path" + fi + repo_path="$target_dir/$rel_path" + else + repo_path="$(_repo_path "$file_path")" + fi + + local full_repo_path="$HOME/.cfg/$repo_path" + mkdir -p "$(dirname "$full_repo_path")" + cp -a "$file_path" "$full_repo_path" + + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" add "$repo_path" + + echo "Added: $file_path -> $repo_path" + done + ;; + rm) + local rm_opts="" + local file_path_list=() + + for arg in "$@"; do + if [[ "$arg" == "-"* ]]; then + rm_opts+=" $arg" + else + file_path_list+=("$arg") + fi + done + + for file_path in "${file_path_list[@]}"; do + local repo_path="$(_repo_path "$file_path")" + + if [[ "$rm_opts" == *"-r"* ]]; then + _config rm --cached -r "$repo_path" + else + _config rm --cached "$repo_path" + fi + + eval "rm $rm_opts \"$file_path\"" + echo "Removed: $file_path" + done + ;; + sync) + local direction="${1:-to-repo}"; shift + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ "$direction" == "to-repo" ]]; then + if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + cp -a "$sys_file" "$full_repo_path" + echo "Synced to repo: $sys_file" + fi + elif [[ "$direction" == "from-repo" ]]; then + if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then + local dest_dir="$(dirname "$sys_file")" + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + echo "Synced from repo: $sys_file" + fi + fi + done + ;; + status) + local auto_synced=() + while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ -e "$sys_file" && -e "$full_repo_path" ]]; then + if ! diff -q "$full_repo_path" "$sys_file" >/dev/null 2>&1; then + cp -fa "$sys_file" "$full_repo_path" + auto_synced+=("$repo_file") + fi + fi + done < <(_config ls-files) + if [[ ${#auto_synced[@]} -gt 0 ]]; then + echo "=== Auto-synced Files ===" + for repo_file in "${auto_synced[@]}"; do + echo "synced: $(_sys_path "$repo_file") -> $repo_file" + done + echo + fi + _config status + echo + ;; + deploy) + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" # destination only + + # Only continue if the source exists + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if needed + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Deployed: $repo_file -> $sys_file" + fi + done + ;; + checkout) + echo "Checking out dotfiles from .cfg..." + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" + + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if it doesn't exist + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Checked out: $repo_file -> $sys_file" + fi + done + ;; + backup) + local timestamp=$(date +%Y%m%d%H%M%S) + local backup_dir="$HOME/.dotfiles_backup/$timestamp" + echo "Backing up existing dotfiles to $backup_dir..." + + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + if [[ -e "$sys_file" ]]; then + local dest_dir_full="$backup_dir/$(dirname "$repo_file")" + mkdir -p "$dest_dir_full" + cp -a "$sys_file" "$backup_dir/$repo_file" + fi + done + echo "Backup complete. To restore, copy files from $backup_dir to their original locations." + ;; + *) + _config "$cmd" "$@" + ;; + esac + } +fi +EOF + print_success "Added config function to $profile" + else + print_info "Config function already exists in $profile or file not writable" + fi + done + + return 0 +} + + +deploy_config() { + print_section "Deploying Configuration" + save_state "deploy_config" "started" + + # Install and setup the config command first + install_config_command + + # Deploy dotfiles from repository to system + if [[ -d "$DOTFILES_DIR" ]]; then + print_info "Deploying dotfiles from repository to system locations..." + + # Source shell configuration to make config function available + reload_shell_config + + # Check if config function is available + if declare -f config >/dev/null 2>&1 || type config >/dev/null 2>&1; then + print_info "Config function available, deploying files..." + + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "config restore ." + print_dry_run "config reset" + print_dry_run "config deploy" + else + # Use the config function to deploy files + if config deploy; then + print_success "Dotfiles deployed successfully" + else + print_warning "Some files may have failed to deploy" + fi + fi + else + print_info "Config function not available, using manual deployment..." + manual_deploy_dotfiles + fi + + # Set appropriate permissions + set_dotfile_permissions + + else + print_warning "Dotfiles directory not found, skipping deployment" + fi + + mark_step_completed "deploy_config" +} + +reload_shell_config() { + print_info "Reloading shell configuration..." + + # Source common shell files if they exist + local shell_files=() + + case "$(basename "$SHELL")" in + bash) + shell_files+=("$HOME/.bashrc" "$HOME/.profile") + ;; + zsh) + shell_files+=("$HOME/.zshrc" "$HOME/.config/zsh/.zshrc" "$HOME/.profile") + ;; + *) + shell_files+=("$HOME/.profile") + ;; + esac + + for shell_file in "${shell_files[@]}"; do + if [[ -f "$shell_file" ]]; then + print_info "Sourcing: $shell_file" + # shellcheck disable=SC1090 + source "$shell_file" 2>/dev/null || print_warning "Failed to source $shell_file" + fi + done +} + +#====================================== +# Installation Step Functions +#====================================== + +setup_environment() { + print_section "Setting Up Environment" + save_state "setup_environment" "started" + + detect_os + detect_privilege_tools + detect_package_manager || { + print_error "Cannot proceed without a supported package manager" + mark_step_failed "setup_environment" + return 1 + } + + if [[ -n "$PRIVILEGE_TOOL" ]]; then + test_privilege_access || { + print_error "Cannot obtain necessary privileges" + mark_step_failed "setup_environment" + return 1 + } + fi + + mark_step_completed "setup_environment" +} + +check_connectivity() { + print_section "Checking Connectivity" + save_state "check_connectivity" "started" + + if check_internet_connectivity; then + mark_step_completed "check_connectivity" + return 0 + else + print_warning "Limited internet connectivity - some features may be unavailable" + mark_step_completed "check_connectivity" + return 0 # Don't fail completely + fi +} + +install_dependencies() { + print_section "Installing Dependencies" + save_state "install_dependencies" "started" + + if install_dependencies_if_missing; then + mark_step_completed "install_dependencies" + return 0 + else + mark_step_failed "install_dependencies" + return 1 + fi +} + +install_dotfiles() { + print_section "Installing Dotfiles" + save_state "install_dotfiles" "started" + + local update=false + + # Check internet connectivity for git operations + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping dotfiles installation" + mark_step_completed "install_dotfiles" + return 0 + fi + + if [[ -d "$DOTFILES_DIR" ]]; then + if [[ "$UPDATE_MODE" == true ]] || prompt_user "Dotfiles repository already exists. Update it?"; then + print_info "Updating existing dotfiles..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then + update=true + print_success "Dotfiles updated successfully" + else + print_error "Failed to pull updates" + mark_step_failed "install_dotfiles" + return 1 + fi + else + print_skip "Skipping dotfiles update" + mark_step_completed "install_dotfiles" + return 0 + fi + else + print_info "Cloning dotfiles repository..." + if execute_command "git clone --bare '$DOTFILES_URL' '$DOTFILES_DIR'"; then + print_success "Dotfiles repository cloned" + else + print_error "Failed to clone dotfiles repository" + mark_step_failed "install_dotfiles" + return 1 + fi + fi + + # Configure the repository + execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' config status.showUntrackedFiles no" + + mark_step_completed "install_dotfiles" + print_success "Dotfiles installed successfully" +} + +setup_user_dirs() { + print_section "Setting Up User Directories" + save_state "setup_user_dirs" "started" + + local directories=('.cache' '.config' '.local/bin' '.local/share' '.scripts') + + for dir in "${directories[@]}"; do + create_dir "$HOME/$dir" + done + + # Set up XDG directories + if command_exists xdg-user-dirs-update; then + execute_command "xdg-user-dirs-update" + print_success "XDG user directories configured" + fi + + mark_step_completed "setup_user_dirs" +} + +install_essentials() { + print_section "Installing Essential Tools" + save_state "install_essentials" "started" + + # Install package processing tools first + for tool in "${PACKAGE_TOOLS[@]}"; do + if ! command_exists "$tool"; then + case "$tool" in + yq) + if install_yq; then + print_success "Installed package tool: $tool" + else + print_error "Failed to install package tool: $tool" + mark_step_failed "install_essentials" + return 1 + fi + ;; + jq) + if install_single_package "jq" "essential"; then + print_success "Installed package tool: $tool" + else + print_error "Failed to install package tool: $tool" + mark_step_failed "install_essentials" + return 1 + fi + ;; + esac + else + print_info "Package tool already available: $tool" + fi + done + + mark_step_completed "install_essentials" +} + +install_packages() { + print_section "Installing Packages" + save_state "install_packages" "started" + + # Skip if essentials-only mode + if [[ "$INSTALL_MODE" == "essentials" ]]; then + print_skip "Package installation (essentials-only mode)" + mark_step_completed "install_packages" + return 0 + fi + + # Skip if no internet and packages require download + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping package installation" + mark_step_completed "install_packages" + return 0 + fi + + # Determine profile to install + local profile="$INSTALL_MODE" + if [[ "$INSTALL_MODE" == "ask" ]]; then + profile="dev" # Default + fi + + # Change to home directory to find packages.yml + local original_dir="$PWD" + cd "$HOME" 2>/dev/null || true + + # Look for packages.yml in common locations + local packages_files=("$PACKAGES_FILE" "common/$PACKAGES_FILE" ".cfg/common/$PACKAGES_FILE") + local found_packages_file="" + + for pf in "${packages_files[@]}"; do + if [[ -f "$pf" ]]; then + found_packages_file="$pf" + break + fi + done + + if [[ -n "$found_packages_file" ]]; then + if install_packages_from_yaml "$found_packages_file" "$profile"; then + mark_step_completed "install_packages" + else + print_warning "Some packages failed to install, but continuing..." + mark_step_completed "install_packages" # Don't fail the whole installation + fi + else + print_warning "packages.yml not found, skipping package installation" + mark_step_completed "install_packages" + fi + + cd "$original_dir" 2>/dev/null || true +} + +setup_shell() { + print_section "Setting Up Shell Environment" + save_state "setup_shell" "started" + + if command_exists zsh; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Change default shell to Zsh?"; then + local zsh_path + zsh_path="$(command -v zsh)" + if execute_with_privilege "chsh -s '$zsh_path' '$USER'"; then + print_success "Default shell changed to Zsh" + print_warning "Please log out and log back in to apply changes" + else + print_error "Failed to change default shell" + fi + fi + else + print_warning "Zsh not installed, skipping shell setup" + fi + + # Install Zsh plugins if in dotfiles directory + if [[ -f "$HOME/.zshrc" || -f "$HOME/.config/zsh/.zshrc" ]]; then + install_zsh_plugins + fi + + mark_step_completed "setup_shell" +} + +install_zsh_plugins() { + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping Zsh plugins installation" + return 0 + fi + + local zsh_plugins_dir="$HOME/.config/zsh/plugins" + + print_info "Installing Zsh plugins..." + create_dir "$HOME/.config/zsh" + create_dir "$zsh_plugins_dir" + + local plugins=( + "zsh-you-should-use:https://github.com/MichaelAquilina/zsh-you-should-use.git" + "zsh-syntax-highlighting:https://github.com/zsh-users/zsh-syntax-highlighting.git" + "zsh-autosuggestions:https://github.com/zsh-users/zsh-autosuggestions.git" + ) + + for plugin_info in "${plugins[@]}"; do + local plugin_name="${plugin_info%:*}" + local plugin_url="${plugin_info#*:}" + local plugin_dir="$zsh_plugins_dir/$plugin_name" + + if [[ ! -d "$plugin_dir" ]]; then + print_info "Installing $plugin_name..." + if execute_command "git clone '$plugin_url' '$plugin_dir'"; then + print_success "Installed $plugin_name" + else + print_error "Failed to install $plugin_name" + fi + else + print_info "$plugin_name already installed" + fi + done +} + +setup_ssh() { + print_section "Setting Up SSH" + save_state "setup_ssh" "started" + + local ssh_dir="$HOME/.ssh" + + if [[ ! -f "$ssh_dir/id_rsa" && ! -f "$ssh_dir/id_ed25519" ]]; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Generate SSH key pair?"; then + create_dir "$ssh_dir" 700 + + local email="${USER}@${HOSTNAME:-$(hostname)}" + local key_file="$ssh_dir/id_ed25519" + + if execute_command "ssh-keygen -t ed25519 -f '$key_file' -N '' -C '$email'"; then + print_success "SSH key pair generated (Ed25519)" + execute_command "chmod 600 '$key_file'" + execute_command "chmod 644 '$key_file.pub'" + + if [[ "$DRY_RUN" != true ]] && [[ -f "$key_file.pub" ]]; then + print_info "Your public key:" + print_color "$GREEN" "$(cat "$key_file.pub")" + print_info "Copy this key to your Git hosting service" + fi + else + print_error "Failed to generate SSH key" + mark_step_failed "setup_ssh" + return 1 + fi + fi + else + print_info "SSH key already exists" + fi + + mark_step_completed "setup_ssh" +} + +# Helper function to detect the init system +detect_init_system() { + if [ -d /run/systemd/system ]; then + echo "systemd" + elif command -v rc-service &>/dev/null; then + echo "openrc" + elif [ -d /etc/sv ]; then + echo "runit" + elif command -v service &>/dev/null; then + echo "sysvinit" + else + echo "unknown" + fi +} + +# Helper function to manage a service (enable/start) +manage_service() { + local action="$1" + local service="$2" + local init_system="$3" + local success=false + + case "$init_system" in + systemd) + if [ "$action" == "enable" ]; then + execute_command "$PRIVILEGE_TOOL systemctl enable '$service'" + success=$? + elif [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL systemctl start '$service'" + success=$? + fi + ;; + openrc) + if [ "$action" == "enable" ]; then + execute_command "$PRIVILEGE_TOOL rc-update add '$service' default" + success=$? + elif [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL rc-service '$service' start" + success=$? + fi + ;; + runit) + if [ "$action" == "enable" ]; then + # Runit services are enabled by creating a symlink in the run level directory + execute_command "$PRIVILEGE_TOOL ln -sf /etc/sv/'$service' /var/service/" + success=$? + elif [ "$action" == "start" ]; then + # The 'start' action is usually implied by the symlink, but you can + # manually start it if needed + execute_command "$PRIVILEGE_TOOL sv start '$service'" + success=$? + fi + ;; + sysvinit|unknown) + # Use the generic 'service' command + if [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL service '$service' start" + success=$? + fi + # Enabling is system-dependent for sysvinit/unknown; we'll check for chkconfig + if [ "$action" == "enable" ]; then + if command -v chkconfig &>/dev/null; then + execute_command "$PRIVILEGE_TOOL chkconfig '$service' on" + success=$? + else + success=0 + fi + fi + ;; + *) + print_error "Unknown init system: $init_system. Cannot $action service '$service'." + return 1 + ;; + esac + + return $((1 - success)) +} + +# Configure system services +configure_services() { + print_section "Configuring System Services" + save_state "configure_services" "started" + + if [[ "$CFG_OS" != "linux" ]]; then + print_skip "Service configuration (not supported on $CFG_OS)" + mark_step_completed "configure_services" + return 0 + fi + + # Detect the init system once + local INIT_SYSTEM=$(detect_init_system) + print_info "Detected Init System: $INIT_SYSTEM" + + # Enable TLP for laptop power management + if command_exists tlp; then + print_info "TLP is installed" + if [[ "$FORCE_MODE" == true ]] || prompt_user "Enable TLP power management service?"; then + if manage_service "enable" "tlp" "$INIT_SYSTEM"; then + manage_service "start" "tlp" "$INIT_SYSTEM" + print_success "TLP enabled and started" + else + print_error "Failed to enable TLP" + fi + fi + elif [[ "$FORCE_MODE" == true ]] || prompt_user "Install and enable TLP for better battery life?"; then + case "$DISTRO" in + PACMAN) execute_command "$PRIVILEGE_TOOL pacman -S --noconfirm tlp tlp-rdw" ;; + APT) execute_command "$PRIVILEGE_TOOL apt install -y tlp tlp-rdw" ;; + DNF) execute_command "$PRIVILEGE_TOOL dnf install -y tlp tlp-rdw" ;; + esac + + if command_exists tlp; then + manage_service "enable" "tlp" "$INIT_SYSTEM" + manage_service "start" "tlp" "$INIT_SYSTEM" + print_success "TLP installed, enabled and started" + fi + fi + + # Configure other useful services + local services_to_enable=() + + # Check for and configure common services + # NOTE: The 'is-enabled' check is non-portable and removed for simplicity + if command_exists docker; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Enable Docker service?"; then + services_to_enable+=("docker") + fi + fi + + if command_exists bluetooth; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Enable Bluetooth service?"; then + services_to_enable+=("bluetooth") + fi + fi + + # Enable selected services + for service in "${services_to_enable[@]}"; do + if manage_service "enable" "$service" "$INIT_SYSTEM"; then + manage_service "start" "$service" "$INIT_SYSTEM" + print_success "Enabled and started $service" + else + print_error "Failed to enable $service" + fi + done + + mark_step_completed "configure_services" +} + +setup_tmux_plugins() { + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping Tmux plugins installation" + return 0 + fi + + local tpm_dir="$HOME/.config/tmux/plugins/tpm" + local plugins_dir="$HOME/.config/tmux/plugins" + + if [[ ! -f "$HOME/.tmux.conf" && ! -f "$HOME/.config/tmux/tmux.conf" ]]; then + print_info "Tmux config not found, skipping plugin setup" + return 0 + fi + + print_info "Setting up Tmux plugins..." + create_dir "$plugins_dir" + + if [[ ! -d "$tpm_dir" || ! "$(ls -A "$tpm_dir" 2>/dev/null)" ]]; then + print_info "Installing Tmux Plugin Manager (TPM)..." + if execute_command "git clone https://github.com/tmux-plugins/tpm '$tpm_dir'"; then + print_success "TPM installed successfully" + print_info "Run 'tmux' and press 'prefix + I' to install plugins" + else + print_error "Failed to install TPM" + fi + else + print_info "TPM already installed" + fi +} + +setup_development() { + print_section "Setting Up Development Environment" + save_state "setup_development" "started" + + # Git configuration + if command_exists git; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Configure Git global settings?"; then + configure_git + fi + fi + + # Development tools based on install mode + case "$INSTALL_MODE" in + dev|full) + install_development_tools + ;; + *) + print_info "Skipping development tools installation for mode: $INSTALL_MODE" + ;; + esac + + mark_step_completed "setup_development" +} + +configure_git() { + local git_name="${USER}" + local git_email="${USER}@${HOSTNAME:-$(hostname)}" + + if [[ "$FORCE_MODE" != true ]]; then + print_color "$YELLOW" "Enter your Git username [$git_name]: " + read -r input_name + [[ -n "$input_name" ]] && git_name="$input_name" + + print_color "$YELLOW" "Enter your Git email [$git_email]: " + read -r input_email + [[ -n "$input_email" ]] && git_email="$input_email" + fi + + execute_command "git config --global user.name '$git_name'" + execute_command "git config --global user.email '$git_email'" + execute_command "git config --global init.defaultBranch main" + execute_command "git config --global pull.rebase false" + print_success "Git configured with name: $git_name, email: $git_email" +} + +install_development_tools() { + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping development tools installation" + return 0 + fi + + print_info "Installing development tools..." + + # Install Rust if not present + if ! command_exists rustc; then + install_rust + fi + + # Install Node.js via NVM if not present + if ! command_exists node; then + install_nvm + install_node + fi + + # Install Yarn if Node.js is available + if command_exists npm && ! command_exists yarn; then + install_yarn + fi +} + +install_rust() { + print_info "Installing Rust via rustup..." + + if command_exists rustup; then + print_info "Rust already installed" + return 0 + fi + + local cargo_home="${XDG_DATA_HOME:-$HOME/.local/share}/cargo" + local rustup_home="${XDG_DATA_HOME:-$HOME/.local/share}/rustup" + + create_dir "$(dirname "$cargo_home")" + + if execute_command "CARGO_HOME='$cargo_home' RUSTUP_HOME='$rustup_home' curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"; then + print_success "Rust installed successfully" + + # Add to PATH for current session + if [[ -f "$cargo_home/env" ]]; then + source "$cargo_home/env" + fi + + return 0 + else + print_error "Failed to install Rust" + return 1 + fi +} + +install_nvm() { + local nvm_dir="$HOME/.config/nvm" + + if [[ -d "$nvm_dir" && -f "$nvm_dir/nvm.sh" ]]; then + print_info "NVM already installed" + return 0 + fi + + print_info "Installing Node Version Manager (NVM)..." + create_dir "$nvm_dir" + + if execute_command "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | NVM_DIR='$nvm_dir' bash"; then + export NVM_DIR="$nvm_dir" + if [[ -s "$NVM_DIR/nvm.sh" ]]; then + source "$NVM_DIR/nvm.sh" + print_success "NVM installed successfully" + return 0 + else + print_error "NVM installation failed - script not found" + return 1 + fi + else + print_error "Failed to install NVM" + return 1 + fi +} + +install_node() { + if command_exists node; then + print_info "Node.js already installed" + return 0 + fi + + print_info "Installing Node.js..." + + # Source NVM if available + local nvm_dir="$HOME/.config/nvm" + if [[ -s "$nvm_dir/nvm.sh" ]]; then + export NVM_DIR="$nvm_dir" + source "$NVM_DIR/nvm.sh" + fi + + if command_exists nvm; then + if execute_command "nvm install --lts" && execute_command "nvm use --lts" && execute_command "nvm alias default lts/*"; then + print_success "Node.js installed successfully" + return 0 + else + print_error "Failed to install Node.js via NVM" + return 1 + fi + else + print_error "NVM not available for Node.js installation" + return 1 + fi +} + +install_yarn() { + print_info "Installing Yarn..." + + if execute_command "curl -o- -L https://yarnpkg.com/install.sh | bash"; then + print_success "Yarn installed successfully" + + # Add to PATH for current session + local yarn_bin="$HOME/.yarn/bin" + if [[ -d "$yarn_bin" && ":$PATH:" != *":$yarn_bin:"* ]]; then + export PATH="$yarn_bin:$PATH" + fi + + return 0 + else + print_error "Failed to install Yarn" + return 1 + fi +} + +apply_tweaks() { + print_section "Applying System Tweaks" + save_state "apply_tweaks" "started" + + case "$CFG_OS" in + linux) + apply_linux_tweaks + ;; + macos) + apply_macos_tweaks + ;; + *) + print_info "No system tweaks defined for $CFG_OS" + ;; + esac + + mark_step_completed "apply_tweaks" +} + +apply_linux_tweaks() { + # --- Locale tweak --- + if command -v localectl >/dev/null 2>&1; then + local current_locale + current_locale=$(localectl status | grep "System Locale" | cut -d= -f2 | cut -d, -f1) + if [[ "$current_locale" != "en_US.UTF-8" ]]; then + if prompt_user "Set system locale to en_US.UTF-8?"; then + if execute_with_privilege "localectl set-locale LANG=en_US.UTF-8"; then + print_success "Locale set to en_US.UTF-8" + else + print_error "Failed to set locale" + fi + fi + fi + fi + + # --- Power / Display timeout tweaks --- + if command -v gsettings >/dev/null 2>&1; then + print_info "Setting GNOME power/display timeouts to 'never'" + + # Turn off blank screen + gsettings set org.gnome.desktop.session idle-delay 0 + + # Turn off automatic suspend + gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' + gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'nothing' + + print_success "GNOME power/display settings applied" + else + print_info "gsettings not found; skipping GNOME power/display tweaks" + fi + + print_info "Linux system tweaks applied" +} + +apply_macos_tweaks() { + print_info "macOS system tweaks applied (placeholder)" +} + +#====================================== +# Installation Mode Selection +#====================================== + +select_installation_mode() { + if [[ "$INSTALL_MODE" != "ask" ]]; then + return 0 # Mode already set via command line + fi + + print_header "Installation Mode Selection" + + print_color "$CYAN" "Available installation modes:" + echo + + local mode_number=1 + local mode_options=() + + for mode in essentials minimal dev server full; do + local description="${INSTALLATION_PROFILES[$mode]}" + print_color "$YELLOW" "$mode_number. $mode - $description" + mode_options+=("$mode") + ((mode_number++)) + done + + echo + print_color "$CYAN" "You can also specify a custom profile from the profiles/ directory" + echo + + while true; do + print_color "$YELLOW" "Select installation mode [1-5] or enter profile name [dev]: " + read -r response + + if [[ -z "$response" ]]; then + INSTALL_MODE="dev" + break + elif [[ "$response" =~ ^[1-5]$ ]]; then + INSTALL_MODE="${mode_options[$((response-1))]}" + break + elif [[ "$response" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then + # Check if it's a valid profile + if [[ -f "profiles/$response.yml" ]] || [[ "${INSTALLATION_PROFILES[$response]:-}" ]]; then + INSTALL_MODE="$response" + break + else + print_warning "Profile '$response' not found" + fi + else + print_warning "Invalid selection. Please enter 1-5 or a profile name" + fi + done + + print_success "Selected installation mode: $INSTALL_MODE" + print_info "Description: ${INSTALLATION_PROFILES[$INSTALL_MODE]:-Custom profile}" +} + +#====================================== +# Command Line Argument Parsing +#====================================== + +show_help() { + cat << EOF +Dotfiles Installation Script + +USAGE: + $0 [OPTIONS] + +OPTIONS: + -h, --help Show this help message + -r, --resume Resume from last failed step + -u, --update Update existing dotfiles and packages + -v, --verbose Enable verbose output + -n, --dry-run Show what would be done without executing + -f, --force Force reinstallation and skip prompts + -m, --mode MODE Installation mode (essentials|minimal|dev|server|full|PROFILE) + +INSTALLATION MODES: + essentials Install only essential packages (git, curl, etc.) + minimal Minimal setup for basic development + dev Full development environment (default) + server Server configuration + full Complete installation with all packages + PROFILE Custom profile from profiles/ directory + +EXAMPLES: + $0 # Interactive installation (asks for mode) + $0 --mode essentials # Install essentials only + $0 --mode dev # Development environment + $0 --resume # Resume from last failed step + $0 --update --mode full # Update and install all packages + $0 --dry-run --mode dev # Preview development installation + +EOF +} + +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -r|--resume) + RESUME_MODE=true + shift + ;; + -u|--update) + UPDATE_MODE=true + shift + ;; + -v|--verbose) + VERBOSE_MODE=true + shift + ;; + -n|--dry-run) + DRY_RUN=true + shift + ;; + -f|--force) + FORCE_MODE=true + shift + ;; + -m|--mode) + INSTALL_MODE="$2" + shift 2 + ;; + *) + print_error "Unknown option: $1" + show_help + exit 1 + ;; + esac + done + + # Validate install mode + if [[ "$INSTALL_MODE" != "ask" ]]; then + if [[ ! "${INSTALLATION_PROFILES[$INSTALL_MODE]:-}" ]] && [[ ! -f "profiles/$INSTALL_MODE.yml" ]]; then + print_error "Invalid installation mode: $INSTALL_MODE" + print_info "Available modes: ${!INSTALLATION_PROFILES[*]}" + exit 1 + fi + fi +} + +#====================================== +# Summary Functions +#====================================== + +print_installation_summary() { + print_header "Installation Summary" + + local total_steps=${#STEP_ORDER[@]} + local completed_count=${#COMPLETED_STEPS[@]} + local failed_count=${#FAILED_ITEMS[@]} + + print_section "Progress Overview" + print_color "$CYAN" "Installation Mode: $INSTALL_MODE" + print_color "$CYAN" "Total Steps: $total_steps" + print_color "$GREEN" "Completed: $completed_count" + print_color "$RED" "Failed: $failed_count" + + if [[ ${#INSTALL_SUMMARY[@]} -gt 0 ]]; then + print_section "Successful Operations" + printf '%s\n' "${INSTALL_SUMMARY[@]}" + fi + + if [[ ${#FAILED_ITEMS[@]} -gt 0 ]]; then + print_section "Failed Operations" + printf '%s\n' "${FAILED_ITEMS[@]}" + print_warning "Check the log file: $LOG_FILE" + fi + + if [[ ${#SKIPPED_ITEMS[@]} -gt 0 ]]; then + print_section "Skipped Operations" + printf '%s\n' "${SKIPPED_ITEMS[@]}" + fi + + echo + print_color "$GREEN$BOLD" "Installation completed!" + print_info "Log file: $LOG_FILE" "always" +} + +#====================================== +# Main Installation Flow +#====================================== + +execute_step() { + local step_name="$1" + local step_desc="${INSTALLATION_STEPS[$step_name]}" + + if is_step_completed "$step_name" && [[ "$FORCE_MODE" != true ]]; then + print_success "$step_desc (already completed)" + return 0 + fi + + if "$step_name"; then + print_success "$step_desc completed" + mark_step_completed "$step_name" + return 0 + else + print_error "$step_desc failed" + mark_step_failed "$step_name" + return 1 + fi +} + +main() { + parse_arguments "$@" + setup_logging + + print_header "Dotfiles Installation" + + if [[ "$DRY_RUN" == true ]]; then + print_warning "DRY RUN MODE - No changes will be made" + echo + fi + + print_info "Starting installation for user: $USER" "always" + print_info "Log file: $LOG_FILE" "always" + + # Handle resume mode + if [[ "$RESUME_MODE" == true ]]; then + if load_state; then + print_info "Resuming from previous installation..." "always" + print_info "Last step: ${LAST_STEP:-unknown}" "always" + if [[ -n "${COMPLETED_STEPS:-}" ]]; then + eval "COMPLETED_STEPS=(${COMPLETED_STEPS:-})" + fi + else + print_warning "No previous installation state found" + print_info "Starting fresh installation..." + RESUME_MODE=false + fi + fi + + # Select installation mode if not specified + select_installation_mode + + # Show installation plan + echo + print_color "$YELLOW$BOLD" "Installation Plan (Mode: $INSTALL_MODE):" + local step_number=1 + for step in "${STEP_ORDER[@]}"; do + local step_desc="${INSTALLATION_STEPS[$step]}" + if is_step_completed "$step" && [[ "$FORCE_MODE" != true ]]; then + print_color "$GREEN" "$step_number. $step_desc (✓ completed)" + else + print_color "$CYAN" "$step_number. $step_desc" + fi + step_number=$((step_number + 1)) + done + + echo + if [[ "$FORCE_MODE" != true ]] && [[ "$DRY_RUN" != true ]] && ! prompt_user "Continue with installation?"; then + print_info "Installation cancelled by user" + exit 0 + fi + + # Execute installation steps + local failed_steps=() + local step_number=1 + local total_steps=${#STEP_ORDER[@]} + + for step in "${STEP_ORDER[@]}"; do + echo + print_color "$MAGENTA$BOLD" "[$step_number/$total_steps] ${INSTALLATION_STEPS[$step]}" + + if execute_step "$step"; then + print_info "Step completed successfully: $step" + else + failed_steps+=("$step") + print_error "Step failed: $step" + + if [[ "$FORCE_MODE" != true ]] && [[ "$DRY_RUN" != true ]]; then + echo + if ! prompt_user "Step '$step' failed. Continue with remaining steps?" "Y"; then + print_info "Installation stopped by user" + break + fi + fi + fi + + step_number=$((step_number + 1)) + done + + # Post-installation + if [[ ${#failed_steps[@]} -eq 0 ]]; then + print_success "All installation steps completed successfully!" + clear_state + else + print_warning "${#failed_steps[@]} steps failed: ${failed_steps[*]}" + if [[ "${failed_steps[-1]:-}" != "" ]]; then + save_state "${failed_steps[-1]}" "failed" + fi + fi + + print_installation_summary + + # Final recommendations + if [[ "$DRY_RUN" != true ]]; then + echo + print_section "Post-Installation Recommendations" + print_color "$CYAN" "• Restart your terminal or run: source ~/.bashrc (or ~/.zshrc)" + print_color "$CYAN" "• Review your dotfiles configuration in: $DOTFILES_DIR" + print_color "$CYAN" "• Use the 'config' command to manage your dotfiles" + + if [[ ${#failed_steps[@]} -gt 0 ]]; then + print_color "$YELLOW" "• Run '$0 --resume' to retry failed steps" + print_color "$YELLOW" "• Check the log file for detailed error information: $LOG_FILE" + fi + + echo + print_color "$GREEN$BOLD" "Thank you for using the Dotfiles Installation Script!" + fi + + [[ ${#failed_steps[@]} -eq 0 ]] && exit 0 || exit 1 +} + +#====================================== +# Script Entry Point +#====================================== + +cleanup_on_exit() { + local exit_code=$? + + if [[ $exit_code -ne 0 ]] && [[ "$DRY_RUN" != true ]]; then + print_error "Installation interrupted (exit code: $exit_code)" + + if [[ -n "${current_step:-}" ]]; then + save_state "$current_step" "interrupted" + print_info "State saved. Run with --resume to continue" + fi + fi +} + +handle_interrupt() { + print_warning "Installation interrupted by user" + exit 130 +} + +trap cleanup_on_exit EXIT +trap handle_interrupt INT + +# Execute main if script is run directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # Check basic requirements + for req in git curl; do + if ! command_exists "$req"; then + print_error "$req is required but not installed" + exit 1 + fi + done + + main "$@" +fi |
