Files
dotfiles/install.sh
2026-03-18 14:38:46 +13:00

237 lines
7.4 KiB
Bash
Executable File

#!/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" <<EOF
# Auto-switch to zsh if available and interactive
if [ -t 1 ] && [ -n "\$PS1" ] && [ "\$BASH" ] && [ -x "$ZSH_PATH" ]; then
export SHELL="$ZSH_PATH"
exec "$ZSH_PATH"
fi
EOF
fi
fi
info "Installation complete! Please restart your terminal or run 'source $HOME/.zshrc'"