#!/bin/bash # Dotfiles Installation Script - Cross-Platform # Test Developer Setup: Fish or Zsh + Ghostty + Tools set -e # Exit on error DOTFILES_DIR="$HOME/git/dotfiles" BACKUP_DIR="$HOME/.config-backup-$(date +%Y%m%d-%H%M%S)" # Colors GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' BLUE='\033[0;34m' NC='\033[0m' # Package manager and OS detection PACKAGE_MANAGER="" OS="" log() { echo -e "${GREEN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } detect_os() { log "Detecting operating system..." if [[ "$OSTYPE" == "darwin"* ]]; then OS="macos" PACKAGE_MANAGER="brew" elif [[ -f /etc/arch-release ]]; then OS="arch" PACKAGE_MANAGER="pacman" elif [[ -f /etc/debian_version ]]; then OS="debian" PACKAGE_MANAGER="apt" elif [[ -f /etc/fedora-release ]]; then OS="fedora" PACKAGE_MANAGER="dnf" elif [[ -f /etc/redhat-release ]]; then OS="rhel" PACKAGE_MANAGER="yum" elif [[ -f /etc/opensuse-release ]]; then OS="opensuse" PACKAGE_MANAGER="zypper" else error "Unsupported operating system" exit 1 fi log "Detected: $OS with $PACKAGE_MANAGER" } install_package_manager() { case $PACKAGE_MANAGER in "brew") if ! command -v brew &> /dev/null; then log "Installing Homebrew..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" fi ;; "pacman") log "Pacman detected - updating package database..." sudo pacman -Sy ;; "apt") log "APT detected - updating package database..." sudo apt update ;; "dnf") log "DNF detected - updating package database..." sudo dnf check-update || true ;; "yum") log "YUM detected - updating package database..." sudo yum check-update || true ;; "zypper") log "Zypper detected - updating package database..." sudo zypper refresh ;; esac } install_package() { local package=$1 log "Installing $package..." case $PACKAGE_MANAGER in "brew") brew install "$package" ;; "pacman") sudo pacman -S --noconfirm "$package" ;; "apt") sudo apt install -y "$package" ;; "dnf") sudo dnf install -y "$package" ;; "yum") sudo yum install -y "$package" ;; "zypper") sudo zypper install -y "$package" ;; esac } install_cask() { local app=$1 log "Installing $app..." case $PACKAGE_MANAGER in "brew") brew install --cask "$app" ;; "pacman") # This shouldn't be called for Arch anymore since we use yay directly warn "Arch Linux packages should be installed via yay, not individual pacman calls" ;; "apt") case $app in "brave-browser") # Add Brave repository curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg arch=amd64] https://brave-browser-apt-release.s3.brave.com/ stable main" | sudo tee /etc/apt/sources.list.d/brave-browser-release.list sudo apt update sudo apt install -y brave-browser ;; "postman") warn "Install Postman via Snap: sudo snap install postman" ;; "1password") # Add 1Password repository curl -sS https://downloads.1password.com/linux/keys/1password.asc | sudo gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/amd64 stable main' | sudo tee /etc/apt/sources.list.d/1password.list sudo apt update sudo apt install -y 1password ;; "parsec") warn "Download Parsec manually from parseapp.com" ;; "ghostty") warn "Ghostty needs manual installation or Flatpak" ;; esac ;; "dnf"|"yum") case $app in "brave-browser") sudo dnf config-manager --add-repo https://brave-browser-rpm-release.s3.brave.com/x86_64/ sudo rpm --import https://brave-browser-rpm-release.s3.brave.com/brave-core.asc sudo dnf install -y brave-browser ;; *) warn "Install $app via Flatpak: flatpak install $app" ;; esac ;; *) warn "GUI app installation for $app not implemented for $PACKAGE_MANAGER" warn "Consider using Flatpak: flatpak install $app" ;; esac } header() { echo -e "${BLUE} ╔════════════════════════════════════╗ ║ Complete Development Setup ║ ║ Cross-Platform Tool Installer ║ ╚════════════════════════════════════╝${NC}" } check_prerequisites() { log "Checking prerequisites..." detect_os install_package_manager # Check dotfiles directory if [ ! -d "$DOTFILES_DIR" ]; then error "Dotfiles directory not found: $DOTFILES_DIR" log "Please ensure your dotfiles are at: $DOTFILES_DIR" exit 1 fi } backup_configs() { log "Creating backup..." mkdir -p "$BACKUP_DIR" # Backup existing configs [ -d ~/.config/fish ] && cp -r ~/.config/fish "$BACKUP_DIR/" [ -f ~/.zshrc ] && cp ~/.zshrc "$BACKUP_DIR/" [ -d ~/.config/ghostty ] && cp -r ~/.config/ghostty "$BACKUP_DIR/" [ -d ~/.config/neofetch ] && cp -r ~/.config/neofetch "$BACKUP_DIR/" [ -f ~/.config/starship.toml ] && cp ~/.config/starship.toml "$BACKUP_DIR/" log "Backup created: $BACKUP_DIR" } install_tools() { log "Installing development tools..." # Core tools - OS specific package names case $OS in "macos") # Command line tools install_package starship install_package neofetch install_package btop install_package git install_package fzf install_package ripgrep install_package fd install_package bat install_package go install_package python3 install_package node install_package syncthing install_package tailscale install_package vim # GUI Applications install_cask parsec install_cask postman install_cask 1password install_cask brave-browser install_cask ghostty ;; "arch") # Check if yay is installed, install if not if ! command -v yay &> /dev/null; then log "Installing yay AUR helper..." sudo pacman -S --needed base-devel git git clone https://aur.archlinux.org/yay.git /tmp/yay cd /tmp/yay && makepkg -si --noconfirm cd - && rm -rf /tmp/yay fi # Install everything via yay (handles both official repos + AUR) log "Installing all packages via yay..." yay -S --noconfirm \ starship \ neofetch \ btop \ git \ fzf \ ripgrep \ fd \ bat \ go \ python \ nodejs \ npm \ syncthing \ tailscale \ vim \ parsec \ postman-bin \ 1password \ brave-bin \ ghostty ;; "debian") # Command line tools install_package git install_package fzf install_package ripgrep install_package fd-find install_package bat install_package golang-go install_package python3 install_package nodejs install_package npm install_package syncthing install_package vim # Install starship manually curl -sS https://starship.rs/install.sh | sh install_package neofetch # Tailscale curl -fsSL https://tailscale.com/install.sh | sh warn "GUI applications need manual installation on Debian/Ubuntu:" warn "- Parsec, Postman, 1Password, Brave Browser" warn "- btop may need manual installation on older versions" ;; "fedora"|"rhel") # Command line tools install_package git install_package fzf install_package ripgrep install_package fd-find install_package bat install_package golang install_package python3 install_package nodejs install_package npm install_package syncthing install_package vim # Install starship manually curl -sS https://starship.rs/install.sh | sh install_package neofetch # Tailscale curl -fsSL https://tailscale.com/install.sh | sh warn "GUI applications available via Flatpak or manual installation" ;; "opensuse") # Command line tools install_package git install_package fzf install_package ripgrep install_package fd install_package bat install_package go install_package python3 install_package nodejs install_package npm install_package syncthing install_package vim # Install starship manually curl -sS https://starship.rs/install.sh | sh install_package neofetch # Tailscale curl -fsSL https://tailscale.com/install.sh | sh warn "GUI applications may need manual installation" ;; esac # Setup vim and other tools setup_vim install_node_tools log "Tools installation complete!" # Show post-installation notes show_post_install_notes } show_post_install_notes() { case $OS in "macos"|"arch") log "All tools installed successfully!" ;; "debian"|"fedora"|"rhel"|"opensuse") warn "Additional GUI apps can be installed via:" warn "• Flatpak: flatpak install brave postman" warn "• Snap: snap install brave postman" warn "• Manual downloads from official websites" warn "• AppImage versions where available" ;; esac } install_node_tools() { log "Installing Node.js tools..." # Ensure npm is available if command -v npm &> /dev/null; then npm install -g @playwright/test npx playwright install else warn "npm not found, skipping Playwright installation" fi } setup_vim() { log "Setting up Kickstart Vim..." # Backup existing .vimrc [ -f ~/.vimrc ] && mv ~/.vimrc ~/.vimrc.backup.$(date +%Y%m%d-%H%M%S) # Clone or update kickstart.vim if [ -d ~/kickstart.vim ]; then cd ~/kickstart.vim && git pull else git clone https://github.com/theopn/kickstart.vim.git ~/kickstart.vim fi # Create symlink ln -sf ~/kickstart.vim/.vimrc ~/.vimrc # Install plugins vim +PlugInstall +qa } setup_dotfiles_symlinks() { log "Creating dotfiles symlinks..." # Remove existing configs rm -rf ~/.config/neofetch ~/.config/btop rm -f ~/.config/starship.toml # Create symlinks to dotfiles - common configs ln -sf "$DOTFILES_DIR/neofetch" ~/.config/neofetch ln -sf "$DOTFILES_DIR/terminal/starship.toml" ~/.config/starship.toml # Ghostty (only if directory exists in dotfiles) if [ -d "$DOTFILES_DIR/terminal/ghostty" ]; then rm -rf ~/.config/ghostty ln -sf "$DOTFILES_DIR/terminal/ghostty" ~/.config/ghostty fi # Btop config mkdir -p ~/.config/btop if [ -f "$DOTFILES_DIR/btop/btop.conf" ]; then ln -sf "$DOTFILES_DIR/btop/btop.conf" ~/.config/btop/btop.conf fi if [ -d "$DOTFILES_DIR/btop/themes" ]; then ln -sf "$DOTFILES_DIR/btop/themes" ~/.config/btop/themes fi log "Dotfiles symlinks created!" } setup_auto_updates() { log "Setting up automatic system updates..." # Create update script local update_script="$HOME/.local/bin/update-system.sh" mkdir -p "$(dirname "$update_script")" : # Copy the update script content (same as before) cat > "$update_script" << 'EOF' #!/bin/bash # System Update Script # Updates all packages, dotfiles, and development tools DOTFILES_DIR="$HOME/git/dotfiles" LOG_FILE="$HOME/.local/share/system-updates.log" LAST_UPDATE_FILE="$HOME/.local/share/last-system-update" DATE=$(date '+%Y-%m-%d %H:%M:%S') # Colors for output GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' log() { echo -e "${GREEN}[INFO]${NC} $1" echo "[$DATE] INFO: $1" >> "$LOG_FILE" } warn() { echo -e "${YELLOW}[WARN]${NC} $1" echo "[$DATE] WARN: $1" >> "$LOG_FILE" } error() { echo -e "${RED}[ERROR]${NC} $1" echo "[$DATE] ERROR: $1" >> "$LOG_FILE" } detect_os() { if [[ "$OSTYPE" == "darwin"* ]]; then OS="macos" PACKAGE_MANAGER="brew" elif [[ -f /etc/arch-release ]]; then OS="arch" PACKAGE_MANAGER="yay" elif [[ -f /etc/debian_version ]]; then OS="debian" PACKAGE_MANAGER="apt" elif [[ -f /etc/fedora-release ]]; then OS="fedora" PACKAGE_MANAGER="dnf" elif [[ -f /etc/redhat-release ]]; then OS="rhel" PACKAGE_MANAGER="yum" else OS="unknown" PACKAGE_MANAGER="unknown" fi } is_update_needed() { if [ ! -f "$LAST_UPDATE_FILE" ]; then return 0 # No previous update, run it fi local last_update=$(cat "$LAST_UPDATE_FILE" 2>/dev/null || echo "0") local current_date=$(date +%s) local days_since_update=$(( (current_date - last_update) / 86400 )) if [ $days_since_update -ge 30 ]; then log "Last update was $days_since_update days ago, running update..." return 0 else log "Last update was $days_since_update days ago, skipping..." return 1 fi } update_system_packages() { log "Updating system packages ($OS)..." case $PACKAGE_MANAGER in "brew") brew update brew upgrade brew cleanup ;; "yay") yay -Syu --noconfirm yay -Yc --noconfirm ;; "apt") sudo apt update sudo apt upgrade -y sudo apt autoremove -y sudo apt autoclean ;; "dnf") sudo dnf upgrade -y sudo dnf autoremove -y ;; "yum") sudo yum update -y sudo yum clean all ;; esac } update_dotfiles() { log "Updating dotfiles..." if [ -d "$DOTFILES_DIR/.git" ]; then cd "$DOTFILES_DIR" git fetch origin LOCAL=$(git rev-parse @) REMOTE=$(git rev-parse @{u}) if [ "$LOCAL" != "$REMOTE" ]; then git pull origin main || git pull origin master log "Dotfiles updated" else log "Dotfiles up to date" fi fi } update_vim_plugins() { if [ -f "$HOME/.vimrc" ] && command -v vim &> /dev/null; then vim +PlugUpdate +PlugClean! +qa log "Vim plugins updated" fi } update_node_packages() { if command -v npm &> /dev/null; then npm update -g log "npm packages updated" fi if command -v npx &> /dev/null; then npx playwright install 2>/dev/null || true fi } mark_update_completed() { date +%s > "$LAST_UPDATE_FILE" log "Update timestamp saved" } main() { mkdir -p "$(dirname "$LOG_FILE")" mkdir -p "$(dirname "$LAST_UPDATE_FILE")" # Check if running with --force flag if [[ "$1" == "--force" ]] || is_update_needed; then echo "[$DATE] Starting system update..." >> "$LOG_FILE" detect_os log "Starting update process..." update_system_packages update_dotfiles update_vim_plugins update_node_packages mark_update_completed log "Update completed!" echo "[$DATE] Update completed" >> "$LOG_FILE" fi } main "$@" EOF chmod +x "$update_script" # Create startup check script create_startup_check "$update_script" # Setup traditional cron job (for always-on systems) setup_cron_job "$update_script" # Setup system-specific startup methods setup_startup_updates "$update_script" log "Auto-update system configured!" log "Updates will run:" log "• Monthly on 1st at 2:00 AM (if system is on)" log "• At startup if update is overdue (30+ days)" log "• Manual: $update_script --force" log "View logs: tail -f ~/.local/share/system-updates.log" } create_startup_check() { local update_script="$1" local startup_check="$HOME/.local/bin/check-updates-on-startup.sh" cat > "$startup_check" << EOF #!/bin/bash # Check for overdue updates on startup (runs max once per day) DAILY_CHECK_FILE="\$HOME/.local/share/daily-update-check" TODAY=\$(date +%Y-%m-%d) # Only run once per day if [ -f "\$DAILY_CHECK_FILE" ] && [ "\$(cat "\$DAILY_CHECK_FILE" 2>/dev/null)" = "\$TODAY" ]; then exit 0 fi # Mark today as checked echo "\$TODAY" > "\$DAILY_CHECK_FILE" # Run update check in background (non-blocking) nohup "$update_script" > /dev/null 2>&1 & EOF chmod +x "$startup_check" return "$startup_check" } setup_startup_updates() { local update_script="$1" local startup_check="$HOME/.local/bin/check-updates-on-startup.sh" case $OS in "macos") setup_macos_startup "$startup_check" ;; "arch"|"debian"|"fedora"|"rhel") setup_linux_startup "$startup_check" ;; esac } setup_macos_startup() { local startup_check="$1" local plist_file="$HOME/Library/LaunchAgents/com.user.system-updates.plist" log "Setting up macOS LaunchAgent for startup updates..." mkdir -p "$HOME/Library/LaunchAgents" cat > "$plist_file" << EOF Label com.user.system-updates ProgramArguments $startup_check RunAtLoad StandardOutPath $HOME/.local/share/startup-updates.log StandardErrorPath $HOME/.local/share/startup-updates-error.log EOF # Load the LaunchAgent launchctl load "$plist_file" 2>/dev/null || warn "LaunchAgent setup may need manual activation" log "macOS startup updates configured" } setup_linux_startup() { local startup_check="$1" # Try systemd user service first if command -v systemctl &> /dev/null; then setup_systemd_user_service "$startup_check" else # Fallback to shell profile setup_shell_profile_check "$startup_check" fi } setup_systemd_user_service() { local startup_check="$1" local service_dir="$HOME/.config/systemd/user" log "Setting up systemd user service for startup updates..." mkdir -p "$service_dir" # Create service file cat > "$service_dir/system-updates.service" << EOF [Unit] Description=System Updates Check After=network-online.target [Service] Type=oneshot ExecStart=$startup_check StandardOutput=append:$HOME/.local/share/startup-updates.log StandardError=append:$HOME/.local/share/startup-updates-error.log [Install] WantedBy=default.target EOF # Enable the service systemctl --user daemon-reload systemctl --user enable system-updates.service log "systemd user service configured" } setup_shell_profile_check() { local startup_check="$1" log "Adding startup update check to shell profiles..." # Add to shell profiles (as fallback) local check_line="# Auto-update check (once per day) if [ -f \"$startup_check\" ]; then \"$startup_check\" & fi" # Add to profiles that exist for profile in ~/.profile ~/.bash_profile ~/.zprofile; do if [ -f "$profile" ]; then if ! grep -q "check-updates-on-startup" "$profile"; then echo "$check_line" >> "$profile" log "Added to $profile" fi fi done } setup_cron_job() { local script_path="$1" log "Setting up monthly cron job..." # Create cron job entry local cron_job="0 2 1 * * $script_path >> ~/.local/share/cron-updates.log 2>&1" # Add to crontab if not already present (crontab -l 2>/dev/null | grep -v "$script_path"; echo "$cron_job") | crontab - log "Cron job installed: Monthly updates on 1st at 2:00 AM" # Show current crontab log "Current cron jobs:" crontab -l | grep -v "^#" || warn "No cron jobs found" } setup_fish() { log "Setting up Fish shell..." # Install Fish install_package fish # Install Fisher (Fish plugin manager) if ! command -v fisher &> /dev/null; then fish -c "curl -sL https://git.io/fisher | source && fisher install jorgebucaran/fisher" fi # Remove existing fish config rm -rf ~/.config/fish # Create symlink to dotfiles ln -sf "$DOTFILES_DIR/shells/fish" ~/.config/fish # Install Fish plugins log "Installing Fish plugins..." fish -c "fisher install jorgebucaran/nvm.fish" fish -c "fisher install PatrickF1/fzf.fish" fish -c "fisher install franciscolourenco/done" # Install Node.js via NVM (if available) if command -v fish &> /dev/null; then fish -c "nvm install latest && nvm use latest" || warn "NVM setup failed, Node.js should be available via system package" fi # Set as default shell local fish_path=$(which fish) if ! grep -q "$fish_path" /etc/shells; then echo "$fish_path" | sudo tee -a /etc/shells fi chsh -s "$fish_path" log "Fish shell configured!" } setup_zsh() { log "Setting up Zsh shell..." # Install Zsh install_package zsh # Remove existing configs rm -rf ~/.zshrc ~/.oh-my-zsh # Install Oh My Zsh RUNZSH=no sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" # Install NVM for Zsh (cross-platform) if [ ! -d "$HOME/.nvm" ]; then curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash fi # Create symlink to dotfiles ln -sf "$DOTFILES_DIR/shells/zsh/.zshrc" ~/.zshrc # Install Node.js via NVM export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" if command -v nvm &> /dev/null; then nvm install node && nvm use node else warn "NVM setup failed, using system Node.js" fi # Set as default shell local zsh_path=$(which zsh) if ! grep -q "$zsh_path" /etc/shells; then echo "$zsh_path" | sudo tee -a /etc/shells fi chsh -s "$zsh_path" log "Zsh shell configured!" } show_menu() { echo -e "${BLUE} Select your shell setup: 1) Fish Shell - Modern auto-completion - Clean configuration - User-friendly syntax 2) Zsh Shell - POSIX compatible - Oh-My-Zsh framework - Traditional Unix shell Detected OS: $OS ($PACKAGE_MANAGER) Tools to be installed: • Development: Go, Python, Node.js, Git, Vim • Terminal: Starship, Neofetch, btop, fzf, ripgrep, fd, bat • Networking: Syncthing, Tailscale • Applications: Parsec, Postman, 1Password, Brave Browser • Testing: Playwright framework ${NC}" read -p "Choose (1-2): " choice } main() { header check_prerequisites backup_configs install_tools setup_dotfiles_symlinks setup_auto_updates show_menu case $choice in 1) setup_fish log "Fish setup complete! Restart your terminal." ;; 2) setup_zsh log "Zsh setup complete! Restart your terminal." ;; *) error "Invalid choice. Exiting." exit 1 ;; esac echo -e "${GREEN} ╔══════════════════════════════════════════════════════════╗ ║ Setup Complete! ║ ║ ║ ║ ✓ $OS system detected ║ ║ ✓ Development tools installed ║ ║ ✓ Dotfiles symlinked ║ ║ ✓ Shell configured ║ ║ ✓ Smart auto-updates enabled ║ ║ ║ ║ Updates run automatically: ║ ║ • Monthly (1st at 2 AM) if system is on ║ ║ • At startup if overdue (30+ days) ║ ║ • Max once per day (non-blocking) ║ ║ ║ ║ Manual commands: ║ ║ • Force update: ~/.local/bin/update-system.sh --force ║ ║ • View logs: tail -f ~/.local/share/system-updates.log ║ ║ ║ ║ Perfect for laptops and desktops! ║ ║ ║ ║ Restart terminal to see changes! ║ ║ ║ ║ Backup: $BACKUP_DIR ║ ╚══════════════════════════════════════════════════════════╝${NC}" } main "$@"