aboutsummaryrefslogtreecommitdiff
path: root/linux/home
diff options
context:
space:
mode:
authorsrdusr <trevorgray@srdusr.com>2025-09-09 19:35:40 +0200
committersrdusr <trevorgray@srdusr.com>2025-09-09 19:35:40 +0200
commit90b21c97d1cc13aa32588366c2044282aae313e3 (patch)
tree586b452d05d06f47ecf7063a07ff8dc1b6bd6ea2 /linux/home
parent017b0e5c7b84918489e1f83ec4816d18c2017d08 (diff)
downloaddotfiles-90b21c97d1cc13aa32588366c2044282aae313e3.tar.gz
dotfiles-90b21c97d1cc13aa32588366c2044282aae313e3.zip
Install/packages script
Diffstat (limited to 'linux/home')
-rwxr-xr-xlinux/home/install.sh2668
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