From 9f036669a0fc86aaf9c584bb1828cc3469726ac1 Mon Sep 17 00:00:00 2001 From: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Date: Sat, 21 Feb 2026 08:20:29 -0500 Subject: [PATCH 1/6] refactor: modularize dotfiles with shared libraries and new features This is a comprehensive refactoring that improves modularity, reliability, and documentation of the dotfiles repository. ## Architecture Changes - Split 1,260-line install.sh into ~200-line orchestrator - Created lib/ directory with shared libraries: - utils.sh: colors, printing, file operations, OS detection - network.sh: downloads with retry logic and checksum validation - backup.sh: backup registry for rollback support - Created modules/ directory with feature modules: - package-managers.sh: Homebrew, apt, dnf, pacman, apk - dependencies.sh: git, curl, tig, build tools - nodejs.sh: Node.js via package manager or NVM - neovim.sh: binary download with checksums, vim-plug - zsh.sh: Zsh, Oh My Zsh, plugins, Powerlevel10k - link-configs.sh: symlink management - vscode.sh: template-based settings - terminal.sh: iTerm2, GNOME Terminal, Konsole, Alacritty ## New Features - --dry-run mode: preview changes without making them - --rollback command: restore from backup sessions - Retry logic: 3 retries with exponential backoff (2s/4s/8s) - Checksum validation: SHA256 for Neovim downloads - Backup registry: tracked backups in ~/.dotfiles-backups/ ## VSCode Settings Fix - Renamed settings.json to settings.json.template - Removed personal SSH configs from tracked files - Added settings.local.json.template for personal overrides - Updated .gitignore accordingly ## CI/CD - Added .github/workflows/shellcheck.yml for linting - Added .github/workflows/test-install.yml for installation tests - Matrix testing on Ubuntu and macOS ## Test Infrastructure - Added tests/test_utils.sh with 17 unit tests - Added tests/run_tests.sh test runner ## Documentation - Complete README.md rewrite with: - Requirements table - Platform support matrix - Architecture diagram - Installation options table - Rollback documentation Co-Authored-By: Claude Opus 4.5 --- .github/workflows/shellcheck.yml | 58 ++ .github/workflows/test-install.yml | 175 ++++ .gitignore | 17 + README.md | 340 ++++--- fonts/install-fonts.sh | 249 +++-- github/install-github-cli.sh | 585 +++++------ install.sh | 1450 ++++++--------------------- lib/backup.sh | 501 +++++++++ lib/network.sh | 386 +++++++ lib/utils.sh | 435 ++++++++ modules/dependencies.sh | 382 +++++++ modules/link-configs.sh | 341 +++++++ modules/neovim.sh | 527 ++++++++++ modules/nodejs.sh | 345 +++++++ modules/package-managers.sh | 354 +++++++ modules/terminal.sh | 333 ++++++ modules/vscode.sh | 332 ++++++ modules/zsh.sh | 400 ++++++++ nvim/upgrade-nvim.sh | 267 ++--- terminal/install-terminal-themes.sh | 207 +--- tests/run_tests.sh | 301 ++++++ tests/test_utils.sh | 320 ++++++ vscode/settings.json.template | 59 ++ vscode/settings.local.json.template | 25 + 24 files changed, 6256 insertions(+), 2133 deletions(-) create mode 100644 .github/workflows/shellcheck.yml create mode 100644 .github/workflows/test-install.yml create mode 100755 lib/backup.sh create mode 100755 lib/network.sh create mode 100755 lib/utils.sh create mode 100755 modules/dependencies.sh create mode 100755 modules/link-configs.sh create mode 100755 modules/neovim.sh create mode 100755 modules/nodejs.sh create mode 100755 modules/package-managers.sh create mode 100755 modules/terminal.sh create mode 100755 modules/vscode.sh create mode 100755 modules/zsh.sh create mode 100755 tests/run_tests.sh create mode 100755 tests/test_utils.sh create mode 100644 vscode/settings.json.template create mode 100644 vscode/settings.local.json.template diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 0000000..279ddc3 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,58 @@ +name: Shellcheck + +on: + push: + branches: [main] + paths: + - '**.sh' + - '.github/workflows/shellcheck.yml' + pull_request: + branches: [main] + paths: + - '**.sh' + - '.github/workflows/shellcheck.yml' + +jobs: + shellcheck: + name: Shellcheck + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install shellcheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Run shellcheck on all shell scripts + run: | + echo "Finding shell scripts..." + find . -name "*.sh" -type f | head -20 + + echo "" + echo "Running shellcheck..." + find . -name "*.sh" -type f -print0 | \ + xargs -0 shellcheck --severity=warning --shell=bash \ + -e SC1090 \ + -e SC1091 \ + -e SC2034 + + - name: Verify bash syntax + run: | + echo "Checking bash syntax..." + errors=0 + while IFS= read -r -d '' file; do + if ! bash -n "$file" 2>&1; then + echo "Syntax error in: $file" + errors=$((errors + 1)) + fi + done < <(find . -name "*.sh" -type f -print0) + + if [ $errors -gt 0 ]; then + echo "Found $errors files with syntax errors" + exit 1 + fi + + echo "All shell scripts have valid syntax" diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml new file mode 100644 index 0000000..34919db --- /dev/null +++ b/.github/workflows/test-install.yml @@ -0,0 +1,175 @@ +name: Test Installation + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test-dry-run: + name: Test Dry Run + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run dry-run installation + run: | + chmod +x install.sh + ./install.sh --dry-run + + - name: Verify no changes were made + run: | + # Check that no config files were created + if [ -L "$HOME/.zshrc" ]; then + echo "ERROR: .zshrc symlink was created in dry-run mode" + exit 1 + fi + + if [ -d "$HOME/.config/nvim" ]; then + echo "ERROR: nvim config was created in dry-run mode" + exit 1 + fi + + echo "Dry run verified - no changes made" + + test-install-ubuntu: + name: Test Full Installation (Ubuntu) + runs-on: ubuntu-latest + needs: test-dry-run + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y curl git zsh + + - name: Run installation + run: | + chmod +x install.sh + ./install.sh --skip-terminal --skip-vscode + + - name: Verify symlinks created + run: | + echo "Checking symlinks..." + + if [ ! -L "$HOME/.zshrc" ]; then + echo "ERROR: .zshrc symlink not created" + exit 1 + fi + echo "✓ .zshrc symlink exists" + + if [ ! -f "$HOME/.config/nvim/init.vim" ]; then + echo "ERROR: nvim config not created" + exit 1 + fi + echo "✓ nvim config exists" + + if [ ! -d "$HOME/.oh-my-zsh" ]; then + echo "ERROR: oh-my-zsh not installed" + exit 1 + fi + echo "✓ oh-my-zsh installed" + + echo "All verifications passed!" + + - name: Verify Neovim works + run: | + if command -v nvim &>/dev/null; then + nvim --version + echo "✓ Neovim is functional" + else + echo "Neovim not in PATH (may be in ~/.local/bin)" + if [ -f "$HOME/.local/bin/nvim" ]; then + "$HOME/.local/bin/nvim" --version + echo "✓ Neovim found in ~/.local/bin" + fi + fi + + test-install-macos: + name: Test Full Installation (macOS) + runs-on: macos-latest + needs: test-dry-run + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run installation + run: | + chmod +x install.sh + ./install.sh --skip-terminal --skip-vscode --skip-fonts + + - name: Verify symlinks created + run: | + echo "Checking symlinks..." + + if [ ! -L "$HOME/.zshrc" ]; then + echo "ERROR: .zshrc symlink not created" + exit 1 + fi + echo "✓ .zshrc symlink exists" + + if [ ! -f "$HOME/.config/nvim/init.vim" ]; then + echo "ERROR: nvim config not created" + exit 1 + fi + echo "✓ nvim config exists" + + echo "All verifications passed!" + + test-rollback: + name: Test Rollback + runs-on: ubuntu-latest + needs: test-dry-run + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y curl git zsh + + - name: Create pre-existing config + run: | + mkdir -p "$HOME/.config/nvim" + echo "original config" > "$HOME/.config/nvim/init.vim" + echo "original zshrc" > "$HOME/.zshrc" + + - name: Run installation + run: | + chmod +x install.sh + ./install.sh --skip-terminal --skip-vscode --skip-fonts + + - name: Verify backups were created + run: | + if [ ! -d "$HOME/.dotfiles-backups" ]; then + echo "ERROR: Backup directory not created" + exit 1 + fi + echo "✓ Backup directory exists" + + # Check for backup session + sessions=$(ls -1 "$HOME/.dotfiles-backups" | wc -l) + if [ "$sessions" -eq 0 ]; then + echo "ERROR: No backup sessions found" + exit 1 + fi + echo "✓ Found $sessions backup session(s)" + + - name: Test rollback (dry-run) + run: | + # Run rollback in dry-run mode + DRY_RUN=true ./install.sh --rollback <<< "y" || true + echo "✓ Rollback dry-run completed" diff --git a/.gitignore b/.gitignore index 28ce886..a391864 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,18 @@ **/CLAUDE.local.md + +# VSCode personal settings (use templates instead) +vscode/settings.json +vscode/settings.local.json + +# Backup files +*.backup +*.backup.* + +# OS files +.DS_Store +Thumbs.db + +# Editor files +*.swp +*.swo +*~ diff --git a/README.md b/README.md index 1daedcc..ded88d5 100644 --- a/README.md +++ b/README.md @@ -1,188 +1,274 @@ # Dotfiles -Personal configuration files for VSCode, Neovim, and Zsh. These dotfiles are designed to work on both macOS and Linux systems and will automatically install required dependencies. - -## Contents - -- `vscode/`: VS Code configuration -- `nvim/`: Neovim configuration -- `zsh/`: Zsh configuration -- `iterm/`: iTerm2 configuration (macOS) -- `terminal/`: Terminal configurations for Linux -- `fonts/`: Programming fonts installation scripts -- `install.sh`: Installation script that installs dependencies and creates symlinks - -## Features - -- 🚀 **Automatic installation** of all required dependencies -- 🔄 **Cross-platform** support for macOS and major Linux distributions -- 🧩 **Oh My Zsh** with useful plugins pre-configured -- 🎨 **Neovim** with modern plugins and Catppuccin Mocha theme -- 🧰 **VSCode** settings with Catppuccin Mocha theme -- 🖥️ **Terminal configurations** for iTerm2 (macOS) and Linux terminals -- 🔤 **JetBrains Mono font** installation for better readability -- 🌈 **Consistent theming** across all tools with Catppuccin -- 🔁 **Update system** for keeping dotfiles current with `--update` and `--pull` options -- ⏱️ **Automated updates** via cron job to keep everything up-to-date -- 🛠️ **Fallback configurations** for environments with restricted dependencies -- 🐙 **GitHub CLI** installation and configuration for streamlined Git workflows - -## Installation - -1. Clone this repository: - ```bash - git clone https://github.com/yourusername/dotfiles.git - cd dotfiles - ``` +Personal configuration files for development environments. These dotfiles provide a consistent, productive setup across macOS and Linux systems with automatic dependency installation. -2. Make the installation script executable: - ```bash - chmod +x install.sh - ``` +## Requirements -3. Run the installation script: - ```bash - ./install.sh - ``` +| Requirement | Version | Purpose | +|-------------|---------|---------| +| bash | 4.0+ | Installation scripts | +| git | 2.0+ | Version control, plugin installation | +| curl | any | Downloading files | +| Neovim | 0.9.0+ | Lua plugins, Treesitter (auto-installed) | +| Node.js | 16.0+ | Neovim CoC completion (auto-installed) | -### Advanced Installation Options +## Platform Support -The installation script supports various options to customize the installation: +| Platform | Package Manager | Status | +|----------|-----------------|--------| +| macOS | Homebrew | Full support | +| Ubuntu/Debian | apt | Full support | +| Fedora/RHEL | dnf | Full support | +| Arch Linux | pacman | Full support | +| Alpine Linux | apk | Partial support | +| Other Linux | Homebrew | Fallback | + +## Quick Start ```bash -./install.sh --help +# Clone the repository +git clone https://github.com/yourusername/dotfiles.git +cd dotfiles + +# Run the installer +./install.sh ``` -Available options: -- `--skip-fonts`: Skip font installation -- `--skip-neovim`: Skip Neovim configuration -- `--skip-zsh`: Skip Zsh configuration -- `--skip-vscode`: Skip VSCode configuration -- `--skip-terminal`: Skip terminal configuration -- `--update`: Update mode - skip dependency installation, only update configs -- `--pull`: Pull latest changes from git repository before installing -- `--setup-auto-update`: Configure automatic weekly updates via cron - -For example, to install everything except fonts: -```bash -./install.sh --skip-fonts +That's it! The installer will: +- Detect your OS and package manager +- Install all required dependencies +- Set up Zsh with Oh My Zsh and Powerlevel10k +- Configure Neovim with plugins +- Install fonts and terminal themes +- Create symlinks for all configurations + +## Architecture + +``` +dotfiles/ +├── install.sh # Main orchestrator (~200 lines) +├── lib/ # Shared libraries +│ ├── utils.sh # Colors, printing, file operations +│ ├── network.sh # Downloads with retry, checksums +│ └── backup.sh # Backup registry for rollback +├── modules/ # Feature modules +│ ├── package-managers.sh # Homebrew, apt, dnf, pacman, apk +│ ├── dependencies.sh # git, curl, build tools +│ ├── nodejs.sh # Node.js via package manager or NVM +│ ├── neovim.sh # Neovim binary + vim-plug +│ ├── zsh.sh # Zsh, Oh My Zsh, plugins, p10k +│ ├── link-configs.sh # Symlink management +│ ├── vscode.sh # VSCode settings + extensions +│ └── terminal.sh # iTerm2, GNOME Terminal, Konsole +├── nvim/ # Neovim configuration +├── zsh/ # Zsh configuration +├── vscode/ # VSCode settings (template-based) +├── fonts/ # Font installation +├── terminal/ # Terminal themes +├── iterm/ # iTerm2 configuration (macOS) +├── github/ # GitHub CLI setup +└── tests/ # Test infrastructure ``` -To update an existing installation: +## Installation Options + +| Flag | Description | +|------|-------------| +| `--help` | Show help message | +| `--skip-fonts` | Skip font installation | +| `--skip-neovim` | Skip Neovim configuration | +| `--skip-zsh` | Skip Zsh configuration | +| `--skip-vscode` | Skip VSCode configuration | +| `--skip-terminal` | Skip terminal configuration | +| `--update` | Update mode - skip dependency installation | +| `--pull` | Pull latest changes before installing | +| `--dry-run` | Preview changes without making them | +| `--rollback` | Rollback to previous configuration | +| `--setup-auto-update` | Configure weekly automatic updates | + +### Examples + ```bash +# Fresh installation +./install.sh + +# Update existing installation ./install.sh --update -``` -To pull the latest changes and update: -```bash +# Pull latest and update ./install.sh --pull --update -``` -To set up automated weekly updates: -```bash -./install.sh --setup-auto-update -``` +# Preview what would change +./install.sh --dry-run -The script will: +# Skip specific components +./install.sh --skip-fonts --skip-vscode -- Install package managers if needed (Homebrew, apt, dnf, pacman) -- Install and configure Zsh, Oh My Zsh, and plugins -- Install Neovim and vim-plug -- Automatically back up any existing configurations (with .backup suffix) -- Create all necessary symlinks -- Set Zsh as the default shell +# Rollback last changes +./install.sh --rollback -## What Gets Installed +# Set up automatic weekly updates +./install.sh --setup-auto-update +``` -The installation script automatically installs: +## What Gets Installed +### Shell - **Zsh** - Modern shell with advanced features -- **Oh My Zsh** - Framework for managing Zsh configuration -- **Zsh plugins** - autosuggestions, syntax-highlighting, and more -- **Neovim** - Improved Vim editor -- **vim-plug** - Plugin manager for Neovim -- **JetBrains Mono** - Programming font with ligatures -- **VSCode Extensions** - Catppuccin theme for consistent styling -- **GitHub CLI** - Command-line tool for GitHub workflows -- **Terminal Configurations**: - - iTerm2 Configuration (macOS) - - GNOME Terminal, Konsole and Alacritty (Linux) +- **Oh My Zsh** - Zsh configuration framework +- **Powerlevel10k** - Fast, customizable prompt theme +- **zsh-autosuggestions** - Fish-like autosuggestions +- **zsh-syntax-highlighting** - Syntax highlighting for commands -## Customization +### Editor +- **Neovim 0.9+** - Modern Vim with Lua support +- **vim-plug** - Plugin manager +- **CoC.nvim** - Intellisense engine (requires Node.js) +- **Telescope** - Fuzzy finder +- **Treesitter** - Better syntax highlighting -### VSCode +### Tools +- **tig** - Text-mode interface for Git +- **ripgrep** - Fast search tool +- **fd** - Fast file finder +- **fzf** - Fuzzy finder +- **GitHub CLI** - GitHub from the command line -Edit `vscode/settings.json` to customize your VSCode settings. +### Fonts & Themes +- **JetBrains Mono Nerd Font** - Programming font with icons +- **Catppuccin Mocha** - Consistent theme across all tools -### Neovim +## Uninstall / Rollback -Edit `nvim/init.vim` to customize your Neovim configuration. +### Rollback Recent Changes -During installation, you can choose from several Neovim configuration templates: -- **Default**: Basic template with minimal customization -- **Catppuccin**: Configured with the Catppuccin color theme and additional plugins -- **Monokai**: Configured with the Monokai color theme and additional plugins +The installer creates backups in `~/.dotfiles-backups/`. To rollback: -These templates are copied to `~/.config/nvim/personal.vim` and loaded automatically by the main configuration. +```bash +./install.sh --rollback +``` -### Zsh +This will: +1. List available backup sessions +2. Show what will be restored +3. Ask for confirmation +4. Restore previous configurations -Edit `zsh/.zshrc` to customize your Zsh configuration. +### Manual Uninstall -## Updating +To remove dotfiles configurations: -There are several ways to update your dotfiles installation: +```bash +# Remove symlinks +rm -f ~/.zshrc ~/.config/nvim/init.vim -### Updating on an Existing Machine +# Restore backups (if any) +ls ~/.dotfiles-backups/ -If you already have the dependencies installed and just want to update your configurations: +# Remove Oh My Zsh +rm -rf ~/.oh-my-zsh -```bash -./install.sh --update +# Change shell back to bash +chsh -s /bin/bash ``` -This will skip dependency installation and only update your configurations. +## Customization + +### Local Configuration Files + +These files are for machine-specific settings and are not tracked in git: + +| File | Purpose | +|------|---------| +| `~/.zshrc.local` | Local Zsh customizations | +| `~/.config/nvim/personal.vim` | Personal Neovim settings | +| `~/.gitconfig.local` | Git user identity | + +### Neovim Templates + +During installation, one of these templates is automatically selected based on your system: -### Pulling Latest Changes +- **personal.catppuccin.vim** - Full setup with Catppuccin theme (default) +- **personal.monokai.vim** - Monokai theme variant +- **personal.nococ.vim** - For systems without Node.js +- **personal.nolua.vim** - For older Neovim without Lua support -To get the latest changes from the repository and apply them: +### VSCode + +VSCode settings use a template system: +- `vscode/settings.json.template` - Base settings (tracked) +- `vscode/settings.local.json.template` - Personal settings template + +Personal settings (SSH hosts, etc.) should go in `settings.local.json`. + +## Updating + +### Manual Update ```bash +cd /path/to/dotfiles ./install.sh --pull --update ``` -This will pull the latest changes from the git repository and then update your configurations. +### Automatic Updates -### Automated Updates - -You can set up a weekly cron job to automatically update your dotfiles: +Set up weekly automatic updates: ```bash ./install.sh --setup-auto-update ``` -This will create a weekly cron job that runs every Sunday at noon to pull the latest changes and update your configurations. A script will be created at `~/.local/bin/update-dotfiles.sh` that you can also run manually anytime. +This creates a cron job that runs every Sunday at noon. -## Manual Installation +## Troubleshooting -If you prefer to install dependencies manually: +### Fonts not displaying correctly -### Oh My Zsh -```bash -sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" -``` +1. Ensure JetBrains Mono Nerd Font is installed: + ```bash + ./fonts/install-fonts.sh + ``` +2. Set your terminal font to "JetBrainsMono Nerd Font" +3. Restart your terminal + +### Neovim plugins not working + +1. Check Node.js is installed: `node --version` +2. Reinstall plugins: + ```bash + nvim +PlugInstall +qall + ``` +3. Check for errors: `nvim +checkhealth` + +### Zsh not default shell -### Oh My Zsh Plugins ```bash -# Install zsh-autosuggestions -git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions +# Add zsh to allowed shells +echo $(which zsh) | sudo tee -a /etc/shells -# Install zsh-syntax-highlighting -git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting +# Change default shell +chsh -s $(which zsh) + +# Log out and back in ``` -### vim-plug +### Dry run shows unexpected changes + +Use `--dry-run` to preview: ```bash -curl -fLo ~/.local/share/nvim/site/autoload/plug.vim --create-dirs \ - https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim -``` \ No newline at end of file +./install.sh --dry-run +``` + +This shows what would change without making any modifications. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run shellcheck: `./tests/run_tests.sh` +5. Submit a pull request + +## License + +MIT License - feel free to use and modify as you wish. diff --git a/fonts/install-fonts.sh b/fonts/install-fonts.sh index 6b3dbcf..c6ae175 100755 --- a/fonts/install-fonts.sh +++ b/fonts/install-fonts.sh @@ -1,45 +1,50 @@ #!/usr/bin/env bash +# fonts/install-fonts.sh - Install JetBrains Mono Nerd Font +# +# This script installs JetBrains Mono Nerd Font which includes icons +# required for Powerlevel10k and other terminal applications. -# Script to install JetBrains Mono Nerd Font (includes icons for Powerlevel10k) set -eo pipefail -# Colors -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[0;33m' -readonly BLUE='\033[0;34m' -readonly NC='\033[0m' # No Color +# ============================================================================== +# Initialization +# ============================================================================== -# Helper functions -print_info() { - echo -e "${BLUE}INFO:${NC} $1" -} +# Determine script directory and dotfiles root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_DIR="$(dirname "$SCRIPT_DIR")" -print_success() { - echo -e "${GREEN}SUCCESS:${NC} $1" -} +# Source libraries +source "$DOTFILES_DIR/lib/utils.sh" +source "$DOTFILES_DIR/lib/network.sh" -print_warning() { - echo -e "${YELLOW}WARNING:${NC} $1" -} +# ============================================================================== +# Configuration +# ============================================================================== -print_error() { - echo -e "${RED}ERROR:${NC} $1" -} +# Process command line arguments +UPDATE_MODE=false +if [[ "${1:-}" == "--update" ]]; then + UPDATE_MODE=true + print_info "Running in update mode - will only install missing fonts" +fi -# Check for required dependencies -check_dependencies() { +# ============================================================================== +# Dependency Check +# ============================================================================== + +check_font_dependencies() { local missing_deps=() - if ! command -v curl &>/dev/null; then - missing_deps+=("curl") + if ! check_command curl && ! check_command wget; then + missing_deps+=("curl or wget") fi - if ! command -v unzip &>/dev/null; then + if ! check_command unzip; then missing_deps+=("unzip") fi - if [ ${#missing_deps[@]} -gt 0 ]; then + if [[ ${#missing_deps[@]} -gt 0 ]]; then print_error "Missing required dependencies: ${missing_deps[*]}" print_info "Please install them and run this script again" return 1 @@ -47,124 +52,116 @@ check_dependencies() { return 0 } -# Process command line arguments -UPDATE_MODE=false -if [[ "$1" == "--update" ]]; then - UPDATE_MODE=true - print_info "Running in update mode - will only install missing fonts" -fi +# ============================================================================== +# Font Installation +# ============================================================================== -# Check dependencies first -if ! check_dependencies; then - exit 1 -fi +get_font_install_dir() { + local font_dir="" -# Create temporary directory for downloads with cleanup trap -TEMP_DIR=$(mktemp -d) -trap 'rm -rf "$TEMP_DIR"' EXIT + if [[ "$OS" == "macOS" ]]; then + font_dir="$HOME/Library/Fonts" + elif [[ "$OS" == "Linux" ]]; then + font_dir="$HOME/.local/share/fonts" + else + # Try common font directories + if [[ -d "$HOME/Library/Fonts" ]]; then + font_dir="$HOME/Library/Fonts" + elif [[ -d "$HOME/.local/share/fonts" ]]; then + font_dir="$HOME/.local/share/fonts" + elif [[ -d "$HOME/.fonts" ]]; then + font_dir="$HOME/.fonts" + else + font_dir="$HOME/.local/share/fonts" + fi + fi -FONT_DIR="$TEMP_DIR/fonts" + echo "$font_dir" +} -# Get latest Nerd Fonts version from GitHub API -print_info "Fetching latest Nerd Fonts version..." -NERD_FONTS_VERSION=$(curl -s "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" | grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4 | sed 's/^v//') +install_jetbrains_mono() { + local font_install_dir + font_install_dir=$(get_font_install_dir) -if [ -z "$NERD_FONTS_VERSION" ]; then - print_warning "Could not fetch latest version, using fallback v3.3.0" - NERD_FONTS_VERSION="3.3.0" -fi + print_info "Detected OS: $OS" + print_info "Font install directory: $font_install_dir" -JETBRAINS_URL="https://github.com/ryanoasis/nerd-fonts/releases/download/v${NERD_FONTS_VERSION}/JetBrainsMono.zip" -print_info "Using JetBrains Mono Nerd Font v${NERD_FONTS_VERSION}" - -# Detect operating system and set font directory -# Always install fonts regardless of OS detection -if [[ "$OSTYPE" == "darwin"* ]]; then - OS="macOS" - FONT_INSTALL_DIR="$HOME/Library/Fonts" -elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - OS="Linux" - FONT_INSTALL_DIR="$HOME/.local/share/fonts" -else - # Unknown OS - try to detect best font location - OS="Unknown" - print_warning "Unknown OS detected: $OSTYPE" - - # Try common font directories - if [ -d "$HOME/Library/Fonts" ]; then - FONT_INSTALL_DIR="$HOME/Library/Fonts" - elif [ -d "$HOME/.local/share/fonts" ]; then - FONT_INSTALL_DIR="$HOME/.local/share/fonts" - elif [ -d "$HOME/.fonts" ]; then - FONT_INSTALL_DIR="$HOME/.fonts" - else - # Default to Linux-style location - FONT_INSTALL_DIR="$HOME/.local/share/fonts" + # In update mode, check if fonts already exist + if [[ "$UPDATE_MODE" == "true" ]]; then + if ls "$font_install_dir/JetBrainsMonoNerdFont"*.ttf &>/dev/null 2>&1; then + print_info "JetBrains Mono Nerd Font already installed, skipping in update mode" + return 0 + fi fi - print_info "Using font directory: $FONT_INSTALL_DIR" -fi -print_info "Detected OS: $OS" -print_info "Font install directory: $FONT_INSTALL_DIR" + # Get latest version + print_info "Fetching latest Nerd Fonts version..." + local version + version=$(get_latest_github_release "ryanoasis/nerd-fonts" 2>/dev/null) || true -# Create font directory if it doesn't exist -mkdir -p "$FONT_INSTALL_DIR" + if [[ -z "$version" ]]; then + print_warning "Could not fetch latest version, using fallback v3.3.0" + version="v3.3.0" + fi -# Download and install JetBrains Mono -print_info "Downloading JetBrains Mono Nerd Font..." -mkdir -p "$FONT_DIR" -curl -L "$JETBRAINS_URL" -o "$TEMP_DIR/jetbrains-mono.zip" + local version_number="${version#v}" + local download_url="https://github.com/ryanoasis/nerd-fonts/releases/download/${version}/JetBrainsMono.zip" -if [ $? -ne 0 ]; then - print_error "Failed to download JetBrains Mono Nerd Font." - rm -rf "$TEMP_DIR" - exit 1 -fi + print_info "Using JetBrains Mono Nerd Font ${version}" -print_info "Extracting JetBrains Mono Nerd Font..." -unzip -q "$TEMP_DIR/jetbrains-mono.zip" -d "$FONT_DIR" + # Create temp directory + local temp_dir + temp_dir=$(mktemp -d) + trap 'rm -rf "$temp_dir"' EXIT -if [ $? -ne 0 ]; then - print_error "Failed to extract JetBrains Mono Nerd Font." - rm -rf "$TEMP_DIR" - exit 1 -fi + # Download font archive + print_info "Downloading JetBrains Mono Nerd Font..." + if ! download_with_retry "$download_url" "$temp_dir/jetbrains-mono.zip" "JetBrains Mono"; then + print_error "Failed to download JetBrains Mono Nerd Font" + return 1 + fi -print_info "Installing JetBrains Mono Nerd Font..." + # Extract + print_info "Extracting JetBrains Mono Nerd Font..." + if ! unzip -q "$temp_dir/jetbrains-mono.zip" -d "$temp_dir/fonts"; then + print_error "Failed to extract JetBrains Mono Nerd Font" + return 1 + fi -# In update mode, check if fonts already exist -if [ "$UPDATE_MODE" = true ]; then - # Check for at least one JetBrains Mono Nerd Font file - if ls "$FONT_INSTALL_DIR/JetBrainsMonoNerdFont"*.ttf &>/dev/null; then - print_info "JetBrains Mono Nerd Font already installed, skipping in update mode" - else - cp "$FONT_DIR/"*.ttf "$FONT_INSTALL_DIR/" - if [ $? -ne 0 ]; then - print_error "Failed to install JetBrains Mono Nerd Font." - rm -rf "$TEMP_DIR" - exit 1 + # Create font directory + safe_mkdir "$font_install_dir" + + # Install fonts + print_info "Installing JetBrains Mono Nerd Font..." + if ! cp "$temp_dir/fonts/"*.ttf "$font_install_dir/"; then + print_error "Failed to install JetBrains Mono Nerd Font" + return 1 + fi + + # Refresh font cache on Linux + if [[ "$OS" == "Linux" ]] || [[ "$OS" == "Unknown" ]]; then + if check_command fc-cache; then + print_info "Refreshing font cache..." + fc-cache -f >/dev/null 2>&1 && print_success "Font cache refreshed" + else + print_info "fc-cache not found - fonts will be available after next login" fi fi -else - # Install normally in non-update mode - cp "$FONT_DIR/"*.ttf "$FONT_INSTALL_DIR/" - if [ $? -ne 0 ]; then - print_error "Failed to install JetBrains Mono Nerd Font." - rm -rf "$TEMP_DIR" + + print_success "JetBrains Mono Nerd Font installed successfully!" + print_info "Set your terminal font to 'JetBrainsMono Nerd Font' (or 'JetBrainsMono NF')" +} + +# ============================================================================== +# Main +# ============================================================================== + +main() { + if ! check_font_dependencies; then exit 1 fi -fi -# Refresh font cache on Linux/Unix systems -if [ "$OS" = "Linux" ] || [ "$OS" = "Unknown" ]; then - if command -v fc-cache &>/dev/null; then - print_info "Refreshing font cache..." - fc-cache -f > /dev/null 2>&1 && print_success "Font cache refreshed." - else - print_info "fc-cache not found - fonts will be available after next login" - fi -fi + install_jetbrains_mono +} -# Cleanup is handled by trap -print_success "JetBrains Mono Nerd Font installed successfully!" -print_info "Set your terminal font to 'JetBrainsMono Nerd Font' (or 'JetBrainsMono NF')" \ No newline at end of file +main "$@" diff --git a/github/install-github-cli.sh b/github/install-github-cli.sh index 3da8c4a..b7002b0 100755 --- a/github/install-github-cli.sh +++ b/github/install-github-cli.sh @@ -1,471 +1,364 @@ #!/usr/bin/env bash -# Script to install and configure GitHub CLI -# This script handles installation across different platforms and includes -# authentication helpers for a smooth setup experience - -set -eo pipefail # Exit on error, fail on pipe failures - -# Colors for output -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[0;33m' -readonly BLUE='\033[0;34m' -readonly NC='\033[0m' # No Color - -# Helper functions -print_info() { - echo -e "${BLUE}INFO:${NC} $1" -} +# github/install-github-cli.sh - Install and configure GitHub CLI +# +# This script handles installation of the GitHub CLI across different +# platforms and includes authentication helpers. -print_success() { - echo -e "${GREEN}SUCCESS:${NC} $1" -} +set -eo pipefail -print_warning() { - echo -e "${YELLOW}WARNING:${NC} $1" -} +# ============================================================================== +# Initialization +# ============================================================================== -print_error() { - echo -e "${RED}ERROR:${NC} $1" -} +# Determine script directory and dotfiles root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_DIR="$(dirname "$SCRIPT_DIR")" + +# Source libraries +source "$DOTFILES_DIR/lib/utils.sh" +source "$DOTFILES_DIR/lib/network.sh" +source "$DOTFILES_DIR/modules/package-managers.sh" + +# ============================================================================== +# Configuration +# ============================================================================== -# Default options AUTH_MODE=true NON_INTERACTIVE=false UPDATE_MODE=false # Parse command line arguments -parse_args() { - while [[ "$#" -gt 0 ]]; do - case $1 in - --no-auth) AUTH_MODE=false ;; - --non-interactive) NON_INTERACTIVE=true ;; - --update) UPDATE_MODE=true ;; - --help) - echo "Usage: $0 [options]" - echo "Options:" - echo " --no-auth Skip authentication steps" - echo " --non-interactive Run without prompting (for automated scripts)" - echo " --update Only update if already installed" - echo " --help Show this help message" - exit 0 - ;; - *) print_error "Unknown parameter: $1"; exit 1 ;; - esac - shift - done -} +while [[ "$#" -gt 0 ]]; do + case $1 in + --no-auth) AUTH_MODE=false ;; + --non-interactive) NON_INTERACTIVE=true ;; + --update) UPDATE_MODE=true ;; + --help) + echo "Usage: $0 [options]" + echo "Options:" + echo " --no-auth Skip authentication steps" + echo " --non-interactive Run without prompting (for automated scripts)" + echo " --update Only update if already installed" + echo " --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown parameter: $1" + exit 1 + ;; + esac + shift +done -# Process arguments -parse_args "$@" +# ============================================================================== +# Installation Functions +# ============================================================================== -# Function to install GitHub CLI from standalone binary +# Install GitHub CLI from standalone binary install_standalone_binary() { local temp_dir - local arch - local gh_dir + local gh_arch local version_tag - - # Create a temporary directory + + # Create temporary directory temp_dir=$(mktemp -d) - trap 'rm -rf "$temp_dir"' EXIT - cd "$temp_dir" || return 1 - + trap 'rm -rf "$temp_dir"' RETURN + # Determine architecture - arch=$(uname -m) - case "$arch" in - x86_64) arch="amd64" ;; - armv*) arch="arm" ;; - aarch64) arch="arm64" ;; + case "$ARCH" in + x86_64) gh_arch="amd64" ;; + arm64) gh_arch="arm64" ;; + arm32) gh_arch="arm" ;; *) - print_error "Unsupported architecture: $arch" + print_error "Unsupported architecture: $ARCH" return 1 ;; esac - - # Get latest version tag + + # Get latest version print_info "Determining latest GitHub CLI version..." - if ! version_tag=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4 | cut -c2-); then + version_tag=$(get_latest_github_release "cli/cli") || { print_error "Failed to determine latest version" return 1 - fi - - # Download latest release - print_info "Downloading GitHub CLI v${version_tag} for $arch..." - if ! curl -sSL "https://github.com/cli/cli/releases/latest/download/gh_${version_tag}_linux_${arch}.tar.gz" -o "gh.tar.gz"; then - print_error "Download failed" + } + + local version_number="${version_tag#v}" + local archive_name="gh_${version_number}_linux_${gh_arch}.tar.gz" + local download_url="https://github.com/cli/cli/releases/download/${version_tag}/${archive_name}" + + # Download + print_info "Downloading GitHub CLI ${version_tag} for ${gh_arch}..." + if ! download_with_retry "$download_url" "$temp_dir/gh.tar.gz" "GitHub CLI"; then return 1 fi - + # Extract print_info "Extracting archive..." - if ! tar xzf "gh.tar.gz"; then + if ! tar xzf "$temp_dir/gh.tar.gz" -C "$temp_dir"; then print_error "Extraction failed" return 1 fi - - gh_dir=$(find . -type d -name "gh_*" | head -n 1) - if [ -z "$gh_dir" ]; then - print_error "Extraction failed - could not find gh directory" + + local gh_dir + gh_dir=$(find "$temp_dir" -type d -name "gh_*" | head -n 1) + if [[ -z "$gh_dir" ]]; then + print_error "Could not find gh directory in archive" return 1 fi - - # Install to user's bin directory - print_info "Installing to $HOME/.local/bin..." + + # Install + safe_mkdir "$HOME/.local/bin" cp "$gh_dir/bin/gh" "$HOME/.local/bin/" chmod +x "$HOME/.local/bin/gh" - - # Add to PATH if not already there + + # Update PATH if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then export PATH="$HOME/.local/bin:$PATH" - update_shell_config_path + update_shell_path fi - + return 0 } -# Update shell configuration to include .local/bin in PATH -update_shell_config_path() { +update_shell_path() { local path_export='export PATH="$HOME/.local/bin:$PATH"' - - if [ -f "$HOME/.zshrc.local" ] && ! grep -q "$path_export" "$HOME/.zshrc.local"; then + + if [[ -f "$HOME/.zshrc.local" ]] && ! grep -q '.local/bin' "$HOME/.zshrc.local" 2>/dev/null; then echo "$path_export" >> "$HOME/.zshrc.local" print_info "Updated .zshrc.local with PATH" - elif [ -f "$HOME/.zshrc" ] && ! grep -q "$path_export" "$HOME/.zshrc"; then + elif [[ -f "$HOME/.zshrc" ]] && ! grep -q '.local/bin' "$HOME/.zshrc" 2>/dev/null; then echo "$path_export" >> "$HOME/.zshrc" print_info "Updated .zshrc with PATH" - elif [ -f "$HOME/.bashrc" ] && ! grep -q "$path_export" "$HOME/.bashrc"; then + elif [[ -f "$HOME/.bashrc" ]] && ! grep -q '.local/bin' "$HOME/.bashrc" 2>/dev/null; then echo "$path_export" >> "$HOME/.bashrc" print_info "Updated .bashrc with PATH" fi } -# Check if GitHub CLI is already installed -check_gh_installed() { - if command -v gh &>/dev/null; then - return 0 - else - return 1 - fi -} - -# Check if GitHub CLI is authenticated -check_gh_auth() { - if command -v gh &>/dev/null; then - if gh auth status &>/dev/null; then - return 0 - fi - fi - return 1 -} +# Install via package manager +install_gh_package_manager() { + print_info "Installing GitHub CLI via package manager..." -# Install GitHub CLI -install_gh() { - print_info "Installing GitHub CLI..." - - # Detect OS - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS installation - if command -v brew &>/dev/null; then + case "$PACKAGE_MANAGER" in + brew) brew install gh - else - print_error "Homebrew is required but not installed. Please install Homebrew first." - return 1 - fi - elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - # Linux installation - if command -v apt-get &>/dev/null; then - # Debian/Ubuntu - print_info "Detected Debian/Ubuntu system" - + ;; + apt) # Import GitHub CLI GPG key and add repo - curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null - - if [ $? -ne 0 ]; then - print_warning "Failed to import GPG key with sudo. Trying alternative method..." - # Create local keyring directory if needed - mkdir -p "$HOME/.gnupg/keyrings" - curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg > "$HOME/.gnupg/keyrings/githubcli-archive-keyring.gpg" - - # Add to sources list without sudo - echo "deb [arch=$(dpkg --print-architecture) signed-by=$HOME/.gnupg/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > "$HOME/.sources.list.d/github-cli.list" - - # Try with apt-get if available - sudo apt-get update - sudo apt-get install -y gh - else - # Continue with standard installation - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null - sudo apt-get update - sudo apt-get install -y gh - fi - elif command -v dnf &>/dev/null; then - # Fedora/RHEL/CentOS - print_info "Detected Fedora/RHEL/CentOS system" + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | \ + sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null + + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | \ + sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + + sudo apt-get update + sudo apt-get install -y gh + ;; + dnf) sudo dnf install -y 'dnf-command(config-manager)' sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo sudo dnf install -y gh - elif command -v pacman &>/dev/null; then - # Arch Linux - print_info "Detected Arch Linux system" + ;; + pacman) sudo pacman -S --noconfirm github-cli - elif command -v brew &>/dev/null; then - # Linux Homebrew - print_info "Using Homebrew on Linux" - brew install gh - else - # Try standalone binary as last resort - print_info "No package manager detected. Installing standalone binary..." - - # Create directory for binaries - mkdir -p "$HOME/.local/bin" - - install_standalone_binary || return 1 + ;; + *) + return 1 + ;; + esac +} + +install_gh() { + print_info "Installing GitHub CLI..." + + # Try package manager first + if [[ -n "$PACKAGE_MANAGER" ]]; then + if install_gh_package_manager; then + if check_command gh; then + print_success "GitHub CLI installed successfully!" + gh --version + return 0 + fi fi - else - print_error "Unsupported OS: $OSTYPE" - return 1 fi - - # Verify installation - if command -v gh &>/dev/null; then - print_success "GitHub CLI installed successfully!" - gh --version - return 0 - else - print_error "GitHub CLI installation failed." - return 1 + + # Fall back to standalone binary on Linux + if [[ "$OS" == "Linux" ]]; then + print_info "Trying standalone binary installation..." + if install_standalone_binary; then + if check_command gh; then + print_success "GitHub CLI installed successfully!" + gh --version + return 0 + fi + fi fi + + print_error "GitHub CLI installation failed" + return 1 } -# Update GitHub CLI update_gh() { print_info "Updating GitHub CLI..." - - # Detect OS - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS update - if command -v brew &>/dev/null; then - brew upgrade gh - else - print_error "Homebrew is required but not installed." - return 1 - fi - elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - # Linux update - if command -v apt-get &>/dev/null; then - # Debian/Ubuntu + + case "$PACKAGE_MANAGER" in + brew) + brew upgrade gh || true + ;; + apt) sudo apt-get update sudo apt-get install --only-upgrade -y gh - elif command -v dnf &>/dev/null; then - # Fedora/RHEL/CentOS + ;; + dnf) sudo dnf upgrade -y gh - elif command -v pacman &>/dev/null; then - # Arch Linux + ;; + pacman) sudo pacman -Syu --noconfirm github-cli - elif command -v brew &>/dev/null; then - # Linux Homebrew - brew upgrade gh - elif [ -f "$HOME/.local/bin/gh" ]; then + ;; + *) # Manual installation - reinstall - print_info "Updating manually installed GitHub CLI..." - install_gh - else - print_error "Cannot update GitHub CLI. No supported package manager found." - return 1 - fi - else - print_error "Unsupported OS: $OSTYPE" - return 1 - fi - - # Verify update - if command -v gh &>/dev/null; then - print_success "GitHub CLI updated successfully!" + if [[ -f "$HOME/.local/bin/gh" ]]; then + install_standalone_binary + fi + ;; + esac + + if check_command gh; then + print_success "GitHub CLI updated!" gh --version - return 0 - else - print_error "GitHub CLI update failed." - return 1 fi } -# GitHub CLI authentication +# ============================================================================== +# Authentication +# ============================================================================== + +check_gh_auth() { + check_command gh && gh auth status &>/dev/null +} + authenticate_gh() { if check_gh_auth; then print_success "GitHub CLI is already authenticated!" gh auth status return 0 fi - - print_info "Authenticating GitHub CLI..." - - if [ "$NON_INTERACTIVE" = true ]; then - # Non-interactive authentication with token - print_info "Non-interactive mode: Checking for GitHub token..." - - if [ -n "$GITHUB_TOKEN" ]; then - echo "$GITHUB_TOKEN" | gh auth login --with-token - if [ $? -eq 0 ]; then + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + echo "$GITHUB_TOKEN" | gh auth login --with-token && { print_success "Authenticated with GitHub token" return 0 - else - print_warning "Failed to authenticate with GitHub token" - print_info "You can authenticate later by running: gh auth login" - return 0 # Return success - auth failure shouldn't fail the script - fi - else - print_info "GitHub CLI installed but not authenticated (no GITHUB_TOKEN set)" - print_info "To authenticate later, run: gh auth login" - return 0 # Return success - missing token is expected in non-interactive mode + } fi - else - # Interactive authentication - print_info "Starting interactive GitHub authentication..." - print_info "You will be prompted to authenticate with GitHub" - - if [ -t 0 ]; then - # Terminal is interactive - gh auth login + print_info "GitHub CLI installed but not authenticated (no GITHUB_TOKEN set)" + print_info "To authenticate later, run: gh auth login" + return 0 + fi - if [ $? -eq 0 ]; then - print_success "Successfully authenticated with GitHub!" - return 0 - else - print_warning "GitHub authentication was cancelled or failed" - print_info "You can authenticate later by running: gh auth login" - return 0 # Return success - user chose not to auth now - fi - else - print_info "GitHub CLI installed but not authenticated (non-interactive terminal)" - print_info "To authenticate later, run: gh auth login" - return 0 # Return success - fi + # Interactive authentication + if [[ -t 0 ]]; then + print_info "Starting interactive GitHub authentication..." + gh auth login || { + print_warning "GitHub authentication was cancelled or failed" + print_info "You can authenticate later by running: gh auth login" + } + else + print_info "GitHub CLI installed but not authenticated (non-interactive terminal)" + print_info "To authenticate later, run: gh auth login" fi + + return 0 } -# Configure GitHub CLI +# ============================================================================== +# Configuration +# ============================================================================== + configure_gh() { print_info "Configuring GitHub CLI..." - # Set default git protocol to SSH (ignore errors - might fail if not authenticated) + # Set defaults (ignore errors if not authenticated) gh config set git_protocol ssh 2>/dev/null || true - # Set editor based on preference or availability - configure_gh_editor - - # Set up shell completion - configure_gh_completion - - print_success "GitHub CLI configured!" -} - -# Configure GitHub CLI editor -configure_gh_editor() { - if [ -n "$EDITOR" ]; then + # Set editor + if [[ -n "${EDITOR:-}" ]]; then gh config set editor "$EDITOR" 2>/dev/null || true - elif command -v nvim &>/dev/null; then + elif check_command nvim; then gh config set editor nvim 2>/dev/null || true - elif command -v vim &>/dev/null; then + elif check_command vim; then gh config set editor vim 2>/dev/null || true fi -} -# Set up shell completion -configure_gh_completion() { - local shell_type completion_line - - shell_type=$(basename "$SHELL") - completion_line='eval "$(gh completion -s '"$shell_type"')"' - + # Setup shell completion + local shell_type + shell_type=$(basename "${SHELL:-bash}") + local completion_line='eval "$(gh completion -s '"$shell_type"')"' + case "$shell_type" in zsh) - # Check if completion is already configured - if ! grep -q "gh completion" "$HOME/.zshrc" 2>/dev/null && ! grep -q "gh completion" "$HOME/.zshrc.local" 2>/dev/null; then - print_info "Adding GitHub CLI completions to Zsh..." - - if [ -f "$HOME/.zshrc.local" ]; then - echo '# GitHub CLI completion' >> "$HOME/.zshrc.local" - echo "$completion_line" >> "$HOME/.zshrc.local" + if ! grep -q "gh completion" "$HOME/.zshrc" 2>/dev/null && \ + ! grep -q "gh completion" "$HOME/.zshrc.local" 2>/dev/null; then + if [[ -f "$HOME/.zshrc.local" ]]; then + echo -e "\n# GitHub CLI completion\n$completion_line" >> "$HOME/.zshrc.local" else - echo '# GitHub CLI completion' >> "$HOME/.zshrc" - echo "$completion_line" >> "$HOME/.zshrc" + echo -e "\n# GitHub CLI completion\n$completion_line" >> "$HOME/.zshrc" fi fi ;; bash) - # Check if completion is already configured if ! grep -q "gh completion" "$HOME/.bashrc" 2>/dev/null; then - print_info "Adding GitHub CLI completions to Bash..." - echo '# GitHub CLI completion' >> "$HOME/.bashrc" - echo "$completion_line" >> "$HOME/.bashrc" + echo -e "\n# GitHub CLI completion\n$completion_line" >> "$HOME/.bashrc" fi ;; - *) - print_warning "Shell completion not configured for $shell_type" - ;; esac + + print_success "GitHub CLI configured!" } -# Main execution +# ============================================================================== +# Main +# ============================================================================== + main() { - if check_gh_installed; then - handle_existing_installation + if check_command gh; then + if [[ "$UPDATE_MODE" == "true" ]]; then + update_gh + else + print_success "GitHub CLI is already installed!" + gh --version + + if [[ "$AUTH_MODE" == "true" ]] && ! check_gh_auth; then + authenticate_gh + fi + fi else - handle_new_installation + if [[ "$UPDATE_MODE" == "true" ]]; then + print_info "GitHub CLI not installed. Skipping update." + return 0 + fi + + if ! install_gh; then + print_warning "Failed to install GitHub CLI. Continuing without it." + return 0 + fi + + if [[ "$AUTH_MODE" == "true" ]]; then + authenticate_gh + fi fi - # Configure GitHub CLI (only if installed and functional) - if check_gh_installed && gh --version &>/dev/null; then + # Configure if installed and functional + if check_command gh && gh --version &>/dev/null; then configure_gh print_success "GitHub CLI setup complete!" - # Show auth status for user awareness if check_gh_auth; then print_info "GitHub CLI is authenticated" else print_info "GitHub CLI is not authenticated. Run 'gh auth login' when ready." fi fi - - return 0 -} - -# Handle existing GitHub CLI installation -handle_existing_installation() { - # If update mode, update - if [ "$UPDATE_MODE" = true ]; then - update_gh || print_warning "GitHub CLI update had issues, continuing..." - else - print_success "GitHub CLI is already installed!" - gh --version - - # Authenticate if needed and requested (but don't fail if auth fails) - if [ "$AUTH_MODE" = true ] && ! check_gh_auth; then - authenticate_gh || true # Ignore auth failures - fi - fi -} - -# Handle new GitHub CLI installation -handle_new_installation() { - # Skip installation if update mode and not installed - if [ "$UPDATE_MODE" = true ]; then - print_info "GitHub CLI not installed. Skipping update." - return 0 - fi - - # Install GitHub CLI - if ! install_gh; then - print_warning "Failed to install GitHub CLI. Continuing without it." - return 0 # Don't fail the whole script - fi - - # Authenticate if needed and requested (but don't fail if auth fails) - if [ "$AUTH_MODE" = true ] && ! check_gh_auth; then - authenticate_gh || true # Ignore auth failures - fi } -# Run main function -main \ No newline at end of file +main diff --git a/install.sh b/install.sh index 1509fdf..3bb430a 100755 --- a/install.sh +++ b/install.sh @@ -1,1261 +1,405 @@ #!/bin/bash +# install.sh - Dotfiles installation orchestrator +# +# This script coordinates the installation of all dotfiles components +# by sourcing modular libraries and calling their functions. +# +# Usage: +# ./install.sh [options] +# +# Options: +# --skip-fonts Skip font installation +# --skip-neovim Skip Neovim configuration +# --skip-zsh Skip Zsh configuration +# --skip-vscode Skip VSCode configuration +# --skip-terminal Skip terminal configuration +# --update Update mode - skip dependency installation, only update configs +# --pull Pull latest changes from git repository before installing +# --setup-auto-update Configure automatic weekly updates via cron +# --dry-run Show what would be done without making changes +# --rollback Rollback to previous configuration +# --help Show this help message -# Enable strict mode set -euo pipefail IFS=$'\n\t' -# Start timing the script execution +# ============================================================================== +# Initialization +# ============================================================================== + +# Start timing START_TIME=$(date +%s) -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color +# Determine dotfiles directory (resolving symlinks) +SCRIPT_PATH="${BASH_SOURCE[0]}" +while [[ -L "$SCRIPT_PATH" ]]; do + SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" + SCRIPT_PATH="$(readlink "$SCRIPT_PATH")" + [[ "$SCRIPT_PATH" != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH" +done +DOTFILES_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" -# Helper functions -print_info() { - echo -e "${BLUE}INFO:${NC} $1" -} +# ============================================================================== +# Source Libraries +# ============================================================================== -print_success() { - echo -e "${GREEN}SUCCESS:${NC} $1" -} +source "$DOTFILES_DIR/lib/utils.sh" +source "$DOTFILES_DIR/lib/network.sh" +source "$DOTFILES_DIR/lib/backup.sh" -print_warning() { - echo -e "${YELLOW}WARNING:${NC} $1" -} +# ============================================================================== +# Source Modules +# ============================================================================== -print_error() { - echo -e "${RED}ERROR:${NC} $1" -} +source "$DOTFILES_DIR/modules/package-managers.sh" +source "$DOTFILES_DIR/modules/dependencies.sh" +source "$DOTFILES_DIR/modules/nodejs.sh" +source "$DOTFILES_DIR/modules/neovim.sh" +source "$DOTFILES_DIR/modules/zsh.sh" +source "$DOTFILES_DIR/modules/link-configs.sh" +source "$DOTFILES_DIR/modules/vscode.sh" +source "$DOTFILES_DIR/modules/terminal.sh" + +# ============================================================================== +# CLI Options +# ============================================================================== -# Process CLI parameters SKIP_FONTS=false SKIP_NEOVIM=false SKIP_ZSH=false SKIP_VSCODE=false SKIP_TERMINAL=false UPDATE_MODE=false +DO_ROLLBACK=false + +# Parse command line arguments +parse_arguments() { + while [[ "$#" -gt 0 ]]; do + case $1 in + --skip-fonts) SKIP_FONTS=true ;; + --skip-neovim) SKIP_NEOVIM=true ;; + --skip-zsh) SKIP_ZSH=true ;; + --skip-vscode) SKIP_VSCODE=true ;; + --skip-terminal) SKIP_TERMINAL=true ;; + --update) UPDATE_MODE=true ;; + --dry-run) DRY_RUN=true ;; + --rollback) DO_ROLLBACK=true ;; + --pull) + print_info "Pulling latest changes from git repository..." + git -C "$DOTFILES_DIR" pull + print_success "Repository updated to latest version" + ;; + --setup-auto-update) + setup_auto_update + exit 0 + ;; + --help) + show_help + exit 0 + ;; + *) + print_error "Unknown parameter: $1" + echo "Run '$0 --help' for usage information" + exit 1 + ;; + esac + shift + done +} -# Initialize variables that may be used before being set -NODE_INSTALL_SUCCESS=false +show_help() { + cat << EOF +Usage: $0 [options] + +Options: + --skip-fonts Skip font installation + --skip-neovim Skip Neovim configuration + --skip-zsh Skip Zsh configuration + --skip-vscode Skip VSCode configuration + --skip-terminal Skip terminal configuration + --update Update mode - skip dependency installation, only update configs + --pull Pull latest changes from git repository before installing + --setup-auto-update Configure automatic weekly updates via cron + --dry-run Show what would be done without making changes + --rollback Rollback to previous configuration + --help Show this help message + +Examples: + Fresh install: $0 + Update existing: $0 --update + Pull and update: $0 --pull --update + Preview changes: $0 --dry-run + Rollback last changes: $0 --rollback + Skip some components: $0 --skip-fonts --skip-vscode + +For more information, see README.md +EOF +} -# Process command line arguments -while [[ "$#" -gt 0 ]]; do - case $1 in - --skip-fonts) SKIP_FONTS=true ;; - --skip-neovim) SKIP_NEOVIM=true ;; - --skip-zsh) SKIP_ZSH=true ;; - --skip-vscode) SKIP_VSCODE=true ;; - --skip-terminal) SKIP_TERMINAL=true ;; - --update) UPDATE_MODE=true ;; - --pull) - print_info "Pulling latest changes from git repository..." - git pull - print_success "Repository updated to latest version" - ;; - --setup-auto-update) - print_info "Setting up automatic updates..." - # Create update script in user's local bin - AUTO_UPDATE_SCRIPT="$HOME/.local/bin/update-dotfiles.sh" - mkdir -p "$(dirname "$AUTO_UPDATE_SCRIPT")" - - echo '#!/bin/bash' > "$AUTO_UPDATE_SCRIPT" - echo "cd $(pwd) && ./install.sh --pull --update" >> "$AUTO_UPDATE_SCRIPT" - chmod +x "$AUTO_UPDATE_SCRIPT" - - # Set up weekly cron job - (crontab -l 2>/dev/null || echo "") | grep -v "update-dotfiles.sh" | { cat; echo "0 12 * * 0 $AUTO_UPDATE_SCRIPT"; } | crontab - - - print_success "Automatic weekly updates configured! Updates will run every Sunday at noon." - print_info "To manually trigger an update, run: $AUTO_UPDATE_SCRIPT" - exit 0 - ;; - --help) - echo "Usage: $0 [options]" - echo "Options:" - echo " --skip-fonts Skip font installation" - echo " --skip-neovim Skip Neovim configuration" - echo " --skip-zsh Skip Zsh configuration" - echo " --skip-vscode Skip VSCode configuration" - echo " --skip-terminal Skip terminal configuration" - echo " --update Update mode - skip dependency installation, only update configs" - echo " --pull Pull latest changes from git repository before installing" - echo " --setup-auto-update Configure automatic weekly updates via cron" - echo " --help Show this help message" - echo "" - echo "Update scenarios:" - echo " 1. Existing machine with nvim/zsh: Use --update to skip package installation" - echo " 2. Update existing dotfiles: Use --pull --update to get latest changes" - echo " 3. Automate updates: Use --setup-auto-update to configure weekly auto-updates" - exit 0 - ;; - *) echo "Unknown parameter: $1"; exit 1 ;; - esac - shift -done +setup_auto_update() { + print_info "Setting up automatic updates..." -# Create a flag file to detect if we're resuming after oh-my-zsh installation -FLAG_FILE="/tmp/dotfiles_install_in_progress" + local auto_update_script="$HOME/.local/bin/update-dotfiles.sh" + safe_mkdir "$(dirname "$auto_update_script")" -# Handle script exit -cleanup() { - # Remove temporary files here if any - if [ -n "${FLAG_FILE:-}" ] && [ -f "$FLAG_FILE" ]; then - rm -f "$FLAG_FILE" - fi -} + cat > "$auto_update_script" << EOF +#!/bin/bash +cd "$DOTFILES_DIR" && ./install.sh --pull --update +EOF + chmod +x "$auto_update_script" -# Trap signals for cleanup -trap 'cleanup; echo -e "\n${RED}Script interrupted. Exiting...${NC}"; exit 1' INT TERM -trap 'cleanup' EXIT -trap 'echo -e "${RED}ERROR:${NC} Command failed at line $LINENO: $BASH_COMMAND"' ERR + # Set up weekly cron job + (crontab -l 2>/dev/null || echo "") | grep -v "update-dotfiles.sh" | \ + { cat; echo "0 12 * * 0 $auto_update_script"; } | crontab - -check_command() { - if [ -z "$1" ]; then - print_error "No command specified for check_command" - return 2 - fi - - if ! command -v "$1" &> /dev/null; then - return 1 - else - return 0 - fi + print_success "Automatic weekly updates configured!" + print_info "Updates will run every Sunday at noon." + print_info "To manually trigger an update, run: $auto_update_script" } -# run_command function has been removed as it was unused +# ============================================================================== +# Signal Handling +# ============================================================================== -install_homebrew() { - print_info "Installing Homebrew..." - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - - # Add Homebrew to PATH based on OS and architecture - if [[ "$OSTYPE" == "darwin"* ]]; then - if [[ $(uname -m) == "arm64" ]]; then - # M1/M2 Mac - echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.bash_profile - eval "$(/opt/homebrew/bin/brew shellenv)" - else - # Intel Mac - echo 'eval "$(/usr/local/bin/brew shellenv)"' >> ~/.bash_profile - eval "$(/usr/local/bin/brew shellenv)" - fi - else - # Linux - echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.bash_profile - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" - fi - - print_success "Homebrew installed successfully" +cleanup() { + # Remove any temporary files + : } -# Save current directory -DOTFILES_DIR="$(pwd)" - -# Check OS -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - OS="Linux" -elif [[ "$OSTYPE" == "darwin"* ]]; then - OS="macOS" -else - OS="Unknown" - print_warning "Unsupported OS detected: $OSTYPE. Some features may not work properly." -fi - -print_info "Detected OS: $OS" +trap 'cleanup; echo -e "\n${RED:-}Script interrupted. Exiting...${NC:-}"; exit 1' INT TERM +trap 'cleanup' EXIT +trap 'print_error "Command failed at line $LINENO: $BASH_COMMAND"' ERR -# Copy gitconfig.local template if it doesn't exist -if [ -f "$DOTFILES_DIR/gitconfig.local.template" ] && [ ! -f "$HOME/.gitconfig.local" ]; then - print_info "Creating git local configuration template..." - cp "$DOTFILES_DIR/gitconfig.local.template" "$HOME/.gitconfig.local" - print_success "Created ~/.gitconfig.local template - edit this file to set your git identity" -fi +# ============================================================================== +# Main Installation +# ============================================================================== +run_installation() { + print_section "Dotfiles Installation" + print_info "Detected OS: $OS ($ARCH)" + print_info "Dotfiles directory: $DOTFILES_DIR" -# Create a flag file to detect if we're resuming after oh-my-zsh installation -FLAG_FILE="/tmp/dotfiles_install_in_progress" + if [[ "$DRY_RUN" == "true" ]]; then + print_warning "DRY RUN MODE - No changes will be made" + fi -# Check if we're resuming after oh-my-zsh installation -if [ -f "$FLAG_FILE" ]; then - print_info "Resuming installation after oh-my-zsh setup..." - rm "$FLAG_FILE" - cd "$DOTFILES_DIR" -else - # First run of the script - # If not in update mode, install package managers and dependencies - if [ "$UPDATE_MODE" = false ]; then - # Install package managers if needed - if [ "$OS" = "macOS" ] && ! check_command brew; then - print_info "Homebrew not found. Installing..." - install_homebrew - elif [ "$OS" = "Linux" ]; then - if check_command apt-get; then - print_info "Debian/Ubuntu detected" - sudo apt-get update - elif check_command dnf; then - print_info "Fedora/RHEL detected" - sudo dnf check-update - elif check_command pacman; then - print_info "Arch Linux detected" - sudo pacman -Sy - elif ! check_command brew; then - print_info "Installing Homebrew for Linux..." - install_homebrew - fi - fi + # Initialize backup session + init_backup_session "install" - # Install dependencies - print_info "Installing dependencies..." + # Determine if we should backup + local backup_flag="" + if [[ "$UPDATE_MODE" != "true" ]]; then + backup_flag="--backup" + print_info "Will automatically backup any existing configurations" else - print_info "Running in update mode - skipping dependency installation" + print_info "Update mode: Will overwrite existing configurations" fi - # Install essential dependencies first - if ! check_command git; then - print_info "Installing git (required for dotfiles)..." - if [ "$OS" = "macOS" ]; then - brew install git - elif [ "$OS" = "Linux" ]; then - if check_command apt-get; then - sudo apt-get update -y - sudo apt-get install -y git - elif check_command dnf; then - sudo dnf install -y git - elif check_command pacman; then - sudo pacman -S --noconfirm git - elif check_command brew; then - brew install git - else - print_error "Could not install git. Please install it manually and run this script again." - exit 1 - fi - fi - - if check_command git; then - print_success "git installed successfully" - else - print_error "Failed to install git. This is required to continue." - exit 1 - fi + # Setup package managers and install dependencies (skip in update mode) + if [[ "$UPDATE_MODE" != "true" ]]; then + setup_package_managers + install_dependencies else - print_success "git is already installed" - fi - - # Install tig (text-mode interface for Git) - if ! check_command tig; then - print_info "Installing tig (text-mode interface for Git)..." - if [ "$OS" = "macOS" ]; then - brew install tig - elif [ "$OS" = "Linux" ]; then - if check_command apt-get; then - sudo apt-get install -y tig - elif check_command dnf; then - sudo dnf install -y tig - elif check_command pacman; then - sudo pacman -S --noconfirm tig - elif check_command brew; then - brew install tig - else - print_warning "Could not install tig. You can install it manually later." - fi - fi - - if check_command tig; then - print_success "tig installed successfully" - else - print_warning "Failed to install tig, but continuing with installation. You can install tig manually later." - fi - else - print_success "tig is already installed" + # Re-detect package manager even in update mode + detect_package_manager fi - # Install curl for downloading - if ! check_command curl; then - print_info "Installing curl (required for downloads)..." - if [ "$OS" = "macOS" ]; then - brew install curl - elif [ "$OS" = "Linux" ]; then - if check_command apt-get; then - sudo apt-get update -y - sudo apt-get install -y curl - elif check_command dnf; then - sudo dnf install -y curl - elif check_command pacman; then - sudo pacman -S --noconfirm curl - elif check_command brew; then - brew install curl - else - print_error "Could not install curl. Please install it manually." - fi - fi - - if check_command curl; then - print_success "curl installed successfully" - else - print_error "Failed to install curl. Some features may not work properly." - fi - else - print_success "curl is already installed" - fi - - # Install Node.js (required for Neovim CoC) - NODE_INSTALL_SUCCESS=false - if ! check_command node; then - print_info "Installing Node.js (required for Neovim code completion)..." - if [ "$OS" = "macOS" ]; then - brew install node >/dev/null 2>&1 && NODE_INSTALL_SUCCESS=true - elif [ "$OS" = "Linux" ]; then - if check_command apt-get; then - # First try the system package manager - if sudo apt-get install -y nodejs npm >/dev/null 2>&1; then - NODE_INSTALL_SUCCESS=true - else - # If that fails, try NodeSource - print_info "Trying alternative Node.js installation method..." - # Download to a temp file first for security - TEMP_NODEJS_SCRIPT=$(mktemp) - if curl -fsSL https://deb.nodesource.com/setup_lts.x -o "$TEMP_NODEJS_SCRIPT" 2>/dev/null; then - if sudo -E bash "$TEMP_NODEJS_SCRIPT" >/dev/null 2>&1; then - if sudo apt-get install -y nodejs >/dev/null 2>&1; then - NODE_INSTALL_SUCCESS=true - fi - fi - # Remove the temp file - rm -f "$TEMP_NODEJS_SCRIPT" - fi - fi - elif check_command dnf; then - sudo dnf install -y nodejs >/dev/null 2>&1 && NODE_INSTALL_SUCCESS=true - elif check_command pacman; then - sudo pacman -S --noconfirm nodejs npm >/dev/null 2>&1 && NODE_INSTALL_SUCCESS=true - elif check_command brew; then - brew install node >/dev/null 2>&1 && NODE_INSTALL_SUCCESS=true - fi - - # If all package managers fail, try NVM as a fallback - if [ "$NODE_INSTALL_SUCCESS" = false ] && command -v curl &>/dev/null; then - print_info "Trying to install Node.js via NVM..." - # Install NVM - export NVM_DIR="$HOME/.nvm" - if [ ! -d "$NVM_DIR" ]; then - mkdir -p "$NVM_DIR" - # Download to a temp file first for security - TEMP_NVM_SCRIPT=$(mktemp) - if curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh -o "$TEMP_NVM_SCRIPT" 2>/dev/null; then - bash "$TEMP_NVM_SCRIPT" >/dev/null 2>&1 - rm -f "$TEMP_NVM_SCRIPT" - fi - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" >/dev/null 2>&1 - fi - - # Install Node.js via NVM if NVM is available - if command -v nvm &>/dev/null; then - nvm install --lts >/dev/null 2>&1 && NODE_INSTALL_SUCCESS=true - nvm use --lts >/dev/null 2>&1 - fi - fi - fi - - if check_command node; then - print_success "Node.js installed successfully" - NODE_INSTALL_SUCCESS=true - else - print_warning "Failed to install Node.js. Creating fallback configuration for Neovim without CoC." - # Create a flag file to indicate we should use a CoC-less config - touch "$HOME/.config/nvim/.no-coc" - fi - else - print_success "Node.js is already installed" - NODE_INSTALL_SUCCESS=true - fi + # Git configuration + link_git_config "$DOTFILES_DIR" - # Install build tools (for telescope-fzf-native and other plugins) - if ! check_command make || ! check_command gcc; then - print_info "Installing build tools (required for some Neovim plugins)..." - if [ "$OS" = "macOS" ]; then - xcode-select --install 2>/dev/null || true - elif [ "$OS" = "Linux" ]; then - if check_command apt-get; then - sudo apt-get install -y build-essential - elif check_command dnf; then - sudo dnf groupinstall -y "Development Tools" - elif check_command pacman; then - sudo pacman -S --noconfirm base-devel - elif check_command brew; then - brew install gcc make - else - print_warning "Could not install build tools. Some Neovim plugins might not work properly." - fi - fi - - if check_command make && check_command gcc; then - print_success "Build tools installed successfully" - else - print_warning "Failed to install build tools. Some Neovim plugins might have limited functionality." - fi - else - print_success "Build tools are already installed" + # Node.js (needed for Neovim CoC) + if [[ "$SKIP_NEOVIM" != "true" ]] && [[ "$UPDATE_MODE" != "true" ]]; then + install_nodejs fi - # Install zsh - if ! check_command zsh; then - print_info "Installing zsh..." - if [ "$OS" = "macOS" ]; then - brew install zsh - elif [ "$OS" = "Linux" ]; then - if check_command apt-get; then - sudo apt-get update -y - sudo apt-get install -y zsh - elif check_command dnf; then - sudo dnf install -y zsh - elif check_command pacman; then - sudo pacman -S --noconfirm zsh - elif check_command brew; then - brew install zsh - else - print_error "Could not install zsh. Please install it manually." - fi - fi - - if check_command zsh; then - print_success "zsh installed successfully" + # Neovim + if [[ "$SKIP_NEOVIM" != "true" ]]; then + if [[ "$UPDATE_MODE" == "true" ]]; then + # In update mode, just update plugins + install_nvim_plugins --update + install_coc_extensions --update else - print_error "Failed to install zsh, but continuing with installation. You can install zsh manually later." + install_neovim fi - else - print_success "zsh is already installed" + link_nvim_config "$DOTFILES_DIR" "$backup_flag" fi - # Install/Update Neovim - NVIM_VERSION="0.9.0" # Specify minimum required version - - # Function to check if current Neovim version is sufficient - check_nvim_version() { - if ! command -v nvim &> /dev/null; then - return 1 - fi - - local current_version=$(nvim --version | head -n1 | cut -d' ' -f2 | sed 's/^v//') - if [ "$(printf "%s\n%s" "$current_version" "$NVIM_VERSION" | sort -V | head -n1)" = "$NVIM_VERSION" ]; then - # Current version is greater than or equal to required - return 0 - else - return 1 - fi - } - - # If we have our upgrade script, use that for consistency across all modes - if [ -f "$DOTFILES_DIR/nvim/upgrade-nvim.sh" ]; then - print_info "Checking Neovim version requirements..." - if [ "$UPDATE_MODE" = true ]; then - # In update mode, run non-interactively - "$DOTFILES_DIR/nvim/upgrade-nvim.sh" --non-interactive + # Zsh + if [[ "$SKIP_ZSH" != "true" ]]; then + if [[ "$UPDATE_MODE" != "true" ]]; then + zsh_setup "$DOTFILES_DIR" else - # In fresh install mode, check if we need to install/update - if ! check_command nvim || ! check_nvim_version; then - "$DOTFILES_DIR/nvim/upgrade-nvim.sh" --non-interactive - fi + zsh_update fi - # Fall back to the built-in method if the script doesn't exist - elif ! check_command nvim || ! check_nvim_version; then - print_info "Installing/Updating Neovim to v$NVIM_VERSION or newer..." - - if [ "$OS" = "macOS" ]; then - brew install neovim - elif [ "$OS" = "Linux" ]; then - # Detect architecture (Neovim uses linux-x86_64/linux-arm64 naming) - ARCH=$(uname -m) - NVIM_ARCH="" - case "$ARCH" in - x86_64|amd64) - NVIM_ARCH="linux-x86_64" - ;; - aarch64|arm64) - NVIM_ARCH="linux-arm64" - ;; - *) - print_warning "Architecture $ARCH may not have prebuilt Neovim binaries" - NVIM_ARCH="" - ;; - esac - - NVIM_INSTALLED=false - - # Try binary download if architecture is supported - if [ -n "$NVIM_ARCH" ]; then - NVIM_DIR="$HOME/.local/bin" - mkdir -p "$NVIM_DIR" - mkdir -p "$HOME/.local/share/nvim" - - print_info "Downloading Neovim binary package for $NVIM_ARCH..." - if curl -L "https://github.com/neovim/neovim/releases/download/stable/nvim-${NVIM_ARCH}.tar.gz" -o "/tmp/nvim-${NVIM_ARCH}.tar.gz"; then - print_info "Extracting Neovim..." - tar xzf "/tmp/nvim-${NVIM_ARCH}.tar.gz" -C "/tmp" - - # Find extracted directory (naming may vary) - EXTRACT_DIR=$(find /tmp -maxdepth 1 -type d -name "nvim-*" | head -1) - - if [ -n "$EXTRACT_DIR" ] && [ -d "$EXTRACT_DIR" ]; then - cp -f "$EXTRACT_DIR/bin/nvim" "$NVIM_DIR/" - cp -rf "$EXTRACT_DIR/share/nvim/"* "$HOME/.local/share/nvim/" - rm -rf "$EXTRACT_DIR" "/tmp/nvim-${NVIM_ARCH}.tar.gz" - chmod +x "$NVIM_DIR/nvim" - - if [[ ":$PATH:" != *":$NVIM_DIR:"* ]]; then - if [ -f "$HOME/.zshrc.local" ]; then - echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$HOME/.zshrc.local" - else - echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$HOME/.zshrc" - fi - export PATH="$HOME/.local/bin:$PATH" - fi - - print_success "Neovim installed to $NVIM_DIR/nvim" - NVIM_INSTALLED=true - else - print_error "Failed to extract Neovim. Archive may be corrupted." - rm -f "/tmp/nvim-${NVIM_ARCH}.tar.gz" - fi - else - print_error "Failed to download Neovim binary." - fi - fi + link_zsh_config "$DOTFILES_DIR" "$backup_flag" + fi - # Fallback to package manager if binary install failed - if [ "$NVIM_INSTALLED" = false ]; then - print_info "Falling back to package manager installation..." - if check_command apt-get; then - sudo apt-get update -y - sudo apt-get install -y neovim - elif check_command dnf; then - sudo dnf install -y neovim - elif check_command pacman; then - sudo pacman -S --noconfirm neovim - elif check_command brew; then - brew install neovim - else - print_error "Could not install Neovim through any method." - fi - fi - fi - - # Verify installation - if check_command nvim; then - nvim_installed_version=$(nvim --version | head -n1) - print_success "Neovim installed successfully: $nvim_installed_version" - else - print_error "Failed to install Neovim. The Neovim configuration will still be set up, but you'll need to install Neovim manually to use it." - fi - else - nvim_installed_version=$(nvim --version | head -n1) - print_success "Neovim $nvim_installed_version is already installed and meets version requirements" + # VSCode + if [[ "$SKIP_VSCODE" != "true" ]]; then + vscode_setup "$DOTFILES_DIR" "$backup_flag" fi - # Install vim-plug for Neovim - if ! [ -f "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/autoload/plug.vim" ] && ! [ -f "$HOME/.vim/autoload/plug.vim" ]; then - print_info "Installing vim-plug for Neovim..." - - # Make sure curl is available for this - if command -v curl &> /dev/null; then - # Create the directory in case it doesn't exist - mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/autoload" - - # Download vim-plug - if curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/autoload/plug.vim" --create-dirs \ - https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim; then - print_success "vim-plug installed successfully" - else - print_error "Failed to download vim-plug. Neovim plugins won't be available." - fi + # Fonts + if [[ "$SKIP_FONTS" != "true" ]]; then + print_section "Installing Fonts" + if [[ "$UPDATE_MODE" == "true" ]]; then + "$DOTFILES_DIR/fonts/install-fonts.sh" --update else - print_error "curl is required to install vim-plug. Neovim plugins won't be available." + "$DOTFILES_DIR/fonts/install-fonts.sh" fi - else - print_success "vim-plug is already installed" fi - # Install Oh My Zsh (needs special handling as it changes the shell) - if [ ! -d "$HOME/.oh-my-zsh" ]; then - print_info "Installing Oh My Zsh..." - # Create flag file to mark that we need to resume after oh-my-zsh - echo "$DOTFILES_DIR" > "$FLAG_FILE" - - # Check if git is installed - if ! command -v git &> /dev/null; then - print_error "Git is required to install Oh My Zsh. Please install git first." - else - # Clone instead of using the installer to avoid the shell switch - git clone https://github.com/ohmyzsh/ohmyzsh.git "$HOME/.oh-my-zsh" - if [ $? -ne 0 ]; then - print_error "Failed to clone Oh My Zsh repository." - else - # Don't automatically change the shell - we'll manually set up .zshrc - print_success "Oh My Zsh installed successfully" - - # Install Oh My Zsh plugins and themes - print_info "Installing Oh My Zsh plugins and themes..." - mkdir -p "${HOME}/.oh-my-zsh/custom/plugins" - mkdir -p "${HOME}/.oh-my-zsh/custom/themes" - - # Install plugins - git clone https://github.com/zsh-users/zsh-autosuggestions "${HOME}/.oh-my-zsh/custom/plugins/zsh-autosuggestions" - git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${HOME}/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting" - - # Install Powerlevel10k theme - if [ ! -d "${HOME}/.oh-my-zsh/custom/themes/powerlevel10k" ]; then - print_info "Installing Powerlevel10k theme..." - git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${HOME}/.oh-my-zsh/custom/themes/powerlevel10k" - print_success "Powerlevel10k theme installed successfully" - fi - - # Create a basic p10k configuration if it doesn't exist - if [ ! -f "${HOME}/.p10k.zsh" ] && [ -f "$DOTFILES_DIR/zsh/.p10k.zsh" ]; then - print_info "Setting up Powerlevel10k configuration..." - cp "$DOTFILES_DIR/zsh/.p10k.zsh" "${HOME}/.p10k.zsh" - print_success "Powerlevel10k configuration created" - fi - - print_success "Oh My Zsh plugins and themes installed successfully" - fi - fi - else - print_success "Oh My Zsh is already installed" - - # Check and install Oh My Zsh plugins if needed - if [ ! -d "${HOME}/.oh-my-zsh/custom/plugins/zsh-autosuggestions" ]; then - print_info "Installing zsh-autosuggestions plugin..." - mkdir -p "${HOME}/.oh-my-zsh/custom/plugins" - git clone https://github.com/zsh-users/zsh-autosuggestions "${HOME}/.oh-my-zsh/custom/plugins/zsh-autosuggestions" - print_success "zsh-autosuggestions installed successfully" - fi - - if [ ! -d "${HOME}/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting" ]; then - print_info "Installing zsh-syntax-highlighting plugin..." - mkdir -p "${HOME}/.oh-my-zsh/custom/plugins" - git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "${HOME}/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting" - print_success "zsh-syntax-highlighting installed successfully" - fi - - # Check and install Powerlevel10k theme if needed - if [ ! -d "${HOME}/.oh-my-zsh/custom/themes/powerlevel10k" ]; then - print_info "Installing Powerlevel10k theme..." - mkdir -p "${HOME}/.oh-my-zsh/custom/themes" - git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${HOME}/.oh-my-zsh/custom/themes/powerlevel10k" - print_success "Powerlevel10k theme installed successfully" - fi - - # Create a basic p10k configuration if it doesn't exist - if [ ! -f "${HOME}/.p10k.zsh" ] && [ -f "$DOTFILES_DIR/zsh/.p10k.zsh" ]; then - print_info "Setting up Powerlevel10k configuration..." - cp "$DOTFILES_DIR/zsh/.p10k.zsh" "${HOME}/.p10k.zsh" - print_success "Powerlevel10k configuration created" - fi + # Terminal + if [[ "$SKIP_TERMINAL" != "true" ]]; then + terminal_setup "$DOTFILES_DIR" "$backup_flag" fi -fi -# Function to backup existing configuration -backup_if_exists() { - if [ -f "$1" ] || [ -d "$1" ]; then - BACKUP_PATH="$1.backup" - print_info "Backing up existing $1 to $BACKUP_PATH" - if mv "$1" "$BACKUP_PATH" 2>/dev/null; then - print_success "Backup created: $BACKUP_PATH" - return 0 + # GitHub CLI + if [[ -f "$DOTFILES_DIR/github/install-github-cli.sh" ]]; then + print_section "Setting up GitHub CLI" + if [[ "$UPDATE_MODE" == "true" ]]; then + "$DOTFILES_DIR/github/install-github-cli.sh" --update --non-interactive || \ + print_warning "GitHub CLI setup had issues (non-fatal)" else - print_error "Failed to create backup of $1. Check permissions." - return 2 + "$DOTFILES_DIR/github/install-github-cli.sh" --non-interactive || \ + print_warning "GitHub CLI setup had issues (non-fatal)" fi fi - return 1 + + # Cleanup old backups (keep last 5) + cleanup_old_backups 5 } -# Determine backup behavior based on update mode -if [ "$UPDATE_MODE" = true ]; then - SHOULD_BACKUP=false - print_info "Update mode: Will keep and overwrite existing configurations" -else - SHOULD_BACKUP=true - print_info "Will automatically backup any existing configurations" -fi +run_rollback() { + print_section "Rollback" -# Create symlinks -print_info "Creating symlinks and configuring applications..." + # List available sessions + list_backup_sessions -# VSCode (if not skipped) -if [ "$SKIP_VSCODE" = false ] && check_command code; then - # Install VSCode extensions - print_info "Installing VSCode extensions..." - - # Catppuccin theme - code --install-extension catppuccin.catppuccin-vsc 2>/dev/null || true - print_success "VSCode Catppuccin theme installed!" - - # Code Spell Checker - code --install-extension streetsidesoftware.code-spell-checker 2>/dev/null || true - print_success "VSCode Code Spell Checker installed!" - - # Markdown Table Formatter - code --install-extension fcrespo82.markdown-table-formatter 2>/dev/null || true - print_success "VSCode Markdown Table Formatter installed!" - - if [ "$OS" = "macOS" ] && [ -d "$HOME/Library/Application Support/Code/User" ]; then - vscode_config_dir="$HOME/Library/Application Support/Code/User" - print_info "Creating VSCode symlinks..." - vscode_settings_path="$vscode_config_dir/settings.json" - - # Backup existing settings if option selected - if [ "$SHOULD_BACKUP" = true ] && [ -f "$vscode_settings_path" ]; then - backup_if_exists "$vscode_settings_path" - fi - - ln -sf "$DOTFILES_DIR/vscode/settings.json" "$vscode_settings_path" - print_success "VSCode settings linked!" - elif [ "$OS" = "Linux" ] && [ -d "$HOME/.config/Code/User" ]; then - vscode_config_dir="$HOME/.config/Code/User" - print_info "Creating VSCode symlinks..." - vscode_settings_path="$vscode_config_dir/settings.json" - - # Backup existing settings if option selected - if [ "$SHOULD_BACKUP" = true ] && [ -f "$vscode_settings_path" ]; then - backup_if_exists "$vscode_settings_path" - fi - - ln -sf "$DOTFILES_DIR/vscode/settings.json" "$vscode_settings_path" - print_success "VSCode settings linked!" - else - print_warning "VSCode user directory not found. Skipping config linking..." - fi -else - print_warning "VSCode not found. Skipping VSCode setup..." -fi - -# Neovim (if not skipped) -if [ "$SKIP_NEOVIM" = false ]; then - print_info "Setting up Neovim configuration..." - nvim_config_dir="$HOME/.config/nvim" - nvim_init_path="$nvim_config_dir/init.vim" + echo "" - # Backup existing config if option selected - if [ "$SHOULD_BACKUP" = true ] && [ -d "$nvim_config_dir" ]; then - # Try to remove the directory first if it exists (safer approach) - if ! backup_if_exists "$nvim_config_dir"; then - print_warning "Could not backup Neovim config dir, attempting to remove it instead" - rm -rf "$nvim_config_dir" 2>/dev/null || true - fi - elif [ "$SHOULD_BACKUP" = true ] && [ -f "$nvim_init_path" ]; then - backup_if_exists "$nvim_init_path" - fi + # Perform rollback + rollback_session +} - # Create directory if it doesn't exist - if [ ! -d "$nvim_config_dir" ]; then - print_info "Creating Neovim config directory..." - mkdir -p "$nvim_config_dir" - fi +# ============================================================================== +# Summary +# ============================================================================== - print_info "Creating Neovim symlinks..." - if [ -f "$DOTFILES_DIR/nvim/init.vim" ]; then - ln -sf "$DOTFILES_DIR/nvim/init.vim" "$nvim_init_path" - print_success "Neovim config linked!" - - # Create personal.vim template if it doesn't exist - personal_nvim_dir="$HOME/.config/nvim" - personal_nvim_path="$personal_nvim_dir/personal.vim" - - if [ ! -f "$personal_nvim_path" ]; then - mkdir -p "$personal_nvim_dir" - - # Check for available templates - TEMPLATES_AVAILABLE="" - if [ -f "$DOTFILES_DIR/nvim/personal.vim.template" ]; then - TEMPLATES_AVAILABLE="${TEMPLATES_AVAILABLE}default " - fi - if [ -f "$DOTFILES_DIR/nvim/personal.catppuccin.vim" ]; then - TEMPLATES_AVAILABLE="${TEMPLATES_AVAILABLE}catppuccin " - fi - if [ -f "$DOTFILES_DIR/nvim/personal.monokai.vim" ]; then - TEMPLATES_AVAILABLE="${TEMPLATES_AVAILABLE}monokai " - fi - if [ -f "$DOTFILES_DIR/nvim/personal.nolua.vim" ]; then - TEMPLATES_AVAILABLE="${TEMPLATES_AVAILABLE}nolua " - fi - if [ -f "$DOTFILES_DIR/nvim/personal.nococ.vim" ]; then - TEMPLATES_AVAILABLE="${TEMPLATES_AVAILABLE}nococ " - fi - - # Check if Neovim has Lua support - HAS_LUA=false - if check_command nvim; then - # Create a temporary Lua test file - NVIM_LUA_TEST="/tmp/nvim_lua_test.lua" - echo "print('has_lua')" > "$NVIM_LUA_TEST" - - # Try executing a Lua file directly - if nvim --headless -c "lua dofile('$NVIM_LUA_TEST')" -c q 2>&1 | grep -q "has_lua"; then - HAS_LUA=true - print_info "Detected Neovim with Lua support" - # Fallback to checking version number - elif nvim --version | grep -q "LuaJIT"; then - HAS_LUA=true - print_info "Detected Neovim with LuaJIT support" - else - print_warning "Neovim without Lua support detected, will use compatible configuration" - fi - - # Clean up test file - rm -f "$NVIM_LUA_TEST" - fi - - # Determine which template to use - always prefer Catppuccin when possible - TEMPLATE_CHOICE="" - if [ -n "$TEMPLATES_AVAILABLE" ]; then - # Choose template based on system capabilities - prefer Catppuccin - if [ "$HAS_LUA" = false ] && [ -f "$DOTFILES_DIR/nvim/personal.nolua.vim" ]; then - # No Lua support - use nolua config - TEMPLATE_CHOICE="nolua" - print_info "Using no-Lua configuration (Neovim lacks Lua support)" - elif [ -f "$HOME/.config/nvim/.no-coc" ] && [ -f "$DOTFILES_DIR/nvim/personal.nococ.vim" ]; then - # No Node.js - use nococ config - TEMPLATE_CHOICE="nococ" - print_info "Using no-CoC configuration (Node.js not available)" - elif [ -f "$DOTFILES_DIR/nvim/personal.catppuccin.vim" ]; then - # Full capabilities - use Catppuccin - TEMPLATE_CHOICE="catppuccin" - print_info "Using Catppuccin theme configuration" - else - # Fallback to default - TEMPLATE_CHOICE="default" - fi - fi - - # Copy the chosen template - case $TEMPLATE_CHOICE in - catppuccin) - if [ -f "$DOTFILES_DIR/nvim/personal.catppuccin.vim" ]; then - cp "$DOTFILES_DIR/nvim/personal.catppuccin.vim" "$personal_nvim_path" - print_success "Created $personal_nvim_path with Catppuccin theme configuration" - else - print_error "Catppuccin template not found, falling back to default" - cp "$DOTFILES_DIR/nvim/personal.vim.template" "$personal_nvim_path" - print_info "Created $personal_nvim_path template for custom Neovim configuration" - fi - ;; - monokai) - if [ -f "$DOTFILES_DIR/nvim/personal.monokai.vim" ]; then - cp "$DOTFILES_DIR/nvim/personal.monokai.vim" "$personal_nvim_path" - print_success "Created $personal_nvim_path with Monokai theme configuration" - else - print_error "Monokai template not found, falling back to default" - cp "$DOTFILES_DIR/nvim/personal.vim.template" "$personal_nvim_path" - print_info "Created $personal_nvim_path template for custom Neovim configuration" - fi - ;; - nolua) - if [ -f "$DOTFILES_DIR/nvim/personal.nolua.vim" ]; then - cp "$DOTFILES_DIR/nvim/personal.nolua.vim" "$personal_nvim_path" - print_success "Created $personal_nvim_path with no-Lua compatible configuration" - else - print_error "No-Lua template not found, falling back to default" - cp "$DOTFILES_DIR/nvim/personal.vim.template" "$personal_nvim_path" - print_info "Created $personal_nvim_path template for custom Neovim configuration" - fi - ;; - nococ) - if [ -f "$DOTFILES_DIR/nvim/personal.nococ.vim" ]; then - cp "$DOTFILES_DIR/nvim/personal.nococ.vim" "$personal_nvim_path" - print_success "Created $personal_nvim_path with CoC-less configuration" - else - print_error "No-CoC template not found, falling back to default" - cp "$DOTFILES_DIR/nvim/personal.vim.template" "$personal_nvim_path" - print_info "Created $personal_nvim_path template for custom Neovim configuration" - fi - ;; - *) - # Default template - if [ -f "$DOTFILES_DIR/nvim/personal.vim.template" ]; then - cp "$DOTFILES_DIR/nvim/personal.vim.template" "$personal_nvim_path" - print_info "Created $personal_nvim_path template for custom Neovim configuration" - else - print_warning "No Neovim templates available" - fi - ;; - esac - fi - else - print_error "Neovim config file not found: $DOTFILES_DIR/nvim/init.vim" - fi -fi +print_summary() { + print_section "Installation Summary" -# ZSH (if not skipped) -if [ "$SKIP_ZSH" = false ]; then - print_info "Setting up Zsh configuration..." - zsh_config_path="$HOME/.zshrc" + # Build status list + local summary="" - # Backup existing config if option selected - if [ "$SHOULD_BACKUP" = true ] && [ -f "$zsh_config_path" ]; then - backup_if_exists "$zsh_config_path" + # Zsh status + if check_command zsh; then + summary+="\n${GREEN:-}✓${NC:-} Zsh is available" + else + summary+="\n${RED:-}✗${NC:-} Zsh was not installed properly" fi - print_info "Creating Zsh symlinks..." - if [ -f "$DOTFILES_DIR/zsh/.zshrc" ]; then - ln -sf "$DOTFILES_DIR/zsh/.zshrc" "$zsh_config_path" - print_success "Zsh config linked!" - - # Create the .zshrc.local template if it doesn't exist - local_zshrc_path="$HOME/.zshrc.local" - if [ ! -f "$local_zshrc_path" ] && [ -f "$DOTFILES_DIR/zsh/.zshrc.local.template" ]; then - cp "$DOTFILES_DIR/zsh/.zshrc.local.template" "$local_zshrc_path" - print_info "Created $local_zshrc_path template for custom configuration" - fi + # Neovim status + if check_command nvim; then + summary+="\n${GREEN:-}✓${NC:-} Neovim is available" else - print_error "Zsh config file not found: $DOTFILES_DIR/zsh/.zshrc" + summary+="\n${RED:-}✗${NC:-} Neovim was not installed properly" fi -fi - -# Check Node.js availability (needed for CoC) - this runs in both fresh and update modes -if check_command node; then - NODE_INSTALL_SUCCESS=true -fi -# Install Neovim plugins (if not skipped) -# First verify nvim is actually functional before attempting plugin operations -NVIM_FUNCTIONAL=false -if [ "$SKIP_NEOVIM" = false ] && check_command nvim; then - # Verify nvim can actually run - if nvim --version &>/dev/null; then - NVIM_FUNCTIONAL=true + # Tig status + if check_command tig; then + summary+="\n${GREEN:-}✓${NC:-} Tig is available" else - print_warning "Neovim found but not functional. Skipping plugin installation." + summary+="\n${RED:-}✗${NC:-} Tig was not installed properly" fi -fi -if [ "$NVIM_FUNCTIONAL" = true ]; then - if [ -f "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/autoload/plug.vim" ] || [ -f "$HOME/.vim/autoload/plug.vim" ]; then - # Check if we need to use a CoC-less configuration - if [ -f "$HOME/.config/nvim/.no-coc" ] || [ "$NODE_INSTALL_SUCCESS" = false ]; then - # CoC can't be used, so let's create a modified init.vim that doesn't depend on it - print_info "Creating CoC-less Neovim configuration..." - NVIM_CONFIG_DIR="$HOME/.config/nvim" - NVIM_INIT_PATH="$NVIM_CONFIG_DIR/init.vim" - - # Create a backup of the original init.vim if not already backed up and not in update mode - if [ ! -f "$NVIM_INIT_PATH.original" ] && [ -f "$NVIM_INIT_PATH" ] && [ "$UPDATE_MODE" = false ]; then - cp "$NVIM_INIT_PATH" "$NVIM_INIT_PATH.original" - fi - - # Use a CoC-less configuration if available - if [ -f "$DOTFILES_DIR/nvim/personal.nococ.vim" ]; then - # Use the pre-made CoC-less configuration - mkdir -p "$NVIM_CONFIG_DIR" - cp "$DOTFILES_DIR/nvim/personal.nococ.vim" "$NVIM_CONFIG_DIR/personal.vim" - print_success "Using pre-configured Node.js-free Neovim setup" - # Fallback to modifying existing init.vim as a last resort - elif [ -f "$NVIM_INIT_PATH" ]; then - # Replace CoC with simple autocompletion - sed -i.bak '/neoclide\/coc.nvim/d' "$NVIM_INIT_PATH" 2>/dev/null || sed -i '' '/neoclide\/coc.nvim/d' "$NVIM_INIT_PATH" - sed -i.bak '/g:coc_/d' "$NVIM_INIT_PATH" 2>/dev/null || sed -i '' '/g:coc_/d' "$NVIM_INIT_PATH" - - # Add simple autocompletion - echo ' -" Simple built-in autocompletion (since CoC is not available) -set omnifunc=syntaxcomplete#Complete -inoremap -' >> "$NVIM_INIT_PATH" - - print_success "Created basic Node.js-free Neovim configuration" - fi - fi - - print_info "Installing/Updating Neovim plugins..." - # Use a safer approach to install plugins - PlugUpdate instead of PlugInstall in update mode - if [ "$UPDATE_MODE" = true ]; then - nvim --headless +PlugUpdate +qall 2>/dev/null || true - print_success "Neovim plugins updated!" - else - nvim --headless +PlugInstall +qall 2>/dev/null || true - print_success "Neovim plugins installed!" - fi - - # Check for Node.js (required for CoC) and only if we don't have a no-coc flag - if [ ! -f "$HOME/.config/nvim/.no-coc" ] && command -v node &> /dev/null; then - print_info "Installing CoC extensions for Neovim..." - # Install/Update CoC extensions - if [ "$UPDATE_MODE" = true ]; then - nvim --headless +"CocUpdate" +qall 2>/dev/null || true - print_success "CoC extensions updated!" - else - nvim --headless +"CocInstall -sync coc-json coc-yaml coc-toml coc-tsserver coc-markdownlint" +qall 2>/dev/null || true - print_success "CoC extensions installed!" - fi - elif [ -f "$HOME/.config/nvim/.no-coc" ]; then - print_info "Skipping CoC extensions (Node.js not available, using fallback configuration)" - fi - - # Make sure telescope-fzf-native is built (requires make, gcc, etc.) - if command -v make &> /dev/null && (command -v gcc &> /dev/null || command -v clang &> /dev/null); then - TELESCOPE_FZF_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/nvim/plugged/telescope-fzf-native.nvim" - if [ -d "$TELESCOPE_FZF_DIR" ]; then - print_info "Building telescope-fzf-native..." - (cd "$TELESCOPE_FZF_DIR" && make) 2>/dev/null || true - print_success "telescope-fzf-native built!" - fi - else - print_warning "Building tools (make, gcc) not found. telescope-fzf-native won't be built. Install them for faster fuzzy finding." - fi + # Config files + if [[ -f "$HOME/.config/nvim/init.vim" ]]; then + summary+="\n${GREEN:-}✓${NC:-} Neovim configuration is in place" else - print_warning "vim-plug not found. Skipping Neovim plugin installation." + summary+="\n${RED:-}✗${NC:-} Neovim configuration is missing" fi -elif [ "$SKIP_NEOVIM" = false ]; then - print_warning "Neovim not found or not functional. Skipping plugin installation." -fi -# Install fonts (if not skipped) -if [ "$SKIP_FONTS" = false ]; then - print_info "Installing fonts..." - # Add update mode flag if we're in update mode - if [ "$UPDATE_MODE" = true ]; then - "$DOTFILES_DIR/fonts/install-fonts.sh" --update + if [[ -f "$HOME/.zshrc" ]]; then + summary+="\n${GREEN:-}✓${NC:-} Zsh configuration is in place" else - "$DOTFILES_DIR/fonts/install-fonts.sh" + summary+="\n${RED:-}✗${NC:-} Zsh configuration is missing" fi -else - print_info "Skipping font installation as requested" -fi - -# Configure terminals (if not skipped) -if [ "$SKIP_TERMINAL" = false ]; then - if [ "$OS" = "macOS" ]; then - # Configure iTerm2 (macOS only) - if [ -d "/Applications/iTerm.app" ] || [ -d "$HOME/Applications/iTerm.app" ]; then - print_info "Configuring iTerm2..." - - # Backup existing iTerm2 preferences if not in update mode - # Note: These backups are optional - iTerm2 config just points to dotfiles folder - if [ "$SHOULD_BACKUP" = true ] && [ "$UPDATE_MODE" = false ]; then - ITERM_PLIST="$HOME/Library/Preferences/com.googlecode.iterm2.plist" - if [ -f "$ITERM_PLIST" ]; then - backup_if_exists "$ITERM_PLIST" || print_warning "Could not backup iTerm2 plist (non-fatal)" - fi - # Skip Application Support backup - often has locked files and isn't needed - # since we're just telling iTerm2 to load prefs from dotfiles folder - fi - - # Configure iTerm2 to use our preferences - print_info "Setting iTerm2 to load preferences from dotfiles..." - - # First ensure we have a proper plist file - if [ -f "$DOTFILES_DIR/iterm/com.googlecode.iterm2.plist" ]; then - print_info "Ensuring iTerm2 plist file is properly formatted..." - # Validate plist format - if ! plutil -lint "$DOTFILES_DIR/iterm/com.googlecode.iterm2.plist" >/dev/null 2>&1; then - print_warning "iTerm2 plist file is malformed, creating a new properly formatted one..." - # Export current preferences to create a properly formatted file - defaults export com.googlecode.iterm2 /tmp/iterm2.plist.tmp - mv /tmp/iterm2.plist.tmp "$DOTFILES_DIR/iterm/com.googlecode.iterm2.plist" - fi - else - print_info "No existing iTerm2 plist found, creating a new one..." - # Export current preferences to create a properly formatted file - defaults export com.googlecode.iterm2 "$DOTFILES_DIR/iterm/com.googlecode.iterm2.plist" - fi - - defaults write com.googlecode.iterm2 LoadPrefsFromCustomFolder -bool true - defaults write com.googlecode.iterm2 PrefsCustomFolder -string "$DOTFILES_DIR/iterm" - - # Create profiles directory if needed - mkdir -p "$HOME/Library/Application Support/iTerm2/DynamicProfiles" - - print_success "iTerm2 configured! Please restart iTerm2 for changes to take effect." - print_info "Note: You may need to run 'killall cfprefsd' to force preference reload." + # Node.js status + if check_command node; then + summary+="\n${GREEN:-}✓${NC:-} Node.js is available (for Neovim CoC)" else - print_warning "iTerm2 not found. Skipping iTerm2 setup..." + summary+="\n${YELLOW:-}○${NC:-} Node.js is not available (Neovim CoC disabled)" fi + + # Git config + if [[ -f "$HOME/.gitconfig.local" ]]; then + summary+="\n${GREEN:-}✓${NC:-} Git local configuration is in place" else - # Configure Linux terminals - print_info "Configuring terminal emulators..." - # Run the terminal script with update flag if needed - if [ "$UPDATE_MODE" = true ]; then - "$DOTFILES_DIR/terminal/install-terminal-themes.sh" --update - else - "$DOTFILES_DIR/terminal/install-terminal-themes.sh" - fi + summary+="\n${YELLOW:-}○${NC:-} Git local configuration is missing" fi -else - print_info "Skipping terminal configuration as requested" -fi -# Set zsh as default shell if it's not already (and we're not skipping zsh) -if [ "$SKIP_ZSH" = false ] && [ "$SHELL" != "$(which zsh)" ] && check_command zsh; then - print_info "Setting zsh as default shell..." - ZSH_PATH=$(which zsh) - - if [ -f "$ZSH_PATH" ]; then - # Ensure zsh is in /etc/shells - if ! grep -q "$ZSH_PATH" /etc/shells 2>/dev/null; then - if command -v sudo >/dev/null 2>&1; then - echo "$ZSH_PATH" | sudo tee -a /etc/shells >/dev/null 2>&1 || true - fi - fi - - # Check if zsh is in /etc/shells before attempting chsh - if grep -q "$ZSH_PATH" /etc/shells 2>/dev/null; then - # Use sudo chsh to avoid password prompt hanging - # This works on most Linux systems where user has sudo access - if sudo chsh -s "$ZSH_PATH" "$USER" 2>/dev/null; then - print_success "Default shell changed to zsh" - print_info "Log out and back in (or reboot) for the shell change to take effect" - print_info "Or start zsh now by typing: zsh" - else - print_warning "Could not automatically change default shell" - print_info "Run manually: sudo chsh -s $ZSH_PATH $USER" - print_info "Or start zsh now by typing: zsh" - fi + # GitHub CLI + if check_command gh; then + if gh auth status &>/dev/null; then + summary+="\n${GREEN:-}✓${NC:-} GitHub CLI is installed and authenticated" else - print_warning "zsh not in /etc/shells, cannot change default shell" - print_info "Add it manually: echo $ZSH_PATH | sudo tee -a /etc/shells" - print_info "Then run: sudo chsh -s $ZSH_PATH $USER" + summary+="\n${YELLOW:-}○${NC:-} GitHub CLI is installed but not authenticated" fi else - print_error "Could not find zsh binary" + summary+="\n${YELLOW:-}○${NC:-} GitHub CLI is not installed" fi -fi - -# Calculate execution time -END_TIME=$(date +%s) -EXECUTION_TIME=$((END_TIME - START_TIME)) -MINUTES=$((EXECUTION_TIME / 60)) -SECONDS=$((EXECUTION_TIME % 60)) -# Cleanup any temporary files that might have been left behind -if [ -n "${TEMP_NODEJS_SCRIPT+x}" ] || [ -n "${TEMP_NVM_SCRIPT+x}" ]; then - for TEMP_FILE in "${TEMP_NODEJS_SCRIPT:-}" "${TEMP_NVM_SCRIPT:-}"; do - if [ -n "$TEMP_FILE" ] && [ -f "$TEMP_FILE" ]; then - rm -f "$TEMP_FILE" + # VSCode extensions (if VSCode is installed) + if check_command code; then + if code --list-extensions 2>/dev/null | grep -q "catppuccin.catppuccin-vsc"; then + summary+="\n${GREEN:-}✓${NC:-} VSCode Catppuccin theme is installed" + else + summary+="\n${YELLOW:-}○${NC:-} VSCode Catppuccin theme not found" fi - done -fi - -# Install GitHub CLI BEFORE building the summary so status is accurate -if [ -f "$DOTFILES_DIR/github/install-github-cli.sh" ]; then - print_info "Setting up GitHub CLI..." - if [ "$UPDATE_MODE" = true ]; then - "$DOTFILES_DIR/github/install-github-cli.sh" --update --non-interactive || print_warning "GitHub CLI setup had issues (non-fatal)" - else - "$DOTFILES_DIR/github/install-github-cli.sh" --non-interactive || print_warning "GitHub CLI setup had issues (non-fatal)" fi -fi -# Verify installations -INSTALLATION_SUMMARY="" + echo -e "$summary" -# Adapt summary title based on mode -if [ "$UPDATE_MODE" = true ]; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n${BLUE}Configuration Status:${NC}" -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n${BLUE}Installation Status:${NC}" -fi + # Calculate and display execution time + local end_time + end_time=$(date +%s) + local execution_time=$((end_time - START_TIME)) + local minutes=$((execution_time / 60)) + local seconds=$((execution_time % 60)) -if check_command zsh; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ Zsh is available" -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ Zsh was not installed properly" -fi - -if check_command nvim; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ Neovim is available" -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ Neovim was not installed properly" -fi - -if check_command tig; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ Tig is available" -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ Tig was not installed properly" -fi - -if [ -f "$HOME/.config/nvim/init.vim" ]; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ Neovim configuration is in place" -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ Neovim configuration is missing" -fi - -if [ -f "$HOME/.zshrc" ]; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ Zsh configuration is in place" -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ Zsh configuration is missing" -fi - -if command -v node &>/dev/null; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ Node.js is available (for Neovim CoC)" -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ Node.js is not available (Neovim CoC disabled)" -fi - -# VSCode extensions -if check_command code; then - if code --list-extensions 2>/dev/null | grep -q "catppuccin.catppuccin-vsc"; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ VSCode Catppuccin theme is installed" - else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ VSCode Catppuccin theme installation failed" - fi - - if code --list-extensions 2>/dev/null | grep -q "streetsidesoftware.code-spell-checker"; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ VSCode Code Spell Checker is installed" + echo "" + if [[ "$UPDATE_MODE" == "true" ]]; then + print_success "Update completed in ${minutes}m ${seconds}s" else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ VSCode Code Spell Checker installation failed" + print_success "Installation completed in ${minutes}m ${seconds}s" fi - - if code --list-extensions 2>/dev/null | grep -q "fcrespo82.markdown-table-formatter"; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ VSCode Markdown Table Formatter is installed" - else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ VSCode Markdown Table Formatter installation failed" - fi -fi -# Add Git configuration status -if [ -f "$HOME/.gitconfig.local" ]; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ Git local configuration is in place" -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ Git local configuration is missing" -fi + # Post-install notes + echo "" + print_info "Note: You may need to restart your terminal to see all changes." + print_info "To apply zsh changes without restarting: source ~/.zshrc" -# Add GitHub CLI status -if command -v gh &>/dev/null; then - if gh auth status &>/dev/null; then - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ GitHub CLI is installed and authenticated" - else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✓ GitHub CLI is installed but not authenticated" + if [[ "$UPDATE_MODE" == "true" ]]; then + echo "" + print_info "To set up automated weekly updates, run: $0 --setup-auto-update" fi -else - INSTALLATION_SUMMARY="${INSTALLATION_SUMMARY}\n✗ GitHub CLI is not installed" -fi - -# Display status and next steps -if [ "$UPDATE_MODE" = true ]; then - echo -e "\n${GREEN}All dotfiles have been updated!${NC}" -else - echo -e "\n${GREEN}All dotfiles have been linked!${NC}" -fi +} -print_info "Note: You may need to restart your terminal to see all changes." -print_info "To apply zsh changes without restarting: source ~/.zshrc" +# ============================================================================== +# Entry Point +# ============================================================================== -# Print installation summary -echo -e "\n${BLUE}Installation Summary:${NC}${INSTALLATION_SUMMARY}" +main() { + parse_arguments "$@" -# Auto-update info -if [ "$UPDATE_MODE" = true ]; then - echo -e "\n${BLUE}Update Information:${NC}" - echo -e "✓ This was an update operation" - echo -e "• To set up automated weekly updates, run: ./install.sh --setup-auto-update" - echo -e "• To manually update in the future, run: ./install.sh --pull --update" -fi + if [[ "$DO_ROLLBACK" == "true" ]]; then + run_rollback + else + run_installation + print_summary + fi +} -if [ "$UPDATE_MODE" = true ]; then - echo -e "\n${GREEN}Update completed in ${MINUTES}m ${SECONDS}s${NC}" -else - echo -e "\n${GREEN}Installation completed in ${MINUTES}m ${SECONDS}s${NC}" -fi \ No newline at end of file +main "$@" diff --git a/lib/backup.sh b/lib/backup.sh new file mode 100755 index 0000000..2b104b1 --- /dev/null +++ b/lib/backup.sh @@ -0,0 +1,501 @@ +#!/usr/bin/env bash +# lib/backup.sh - Backup registry library for dotfiles installation +# +# This library provides backup functionality with a registry system +# that enables rollback of changes made during installation. +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" # Required dependency +# source "$DOTFILES_DIR/lib/backup.sh" + +# Prevent multiple sourcing +[[ -n "${_BACKUP_SH_LOADED:-}" ]] && return 0 +readonly _BACKUP_SH_LOADED=1 + +# Ensure utils.sh is loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before lib/backup.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Configuration +# ============================================================================== + +# Base directory for all backups +readonly BACKUP_BASE_DIR="${BACKUP_BASE_DIR:-$HOME/.dotfiles-backups}" + +# Current session variables (set by init_backup_session) +BACKUP_SESSION_DIR="" +BACKUP_MANIFEST_FILE="" +BACKUP_SESSION_ID="" + +# ============================================================================== +# Session Management +# ============================================================================== + +# Initialize a new backup session +# Usage: init_backup_session [session_name] +# Sets: BACKUP_SESSION_DIR, BACKUP_MANIFEST_FILE, BACKUP_SESSION_ID +init_backup_session() { + local session_name="${1:-install}" + local timestamp + + timestamp=$(date +%Y%m%d_%H%M%S) + BACKUP_SESSION_ID="${session_name}_${timestamp}" + BACKUP_SESSION_DIR="${BACKUP_BASE_DIR}/${BACKUP_SESSION_ID}" + BACKUP_MANIFEST_FILE="${BACKUP_SESSION_DIR}/manifest.json" + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "create backup session: $BACKUP_SESSION_ID" + return 0 + fi + + # Create backup directory + if ! mkdir -p "$BACKUP_SESSION_DIR"; then + print_error "Failed to create backup directory: $BACKUP_SESSION_DIR" + return 1 + fi + + # Initialize manifest file + cat > "$BACKUP_MANIFEST_FILE" << EOF +{ + "session_id": "$BACKUP_SESSION_ID", + "created_at": "$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)", + "dotfiles_dir": "$(get_dotfiles_dir)", + "os": "$OS", + "arch": "$ARCH", + "backups": [] +} +EOF + + print_debug "Initialized backup session: $BACKUP_SESSION_ID" + print_debug "Backup directory: $BACKUP_SESSION_DIR" + + export BACKUP_SESSION_DIR BACKUP_MANIFEST_FILE BACKUP_SESSION_ID +} + +# Get the latest backup session (for rollback) +# Usage: get_latest_backup_session +# Outputs: session ID of the most recent backup +get_latest_backup_session() { + local latest + + if [[ ! -d "$BACKUP_BASE_DIR" ]]; then + print_error "No backup directory found: $BACKUP_BASE_DIR" + return 1 + fi + + # Find the most recent session directory + latest=$(find "$BACKUP_BASE_DIR" -mindepth 1 -maxdepth 1 -type d -name "*_*" | sort -r | head -1) + + if [[ -z "$latest" ]]; then + print_error "No backup sessions found" + return 1 + fi + + basename "$latest" +} + +# List all backup sessions +# Usage: list_backup_sessions +# Outputs: list of session IDs with dates +list_backup_sessions() { + local session_dir + local manifest + local created_at + + if [[ ! -d "$BACKUP_BASE_DIR" ]]; then + print_info "No backup directory found" + return 0 + fi + + echo "Available backup sessions:" + echo "" + + for session_dir in "$BACKUP_BASE_DIR"/*/; do + [[ ! -d "$session_dir" ]] && continue + + manifest="${session_dir}manifest.json" + if [[ -f "$manifest" ]]; then + created_at=$(grep -o '"created_at": "[^"]*"' "$manifest" | cut -d'"' -f4) + echo " - $(basename "$session_dir") (created: $created_at)" + else + echo " - $(basename "$session_dir") (no manifest)" + fi + done +} + +# ============================================================================== +# Backup Operations +# ============================================================================== + +# Register a backup in the manifest +# Usage: register_backup [type] +# Types: file, directory, symlink +register_backup() { + local original="$1" + local backup="$2" + local type="${3:-file}" + local temp_file + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "register backup: $original -> $backup" + return 0 + fi + + if [[ -z "$BACKUP_MANIFEST_FILE" ]] || [[ ! -f "$BACKUP_MANIFEST_FILE" ]]; then + print_error "No backup session initialized. Call init_backup_session first." + return 1 + fi + + # Create a temporary file for the updated manifest + temp_file=$(mktemp) + + # Use awk to add the backup entry to the JSON array + awk -v orig="$original" -v back="$backup" -v t="$type" ' + /"backups": \[/ { + print + getline + if ($0 ~ /\]/) { + # Empty array, add first entry + printf " {\"original\": \"%s\", \"backup\": \"%s\", \"type\": \"%s\"}\n", orig, back, t + } else { + # Non-empty array, add comma and entry + print + while (getline && $0 !~ /\]/) { + print + } + printf ",\n {\"original\": \"%s\", \"backup\": \"%s\", \"type\": \"%s\"}\n", orig, back, t + } + print " ]" + next + } + { print } + ' "$BACKUP_MANIFEST_FILE" > "$temp_file" + + mv "$temp_file" "$BACKUP_MANIFEST_FILE" + print_debug "Registered backup: $original -> $backup" +} + +# Backup a file or directory with registry +# Usage: backup_with_registry +# Returns: 0 on success, 1 if path doesn't exist, 2 on error +backup_with_registry() { + local path="$1" + local backup_name + local backup_path + local file_type + + # Check if path exists + if [[ ! -e "$path" ]]; then + print_debug "Nothing to backup (doesn't exist): $path" + return 1 + fi + + # Ensure session is initialized + if [[ -z "$BACKUP_SESSION_DIR" ]]; then + print_warning "No backup session initialized, using simple backup" + backup_if_exists "$path" + return $? + fi + + # Generate backup path + # Convert absolute path to relative for storage + backup_name="${path#$HOME/}" + backup_name="${backup_name//\//__}" # Replace / with __ + backup_path="${BACKUP_SESSION_DIR}/${backup_name}" + + # Determine type + if [[ -L "$path" ]]; then + file_type="symlink" + elif [[ -d "$path" ]]; then + file_type="directory" + else + file_type="file" + fi + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "backup ($file_type): $path -> $backup_path" + return 0 + fi + + print_info "Backing up: $path" + + # Perform backup + if [[ -L "$path" ]]; then + # For symlinks, store the target + readlink "$path" > "$backup_path" + elif [[ -d "$path" ]]; then + # For directories, use cp -r + if ! cp -r "$path" "$backup_path"; then + print_error "Failed to backup directory: $path" + return 2 + fi + else + # For files, use cp + if ! cp "$path" "$backup_path"; then + print_error "Failed to backup file: $path" + return 2 + fi + fi + + # Register in manifest + register_backup "$path" "$backup_path" "$file_type" + + print_success "Backed up: $path" + return 0 +} + +# ============================================================================== +# Rollback Operations +# ============================================================================== + +# Rollback a specific backup session +# Usage: rollback_session [session_id] +# If no session_id provided, uses the latest session +rollback_session() { + local session_id="${1:-}" + local session_dir + local manifest + local backup_count + + # Get session ID if not provided + if [[ -z "$session_id" ]]; then + session_id=$(get_latest_backup_session) || return 1 + fi + + session_dir="${BACKUP_BASE_DIR}/${session_id}" + manifest="${session_dir}/manifest.json" + + # Verify session exists + if [[ ! -d "$session_dir" ]]; then + print_error "Backup session not found: $session_id" + return 1 + fi + + if [[ ! -f "$manifest" ]]; then + print_error "Manifest not found for session: $session_id" + return 1 + fi + + print_section "Rollback Session: $session_id" + + # Parse manifest and show what will be restored + print_info "The following items will be restored:" + echo "" + + # Extract backups from manifest using grep/sed (portable) + backup_count=$(grep -c '"original":' "$manifest" 2>/dev/null || echo "0") + + if [[ "$backup_count" == "0" ]]; then + print_info "No backups in this session" + return 0 + fi + + # Show each backup + grep '"original":' "$manifest" | while read -r line; do + local original + original=$(echo "$line" | sed 's/.*"original": "\([^"]*\)".*/\1/') + echo " - $original" + done + + echo "" + + # Confirm rollback + if [[ "$DRY_RUN" != "true" ]]; then + if ! confirm "Proceed with rollback?"; then + print_info "Rollback cancelled" + return 0 + fi + fi + + # Perform rollback + _perform_rollback "$manifest" +} + +# Internal function to perform the actual rollback +_perform_rollback() { + local manifest="$1" + local original + local backup + local type + local success=true + + # Process each backup entry + # Using grep and process substitution for portability + while IFS= read -r entry; do + # Skip if empty + [[ -z "$entry" ]] && continue + + # Extract fields (simple parsing) + original=$(echo "$entry" | grep -o '"original": "[^"]*"' | cut -d'"' -f4) + backup=$(echo "$entry" | grep -o '"backup": "[^"]*"' | cut -d'"' -f4) + type=$(echo "$entry" | grep -o '"type": "[^"]*"' | cut -d'"' -f4) + + [[ -z "$original" ]] && continue + + print_info "Restoring: $original" + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "restore $type: $backup -> $original" + continue + fi + + # Remove current version if it exists + if [[ -e "$original" ]] || [[ -L "$original" ]]; then + rm -rf "$original" + fi + + # Restore based on type + case "$type" in + symlink) + if [[ -f "$backup" ]]; then + local target + target=$(cat "$backup") + if ln -s "$target" "$original"; then + print_success "Restored symlink: $original -> $target" + else + print_error "Failed to restore symlink: $original" + success=false + fi + fi + ;; + directory) + if [[ -d "$backup" ]]; then + if cp -r "$backup" "$original"; then + print_success "Restored directory: $original" + else + print_error "Failed to restore directory: $original" + success=false + fi + fi + ;; + file|*) + if [[ -f "$backup" ]]; then + # Ensure parent directory exists + mkdir -p "$(dirname "$original")" + if cp "$backup" "$original"; then + print_success "Restored file: $original" + else + print_error "Failed to restore file: $original" + success=false + fi + fi + ;; + esac + done < <(grep -A3 '"original":' "$manifest" | paste - - - - | sed 's/--/\n/g') + + if [[ "$success" == "true" ]]; then + print_success "Rollback completed successfully!" + return 0 + else + print_warning "Rollback completed with some errors" + return 1 + fi +} + +# ============================================================================== +# Cleanup Operations +# ============================================================================== + +# Remove old backup sessions (keep last N) +# Usage: cleanup_old_backups [keep_count] +cleanup_old_backups() { + local keep_count="${1:-5}" + local sessions + local count + local to_remove + + if [[ ! -d "$BACKUP_BASE_DIR" ]]; then + return 0 + fi + + # Get list of sessions sorted by date (newest first) + mapfile -t sessions < <(find "$BACKUP_BASE_DIR" -mindepth 1 -maxdepth 1 -type d -name "*_*" | sort -r) + count=${#sessions[@]} + + if [[ $count -le $keep_count ]]; then + print_debug "No old backups to clean up ($count sessions, keeping $keep_count)" + return 0 + fi + + to_remove=$((count - keep_count)) + print_info "Cleaning up $to_remove old backup session(s)..." + + for ((i = keep_count; i < count; i++)); do + local session="${sessions[$i]}" + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "remove old backup: $(basename "$session")" + else + rm -rf "$session" + print_debug "Removed: $(basename "$session")" + fi + done + + print_success "Backup cleanup complete" +} + +# Remove a specific backup session +# Usage: remove_backup_session +remove_backup_session() { + local session_id="$1" + local session_dir="${BACKUP_BASE_DIR}/${session_id}" + + if [[ ! -d "$session_dir" ]]; then + print_error "Session not found: $session_id" + return 1 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "remove backup session: $session_id" + return 0 + fi + + if rm -rf "$session_dir"; then + print_success "Removed backup session: $session_id" + return 0 + else + print_error "Failed to remove backup session: $session_id" + return 1 + fi +} + +# ============================================================================== +# Information Functions +# ============================================================================== + +# Get backup session info +# Usage: get_session_info [session_id] +get_session_info() { + local session_id="${1:-}" + local session_dir + local manifest + + if [[ -z "$session_id" ]]; then + session_id=$(get_latest_backup_session) || return 1 + fi + + session_dir="${BACKUP_BASE_DIR}/${session_id}" + manifest="${session_dir}/manifest.json" + + if [[ ! -f "$manifest" ]]; then + print_error "Manifest not found for session: $session_id" + return 1 + fi + + echo "Session: $session_id" + echo "Directory: $session_dir" + echo "" + + # Show basic info from manifest + grep -E '"(created_at|dotfiles_dir|os|arch)"' "$manifest" | \ + sed 's/[",]//g' | \ + sed 's/^[ ]*/ /' + + echo "" + echo "Backups:" + grep '"original":' "$manifest" | \ + sed 's/.*"original": "\([^"]*\)".*/ - \1/' +} diff --git a/lib/network.sh b/lib/network.sh new file mode 100755 index 0000000..416ab7f --- /dev/null +++ b/lib/network.sh @@ -0,0 +1,386 @@ +#!/usr/bin/env bash +# lib/network.sh - Network operations library for dotfiles installation +# +# This library provides network-related functions with retry logic, +# checksum validation, and GitHub API integration. +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" # Required dependency +# source "$DOTFILES_DIR/lib/network.sh" + +# Prevent multiple sourcing +[[ -n "${_NETWORK_SH_LOADED:-}" ]] && return 0 +readonly _NETWORK_SH_LOADED=1 + +# Ensure utils.sh is loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before lib/network.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Configuration +# ============================================================================== + +# Default retry settings +readonly NETWORK_MAX_RETRIES="${NETWORK_MAX_RETRIES:-3}" +readonly NETWORK_INITIAL_DELAY="${NETWORK_INITIAL_DELAY:-2}" # seconds +readonly NETWORK_CONNECT_TIMEOUT="${NETWORK_CONNECT_TIMEOUT:-30}" # seconds +readonly NETWORK_MAX_TIMEOUT="${NETWORK_MAX_TIMEOUT:-300}" # 5 minutes + +# ============================================================================== +# Download Functions +# ============================================================================== + +# Download a file with retry logic and exponential backoff +# Usage: download_with_retry [description] +# Returns: 0 on success, 1 on failure +download_with_retry() { + local url="$1" + local output="$2" + local description="${3:-file}" + local attempt=1 + local delay="$NETWORK_INITIAL_DELAY" + local success=false + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "download $description from: $url" + return 0 + fi + + # Create output directory if needed + safe_mkdir "$(dirname "$output")" + + while [[ $attempt -le $NETWORK_MAX_RETRIES ]]; do + print_debug "Download attempt $attempt/$NETWORK_MAX_RETRIES: $description" + + # Try wget first (better progress display and resumption) + if check_command wget; then + if wget -q --show-progress \ + --connect-timeout="$NETWORK_CONNECT_TIMEOUT" \ + --timeout="$NETWORK_MAX_TIMEOUT" \ + -O "$output" \ + "$url" 2>/dev/null; then + success=true + break + fi + fi + + # Fall back to curl + if [[ "$success" == "false" ]] && check_command curl; then + if curl -fsSL \ + --connect-timeout "$NETWORK_CONNECT_TIMEOUT" \ + --max-time "$NETWORK_MAX_TIMEOUT" \ + -o "$output" \ + "$url" 2>/dev/null; then + success=true + break + fi + fi + + # Check if we have any download tool + if ! check_command wget && ! check_command curl; then + print_error "Neither wget nor curl is available. Cannot download files." + return 1 + fi + + # Failed, prepare for retry + if [[ $attempt -lt $NETWORK_MAX_RETRIES ]]; then + print_warning "Download failed (attempt $attempt/$NETWORK_MAX_RETRIES). Retrying in ${delay}s..." + sleep "$delay" + delay=$((delay * 2)) # Exponential backoff + fi + + ((attempt++)) + done + + if [[ "$success" == "true" ]]; then + # Verify file was actually downloaded + if [[ -s "$output" ]]; then + print_debug "Successfully downloaded: $description" + return 0 + else + print_error "Downloaded file is empty: $output" + rm -f "$output" + return 1 + fi + else + print_error "Failed to download $description after $NETWORK_MAX_RETRIES attempts" + rm -f "$output" + return 1 + fi +} + +# Download to a temporary file +# Usage: download_to_temp [prefix] +# Outputs: path to temporary file +# Returns: 0 on success, 1 on failure +download_to_temp() { + local url="$1" + local prefix="${2:-download}" + local temp_file + + temp_file=$(mktemp "/tmp/${prefix}.XXXXXX") + + if download_with_retry "$url" "$temp_file" "$prefix"; then + echo "$temp_file" + return 0 + else + rm -f "$temp_file" + return 1 + fi +} + +# ============================================================================== +# Checksum Validation +# ============================================================================== + +# Calculate SHA256 checksum of a file (cross-platform) +# Usage: calculate_sha256 +# Outputs: checksum string +calculate_sha256() { + local file="$1" + + if [[ ! -f "$file" ]]; then + print_error "File not found for checksum: $file" + return 1 + fi + + if check_command sha256sum; then + sha256sum "$file" | cut -d' ' -f1 + elif check_command shasum; then + shasum -a 256 "$file" | cut -d' ' -f1 + else + print_error "No SHA256 tool available (need sha256sum or shasum)" + return 1 + fi +} + +# Validate a file's SHA256 checksum +# Usage: validate_checksum +# Returns: 0 if valid, 1 if invalid +validate_checksum() { + local file="$1" + local expected="$2" + local actual + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "validate checksum of: $file" + return 0 + fi + + actual=$(calculate_sha256 "$file") || return 1 + + # Normalize checksums to lowercase for comparison + expected="${expected,,}" + actual="${actual,,}" + + if [[ "$actual" == "$expected" ]]; then + print_debug "Checksum valid for: $file" + return 0 + else + print_error "Checksum mismatch for: $file" + print_error " Expected: $expected" + print_error " Actual: $actual" + return 1 + fi +} + +# Download and validate a file with checksum +# Usage: download_and_verify [description] +# Returns: 0 on success, 1 on failure +download_and_verify() { + local url="$1" + local output="$2" + local checksum="$3" + local description="${4:-file}" + + if ! download_with_retry "$url" "$output" "$description"; then + return 1 + fi + + if [[ "$DRY_RUN" != "true" ]]; then + if ! validate_checksum "$output" "$checksum"; then + print_error "Removing corrupted download: $output" + rm -f "$output" + return 1 + fi + fi + + return 0 +} + +# ============================================================================== +# GitHub API Functions +# ============================================================================== + +# Get the latest release version from a GitHub repository +# Usage: get_latest_github_release +# Outputs: version tag (e.g., "v1.2.3" or "1.2.3") +get_latest_github_release() { + local repo="$1" + local api_url="https://api.github.com/repos/${repo}/releases/latest" + local version + + print_debug "Fetching latest release for: $repo" + + if check_command curl; then + version=$(curl -fsSL "$api_url" 2>/dev/null | grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4) + elif check_command wget; then + version=$(wget -qO- "$api_url" 2>/dev/null | grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4) + else + print_error "Neither curl nor wget available" + return 1 + fi + + if [[ -z "$version" ]]; then + print_error "Failed to fetch latest release for: $repo" + return 1 + fi + + echo "$version" +} + +# Get SHA256 checksum from a GitHub release +# Usage: get_github_release_sha256 +# Outputs: checksum string for the specified asset +# Example: get_github_release_sha256 "neovim/neovim" "stable" "nvim-linux64.tar.gz" "*.sha256sum" +get_github_release_sha256() { + local repo="$1" + local version="$2" + local asset_name="$3" + local checksum_pattern="${4:-SHA256SUMS}" + local checksum_url + local checksum_content + local checksum + + # Build the checksum file URL + # Common patterns: SHA256SUMS, *.sha256sum, *_checksums.txt + case "$checksum_pattern" in + *.sha256sum) + checksum_url="https://github.com/${repo}/releases/download/${version}/${asset_name}.sha256sum" + ;; + *checksums*) + # GitHub CLI style: gh_X.X.X_checksums.txt + local version_clean="${version#v}" + checksum_url="https://github.com/${repo}/releases/download/${version}/gh_${version_clean}_checksums.txt" + ;; + *) + checksum_url="https://github.com/${repo}/releases/download/${version}/${checksum_pattern}" + ;; + esac + + print_debug "Fetching checksum from: $checksum_url" + + # Download checksum file + if check_command curl; then + checksum_content=$(curl -fsSL "$checksum_url" 2>/dev/null) + elif check_command wget; then + checksum_content=$(wget -qO- "$checksum_url" 2>/dev/null) + else + print_warning "Cannot fetch checksum: no download tool available" + return 1 + fi + + if [[ -z "$checksum_content" ]]; then + print_warning "Could not fetch checksum file for: $asset_name" + return 1 + fi + + # Extract checksum for the specific asset + # Checksums are usually in format: or + checksum=$(echo "$checksum_content" | grep -E "(^[a-f0-9]{64})\s+.*${asset_name}$" | head -1 | awk '{print $1}') + + if [[ -z "$checksum" ]]; then + # Try alternate format where filename comes first + checksum=$(echo "$checksum_content" | grep "${asset_name}" | head -1 | awk '{print $1}') + fi + + if [[ -z "$checksum" ]] || [[ ${#checksum} -ne 64 ]]; then + print_warning "Could not extract valid checksum for: $asset_name" + return 1 + fi + + echo "$checksum" +} + +# Download a GitHub release asset +# Usage: download_github_release [verify_checksum] +# Returns: 0 on success, 1 on failure +download_github_release() { + local repo="$1" + local version="$2" + local asset_name="$3" + local output="$4" + local verify="${5:-true}" + local download_url + local checksum + + download_url="https://github.com/${repo}/releases/download/${version}/${asset_name}" + + print_info "Downloading ${asset_name} from ${repo}..." + + # Try to get checksum if verification is enabled + if [[ "$verify" == "true" ]]; then + checksum=$(get_github_release_sha256 "$repo" "$version" "$asset_name" "*.sha256sum" 2>/dev/null) || true + + if [[ -n "$checksum" ]]; then + print_debug "Found checksum for verification: ${checksum:0:16}..." + if download_and_verify "$download_url" "$output" "$checksum" "$asset_name"; then + return 0 + fi + else + print_debug "No checksum available, downloading without verification" + fi + fi + + # Download without verification (or if verification failed) + download_with_retry "$download_url" "$output" "$asset_name" +} + +# ============================================================================== +# Network Connectivity +# ============================================================================== + +# Check if we have internet connectivity +# Usage: check_internet +# Returns: 0 if connected, 1 if not +check_internet() { + local test_hosts=("github.com" "google.com" "1.1.1.1") + + for host in "${test_hosts[@]}"; do + if check_command ping; then + if ping -c 1 -W 2 "$host" &>/dev/null; then + return 0 + fi + elif check_command curl; then + if curl -fsS --connect-timeout 2 "https://$host" &>/dev/null; then + return 0 + fi + fi + done + + return 1 +} + +# Wait for internet connectivity +# Usage: wait_for_internet [timeout_seconds] +# Returns: 0 if connected within timeout, 1 if timeout +wait_for_internet() { + local timeout="${1:-30}" + local elapsed=0 + + print_info "Waiting for internet connectivity..." + + while [[ $elapsed -lt $timeout ]]; do + if check_internet; then + print_success "Internet connection established" + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + + print_error "No internet connection after ${timeout}s" + return 1 +} diff --git a/lib/utils.sh b/lib/utils.sh new file mode 100755 index 0000000..c06dc59 --- /dev/null +++ b/lib/utils.sh @@ -0,0 +1,435 @@ +#!/usr/bin/env bash +# lib/utils.sh - Shared utility functions for dotfiles installation +# +# This library provides common functions used across all installation scripts. +# Source this file at the beginning of any script that needs these utilities. +# +# Usage: +# source "$(dirname "${BASH_SOURCE[0]}")/../lib/utils.sh" +# # or +# source "$DOTFILES_DIR/lib/utils.sh" + +# Prevent multiple sourcing +[[ -n "${_UTILS_SH_LOADED:-}" ]] && return 0 +readonly _UTILS_SH_LOADED=1 + +# ============================================================================== +# Colors +# ============================================================================== + +# Only use colors if stdout is a terminal +if [[ -t 1 ]]; then + readonly RED='\033[0;31m' + readonly GREEN='\033[0;32m' + readonly YELLOW='\033[0;33m' + readonly BLUE='\033[0;34m' + readonly MAGENTA='\033[0;35m' + readonly CYAN='\033[0;36m' + readonly BOLD='\033[1m' + readonly NC='\033[0m' # No Color +else + readonly RED='' + readonly GREEN='' + readonly YELLOW='' + readonly BLUE='' + readonly MAGENTA='' + readonly CYAN='' + readonly BOLD='' + readonly NC='' +fi + +# ============================================================================== +# Global Variables +# ============================================================================== + +# Dry run mode - when true, operations are logged but not executed +DRY_RUN="${DRY_RUN:-false}" + +# Verbose mode - when true, more detailed output is shown +VERBOSE="${VERBOSE:-false}" + +# ============================================================================== +# Printing Functions +# ============================================================================== + +# Print an informational message +print_info() { + echo -e "${BLUE}INFO:${NC} $1" +} + +# Print a success message +print_success() { + echo -e "${GREEN}SUCCESS:${NC} $1" +} + +# Print a warning message +print_warning() { + echo -e "${YELLOW}WARNING:${NC} $1" +} + +# Print an error message +print_error() { + echo -e "${RED}ERROR:${NC} $1" +} + +# Print a debug message (only shown in verbose mode) +print_debug() { + if [[ "$VERBOSE" == "true" ]]; then + echo -e "${CYAN}DEBUG:${NC} $1" + fi +} + +# Print a dry-run message +print_dry_run() { + echo -e "${MAGENTA}[DRY-RUN]${NC} Would $1" +} + +# Print a section header +print_section() { + echo "" + echo -e "${BOLD}${BLUE}=== $1 ===${NC}" + echo "" +} + +# ============================================================================== +# Command Checking +# ============================================================================== + +# Check if a command exists +# Usage: check_command +# Returns: 0 if command exists, 1 if not, 2 if no argument provided +check_command() { + if [[ -z "${1:-}" ]]; then + print_error "No command specified for check_command" + return 2 + fi + + if command -v "$1" &>/dev/null; then + return 0 + else + return 1 + fi +} + +# Require a command to exist, exit if it doesn't +# Usage: require_command [error_message] +require_command() { + local cmd="$1" + local msg="${2:-$cmd is required but not installed}" + + if ! check_command "$cmd"; then + print_error "$msg" + exit 1 + fi +} + +# ============================================================================== +# OS Detection +# ============================================================================== + +# Detect operating system +# Sets global OS variable to: macOS, Linux, or Unknown +detect_os() { + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + OS="Linux" + elif [[ "$OSTYPE" == "darwin"* ]]; then + OS="macOS" + else + OS="Unknown" + print_warning "Unsupported OS detected: $OSTYPE. Some features may not work properly." + fi + export OS +} + +# Detect Linux distribution +# Sets global DISTRO variable +detect_distro() { + if [[ "$OS" != "Linux" ]]; then + DISTRO="N/A" + export DISTRO + return + fi + + if [[ -f /etc/os-release ]]; then + # shellcheck source=/dev/null + . /etc/os-release + DISTRO="${ID:-unknown}" + elif check_command lsb_release; then + DISTRO=$(lsb_release -si | tr '[:upper:]' '[:lower:]') + else + DISTRO="unknown" + fi + export DISTRO +} + +# Detect architecture +# Sets global ARCH variable to normalized architecture name +detect_arch() { + local raw_arch + raw_arch=$(uname -m) + + case "$raw_arch" in + x86_64|amd64) + ARCH="x86_64" + ;; + aarch64|arm64) + ARCH="arm64" + ;; + armv7l|armhf) + ARCH="arm32" + ;; + *) + ARCH="$raw_arch" + ;; + esac + export ARCH +} + +# ============================================================================== +# Directory Functions +# ============================================================================== + +# Get the dotfiles directory (where this repo is located) +# This resolves symlinks to find the actual directory +get_dotfiles_dir() { + local script_path="${BASH_SOURCE[1]:-$0}" + local dir + + # Resolve symlinks + while [[ -L "$script_path" ]]; do + dir="$(cd -P "$(dirname "$script_path")" && pwd)" + script_path="$(readlink "$script_path")" + [[ "$script_path" != /* ]] && script_path="$dir/$script_path" + done + + dir="$(cd -P "$(dirname "$script_path")" && pwd)" + + # If we're in lib/ or modules/, go up one level + if [[ "$(basename "$dir")" == "lib" ]] || [[ "$(basename "$dir")" == "modules" ]]; then + dir="$(dirname "$dir")" + fi + + echo "$dir" +} + +# Create directory safely with dry-run support +# Usage: safe_mkdir +safe_mkdir() { + local dir="$1" + + if [[ -d "$dir" ]]; then + print_debug "Directory already exists: $dir" + return 0 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "create directory: $dir" + return 0 + fi + + if mkdir -p "$dir"; then + print_debug "Created directory: $dir" + return 0 + else + print_error "Failed to create directory: $dir" + return 1 + fi +} + +# ============================================================================== +# File Operations with Dry-Run Support +# ============================================================================== + +# Create a symlink safely with dry-run support +# Usage: safe_symlink +safe_symlink() { + local source="$1" + local target="$2" + + # Verify source exists + if [[ ! -e "$source" ]]; then + print_error "Source does not exist: $source" + return 1 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "symlink: $target -> $source" + return 0 + fi + + # Remove existing target if it's a symlink or file + if [[ -L "$target" ]] || [[ -f "$target" ]]; then + rm -f "$target" + fi + + # Create parent directory if needed + safe_mkdir "$(dirname "$target")" + + if ln -sf "$source" "$target"; then + print_debug "Created symlink: $target -> $source" + return 0 + else + print_error "Failed to create symlink: $target -> $source" + return 1 + fi +} + +# Copy a file safely with dry-run support +# Usage: safe_copy +safe_copy() { + local source="$1" + local target="$2" + + # Verify source exists + if [[ ! -e "$source" ]]; then + print_error "Source does not exist: $source" + return 1 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "copy: $source -> $target" + return 0 + fi + + # Create parent directory if needed + safe_mkdir "$(dirname "$target")" + + if cp -f "$source" "$target"; then + print_debug "Copied: $source -> $target" + return 0 + else + print_error "Failed to copy: $source -> $target" + return 1 + fi +} + +# Remove a file or directory safely with dry-run support +# Usage: safe_remove +safe_remove() { + local path="$1" + + if [[ ! -e "$path" ]]; then + print_debug "Path does not exist (nothing to remove): $path" + return 0 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "remove: $path" + return 0 + fi + + if rm -rf "$path"; then + print_debug "Removed: $path" + return 0 + else + print_error "Failed to remove: $path" + return 1 + fi +} + +# ============================================================================== +# Backup Functions (simple version - see lib/backup.sh for registry support) +# ============================================================================== + +# Backup a file or directory if it exists +# Usage: backup_if_exists +# Returns: 0 if backup created, 1 if path didn't exist, 2 on error +backup_if_exists() { + local path="$1" + local backup_path="${path}.backup.$(date +%Y%m%d_%H%M%S)" + + if [[ ! -e "$path" ]]; then + print_debug "Nothing to backup (doesn't exist): $path" + return 1 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "backup: $path -> $backup_path" + return 0 + fi + + print_info "Backing up: $path -> $backup_path" + + if mv "$path" "$backup_path"; then + print_success "Backup created: $backup_path" + return 0 + else + print_error "Failed to create backup of $path. Check permissions." + return 2 + fi +} + +# ============================================================================== +# Version Comparison +# ============================================================================== + +# Compare two version strings +# Usage: version_compare +# Returns: 0 if v1 >= v2, 1 if v1 < v2 +version_compare() { + local v1="$1" + local v2="$2" + + # Remove leading 'v' if present + v1="${v1#v}" + v2="${v2#v}" + + if [[ "$v1" == "$v2" ]]; then + return 0 + fi + + # Sort versions and check if v1 comes after v2 + local sorted + sorted=$(printf '%s\n%s' "$v1" "$v2" | sort -V | head -n1) + + if [[ "$sorted" == "$v2" ]]; then + return 0 # v1 >= v2 + else + return 1 # v1 < v2 + fi +} + +# ============================================================================== +# User Interaction +# ============================================================================== + +# Ask user for confirmation +# Usage: confirm [default: y/n] +# Returns: 0 for yes, 1 for no +confirm() { + local prompt="$1" + local default="${2:-y}" + local reply + + if [[ "$default" == "y" ]]; then + prompt="$prompt [Y/n] " + else + prompt="$prompt [y/N] " + fi + + # In non-interactive mode, use default + if [[ ! -t 0 ]]; then + [[ "$default" == "y" ]] + return $? + fi + + read -r -p "$prompt" reply + reply="${reply:-$default}" + + case "$reply" in + [Yy]*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +# ============================================================================== +# Initialization +# ============================================================================== + +# Run detection functions on load +detect_os +detect_arch diff --git a/modules/dependencies.sh b/modules/dependencies.sh new file mode 100755 index 0000000..024ec41 --- /dev/null +++ b/modules/dependencies.sh @@ -0,0 +1,382 @@ +#!/usr/bin/env bash +# modules/dependencies.sh - Core dependency installation +# +# Handles installation of essential dependencies: +# - git (version control) +# - curl (downloading) +# - tig (text-mode git interface) +# - build tools (gcc, make for compiling) +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" +# source "$DOTFILES_DIR/modules/package-managers.sh" +# source "$DOTFILES_DIR/modules/dependencies.sh" + +# Prevent multiple sourcing +[[ -n "${_DEPENDENCIES_SH_LOADED:-}" ]] && return 0 +readonly _DEPENDENCIES_SH_LOADED=1 + +# Ensure required libraries are loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before modules/dependencies.sh" >&2 + exit 1 +fi + +if [[ -z "${_PACKAGE_MANAGERS_SH_LOADED:-}" ]]; then + echo "ERROR: modules/package-managers.sh must be sourced before modules/dependencies.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Individual Dependency Installation +# ============================================================================== + +# Install git +install_git() { + if check_command git; then + print_success "git is already installed" + return 0 + fi + + print_info "Installing git (required for dotfiles)..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install git" + return 0 + fi + + if install_package git; then + if check_command git; then + print_success "git installed successfully" + return 0 + fi + fi + + print_error "Failed to install git. This is required to continue." + return 1 +} + +# Install curl +install_curl() { + if check_command curl; then + print_success "curl is already installed" + return 0 + fi + + print_info "Installing curl (required for downloads)..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install curl" + return 0 + fi + + if install_package curl; then + if check_command curl; then + print_success "curl installed successfully" + return 0 + fi + fi + + print_error "Failed to install curl. Some features may not work properly." + return 1 +} + +# Install wget (useful backup for downloads) +install_wget() { + if check_command wget; then + print_success "wget is already installed" + return 0 + fi + + print_info "Installing wget..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install wget" + return 0 + fi + + install_package wget || print_warning "wget installation failed (non-critical)" + return 0 +} + +# Install tig (text-mode git interface) +install_tig() { + if check_command tig; then + print_success "tig is already installed" + return 0 + fi + + print_info "Installing tig (text-mode interface for Git)..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install tig" + return 0 + fi + + if install_package tig; then + if check_command tig; then + print_success "tig installed successfully" + return 0 + fi + fi + + print_warning "Failed to install tig (non-critical). You can install it manually later." + return 0 +} + +# Install unzip +install_unzip() { + if check_command unzip; then + print_success "unzip is already installed" + return 0 + fi + + print_info "Installing unzip..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install unzip" + return 0 + fi + + install_package unzip || print_warning "unzip installation failed" + return 0 +} + +# Install build tools (gcc, make, etc.) +install_build_tools() { + local need_build_tools=false + + if ! check_command make; then + need_build_tools=true + fi + + if ! check_command gcc && ! check_command clang; then + need_build_tools=true + fi + + if [[ "$need_build_tools" == "false" ]]; then + print_success "Build tools are already installed" + return 0 + fi + + print_info "Installing build tools (required for some Neovim plugins)..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install build tools" + return 0 + fi + + local result=0 + + case "$PACKAGE_MANAGER" in + apt) + sudo apt-get install -y build-essential || result=1 + ;; + dnf) + sudo dnf groupinstall -y "Development Tools" || result=1 + ;; + pacman) + sudo pacman -S --noconfirm base-devel || result=1 + ;; + apk) + sudo apk add build-base || result=1 + ;; + brew) + if [[ "$OS" == "macOS" ]]; then + # On macOS, trigger Xcode command line tools installation + xcode-select --install 2>/dev/null || true + # Wait a bit for the dialog to appear + sleep 2 + print_info "Xcode Command Line Tools installation may have been triggered." + print_info "Please complete the installation if prompted." + else + brew install gcc make || result=1 + fi + ;; + *) + print_warning "Could not install build tools - no supported package manager" + result=1 + ;; + esac + + if [[ $result -eq 0 ]] && check_command make; then + print_success "Build tools installed successfully" + return 0 + else + print_warning "Build tools installation may have failed. Some Neovim plugins might not work." + return 1 + fi +} + +# ============================================================================== +# Ripgrep (for faster searching) +# ============================================================================== + +install_ripgrep() { + if check_command rg; then + print_success "ripgrep is already installed" + return 0 + fi + + print_info "Installing ripgrep (fast search tool)..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install ripgrep" + return 0 + fi + + # Package name varies by distribution + case "$PACKAGE_MANAGER" in + apt) + install_package ripgrep + ;; + dnf) + install_package ripgrep + ;; + pacman) + install_package ripgrep + ;; + apk) + install_package ripgrep + ;; + brew) + install_package ripgrep + ;; + *) + print_warning "Cannot install ripgrep" + return 1 + ;; + esac + + if check_command rg; then + print_success "ripgrep installed successfully" + else + print_warning "ripgrep installation failed (optional)" + fi + + return 0 +} + +# ============================================================================== +# fd (fast file finder) +# ============================================================================== + +install_fd() { + if check_command fd || check_command fdfind; then + print_success "fd is already installed" + return 0 + fi + + print_info "Installing fd (fast file finder)..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install fd" + return 0 + fi + + # Package name varies by distribution + case "$PACKAGE_MANAGER" in + apt) + install_package fd-find + # Create alias if installed as fd-find + if check_command fdfind && ! check_command fd; then + print_info "Creating fd symlink for fd-find..." + mkdir -p "$HOME/.local/bin" + ln -sf "$(which fdfind)" "$HOME/.local/bin/fd" + fi + ;; + dnf) + install_package fd-find + ;; + pacman) + install_package fd + ;; + apk) + install_package fd + ;; + brew) + install_package fd + ;; + *) + print_warning "Cannot install fd" + return 1 + ;; + esac + + return 0 +} + +# ============================================================================== +# fzf (fuzzy finder) +# ============================================================================== + +install_fzf() { + if check_command fzf; then + print_success "fzf is already installed" + return 0 + fi + + print_info "Installing fzf (fuzzy finder)..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install fzf" + return 0 + fi + + install_package fzf || print_warning "fzf installation failed (optional)" + return 0 +} + +# ============================================================================== +# Main Installation Function +# ============================================================================== + +# Install all core dependencies +# Usage: install_dependencies [--minimal] +install_dependencies() { + local minimal="${1:-}" + + print_section "Installing Dependencies" + + # Essential dependencies (always install) + install_git || return 1 + install_curl || return 1 + + # Install unzip (needed for fonts and some downloads) + install_unzip + + # Optional but recommended dependencies + if [[ "$minimal" != "--minimal" ]]; then + install_wget + install_tig + install_build_tools + install_ripgrep + install_fd + install_fzf + fi + + print_success "Core dependencies installed" + return 0 +} + +# Check if all required dependencies are installed +# Usage: check_dependencies +# Returns: 0 if all required deps are present, 1 otherwise +check_required_dependencies() { + local missing=() + + if ! check_command git; then + missing+=("git") + fi + + if ! check_command curl && ! check_command wget; then + missing+=("curl or wget") + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + print_error "Missing required dependencies: ${missing[*]}" + return 1 + fi + + return 0 +} diff --git a/modules/link-configs.sh b/modules/link-configs.sh new file mode 100755 index 0000000..561efce --- /dev/null +++ b/modules/link-configs.sh @@ -0,0 +1,341 @@ +#!/usr/bin/env bash +# modules/link-configs.sh - Configuration file symlinking +# +# Handles creating symlinks for all configuration files: +# - Neovim configuration +# - Zsh configuration +# - Git configuration +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" +# source "$DOTFILES_DIR/lib/backup.sh" +# source "$DOTFILES_DIR/modules/link-configs.sh" + +# Prevent multiple sourcing +[[ -n "${_LINK_CONFIGS_SH_LOADED:-}" ]] && return 0 +readonly _LINK_CONFIGS_SH_LOADED=1 + +# Ensure required libraries are loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before modules/link-configs.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Neovim Configuration +# ============================================================================== + +# Select Neovim template based on system capabilities +# Usage: select_nvim_template +# Outputs: template filename or empty string +select_nvim_template() { + local dotfiles_dir="$1" + local template="" + + # Check for available templates + local has_default=false + local has_catppuccin=false + local has_monokai=false + local has_nolua=false + local has_nococ=false + + [[ -f "$dotfiles_dir/nvim/personal.vim.template" ]] && has_default=true + [[ -f "$dotfiles_dir/nvim/personal.catppuccin.vim" ]] && has_catppuccin=true + [[ -f "$dotfiles_dir/nvim/personal.monokai.vim" ]] && has_monokai=true + [[ -f "$dotfiles_dir/nvim/personal.nolua.vim" ]] && has_nolua=true + [[ -f "$dotfiles_dir/nvim/personal.nococ.vim" ]] && has_nococ=true + + # Check system capabilities + local has_lua=false + local has_node=false + + if check_command nvim; then + # Check for Lua support + if nvim --version | grep -q "LuaJIT"; then + has_lua=true + fi + fi + + if check_command node; then + has_node=true + fi + + # Check for no-coc marker + if [[ -f "$HOME/.config/nvim/.no-coc" ]]; then + has_node=false + fi + + # Select template based on capabilities + if [[ "$has_lua" == "false" ]] && [[ "$has_nolua" == "true" ]]; then + template="personal.nolua.vim" + print_info "Using no-Lua configuration (Neovim lacks Lua support)" + elif [[ "$has_node" == "false" ]] && [[ "$has_nococ" == "true" ]]; then + template="personal.nococ.vim" + print_info "Using no-CoC configuration (Node.js not available)" + elif [[ "$has_catppuccin" == "true" ]]; then + template="personal.catppuccin.vim" + print_info "Using Catppuccin theme configuration" + elif [[ "$has_default" == "true" ]]; then + template="personal.vim.template" + fi + + echo "$template" +} + +# Link Neovim configuration +# Usage: link_nvim_config [--backup] +link_nvim_config() { + local dotfiles_dir="$1" + local should_backup="${2:-}" + local nvim_config_dir="$HOME/.config/nvim" + local nvim_init="$nvim_config_dir/init.vim" + local personal_vim="$nvim_config_dir/personal.vim" + + print_info "Setting up Neovim configuration..." + + # Check if source exists + if [[ ! -f "$dotfiles_dir/nvim/init.vim" ]]; then + print_error "Neovim config file not found: $dotfiles_dir/nvim/init.vim" + return 1 + fi + + # Backup existing config if requested + if [[ "$should_backup" == "--backup" ]]; then + if [[ -d "$nvim_config_dir" ]]; then + backup_with_registry "$nvim_config_dir" || backup_if_exists "$nvim_config_dir" || true + fi + fi + + # Create config directory + safe_mkdir "$nvim_config_dir" + + # Create symlink for init.vim + if safe_symlink "$dotfiles_dir/nvim/init.vim" "$nvim_init"; then + print_success "Neovim config linked" + else + print_error "Failed to link Neovim config" + return 1 + fi + + # Setup personal.vim if it doesn't exist + if [[ ! -f "$personal_vim" ]]; then + local template + template=$(select_nvim_template "$dotfiles_dir") + + if [[ -n "$template" ]] && [[ -f "$dotfiles_dir/nvim/$template" ]]; then + if safe_copy "$dotfiles_dir/nvim/$template" "$personal_vim"; then + print_success "Created $personal_vim" + fi + fi + fi + + return 0 +} + +# ============================================================================== +# Zsh Configuration +# ============================================================================== + +# Link Zsh configuration +# Usage: link_zsh_config [--backup] +link_zsh_config() { + local dotfiles_dir="$1" + local should_backup="${2:-}" + local zshrc="$HOME/.zshrc" + local zshrc_local="$HOME/.zshrc.local" + + print_info "Setting up Zsh configuration..." + + # Check if source exists + if [[ ! -f "$dotfiles_dir/zsh/.zshrc" ]]; then + print_error "Zsh config file not found: $dotfiles_dir/zsh/.zshrc" + return 1 + fi + + # Backup existing config if requested + if [[ "$should_backup" == "--backup" ]]; then + backup_with_registry "$zshrc" || backup_if_exists "$zshrc" || true + fi + + # Create symlink for .zshrc + if safe_symlink "$dotfiles_dir/zsh/.zshrc" "$zshrc"; then + print_success "Zsh config linked" + else + print_error "Failed to link Zsh config" + return 1 + fi + + # Create .zshrc.local template if it doesn't exist + if [[ ! -f "$zshrc_local" ]] && [[ -f "$dotfiles_dir/zsh/.zshrc.local.template" ]]; then + if safe_copy "$dotfiles_dir/zsh/.zshrc.local.template" "$zshrc_local"; then + print_info "Created $zshrc_local template for custom configuration" + fi + fi + + return 0 +} + +# ============================================================================== +# Git Configuration +# ============================================================================== + +# Link Git configuration +# Usage: link_git_config +link_git_config() { + local dotfiles_dir="$1" + local gitconfig_local="$HOME/.gitconfig.local" + + # Create gitconfig.local from template if it doesn't exist + if [[ ! -f "$gitconfig_local" ]] && [[ -f "$dotfiles_dir/gitconfig.local.template" ]]; then + print_info "Creating git local configuration template..." + + if safe_copy "$dotfiles_dir/gitconfig.local.template" "$gitconfig_local"; then + print_success "Created $gitconfig_local template - edit this file to set your git identity" + fi + fi + + return 0 +} + +# ============================================================================== +# iTerm2 Shell Integration +# ============================================================================== + +# Link iTerm2 shell integration +# Usage: link_iterm_integration +link_iterm_integration() { + local dotfiles_dir="$1" + local iterm_integration="$HOME/.iterm2_shell_integration.zsh" + local source_file="$dotfiles_dir/iterm/.iterm2_shell_integration.zsh" + + if [[ ! -f "$source_file" ]]; then + return 0 + fi + + if [[ -f "$iterm_integration" ]]; then + print_debug "iTerm2 shell integration already exists" + return 0 + fi + + print_info "Linking iTerm2 shell integration..." + + safe_symlink "$source_file" "$iterm_integration" +} + +# ============================================================================== +# Alacritty Configuration +# ============================================================================== + +# Link Alacritty configuration +# Usage: link_alacritty_config [--backup] +link_alacritty_config() { + local dotfiles_dir="$1" + local should_backup="${2:-}" + local alacritty_dir="$HOME/.config/alacritty" + local alacritty_config="$alacritty_dir/alacritty.yml" + local source_file="$dotfiles_dir/terminal/alacritty.yml" + + if [[ ! -f "$source_file" ]]; then + return 0 + fi + + if ! check_command alacritty; then + return 0 + fi + + print_info "Setting up Alacritty configuration..." + + # Backup if requested + if [[ "$should_backup" == "--backup" ]] && [[ -f "$alacritty_config" ]]; then + backup_with_registry "$alacritty_config" || backup_if_exists "$alacritty_config" || true + fi + + safe_mkdir "$alacritty_dir" + safe_copy "$source_file" "$alacritty_config" + + print_success "Alacritty configuration installed" +} + +# ============================================================================== +# Main Linking Function +# ============================================================================== + +# Link all configuration files +# Usage: link_configs [--backup] [--update] +link_configs() { + local dotfiles_dir="$1" + shift + + local backup_flag="" + local update_mode="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --backup) backup_flag="--backup" ;; + --update) update_mode="true" ;; + *) ;; + esac + shift + done + + print_section "Linking Configuration Files" + + # Determine backup behavior + if [[ "$update_mode" == "true" ]]; then + print_info "Update mode: Will overwrite existing configurations" + backup_flag="" + elif [[ -n "$backup_flag" ]]; then + print_info "Will backup any existing configurations" + fi + + # Link all configurations + link_nvim_config "$dotfiles_dir" "$backup_flag" + link_zsh_config "$dotfiles_dir" "$backup_flag" + link_git_config "$dotfiles_dir" + link_iterm_integration "$dotfiles_dir" + link_alacritty_config "$dotfiles_dir" "$backup_flag" + + print_success "Configuration files linked" +} + +# ============================================================================== +# Verification +# ============================================================================== + +# Verify all configuration links are in place +# Usage: verify_configs +verify_configs() { + local all_good=true + + print_info "Verifying configuration links..." + + # Check Neovim config + if [[ -f "$HOME/.config/nvim/init.vim" ]]; then + print_success "Neovim configuration is in place" + else + print_warning "Neovim configuration is missing" + all_good=false + fi + + # Check Zsh config + if [[ -f "$HOME/.zshrc" ]]; then + print_success "Zsh configuration is in place" + else + print_warning "Zsh configuration is missing" + all_good=false + fi + + # Check Git config + if [[ -f "$HOME/.gitconfig.local" ]]; then + print_success "Git local configuration is in place" + else + print_warning "Git local configuration is missing" + all_good=false + fi + + if [[ "$all_good" == "true" ]]; then + return 0 + else + return 1 + fi +} diff --git a/modules/neovim.sh b/modules/neovim.sh new file mode 100755 index 0000000..87875d5 --- /dev/null +++ b/modules/neovim.sh @@ -0,0 +1,527 @@ +#!/usr/bin/env bash +# modules/neovim.sh - Neovim installation and configuration +# +# Handles Neovim installation: +# - Binary download for Linux (with checksum validation) +# - Homebrew for macOS +# - Package manager fallback +# - vim-plug plugin manager +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" +# source "$DOTFILES_DIR/lib/network.sh" +# source "$DOTFILES_DIR/modules/package-managers.sh" +# source "$DOTFILES_DIR/modules/neovim.sh" + +# Prevent multiple sourcing +[[ -n "${_NEOVIM_SH_LOADED:-}" ]] && return 0 +readonly _NEOVIM_SH_LOADED=1 + +# Ensure required libraries are loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before modules/neovim.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Configuration +# ============================================================================== + +# Minimum required Neovim version +readonly NVIM_MIN_VERSION="${NVIM_MIN_VERSION:-0.9.0}" + +# Installation directories +readonly NVIM_USER_BIN="$HOME/.local/bin" +readonly NVIM_USER_SHARE="$HOME/.local/share/nvim" + +# ============================================================================== +# Version Checking +# ============================================================================== + +# Get current Neovim version +get_nvim_version() { + if check_command nvim; then + nvim --version 2>/dev/null | head -n1 | cut -d' ' -f2 | sed 's/^v//' + else + echo "" + fi +} + +# Check if Neovim version meets minimum requirement +# Usage: check_nvim_version [minimum_version] +check_nvim_version() { + local min_version="${1:-$NVIM_MIN_VERSION}" + local current_version + + current_version=$(get_nvim_version) + + if [[ -z "$current_version" ]]; then + print_debug "Neovim not found" + return 1 + fi + + if version_compare "$current_version" "$min_version"; then + print_debug "Neovim $current_version meets minimum $min_version" + return 0 + else + print_debug "Neovim $current_version is below minimum $min_version" + return 1 + fi +} + +# Check if Neovim is functional +is_nvim_functional() { + if ! check_command nvim; then + return 1 + fi + + # Try to run version check + nvim --version &>/dev/null +} + +# Check if Neovim has Lua support +has_nvim_lua_support() { + if ! check_command nvim; then + return 1 + fi + + # Check for LuaJIT in version output + if nvim --version | grep -q "LuaJIT"; then + return 0 + fi + + # Try executing Lua code + local test_file="/tmp/nvim_lua_test_$$.lua" + echo "print('ok')" > "$test_file" + + if nvim --headless -c "lua dofile('$test_file')" -c q 2>&1 | grep -q "ok"; then + rm -f "$test_file" + return 0 + fi + + rm -f "$test_file" + return 1 +} + +# ============================================================================== +# Architecture Detection +# ============================================================================== + +# Get Neovim archive name for current architecture +get_nvim_archive_name() { + case "$ARCH" in + x86_64) + echo "nvim-linux-x86_64" + ;; + arm64) + echo "nvim-linux-arm64" + ;; + arm32) + print_warning "32-bit ARM is not supported by Neovim prebuilt binaries" + echo "" + ;; + *) + echo "" + ;; + esac +} + +# ============================================================================== +# Binary Installation (Linux) +# ============================================================================== + +# Download and install Neovim binary +# Usage: install_nvim_binary [version] +install_nvim_binary() { + local version="${1:-stable}" + local archive_name + local download_url + local temp_dir + local checksum + + archive_name=$(get_nvim_archive_name) + + if [[ -z "$archive_name" ]]; then + print_warning "No prebuilt binary available for architecture: $ARCH" + return 1 + fi + + print_info "Installing Neovim $version binary for $ARCH..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "download and install Neovim $version" + return 0 + fi + + # Create directories + mkdir -p "$NVIM_USER_BIN" "$NVIM_USER_SHARE" + + # Create temp directory + temp_dir=$(mktemp -d) + trap 'rm -rf "$temp_dir"' RETURN + + download_url="https://github.com/neovim/neovim/releases/download/${version}/${archive_name}.tar.gz" + + # Try to get checksum + checksum=$(get_github_release_sha256 "neovim/neovim" "$version" "${archive_name}.tar.gz" "*.sha256sum" 2>/dev/null) || true + + # Download + local archive_path="$temp_dir/${archive_name}.tar.gz" + + if [[ -n "$checksum" ]]; then + print_info "Downloading with checksum verification..." + if ! download_and_verify "$download_url" "$archive_path" "$checksum" "Neovim"; then + print_warning "Verified download failed, trying without verification..." + if ! download_with_retry "$download_url" "$archive_path" "Neovim"; then + return 1 + fi + fi + else + print_info "Downloading Neovim binary..." + if ! download_with_retry "$download_url" "$archive_path" "Neovim"; then + return 1 + fi + fi + + # Extract + print_info "Extracting Neovim..." + if ! tar -xzf "$archive_path" -C "$temp_dir"; then + print_error "Failed to extract Neovim archive" + return 1 + fi + + # Find extracted directory (naming may vary) + local extract_dir + extract_dir=$(find "$temp_dir" -maxdepth 1 -type d -name "nvim-*" | head -1) + + if [[ -z "$extract_dir" ]] || [[ ! -d "$extract_dir" ]]; then + print_error "Failed to find extracted Neovim directory" + return 1 + fi + + # Install + print_info "Installing Neovim to $NVIM_USER_BIN..." + cp -f "$extract_dir/bin/nvim" "$NVIM_USER_BIN/" + chmod +x "$NVIM_USER_BIN/nvim" + + # Copy runtime files + if [[ -d "$extract_dir/share/nvim" ]]; then + cp -rf "$extract_dir/share/nvim/"* "$NVIM_USER_SHARE/" + fi + + # Ensure PATH includes user bin + add_to_path "$NVIM_USER_BIN" + + # Verify + if "$NVIM_USER_BIN/nvim" --version &>/dev/null; then + local installed_version + installed_version=$("$NVIM_USER_BIN/nvim" --version | head -n1) + print_success "Neovim installed: $installed_version" + return 0 + else + print_error "Neovim installation verification failed" + return 1 + fi +} + +# Add directory to PATH in shell config +add_to_path() { + local dir="$1" + local path_export="export PATH=\"$dir:\$PATH\"" + + # Check if already in PATH + if [[ ":$PATH:" == *":$dir:"* ]]; then + return 0 + fi + + # Add to current session + export PATH="$dir:$PATH" + + # Add to shell config + if [[ -f "$HOME/.zshrc.local" ]]; then + if ! grep -q "$dir" "$HOME/.zshrc.local" 2>/dev/null; then + echo "$path_export" >> "$HOME/.zshrc.local" + fi + elif [[ -f "$HOME/.zshrc" ]]; then + if ! grep -q "$dir" "$HOME/.zshrc" 2>/dev/null; then + echo "$path_export" >> "$HOME/.zshrc" + fi + elif [[ -f "$HOME/.bashrc" ]]; then + if ! grep -q "$dir" "$HOME/.bashrc" 2>/dev/null; then + echo "$path_export" >> "$HOME/.bashrc" + fi + fi +} + +# ============================================================================== +# Package Manager Installation +# ============================================================================== + +# Install Neovim via package manager +install_nvim_package_manager() { + print_info "Installing Neovim via package manager..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install Neovim via $PACKAGE_MANAGER" + return 0 + fi + + case "$PACKAGE_MANAGER" in + brew) + brew install neovim + ;; + apt) + sudo apt-get update -y + sudo apt-get install -y neovim + ;; + dnf) + sudo dnf install -y neovim + ;; + pacman) + sudo pacman -S --noconfirm neovim + ;; + apk) + sudo apk add neovim + ;; + *) + print_error "Unknown package manager: $PACKAGE_MANAGER" + return 1 + ;; + esac + + if check_command nvim; then + print_success "Neovim installed: $(nvim --version | head -n1)" + return 0 + fi + + return 1 +} + +# ============================================================================== +# vim-plug Installation +# ============================================================================== + +# Install vim-plug plugin manager +install_vim_plug() { + local plug_path="${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/autoload/plug.vim" + + if [[ -f "$plug_path" ]] || [[ -f "$HOME/.vim/autoload/plug.vim" ]]; then + print_success "vim-plug is already installed" + return 0 + fi + + print_info "Installing vim-plug for Neovim..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install vim-plug" + return 0 + fi + + if ! check_command curl; then + print_error "curl is required to install vim-plug" + return 1 + fi + + # Create directory + mkdir -p "$(dirname "$plug_path")" + + # Download vim-plug + if curl -fLo "$plug_path" --create-dirs \ + https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim; then + print_success "vim-plug installed successfully" + return 0 + else + print_error "Failed to download vim-plug" + return 1 + fi +} + +# ============================================================================== +# Plugin Installation +# ============================================================================== + +# Install Neovim plugins via vim-plug +# Usage: install_nvim_plugins [--update] +install_nvim_plugins() { + local update_mode="${1:-}" + + if ! is_nvim_functional; then + print_warning "Neovim not functional, skipping plugin installation" + return 1 + fi + + local plug_path="${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/autoload/plug.vim" + if [[ ! -f "$plug_path" ]] && [[ ! -f "$HOME/.vim/autoload/plug.vim" ]]; then + print_warning "vim-plug not found, skipping plugin installation" + return 1 + fi + + print_info "Installing Neovim plugins..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install/update Neovim plugins" + return 0 + fi + + if [[ "$update_mode" == "--update" ]]; then + nvim --headless +PlugUpdate +qall 2>/dev/null || true + print_success "Neovim plugins updated" + else + nvim --headless +PlugInstall +qall 2>/dev/null || true + print_success "Neovim plugins installed" + fi + + # Build telescope-fzf-native if present + build_telescope_fzf +} + +# Build telescope-fzf-native plugin +build_telescope_fzf() { + local fzf_dir="${XDG_DATA_HOME:-$HOME/.local/share}/nvim/plugged/telescope-fzf-native.nvim" + + if [[ ! -d "$fzf_dir" ]]; then + return 0 + fi + + if ! check_command make; then + print_warning "make not found, cannot build telescope-fzf-native" + return 1 + fi + + if ! check_command gcc && ! check_command clang; then + print_warning "No C compiler found, cannot build telescope-fzf-native" + return 1 + fi + + print_info "Building telescope-fzf-native..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "build telescope-fzf-native" + return 0 + fi + + (cd "$fzf_dir" && make) 2>/dev/null || true + print_success "telescope-fzf-native built" +} + +# ============================================================================== +# CoC (Conquer of Completion) Setup +# ============================================================================== + +# Install CoC extensions +# Usage: install_coc_extensions [--update] +install_coc_extensions() { + local update_mode="${1:-}" + + # Check if we should skip CoC + if [[ -f "$HOME/.config/nvim/.no-coc" ]]; then + print_info "Skipping CoC extensions (Node.js not available)" + return 0 + fi + + if ! check_command node; then + print_info "Skipping CoC extensions (Node.js not installed)" + return 0 + fi + + if ! is_nvim_functional; then + return 1 + fi + + print_info "Installing CoC extensions..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install CoC extensions" + return 0 + fi + + if [[ "$update_mode" == "--update" ]]; then + nvim --headless +"CocUpdate" +qall 2>/dev/null || true + print_success "CoC extensions updated" + else + nvim --headless +"CocInstall -sync coc-json coc-yaml coc-toml coc-tsserver coc-markdownlint" +qall 2>/dev/null || true + print_success "CoC extensions installed" + fi +} + +# ============================================================================== +# Main Installation Function +# ============================================================================== + +# Install Neovim with all components +# Usage: install_neovim [--force] [--skip-plugins] +install_neovim() { + local force="" + local skip_plugins="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --force) force="--force" ;; + --skip-plugins) skip_plugins="--skip-plugins" ;; + *) ;; + esac + shift + done + + print_section "Installing Neovim" + + # Check if already installed and meets version requirement + if [[ -z "$force" ]] && check_nvim_version; then + local version + version=$(get_nvim_version) + print_success "Neovim v$version is already installed and meets requirements" + else + # Install Neovim + if [[ "$OS" == "macOS" ]]; then + install_nvim_package_manager + elif [[ "$OS" == "Linux" ]]; then + # Try binary installation first, fall back to package manager + if ! install_nvim_binary; then + print_info "Binary installation failed, trying package manager..." + install_nvim_package_manager + fi + else + install_nvim_package_manager + fi + fi + + # Verify installation + if ! check_command nvim; then + print_error "Neovim installation failed" + return 1 + fi + + # Install vim-plug + install_vim_plug + + # Install plugins (unless skipped) + if [[ -z "$skip_plugins" ]]; then + install_nvim_plugins + install_coc_extensions + fi + + print_success "Neovim setup complete" +} + +# Upgrade Neovim to latest version +# Usage: upgrade_neovim +upgrade_neovim() { + print_section "Upgrading Neovim" + + local current_version + current_version=$(get_nvim_version) + + if [[ -n "$current_version" ]]; then + print_info "Current version: v$current_version" + fi + + if [[ "$OS" == "Linux" ]]; then + install_nvim_binary "stable" + else + install_nvim_package_manager + fi + + # Update plugins + install_nvim_plugins --update + install_coc_extensions --update +} diff --git a/modules/nodejs.sh b/modules/nodejs.sh new file mode 100755 index 0000000..a61092e --- /dev/null +++ b/modules/nodejs.sh @@ -0,0 +1,345 @@ +#!/usr/bin/env bash +# modules/nodejs.sh - Node.js installation and configuration +# +# Handles Node.js installation via: +# - System package manager +# - NVM (Node Version Manager) as fallback +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" +# source "$DOTFILES_DIR/lib/network.sh" +# source "$DOTFILES_DIR/modules/package-managers.sh" +# source "$DOTFILES_DIR/modules/nodejs.sh" + +# Prevent multiple sourcing +[[ -n "${_NODEJS_SH_LOADED:-}" ]] && return 0 +readonly _NODEJS_SH_LOADED=1 + +# Ensure required libraries are loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before modules/nodejs.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Configuration +# ============================================================================== + +# NVM directory +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + +# Track Node.js installation status +NODE_INSTALL_SUCCESS=false + +# ============================================================================== +# Version Checking +# ============================================================================== + +# Get current Node.js version +get_node_version() { + if check_command node; then + node --version 2>/dev/null | sed 's/^v//' + else + echo "" + fi +} + +# Check if Node.js version meets minimum requirement +# Usage: check_node_version [minimum_version] +check_node_version() { + local min_version="${1:-16.0.0}" + local current_version + + current_version=$(get_node_version) + + if [[ -z "$current_version" ]]; then + print_debug "Node.js not found" + return 1 + fi + + if version_compare "$current_version" "$min_version"; then + print_debug "Node.js $current_version meets minimum $min_version" + return 0 + else + print_debug "Node.js $current_version is below minimum $min_version" + return 1 + fi +} + +# ============================================================================== +# NVM Installation +# ============================================================================== + +# Install NVM (Node Version Manager) +install_nvm() { + if [[ -s "$NVM_DIR/nvm.sh" ]]; then + print_success "NVM is already installed" + return 0 + fi + + print_info "Installing NVM (Node Version Manager)..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install NVM" + return 0 + fi + + # Create NVM directory + mkdir -p "$NVM_DIR" + + # Download NVM install script to temp file for security + local temp_script + temp_script=$(mktemp) + + if ! curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh -o "$temp_script" 2>/dev/null; then + print_error "Failed to download NVM installer" + rm -f "$temp_script" + return 1 + fi + + # Run installer + if bash "$temp_script" >/dev/null 2>&1; then + rm -f "$temp_script" + print_success "NVM installed successfully" + + # Source NVM + # shellcheck source=/dev/null + [[ -s "$NVM_DIR/nvm.sh" ]] && \. "$NVM_DIR/nvm.sh" + + return 0 + else + rm -f "$temp_script" + print_error "NVM installation failed" + return 1 + fi +} + +# Setup NVM in current shell +setup_nvm() { + if [[ -s "$NVM_DIR/nvm.sh" ]]; then + # shellcheck source=/dev/null + \. "$NVM_DIR/nvm.sh" + print_debug "NVM loaded" + return 0 + fi + return 1 +} + +# Install Node.js via NVM +# Usage: install_node_via_nvm [version] +install_node_via_nvm() { + local version="${1:-lts/*}" + + # Ensure NVM is available + if ! setup_nvm; then + if ! install_nvm; then + return 1 + fi + setup_nvm + fi + + print_info "Installing Node.js via NVM..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "nvm install $version" + return 0 + fi + + # Check if nvm function is available + if ! command -v nvm &>/dev/null; then + print_error "NVM is not available in current shell" + return 1 + fi + + # Install Node.js + if nvm install "$version" >/dev/null 2>&1; then + nvm use "$version" >/dev/null 2>&1 + print_success "Node.js installed via NVM: $(node --version)" + NODE_INSTALL_SUCCESS=true + return 0 + else + print_error "Failed to install Node.js via NVM" + return 1 + fi +} + +# ============================================================================== +# Package Manager Installation +# ============================================================================== + +# Install Node.js via system package manager +install_node_via_package_manager() { + print_info "Installing Node.js via package manager..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install Node.js via $PACKAGE_MANAGER" + return 0 + fi + + local result=1 + + case "$PACKAGE_MANAGER" in + apt) + # Try system package first + if sudo apt-get install -y nodejs npm >/dev/null 2>&1; then + result=0 + else + # Try NodeSource repository + print_info "Trying NodeSource repository..." + local temp_script + temp_script=$(mktemp) + + if curl -fsSL https://deb.nodesource.com/setup_lts.x -o "$temp_script" 2>/dev/null; then + if sudo -E bash "$temp_script" >/dev/null 2>&1; then + if sudo apt-get install -y nodejs >/dev/null 2>&1; then + result=0 + fi + fi + rm -f "$temp_script" + fi + fi + ;; + dnf) + sudo dnf install -y nodejs >/dev/null 2>&1 && result=0 + ;; + pacman) + sudo pacman -S --noconfirm nodejs npm >/dev/null 2>&1 && result=0 + ;; + apk) + sudo apk add nodejs npm >/dev/null 2>&1 && result=0 + ;; + brew) + brew install node >/dev/null 2>&1 && result=0 + ;; + *) + print_warning "Unknown package manager: $PACKAGE_MANAGER" + ;; + esac + + if [[ $result -eq 0 ]] && check_command node; then + print_success "Node.js installed: $(node --version)" + NODE_INSTALL_SUCCESS=true + return 0 + fi + + return 1 +} + +# ============================================================================== +# Main Installation Function +# ============================================================================== + +# Install Node.js using the best available method +# Usage: install_nodejs [--force] +install_nodejs() { + local force="${1:-}" + + # Check if Node.js is already installed + if [[ "$force" != "--force" ]] && check_command node; then + local version + version=$(get_node_version) + print_success "Node.js is already installed: v$version" + NODE_INSTALL_SUCCESS=true + return 0 + fi + + print_info "Installing Node.js (required for Neovim code completion)..." + + # Try package manager first + if install_node_via_package_manager; then + return 0 + fi + + # Fall back to NVM + print_info "Package manager installation failed, trying NVM..." + if install_node_via_nvm; then + return 0 + fi + + # All methods failed + print_warning "Failed to install Node.js" + print_info "Creating fallback configuration for Neovim without CoC" + + # Create marker file for CoC-less config + mkdir -p "$HOME/.config/nvim" + touch "$HOME/.config/nvim/.no-coc" + + NODE_INSTALL_SUCCESS=false + return 1 +} + +# ============================================================================== +# NPM Package Management +# ============================================================================== + +# Install a global npm package +# Usage: npm_install_global +npm_install_global() { + local package="$1" + + if ! check_command npm; then + print_warning "npm not available, cannot install: $package" + return 1 + fi + + print_info "Installing npm package: $package" + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "npm install -g $package" + return 0 + fi + + npm install -g "$package" +} + +# Check if Node.js is functional (can run basic commands) +is_node_functional() { + if ! check_command node; then + return 1 + fi + + # Try to execute a simple command + if node -e "console.log('ok')" &>/dev/null; then + return 0 + fi + + return 1 +} + +# ============================================================================== +# Shell Configuration +# ============================================================================== + +# Add NVM to shell configuration +configure_nvm_shell() { + local nvm_config=' +# NVM Configuration +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" +' + + # Check if already configured + if grep -q 'NVM_DIR' "$HOME/.zshrc" 2>/dev/null || \ + grep -q 'NVM_DIR' "$HOME/.zshrc.local" 2>/dev/null; then + print_debug "NVM already configured in shell" + return 0 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "add NVM configuration to shell" + return 0 + fi + + # Add to .zshrc.local if it exists, otherwise .zshrc + if [[ -f "$HOME/.zshrc.local" ]]; then + echo "$nvm_config" >> "$HOME/.zshrc.local" + print_info "Added NVM configuration to ~/.zshrc.local" + elif [[ -f "$HOME/.zshrc" ]]; then + echo "$nvm_config" >> "$HOME/.zshrc" + print_info "Added NVM configuration to ~/.zshrc" + elif [[ -f "$HOME/.bashrc" ]]; then + echo "$nvm_config" >> "$HOME/.bashrc" + print_info "Added NVM configuration to ~/.bashrc" + fi +} diff --git a/modules/package-managers.sh b/modules/package-managers.sh new file mode 100755 index 0000000..d72bfe7 --- /dev/null +++ b/modules/package-managers.sh @@ -0,0 +1,354 @@ +#!/usr/bin/env bash +# modules/package-managers.sh - Package manager setup and utilities +# +# Handles detection and installation of package managers across platforms: +# - Homebrew (macOS and Linux) +# - apt (Debian/Ubuntu) +# - dnf (Fedora/RHEL) +# - pacman (Arch Linux) +# - apk (Alpine Linux) +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" +# source "$DOTFILES_DIR/modules/package-managers.sh" + +# Prevent multiple sourcing +[[ -n "${_PACKAGE_MANAGERS_SH_LOADED:-}" ]] && return 0 +readonly _PACKAGE_MANAGERS_SH_LOADED=1 + +# Ensure utils.sh is loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before modules/package-managers.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Package Manager Detection +# ============================================================================== + +# Global variable to track detected package manager +PACKAGE_MANAGER="" + +# Detect the available package manager +# Sets: PACKAGE_MANAGER variable +detect_package_manager() { + if [[ "$OS" == "macOS" ]]; then + if check_command brew; then + PACKAGE_MANAGER="brew" + fi + elif [[ "$OS" == "Linux" ]]; then + if check_command apt-get; then + PACKAGE_MANAGER="apt" + elif check_command dnf; then + PACKAGE_MANAGER="dnf" + elif check_command pacman; then + PACKAGE_MANAGER="pacman" + elif check_command apk; then + PACKAGE_MANAGER="apk" + elif check_command brew; then + PACKAGE_MANAGER="brew" + fi + fi + + export PACKAGE_MANAGER + print_debug "Detected package manager: ${PACKAGE_MANAGER:-none}" +} + +# ============================================================================== +# Homebrew +# ============================================================================== + +# Install Homebrew +# Works on both macOS and Linux +install_homebrew() { + if check_command brew; then + print_info "Homebrew is already installed" + return 0 + fi + + print_info "Installing Homebrew..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install Homebrew" + return 0 + fi + + # Download and run Homebrew installer + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + + # Add Homebrew to PATH based on OS and architecture + local brew_path="" + + if [[ "$OS" == "macOS" ]]; then + if [[ "$ARCH" == "arm64" ]]; then + brew_path="/opt/homebrew/bin/brew" + else + brew_path="/usr/local/bin/brew" + fi + else + brew_path="/home/linuxbrew/.linuxbrew/bin/brew" + fi + + if [[ -f "$brew_path" ]]; then + eval "$("$brew_path" shellenv)" + + # Add to bash_profile for persistence + if [[ -f "$HOME/.bash_profile" ]]; then + echo "eval \"\$($brew_path shellenv)\"" >> "$HOME/.bash_profile" + fi + + print_success "Homebrew installed successfully" + PACKAGE_MANAGER="brew" + export PACKAGE_MANAGER + return 0 + else + print_error "Homebrew installation failed" + return 1 + fi +} + +# ============================================================================== +# Package Manager Operations +# ============================================================================== + +# Update package manager cache/index +# Usage: update_package_cache +update_package_cache() { + print_info "Updating package manager cache..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "update package cache for $PACKAGE_MANAGER" + return 0 + fi + + case "$PACKAGE_MANAGER" in + apt) + sudo apt-get update -y + ;; + dnf) + sudo dnf check-update || true # Returns non-zero if updates available + ;; + pacman) + sudo pacman -Sy + ;; + apk) + sudo apk update + ;; + brew) + brew update + ;; + *) + print_warning "Unknown package manager: $PACKAGE_MANAGER" + return 1 + ;; + esac + + print_success "Package cache updated" +} + +# Install a package using the detected package manager +# Usage: install_package [alternate_names...] +# alternate_names: name variations for different package managers (apt:name dnf:name etc) +install_package() { + local package="$1" + shift + local alternates=("$@") + local pkg_name="$package" + + # Check for package manager specific name + for alt in "${alternates[@]}"; do + local pm="${alt%%:*}" + local name="${alt#*:}" + if [[ "$pm" == "$PACKAGE_MANAGER" ]]; then + pkg_name="$name" + break + fi + done + + print_info "Installing $pkg_name..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install package: $pkg_name via $PACKAGE_MANAGER" + return 0 + fi + + local result=0 + case "$PACKAGE_MANAGER" in + apt) + sudo apt-get install -y "$pkg_name" || result=1 + ;; + dnf) + sudo dnf install -y "$pkg_name" || result=1 + ;; + pacman) + sudo pacman -S --noconfirm "$pkg_name" || result=1 + ;; + apk) + sudo apk add "$pkg_name" || result=1 + ;; + brew) + brew install "$pkg_name" || result=1 + ;; + *) + print_error "No package manager available to install: $pkg_name" + return 1 + ;; + esac + + if [[ $result -eq 0 ]]; then + print_success "$pkg_name installed successfully" + else + print_warning "Failed to install $pkg_name" + fi + + return $result +} + +# Install multiple packages at once +# Usage: install_packages ... +install_packages() { + local packages=("$@") + + if [[ ${#packages[@]} -eq 0 ]]; then + return 0 + fi + + print_info "Installing packages: ${packages[*]}" + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install packages: ${packages[*]}" + return 0 + fi + + case "$PACKAGE_MANAGER" in + apt) + sudo apt-get install -y "${packages[@]}" + ;; + dnf) + sudo dnf install -y "${packages[@]}" + ;; + pacman) + sudo pacman -S --noconfirm "${packages[@]}" + ;; + apk) + sudo apk add "${packages[@]}" + ;; + brew) + brew install "${packages[@]}" + ;; + *) + print_error "No package manager available" + return 1 + ;; + esac +} + +# Check if a package is installed +# Usage: is_package_installed +is_package_installed() { + local package="$1" + + case "$PACKAGE_MANAGER" in + apt) + dpkg -l "$package" 2>/dev/null | grep -q "^ii" + ;; + dnf) + rpm -q "$package" &>/dev/null + ;; + pacman) + pacman -Qi "$package" &>/dev/null + ;; + apk) + apk info -e "$package" &>/dev/null + ;; + brew) + brew list "$package" &>/dev/null + ;; + *) + return 1 + ;; + esac +} + +# ============================================================================== +# Setup Functions +# ============================================================================== + +# Setup package managers for the system +# Installs Homebrew if needed on macOS or if no package manager available +setup_package_managers() { + print_section "Setting up Package Managers" + + # Detect current package manager + detect_package_manager + + if [[ "$OS" == "macOS" ]]; then + # macOS always uses Homebrew + if [[ -z "$PACKAGE_MANAGER" ]]; then + install_homebrew + else + print_success "Homebrew is already installed" + fi + elif [[ "$OS" == "Linux" ]]; then + if [[ -n "$PACKAGE_MANAGER" ]]; then + print_success "Using package manager: $PACKAGE_MANAGER" + # Update cache + update_package_cache + else + # No native package manager, try Homebrew + print_info "No native package manager found, installing Homebrew..." + install_homebrew + fi + fi + + # Re-detect after potential installation + detect_package_manager + + if [[ -z "$PACKAGE_MANAGER" ]]; then + print_error "No package manager available. Cannot continue." + return 1 + fi + + return 0 +} + +# ============================================================================== +# Snap Support (optional fallback) +# ============================================================================== + +# Check if snap is available +has_snap() { + check_command snap && [[ -d /snap ]] +} + +# Install a package via snap +# Usage: install_snap [--classic] +install_snap() { + local package="$1" + local classic="${2:-}" + + if ! has_snap; then + print_warning "Snap is not available on this system" + return 1 + fi + + print_info "Installing $package via snap..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "snap install $package $classic" + return 0 + fi + + if [[ "$classic" == "--classic" ]]; then + sudo snap install "$package" --classic + else + sudo snap install "$package" + fi +} + +# ============================================================================== +# Initialization +# ============================================================================== + +# Auto-detect package manager on load +detect_package_manager diff --git a/modules/terminal.sh b/modules/terminal.sh new file mode 100755 index 0000000..4550d1b --- /dev/null +++ b/modules/terminal.sh @@ -0,0 +1,333 @@ +#!/usr/bin/env bash +# modules/terminal.sh - Terminal emulator configuration +# +# Handles terminal configuration for: +# - iTerm2 (macOS) +# - GNOME Terminal (Linux) +# - Konsole (KDE) +# - Alacritty (cross-platform) +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" +# source "$DOTFILES_DIR/lib/backup.sh" +# source "$DOTFILES_DIR/modules/terminal.sh" + +# Prevent multiple sourcing +[[ -n "${_TERMINAL_SH_LOADED:-}" ]] && return 0 +readonly _TERMINAL_SH_LOADED=1 + +# Ensure required libraries are loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before modules/terminal.sh" >&2 + exit 1 +fi + +# ============================================================================== +# iTerm2 (macOS) +# ============================================================================== + +# Check if iTerm2 is installed +is_iterm_installed() { + [[ -d "/Applications/iTerm.app" ]] || [[ -d "$HOME/Applications/iTerm.app" ]] +} + +# Setup iTerm2 configuration +# Usage: setup_iterm [--backup] +setup_iterm() { + local dotfiles_dir="$1" + local should_backup="${2:-}" + + if ! is_iterm_installed; then + print_debug "iTerm2 not found" + return 0 + fi + + print_info "Configuring iTerm2..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "configure iTerm2" + return 0 + fi + + # Backup existing preferences if requested + if [[ "$should_backup" == "--backup" ]]; then + local iterm_plist="$HOME/Library/Preferences/com.googlecode.iterm2.plist" + if [[ -f "$iterm_plist" ]]; then + backup_with_registry "$iterm_plist" || backup_if_exists "$iterm_plist" || true + fi + fi + + # Ensure plist file exists and is valid + local iterm_plist_source="$dotfiles_dir/iterm/com.googlecode.iterm2.plist" + + if [[ -f "$iterm_plist_source" ]]; then + # Validate plist format + if ! plutil -lint "$iterm_plist_source" >/dev/null 2>&1; then + print_warning "iTerm2 plist file is malformed, creating a new properly formatted one..." + defaults export com.googlecode.iterm2 "$iterm_plist_source" 2>/dev/null || true + fi + else + # Export current preferences + print_info "Creating iTerm2 plist file..." + defaults export com.googlecode.iterm2 "$iterm_plist_source" 2>/dev/null || true + fi + + # Configure iTerm2 to use our preferences + print_info "Setting iTerm2 to load preferences from dotfiles..." + defaults write com.googlecode.iterm2 LoadPrefsFromCustomFolder -bool true + defaults write com.googlecode.iterm2 PrefsCustomFolder -string "$dotfiles_dir/iterm" + + # Create DynamicProfiles directory + mkdir -p "$HOME/Library/Application Support/iTerm2/DynamicProfiles" + + print_success "iTerm2 configured!" + print_info "Please restart iTerm2 for changes to take effect." + print_info "Note: You may need to run 'killall cfprefsd' to force preference reload." + + return 0 +} + +# ============================================================================== +# Desktop Environment Detection (Linux) +# ============================================================================== + +# Detect Linux desktop environment +# Outputs: desktop environment name (lowercase) +detect_desktop_environment() { + local de="" + + # Try various environment variables + if [[ -n "${XDG_CURRENT_DESKTOP:-}" ]]; then + de="$XDG_CURRENT_DESKTOP" + elif [[ -n "${DESKTOP_SESSION:-}" ]]; then + de="$DESKTOP_SESSION" + elif [[ -n "${XDG_DATA_DIRS:-}" ]]; then + de=$(echo "$XDG_DATA_DIRS" | grep -Eo 'gnome|kde|xfce|cinnamon|mate' | head -1) + fi + + # Fallback: detect by running processes + if [[ -z "$de" ]]; then + if check_command gnome-shell || check_command gnome-session; then + de="gnome" + elif check_command plasmashell; then + de="kde" + elif check_command xfce4-session; then + de="xfce" + fi + fi + + # Normalize to lowercase + echo "${de,,}" +} + +# ============================================================================== +# GNOME Terminal +# ============================================================================== + +# Check if GNOME Terminal is available +has_gnome_terminal() { + check_command gnome-terminal && check_command dconf +} + +# Setup GNOME Terminal theme +# Usage: setup_gnome_terminal +setup_gnome_terminal() { + local dotfiles_dir="$1" + local theme_file="$dotfiles_dir/terminal/gnome-terminal-catppuccin.dconf" + + if ! has_gnome_terminal; then + return 0 + fi + + if [[ ! -f "$theme_file" ]]; then + print_warning "GNOME Terminal theme file not found" + return 1 + fi + + print_info "Setting up GNOME Terminal theme..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "load GNOME Terminal theme" + return 0 + fi + + # Check for dconf + if ! check_command dconf; then + print_warning "dconf not found. Attempting to install..." + + case "$PACKAGE_MANAGER" in + apt) + sudo apt-get install -y dconf-cli + ;; + dnf) + sudo dnf install -y dconf + ;; + pacman) + sudo pacman -S --noconfirm dconf + ;; + *) + print_error "Could not install dconf" + return 1 + ;; + esac + fi + + if check_command dconf; then + dconf load /org/gnome/terminal/legacy/profiles:/ < "$theme_file" + print_success "GNOME Terminal theme installed" + else + print_error "dconf still not available" + + # Create fallback script + local fallback_script="$HOME/.local/bin/apply-terminal-theme.sh" + safe_mkdir "$(dirname "$fallback_script")" + + cat > "$fallback_script" << EOF +#!/bin/bash +# Run this script to apply the Catppuccin Mocha theme to GNOME Terminal +dconf load /org/gnome/terminal/legacy/profiles:/ < "$theme_file" +EOF + chmod +x "$fallback_script" + print_info "Created $fallback_script. Run it after installing dconf." + fi + + return 0 +} + +# ============================================================================== +# Konsole (KDE) +# ============================================================================== + +# Check if Konsole is available +has_konsole() { + check_command konsole +} + +# Setup Konsole theme +# Usage: setup_konsole [--backup] +setup_konsole() { + local dotfiles_dir="$1" + local should_backup="${2:-}" + local konsole_dir="$HOME/.local/share/konsole" + local theme_file="$konsole_dir/Catppuccin-Mocha.colorscheme" + local source_file="$dotfiles_dir/terminal/Catppuccin-Mocha.colorscheme" + + if ! has_konsole; then + return 0 + fi + + if [[ ! -f "$source_file" ]]; then + print_warning "Konsole theme file not found" + return 1 + fi + + print_info "Setting up Konsole theme..." + + # Backup if requested + if [[ "$should_backup" == "--backup" ]] && [[ -f "$theme_file" ]]; then + backup_with_registry "$theme_file" || backup_if_exists "$theme_file" || true + fi + + safe_mkdir "$konsole_dir" + + if safe_copy "$source_file" "$theme_file"; then + print_success "Konsole theme installed" + print_info "Please go to Konsole settings to apply it." + else + print_error "Failed to install Konsole theme" + return 1 + fi + + return 0 +} + +# ============================================================================== +# Alacritty +# ============================================================================== + +# Check if Alacritty is available +has_alacritty() { + check_command alacritty +} + +# Setup Alacritty configuration +# Usage: setup_alacritty [--backup] +setup_alacritty() { + local dotfiles_dir="$1" + local should_backup="${2:-}" + local alacritty_dir="$HOME/.config/alacritty" + local config_file="$alacritty_dir/alacritty.yml" + local source_file="$dotfiles_dir/terminal/alacritty.yml" + + if ! has_alacritty; then + return 0 + fi + + if [[ ! -f "$source_file" ]]; then + print_debug "Alacritty config not found in dotfiles" + return 0 + fi + + print_info "Setting up Alacritty..." + + # Backup if requested + if [[ "$should_backup" == "--backup" ]]; then + if [[ -f "$config_file" ]]; then + backup_with_registry "$config_file" || backup_if_exists "$config_file" || true + elif [[ -d "$alacritty_dir" ]]; then + backup_with_registry "$alacritty_dir" || backup_if_exists "$alacritty_dir" || true + fi + fi + + safe_mkdir "$alacritty_dir" + + if safe_copy "$source_file" "$config_file"; then + print_success "Alacritty configuration installed" + else + print_error "Failed to install Alacritty configuration" + return 1 + fi + + return 0 +} + +# ============================================================================== +# Main Setup Function +# ============================================================================== + +# Complete terminal setup +# Usage: terminal_setup [--backup] +terminal_setup() { + local dotfiles_dir="$1" + local should_backup="${2:-}" + + print_section "Setting up Terminal" + + if [[ "$OS" == "macOS" ]]; then + # macOS: iTerm2 and Alacritty + setup_iterm "$dotfiles_dir" "$should_backup" + setup_alacritty "$dotfiles_dir" "$should_backup" + elif [[ "$OS" == "Linux" ]]; then + # Linux: Detect DE and setup appropriate terminal + local de + de=$(detect_desktop_environment) + print_info "Detected desktop environment: ${de:-unknown}" + + case "$de" in + *gnome*|*unity*|*ubuntu*) + setup_gnome_terminal "$dotfiles_dir" + ;; + *kde*|*plasma*) + setup_konsole "$dotfiles_dir" "$should_backup" + ;; + *) + print_info "Unknown desktop environment, skipping DE-specific terminal setup" + ;; + esac + + # Alacritty is DE-independent + setup_alacritty "$dotfiles_dir" "$should_backup" + fi + + print_success "Terminal setup complete" +} diff --git a/modules/vscode.sh b/modules/vscode.sh new file mode 100755 index 0000000..ab1c7b5 --- /dev/null +++ b/modules/vscode.sh @@ -0,0 +1,332 @@ +#!/usr/bin/env bash +# modules/vscode.sh - VSCode configuration and extensions +# +# Handles: +# - VSCode settings configuration (template-based) +# - Extension installation +# - Cross-platform support (macOS and Linux) +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" +# source "$DOTFILES_DIR/lib/backup.sh" +# source "$DOTFILES_DIR/modules/vscode.sh" + +# Prevent multiple sourcing +[[ -n "${_VSCODE_SH_LOADED:-}" ]] && return 0 +readonly _VSCODE_SH_LOADED=1 + +# Ensure required libraries are loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before modules/vscode.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Configuration +# ============================================================================== + +# List of recommended extensions +readonly VSCODE_EXTENSIONS=( + "catppuccin.catppuccin-vsc" + "streetsidesoftware.code-spell-checker" + "fcrespo82.markdown-table-formatter" +) + +# ============================================================================== +# VSCode Detection +# ============================================================================== + +# Get VSCode configuration directory +# Outputs: path to VSCode User directory +get_vscode_config_dir() { + local config_dir="" + + if [[ "$OS" == "macOS" ]]; then + config_dir="$HOME/Library/Application Support/Code/User" + elif [[ "$OS" == "Linux" ]]; then + config_dir="$HOME/.config/Code/User" + fi + + # Check for VSCode Insiders + if [[ ! -d "$config_dir" ]]; then + if [[ "$OS" == "macOS" ]]; then + config_dir="$HOME/Library/Application Support/Code - Insiders/User" + elif [[ "$OS" == "Linux" ]]; then + config_dir="$HOME/.config/Code - Insiders/User" + fi + fi + + echo "$config_dir" +} + +# Check if VSCode is installed +is_vscode_installed() { + check_command code || check_command code-insiders +} + +# Get VSCode command name +get_vscode_cmd() { + if check_command code; then + echo "code" + elif check_command code-insiders; then + echo "code-insiders" + else + echo "" + fi +} + +# ============================================================================== +# Settings Management +# ============================================================================== + +# Setup VSCode settings from template +# Usage: setup_vscode_settings [--backup] +setup_vscode_settings() { + local dotfiles_dir="$1" + local should_backup="${2:-}" + local config_dir + local settings_file + local settings_local_file + + config_dir=$(get_vscode_config_dir) + + if [[ -z "$config_dir" ]]; then + print_warning "Could not determine VSCode config directory" + return 1 + fi + + settings_file="$config_dir/settings.json" + settings_local_file="$config_dir/settings.local.json" + + # Check for template + local template_file="$dotfiles_dir/vscode/settings.json.template" + local legacy_file="$dotfiles_dir/vscode/settings.json" + + # Use template if available, fall back to legacy + local source_file="" + if [[ -f "$template_file" ]]; then + source_file="$template_file" + elif [[ -f "$legacy_file" ]]; then + source_file="$legacy_file" + print_debug "Using legacy settings.json (consider migrating to template)" + else + print_warning "No VSCode settings template found" + return 1 + fi + + # Create config directory + safe_mkdir "$config_dir" + + # Backup existing settings if requested + if [[ "$should_backup" == "--backup" ]] && [[ -f "$settings_file" ]]; then + backup_with_registry "$settings_file" || backup_if_exists "$settings_file" || true + fi + + print_info "Setting up VSCode settings..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "copy VSCode settings from template" + return 0 + fi + + # Copy settings from template + if cp "$source_file" "$settings_file"; then + print_success "VSCode settings installed" + else + print_error "Failed to install VSCode settings" + return 1 + fi + + # Create local settings file if it doesn't exist + local local_template="$dotfiles_dir/vscode/settings.local.json.template" + if [[ ! -f "$settings_local_file" ]] && [[ -f "$local_template" ]]; then + cp "$local_template" "$settings_local_file" + print_info "Created $settings_local_file for personal settings" + elif [[ ! -f "$settings_local_file" ]]; then + # Create empty local settings + echo "{}" > "$settings_local_file" + print_info "Created empty $settings_local_file for personal settings" + fi + + return 0 +} + +# ============================================================================== +# Extension Management +# ============================================================================== + +# Check if an extension is installed +# Usage: is_extension_installed +is_extension_installed() { + local ext_id="$1" + local vscode_cmd + + vscode_cmd=$(get_vscode_cmd) + [[ -z "$vscode_cmd" ]] && return 1 + + "$vscode_cmd" --list-extensions 2>/dev/null | grep -qi "^${ext_id}$" +} + +# Install a VSCode extension +# Usage: install_extension +install_extension() { + local ext_id="$1" + local vscode_cmd + + vscode_cmd=$(get_vscode_cmd) + [[ -z "$vscode_cmd" ]] && return 1 + + if is_extension_installed "$ext_id"; then + print_debug "Extension already installed: $ext_id" + return 0 + fi + + print_info "Installing extension: $ext_id" + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install extension: $ext_id" + return 0 + fi + + if "$vscode_cmd" --install-extension "$ext_id" 2>/dev/null; then + print_success "Installed: $ext_id" + return 0 + else + print_warning "Failed to install: $ext_id" + return 1 + fi +} + +# Install all recommended extensions +install_vscode_extensions() { + local vscode_cmd + + vscode_cmd=$(get_vscode_cmd) + + if [[ -z "$vscode_cmd" ]]; then + print_warning "VSCode command not found" + return 1 + fi + + print_info "Installing VSCode extensions..." + + for ext in "${VSCODE_EXTENSIONS[@]}"; do + install_extension "$ext" + done + + print_success "VSCode extensions installation complete" +} + +# Update all installed extensions +update_vscode_extensions() { + local vscode_cmd + + vscode_cmd=$(get_vscode_cmd) + + if [[ -z "$vscode_cmd" ]]; then + return 1 + fi + + print_info "Updating VSCode extensions..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "update VSCode extensions" + return 0 + fi + + # VSCode doesn't have a built-in update command + # Extensions auto-update, so we just reinstall to force latest + for ext in "${VSCODE_EXTENSIONS[@]}"; do + "$vscode_cmd" --install-extension "$ext" --force 2>/dev/null || true + done + + print_success "VSCode extensions updated" +} + +# ============================================================================== +# Main Setup Function +# ============================================================================== + +# Complete VSCode setup +# Usage: vscode_setup [--backup] [--skip-extensions] +vscode_setup() { + local dotfiles_dir="$1" + shift + + local backup_flag="" + local skip_extensions="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --backup) backup_flag="--backup" ;; + --skip-extensions) skip_extensions="true" ;; + *) ;; + esac + shift + done + + if ! is_vscode_installed; then + print_warning "VSCode not found. Skipping VSCode setup." + return 0 + fi + + print_section "Setting up VSCode" + + # Check for config directory + local config_dir + config_dir=$(get_vscode_config_dir) + + if [[ -z "$config_dir" ]] || [[ ! -d "$(dirname "$config_dir")" ]]; then + print_warning "VSCode user directory not found. Skipping config linking." + return 0 + fi + + # Setup settings + setup_vscode_settings "$dotfiles_dir" "$backup_flag" + + # Install extensions (unless skipped) + if [[ -z "$skip_extensions" ]]; then + install_vscode_extensions + fi + + print_success "VSCode setup complete" +} + +# ============================================================================== +# Verification +# ============================================================================== + +# Verify VSCode setup +verify_vscode_setup() { + local all_good=true + + if ! is_vscode_installed; then + print_info "VSCode not installed" + return 0 + fi + + print_info "Verifying VSCode setup..." + + # Check settings file + local config_dir + config_dir=$(get_vscode_config_dir) + + if [[ -f "$config_dir/settings.json" ]]; then + print_success "VSCode settings.json is in place" + else + print_warning "VSCode settings.json is missing" + all_good=false + fi + + # Check extensions + for ext in "${VSCODE_EXTENSIONS[@]}"; do + if is_extension_installed "$ext"; then + print_success "Extension installed: $ext" + else + print_warning "Extension missing: $ext" + all_good=false + fi + done + + [[ "$all_good" == "true" ]] +} diff --git a/modules/zsh.sh b/modules/zsh.sh new file mode 100755 index 0000000..096f058 --- /dev/null +++ b/modules/zsh.sh @@ -0,0 +1,400 @@ +#!/usr/bin/env bash +# modules/zsh.sh - Zsh installation and configuration +# +# Handles: +# - Zsh shell installation +# - Oh My Zsh framework +# - Zsh plugins (autosuggestions, syntax-highlighting) +# - Powerlevel10k theme +# - Shell change to zsh +# +# Usage: +# source "$DOTFILES_DIR/lib/utils.sh" +# source "$DOTFILES_DIR/modules/package-managers.sh" +# source "$DOTFILES_DIR/modules/zsh.sh" + +# Prevent multiple sourcing +[[ -n "${_ZSH_SH_LOADED:-}" ]] && return 0 +readonly _ZSH_SH_LOADED=1 + +# Ensure required libraries are loaded +if [[ -z "${_UTILS_SH_LOADED:-}" ]]; then + echo "ERROR: lib/utils.sh must be sourced before modules/zsh.sh" >&2 + exit 1 +fi + +# ============================================================================== +# Configuration +# ============================================================================== + +# Oh My Zsh directory +export ZSH="${ZSH:-$HOME/.oh-my-zsh}" +export ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}" + +# ============================================================================== +# Zsh Installation +# ============================================================================== + +# Install Zsh shell +install_zsh() { + if check_command zsh; then + print_success "Zsh is already installed" + return 0 + fi + + print_info "Installing Zsh..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install Zsh" + return 0 + fi + + if install_package zsh; then + if check_command zsh; then + print_success "Zsh installed successfully" + return 0 + fi + fi + + print_error "Failed to install Zsh" + return 1 +} + +# ============================================================================== +# Oh My Zsh +# ============================================================================== + +# Install Oh My Zsh framework +install_oh_my_zsh() { + if [[ -d "$ZSH" ]]; then + print_success "Oh My Zsh is already installed" + return 0 + fi + + print_info "Installing Oh My Zsh..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install Oh My Zsh" + return 0 + fi + + if ! check_command git; then + print_error "Git is required to install Oh My Zsh" + return 1 + fi + + # Clone Oh My Zsh (avoiding the install script which changes shell) + if git clone https://github.com/ohmyzsh/ohmyzsh.git "$ZSH"; then + print_success "Oh My Zsh installed successfully" + + # Create custom directories + mkdir -p "$ZSH_CUSTOM/plugins" + mkdir -p "$ZSH_CUSTOM/themes" + + return 0 + else + print_error "Failed to clone Oh My Zsh repository" + return 1 + fi +} + +# Update Oh My Zsh +update_oh_my_zsh() { + if [[ ! -d "$ZSH" ]]; then + print_warning "Oh My Zsh not installed" + return 1 + fi + + print_info "Updating Oh My Zsh..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "update Oh My Zsh" + return 0 + fi + + (cd "$ZSH" && git pull --rebase --quiet) + print_success "Oh My Zsh updated" +} + +# ============================================================================== +# Zsh Plugins +# ============================================================================== + +# Install zsh-autosuggestions plugin +install_zsh_autosuggestions() { + local plugin_dir="$ZSH_CUSTOM/plugins/zsh-autosuggestions" + + if [[ -d "$plugin_dir" ]]; then + print_success "zsh-autosuggestions is already installed" + return 0 + fi + + print_info "Installing zsh-autosuggestions plugin..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install zsh-autosuggestions" + return 0 + fi + + mkdir -p "$ZSH_CUSTOM/plugins" + + if git clone https://github.com/zsh-users/zsh-autosuggestions "$plugin_dir"; then + print_success "zsh-autosuggestions installed" + return 0 + else + print_error "Failed to install zsh-autosuggestions" + return 1 + fi +} + +# Install zsh-syntax-highlighting plugin +install_zsh_syntax_highlighting() { + local plugin_dir="$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" + + if [[ -d "$plugin_dir" ]]; then + print_success "zsh-syntax-highlighting is already installed" + return 0 + fi + + print_info "Installing zsh-syntax-highlighting plugin..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install zsh-syntax-highlighting" + return 0 + fi + + mkdir -p "$ZSH_CUSTOM/plugins" + + if git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "$plugin_dir"; then + print_success "zsh-syntax-highlighting installed" + return 0 + else + print_error "Failed to install zsh-syntax-highlighting" + return 1 + fi +} + +# Install all Zsh plugins +install_zsh_plugins() { + print_info "Installing Zsh plugins..." + + install_zsh_autosuggestions + install_zsh_syntax_highlighting + + print_success "Zsh plugins installed" +} + +# Update Zsh plugins +update_zsh_plugins() { + print_info "Updating Zsh plugins..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "update Zsh plugins" + return 0 + fi + + local plugin_dir + for plugin_dir in "$ZSH_CUSTOM/plugins"/*/; do + [[ ! -d "$plugin_dir/.git" ]] && continue + + local plugin_name + plugin_name=$(basename "$plugin_dir") + print_debug "Updating plugin: $plugin_name" + (cd "$plugin_dir" && git pull --rebase --quiet) || true + done + + print_success "Zsh plugins updated" +} + +# ============================================================================== +# Powerlevel10k Theme +# ============================================================================== + +# Install Powerlevel10k theme +install_powerlevel10k() { + local theme_dir="$ZSH_CUSTOM/themes/powerlevel10k" + + if [[ -d "$theme_dir" ]]; then + print_success "Powerlevel10k is already installed" + return 0 + fi + + print_info "Installing Powerlevel10k theme..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install Powerlevel10k" + return 0 + fi + + mkdir -p "$ZSH_CUSTOM/themes" + + if git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "$theme_dir"; then + print_success "Powerlevel10k theme installed" + return 0 + else + print_error "Failed to install Powerlevel10k" + return 1 + fi +} + +# Update Powerlevel10k theme +update_powerlevel10k() { + local theme_dir="$ZSH_CUSTOM/themes/powerlevel10k" + + if [[ ! -d "$theme_dir" ]]; then + return 0 + fi + + print_info "Updating Powerlevel10k theme..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "update Powerlevel10k" + return 0 + fi + + (cd "$theme_dir" && git pull --rebase --quiet) + print_success "Powerlevel10k updated" +} + +# Setup Powerlevel10k configuration +# Usage: setup_p10k_config +setup_p10k_config() { + local dotfiles_dir="$1" + local p10k_config="$HOME/.p10k.zsh" + local p10k_template="$dotfiles_dir/zsh/.p10k.zsh" + + if [[ -f "$p10k_config" ]]; then + print_debug "Powerlevel10k configuration already exists" + return 0 + fi + + if [[ ! -f "$p10k_template" ]]; then + print_debug "No Powerlevel10k template found" + return 0 + fi + + print_info "Setting up Powerlevel10k configuration..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "copy Powerlevel10k config" + return 0 + fi + + cp "$p10k_template" "$p10k_config" + print_success "Powerlevel10k configuration created" +} + +# ============================================================================== +# Shell Change +# ============================================================================== + +# Set Zsh as the default shell +set_zsh_as_default() { + local zsh_path + + # Get Zsh path + zsh_path=$(which zsh 2>/dev/null) + + if [[ -z "$zsh_path" ]] || [[ ! -f "$zsh_path" ]]; then + print_error "Could not find Zsh binary" + return 1 + fi + + # Check if already default + if [[ "$SHELL" == "$zsh_path" ]]; then + print_success "Zsh is already the default shell" + return 0 + fi + + print_info "Setting Zsh as default shell..." + + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "set $zsh_path as default shell" + return 0 + fi + + # Ensure Zsh is in /etc/shells + if ! grep -q "$zsh_path" /etc/shells 2>/dev/null; then + print_info "Adding Zsh to /etc/shells..." + if command -v sudo >/dev/null 2>&1; then + echo "$zsh_path" | sudo tee -a /etc/shells >/dev/null 2>&1 || true + fi + fi + + # Check if Zsh is in /etc/shells + if ! grep -q "$zsh_path" /etc/shells 2>/dev/null; then + print_warning "Zsh not in /etc/shells, cannot change default shell" + print_info "Add it manually: echo $zsh_path | sudo tee -a /etc/shells" + print_info "Then run: sudo chsh -s $zsh_path $USER" + return 1 + fi + + # Change shell using sudo chsh (avoids password prompt issues) + if sudo chsh -s "$zsh_path" "$USER" 2>/dev/null; then + print_success "Default shell changed to Zsh" + print_info "Log out and back in (or reboot) for the shell change to take effect" + print_info "Or start Zsh now by typing: zsh" + return 0 + else + print_warning "Could not automatically change default shell" + print_info "Run manually: sudo chsh -s $zsh_path $USER" + print_info "Or start Zsh now by typing: zsh" + return 1 + fi +} + +# ============================================================================== +# Main Setup Function +# ============================================================================== + +# Complete Zsh setup +# Usage: zsh_setup [--skip-shell-change] +zsh_setup() { + local dotfiles_dir="$1" + local skip_shell_change="" + + shift + while [[ $# -gt 0 ]]; do + case "$1" in + --skip-shell-change) skip_shell_change="true" ;; + *) ;; + esac + shift + done + + print_section "Setting up Zsh" + + # Install Zsh + install_zsh || return 1 + + # Install Oh My Zsh + install_oh_my_zsh || return 1 + + # Install plugins + install_zsh_plugins + + # Install Powerlevel10k + install_powerlevel10k + + # Setup p10k config + setup_p10k_config "$dotfiles_dir" + + # Set Zsh as default (unless skipped) + if [[ -z "$skip_shell_change" ]]; then + set_zsh_as_default + fi + + print_success "Zsh setup complete" +} + +# Update all Zsh components +# Usage: zsh_update +zsh_update() { + print_section "Updating Zsh Components" + + update_oh_my_zsh + update_zsh_plugins + update_powerlevel10k + + print_success "Zsh components updated" +} diff --git a/nvim/upgrade-nvim.sh b/nvim/upgrade-nvim.sh index 7de30c3..a999154 100755 --- a/nvim/upgrade-nvim.sh +++ b/nvim/upgrade-nvim.sh @@ -1,225 +1,86 @@ #!/usr/bin/env bash -# Script to upgrade Neovim to a specific version in environments where the system package is outdated -# This is especially useful for remote environments like Coder +# nvim/upgrade-nvim.sh - Upgrade Neovim to latest stable version +# +# This script upgrades Neovim to the latest stable release, which is +# especially useful for remote environments where the system package is outdated. -set -eo pipefail # Exit on error, fail on pipe failures +set -eo pipefail -# Required minimum Neovim version -readonly REQUIRED_VERSION="0.9.0" +# ============================================================================== +# Initialization +# ============================================================================== -# Detect architecture and return archive name -# Note: Neovim naming changed - newer releases use linux-x86_64/linux-arm64 -detect_arch() { - local arch=$(uname -m) - case "$arch" in - x86_64|amd64) - echo "linux-x86_64" - ;; - aarch64|arm64) - echo "linux-arm64" - ;; - armv7l|armhf) - # Neovim doesn't provide 32-bit ARM builds, need to use package manager - echo "arm32-unsupported" - ;; - *) - echo "unknown" - ;; - esac -} +# Determine script directory and dotfiles root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_DIR="$(dirname "$SCRIPT_DIR")" -# Colors for output -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[0;33m' -readonly BLUE='\033[0;34m' -readonly NC='\033[0m' # No Color +# Source libraries +source "$DOTFILES_DIR/lib/utils.sh" +source "$DOTFILES_DIR/lib/network.sh" -# Setup directories upfront to avoid repetition -readonly USER_BIN_DIR="$HOME/.local/bin" -readonly USER_SHARE_DIR="$HOME/.local/share/nvim" -mkdir -p "$USER_BIN_DIR" "$USER_SHARE_DIR" +# Source neovim module for shared functions +source "$DOTFILES_DIR/modules/package-managers.sh" +source "$DOTFILES_DIR/modules/neovim.sh" -# Add user bin directory to PATH if not already there -if [[ ":$PATH:" != *":$USER_BIN_DIR:"* ]]; then - export PATH="$USER_BIN_DIR:$PATH" -fi +# ============================================================================== +# Configuration +# ============================================================================== -# Function to check if Neovim version is sufficient -check_nvim_version() { - if ! command -v nvim &>/dev/null; then - echo -e "${YELLOW}Neovim not found.${NC}" - return 1 - fi - - local CURRENT_VERSION=$(nvim --version | head -n1 | cut -d ' ' -f2 | sed 's/^v//') - - # Compare versions - if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$CURRENT_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]; then - echo -e "${YELLOW}Neovim $CURRENT_VERSION is older than required version $REQUIRED_VERSION${NC}" - return 1 - else - echo -e "${GREEN}Neovim $CURRENT_VERSION meets required version $REQUIRED_VERSION${NC}" - return 0 - fi -} - -# Function to download Neovim with fallback mechanisms -download_neovim() { - local temp_dir=$1 - local arch=$(detect_arch) - local dl_success=false - - # Handle unsupported architectures - if [ "$arch" = "arm32-unsupported" ]; then - echo -e "${YELLOW}32-bit ARM is not supported by Neovim prebuilt binaries.${NC}" - echo -e "${YELLOW}Please install Neovim via your package manager: sudo apt install neovim${NC}" - return 1 - fi - - if [ "$arch" = "unknown" ]; then - echo -e "${RED}Unknown architecture: $(uname -m)${NC}" - return 1 - fi - - local archive_name="nvim-${arch}" - local output_file="$temp_dir/${archive_name}.tar.gz" - - # Try stable release first (has ARM64 builds) - local download_url="https://github.com/neovim/neovim/releases/download/stable/${archive_name}.tar.gz" - - echo -e "${BLUE}Downloading Neovim stable for ${arch}...${NC}" - - # Try wget first if available (more reliable for large files) - if command -v wget &>/dev/null; then - wget -q --show-progress "$download_url" -O "$output_file" && dl_success=true - fi - - # If wget failed or isn't available, try curl - if [ "$dl_success" = false ] && command -v curl &>/dev/null; then - curl -L --progress-bar -o "$output_file" "$download_url" && dl_success=true - fi - - # Check if download succeeded - if [ "$dl_success" = false ] || [ ! -s "$output_file" ]; then - echo -e "${RED}Failed to download Neovim. Check your internet connection.${NC}" - return 1 - fi - - # Store the archive name for extraction - echo "$archive_name" > "$temp_dir/.archive_name" - return 0 -} +NON_INTERACTIVE=false -# Function to install latest Neovim -install_latest_neovim() { - # Create a clean temporary directory for downloads - local TEMP_DIR=$(mktemp -d) - trap 'rm -rf "$TEMP_DIR"' EXIT - - # Download Neovim - if ! download_neovim "$TEMP_DIR"; then - return 1 - fi - - # Get the archive name from download function - local archive_name=$(cat "$TEMP_DIR/.archive_name" 2>/dev/null || echo "nvim-linux64") - - # Extract - echo -e "${BLUE}Extracting Neovim...${NC}" - tar -xzf "$TEMP_DIR/${archive_name}.tar.gz" -C "$TEMP_DIR" - - # Check if extraction was successful (handle both naming conventions) - local extract_dir="$TEMP_DIR/${archive_name}" - if [ ! -d "$extract_dir" ]; then - # Try alternative naming (some versions use nvim-linux64 even for arm64) - extract_dir=$(find "$TEMP_DIR" -maxdepth 1 -type d -name "nvim-*" | head -1) - fi - - if [ -z "$extract_dir" ] || [ ! -d "$extract_dir" ]; then - echo -e "${RED}Failed to extract Neovim. Archive may be corrupted.${NC}" - return 1 - fi +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --non-interactive) + NON_INTERACTIVE=true + ;; + --help) + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --non-interactive Run without prompting (for automated scripts)" + echo " --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown parameter: $1" + exit 1 + ;; + esac + shift +done - # Install - echo -e "${BLUE}Installing Neovim...${NC}" - cp -f "$extract_dir/bin/nvim" "$USER_BIN_DIR/" - cp -rf "$extract_dir/share/nvim/"* "$USER_SHARE_DIR/" - - # Make executable - chmod +x "$USER_BIN_DIR/nvim" - - # Update shell configuration files if needed - update_shell_config - - # Verify - echo -e "${BLUE}Verifying installation...${NC}" - local NEW_VERSION=$("$USER_BIN_DIR/nvim" --version | head -n1) - - if [ -n "$NEW_VERSION" ]; then - echo -e "${GREEN}$NEW_VERSION installed successfully!${NC}" - echo -e "${GREEN}Neovim installation complete!${NC}" - return 0 - else - echo -e "${RED}Verification failed. Neovim may not have installed correctly.${NC}" - return 1 - fi -} +# ============================================================================== +# Main +# ============================================================================== -# Function to update shell configuration files -update_shell_config() { - local PATH_EXPORT='export PATH="$HOME/.local/bin:$PATH"' - local CONFIG_UPDATED=false +main() { + # Check current version + if check_nvim_version; then + local current + current=$(get_nvim_version) + print_success "Neovim v$current meets required version $NVIM_MIN_VERSION" - # Try to update .zshrc.local first if it exists - if [ -f "$HOME/.zshrc.local" ]; then - if ! grep -q '.local/bin' "$HOME/.zshrc.local" 2>/dev/null; then - echo "$PATH_EXPORT" >> "$HOME/.zshrc.local" - CONFIG_UPDATED=true - fi - # If no .zshrc.local, try .zshrc - elif [ -f "$HOME/.zshrc" ]; then - if ! grep -q '.local/bin' "$HOME/.zshrc" 2>/dev/null; then - echo "$PATH_EXPORT" >> "$HOME/.zshrc" - CONFIG_UPDATED=true - fi - # Last resort, update .bashrc - elif [ -f "$HOME/.bashrc" ]; then - if ! grep -q '.local/bin' "$HOME/.bashrc" 2>/dev/null; then - echo "$PATH_EXPORT" >> "$HOME/.bashrc" - CONFIG_UPDATED=true + if [[ "$NON_INTERACTIVE" == "true" ]]; then + return 0 fi - fi - if [ "$CONFIG_UPDATED" = true ]; then - echo -e "${BLUE}Updated shell configuration to include $USER_BIN_DIR in PATH${NC}" + if ! confirm "Do you want to upgrade anyway?"; then + return 0 + fi fi -} -# Main entry point -main() { - # Check if we need to upgrade - if check_nvim_version; then - # Already have a sufficient version - return 0 - fi - - # If we got here, we need to upgrade - if [ "$1" = "--non-interactive" ]; then - # Non-interactive mode (used by install.sh) - echo -e "${BLUE}Upgrading Neovim automatically...${NC}" - install_latest_neovim + # Need to upgrade + if [[ "$NON_INTERACTIVE" == "true" ]]; then + print_info "Upgrading Neovim automatically..." + upgrade_neovim else - # Interactive mode (when run directly) - echo -e "${YELLOW}Do you want to install/upgrade Neovim? (y/n)${NC}" - read -r ANSWER - if [[ "$ANSWER" =~ ^[Yy]$ ]]; then - install_latest_neovim + if confirm "Do you want to install/upgrade Neovim?"; then + upgrade_neovim else - echo -e "${YELLOW}Skipping Neovim upgrade. Some plugins might not work properly.${NC}" + print_warning "Skipping Neovim upgrade. Some plugins might not work properly." fi fi } -# Run main function with all arguments passed to the script -main "$@" \ No newline at end of file +main "$@" diff --git a/terminal/install-terminal-themes.sh b/terminal/install-terminal-themes.sh index 35a85f9..aa59c8f 100755 --- a/terminal/install-terminal-themes.sh +++ b/terminal/install-terminal-themes.sh @@ -1,196 +1,47 @@ #!/usr/bin/env bash +# terminal/install-terminal-themes.sh - Install terminal themes +# +# This script installs Catppuccin Mocha theme for various terminal emulators. -# Script to install terminal themes (Catppuccin Mocha) set -eo pipefail -# Colors -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[0;33m' -readonly BLUE='\033[0;34m' -readonly NC='\033[0m' # No Color +# ============================================================================== +# Initialization +# ============================================================================== -# Helper functions -print_info() { - echo -e "${BLUE}INFO:${NC} $1" -} - -print_success() { - echo -e "${GREEN}SUCCESS:${NC} $1" -} +# Determine script directory and dotfiles root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_DIR="$(dirname "$SCRIPT_DIR")" -print_warning() { - echo -e "${YELLOW}WARNING:${NC} $1" -} +# Source libraries +source "$DOTFILES_DIR/lib/utils.sh" +source "$DOTFILES_DIR/lib/backup.sh" +source "$DOTFILES_DIR/modules/package-managers.sh" +source "$DOTFILES_DIR/modules/terminal.sh" -print_error() { - echo -e "${RED}ERROR:${NC} $1" -} +# ============================================================================== +# Configuration +# ============================================================================== -# Process command line arguments UPDATE_MODE=false -if [[ "$1" == "--update" ]]; then +if [[ "${1:-}" == "--update" ]]; then UPDATE_MODE=true print_info "Running in update mode - will overwrite existing configurations" fi -# Function to backup existing configuration -backup_if_exists() { - if [ -f "$1" ] || [ -d "$1" ]; then - BACKUP_PATH="$1.backup" - print_info "Backing up existing $1 to $BACKUP_PATH" - if mv "$1" "$BACKUP_PATH" 2>/dev/null; then - print_success "Backup created: $BACKUP_PATH" - return 0 - else - print_error "Failed to create backup of $1. Check permissions." - return 2 - fi - fi - return 1 -} - -# Determine backup behavior based on update mode -if [ "$UPDATE_MODE" = true ]; then - SHOULD_BACKUP=false - print_info "Update mode: Not backing up existing configurations" -else - SHOULD_BACKUP=true +# Determine backup behavior +BACKUP_FLAG="" +if [[ "$UPDATE_MODE" != "true" ]]; then + BACKUP_FLAG="--backup" print_info "Regular mode: Will backup existing configurations" fi -# Directory where this script is located -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# ============================================================================== +# Main +# ============================================================================== -# Detect operating system -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - print_info "Detected Linux system. Setting up terminal themes..." - - # Check for desktop environment - DE="" - if [ -n "$XDG_CURRENT_DESKTOP" ]; then - DE=$XDG_CURRENT_DESKTOP - elif [ -n "$DESKTOP_SESSION" ]; then - DE=$DESKTOP_SESSION - elif [ -n "$XDG_DATA_DIRS" ]; then - DE=$(echo "$XDG_DATA_DIRS" | grep -Eo 'gnome|kde|xfce|cinnamon|mate') - fi - - # Convert to lowercase if DE is not empty - if [ -n "$DE" ]; then - DE=$(echo "$DE" | tr '[:upper:]' '[:lower:]') - else - # Try to detect by looking for common executables - if command -v gnome-shell &> /dev/null || command -v gnome-session &> /dev/null; then - DE="gnome" - elif command -v plasmashell &> /dev/null; then - DE="kde" - elif command -v xfce4-session &> /dev/null; then - DE="xfce" - else - DE="unknown" - fi - fi - - print_info "Detected desktop environment: $DE" - - # Setup based on desktop environment - if [[ "$DE" == *"gnome"* ]] || [[ "$DE" == *"unity"* ]] || [[ "$DE" == *"ubuntu"* ]]; then - print_info "Setting up GNOME Terminal..." - # Check for dconf, attempt to install if missing - if ! command -v dconf &> /dev/null; then - print_warning "dconf command not found. Attempting to install..." - if command -v apt-get &> /dev/null; then - sudo apt-get update -y - sudo apt-get install -y dconf-cli - elif command -v dnf &> /dev/null; then - sudo dnf install -y dconf - elif command -v pacman &> /dev/null; then - sudo pacman -S --noconfirm dconf - else - print_error "Could not install dconf. Please install it manually." - fi - fi - - # Try using dconf again after potential installation - if command -v dconf &> /dev/null; then - dconf load /org/gnome/terminal/legacy/profiles:/ < "$SCRIPT_DIR/gnome-terminal-catppuccin.dconf" - print_success "GNOME Terminal theme installed!" - else - print_error "dconf command not found. Unable to configure GNOME Terminal." - - # Fallback: create a simple script to apply the theme - THEME_SCRIPT="$HOME/.local/bin/apply-terminal-theme.sh" - mkdir -p "$(dirname "$THEME_SCRIPT")" - echo "#!/bin/bash" > "$THEME_SCRIPT" - echo "# Run this script to apply the Catppuccin Mocha theme to GNOME Terminal" >> "$THEME_SCRIPT" - echo "dconf load /org/gnome/terminal/legacy/profiles:/ < $SCRIPT_DIR/gnome-terminal-catppuccin.dconf" >> "$THEME_SCRIPT" - chmod +x "$THEME_SCRIPT" - print_info "Created $THEME_SCRIPT. Run it after installing dconf." - fi - elif [[ "$DE" == *"kde"* ]] || [[ "$DE" == *"plasma"* ]]; then - print_info "Setting up Konsole (KDE)..." - KONSOLE_DIR="$HOME/.local/share/konsole" - KONSOLE_THEME="$KONSOLE_DIR/Catppuccin-Mocha.colorscheme" - - # Backup existing config if option selected - if [ "$SHOULD_BACKUP" = true ] && [ -f "$KONSOLE_THEME" ]; then - backup_if_exists "$KONSOLE_THEME" - fi - - mkdir -p "$KONSOLE_DIR" - cp "$SCRIPT_DIR/Catppuccin-Mocha.colorscheme" "$KONSOLE_DIR/" - print_success "Konsole theme installed! Please go to Konsole settings to apply it." - else - print_warning "Unknown or unsupported desktop environment: $DE" - print_info "Installing only Alacritty configuration if available." - fi - - # Setup Alacritty regardless of DE (if installed) - if command -v alacritty &> /dev/null; then - print_info "Setting up Alacritty..." - ALACRITTY_DIR="$HOME/.config/alacritty" - ALACRITTY_CONFIG="$ALACRITTY_DIR/alacritty.yml" - - # Backup existing config if option selected - if [ "$SHOULD_BACKUP" = true ] && [ -f "$ALACRITTY_CONFIG" ]; then - backup_if_exists "$ALACRITTY_CONFIG" - elif [ "$SHOULD_BACKUP" = true ] && [ -d "$ALACRITTY_DIR" ]; then - backup_if_exists "$ALACRITTY_DIR" - fi - - mkdir -p "$ALACRITTY_DIR" - cp "$SCRIPT_DIR/alacritty.yml" "$ALACRITTY_CONFIG" - print_success "Alacritty configuration installed!" - else - print_info "Alacritty not found. Skipping Alacritty configuration." - fi - -elif [[ "$OSTYPE" == "darwin"* ]]; then - print_info "Detected macOS. Setting up Alacritty if installed..." - - # Setup Alacritty if it exists on macOS - if command -v alacritty &> /dev/null; then - print_info "Setting up Alacritty..." - ALACRITTY_DIR="$HOME/.config/alacritty" - ALACRITTY_CONFIG="$ALACRITTY_DIR/alacritty.yml" - - # Backup existing config if option selected - if [ "$SHOULD_BACKUP" = true ] && [ -f "$ALACRITTY_CONFIG" ]; then - backup_if_exists "$ALACRITTY_CONFIG" - elif [ "$SHOULD_BACKUP" = true ] && [ -d "$ALACRITTY_DIR" ]; then - backup_if_exists "$ALACRITTY_DIR" - fi - - mkdir -p "$ALACRITTY_DIR" - cp "$SCRIPT_DIR/alacritty.yml" "$ALACRITTY_CONFIG" - print_success "Alacritty configuration installed!" - else - print_info "Alacritty not found. Skipping Alacritty configuration." - fi - -else - print_warning "Unsupported OS detected: $OSTYPE. Terminal configuration may not work properly." -fi +main() { + terminal_setup "$DOTFILES_DIR" "$BACKUP_FLAG" +} -print_success "Terminal configuration completed!" \ No newline at end of file +main "$@" diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..c1bec59 --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +# tests/run_tests.sh - Test runner for dotfiles +# +# Runs all tests: +# 1. Shellcheck on all .sh files +# 2. Bash syntax validation +# 3. Unit tests +# +# Usage: +# ./tests/run_tests.sh [--quick] +# +# Options: +# --quick Skip shellcheck (faster, for development) + +set -euo pipefail + +# ============================================================================== +# Configuration +# ============================================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_DIR="$(dirname "$SCRIPT_DIR")" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +# Options +QUICK_MODE=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --quick) QUICK_MODE=true ;; + --help) + echo "Usage: $0 [--quick]" + echo "" + echo "Options:" + echo " --quick Skip shellcheck (faster)" + exit 0 + ;; + *) ;; + esac + shift +done + +# Track overall status +OVERALL_STATUS=0 + +# ============================================================================== +# Helper Functions +# ============================================================================== + +print_header() { + echo "" + echo -e "${BOLD}${BLUE}=== $1 ===${NC}" + echo "" +} + +print_success() { + echo -e "${GREEN}✓${NC} $1" +} + +print_error() { + echo -e "${RED}✗${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}!${NC} $1" +} + +# ============================================================================== +# Shellcheck +# ============================================================================== + +run_shellcheck() { + print_header "Running Shellcheck" + + if ! command -v shellcheck &>/dev/null; then + print_warning "shellcheck not installed, skipping" + print_warning "Install with: brew install shellcheck (macOS) or apt install shellcheck (Linux)" + return 0 + fi + + local files_checked=0 + local files_failed=0 + + while IFS= read -r -d '' file; do + files_checked=$((files_checked + 1)) + + if shellcheck --severity=warning --shell=bash \ + -e SC1090 \ + -e SC1091 \ + -e SC2034 \ + "$file" 2>/dev/null; then + print_success "$file" + else + print_error "$file" + files_failed=$((files_failed + 1)) + fi + done < <(find "$DOTFILES_DIR" -name "*.sh" -type f -not -path "*/\.*" -print0) + + echo "" + echo "Checked $files_checked files, $files_failed failed" + + if [[ $files_failed -gt 0 ]]; then + OVERALL_STATUS=1 + return 1 + fi + + return 0 +} + +# ============================================================================== +# Bash Syntax Validation +# ============================================================================== + +run_syntax_check() { + print_header "Checking Bash Syntax" + + local files_checked=0 + local files_failed=0 + + while IFS= read -r -d '' file; do + files_checked=$((files_checked + 1)) + + if bash -n "$file" 2>/dev/null; then + print_success "$file" + else + print_error "$file" + bash -n "$file" 2>&1 | head -5 + files_failed=$((files_failed + 1)) + fi + done < <(find "$DOTFILES_DIR" -name "*.sh" -type f -not -path "*/\.*" -print0) + + echo "" + echo "Checked $files_checked files, $files_failed failed" + + if [[ $files_failed -gt 0 ]]; then + OVERALL_STATUS=1 + return 1 + fi + + return 0 +} + +# ============================================================================== +# Unit Tests +# ============================================================================== + +run_unit_tests() { + print_header "Running Unit Tests" + + local tests_dir="$DOTFILES_DIR/tests" + local tests_run=0 + local tests_failed=0 + + for test_file in "$tests_dir"/test_*.sh; do + [[ ! -f "$test_file" ]] && continue + + tests_run=$((tests_run + 1)) + local test_name + test_name=$(basename "$test_file") + + echo "Running $test_name..." + if bash "$test_file"; then + print_success "$test_name" + else + print_error "$test_name" + tests_failed=$((tests_failed + 1)) + fi + echo "" + done + + if [[ $tests_run -eq 0 ]]; then + print_warning "No unit test files found" + return 0 + fi + + echo "Ran $tests_run test files, $tests_failed failed" + + if [[ $tests_failed -gt 0 ]]; then + OVERALL_STATUS=1 + return 1 + fi + + return 0 +} + +# ============================================================================== +# Library Sourcing Test +# ============================================================================== + +run_source_test() { + print_header "Testing Library Sourcing" + + # Test that all libraries can be sourced without errors + local libs=( + "lib/utils.sh" + "lib/network.sh" + "lib/backup.sh" + ) + + local modules=( + "modules/package-managers.sh" + "modules/dependencies.sh" + "modules/nodejs.sh" + "modules/neovim.sh" + "modules/zsh.sh" + "modules/link-configs.sh" + "modules/vscode.sh" + "modules/terminal.sh" + ) + + local failed=0 + + # Test sourcing all libs + for lib in "${libs[@]}"; do + local lib_path="$DOTFILES_DIR/$lib" + if [[ -f "$lib_path" ]]; then + if bash -c "source '$lib_path'" 2>/dev/null; then + print_success "$lib" + else + print_error "$lib" + failed=$((failed + 1)) + fi + else + print_warning "$lib (not found)" + fi + done + + # Test sourcing all modules (requires libs first) + for module in "${modules[@]}"; do + local module_path="$DOTFILES_DIR/$module" + if [[ -f "$module_path" ]]; then + if bash -c " + source '$DOTFILES_DIR/lib/utils.sh' + source '$DOTFILES_DIR/lib/network.sh' + source '$DOTFILES_DIR/lib/backup.sh' + source '$DOTFILES_DIR/modules/package-managers.sh' + source '$module_path' + " 2>/dev/null; then + print_success "$module" + else + print_error "$module" + failed=$((failed + 1)) + fi + else + print_warning "$module (not found)" + fi + done + + if [[ $failed -gt 0 ]]; then + OVERALL_STATUS=1 + return 1 + fi + + return 0 +} + +# ============================================================================== +# Main +# ============================================================================== + +main() { + echo -e "${BOLD}Dotfiles Test Suite${NC}" + echo "====================" + + # Always run syntax check + run_syntax_check || true + + # Run shellcheck unless in quick mode + if [[ "$QUICK_MODE" != "true" ]]; then + run_shellcheck || true + else + print_header "Skipping Shellcheck (quick mode)" + fi + + # Test library sourcing + run_source_test || true + + # Run unit tests + run_unit_tests || true + + # Summary + print_header "Summary" + + if [[ $OVERALL_STATUS -eq 0 ]]; then + echo -e "${GREEN}${BOLD}All tests passed!${NC}" + else + echo -e "${RED}${BOLD}Some tests failed!${NC}" + fi + + exit $OVERALL_STATUS +} + +main "$@" diff --git a/tests/test_utils.sh b/tests/test_utils.sh new file mode 100755 index 0000000..5bd3e3f --- /dev/null +++ b/tests/test_utils.sh @@ -0,0 +1,320 @@ +#!/usr/bin/env bash +# tests/test_utils.sh - Unit tests for lib/utils.sh +# +# Run with: ./tests/test_utils.sh +# Or via test runner: ./tests/run_tests.sh + +set -euo pipefail + +# ============================================================================== +# Test Framework +# ============================================================================== + +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +# Assert functions +assert_equals() { + local expected="$1" + local actual="$2" + local message="${3:-}" + + if [[ "$expected" == "$actual" ]]; then + return 0 + else + echo " Expected: '$expected'" + echo " Actual: '$actual'" + return 1 + fi +} + +assert_true() { + local condition="$1" + local message="${2:-}" + + if eval "$condition"; then + return 0 + else + echo " Condition failed: $condition" + return 1 + fi +} + +assert_false() { + local condition="$1" + local message="${2:-}" + + if ! eval "$condition"; then + return 0 + else + echo " Condition should have been false: $condition" + return 1 + fi +} + +assert_file_exists() { + local file="$1" + + if [[ -f "$file" ]]; then + return 0 + else + echo " File does not exist: $file" + return 1 + fi +} + +assert_file_not_exists() { + local file="$1" + + if [[ ! -f "$file" ]]; then + return 0 + else + echo " File should not exist: $file" + return 1 + fi +} + +# Test runner +run_test() { + local test_name="$1" + local test_func="$2" + + TESTS_RUN=$((TESTS_RUN + 1)) + echo -n " $test_name... " + + if $test_func; then + echo -e "${GREEN}PASS${NC}" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + echo -e "${RED}FAIL${NC}" + TESTS_FAILED=$((TESTS_FAILED + 1)) + fi +} + +# ============================================================================== +# Setup +# ============================================================================== + +# Determine script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_DIR="$(dirname "$SCRIPT_DIR")" + +# Create temp directory for tests +TEST_TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEST_TEMP_DIR"' EXIT + +# Source the library being tested +source "$DOTFILES_DIR/lib/utils.sh" + +# ============================================================================== +# Tests for check_command +# ============================================================================== + +test_check_command_finds_existing() { + check_command bash +} + +test_check_command_returns_false_for_missing() { + ! check_command nonexistent_command_12345 +} + +test_check_command_returns_error_for_empty() { + local result + result=$(check_command "" 2>&1) || true + [[ "$result" == *"No command specified"* ]] +} + +# ============================================================================== +# Tests for OS Detection +# ============================================================================== + +test_os_is_detected() { + [[ -n "$OS" ]] +} + +test_arch_is_detected() { + [[ -n "$ARCH" ]] +} + +# ============================================================================== +# Tests for Version Comparison +# ============================================================================== + +test_version_compare_equal() { + version_compare "1.0.0" "1.0.0" +} + +test_version_compare_greater() { + version_compare "2.0.0" "1.0.0" +} + +test_version_compare_less() { + ! version_compare "1.0.0" "2.0.0" +} + +test_version_compare_with_v_prefix() { + version_compare "v1.5.0" "1.4.0" +} + +# ============================================================================== +# Tests for Dry Run Mode +# ============================================================================== + +test_dry_run_safe_mkdir_no_create() { + DRY_RUN=true + local test_dir="$TEST_TEMP_DIR/dry_run_test_dir" + + safe_mkdir "$test_dir" + + # Directory should NOT be created in dry run mode + [[ ! -d "$test_dir" ]] +} + +test_dry_run_safe_symlink_no_create() { + DRY_RUN=true + local source_file="$TEST_TEMP_DIR/source_file" + local target_link="$TEST_TEMP_DIR/target_link" + + echo "test content" > "$source_file" + safe_symlink "$source_file" "$target_link" + + # Symlink should NOT be created in dry run mode + [[ ! -L "$target_link" ]] +} + +test_dry_run_safe_copy_no_create() { + DRY_RUN=true + local source_file="$TEST_TEMP_DIR/copy_source" + local target_file="$TEST_TEMP_DIR/copy_target" + + echo "test content" > "$source_file" + safe_copy "$source_file" "$target_file" + + # File should NOT be copied in dry run mode + [[ ! -f "$target_file" ]] +} + +# ============================================================================== +# Tests for File Operations (non-dry-run) +# ============================================================================== + +test_safe_mkdir_creates_directory() { + DRY_RUN=false + local test_dir="$TEST_TEMP_DIR/real_test_dir" + + safe_mkdir "$test_dir" + + [[ -d "$test_dir" ]] +} + +test_safe_symlink_creates_link() { + DRY_RUN=false + local source_file="$TEST_TEMP_DIR/link_source" + local target_link="$TEST_TEMP_DIR/link_target" + + echo "link content" > "$source_file" + safe_symlink "$source_file" "$target_link" + + [[ -L "$target_link" ]] && [[ "$(readlink "$target_link")" == "$source_file" ]] +} + +test_safe_copy_copies_file() { + DRY_RUN=false + local source_file="$TEST_TEMP_DIR/real_copy_source" + local target_file="$TEST_TEMP_DIR/real_copy_target" + + echo "copy content" > "$source_file" + safe_copy "$source_file" "$target_file" + + [[ -f "$target_file" ]] && [[ "$(cat "$target_file")" == "copy content" ]] +} + +# ============================================================================== +# Tests for Backup +# ============================================================================== + +test_backup_if_exists_creates_backup() { + DRY_RUN=false + local test_file="$TEST_TEMP_DIR/file_to_backup" + + echo "backup content" > "$test_file" + backup_if_exists "$test_file" + + # Original should be gone, backup should exist + [[ ! -f "$test_file" ]] && ls "$TEST_TEMP_DIR/file_to_backup.backup."* &>/dev/null +} + +test_backup_if_exists_returns_1_for_missing() { + DRY_RUN=false + local missing_file="$TEST_TEMP_DIR/nonexistent_file" + + local result + result=$(backup_if_exists "$missing_file" 2>&1; echo $?) + [[ "${result: -1}" == "1" ]] +} + +# ============================================================================== +# Run Tests +# ============================================================================== + +echo "" +echo "Running utils.sh unit tests..." +echo "==============================" +echo "" + +echo "check_command tests:" +run_test "finds existing commands" test_check_command_finds_existing +run_test "returns false for missing commands" test_check_command_returns_false_for_missing +run_test "returns error for empty argument" test_check_command_returns_error_for_empty + +echo "" +echo "OS detection tests:" +run_test "OS is detected" test_os_is_detected +run_test "ARCH is detected" test_arch_is_detected + +echo "" +echo "Version comparison tests:" +run_test "equal versions" test_version_compare_equal +run_test "greater version" test_version_compare_greater +run_test "lesser version" test_version_compare_less +run_test "handles v prefix" test_version_compare_with_v_prefix + +echo "" +echo "Dry run mode tests:" +run_test "safe_mkdir doesn't create in dry run" test_dry_run_safe_mkdir_no_create +run_test "safe_symlink doesn't create in dry run" test_dry_run_safe_symlink_no_create +run_test "safe_copy doesn't copy in dry run" test_dry_run_safe_copy_no_create + +echo "" +echo "File operations tests:" +run_test "safe_mkdir creates directory" test_safe_mkdir_creates_directory +run_test "safe_symlink creates link" test_safe_symlink_creates_link +run_test "safe_copy copies file" test_safe_copy_copies_file + +echo "" +echo "Backup tests:" +run_test "backup_if_exists creates backup" test_backup_if_exists_creates_backup +run_test "backup_if_exists returns 1 for missing file" test_backup_if_exists_returns_1_for_missing + +# ============================================================================== +# Summary +# ============================================================================== + +echo "" +echo "==============================" +echo "Tests: $TESTS_RUN | Passed: $TESTS_PASSED | Failed: $TESTS_FAILED" + +if [[ $TESTS_FAILED -gt 0 ]]; then + echo -e "${RED}Some tests failed!${NC}" + exit 1 +else + echo -e "${GREEN}All tests passed!${NC}" + exit 0 +fi diff --git a/vscode/settings.json.template b/vscode/settings.json.template new file mode 100644 index 0000000..8b07b23 --- /dev/null +++ b/vscode/settings.json.template @@ -0,0 +1,59 @@ +{ + // Editor settings + "editor.fontFamily": "JetBrains Mono, Menlo, Monaco, 'Courier New', monospace", + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.renderWhitespace": "selection", + "editor.rulers": [ + 80, + 120 + ], + "editor.minimap.enabled": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + // Appearance + "workbench.colorTheme": "Catppuccin Mocha", + // Terminal settings + "terminal.integrated.fontSize": 12, + "terminal.integrated.defaultProfile.osx": "zsh", + "terminal.integrated.defaultProfile.linux": "zsh", + // File handling + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.autoSave": "onFocusChange", + // Privacy + "telemetry.telemetryLevel": "off", + // Remote development + "remote.SSH.connectTimeout": 1800, + // UI preferences + "chat.commandCenter.enabled": false, + // Code Spell Checker settings + "cSpell.enabled": true, + "cSpell.language": "en", + "cSpell.enableFiletypes": [ + "markdown", + "plaintext", + "yaml", + "json", + "javascript", + "typescript", + "python", + "go", + "shellscript" + ], + "cSpell.diagnosticLevel": "Information", + "cSpell.ignorePaths": [ + "node_modules", + "vscode-extension", + ".git", + "*.lock" + ], + // Markdown Table Formatter settings + "markdown-table-formatter.limitLastColumnLength": "Follow header row length", + "files.associations": { + "*.mdx": "markdown" + }, + "claudeCode.preferredLocation": "panel" +} diff --git a/vscode/settings.local.json.template b/vscode/settings.local.json.template new file mode 100644 index 0000000..52f0dea --- /dev/null +++ b/vscode/settings.local.json.template @@ -0,0 +1,25 @@ +{ + // ========================================================================== + // Personal/Machine-Specific VSCode Settings + // ========================================================================== + // + // This file is for settings that are specific to your machine or contain + // personal information (like SSH remote hosts). + // + // Copy this file to your VSCode User directory as settings.local.json + // and customize as needed. This file is gitignored. + // + // Location: + // macOS: ~/Library/Application Support/Code/User/settings.local.json + // Linux: ~/.config/Code/User/settings.local.json + // + // Note: VSCode doesn't natively support settings.local.json, but you can + // use the "Settings Sync" extension to manage machine-specific overrides, + // or manually merge these settings into your settings.json. + // ========================================================================== + + // Remote SSH platforms (add your remote hosts here) + // "remote.SSH.remotePlatform": { + // "your-remote-host": "linux" + // } +} From 65a26a3a2ecda8df53b36bf78a321a981c0e25e1 Mon Sep 17 00:00:00 2001 From: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:41:40 -0500 Subject: [PATCH 2/6] fix: resolve shellcheck errors and improve dry-run support - lib/utils.sh: Declare and assign backup_path separately (SC2155) - modules/neovim.sh: Add SC2120 disable for optional args - modules/nodejs.sh: Add SC2120 disable for optional args - nvim/install-nvim.sh: Remove 'local' outside function (SC2168) - fonts/install-fonts.sh: Add --dry-run flag and proper dry-run checks - github/install-github-cli.sh: Add --dry-run flag and proper dry-run checks - install.sh: Pass --dry-run to child scripts when in dry-run mode Co-Authored-By: Claude Opus 4.5 --- fonts/install-fonts.sh | 19 +++++++++++++++---- github/install-github-cli.sh | 17 +++++++++++++++++ install.sh | 21 +++++++++------------ lib/utils.sh | 3 ++- modules/neovim.sh | 1 + modules/nodejs.sh | 1 + nvim/install-nvim.sh | 2 +- 7 files changed, 46 insertions(+), 18 deletions(-) diff --git a/fonts/install-fonts.sh b/fonts/install-fonts.sh index c6ae175..ca51b35 100755 --- a/fonts/install-fonts.sh +++ b/fonts/install-fonts.sh @@ -24,10 +24,14 @@ source "$DOTFILES_DIR/lib/network.sh" # Process command line arguments UPDATE_MODE=false -if [[ "${1:-}" == "--update" ]]; then - UPDATE_MODE=true - print_info "Running in update mode - will only install missing fonts" -fi +while [[ "$#" -gt 0 ]]; do + case $1 in + --update) UPDATE_MODE=true; print_info "Running in update mode - will only install missing fonts" ;; + --dry-run) DRY_RUN=true ;; + *) ;; + esac + shift +done # ============================================================================== # Dependency Check @@ -109,6 +113,13 @@ install_jetbrains_mono() { print_info "Using JetBrains Mono Nerd Font ${version}" + # Dry-run mode - just show what would happen + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "download JetBrains Mono Nerd Font ${version}" + print_dry_run "install fonts to $font_install_dir" + return 0 + fi + # Create temp directory local temp_dir temp_dir=$(mktemp -d) diff --git a/github/install-github-cli.sh b/github/install-github-cli.sh index b7002b0..6a6c0dc 100755 --- a/github/install-github-cli.sh +++ b/github/install-github-cli.sh @@ -33,12 +33,14 @@ while [[ "$#" -gt 0 ]]; do --no-auth) AUTH_MODE=false ;; --non-interactive) NON_INTERACTIVE=true ;; --update) UPDATE_MODE=true ;; + --dry-run) DRY_RUN=true ;; --help) echo "Usage: $0 [options]" echo "Options:" echo " --no-auth Skip authentication steps" echo " --non-interactive Run without prompting (for automated scripts)" echo " --update Only update if already installed" + echo " --dry-run Show what would be done without making changes" echo " --help Show this help message" exit 0 ;; @@ -321,6 +323,21 @@ configure_gh() { # ============================================================================== main() { + # Dry-run mode - just show what would happen + if [[ "$DRY_RUN" == "true" ]]; then + if check_command gh; then + print_success "GitHub CLI is already installed!" + gh --version + if [[ "$UPDATE_MODE" == "true" ]]; then + print_dry_run "update GitHub CLI" + fi + else + print_dry_run "install GitHub CLI" + fi + print_dry_run "configure GitHub CLI" + return 0 + fi + if check_command gh; then if [[ "$UPDATE_MODE" == "true" ]]; then update_gh diff --git a/install.sh b/install.sh index 3bb430a..1e7431e 100755 --- a/install.sh +++ b/install.sh @@ -242,11 +242,10 @@ run_installation() { # Fonts if [[ "$SKIP_FONTS" != "true" ]]; then print_section "Installing Fonts" - if [[ "$UPDATE_MODE" == "true" ]]; then - "$DOTFILES_DIR/fonts/install-fonts.sh" --update - else - "$DOTFILES_DIR/fonts/install-fonts.sh" - fi + local font_args=() + [[ "$UPDATE_MODE" == "true" ]] && font_args+=(--update) + [[ "$DRY_RUN" == "true" ]] && font_args+=(--dry-run) + "$DOTFILES_DIR/fonts/install-fonts.sh" "${font_args[@]}" fi # Terminal @@ -257,13 +256,11 @@ run_installation() { # GitHub CLI if [[ -f "$DOTFILES_DIR/github/install-github-cli.sh" ]]; then print_section "Setting up GitHub CLI" - if [[ "$UPDATE_MODE" == "true" ]]; then - "$DOTFILES_DIR/github/install-github-cli.sh" --update --non-interactive || \ - print_warning "GitHub CLI setup had issues (non-fatal)" - else - "$DOTFILES_DIR/github/install-github-cli.sh" --non-interactive || \ - print_warning "GitHub CLI setup had issues (non-fatal)" - fi + local gh_args=(--non-interactive) + [[ "$UPDATE_MODE" == "true" ]] && gh_args+=(--update) + [[ "$DRY_RUN" == "true" ]] && gh_args+=(--dry-run) + "$DOTFILES_DIR/github/install-github-cli.sh" "${gh_args[@]}" || \ + print_warning "GitHub CLI setup had issues (non-fatal)" fi # Cleanup old backups (keep last 5) diff --git a/lib/utils.sh b/lib/utils.sh index c06dc59..cbb389c 100755 --- a/lib/utils.sh +++ b/lib/utils.sh @@ -336,7 +336,8 @@ safe_remove() { # Returns: 0 if backup created, 1 if path didn't exist, 2 on error backup_if_exists() { local path="$1" - local backup_path="${path}.backup.$(date +%Y%m%d_%H%M%S)" + local backup_path + backup_path="${path}.backup.$(date +%Y%m%d_%H%M%S)" if [[ ! -e "$path" ]]; then print_debug "Nothing to backup (doesn't exist): $path" diff --git a/modules/neovim.sh b/modules/neovim.sh index 87875d5..0936e28 100755 --- a/modules/neovim.sh +++ b/modules/neovim.sh @@ -49,6 +49,7 @@ get_nvim_version() { # Check if Neovim version meets minimum requirement # Usage: check_nvim_version [minimum_version] +# shellcheck disable=SC2120 check_nvim_version() { local min_version="${1:-$NVIM_MIN_VERSION}" local current_version diff --git a/modules/nodejs.sh b/modules/nodejs.sh index a61092e..ee377bd 100755 --- a/modules/nodejs.sh +++ b/modules/nodejs.sh @@ -127,6 +127,7 @@ setup_nvm() { # Install Node.js via NVM # Usage: install_node_via_nvm [version] +# shellcheck disable=SC2120 install_node_via_nvm() { local version="${1:-lts/*}" diff --git a/nvim/install-nvim.sh b/nvim/install-nvim.sh index 589f406..8100af5 100644 --- a/nvim/install-nvim.sh +++ b/nvim/install-nvim.sh @@ -107,7 +107,7 @@ if [ -d "$EXTRACT_DIR" ]; then export PATH="$HOME/.local/bin:$PATH" # Add to shell config - prefer .zshrc.local if it exists, otherwise .zshrc or .bashrc - local PATH_EXPORT='export PATH="$HOME/.local/bin:$PATH"' + PATH_EXPORT='export PATH="$HOME/.local/bin:$PATH"' if [ -f "$HOME/.zshrc.local" ]; then if ! grep -q '.local/bin' "$HOME/.zshrc.local" 2>/dev/null; then echo "$PATH_EXPORT" >> "$HOME/.zshrc.local" From 828bb15417487aa4999a3102b773b80351b0eb8a Mon Sep 17 00:00:00 2001 From: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:49:53 -0500 Subject: [PATCH 3/6] fix: skip neovim verification in dry-run mode In dry-run mode, the neovim installation functions return success after printing what they would do, but the verification check fails because nvim isn't actually installed. Skip the verification step in dry-run mode to allow the dry-run to complete successfully. Co-Authored-By: Claude Opus 4.5 --- modules/neovim.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/neovim.sh b/modules/neovim.sh index 0936e28..e02ec5e 100755 --- a/modules/neovim.sh +++ b/modules/neovim.sh @@ -486,6 +486,17 @@ install_neovim() { fi fi + # In dry-run mode, skip verification + if [[ "$DRY_RUN" == "true" ]]; then + install_vim_plug + if [[ -z "$skip_plugins" ]]; then + install_nvim_plugins + install_coc_extensions + fi + print_success "Neovim setup complete" + return 0 + fi + # Verify installation if ! check_command nvim; then print_error "Neovim installation failed" From 37bd8fa8391d5ef03e8d10e175b16d37810e9b61 Mon Sep 17 00:00:00 2001 From: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:51:21 -0500 Subject: [PATCH 4/6] fix: move dry-run checks before nvim functional checks The install_nvim_plugins and install_coc_extensions functions were checking if neovim was functional before checking dry-run mode. This caused failures in dry-run mode when neovim isn't installed. Move dry-run checks before functional checks so these functions return success in dry-run mode without needing a working neovim installation. Co-Authored-By: Claude Opus 4.5 --- modules/neovim.sh | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/modules/neovim.sh b/modules/neovim.sh index e02ec5e..3c69860 100755 --- a/modules/neovim.sh +++ b/modules/neovim.sh @@ -346,6 +346,14 @@ install_vim_plug() { install_nvim_plugins() { local update_mode="${1:-}" + print_info "Installing Neovim plugins..." + + # Check dry-run mode first + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "install/update Neovim plugins" + return 0 + fi + if ! is_nvim_functional; then print_warning "Neovim not functional, skipping plugin installation" return 1 @@ -357,13 +365,6 @@ install_nvim_plugins() { return 1 fi - print_info "Installing Neovim plugins..." - - if [[ "$DRY_RUN" == "true" ]]; then - print_dry_run "install/update Neovim plugins" - return 0 - fi - if [[ "$update_mode" == "--update" ]]; then nvim --headless +PlugUpdate +qall 2>/dev/null || true print_success "Neovim plugins updated" @@ -425,17 +426,18 @@ install_coc_extensions() { return 0 fi - if ! is_nvim_functional; then - return 1 - fi - print_info "Installing CoC extensions..." + # Check dry-run mode before functional check if [[ "$DRY_RUN" == "true" ]]; then print_dry_run "install CoC extensions" return 0 fi + if ! is_nvim_functional; then + return 1 + fi + if [[ "$update_mode" == "--update" ]]; then nvim --headless +"CocUpdate" +qall 2>/dev/null || true print_success "CoC extensions updated" From 81a63fd908022a2f4a59f9996bba43e72ae56d61 Mon Sep 17 00:00:00 2001 From: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:52:51 -0500 Subject: [PATCH 5/6] fix: move dry-run check before zsh path check The set_zsh_as_default function was checking if zsh exists before checking dry-run mode. On systems without zsh (like fresh Ubuntu), this caused failures in dry-run mode. Co-Authored-By: Claude Opus 4.5 --- modules/zsh.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/zsh.sh b/modules/zsh.sh index 096f058..099edd9 100755 --- a/modules/zsh.sh +++ b/modules/zsh.sh @@ -292,6 +292,12 @@ setup_p10k_config() { set_zsh_as_default() { local zsh_path + # Check dry-run mode first + if [[ "$DRY_RUN" == "true" ]]; then + print_dry_run "set Zsh as default shell" + return 0 + fi + # Get Zsh path zsh_path=$(which zsh 2>/dev/null) @@ -308,11 +314,6 @@ set_zsh_as_default() { print_info "Setting Zsh as default shell..." - if [[ "$DRY_RUN" == "true" ]]; then - print_dry_run "set $zsh_path as default shell" - return 0 - fi - # Ensure Zsh is in /etc/shells if ! grep -q "$zsh_path" /etc/shells 2>/dev/null; then print_info "Adding Zsh to /etc/shells..." From 0fe8035fe20dc48c18a77f1085672b5377d5a93b Mon Sep 17 00:00:00 2001 From: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:56:41 -0500 Subject: [PATCH 6/6] chore: remove GitHub Actions workflows CI is overkill for a personal dotfiles repo. The dry-run flag and local test runner (tests/run_tests.sh) provide sufficient testing. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/shellcheck.yml | 58 ---------- .github/workflows/test-install.yml | 175 ----------------------------- 2 files changed, 233 deletions(-) delete mode 100644 .github/workflows/shellcheck.yml delete mode 100644 .github/workflows/test-install.yml diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml deleted file mode 100644 index 279ddc3..0000000 --- a/.github/workflows/shellcheck.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Shellcheck - -on: - push: - branches: [main] - paths: - - '**.sh' - - '.github/workflows/shellcheck.yml' - pull_request: - branches: [main] - paths: - - '**.sh' - - '.github/workflows/shellcheck.yml' - -jobs: - shellcheck: - name: Shellcheck - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install shellcheck - run: | - sudo apt-get update - sudo apt-get install -y shellcheck - - - name: Run shellcheck on all shell scripts - run: | - echo "Finding shell scripts..." - find . -name "*.sh" -type f | head -20 - - echo "" - echo "Running shellcheck..." - find . -name "*.sh" -type f -print0 | \ - xargs -0 shellcheck --severity=warning --shell=bash \ - -e SC1090 \ - -e SC1091 \ - -e SC2034 - - - name: Verify bash syntax - run: | - echo "Checking bash syntax..." - errors=0 - while IFS= read -r -d '' file; do - if ! bash -n "$file" 2>&1; then - echo "Syntax error in: $file" - errors=$((errors + 1)) - fi - done < <(find . -name "*.sh" -type f -print0) - - if [ $errors -gt 0 ]; then - echo "Found $errors files with syntax errors" - exit 1 - fi - - echo "All shell scripts have valid syntax" diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml deleted file mode 100644 index 34919db..0000000 --- a/.github/workflows/test-install.yml +++ /dev/null @@ -1,175 +0,0 @@ -name: Test Installation - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test-dry-run: - name: Test Dry Run - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run dry-run installation - run: | - chmod +x install.sh - ./install.sh --dry-run - - - name: Verify no changes were made - run: | - # Check that no config files were created - if [ -L "$HOME/.zshrc" ]; then - echo "ERROR: .zshrc symlink was created in dry-run mode" - exit 1 - fi - - if [ -d "$HOME/.config/nvim" ]; then - echo "ERROR: nvim config was created in dry-run mode" - exit 1 - fi - - echo "Dry run verified - no changes made" - - test-install-ubuntu: - name: Test Full Installation (Ubuntu) - runs-on: ubuntu-latest - needs: test-dry-run - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y curl git zsh - - - name: Run installation - run: | - chmod +x install.sh - ./install.sh --skip-terminal --skip-vscode - - - name: Verify symlinks created - run: | - echo "Checking symlinks..." - - if [ ! -L "$HOME/.zshrc" ]; then - echo "ERROR: .zshrc symlink not created" - exit 1 - fi - echo "✓ .zshrc symlink exists" - - if [ ! -f "$HOME/.config/nvim/init.vim" ]; then - echo "ERROR: nvim config not created" - exit 1 - fi - echo "✓ nvim config exists" - - if [ ! -d "$HOME/.oh-my-zsh" ]; then - echo "ERROR: oh-my-zsh not installed" - exit 1 - fi - echo "✓ oh-my-zsh installed" - - echo "All verifications passed!" - - - name: Verify Neovim works - run: | - if command -v nvim &>/dev/null; then - nvim --version - echo "✓ Neovim is functional" - else - echo "Neovim not in PATH (may be in ~/.local/bin)" - if [ -f "$HOME/.local/bin/nvim" ]; then - "$HOME/.local/bin/nvim" --version - echo "✓ Neovim found in ~/.local/bin" - fi - fi - - test-install-macos: - name: Test Full Installation (macOS) - runs-on: macos-latest - needs: test-dry-run - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run installation - run: | - chmod +x install.sh - ./install.sh --skip-terminal --skip-vscode --skip-fonts - - - name: Verify symlinks created - run: | - echo "Checking symlinks..." - - if [ ! -L "$HOME/.zshrc" ]; then - echo "ERROR: .zshrc symlink not created" - exit 1 - fi - echo "✓ .zshrc symlink exists" - - if [ ! -f "$HOME/.config/nvim/init.vim" ]; then - echo "ERROR: nvim config not created" - exit 1 - fi - echo "✓ nvim config exists" - - echo "All verifications passed!" - - test-rollback: - name: Test Rollback - runs-on: ubuntu-latest - needs: test-dry-run - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y curl git zsh - - - name: Create pre-existing config - run: | - mkdir -p "$HOME/.config/nvim" - echo "original config" > "$HOME/.config/nvim/init.vim" - echo "original zshrc" > "$HOME/.zshrc" - - - name: Run installation - run: | - chmod +x install.sh - ./install.sh --skip-terminal --skip-vscode --skip-fonts - - - name: Verify backups were created - run: | - if [ ! -d "$HOME/.dotfiles-backups" ]; then - echo "ERROR: Backup directory not created" - exit 1 - fi - echo "✓ Backup directory exists" - - # Check for backup session - sessions=$(ls -1 "$HOME/.dotfiles-backups" | wc -l) - if [ "$sessions" -eq 0 ]; then - echo "ERROR: No backup sessions found" - exit 1 - fi - echo "✓ Found $sessions backup session(s)" - - - name: Test rollback (dry-run) - run: | - # Run rollback in dry-run mode - DRY_RUN=true ./install.sh --rollback <<< "y" || true - echo "✓ Rollback dry-run completed"