From 66cbb97f916e183ed91f79d7614801f5087d53e4 Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Mon, 4 Aug 2025 19:16:02 -0500 Subject: [PATCH 1/8] feat: restructure dotfiles with GNU Stow organization - Reorganize dotfiles into stow/ subdirectories (bash/, git/) - Add package management system with categorized package lists - Enhance install.sh with interactive package selection - Fix package selection logic and improve error handling - Update dotpull alias to work with new stow structure - Add comprehensive documentation in README.md Breaking Changes: - Dotfiles moved from root to stow/ subdirectories - Install script now requires user interaction for package selection - Stow commands now target individual config directories --- README.md | 56 +++++++++- install.sh | 130 +++++++++++++++++++---- packages/base.txt | 13 +++ packages/cli-tools.txt | 11 ++ packages/dev.txt | 15 +++ packages/gui.txt | 10 ++ .bash_aliases => stow/bash/.bash_aliases | 2 +- .bashrc => stow/bash/.bashrc | 0 .inputrc => stow/bash/.inputrc | 0 .gitconfig => stow/git/.gitconfig | 0 10 files changed, 216 insertions(+), 21 deletions(-) create mode 100644 packages/base.txt create mode 100644 packages/cli-tools.txt create mode 100644 packages/dev.txt create mode 100644 packages/gui.txt rename .bash_aliases => stow/bash/.bash_aliases (86%) rename .bashrc => stow/bash/.bashrc (100%) rename .inputrc => stow/bash/.inputrc (100%) rename .gitconfig => stow/git/.gitconfig (100%) diff --git a/README.md b/README.md index 01dee29..5db55be 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,62 @@ # Ryan's Dotfiles -Minimal Bash dotfiles for SSH and remote environments. +A comprehensive dotfiles repository for SSH, remote environments, and development setups. + +## Structure + +``` +~/.dotfiles/ +├── stow/ # Configs organized for GNU Stow +│ ├── bash/ # Bash configuration (.bashrc, .bash_aliases, .inputrc) +│ ├── git/ # Git configuration (.gitconfig) +│ └── [other configs] # Additional configurations as needed +├── packages/ +│ ├── base.txt # MUST HAVE - Core system utilities +│ ├── cli-tools.txt # Nice-to-haves - Enhanced CLI tools +│ ├── dev.txt # Development tools (docker, node, etc) +│ └── gui.txt # Desktop applications +├── install.sh # Bootstrap script with package management +└── README.md # Documentation +``` ## Install ```bash bash <(curl -sL https://ryans.tools/dotfiles) +``` + +The installer will: + +1. Clone this repository to `~/.dotfiles` +2. Prompt you to choose which package sets to install +3. Backup any existing conflicting files +4. Use GNU Stow to symlink configurations +5. Set up bash aliases including the `dotpull` update alias + +## Package Categories + +- **Base**: Essential system utilities (curl, git, stow, vim, etc.) +- **CLI Tools**: Enhanced command-line tools (bat, fzf, ripgrep, etc.) +- **Development**: Programming languages and dev tools (nodejs, python, docker, etc.) +- **GUI**: Desktop applications (firefox, code, vlc, etc.) + +## Updating + +After initial installation, use the `dotpull` alias to update your dotfiles: + +```bash +dotpull +``` + +This alias will pull the latest changes and re-stow all configurations. + +## Manual Management + +To manage individual configurations: + +```bash +cd ~/.dotfiles/stow +stow -t ~ bash # Link bash config +stow -t ~ git # Link git config +stow -D bash # Unlink bash config +``` diff --git a/install.sh b/install.sh index 2da1367..c4a2832 100644 --- a/install.sh +++ b/install.sh @@ -4,6 +4,50 @@ set -euo pipefail DOTFILES_REPO="https://gitea.purpleraft.com/ryan/dotfiles" DOTFILES_DIR="$HOME/.dotfiles" +# Function to install packages from a file +install_packages() { + local package_file="$1" + local description="$2" + + if [ -f "$package_file" ]; then + echo "📦 Installing $description..." + + # Read packages into array, skipping comments and empty lines + packages=() + while IFS= read -r line; do + # Skip comments and empty lines + [[ "$line" =~ ^[[:space:]]*# ]] && continue + [[ -z "${line// }" ]] && continue + packages+=("$line") + done < "$package_file" + + if [ ${#packages[@]} -eq 0 ]; then + echo " No packages found in $package_file" + return + fi + + # Detect package manager and install packages + if command -v apt &> /dev/null; then + # Debian/Ubuntu + sudo apt update && sudo apt install -y "${packages[@]}" + elif command -v yum &> /dev/null; then + # RHEL/CentOS + sudo yum install -y "${packages[@]}" + elif command -v dnf &> /dev/null; then + # Fedora + sudo dnf install -y "${packages[@]}" + elif command -v pacman &> /dev/null; then + # Arch Linux + sudo pacman -S --noconfirm "${packages[@]}" + elif command -v brew &> /dev/null; then + # macOS + brew install "${packages[@]}" + else + echo "⚠️ No supported package manager found. Please install packages manually from $package_file" + fi + fi +} + # Check if stow is installed if ! command -v stow &> /dev/null; then echo "❌ GNU Stow is not installed. Please install it first:" @@ -15,33 +59,81 @@ fi # Clone or update the repo if [ -d "$DOTFILES_DIR/.git" ]; then - echo "Updating existing dotfiles repo..." + echo "🔄 Updating existing dotfiles repo..." git -C "$DOTFILES_DIR" pull --quiet else - echo "Cloning dotfiles into $DOTFILES_DIR..." + echo "📥 Cloning dotfiles into $DOTFILES_DIR..." git clone "$DOTFILES_REPO" "$DOTFILES_DIR" fi -# Backup existing files that would conflict with stow (excluding SSH config - it's machine-specific) -for file in .bashrc .bash_aliases .inputrc .gitconfig; do - if [ -f "$HOME/$file" ] && [ ! -L "$HOME/$file" ]; then - echo "⚠️ Backing up existing file: $HOME/$file -> $HOME/${file}.bak" - mv "$HOME/$file" "$HOME/${file}.bak" - fi +cd "$DOTFILES_DIR" + +# Install packages (with user confirmation) +echo "" +echo "Package installation options:" +echo "1. Base packages (MUST HAVE) - curl, git, stow, vim, etc." +echo "2. CLI tools (Nice-to-haves) - bat, fzf, ripgrep, etc." +echo "3. Development tools - nodejs, python, docker, etc." +echo "4. GUI applications - firefox, code, vlc, etc." +echo "5. Skip package installation" +echo "" + +read -p "Choose packages to install (1-5, or comma-separated like 1,2): " package_choice + +# Install selected packages +if [[ "$package_choice" == *"1"* ]]; then + install_packages "packages/base.txt" "base packages" +fi +if [[ "$package_choice" == *"2"* ]]; then + install_packages "packages/cli-tools.txt" "CLI tools" +fi +if [[ "$package_choice" == *"3"* ]]; then + install_packages "packages/dev.txt" "development tools" +fi +if [[ "$package_choice" == *"4"* ]]; then + install_packages "packages/gui.txt" "GUI applications" +fi + +# Backup existing files that would conflict with stow +echo "" +echo "🔗 Setting up dotfile symlinks..." +for config_dir in stow/*/; do + if [ -d "$config_dir" ]; then + config_name=$(basename "$config_dir") + echo " Checking for conflicts with $config_name config..." + + # Find all files that would be stowed and backup if they exist + find "$config_dir" -type f | while read -r file; do + relative_file="${file#$config_dir}" + target_file="$HOME/$relative_file" + + if [ -f "$target_file" ] && [ ! -L "$target_file" ]; then + echo " ⚠️ Backing up existing file: $target_file -> ${target_file}.bak" + mv "$target_file" "${target_file}.bak" + fi + done + fi done -# Change to dotfiles directory and use stow to create symlinks -cd "$DOTFILES_DIR" -echo "🔗 Using Stow to symlink dotfiles..." -if ! stow --adopt -t "$HOME" . 2>/dev/null; then - echo "🔄 Adopting failed, trying regular stow..." - stow -t "$HOME" . -fi +# Use stow to create symlinks for each configuration +cd stow +for config_dir in */; do + if [ -d "$config_dir" ]; then + config_name=$(basename "$config_dir") + echo " 🔗 Stowing $config_name configuration..." + if ! stow -t "$HOME" "$config_name" 2>/dev/null; then + echo " ⚠️ Stow failed for $config_name, trying with --adopt..." + stow --adopt -t "$HOME" "$config_name" + fi + fi +done # Optionally source the new bashrc -if [[ $- == *i* ]]; then - echo "Reloading Bash config..." - source ~/.bashrc +if [[ $- == *i* ]] && [ -f "$HOME/.bashrc" ]; then + echo "🔄 Reloading Bash config..." + source ~/.bashrc fi -echo "✅ Dotfiles install complete." +echo "" +echo "✅ Dotfiles install complete!" +echo "💡 Use 'dotpull' alias to update your dotfiles in the future." diff --git a/packages/base.txt b/packages/base.txt new file mode 100644 index 0000000..8131853 --- /dev/null +++ b/packages/base.txt @@ -0,0 +1,13 @@ +# Base packages - MUST HAVE +# Core system utilities +curl +wget +git +stow +vim +nano +bash-completion +tree +htop +unzip +zip diff --git a/packages/cli-tools.txt b/packages/cli-tools.txt new file mode 100644 index 0000000..1b3f2c0 --- /dev/null +++ b/packages/cli-tools.txt @@ -0,0 +1,11 @@ +# CLI Tools - Nice-to-haves +# Enhanced command line tools +bat +fd-find +ripgrep +fzf +tmux +screen +jq +ncdu +exa diff --git a/packages/dev.txt b/packages/dev.txt new file mode 100644 index 0000000..d70da8a --- /dev/null +++ b/packages/dev.txt @@ -0,0 +1,15 @@ +# Development Tools +# Programming languages and tools +nodejs +npm +python3 +python3-pip +docker +docker-compose +build-essential +make +gcc +g++ +golang-go +rust +cargo diff --git a/packages/gui.txt b/packages/gui.txt new file mode 100644 index 0000000..ad45e98 --- /dev/null +++ b/packages/gui.txt @@ -0,0 +1,10 @@ +# GUI Applications +# Desktop applications (for systems with GUI) +firefox +code +vlc +gimp +thunderbird +libreoffice +chromium-browser +terminator diff --git a/.bash_aliases b/stow/bash/.bash_aliases similarity index 86% rename from .bash_aliases rename to stow/bash/.bash_aliases index 1165872..7d11949 100644 --- a/.bash_aliases +++ b/stow/bash/.bash_aliases @@ -7,7 +7,7 @@ alias h='history' alias grep='grep --color=auto' alias d='docker' alias dc='docker compose' -alias dotpull='echo "🔄 Updating dotfiles..." && git -C ~/.dotfiles pull && echo "🔗 Re-stowing dotfiles..." && (cd ~/.dotfiles && stow --adopt -t ~ . 2>/dev/null || stow -t ~ .) && echo "✅ Done."' +alias dotpull='echo "🔄 Updating dotfiles..." && git -C ~/.dotfiles pull && echo "🔗 Re-stowing dotfiles..." && (cd ~/.dotfiles/stow && for dir in */; do if [ -d "$dir" ]; then echo " Stowing $dir..." && stow -R -t ~ "$dir" || echo " ⚠️ Failed to stow $dir"; fi; done) && echo "✅ Done."' alias reloadbash='source ~/.bashrc && echo "Bash config reloaded."' diff --git a/.bashrc b/stow/bash/.bashrc similarity index 100% rename from .bashrc rename to stow/bash/.bashrc diff --git a/.inputrc b/stow/bash/.inputrc similarity index 100% rename from .inputrc rename to stow/bash/.inputrc diff --git a/.gitconfig b/stow/git/.gitconfig similarity index 100% rename from .gitconfig rename to stow/git/.gitconfig From c11d8464028bf46e1f20bc88e651bda26ea8b774 Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Mon, 4 Aug 2025 19:19:40 -0500 Subject: [PATCH 2/8] feat: enhance branch selection in install script with user prompts and default handling --- install.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index c4a2832..34d3bb9 100644 --- a/install.sh +++ b/install.sh @@ -4,6 +4,51 @@ set -euo pipefail DOTFILES_REPO="https://gitea.purpleraft.com/ryan/dotfiles" DOTFILES_DIR="$HOME/.dotfiles" +# Handle branch selection +if [ $# -eq 0 ]; then + echo "🌿 Fetching available branches..." + + # Get list of remote branches + branches=$(git ls-remote --heads "$DOTFILES_REPO" | sed 's/.*refs\/heads\///' | sort) + + if [ -z "$branches" ]; then + echo "⚠️ Could not fetch branches, using default 'main'" + BRANCH="main" + else + echo "Available branches:" + branch_array=() + i=1 + while IFS= read -r branch; do + echo "$i. $branch" + branch_array+=("$branch") + ((i++)) + done <<< "$branches" + + echo "$i. Enter custom branch name" + echo "" + read -p "Choose branch (1-$i) [default: main]: " branch_choice + + if [[ "$branch_choice" =~ ^[0-9]+$ ]] && [ "$branch_choice" -ge 1 ] && [ "$branch_choice" -lt "$i" ]; then + # Valid branch selection + BRANCH="${branch_array[$((branch_choice-1))]}" + elif [ "$branch_choice" -eq "$i" ]; then + # Custom branch name + read -p "Enter branch name: " custom_branch + BRANCH="$custom_branch" + else + # Default or invalid choice + if [[ " ${branch_array[*]} " =~ " main " ]]; then + BRANCH="main" + else + BRANCH="${branch_array[0]}" # Use first available branch + fi + echo "Using default branch: $BRANCH" + fi + fi +else + BRANCH="$1" +fi + # Function to install packages from a file install_packages() { local package_file="$1" @@ -57,13 +102,17 @@ if ! command -v stow &> /dev/null; then exit 1 fi +echo "🚀 Installing dotfiles from branch: $BRANCH" + # Clone or update the repo if [ -d "$DOTFILES_DIR/.git" ]; then echo "🔄 Updating existing dotfiles repo..." + git -C "$DOTFILES_DIR" fetch --quiet + git -C "$DOTFILES_DIR" checkout "$BRANCH" --quiet git -C "$DOTFILES_DIR" pull --quiet else echo "📥 Cloning dotfiles into $DOTFILES_DIR..." - git clone "$DOTFILES_REPO" "$DOTFILES_DIR" + git clone -b "$BRANCH" "$DOTFILES_REPO" "$DOTFILES_DIR" fi cd "$DOTFILES_DIR" From f824ca52d3eaa49f4762a960c65b40d9311786b3 Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Mon, 4 Aug 2025 20:39:16 -0500 Subject: [PATCH 3/8] feat: restructure dotfiles setup with bootstrap and setup scripts, enhance branch selection and backup processes --- README.md | 32 +++++++-- bootstrap.sh | 71 +++++++++++++++++++ install.sh | 51 -------------- scripts/05-branch-selection.sh | 100 ++++++++++++++++++++++++++ scripts/10-backup-files.sh | 42 ++++++++--- scripts/20-setup-stow.sh | 45 +++++++++--- scripts/30-install-packages.sh | 125 +++++++++++++++++++++++++++++++++ scripts/utils.sh | 46 ++++++++++++ setup.sh | 56 +++++++++++++++ 9 files changed, 493 insertions(+), 75 deletions(-) create mode 100644 bootstrap.sh delete mode 100644 install.sh create mode 100644 scripts/05-branch-selection.sh create mode 100644 scripts/30-install-packages.sh create mode 100644 setup.sh diff --git a/README.md b/README.md index 5db55be..d8b080c 100644 --- a/README.md +++ b/README.md @@ -15,23 +15,43 @@ A comprehensive dotfiles repository for SSH, remote environments, and developmen │ ├── cli-tools.txt # Nice-to-haves - Enhanced CLI tools │ ├── dev.txt # Development tools (docker, node, etc) │ └── gui.txt # Desktop applications -├── install.sh # Bootstrap script with package management +├── bootstrap.sh # Get repo and select branch +├── setup.sh # Main orchestrator - runs all configuration └── README.md # Documentation ``` ## Install +**From internet:** + ```bash bash <(curl -sL https://ryans.tools/dotfiles) ``` +**From local copy (USB drive, etc.):** + +```bash +./bootstrap.sh +``` + The installer will: -1. Clone this repository to `~/.dotfiles` -2. Prompt you to choose which package sets to install -3. Backup any existing conflicting files -4. Use GNU Stow to symlink configurations -5. Set up bash aliases including the `dotpull` update alias +1. **Bootstrap:** Clone repository (or use local copy), select branch if desired +2. **Setup:** Check dependencies, backup conflicts, install packages, configure with GNU Stow + +## Updates + +**Regular updates (recommended):** + +```bash +dotpull +``` + +**Branch switching or major re-setup:** + +```bash +cd ~/.dotfiles && ./bootstrap.sh +``` ## Package Categories diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..0aabd52 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -euo pipefail + +DOTFILES_REPO="https://gitea.purpleraft.com/ryan/dotfiles" +DOTFILES_DIR="$HOME/.dotfiles" + +echo "Starting dotfiles bootstrap..." + +# Clone repo (new installs) or update for branch switching +if [ -d "$DOTFILES_DIR/.git" ]; then + echo "Updating existing dotfiles repo..." + git -C "$DOTFILES_DIR" pull --quiet +else + echo "Cloning dotfiles into $DOTFILES_DIR..." + git clone "$DOTFILES_REPO" "$DOTFILES_DIR" +fi + +cd "$DOTFILES_DIR" + +# Branch selection - allows choosing which version of dotfiles to install +# - New installs: git clone defaults to 'main', but you can switch to any branch +# - Existing installs: switch from current branch to a different one +# - Only runs in interactive terminals (skipped in automated/CI environments) +if [ -t 0 ] && [ -t 1 ] && [ -d ".git" ]; then + current_branch=$(git branch --show-current 2>/dev/null || echo "unknown") + echo "Current branch: $current_branch" + + echo + read -p "Switch to a different branch? (y/N): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Available branches:" + git branch -a --format="%(refname:short)" | grep -v "HEAD" | sed 's|^origin/||' | sort -u | nl -w2 -s'. ' + + branches=($(git branch -a --format="%(refname:short)" | grep -v "HEAD" | sed 's|^origin/||' | sort -u)) + + while true; do + read -p "Enter branch number (1-${#branches[@]}): " branch_num + + if [[ "$branch_num" =~ ^[0-9]+$ ]] && [ "$branch_num" -ge 1 ] && [ "$branch_num" -le "${#branches[@]}" ]; then + selected_branch="${branches[$((branch_num-1))]}" + echo "Switching to branch: $selected_branch" + + if git show-ref --verify --quiet "refs/heads/$selected_branch"; then + git checkout "$selected_branch" + elif git show-ref --verify --quiet "refs/remotes/origin/$selected_branch"; then + git checkout -b "$selected_branch" "origin/$selected_branch" + else + echo "Error: Branch $selected_branch not found" + exit 1 + fi + + git pull --quiet || true + echo "Switched to branch: $selected_branch" + break + else + echo "Invalid selection. Please enter a number between 1 and ${#branches[@]}" + fi + done + fi +fi + +# Hand off to setup +if [ -f "setup.sh" ]; then + echo "Starting setup..." + exec "./setup.sh" +else + echo "Error: No setup.sh found!" + exit 1 +fi diff --git a/install.sh b/install.sh deleted file mode 100644 index 6546502..0000000 --- a/install.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -set -euo pipefail - -DOTFILES_REPO="https://gitea.purpleraft.com/ryan/dotfiles" -DOTFILES_DIR="$HOME/.dotfiles" - -# Source utilities if available -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -if [ -f "$SCRIPT_DIR/scripts/utils.sh" ]; then - source "$SCRIPT_DIR/scripts/utils.sh" -else - # Fallback logging functions if utils not available - log_info() { echo "ℹ️ $1"; } - log_success() { echo "✅ $1"; } - log_warning() { echo "⚠️ $1"; } - log_error() { echo "❌ $1"; } -fi - -log_info "🚀 Starting dotfiles installation..." - -# Clone or update the repo -if [ -d "$DOTFILES_DIR/.git" ]; then - log_info "Updating existing dotfiles repo..." - git -C "$DOTFILES_DIR" pull --quiet -else - log_info "Cloning dotfiles into $DOTFILES_DIR..." - git clone "$DOTFILES_REPO" "$DOTFILES_DIR" -fi - -cd "$DOTFILES_DIR" - -# Auto-discover and run setup scripts -if [ -d "scripts" ]; then - log_info "Running setup scripts..." - - # Look for numbered scripts and run them in order - for script in scripts/[0-9][0-9]-*.sh; do - if [ -f "$script" ]; then - # Make executable and run - chmod +x "$script" 2>/dev/null || true - script_name=$(basename "$script") - log_info "Running $script_name..." - "$script" "$DOTFILES_DIR" - fi - done -else - log_error "No scripts directory found! Something went wrong with the installation." - exit 1 -fi - -log_success "Dotfiles install complete!" diff --git a/scripts/05-branch-selection.sh b/scripts/05-branch-selection.sh new file mode 100644 index 0000000..a873513 --- /dev/null +++ b/scripts/05-branch-selection.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# scripts/05-branch-selection.sh - Allow user to select git branch + +set -euo pipefail + +# Source utilities +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/utils.sh" + +select_branch() { + local dotfiles_dir="$1" + + # Change to dotfiles directory + cd "$dotfiles_dir" + + # Get current branch + local current_branch + current_branch=$(git branch --show-current 2>/dev/null || echo "unknown") + + log_info "Current branch: $current_branch" + + # Only prompt if we're in an interactive terminal + if ! is_interactive; then + log_info "Non-interactive mode, staying on current branch: $current_branch" + return 0 + fi + + # Get available branches (both local and remote) + log_info "Available branches:" + git branch -a --format="%(refname:short)" | grep -v "HEAD" | sort -u | nl -w2 -s'. ' + + echo + read -p "Switch to a different branch? (y/N): " -n 1 -r + echo + + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_info "Staying on current branch: $current_branch" + return 0 + fi + + # Get list of branches for selection + local branches=() + while IFS= read -r branch; do + # Clean up branch names (remove origin/ prefix, etc.) + clean_branch=$(echo "$branch" | sed 's|^origin/||' | sed 's|^remotes/origin/||') + # Skip HEAD and duplicates + if [[ "$clean_branch" != "HEAD" ]] && [[ ! " ${branches[@]} " =~ " ${clean_branch} " ]]; then + branches+=("$clean_branch") + fi + done < <(git branch -a --format="%(refname:short)" | grep -v "HEAD") + + # Sort branches + IFS=$'\n' branches=($(sort <<<"${branches[*]}")) + + echo "Select a branch:" + for i in "${!branches[@]}"; do + echo "$((i+1)). ${branches[i]}" + done + echo + + while true; do + read -p "Enter branch number (1-${#branches[@]}): " branch_num + + if [[ "$branch_num" =~ ^[0-9]+$ ]] && [ "$branch_num" -ge 1 ] && [ "$branch_num" -le "${#branches[@]}" ]; then + selected_branch="${branches[$((branch_num-1))]}" + break + else + log_warning "Invalid selection. Please enter a number between 1 and ${#branches[@]}" + fi + done + + # Switch to selected branch + log_info "Switching to branch: $selected_branch" + + if git show-ref --verify --quiet "refs/heads/$selected_branch"; then + # Local branch exists + git checkout "$selected_branch" + elif git show-ref --verify --quiet "refs/remotes/origin/$selected_branch"; then + # Remote branch exists, create local tracking branch + git checkout -b "$selected_branch" "origin/$selected_branch" + else + log_error "Branch $selected_branch not found" + return 1 + fi + + # Pull latest changes + log_info "Pulling latest changes..." + git pull --quiet || true + + log_success "Switched to branch: $selected_branch" +} + +# Run if called directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + if [ $# -ne 1 ]; then + log_error "Usage: $0 " + exit 1 + fi + select_branch "$1" +fi diff --git a/scripts/10-backup-files.sh b/scripts/10-backup-files.sh index 84ef4f1..52b1c78 100644 --- a/scripts/10-backup-files.sh +++ b/scripts/10-backup-files.sh @@ -12,17 +12,41 @@ backup_files() { log_info "Checking for file conflicts..." - # Files that might conflict with stow - local files_to_check=(.bashrc .bash_aliases .inputrc .gitconfig) local backed_up=() - for file in "${files_to_check[@]}"; do - if [ -f "$HOME/$file" ] && [ ! -L "$HOME/$file" ]; then - log_warning "Backing up existing file: $HOME/$file -> $HOME/${file}.bak" - mv "$HOME/$file" "$HOME/${file}.bak" - backed_up+=("$file") - fi - done + # Auto-discover files from stow directories + if [ -d "$dotfiles_dir/stow" ]; then + # Use new stow structure - scan all files in stow subdirectories + for config_dir in "$dotfiles_dir/stow"/*; do + if [ -d "$config_dir" ]; then + log_info "Checking $(basename "$config_dir") configuration for conflicts..." + + # Find all files that would be stowed + find "$config_dir" -type f | while read -r file; do + # Get relative path from config directory + relative_file="${file#$config_dir/}" + target_file="$HOME/$relative_file" + + if [ -f "$target_file" ] && [ ! -L "$target_file" ]; then + log_warning "Backing up existing file: $target_file -> ${target_file}.bak" + mv "$target_file" "${target_file}.bak" + backed_up+=("$relative_file") + fi + done + fi + done + else + # Fallback to old structure - hardcoded file list + local files_to_check=(.bashrc .bash_aliases .inputrc .gitconfig) + + for file in "${files_to_check[@]}"; do + if [ -f "$HOME/$file" ] && [ ! -L "$HOME/$file" ]; then + log_warning "Backing up existing file: $HOME/$file -> $HOME/${file}.bak" + mv "$HOME/$file" "$HOME/${file}.bak" + backed_up+=("$file") + fi + done + fi if [ ${#backed_up[@]} -gt 0 ]; then log_info "Backed up ${#backed_up[@]} file(s): ${backed_up[*]}" diff --git a/scripts/20-setup-stow.sh b/scripts/20-setup-stow.sh index 3fbe526..cc1afb0 100644 --- a/scripts/20-setup-stow.sh +++ b/scripts/20-setup-stow.sh @@ -10,16 +10,43 @@ source "$SCRIPT_DIR/utils.sh" setup_stow() { local dotfiles_dir="$1" - # Change to dotfiles directory and use stow to create symlinks - cd "$dotfiles_dir" - log_info "Using Stow to symlink dotfiles..." - - if ! stow --adopt -t "$HOME" . 2>/dev/null; then - log_warning "Adopting failed, trying regular stow..." - stow -t "$HOME" . + if [ -d "$dotfiles_dir/stow" ]; then + # Use new stow structure - stow each subdirectory individually + cd "$dotfiles_dir/stow" + log_info "Using Stow to symlink dotfiles from stow/ subdirectories..." + + local stowed=() + + for config_dir in *; do + if [ -d "$config_dir" ]; then + log_info "Stowing $config_dir configuration..." + + if ! stow --adopt -t "$HOME" "$config_dir" 2>/dev/null; then + log_warning "Adopting failed for $config_dir, trying regular stow..." + stow -t "$HOME" "$config_dir" + fi + + stowed+=("$config_dir") + fi + done + + if [ ${#stowed[@]} -gt 0 ]; then + log_success "Stowed ${#stowed[@]} configuration(s): ${stowed[*]}" + else + log_warning "No stow subdirectories found in $dotfiles_dir/stow" + fi + else + # Fallback to old structure - stow entire directory + cd "$dotfiles_dir" + log_info "Using Stow to symlink dotfiles from root directory..." + + if ! stow --adopt -t "$HOME" . 2>/dev/null; then + log_warning "Adopting failed, trying regular stow..." + stow -t "$HOME" . + fi + + log_success "Stow setup complete" fi - - log_success "Stow setup complete" } # Run if called directly diff --git a/scripts/30-install-packages.sh b/scripts/30-install-packages.sh new file mode 100644 index 0000000..3670279 --- /dev/null +++ b/scripts/30-install-packages.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# scripts/30-install-packages.sh - Install packages from package lists + +set -euo pipefail + +# Source utilities +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/utils.sh" + +# Detect package manager +detect_package_manager() { + if command_exists apt; then + echo "apt" + elif command_exists yum; then + echo "yum" + elif command_exists dnf; then + echo "dnf" + elif command_exists pacman; then + echo "pacman" + elif command_exists brew; then + echo "brew" + else + echo "unknown" + fi +} + +# Install packages with detected package manager +install_packages() { + local package_manager="$1" + shift + local packages=("$@") + + if [ ${#packages[@]} -eq 0 ]; then + log_info "No packages to install" + return 0 + fi + + case "$package_manager" in + apt) + log_info "Installing packages with apt..." + sudo apt update && sudo apt install -y "${packages[@]}" + ;; + yum) + log_info "Installing packages with yum..." + sudo yum install -y "${packages[@]}" + ;; + dnf) + log_info "Installing packages with dnf..." + sudo dnf install -y "${packages[@]}" + ;; + pacman) + log_info "Installing packages with pacman..." + sudo pacman -S --noconfirm "${packages[@]}" + ;; + brew) + log_info "Installing packages with brew..." + brew install "${packages[@]}" + ;; + *) + log_error "Unknown package manager. Please install packages manually." + return 1 + ;; + esac +} + +# Read packages from file +read_package_file() { + local file="$1" + if [ -f "$file" ]; then + # Filter out comments and empty lines + grep -v '^#' "$file" | grep -v '^[[:space:]]*$' | tr '\n' ' ' + fi +} + +install_package_lists() { + local dotfiles_dir="$1" + local packages_dir="$dotfiles_dir/packages" + + if [ ! -d "$packages_dir" ]; then + log_warning "No packages directory found at $packages_dir" + return 0 + fi + + local package_manager + package_manager=$(detect_package_manager) + + if [ "$package_manager" = "unknown" ]; then + log_error "No supported package manager found" + return 1 + fi + + log_info "Detected package manager: $package_manager" + + # Install package lists in order + local package_files=("base.txt" "cli-tools.txt" "dev.txt" "gui.txt") + + for package_file in "${package_files[@]}"; do + local file_path="$packages_dir/$package_file" + if [ -f "$file_path" ]; then + log_info "Installing packages from $package_file..." + local packages + packages=$(read_package_file "$file_path") + + if [ -n "$packages" ]; then + # Convert space-separated string to array + read -ra package_array <<< "$packages" + install_packages "$package_manager" "${package_array[@]}" + log_success "Completed installation from $package_file" + else + log_info "No packages found in $package_file" + fi + else + log_info "Package file $package_file not found, skipping" + fi + done +} + +# Run if called directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + if [ $# -ne 1 ]; then + log_error "Usage: $0 " + exit 1 + fi + install_package_lists "$1" +fi diff --git a/scripts/utils.sh b/scripts/utils.sh index 465c702..0860494 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -34,3 +34,49 @@ command_exists() { is_interactive() { [[ $- == *i* ]] } + +# Branch selection utilities +list_branches() { + local dotfiles_dir="$1" + cd "$dotfiles_dir" + + # Get available branches (both local and remote) + local branches=() + while IFS= read -r branch; do + # Clean up branch names (remove origin/ prefix, etc.) + clean_branch=$(echo "$branch" | sed 's|^origin/||' | sed 's|^remotes/origin/||') + # Skip HEAD and duplicates + if [[ "$clean_branch" != "HEAD" ]] && [[ ! " ${branches[@]} " =~ " ${clean_branch} " ]]; then + branches+=("$clean_branch") + fi + done < <(git branch -a --format="%(refname:short)" | grep -v "HEAD") + + # Sort and return branches + printf '%s\n' "${branches[@]}" | sort +} + +switch_to_branch() { + local dotfiles_dir="$1" + local selected_branch="$2" + + cd "$dotfiles_dir" + + log_info "Switching to branch: $selected_branch" + + if git show-ref --verify --quiet "refs/heads/$selected_branch"; then + # Local branch exists + git checkout "$selected_branch" + elif git show-ref --verify --quiet "refs/remotes/origin/$selected_branch"; then + # Remote branch exists, create local tracking branch + git checkout -b "$selected_branch" "origin/$selected_branch" + else + log_error "Branch $selected_branch not found" + return 1 + fi + + # Pull latest changes + log_info "Pulling latest changes..." + git pull --quiet || true + + log_success "Switched to branch: $selected_branch" +} diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..f141a6b --- /dev/null +++ b/setup.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# setup.sh - Main dotfiles setup orchestrator +# Runs all numbered scripts in sequence + +set -euo pipefail + +# Get the absolute path to the directory containing this script +# This handles symlinks and relative paths to always find the dotfiles root +DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source utilities with fallback +if [ -f "$DOTFILES_DIR/scripts/utils.sh" ]; then + source "$DOTFILES_DIR/scripts/utils.sh" +else + echo "Error: scripts/utils.sh not found! Repository structure is corrupted." + exit 1 +fi + +log_info "🔧 Starting dotfiles setup..." +log_info "Working directory: $DOTFILES_DIR" + +# Auto-discover and run setup scripts +if [ -d "$DOTFILES_DIR/scripts" ]; then + log_info "Running setup scripts..." + + # Look for numbered scripts and run them in order + script_count=0 + for script in "$DOTFILES_DIR/scripts"/[0-9][0-9]-*.sh; do + if [ -f "$script" ]; then + # Make executable and run + chmod +x "$script" 2>/dev/null || true + script_name=$(basename "$script") + log_info "Running $script_name..." + + if "$script" "$DOTFILES_DIR"; then + log_success "✓ $script_name completed" + else + log_error "✗ $script_name failed" + exit 1 + fi + + ((script_count++)) + fi + done + + if [ $script_count -eq 0 ]; then + log_warning "No numbered scripts found in scripts/ directory" + else + log_success "Completed $script_count setup script(s)" + fi +else + log_error "No scripts directory found! Something went wrong with the installation." + exit 1 +fi + +log_success "🎉 Dotfiles setup complete!" From 67504dccffc4c0b6627bad886288cce45d958131 Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Mon, 4 Aug 2025 20:44:53 -0500 Subject: [PATCH 4/8] remove: standalone branch selection script - functionality moved to bootstrap.sh --- scripts/05-branch-selection.sh | 100 --------------------------------- 1 file changed, 100 deletions(-) delete mode 100644 scripts/05-branch-selection.sh diff --git a/scripts/05-branch-selection.sh b/scripts/05-branch-selection.sh deleted file mode 100644 index a873513..0000000 --- a/scripts/05-branch-selection.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash -# scripts/05-branch-selection.sh - Allow user to select git branch - -set -euo pipefail - -# Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/utils.sh" - -select_branch() { - local dotfiles_dir="$1" - - # Change to dotfiles directory - cd "$dotfiles_dir" - - # Get current branch - local current_branch - current_branch=$(git branch --show-current 2>/dev/null || echo "unknown") - - log_info "Current branch: $current_branch" - - # Only prompt if we're in an interactive terminal - if ! is_interactive; then - log_info "Non-interactive mode, staying on current branch: $current_branch" - return 0 - fi - - # Get available branches (both local and remote) - log_info "Available branches:" - git branch -a --format="%(refname:short)" | grep -v "HEAD" | sort -u | nl -w2 -s'. ' - - echo - read -p "Switch to a different branch? (y/N): " -n 1 -r - echo - - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - log_info "Staying on current branch: $current_branch" - return 0 - fi - - # Get list of branches for selection - local branches=() - while IFS= read -r branch; do - # Clean up branch names (remove origin/ prefix, etc.) - clean_branch=$(echo "$branch" | sed 's|^origin/||' | sed 's|^remotes/origin/||') - # Skip HEAD and duplicates - if [[ "$clean_branch" != "HEAD" ]] && [[ ! " ${branches[@]} " =~ " ${clean_branch} " ]]; then - branches+=("$clean_branch") - fi - done < <(git branch -a --format="%(refname:short)" | grep -v "HEAD") - - # Sort branches - IFS=$'\n' branches=($(sort <<<"${branches[*]}")) - - echo "Select a branch:" - for i in "${!branches[@]}"; do - echo "$((i+1)). ${branches[i]}" - done - echo - - while true; do - read -p "Enter branch number (1-${#branches[@]}): " branch_num - - if [[ "$branch_num" =~ ^[0-9]+$ ]] && [ "$branch_num" -ge 1 ] && [ "$branch_num" -le "${#branches[@]}" ]; then - selected_branch="${branches[$((branch_num-1))]}" - break - else - log_warning "Invalid selection. Please enter a number between 1 and ${#branches[@]}" - fi - done - - # Switch to selected branch - log_info "Switching to branch: $selected_branch" - - if git show-ref --verify --quiet "refs/heads/$selected_branch"; then - # Local branch exists - git checkout "$selected_branch" - elif git show-ref --verify --quiet "refs/remotes/origin/$selected_branch"; then - # Remote branch exists, create local tracking branch - git checkout -b "$selected_branch" "origin/$selected_branch" - else - log_error "Branch $selected_branch not found" - return 1 - fi - - # Pull latest changes - log_info "Pulling latest changes..." - git pull --quiet || true - - log_success "Switched to branch: $selected_branch" -} - -# Run if called directly -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - if [ $# -ne 1 ]; then - log_error "Usage: $0 " - exit 1 - fi - select_branch "$1" -fi From 2ef00a0dd9ec218eece0fc0026a64b0c0b52db97 Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Mon, 4 Aug 2025 20:51:13 -0500 Subject: [PATCH 5/8] fix: enhance git pull handling in bootstrap script to manage local changes --- bootstrap.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/bootstrap.sh b/bootstrap.sh index 0aabd52..e8a1719 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -9,14 +9,27 @@ echo "Starting dotfiles bootstrap..." # Clone repo (new installs) or update for branch switching if [ -d "$DOTFILES_DIR/.git" ]; then echo "Updating existing dotfiles repo..." - git -C "$DOTFILES_DIR" pull --quiet + cd "$DOTFILES_DIR" + + # Handle local changes if they exist + if ! git diff-index --quiet HEAD --; then + echo "Local changes detected:" + echo "1) Discard 2) Stash 3) Abort" + read -p "Choice (1-3): " choice + case $choice in + 1) git reset --hard HEAD ;; + 2) git stash push -m "Bootstrap $(date +%Y%m%d-%H%M)" ;; + *) echo "Aborted. Check 'git status' in $DOTFILES_DIR"; exit 1 ;; + esac + fi + + git pull --quiet || { echo "Error: Failed to update repository"; exit 1; } else echo "Cloning dotfiles into $DOTFILES_DIR..." git clone "$DOTFILES_REPO" "$DOTFILES_DIR" + cd "$DOTFILES_DIR" fi -cd "$DOTFILES_DIR" - # Branch selection - allows choosing which version of dotfiles to install # - New installs: git clone defaults to 'main', but you can switch to any branch # - Existing installs: switch from current branch to a different one From b08c9acb29cc19156e7df116e4088b96b8613fa6 Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Mon, 4 Aug 2025 20:53:09 -0500 Subject: [PATCH 6/8] fix: update setup script execution method for improved compatibility --- bootstrap.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bootstrap.sh b/bootstrap.sh index e8a1719..fbeb216 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -77,7 +77,8 @@ fi # Hand off to setup if [ -f "setup.sh" ]; then echo "Starting setup..." - exec "./setup.sh" + chmod +x setup.sh 2>/dev/null || true + bash setup.sh else echo "Error: No setup.sh found!" exit 1 From a245bebffbe58263082fc45c44ccd4d4e2f8e123 Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Mon, 4 Aug 2025 20:59:04 -0500 Subject: [PATCH 7/8] fix: ensure setup scripts are executed with bash for compatibility --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index f141a6b..575a937 100644 --- a/setup.sh +++ b/setup.sh @@ -32,7 +32,7 @@ if [ -d "$DOTFILES_DIR/scripts" ]; then script_name=$(basename "$script") log_info "Running $script_name..." - if "$script" "$DOTFILES_DIR"; then + if bash "$script" "$DOTFILES_DIR"; then log_success "✓ $script_name completed" else log_error "✗ $script_name failed" From 4b50becf6f124468e0909691ab8417cf59536ddb Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Mon, 4 Aug 2025 21:03:46 -0500 Subject: [PATCH 8/8] fix: correct script path in setup.sh for proper execution --- setup.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.sh b/setup.sh index 575a937..751f2ab 100644 --- a/setup.sh +++ b/setup.sh @@ -25,10 +25,8 @@ if [ -d "$DOTFILES_DIR/scripts" ]; then # Look for numbered scripts and run them in order script_count=0 - for script in "$DOTFILES_DIR/scripts"/[0-9][0-9]-*.sh; do + for script in "$DOTFILES_DIR"/scripts/[0-9][0-9]-*.sh; do if [ -f "$script" ]; then - # Make executable and run - chmod +x "$script" 2>/dev/null || true script_name=$(basename "$script") log_info "Running $script_name..."