diff options
Diffstat (limited to 'linux/home')
| -rwxr-xr-x | linux/home/install.sh | 2668 |
1 files changed, 1452 insertions, 1216 deletions
diff --git a/linux/home/install.sh b/linux/home/install.sh index c176c54..60abd0b 100755 --- a/linux/home/install.sh +++ b/linux/home/install.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash -# Dotfiles Installation Script -#========================================================= +# 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 @@ -24,9 +27,13 @@ BOLD='\033[1m' DOTFILES_URL='https://github.com/srdusr/dotfiles.git' DOTFILES_DIR="$HOME/.cfg" LOG_FILE="$HOME/.local/share/dotfiles_install.log" -TRASH_DIR="$HOME/.local/share/Trash" 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=() @@ -40,39 +47,69 @@ 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 current state save_state() { local current_step="$1" - local status="$2" # started, completed, failed + local status="$2" mkdir -p "$(dirname "$STATE_FILE")" @@ -81,19 +118,18 @@ save_state() { echo "STEP_STATUS=$status" echo "TIMESTAMP=$(date +%s)" echo "RESUME_AVAILABLE=true" - - # Save completed steps + echo "PRIVILEGE_CACHED=$PRIVILEGE_CACHED" + echo "INSTALL_MODE=$INSTALL_MODE" echo "COMPLETED_STEPS=(${COMPLETED_STEPS[*]})" - - # Save environment info 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 previous state load_state() { if [[ -f "$STATE_FILE" ]]; then source "$STATE_FILE" @@ -103,18 +139,15 @@ load_state() { fi } -# Clear state file clear_state() { [[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE" } -# Check if step was completed is_step_completed() { local step="$1" [[ " ${COMPLETED_STEPS[*]} " =~ " ${step} " ]] } -# Mark step as completed mark_step_completed() { local step="$1" if ! is_step_completed "$step"; then @@ -123,244 +156,25 @@ mark_step_completed() { save_state "$step" "completed" } -# Mark step as failed mark_step_failed() { local step="$1" save_state "$step" "failed" } #====================================== -# 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 of components - --step STEP Run only specific step - --skip STEP Skip specific step - --list-steps List all available steps - --status Show current installation status - --clean Clean up state and backup files - -STEPS: -EOF - - for step in "${STEP_ORDER[@]}"; do - printf " %-20s %s\n" "$step" "${INSTALLATION_STEPS[$step]}" - done - - cat << EOF - -EXAMPLES: - $0 # Full installation - $0 --resume # Resume from last failed step - $0 --update # Update existing installation - $0 --step install_packages # Run only package installation - $0 --skip setup_ssh # Skip SSH setup - $0 --dry-run # Preview what would be done - -EOF -} - -parse_arguments() { - local specific_steps=() - local skip_steps=() - - 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 - ;; - --step) - if [[ -n "${2:-}" ]]; then - specific_steps+=("$2") - shift 2 - else - print_error "Option --step requires a step name" - exit 1 - fi - ;; - --skip) - if [[ -n "${2:-}" ]]; then - skip_steps+=("$2") - shift 2 - else - print_error "Option --skip requires a step name" - exit 1 - fi - ;; - --list-steps) - echo "Available installation steps:" - for step in "${STEP_ORDER[@]}"; do - printf " %-20s %s\n" "$step" "${INSTALLATION_STEPS[$step]}" - done - exit 0 - ;; - --status) - show_status - exit 0 - ;; - --clean) - cleanup_files - exit 0 - ;; - *) - print_error "Unknown option: $1" - show_help - exit 1 - ;; - esac - done - - # Apply step filters - if [[ ${#specific_steps[@]} -gt 0 ]]; then - STEP_ORDER=("${specific_steps[@]}") - fi - - if [[ ${#skip_steps[@]} -gt 0 ]]; then - local filtered_steps=() - for step in "${STEP_ORDER[@]}"; do - if [[ ! " ${skip_steps[*]} " =~ " ${step} " ]]; then - filtered_steps+=("$step") - fi - done - STEP_ORDER=("${filtered_steps[@]}") - fi -} - -#====================================== -# Status and Cleanup Functions -#====================================== - -show_status() { - print_header "Installation Status" - - if [[ -f "$STATE_FILE" ]]; then - load_state - - print_section "Current State" - print_info "Last step: ${LAST_STEP:-unknown}" - print_info "Step status: ${STEP_STATUS:-unknown}" - print_info "Timestamp: $(date -d "@${TIMESTAMP:-0}" 2>/dev/null || echo "unknown")" - - print_section "Completed Steps" - if [[ ${#COMPLETED_STEPS[@]} -gt 0 ]]; then - for step in "${COMPLETED_STEPS[@]}"; do - print_success "$step: ${INSTALLATION_STEPS[$step]:-unknown}" - done - else - print_info "No steps completed yet" - fi - - print_section "Remaining Steps" - local remaining_steps=() - for step in "${STEP_ORDER[@]}"; do - if ! is_step_completed "$step"; then - remaining_steps+=("$step") - fi - done - - if [[ ${#remaining_steps[@]} -gt 0 ]]; then - for step in "${remaining_steps[@]}"; do - print_warning "$step: ${INSTALLATION_STEPS[$step]:-unknown}" - done - echo - print_info "Run with --resume to continue from where you left off" - else - print_success "All steps completed!" - fi - else - print_info "No installation state found" - print_info "Run the script to start a new installation" - fi -} - -cleanup_files() { - print_header "Cleanup" - - local files_to_clean=( - "$STATE_FILE" - "$LOG_FILE" - ) - - # Find backup directories - mapfile -t backup_dirs < <(find "$HOME" -maxdepth 1 -name ".dotfiles-backup-*" -type d 2>/dev/null || true) - - if [[ ${#backup_dirs[@]} -gt 0 ]]; then - print_section "Backup Directories Found" - for dir in "${backup_dirs[@]}"; do - print_info "$(basename "$dir") - $(ls -la "$dir" 2>/dev/null | wc -l) files" - done - - if prompt_user "Remove backup directories?"; then - for dir in "${backup_dirs[@]}"; do - rm -rf "$dir" && print_success "Removed $(basename "$dir")" - done - fi - fi - - print_section "State and Log Files" - for file in "${files_to_clean[@]}"; do - if [[ -f "$file" ]]; then - print_info "Found: $file" - if prompt_user "Remove $(basename "$file")?"; then - rm -f "$file" && print_success "Removed $(basename "$file")" - fi - fi - done - - print_success "Cleanup completed" -} - -#====================================== -# UI Functions (keeping existing ones and adding new) +# UI Functions #====================================== -# Print colorized output print_color() { local color="$1" local message="$2" echo -e "${color}${message}${NOCOLOR}" - # Log to file if logging is setup if [[ -n "${LOG_FILE:-}" && -f "$LOG_FILE" ]]; then echo "$(date +'%Y-%m-%d %H:%M:%S') - $message" >> "$LOG_FILE" fi } -# Print header with decorative border print_header() { local title="$1" local border_char="=" @@ -373,7 +187,6 @@ print_header() { echo } -# Print section header print_section() { local title="$1" echo @@ -381,27 +194,23 @@ print_section() { print_color "$BLUE" "$(printf '%*s' $((${#title} + 2)) '' | tr ' ' '-')" } -# Print success message print_success() { local message="$1" print_color "$GREEN" "✓ $message" INSTALL_SUMMARY+=("✓ $message") } -# Print error message print_error() { local message="$1" print_color "$RED" "✗ $message" >&2 FAILED_ITEMS+=("✗ $message") } -# Print warning message print_warning() { local message="$1" print_color "$YELLOW" "⚠ $message" } -# Print info message print_info() { local message="$1" if [[ "$VERBOSE_MODE" == true ]] || [[ "${2:-}" == "always" ]]; then @@ -409,156 +218,90 @@ print_info() { fi } -# Print skip message print_skip() { local message="$1" print_color "$YELLOW" "⏭ $message" SKIPPED_ITEMS+=("⏭ $message") } -# Print dry run message print_dry_run() { local message="$1" print_color "$MAGENTA" "[DRY RUN] $message" } #====================================== -# Logging Functions +# Network Connectivity Functions #====================================== -# Setup logging -setup_logging() { - local log_dir - log_dir="$(dirname "$LOG_FILE")" - - # Create log directory if it doesn't exist - if [[ ! -d "$log_dir" ]]; then - mkdir -p "$log_dir" || { - print_error "Failed to create log directory: $log_dir" - exit 1 - } +check_internet_connectivity() { + if [[ "$CONNECTIVITY_CHECKED" == true ]]; then + return $([[ "$INTERNET_AVAILABLE" == true ]] && echo 0 || echo 1) fi - # Create trash directory if it doesn't exist - if [[ ! -d "$TRASH_DIR" ]]; then - mkdir -p "$TRASH_DIR" || { - print_error "Failed to create trash directory: $TRASH_DIR" - exit 1 - } - fi + print_section "Checking Internet Connectivity" - # Archive old log file if it exists - if [[ -f "$LOG_FILE" ]]; then - local archived_log="$TRASH_DIR/dotfiles_install_$(date +%Y%m%d_%H%M%S).log" - mv "$LOG_FILE" "$archived_log" - print_info "Archived previous log to: $archived_log" "always" - fi + local test_urls=("8.8.8.8" "1.1.1.1" "google.com" "github.com") - # Initialize log file - { - echo "=======================================" - echo "Dotfiles Installation Log" - echo "Date: $(date)" - echo "User: $USER" - echo "Host: $HOSTNAME" - echo "OS: $(uname -s)" - echo "Args: $*" - echo "Resume Mode: $RESUME_MODE" - echo "Update Mode: $UPDATE_MODE" - echo "Verbose Mode: $VERBOSE_MODE" - echo "Dry Run: $DRY_RUN" - echo "Force Mode: $FORCE_MODE" - echo "=======================================" - echo - } > "$LOG_FILE" + 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 - print_info "Log file initialized: $LOG_FILE" "always" -} + INTERNET_AVAILABLE=false + CONNECTIVITY_CHECKED=true + print_error "No internet connectivity detected" -# log function -log_message() { - local level="$1" - local message="$2" - local timestamp="$(date +'%Y-%m-%d %H:%M:%S')" - echo "[$level] $timestamp - $message" >> "$LOG_FILE" + # Try to connect to WiFi or prompt user + attempt_network_connection - if [[ "$VERBOSE_MODE" == true ]]; then - case "$level" in - ERROR) print_color "$RED" "[$level] $message" ;; - WARN) print_color "$YELLOW" "[$level] $message" ;; - INFO) print_color "$CYAN" "[$level] $message" ;; - *) echo "[$level] $message" ;; - esac - fi + return 1 } -#====================================== -# User Interaction Functions -#====================================== +attempt_network_connection() { + print_warning "Attempting to establish network connection..." -# prompt function -prompt_user() { - local question="$1" - local default="${2:-Y}" - local response - local timeout="${3:-0}" + # 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" - # Skip prompts in non-interactive mode or when forcing - 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 + 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 [[ "$timeout" -gt 0 ]]; then - if ! read -t "$timeout" -r response; then - print_info "Timed out, using default: $default" - response="$default" + 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 - else - read -r response fi + fi - # Use default if no 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 -} - -# Progress indicator -show_progress() { - local current="$1" - local total="$2" - local message="$3" - local percent=$((current * 100 / total)) - local filled=$((percent / 2)) - local empty=$((50 - filled)) + # Try other connection methods + if command_exists iwctl; then + print_info "You can also connect manually using iwctl" + fi - printf "\r" - print_color "$BLUE" "[$current/$total] " - printf "%s" "$(printf '█%.0s' $(seq 1 $filled))" - printf "%s" "$(printf '░%.0s' $(seq 1 $empty))" - print_color "$BLUE" " ${percent}%% - $message" + return 1 } #====================================== -# System Detection Functions (keeping existing) +# System Detection Functions #====================================== -# Detect operating system detect_os() { case "$(uname -s)" in Linux) CFG_OS="linux" ;; @@ -568,462 +311,702 @@ detect_os() { esac print_info "Detected OS: $CFG_OS" "always" - log_message "INFO" "Detected operating system: $CFG_OS" } -# Detect privilege escalation tools detect_privilege_tools() { - if command -v sudo &>/dev/null; then - PRIVILEGE_TOOL="sudo" - elif command -v doas &>/dev/null; then - PRIVILEGE_TOOL="doas" - elif command -v pkexec &>/dev/null; then - PRIVILEGE_TOOL="pkexec" - elif [[ "$(id -u)" -eq 0 ]]; then - PRIVILEGE_TOOL="" # Running as root - else + if [[ "$(id -u)" -eq 0 ]]; then PRIVILEGE_TOOL="" - print_warning "No privilege escalation tool found" - if prompt_user "Continue without privilege escalation? (Installation may fail for some components)" "N"; then - print_info "Continuing without privilege escalation..." - else - print_error "Privilege escalation required. Exiting." - exit 1 - fi + print_info "Running as root, no privilege escalation needed" + return 0 fi - [[ -n "$PRIVILEGE_TOOL" ]] && print_success "Using privilege escalation tool: $PRIVILEGE_TOOL" + 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 } -# Detect Linux distribution -detect_linux_distro() { - if [[ ! -f /etc/os-release ]]; then - print_error "/etc/os-release not found" - 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 - source /etc/os-release + 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 +} - case "$ID" in - arch|manjaro|endeavouros) DISTRO="PACMAN" ;; - debian|ubuntu|mint|pop) DISTRO="APT" ;; - fedora|rhel|centos|rocky) DISTRO="DNF" ;; - opensuse*|sles) DISTRO="ZYPPER" ;; - gentoo) DISTRO="PORTAGE" ;; - *) - print_warning "Unknown distribution: $ID" - # Try to detect package managers - for pm in pacman apt dnf zypper emerge; do - if command -v "$pm" &>/dev/null; then - case "$pm" in - pacman) DISTRO="PACMAN" ;; - apt) DISTRO="APT" ;; - dnf) DISTRO="DNF" ;; - zypper) DISTRO="ZYPPER" ;; - emerge) DISTRO="PORTAGE" ;; - esac - break - fi - done +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 - if [[ -z "${DISTRO:-}" ]]; then - print_error "Could not detect package manager" - return 1 + # 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 - ;; - esac + done + fi - print_success "Detected Linux distribution: $ID (Package manager: $DISTRO)" - log_message "INFO" "Detected Linux distribution: $ID, Package manager: $DISTRO" + 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 #====================================== -# Check if command exists command_exists() { command -v "$1" &>/dev/null } -# Execute with dry run support execute_command() { local cmd="$*" - log_message "INFO" "Executing: $cmd" if [[ "$DRY_RUN" == true ]]; then - print_warning "DRY RUN MODE - No changes will be made" - echo + print_dry_run "$cmd" + return 0 fi - print_info "Starting installation for user: $USER" "always" - print_info "Log file: $LOG_FILE" "always" - print_info "Mode: $( - [[ "$RESUME_MODE" == true ]] && echo "Resume" || - [[ "$UPDATE_MODE" == true ]] && echo "Update" || - echo "Fresh Install" - )" "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" - print_info "Step status: ${STEP_STATUS:-unknown}" "always" - - # Load completed steps from state - 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 + if [[ "$VERBOSE_MODE" == true ]]; then + print_info "Running: $cmd" fi - # Pre-flight checks - detect_os - detect_privilege_tools + eval "$cmd" +} - if [[ "$CFG_OS" == "linux" ]]; then - detect_linux_distro || { - print_error "Failed to detect Linux distribution" - exit 1 - } - fi +execute_with_privilege() { + local cmd="$*" - # Show installation plan - echo - print_color "$YELLOW$BOLD" "Installation Plan:" - 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)" + if [[ "$DRY_RUN" == true ]]; then + if [[ -n "$PRIVILEGE_TOOL" ]]; then + print_dry_run "$PRIVILEGE_TOOL $cmd" else - print_color "$CYAN" "$step_number. $step_desc" + print_dry_run "$cmd" fi - step_number=$((step_number + 1)) - done + return 0 + fi - echo - if [[ "$FORCE_MODE" != true ]] && ! prompt_user "Continue with installation?"; then - print_info "Installation cancelled by user" - exit 0 + if [[ -n "$PRIVILEGE_TOOL" ]]; then + if [[ "$PRIVILEGE_CACHED" != true ]]; then + test_privilege_access || return 1 + fi + eval "$PRIVILEGE_TOOL $cmd" + else + eval "$cmd" fi +} - # Execute installation steps - local failed_steps=() - local step_number=1 - local total_steps=${#STEP_ORDER[@]} +prompt_user() { + local question="$1" + local default="${2:-Y}" + local response - for step in "${STEP_ORDER[@]}"; do - echo - print_color "$MAGENTA$BOLD" "[$step_number/$total_steps] ${INSTALLATION_STEPS[$step]}" + if [[ "$FORCE_MODE" == true ]]; then + print_info "Auto-answering '$question' with: $default" + [[ "$default" =~ ^[Yy] ]] && return 0 || return 1 + fi - if execute_step "$step"; then - log_message "INFO" "Step completed successfully: $step" + while true; do + if [[ "$default" == "Y" ]]; then + print_color "$YELLOW" "$question [Y/n]: " else - failed_steps+=("$step") - log_message "ERROR" "Step failed: $step" + print_color "$YELLOW" "$question [y/N]: " + fi - # Ask if user wants to continue - if [[ "$FORCE_MODE" != true ]]; then - echo - if ! prompt_user "Step '$step' failed. Continue with remaining steps?" "Y"; then - print_info "Installation stopped by user" - break - fi - fi + read -r response + + if [[ -z "$response" ]]; then + response="$default" fi - step_number=$((step_number + 1)) + case "${response^^}" in + Y|YES) return 0 ;; + N|NO) return 1 ;; + *) print_warning "Please answer Y/yes or N/no" ;; + esac done +} - # Post-installation tasks - if [[ ${#failed_steps[@]} -eq 0 ]]; then - print_success "All installation steps completed successfully!" - clear_state +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_warning "${#failed_steps[@]} steps failed: ${failed_steps[*]}" - save_state "${failed_steps[-1]}" "failed" + print_info "Directory already exists: $dir" fi +} - # Show summary - print_installation_summary +setup_logging() { + local log_dir + log_dir="$(dirname "$LOG_FILE")" - log_message "INFO" "Installation process completed" + if [[ ! -d "$log_dir" ]]; then + mkdir -p "$log_dir" || { + print_error "Failed to create log directory: $log_dir" + exit 1 + } + fi - # Exit with appropriate code - [[ ${#failed_steps[@]} -eq 0 ]] && exit 0 || exit 1 + { + 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" } -#====================================== -# Error Handling and Cleanup -#====================================== +get_package_name() { + local package="$1" + local packages_file="${2:-}" -# Trap for cleanup on exit -cleanup_on_exit() { - local exit_code=$? + # If packages.yml is available, check for distribution-specific mappings + if [[ -n "$packages_file" ]] && [[ -f "$packages_file" ]] && command_exists yq; then + local distro_package="" - if [[ $exit_code -ne 0 ]]; then - print_error "Installation interrupted (exit code: $exit_code)" - log_message "ERROR" "Installation interrupted with exit code: $exit_code" + # 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 - # Save state for resume - if [[ -n "${current_step:-}" ]]; then - save_state "$current_step" "interrupted" - print_info "State saved. Run with --resume to continue from where you left off" + # Return the distribution-specific package name if found + if [[ -n "$distro_package" ]]; then + echo "$distro_package" + return 0 fi fi - # Cleanup temporary files if any - if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "$TEMP_DIR" ]]; then - rm -rf "$TEMP_DIR" - fi + # Fallback to original package name + echo "$package" } -# Trap for handling interruptions -handle_interrupt() { - print_warning "Installation interrupted by user" - log_message "WARN" "Installation interrupted by user (SIGINT)" - exit 130 -} +get_package_use_flags() { + local package="$1" + local packages_file="${2:-}" -# Set up traps -trap cleanup_on_exit EXIT -trap handle_interrupt INT + # 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 +} #====================================== -# MacOS and Windows Support Stubs +# Dependency Installation Functions #====================================== -# Install macOS packages (placeholder) -install_macos_packages() { - local packages_file="$1" +install_dependencies_if_missing() { + print_section "Checking for dependencies git, wget/curl" + save_state "install_dependencies" "started" - print_info "macOS package installation" + local missing_deps=() + local failed_deps=() - if ! command_exists brew; then - 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" - else - print_error "Failed to install Homebrew" - return 1 + # Check for missing essential tools + for tool in "${ESSENTIAL_TOOLS[@]}"; do + if ! command_exists "$tool"; then + missing_deps+=("$tool") fi - fi + done - # Install packages from YAML - local packages=() - if [[ "$DRY_RUN" != true ]]; then - mapfile -t packages < <(yq e '.packages.macos[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) - fi + # 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..." - if [[ ${#packages[@]} -gt 0 ]]; then - for package in "${packages[@]}"; do - if execute_command "brew install '$package'"; then - print_success "Installed $package" + # 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 - print_error "Failed to install $package" + 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 -} - -# Install Windows packages (placeholder) -install_windows_packages() { - local packages_file="$1" - print_info "Windows package installation" - print_warning "Windows package installation not fully implemented" - - # Could implement with Chocolatey, Scoop, or winget - if command_exists choco; then - print_info "Using Chocolatey for package management" - # Implementation would go here - elif command_exists scoop; then - print_info "Using Scoop for package management" - # Implementation would go here - elif command_exists winget; then - print_info "Using Windows Package Manager (winget)" - # Implementation would go here - else - print_warning "No package manager found for Windows" + 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 +} + #====================================== -# Additional Utility Functions +# Package Management Functions #====================================== -# Check system requirements -check_system_requirements() { - local requirements_met=true +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 +} - # Check for required commands - local required_commands=("git" "curl") - for cmd in "${required_commands[@]}"; do - if ! command_exists "$cmd"; then - print_error "Required command not found: $cmd" - requirements_met=false - fi - done +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 +} - # Check disk space (require at least 1GB free) - local available_space - available_space=$(df "$HOME" | awk 'NR==2 {print $4}') - if [[ "$available_space" -lt 1048576 ]]; then # 1GB in KB - print_warning "Low disk space available: $(($available_space / 1024))MB" +install_homebrew() { + if command_exists brew; then + print_info "Homebrew already installed" + return 0 fi - # Check internet connectivity - if ! curl -s --head --request GET https://github.com >/dev/null; then - print_warning "No internet connectivity detected" - print_info "Some features may not work properly" - 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" - return $(($requirements_met ? 0 : 1)) + # 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 } -# Validate configuration files -validate_config() { - local config_dir="$HOME/.config" - local issues_found=false - - # Check for common configuration issues - if [[ -f "$config_dir/packages.yml" ]]; then - if ! yq e '.' "$config_dir/packages.yml" >/dev/null 2>&1; then - print_error "Invalid YAML syntax in packages.yml" - issues_found=true - fi +install_yq() { + if command_exists yq; then + print_info "yq already installed" + return 0 fi - # Check for conflicting dotfiles - local common_conflicts=(".bashrc" ".zshrc" ".vimrc" ".gitconfig") - for file in "${common_conflicts[@]}"; do - if [[ -f "$HOME/$file" ]] && [[ ! -L "$HOME/$file" ]]; then - print_warning "Potential conflict: $HOME/$file exists and is not a symlink" + 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 - done - return $(($issues_found ? 1 : 0)) + print_success "yq installed successfully" + return 0 + else + print_error "Failed to install yq" + return 1 + fi } -#====================================== -# Script Entry Point -#====================================== +parse_packages_from_yaml() { + local packages_file="$1" + local section="$2" + local packages=() -# Execute the main function if script is run directly -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - # Check system requirements first - if ! check_system_requirements; then - print_error "System requirements not met" - exit 1 + if [[ ! -f "$packages_file" ]]; then + print_warning "Package file not found: $packages_file" + return 1 fi - # Run main installation - main "$@" -fi - print_dry_run "$cmd" - return 0 + if ! command_exists yq; then + print_error "yq not available for parsing packages.yml" + return 1 fi - if [[ "$VERBOSE_MODE" == true ]]; then - print_info "Running: $cmd" + # 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 - eval "$cmd" + # Output packages + printf '%s\n' "${packages[@]}" } -# Download file with progress -download_file() { - local url="$1" - local output="$2" +install_packages_from_yaml() { + local packages_file="$1" + local profile="${2:-essentials}" + local failed_packages=() + local installed_count=0 - if [[ "$DRY_RUN" == true ]]; then - print_dry_run "Download: $url -> $output" + 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 - if command_exists wget; then - wget --progress=bar:force -O "$output" "$url" 2>&1 | \ - while IFS= read -r line; do - if [[ "$line" =~ [0-9]+% ]]; then - printf "\r%s" "$line" - fi - done - echo - elif command_exists curl; then - curl --progress-bar -o "$output" "$url" - else - print_error "Neither wget nor curl found" - return 1 - 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 -# Create directory with proper permissions -create_dir() { - local dir="$1" - local permissions="${2:-755}" + # Install packages from each section + for section in "${sections[@]}"; do + print_info "Installing packages from section: $section" - if [[ "$DRY_RUN" == true ]]; then - print_dry_run "Create directory: $dir (mode: $permissions)" - return 0 - fi + local packages + mapfile -t packages < <(parse_packages_from_yaml "$packages_file" "$section") - 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 -} + if [[ ${#packages[@]} -eq 0 ]]; then + print_info "No packages found in section: $section" + continue + fi -# Backup existing files -backup_file() { - local file="$1" - local backup_path="$BACKUP_DIR/$(dirname "${file#$HOME/}")" + print_info "Found ${#packages[@]} packages in section $section" - if [[ "$DRY_RUN" == true ]]; then - print_dry_run "Backup: $file -> $backup_path" - return 0 - fi + for package in "${packages[@]}"; do + [[ -z "$package" ]] && continue - if [[ -e "$file" ]]; then - mkdir -p "$backup_path" - cp -a "$file" "$backup_path/" - print_info "Backed up: $file" + 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 - return 1 } #====================================== -# Git Configuration Functions (keeping existing) +# Dotfiles Management System (Config Command) #====================================== -# Git wrapper to avoid conflicts -git_without_work_tree() { - if [[ -d "$PWD/.git" ]] && [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]]; then - local old_work_tree="$GIT_WORK_TREE" - unset GIT_WORK_TREE - git "$@" - export GIT_WORK_TREE="$old_work_tree" - else - git "$@" +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" "$@" @@ -1050,11 +1033,9 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then # Check for paths that should go to the repository root case "$f" in common/*|linux/*|macos/*|windows/*|profile/*|README.md) - # If path already looks like a repo path, use it as is echo "$f" return ;; - # Otherwise, convert to a relative path "$HOME/"*) f="${f#$HOME/}" ;; @@ -1064,7 +1045,6 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then echo "$CFG_OS/home/$f" } - # Map repository path back to system path _sys_path() { local repo_path="$1" local os_path_pattern="$CFG_OS/" @@ -1076,17 +1056,50 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then fi case "$repo_path" in - # Files in the home directory + # 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/}" ;; - # Other files in the repo root - common/*|profile/*|README.md|linux/*|macos/*|windows/*) + + # Profile configs and README → stay in repo + profile/*|README.md) echo "$HOME/.cfg/$repo_path" ;; + + # Default fallback *) - echo "/$repo_path" - ;; + echo "$HOME/.cfg/$repo_path" + ;; + esac } @@ -1102,24 +1115,55 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then elif command -v pkexec >/dev/null; then pkexec "$@" else - echo "Error: No privilege escalation tool (sudo, doas, pkexec) found." + echo "Error: No privilege escalation tool found." return 1 fi fi } - # NOTE: can change `config` to whatever you feel comfortable ie. dotfiles, dots, cfg etc. + # 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="$(_repo_path "$file_path")" + 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" - _config add "$repo_path" + + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" add "$repo_path" + echo "Added: $file_path -> $repo_path" done ;; @@ -1127,7 +1171,6 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then local rm_opts="" local file_path_list=() - # Separate options from file paths for arg in "$@"; do if [[ "$arg" == "-"* ]]; then rm_opts+=" $arg" @@ -1139,16 +1182,13 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then for file_path in "${file_path_list[@]}"; do local repo_path="$(_repo_path "$file_path")" - # Use a dummy run of `git rm` to handle the recursive flag if [[ "$rm_opts" == *"-r"* ]]; then _config rm --cached -r "$repo_path" else _config rm --cached "$repo_path" fi - # Remove from the filesystem, passing the collected options eval "rm $rm_opts \"$file_path\"" - echo "Removed: $file_path" done ;; @@ -1158,12 +1198,12 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then 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")" ]]; 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")" ]]; 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" @@ -1178,14 +1218,13 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then done ;; status) - # Auto-sync any modified files 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" + cp -fa "$sys_file" "$full_repo_path" auto_synced+=("$repo_file") fi fi @@ -1202,20 +1241,47 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then ;; deploy) _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 [[ -e "$full_repo_path" ]]; then - if [[ -n "$sys_file" ]]; 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 "Deployed: $repo_file -> $sys_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 ;; @@ -1240,22 +1306,160 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then 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 Functions +# Installation Step Functions #====================================== -# Install dotfiles +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 "config pull origin main"; then + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then update=true print_success "Dotfiles updated successfully" else @@ -1279,51 +1483,13 @@ install_dotfiles() { fi fi - # Check for conflicts only if not updating - if [[ "$update" != true ]]; then - local conflicts - conflicts=$(config checkout 2>&1 | grep -E "^\s+" | awk '{print $1}' || true) - - if [[ -n "$conflicts" ]]; then - print_warning "The following files will be overwritten:" - echo "$conflicts" - - if [[ "$FORCE_MODE" == true ]] || prompt_user "Continue and backup/overwrite these files?"; then - # Backup conflicting files - create_dir "$BACKUP_DIR" - print_info "Backing up conflicting files to: $BACKUP_DIR" - - while IFS= read -r file; do - [[ -z "$file" ]] && continue - backup_file "$HOME/$file" - done <<< "$conflicts" - - print_info "Backed up conflicting files to: $BACKUP_DIR" - else - print_error "Installation cancelled by user" - mark_step_failed "install_dotfiles" - return 1 - fi - fi - - # Checkout files - if execute_command "config checkout -f"; then - print_success "Dotfiles checked out successfully" - else - print_error "Failed to checkout dotfiles" - mark_step_failed "install_dotfiles" - return 1 - fi - fi - - # Configure repository - execute_command "config config status.showUntrackedFiles no" + # 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" } -# Create user directories setup_user_dirs() { print_section "Setting Up User Directories" save_state "setup_user_dirs" "started" @@ -1334,332 +1500,167 @@ setup_user_dirs() { create_dir "$HOME/$dir" done - # Handle XDG user directories - if [[ -f "$HOME/.config/user-dirs.dirs" ]]; then - if [[ "$FORCE_MODE" == true ]] || prompt_user "Configure XDG user directories?"; then - if [[ "$DRY_RUN" != true ]]; then - source "$HOME/.config/user-dirs.dirs" - fi - - # Create XDG directories - for var in XDG_DESKTOP_DIR XDG_DOWNLOAD_DIR XDG_TEMPLATES_DIR XDG_PUBLICSHARE_DIR \ - XDG_DOCUMENTS_DIR XDG_MUSIC_DIR XDG_PICTURES_DIR XDG_VIDEOS_DIR; do - local dir_path="${!var:-}" - [[ -n "$dir_path" ]] && create_dir "$dir_path" - done - - print_success "XDG user directories configured" - fi + # 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" } -# yq installation -install_yq() { - local bin_dir="$HOME/.local/bin" - local yq_path="$bin_dir/yq" - - if command_exists yq && [[ "$FORCE_MODE" != true ]]; then - print_info "yq already available" - return 0 - fi - - local yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" - - case "$CFG_OS" in - linux) - case "$(uname -m)" in - x86_64) yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" ;; - aarch64) yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64" ;; - *) print_error "Unsupported architecture for yq installation"; return 1 ;; +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 - ;; - macos) - yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_darwin_amd64" - ;; - *) - print_error "yq installation not supported for $CFG_OS" - return 1 - ;; - esac - - print_info "Installing yq..." - - create_dir "$bin_dir" - download_file "$yq_url" "$yq_path" || return 1 - execute_command "chmod +x '$yq_path'" || return 1 - - # Add to PATH if not already there - if [[ ":$PATH:" != *":$bin_dir:"* ]]; then - export PATH="$bin_dir:$PATH" - if [[ "$DRY_RUN" != true ]]; then - echo "export PATH=\"$bin_dir:\$PATH\"" >> "$HOME/.bashrc" + else + print_info "Package tool already available: $tool" fi - fi + done - print_success "yq installed successfully" + mark_step_completed "install_essentials" } -# package installation install_packages() { print_section "Installing Packages" save_state "install_packages" "started" - local packages_file="$HOME/.config/packages.yml" - - # Check if yq is available for YAML parsing - if ! command_exists yq; then - if [[ "$FORCE_MODE" == true ]] || prompt_user "yq (YAML parser) is required. Install it?"; then - install_yq || { - print_error "Failed to install yq" - mark_step_failed "install_packages" - return 1 - } - else - print_skip "Package installation (requires yq)" - mark_step_completed "install_packages" - return 0 - fi - fi - - if [[ ! -f "$packages_file" ]]; then - print_warning "packages.yml not found at $packages_file, checking current directory..." - if [[ -f "packages.yml" ]]; then - packages_file="packages.yml" - else - print_warning "packages.yml not found, skipping package installation" - mark_step_completed "install_packages" - return 0 - fi - fi - - case "$CFG_OS" in - linux) - install_linux_packages "$packages_file" - ;; - macos) - install_macos_packages "$packages_file" - ;; - windows) - install_windows_packages "$packages_file" - ;; - *) - print_warning "Package installation not supported for $CFG_OS" - ;; - esac - - mark_step_completed "install_packages" -} - -# Linux package installation -install_linux_packages() { - local packages_file="$1" - local failed_packages=() - local installed_packages=() - local skipped_packages=() - - # Get package lists - local base_packages=() - local distro_packages=() - - if [[ "$DRY_RUN" != true ]]; then - mapfile -t base_packages < <(yq e '.packages.base[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) - - case "$DISTRO" in - PACMAN) - mapfile -t distro_packages < <(yq e '.packages.arch[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) - ;; - APT) - mapfile -t distro_packages < <(yq e '.packages.debian[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) - ;; - DNF) - mapfile -t distro_packages < <(yq e '.packages.fedora[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) - ;; - esac + # 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 - # Combine package lists - local all_packages=("${base_packages[@]}" "${distro_packages[@]}") - - if [[ ${#all_packages[@]} -eq 0 ]]; then - print_warning "No packages found in configuration" + # 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 - print_info "Found ${#all_packages[@]} packages to install" - - # Update package database first - if [[ "$UPDATE_MODE" == true ]] || [[ "$FORCE_MODE" == true ]] || prompt_user "Update package database before installing?" "Y" 30; then - print_info "Updating package database..." - case "$DISTRO" in - PACMAN) execute_command "$PRIVILEGE_TOOL pacman -Sy" ;; - APT) execute_command "$PRIVILEGE_TOOL apt update" ;; - DNF) execute_command "$PRIVILEGE_TOOL dnf check-update || true" ;; - ZYPPER) execute_command "$PRIVILEGE_TOOL zypper refresh" ;; - PORTAGE) execute_command "$PRIVILEGE_TOOL emerge --sync" ;; - esac + # Determine profile to install + local profile="$INSTALL_MODE" + if [[ "$INSTALL_MODE" == "ask" ]]; then + profile="dev" # Default fi - # Install packages with progress indicator - local current=0 - for package in "${all_packages[@]}"; do - [[ -z "$package" ]] && continue + # Change to home directory to find packages.yml + local original_dir="$PWD" + cd "$HOME" 2>/dev/null || true - current=$((current + 1)) - show_progress "$current" "${#all_packages[@]}" "$package" + # Look for packages.yml in common locations + local packages_files=("$PACKAGES_FILE" "common/$PACKAGES_FILE" ".cfg/common/$PACKAGES_FILE") + local found_packages_file="" - # Check if package is already installed - local already_installed=false - case "$DISTRO" in - PACMAN) - if pacman -Q "$package" &>/dev/null; then - already_installed=true - fi - ;; - APT) - if dpkg -l "$package" 2>/dev/null | grep -q "^ii"; then - already_installed=true - fi - ;; - DNF) - if rpm -q "$package" &>/dev/null; then - already_installed=true - fi - ;; - esac - - if [[ "$already_installed" == true ]] && [[ "$FORCE_MODE" != true ]]; then - skipped_packages+=("$package") - continue + for pf in "${packages_files[@]}"; do + if [[ -f "$pf" ]]; then + found_packages_file="$pf" + break fi + done - # Install package - local install_cmd="" - case "$DISTRO" in - PACMAN) - install_cmd="$PRIVILEGE_TOOL pacman -S --noconfirm '$package'" - ;; - APT) - install_cmd="$PRIVILEGE_TOOL apt install -y '$package'" - ;; - DNF) - install_cmd="$PRIVILEGE_TOOL dnf install -y '$package'" - ;; - esac - - if execute_command "$install_cmd"; then - installed_packages+=("$package") + if [[ -n "$found_packages_file" ]]; then + if install_packages_from_yaml "$found_packages_file" "$profile"; then + mark_step_completed "install_packages" else - failed_packages+=("$package") + print_warning "Some packages failed to install, but continuing..." + mark_step_completed "install_packages" # Don't fail the whole installation fi - done - - echo # Clear progress line - - # Report results - if [[ ${#installed_packages[@]} -gt 0 ]]; then - print_success "Successfully installed ${#installed_packages[@]} packages" - fi - - if [[ ${#skipped_packages[@]} -gt 0 ]]; then - print_info "Skipped ${#skipped_packages[@]} already installed packages" - fi - - if [[ ${#failed_packages[@]} -gt 0 ]]; then - print_error "Failed to install ${#failed_packages[@]} packages: ${failed_packages[*]}" - return 1 + else + print_warning "packages.yml not found, skipping package installation" + mark_step_completed "install_packages" fi - return 0 + cd "$original_dir" 2>/dev/null || true } -# shell setup setup_shell() { print_section "Setting Up Shell Environment" save_state "setup_shell" "started" - # Install Zsh if requested - if [[ "$FORCE_MODE" == true ]] || prompt_user "Install and configure Zsh?"; then - if ! command_exists zsh; then - print_info "Installing Zsh..." - case "$DISTRO" in - PACMAN) execute_command "$PRIVILEGE_TOOL pacman -S --noconfirm zsh zsh-completions" ;; - APT) execute_command "$PRIVILEGE_TOOL apt install -y zsh zsh-autosuggestions zsh-syntax-highlighting" ;; - DNF) execute_command "$PRIVILEGE_TOOL dnf install -y zsh zsh-autosuggestions zsh-syntax-highlighting" ;; - esac - fi - - if command_exists zsh || [[ "$DRY_RUN" == true ]]; then - if [[ "$FORCE_MODE" == true ]] || prompt_user "Change default shell to Zsh?"; then - local zsh_path - zsh_path="$(which zsh 2>/dev/null || echo "/usr/bin/zsh")" - if execute_command "chsh -s '$zsh_path'"; 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 + 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 - - # Install Zsh plugins - install_zsh_plugins - else - print_error "Zsh installation failed" - mark_step_failed "setup_shell" - return 1 fi else - print_skip "Zsh setup" + 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" } -# Zsh plugin installation install_zsh_plugins() { - local plugins_dir="$HOME/.config/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" - "powerlevel10k:https://github.com/romkatv/powerlevel10k.git" ) - create_dir "$plugins_dir" - - local current=0 for plugin_info in "${plugins[@]}"; do - local plugin_name="${plugin_info%%:*}" - local plugin_url="${plugin_info##*:}" - local plugin_path="$plugins_dir/$plugin_name" - - current=$((current + 1)) - show_progress "$current" "${#plugins[@]}" "$plugin_name" + local plugin_name="${plugin_info%:*}" + local plugin_url="${plugin_info#*:}" + local plugin_dir="$zsh_plugins_dir/$plugin_name" - if [[ -d "$plugin_path" ]]; then - if [[ "$UPDATE_MODE" == true ]] || [[ "$FORCE_MODE" == true ]] || prompt_user "Update $plugin_name?" "Y" 10; then - if execute_command "(cd '$plugin_path' && git pull)"; then - print_success "Updated $plugin_name" - else - print_error "Failed to update $plugin_name" - fi - else - print_skip "Update for $plugin_name" - fi - else + if [[ ! -d "$plugin_dir" ]]; then print_info "Installing $plugin_name..." - if execute_command "git clone --depth=1 '$plugin_url' '$plugin_path'"; then + 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 - echo # Clear progress line } -# Setup SSH setup_ssh() { print_section "Setting Up SSH" save_state "setup_ssh" "started" @@ -1670,24 +1671,14 @@ setup_ssh() { if [[ "$FORCE_MODE" == true ]] || prompt_user "Generate SSH key pair?"; then create_dir "$ssh_dir" 700 - local email - if [[ "$FORCE_MODE" != true ]]; then - print_color "$YELLOW" "Enter email for SSH key (or press Enter for $USER@$HOSTNAME): " - read -r email - fi - email="${email:-$USER@$HOSTNAME}" - - # Use Ed25519 for better security - local key_type="ed25519" + local email="${USER}@${HOSTNAME:-$(hostname)}" local key_file="$ssh_dir/id_ed25519" - if execute_command "ssh-keygen -t '$key_type' -f '$key_file' -N '' -C '$email'"; then + if execute_command "ssh-keygen -t ed25519 -f '$key_file' -N '' -C '$email'"; then print_success "SSH key pair generated (Ed25519)" - execute_command "cat '$key_file.pub' >> '$ssh_dir/authorized_keys'" - execute_command "chmod 600 '$ssh_dir/authorized_keys'" - print_info "Public key added to authorized_keys" + execute_command "chmod 600 '$key_file'" + execute_command "chmod 644 '$key_file.pub'" - # Display public key if [[ "$DRY_RUN" != true ]] && [[ -f "$key_file.pub" ]]; then print_info "Your public key:" print_color "$GREEN" "$(cat "$key_file.pub")" @@ -1707,7 +1698,6 @@ setup_ssh() { } # Helper function to detect the init system -# Returns: systemd, openrc, runit, sysvinit, or unknown detect_init_system() { if [ -d /run/systemd/system ]; then echo "systemd" @@ -1723,8 +1713,6 @@ detect_init_system() { } # Helper function to manage a service (enable/start) -# Usage: manage_service <action> <service_name> -# action: enable | start manage_service() { local action="$1" local service="$2" @@ -1857,208 +1845,461 @@ configure_services() { mark_step_completed "configure_services" } -# Setup development environment +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" - # Install development tools - local dev_tools=() + # Git configuration + if command_exists git; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Configure Git global settings?"; then + configure_git + fi + fi - case "$DISTRO" in - PACMAN) dev_tools=("base-devel" "git" "vim" "neovim" "code") ;; - APT) dev_tools=("build-essential" "git" "vim" "neovim" "curl" "wget") ;; - DNF) dev_tools=("@development-tools" "git" "vim" "neovim" "curl" "wget") ;; + # 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 - if [[ ${#dev_tools[@]} -gt 0 ]]; then - if [[ "$FORCE_MODE" == true ]] || prompt_user "Install development tools?"; then - local failed_dev_tools=() - for tool in "${dev_tools[@]}"; do - case "$DISTRO" in - PACMAN) - if ! execute_command "$PRIVILEGE_TOOL pacman -S --noconfirm '$tool'"; then - failed_dev_tools+=("$tool") - fi - ;; - APT) - if ! execute_command "$PRIVILEGE_TOOL apt install -y '$tool'"; then - failed_dev_tools+=("$tool") - fi - ;; - DNF) - if ! execute_command "$PRIVILEGE_TOOL dnf install -y '$tool'"; then - failed_dev_tools+=("$tool") - fi - ;; - esac - done + mark_step_completed "setup_development" +} - if [[ ${#failed_dev_tools[@]} -eq 0 ]]; then - print_success "Development tools installed" - else - print_warning "Some development tools failed to install: ${failed_dev_tools[*]}" - fi - fi +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 - # Setup Git configuration - if command_exists git; then - if [[ "$FORCE_MODE" == true ]] || prompt_user "Configure Git global settings?"; then - local git_name git_email + 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" +} - if [[ "$FORCE_MODE" != true ]]; then - print_color "$YELLOW" "Enter your Git username: " - read -r git_name - print_color "$YELLOW" "Enter your Git email: " - read -r git_email - else - git_name="${USER}" - git_email="${USER}@$(hostname)" - fi +install_development_tools() { + if [[ "$INTERNET_AVAILABLE" != true ]]; then + print_warning "No internet connectivity - skipping development tools installation" + return 0 + fi - if [[ -n "$git_name" && -n "$git_email" ]]; then - 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" - fi - fi + print_info "Installing development tools..." + + # Install Rust if not present + if ! command_exists rustc; then + install_rust fi - mark_step_completed "setup_development" + # 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 } -# Apply system tweaks -apply_tweaks() { - print_section "Applying System Tweaks" - save_state "apply_tweaks" "started" +install_rust() { + print_info "Installing Rust via rustup..." - if [[ "$CFG_OS" != "linux" ]]; then - print_skip "System tweaks (not supported on $CFG_OS)" - mark_step_completed "apply_tweaks" + if command_exists rustup; then + print_info "Rust already installed" return 0 fi - # Improve system responsiveness - if [[ "$FORCE_MODE" == true ]] || prompt_user "Apply system performance tweaks?"; then - local tweaks_applied=() + 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")" - # Swappiness adjustment - if execute_command "echo 'vm.swappiness=10' | $PRIVILEGE_TOOL tee -a /etc/sysctl.conf"; then - tweaks_applied+=("Reduced swappiness to 10") + 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 - # File descriptor limits - if execute_command "echo '$USER soft nofile 65536' | $PRIVILEGE_TOOL tee -a /etc/security/limits.conf"; then - execute_command "echo '$USER hard nofile 65536' | $PRIVILEGE_TOOL tee -a /etc/security/limits.conf" - tweaks_applied+=("Increased file descriptor limits") + 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..." - # Apply tweaks immediately where possible - if [[ "$DRY_RUN" != true ]]; then - execute_command "$PRIVILEGE_TOOL sysctl vm.swappiness=10" || true + # 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 +} - if [[ ${#tweaks_applied[@]} -gt 0 ]]; then - print_success "Applied system tweaks:" - for tweak in "${tweaks_applied[@]}"; do - print_info " - $tweak" - done - print_warning "Some tweaks require a reboot to take effect" +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)" +} + #====================================== -# Summary and Cleanup +# 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_installation_summary() { print_header "Installation Summary" - # Show progress overview 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" - local completion_percent=$((completed_count * 100 / total_steps)) - print_color "$BLUE" "Completion: ${completion_percent}%" - if [[ ${#INSTALL_SUMMARY[@]} -gt 0 ]]; then print_section "Successful Operations" printf '%s\n' "${INSTALL_SUMMARY[@]}" fi - if [[ ${#SKIPPED_ITEMS[@]} -gt 0 ]]; then - print_section "Skipped Items" - printf '%s\n' "${SKIPPED_ITEMS[@]}" - fi - if [[ ${#FAILED_ITEMS[@]} -gt 0 ]]; then print_section "Failed Operations" printf '%s\n' "${FAILED_ITEMS[@]}" - echo - print_warning "Some operations failed. Check the log file: $LOG_FILE" - print_info "Run with --resume to continue from where you left off" - else - clear_state + print_warning "Check the log file: $LOG_FILE" fi - echo - print_color "$GREEN$BOLD" "Installation completed!" - print_info "Log file: $LOG_FILE" "always" - - if [[ ${#FAILED_ITEMS[@]} -eq 0 ]]; then - print_color "$GREEN" "🎉 All operations completed successfully!" - else - print_color "$YELLOW" "⚠️ Installation completed with ${#FAILED_ITEMS[@]} issues" - fi - - echo - print_section "Next Steps" - print_color "$CYAN" "• Restart your shell or run: exec \$SHELL" - print_color "$CYAN" "• Review configuration files in: $DOTFILES_DIR" - print_color "$CYAN" "• Use 'config status' to manage dotfiles" - - if [[ ${#FAILED_ITEMS[@]} -gt 0 ]]; then - print_color "$YELLOW" "• Run '$0 --resume' to retry failed steps" + if [[ ${#SKIPPED_ITEMS[@]} -gt 0 ]]; then + print_section "Skipped Operations" + printf '%s\n' "${SKIPPED_ITEMS[@]}" fi - if [[ -d "$BACKUP_DIR" ]] && [[ "$DRY_RUN" != true ]]; then - print_color "$CYAN" "• Backup files saved to: $BACKUP_DIR" - fi echo + print_color "$GREEN$BOLD" "Installation completed!" + print_info "Log file: $LOG_FILE" "always" } #====================================== # Main Installation Flow #====================================== -# Execute installation step with error handling execute_step() { local step_name="$1" local step_desc="${INSTALLATION_STEPS[$step_name]}" - print_section "$step_desc" - save_state "$step_name" "started" - - # Skip if already completed and not in force mode if is_step_completed "$step_name" && [[ "$FORCE_MODE" != true ]]; then print_success "$step_desc (already completed)" return 0 fi - # Execute the step function if "$step_name"; then print_success "$step_desc completed" mark_step_completed "$step_name" @@ -2070,13 +2311,9 @@ execute_step() { fi } -# Main installation function main() { - # Parse command line arguments parse_arguments "$@" - - # Initialize - setup_logging "$@" + setup_logging print_header "Dotfiles Installation" @@ -2087,22 +2324,14 @@ main() { print_info "Starting installation for user: $USER" "always" print_info "Log file: $LOG_FILE" "always" - print_info "Mode: $( - [[ "$RESUME_MODE" == true ]] && echo "Resume" || - [[ "$UPDATE_MODE" == true ]] && echo "Update" || - echo "Fresh Install" - )" "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" - print_info "Step status: ${STEP_STATUS:-unknown}" "always" - - # Load completed steps from state if [[ -n "${COMPLETED_STEPS:-}" ]]; then - eval "COMPLETED_STEPS=(${COMPLETED_STEPS})" + eval "COMPLETED_STEPS=(${COMPLETED_STEPS:-})" fi else print_warning "No previous installation state found" @@ -2111,32 +2340,12 @@ main() { fi fi - # Pre-flight checks - detect_os - detect_privilege_tools - - if [[ "$CFG_OS" == "linux" ]]; then - detect_linux_distro || { - print_error "Failed to detect Linux distribution" - exit 1 - } - fi - - # System requirements and validation - if ! check_system_requirements; then - if [[ "$FORCE_MODE" != true ]]; then - print_error "System requirements not met" - if ! prompt_user "Continue anyway? (Some features may not work)"; then - exit 1 - fi - fi - fi - - validate_config || print_warning "Configuration validation found issues" + # Select installation mode if not specified + select_installation_mode # Show installation plan echo - print_color "$YELLOW$BOLD" "Installation Plan:" + 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]}" @@ -2164,12 +2373,11 @@ main() { print_color "$MAGENTA$BOLD" "[$step_number/$total_steps] ${INSTALLATION_STEPS[$step]}" if execute_step "$step"; then - log_message "INFO" "Step completed successfully: $step" + print_info "Step completed successfully: $step" else failed_steps+=("$step") - log_message "ERROR" "Step failed: $step" + print_error "Step failed: $step" - # Ask if user wants to continue if [[ "$FORCE_MODE" != true ]] && [[ "$DRY_RUN" != true ]]; then echo if ! prompt_user "Step '$step' failed. Continue with remaining steps?" "Y"; then @@ -2182,7 +2390,7 @@ main() { step_number=$((step_number + 1)) done - # Post-installation tasks + # Post-installation if [[ ${#failed_steps[@]} -eq 0 ]]; then print_success "All installation steps completed successfully!" clear_state @@ -2193,11 +2401,8 @@ main() { fi fi - # Show summary print_installation_summary - log_message "INFO" "Installation process completed" - # Final recommendations if [[ "$DRY_RUN" != true ]]; then echo @@ -2211,16 +2416,47 @@ main() { print_color "$YELLOW" "• Check the log file for detailed error information: $LOG_FILE" fi - if [[ -d "$BACKUP_DIR" ]]; then - print_color "$CYAN" "• Your original files have been backed up to: $BACKUP_DIR" - fi - echo print_color "$GREEN$BOLD" "Thank you for using the Dotfiles Installation Script!" fi - # Exit with appropriate code [[ ${#failed_steps[@]} -eq 0 ]] && exit 0 || exit 1 } -main "$@" +#====================================== +# 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 |
