Last active
June 27, 2026 17:18
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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