Skip to content

Instantly share code, notes, and snippets.

@MKS-01
Last active June 27, 2026 17:18
Show Gist options
  • Select an option

  • Save MKS-01/988f730c88800388ad1a202fed62bb67 to your computer and use it in GitHub Desktop.

Select an option

Save MKS-01/988f730c88800388ad1a202fed62bb67 to your computer and use it in GitHub Desktop.
Mac developer setup script — Homebrew, iTerm2, VS Code, fnm, Ollama, MLX, Starship and more
Mac developer setup script — Homebrew, iTerm2, VS Code, fnm, Ollama, MLX, Starship and more
#!/usr/bin/env bash
set -euo pipefail
# ─────────────────────────────────────────────────────────────────
# Mac Developer Setup Script
#
# Full dev environment after a fresh format or new Mac:
# Homebrew · iTerm2 · Warp · VS Code · Git · fnm (Node) · Go · Rust
# Python 3.14 · uv · rbenv (Ruby) · Starship · tmux · fzf · zoxide
# Ollama · MLX · React Native (iOS + Android) · macmon · GitHub CLI
#
# Run:
# curl -fsSL https://gist.githubusercontent.com/MKS-01/988f730c88800388ad1a202fed62bb67/raw/mac_setup.sh | bash
# ─────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
RESET='\033[0m'
log() { echo -e "${GREEN}==>${RESET} $*"; }
info() { echo -e "${CYAN} $*${RESET}"; }
warn() { echo -e "${YELLOW}[!]${RESET} $*"; }
# ── Homebrew ──────────────────────────────────
if ! command -v brew &>/dev/null; then
log "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add brew to PATH for Apple Silicon
if [[ "$(uname -m)" == "arm64" ]]; then
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$HOME/.zprofile"
eval "$(/opt/homebrew/bin/brew shellenv)"
fi
else
log "Homebrew already installed, updating..."
brew update
fi
# ── CLI Tools ─────────────────────────────────
log "Installing CLI tools..."
CLI_TOOLS=(
git
curl
wget
jq
ripgrep # fast grep (rg)
fd # fast find
bat # better cat
eza # better ls
fzf # fuzzy finder
zoxide # smarter cd
starship # cross-shell prompt
gh # GitHub CLI
mole # Mac cleaner/optimizer (tw93/mole)
llmfit # detect hardware & rank LLMs that fit (AlexsJones/llmfit)
fnm # fast Node.js version manager
rbenv # Ruby version manager
ruby-build # rbenv plugin to compile Ruby versions
python@3.14
uv # fast Python package/project manager
go
rustup # Rust toolchain manager (keg-only)
tmux # terminal multiplexer
tree # directory tree viewer
nmap # network scanner
ffmpeg # media processing
)
for tool in "${CLI_TOOLS[@]}"; do
if brew list "$tool" &>/dev/null; then
info "Already installed: $tool"
else
brew install "$tool"
fi
done
# ── GUI Apps (Casks) ──────────────────────────
log "Installing GUI apps..."
CASKS=(
iterm2 # terminal emulator
warp # AI-native terminal
visual-studio-code
stats # menu-bar system monitor (exelban/stats)
monitorcontrol # external monitor brightness/volume
google-chrome
iina # media player
imageoptim # image compression
minisim # iOS Simulator launcher
)
cask_installed() {
local cask="$1"
# Check brew registry first
brew list --cask "$cask" &>/dev/null && return 0
# Fall back: check /Applications via brew's reported app artifact
local app
app=$(brew info --cask "$cask" --json=v2 2>/dev/null \
| python3 -c "
import sys, json
try:
d = json.load(sys.stdin)['casks'][0]
arts = [list(a['app']) if isinstance(a.get('app'), list) else [a['app']]
for a in d.get('artifacts', []) if isinstance(a, dict) and 'app' in a]
print(arts[0][0] if arts else '')
except: pass
" 2>/dev/null)
[[ -n "$app" && -d "/Applications/$app" ]]
}
for cask in "${CASKS[@]}"; do
if cask_installed "$cask"; then
info "Already installed: $cask — skipping"
else
brew install --cask "$cask" || warn "Failed to install $cask — skipping"
fi
done
# ── macmon (Apple Silicon monitor) ────────────
log "Installing macmon..."
if brew list macmon &>/dev/null; then
info "macmon already installed"
else
brew install macmon
fi
# ── Node.js via fnm ───────────────────────────
log "Setting up Node.js via fnm..."
if command -v fnm &>/dev/null; then
eval "$(fnm env --use-on-cd)"
fnm install --lts
fnm use lts-latest
fnm default lts-latest
info "Node $(node --version) / npm $(npm --version)"
fi
# ── Ruby via rbenv ────────────────────────────
log "Setting up Ruby via rbenv..."
if command -v rbenv &>/dev/null; then
eval "$(rbenv init - zsh)"
LATEST_RUBY="$(rbenv install -l 2>/dev/null | grep -E '^\s*[0-9]+\.[0-9]+\.[0-9]+$' | tail -1 | tr -d ' ')"
if [[ -n "$LATEST_RUBY" ]]; then
if rbenv versions | grep -q "$LATEST_RUBY"; then
info "Ruby $LATEST_RUBY already installed"
else
rbenv install "$LATEST_RUBY"
fi
rbenv global "$LATEST_RUBY"
info "Ruby $(ruby --version)"
fi
fi
# ── Rust ──────────────────────────────────────
log "Setting up Rust..."
RUSTUP_BIN="$(brew --prefix rustup)/bin"
export PATH="$RUSTUP_BIN:$PATH"
if ! command -v rustc &>/dev/null; then
rustup-init -y --no-modify-path
fi
# Install stable toolchain if not present
if command -v rustup &>/dev/null; then
rustup toolchain install stable --no-self-update 2>/dev/null || true
rustup default stable
info "Rust $(rustc --version 2>/dev/null)"
fi
# ── VS Code Extensions ────────────────────────
log "Installing VS Code extensions..."
VSCODE_EXTENSIONS=(
vscodevim.vim
dbaeumer.vscode-eslint
esbenp.prettier-vscode
eamodio.gitlens
ms-python.python
golang.go
rust-lang.rust-analyzer
github.copilot
PKief.material-icon-theme
zhuangtongfa.material-theme
)
if command -v code &>/dev/null; then
for ext in "${VSCODE_EXTENSIONS[@]}"; do
code --install-extension "$ext" --force 2>/dev/null || warn "Could not install $ext"
done
else
warn "VS Code CLI 'code' not found — open VS Code and add it via: Cmd+Shift+P > Shell Command: Install 'code'"
fi
# ── Ollama ────────────────────────────────────
log "Installing Ollama..."
if ! command -v ollama &>/dev/null; then
curl -fsSL https://ollama.com/install.sh | sh
info "Ollama installed — run 'ollama serve' to start the daemon"
else
info "Ollama already installed ($(ollama --version 2>/dev/null || echo 'unknown version'))"
fi
# ── MLX + MLX-LM (Apple Silicon ML framework) ─
log "Setting up MLX and MLX-LM..."
# Ensure pip is available via python3
if ! command -v pip3 &>/dev/null && ! python3 -m pip --version &>/dev/null 2>&1; then
warn "pip3 not found — skipping MLX install. Run: python3 -m ensurepip --upgrade"
else
PIP="python3 -m pip"
if $PIP show mlx &>/dev/null; then
info "mlx already installed"
else
$PIP install mlx
fi
if $PIP show mlx-lm &>/dev/null; then
info "mlx-lm already installed"
else
$PIP install mlx-lm
fi
info "MLX: $(python3 -c 'import mlx; print(mlx.__version__)' 2>/dev/null || echo 'installed')"
info "mlx-lm ready — usage: mlx_lm.generate --model <hf-model-id> --prompt 'hello'"
fi
# ── Git config ────────────────────────────────
log "Configuring Git..."
if [[ -t 0 ]]; then
read -rp "Git name [$(git config --global user.name 2>/dev/null || echo '')]: " git_name
read -rp "Git email [$(git config --global user.email 2>/dev/null || echo '')]: " git_email
[[ -n "$git_name" ]] && git config --global user.name "$git_name"
[[ -n "$git_email" ]] && git config --global user.email "$git_email"
else
info "No TTY — skipping interactive Git name/email prompt"
info "Run: git config --global user.name 'Your Name'"
info "Run: git config --global user.email 'you@example.com'"
fi
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global core.autocrlf input
# ── Shell config (~/.zshrc) ───────────────────
log "Writing shell config..."
ZSHRC="$HOME/.zshrc"
touch "$ZSHRC"
MARKER="# ── mac_setup.sh ──"
if ! grep -q "$MARKER" "$ZSHRC"; then
cat >> "$ZSHRC" <<'ZSHBLOCK'
# ── mac_setup.sh ────────────────────────────────────────────
# Homebrew (Apple Silicon)
if [[ -f /opt/homebrew/bin/brew ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
fi
# Rust (rustup is keg-only — must be on PATH explicitly)
export PATH="$(brew --prefix rustup)/bin:$PATH"
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
# Go
export GOPATH="$HOME/go"
export PATH="$GOPATH/bin:$PATH"
# Python (brew)
export PATH="$(brew --prefix python@3.14)/libexec/bin:$PATH"
# JDK 17 (Zulu — required by React Native)
export JAVA_HOME="$(/usr/libexec/java_home -v 17 2>/dev/null)"
export PATH="$JAVA_HOME/bin:$PATH"
# Android SDK (React Native)
export ANDROID_HOME="$HOME/Library/Android/sdk"
export PATH="$ANDROID_HOME/emulator:$PATH"
export PATH="$ANDROID_HOME/platform-tools:$PATH"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
# rbenv — Ruby version manager
eval "$(rbenv init - zsh)"
# fnm — Node version manager (auto-switches on .nvmrc / .node-version)
eval "$(fnm env --use-on-cd --shell zsh)"
# zoxide — smarter cd (use 'z <dir>' or 'zi' for interactive)
eval "$(zoxide init zsh)"
# fzf — fuzzy finder key bindings (Ctrl-R history, Ctrl-T file, Alt-C dir)
[[ -f "$(brew --prefix)/opt/fzf/shell/key-bindings.zsh" ]] && \
source "$(brew --prefix)/opt/fzf/shell/key-bindings.zsh"
[[ -f "$(brew --prefix)/opt/fzf/shell/completion.zsh" ]] && \
source "$(brew --prefix)/opt/fzf/shell/completion.zsh"
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
# starship prompt
eval "$(starship init zsh)"
# ── Aliases ──────────────────────────────────
alias ls='eza --icons'
alias ll='eza -lah --icons --git'
alias lt='eza --tree --icons --level=2'
alias cat='bat --paging=never'
alias grep='rg'
alias find='fd'
alias z='zoxide query --interactive 2>/dev/null || zoxide query'
# Git shortcuts
alias g='git'
alias gs='git status -sb'
alias gp='git push'
alias gl='git pull --rebase'
alias glog='git log --oneline --graph --decorate -20'
# Dev
alias py='python3'
alias serve='python3 -m http.server'
# ─────────────────────────────────────────────
ZSHBLOCK
info "Shell config written to $ZSHRC"
else
info "Shell config already present in $ZSHRC — skipping"
fi
# ── Starship prompt config ────────────────────
log "Configuring Starship prompt..."
STARSHIP_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/starship.toml"
mkdir -p "$(dirname "$STARSHIP_CFG")"
if [[ ! -f "$STARSHIP_CFG" ]]; then
cat > "$STARSHIP_CFG" <<'STARSHIPCFG'
"$schema" = 'https://starship.rs/config-schema.json'
format = """
$os$username$hostname$directory$git_branch$git_status$nodejs$python$golang$rust$cmd_duration$line_break$character"""
[os]
disabled = false
[os.symbols]
Macos = " "
[directory]
truncation_length = 4
truncate_to_repo = true
style = "bold cyan"
[git_branch]
symbol = " "
style = "bold purple"
[git_status]
style = "bold red"
[nodejs]
symbol = " "
[python]
symbol = " "
[golang]
symbol = " "
[rust]
symbol = " "
[cmd_duration]
min_time = 2_000
format = "took [$duration](bold yellow) "
[character]
success_symbol = "[❯](bold green)"
error_symbol = "[❯](bold red)"
STARSHIPCFG
info "Starship config written to $STARSHIP_CFG"
else
info "Starship config already exists at $STARSHIP_CFG"
fi
# ── React Native — iOS ────────────────────────
log "Setting up React Native iOS toolchain..."
# Xcode must be installed manually from the App Store.
# Script only configures the command-line tools.
if xcode-select -p &>/dev/null; then
info "Xcode CLI tools path: $(xcode-select -p)"
else
warn "Xcode not found. Install it from the App Store, then re-run this section."
warn " https://apps.apple.com/app/xcode/id497799835"
fi
# Accept Xcode licence (required before any xcodebuild use)
if xcodebuild -version &>/dev/null 2>&1; then
sudo xcodebuild -license accept 2>/dev/null || true
info "Xcode licence accepted"
fi
# Set active developer directory to the full Xcode app (not just CLT)
XCODE_APP="/Applications/Xcode.app"
if [[ -d "$XCODE_APP" ]]; then
sudo xcode-select --switch "$XCODE_APP/Contents/Developer"
info "Active developer directory → $XCODE_APP/Contents/Developer"
fi
# Watchman — file watcher used by Metro bundler
if brew list watchman &>/dev/null; then
info "Watchman already installed"
else
brew install watchman
fi
# CocoaPods (needed by RN iOS build)
if ! command -v pod &>/dev/null; then
brew install cocoapods
else
info "CocoaPods $(pod --version) already installed"
fi
# Ruby bundler (for Gemfile-based pod workflows)
if ! gem list bundler -i &>/dev/null; then
gem install bundler --no-document
fi
# React Native CLI (global)
if command -v fnm &>/dev/null; then
eval "$(fnm env --use-on-cd 2>/dev/null)" || true
fi
if npm list -g react-native &>/dev/null 2>&1; then
info "react-native CLI already installed"
else
npm install -g react-native
fi
# ── React Native — Android ────────────────────
log "Setting up React Native Android toolchain..."
# JDK — Zulu 17 is the version recommended by React Native docs
if brew list --cask zulu@17 &>/dev/null; then
info "Zulu JDK 17 already installed"
else
brew install --cask zulu@17
fi
JAVA_HOME_PATH="$(/usr/libexec/java_home -v 17 2>/dev/null || true)"
# Android Studio — skip if installed via brew or dragged manually
if brew list --cask android-studio &>/dev/null || [[ -d "/Applications/Android Studio.app" ]]; then
info "Android Studio already installed — skipping"
else
brew install --cask android-studio
fi
# Android SDK command-line tools (for sdkmanager without opening the IDE)
if brew list android-commandlinetools &>/dev/null; then
info "Android command-line tools already installed"
else
brew install android-commandlinetools
fi
# Accept all SDK licences up front
if command -v sdkmanager &>/dev/null; then
yes | sdkmanager --licenses 2>/dev/null || true
# Core SDK packages required by React Native
SDK_PACKAGES=(
"platform-tools"
"platforms;android-35"
"build-tools;35.0.0"
"system-images;android-35;google_apis;arm64-v8a"
"emulator"
)
for pkg in "${SDK_PACKAGES[@]}"; do
sdkmanager "$pkg" 2>/dev/null || warn "Could not install SDK package: $pkg"
done
info "Android SDK packages installed"
fi
# ── macOS defaults ────────────────────────────
log "Applying macOS preferences..."
# Show hidden files in Finder
defaults write com.apple.finder AppleShowAllFiles YES
# Show path bar in Finder
defaults write com.apple.finder ShowPathbar YES
# Disable press-and-hold key repeat (better for Vim users)
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
# Fast key repeat
defaults write NSGlobalDomain KeyRepeat -int 2
defaults write NSGlobalDomain InitialKeyRepeat -int 15
# Dock: auto-hide, no recents
defaults write com.apple.dock autohide -bool true
defaults write com.apple.dock show-recents -bool false
killall Dock Finder 2>/dev/null || true
# ── Done ──────────────────────────────────────
echo ""
log "Setup complete!"
echo ""
info "Next steps:"
info " 1. Restart your terminal (or: source ~/.zshrc)"
info " 2. Launch Stats, MonitorControl from Applications"
info " 3. Run 'gh auth login' to authenticate GitHub CLI"
info " 4. Add VS Code 'code' to PATH if not working"
info " 5. Start Ollama daemon: ollama serve"
info " Pull a model: ollama pull llama3.2"
info " Run MLX-LM: mlx_lm.generate --model mlx-community/Llama-3.2-3B-Instruct-4bit --prompt 'hi'"
info ""
info " React Native iOS:"
info " 6. Install Xcode from App Store if not done, then re-run script"
info " Open Xcode → Preferences → Platforms → install iOS Simulator"
info ""
info " React Native Android:"
info " 7. Open Android Studio → More Actions → SDK Manager"
info " Confirm SDK 35 + Build Tools 35.0.0 are installed"
info " 8. Android Studio → More Actions → Virtual Device Manager → create emulator"
info " 9. Verify setup: npx react-native doctor"
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment