Idempotent macOS setup — Homebrew packages, Zsh, Neovim, Node.js, Claude Code, and dev tools.
#!/usr/bin/env bash
# =============================================================================
# macOS Bootstrap Script
#
# Prerequisites (run manually before this script):
# xcode-select --install
# /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# eval "$(/opt/homebrew/bin/brew shellenv)"
# brew install gh
# gh auth login
# git clone --bare https://github.com/starmorph/dotfiles.git $HOME/.dotfiles
# git --git-dir=$HOME/.dotfiles --work-tree=$HOME checkout main -f
# ~/.bootstrap-mac.sh
#
# Re-run (already set up):
# ~/.bootstrap-mac.sh
# =============================================================================
set -euo pipefail
LOG_PREFIX="\033[1;34m==>\033[0m"
log() { echo -e "${LOG_PREFIX} $1"; }
warn() { echo -e "\033[1;33m==> WARNING:\033[0m $1"; }
err() { echo -e "\033[1;31m==> ERROR:\033[0m $1" >&2; }
# ---------------------------------------------------------------------------
# 0. Pre-flight checks
# ---------------------------------------------------------------------------
if [[ "$(uname)" != "Darwin" ]]; then
err "This script is designed for macOS. Detected: $(uname)"
exit 1
fi
if ! command -v brew &>/dev/null; then
err "Homebrew not found. Install it first:"
err ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
exit 1
fi
log "Starting macOS bootstrap..."
log "Detected: macOS $(sw_vers -productVersion) on $(uname -m)"
# ---------------------------------------------------------------------------
# 1. Homebrew packages
# ---------------------------------------------------------------------------
log "Installing Homebrew packages..."
BREW_PACKAGES=(
# CLI tools
bat
eza
gh
git-delta
glow
jq
neovim
ripgrep
tmux
zoxide
# File management
fd
fzf
tree
midnight-commander
# Media
ffmpeg
imagemagick
# Networking
nmap
# Dev tools
nvm
pnpm
pyenv-virtualenv
stripe/stripe-cli/stripe
supabase/tap/supabase
tree-sitter
# macOS specific
terminal-notifier
gnu-sed
gnu-tar
gnu-which
grep
)
for pkg in "${BREW_PACKAGES[@]}"; do
if brew list "$pkg" &>/dev/null; then
log " $pkg — already installed"
else
log " Installing $pkg..."
brew install "$pkg" || warn "Failed to install $pkg — skipping"
fi
done
# Casks
BREW_CASKS=(
visual-studio-code
)
for cask in "${BREW_CASKS[@]}"; do
if brew list --cask "$cask" &>/dev/null; then
log " $cask — already installed"
else
log " Installing $cask..."
brew install --cask "$cask" || warn "Failed to install $cask — skipping"
fi
done
# ---------------------------------------------------------------------------
# 2. Zsh + Oh My Zsh + Powerlevel10k
# ---------------------------------------------------------------------------
log "Setting up zsh..."
# Oh My Zsh
if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
log "Installing Oh My Zsh..."
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
else
log "Oh My Zsh — already installed"
fi
# Powerlevel10k
if [[ ! -d "$HOME/powerlevel10k" ]]; then
log "Installing Powerlevel10k..."
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k
else
log "Powerlevel10k — already installed"
fi
# ---------------------------------------------------------------------------
# 3. Node.js via nvm + pnpm
# ---------------------------------------------------------------------------
log "Setting up Node.js..."
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
if command -v nvm &>/dev/null; then
if ! nvm ls --no-colors 2>/dev/null | grep -q "lts"; then
log "Installing Node.js LTS via nvm..."
nvm install --lts
nvm alias default lts/*
else
log "Node.js LTS — already installed"
fi
else
warn "nvm not loaded — run 'source ~/.zshrc' then 'nvm install --lts'"
fi
# pnpm is already installed via Homebrew above
# ---------------------------------------------------------------------------
# 4. Dotfiles bare repo config
# ---------------------------------------------------------------------------
alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'
if [[ -d "$HOME/.dotfiles" ]]; then
log "Dotfiles bare repo found at ~/.dotfiles"
dotfiles config --local status.showUntrackedFiles no
else
warn "Bare repo not found at ~/.dotfiles — skipping dotfiles config"
warn "Clone it first: git clone --bare https://github.com/starmorph/dotfiles.git \$HOME/.dotfiles"
fi
# ---------------------------------------------------------------------------
# 5. Secrets file
# ---------------------------------------------------------------------------
if [[ ! -f "$HOME/.zsh_secrets" ]]; then
log "Creating empty ~/.zsh_secrets — add your API keys here"
touch "$HOME/.zsh_secrets"
chmod 600 "$HOME/.zsh_secrets"
else
log "~/.zsh_secrets — already exists"
fi
# ---------------------------------------------------------------------------
# 6. SSH key
# ---------------------------------------------------------------------------
if [[ ! -f "$HOME/.ssh/id_ed25519" ]]; then
log "Generating SSH key..."
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
ssh-keygen -t ed25519 -C "$USER@$(hostname -s)" -f "$HOME/.ssh/id_ed25519" -N ""
echo ""
log "Add this public key to GitHub: gh ssh-key add ~/.ssh/id_ed25519.pub"
cat "$HOME/.ssh/id_ed25519.pub"
echo ""
else
log "SSH key — already exists"
fi
# ---------------------------------------------------------------------------
# 7. Claude Code
# ---------------------------------------------------------------------------
if ! command -v claude &>/dev/null; then
log "Installing Claude Code..."
npm install -g @anthropic-ai/claude-code
else
log "Claude Code — already installed"
fi
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
echo ""
log "\033[1;32mBootstrap complete!\033[0m"
echo ""
echo " What was set up:"
echo " - zsh + Oh My Zsh + Powerlevel10k (cypher theme)"
echo " - neovim + LazyVim config"
echo " - tmux with custom config (prefix: Ctrl-a)"
echo " - Node.js LTS (via nvm) + pnpm"
echo " - Claude Code"
echo " - bat, eza, ripgrep, fd, fzf, jq, glow, delta, zoxide"
echo " - 25 shell scripts in ~/bin/"
echo ""
echo " Next steps:"
echo " 1. Restart your terminal (or run: exec zsh)"
echo " 2. Add API keys to ~/.zsh_secrets"
echo " 3. Run: gh auth login (if not already authenticated)"
echo " 4. Install Nerd Font for terminal icons (e.g., MesloLGS NF)"
echo ""How to use
Run chmod +x bootstrap-mac.sh && ./bootstrap-mac.sh in Terminal. Requires macOS with admin access. Installs Homebrew if not present. Safe to re-run.
A bootstrap script for macOS that installs and configures everything a developer needs. Homebrew CLI tools (bat, eza, ripgrep, fd, fzf, jq, glow, delta, zoxide), Oh My Zsh + Powerlevel10k, Node.js via nvm + pnpm, Neovim, tmux, SSH key generation, and Claude Code. Idempotent — skips anything already installed.
2 files included