#!/usr/bin/env bash # Mike's Dotfiles Install Script # Safe to run multiple times (idempotent). # Targets both macOS and Linux (Coder devboxes). set -euo pipefail # --- Configuration --- export HOMEBREW_TEMP="/tmp" # Optional components (default to true) INSTALL_NODE=${INSTALL_NODE:-true} INSTALL_PYTHON=${INSTALL_PYTHON:-true} INSTALL_GO=${INSTALL_GO:-true} INSTALL_JAVA=${INSTALL_JAVA:-true} BREW_PACKAGES=( git diff-so-fancy btop gh zsh starship stow bat fd jq wget eza git-lfs difftastic fzf direnv git-delta zsh-completions gemini-cli claude-code zsh-autosuggestions zsh-syntax-highlighting zsh-history-substring-search zoxide ripgrep ) # Only install tmux if not on macOS [[ "$(uname)" != "Darwin" ]] && BREW_PACKAGES+=(tmux) # Add optional packages to Homebrew list [[ "$INSTALL_GO" == "true" ]] && BREW_PACKAGES+=(go) [[ "$INSTALL_JAVA" == "true" ]] && BREW_PACKAGES+=(openjdk) [[ "$INSTALL_PYTHON" == "true" ]] && BREW_PACKAGES+=(pyenv) MAC_CASK_PACKAGES=(iterm2 font-hack-nerd-font) STOW_PACKAGES=(editorconfig gh git claude cursor starship zsh) # --- Utilities --- info() { echo -e "\033[0;34m[INFO]\033[0m $*"; } warn() { echo -e "\033[0;33m[WARN]\033[0m $*"; } error() { echo -e "\033[0;31m[ERROR]\033[0m $*"; exit 1; } OS_TYPE=$(uname) DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # --- macOS System Setup --- if [[ "$OS_TYPE" == "Darwin" ]]; then info "Configuring macOS system defaults..." defaults write com.apple.finder AppleShowAllFiles YES defaults write com.apple.finder ShowPathbar -bool true defaults write com.apple.finder ShowStatusBar -bool true # Refresh Finder killall Finder 2>/dev/null || true fi # --- Homebrew Setup --- if ! command -v brew &> /dev/null; then info "Installing Homebrew..." NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" fi # Initialize Homebrew environment for the current script if [[ "$OS_TYPE" == "Darwin" ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" 2>/dev/null || eval "$(/usr/local/bin/brew shellenv)" 2>/dev/null ZSH_PATH="$(brew --prefix)/bin/zsh" else # Linux / Coder if [ -d "/home/linuxbrew/.linuxbrew" ]; then eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" fi ZSH_PATH="/home/linuxbrew/.linuxbrew/bin/zsh" fi BREW_BIN=$(command -v brew) # --- Package Installation --- info "Installing Homebrew packages..." $BREW_BIN install "${BREW_PACKAGES[@]}" || warn "Some packages might have failed to install or are already present." if [[ "$OS_TYPE" == "Darwin" ]]; then info "Installing macOS casks..." $BREW_BIN install --cask "${MAC_CASK_PACKAGES[@]}" || warn "Some casks might have failed to install." # Permissions fix for completions chmod go-w "$(brew --prefix)/share" 2>/dev/null || true chmod -R go-w "$(brew --prefix)/share/zsh" 2>/dev/null || true fi # --- Shell Setup --- info "Setting up Zsh..." # Add to /etc/shells if missing if ! grep -q "$ZSH_PATH" /etc/shells; then info "Adding $ZSH_PATH to /etc/shells" if command -v sudo &> /dev/null; then echo "$ZSH_PATH" | sudo tee -a /etc/shells else warn "Sudo not found, skipping /etc/shells update" fi fi # Change default shell CURRENT_LOGIN_SHELL="" if [[ "$OS_TYPE" == "Darwin" ]]; then CURRENT_LOGIN_SHELL=$(dscl . -read "/Users/$USER" UserShell | awk '{print $2}') else CURRENT_LOGIN_SHELL=$(getent passwd "$USER" | cut -d: -f7) fi if [[ "$CURRENT_LOGIN_SHELL" != "$ZSH_PATH" ]]; then info "Changing default shell to $ZSH_PATH" if command -v sudo &> /dev/null; then sudo chsh -s "$ZSH_PATH" "$USER" || warn "Failed to change shell with sudo" else warn "Sudo not found, skipping shell change" fi fi # Oh My Zsh if [ ! -d "$HOME/.oh-my-zsh" ]; then info "Installing Oh My Zsh..." sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended fi # --- Runtime Environments --- # NVM / Node if [[ "$INSTALL_NODE" == "true" ]]; then info "Configuring NVM..." export NVM_DIR="$HOME/.nvm" if [ ! -d "$NVM_DIR" ]; then info "Installing NVM via official script..." curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash fi # Remove incompatible prefix/globalconfig from .npmrc for nvm compatibility if [ -f "$HOME/.npmrc" ]; then info "Cleaning .npmrc of incompatible settings..." if [[ "$OS_TYPE" == "Darwin" ]]; then sed -i '' '/^prefix=/d; /^globalconfig=/d' "$HOME/.npmrc" else sed -i '/^prefix=/d; /^globalconfig=/d' "$HOME/.npmrc" fi fi # shellcheck source=/dev/null if [ -s "$NVM_DIR/nvm.sh" ]; then source "$NVM_DIR/nvm.sh" nvm install --lts npm install -g npm@latest fi fi # Pyenv / Python if [[ "$INSTALL_PYTHON" == "true" ]] && command -v pyenv &> /dev/null; then info "Configuring Pyenv..." eval "$(pyenv init -)" pyenv install 3.10 -s pyenv global 3.10 fi # FZF Setup if command -v fzf &> /dev/null; then info "Configuring FZF..." "$(brew --prefix)/opt/fzf/install" --key-bindings --completion --no-update-rc --no-bash --no-zsh || true fi # --- AI Rules Generation --- generate_rules() { local target_dir="$DOTFILES_DIR/$1" info "Generating rules in $target_dir" mkdir -p "$target_dir" # Remove stale symlinks find "$target_dir" -type l -delete # Create new symlinks for rule in "$DOTFILES_DIR/ai-instructions"/*.md; do [ -e "$rule" ] || continue ln -sf "$rule" "$target_dir/$(basename "$rule")" done } generate_rules "claude/.claude/rules" generate_rules "cursor/.cursor/rules" # --- Stow --- info "Applying dotfile configurations via Stow..." for pkg in "${STOW_PACKAGES[@]}"; do info "Stowing $pkg..." # Detect conflicts and remove non-stow files stow --simulate "$pkg" -t "$HOME" -d "$DOTFILES_DIR" 2>&1 | \ grep -E "(existing target is (not owned by stow|neither a link nor a directory):|over existing target .+ since neither a link)" | \ awk '/over existing target/ { for(i=1;i<=NF;i++) if($i=="target") { print $(i+1); break } next } /:/ { sub(/.*: /, ""); print }' | \ while read -r conflict; do if [ -n "$conflict" ]; then warn "Removing conflict: $HOME/$conflict" rm -rf "$HOME/$conflict" fi done || true stow "$pkg" -t "$HOME" -d "$DOTFILES_DIR" done # --- Git Local Config --- info "Configuring machine-specific Git settings..." GIT_LOCAL_CONFIG="$HOME/.gitconfig_local" WORK_DIR="$HOME/work" if [[ "$OS_TYPE" == "Darwin" ]]; then GH_PATH="/opt/homebrew/bin/gh" else GH_PATH="/home/linuxbrew/.linuxbrew/bin/gh" fi # Set credential helper git config --file "$GIT_LOCAL_CONFIG" credential."https://github.com".helper "!$GH_PATH auth git-credential" git config --file "$GIT_LOCAL_CONFIG" credential."https://gist.github.com".helper "!$GH_PATH auth git-credential" # --- Final Tools & Linux Extras --- if [[ "$OS_TYPE" == "Linux" ]]; then if command -v apt-get &> /dev/null; then info "Installing nano via apt..." sudo apt-get update -qq && sudo apt-get install -y -qq nano || warn "Failed to install nano" fi # Bash auto-switch to zsh if ! grep -q "exec \"$ZSH_PATH\"" "$HOME/.bashrc" 2>/dev/null; then info "Adding Zsh auto-switch to .bashrc" cat >> "$HOME/.bashrc" <