Last active
February 8, 2026 04:37
-
-
Save nsticco/db82987b5121f06884de4010072e2cb0 to your computer and use it in GitHub Desktop.
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
| #!/usr/bin/env bash | |
| # /bin/bash -lc 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null || true)"; eval "$(/usr/local/bin/brew shellenv 2>/dev/null || true)"; /bin/bash <(curl -fsSL https://gist.githubusercontent.com/nsticco/db82987b5121f06884de4010072e2cb0/raw/bootstrap-mac-devops)' | |
| # Bootstrap macOS with DevOps tools + popular AI coding agent CLIs. | |
| # Works on macOS default Bash 3.2 (no ${var,,} / no associative arrays). | |
| # Spec Kit install logic fixed: "latest" installs from repo HEAD (no @latest tag). | |
| set -euo pipefail | |
| ############################################################################### | |
| # Customize by passing named parameters, e.g. | |
| # ./bootstrap-mac-devops.sh --nodejs 24 --java 25 | |
| # | |
| # Disable optional installs with: false|0|no|off|skip|none | |
| nodejs=${nodejs:-24} # Node LTS major (24.x) | |
| java=${java:-25} # Java LTS major (Temurin@25) | |
| keygen=${keygen:-true} # Generate SSH key (ed25519) | |
| # AI coding agent CLIs (default: latest) | |
| codex_cli=${codex_cli:-latest} # npm tag/version for @openai/codex | |
| gemini_cli=${gemini_cli:-latest} # npm tag/version for @google/gemini-cli | |
| claude_code=${claude_code:-latest} # latest|stable|<version> | |
| cursor_cli=${cursor_cli:-latest} # latest only (official script) | |
| github_spec_kit=${github_spec_kit:-latest} # latest OR real tag/branch/sha (not a literal "latest" tag) | |
| ############################################################################### | |
| # Arg parsing: --param value | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| --*) | |
| param="${1#--}" | |
| if [ $# -lt 2 ] || echo "${2:-}" | grep -qE '^--'; then | |
| echo "ERROR: Missing value for $1" >&2 | |
| exit 1 | |
| fi | |
| # shellcheck disable=SC2163 | |
| eval "${param}=\"\$2\"" | |
| shift 2 | |
| ;; | |
| *) | |
| shift | |
| ;; | |
| esac | |
| done | |
| log() { printf "\n==> %s\n\n" "$*"; } | |
| # Bash 3.2-safe lowercase | |
| lower() { printf "%s" "${1:-}" | tr '[:upper:]' '[:lower:]'; } | |
| is_disabled() { | |
| case "$(lower "${1:-}")" in | |
| false|0|no|off|skip|none) return 0 ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| ensure_line() { | |
| line="$1" | |
| file="$2" | |
| touch "$file" | |
| grep -qxF "$line" "$file" 2>/dev/null || echo "$line" >> "$file" | |
| } | |
| ############################################################################### | |
| # Ensure Xcode CLT exists (Homebrew needs it). This may trigger a GUI prompt. | |
| if ! xcode-select -p >/dev/null 2>&1; then | |
| log "Xcode Command Line Tools not found. Triggering install (may prompt a GUI)..." | |
| xcode-select --install || true | |
| echo "Re-run this script after the Command Line Tools finish installing." | |
| exit 1 | |
| fi | |
| ############################################################################### | |
| # Install Homebrew, if not installed | |
| if ! command -v brew >/dev/null 2>&1; then | |
| log "Installing Homebrew..." | |
| # Homebrew installer in NONINTERACTIVE mode requires a cached sudo credential. | |
| sudo -v | |
| NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | |
| fi | |
| # Load Homebrew environment for both Apple Silicon and Intel | |
| if [ -x /opt/homebrew/bin/brew ]; then | |
| eval "$(/opt/homebrew/bin/brew shellenv)" | |
| elif [ -x /usr/local/bin/brew ]; then | |
| eval "$(/usr/local/bin/brew shellenv)" | |
| else | |
| eval "$(brew shellenv)" | |
| fi | |
| # Persist Homebrew env in zsh profile (default shell on modern macOS) | |
| if ! grep -q "brew shellenv" "$HOME/.zprofile" 2>/dev/null; then | |
| cat >> "$HOME/.zprofile" <<'ZPROF' | |
| # Homebrew (added by bootstrap script) | |
| if [ -x /opt/homebrew/bin/brew ]; then | |
| eval "$(/opt/homebrew/bin/brew shellenv)" | |
| elif [ -x /usr/local/bin/brew ]; then | |
| eval "$(/usr/local/bin/brew shellenv)" | |
| fi | |
| ZPROF | |
| fi | |
| BREW_PREFIX="$(brew --prefix)" | |
| ############################################################################### | |
| log "Installing general utilities..." | |
| brew update | |
| brew install git vim nano jq wget python pipx | |
| ############################################################################### | |
| log "Installing Node.js LTS (node@${nodejs})..." | |
| brew install "node@${nodejs}" || true | |
| # Ensure correct node is on PATH for current session and future shells | |
| NODE_PREFIX="$(brew --prefix "node@${nodejs}")" | |
| export PATH="${NODE_PREFIX}/bin:$PATH" | |
| ensure_line "export PATH=\"\$(brew --prefix node@${nodejs})/bin:\$PATH\"" "$HOME/.zprofile" | |
| ensure_line "export PATH=\"\$(brew --prefix node@${nodejs})/bin:\$PATH\"" "$HOME/.bash_profile" | |
| # npm globals without sudo | |
| mkdir -p "$HOME/.npm-global" | |
| npm config set prefix "$HOME/.npm-global" >/dev/null | |
| export PATH="$HOME/.npm-global/bin:$PATH" | |
| ensure_line 'export PATH="$HOME/.npm-global/bin:$PATH"' "$HOME/.zprofile" | |
| ensure_line 'export PATH="$HOME/.npm-global/bin:$PATH"' "$HOME/.bash_profile" | |
| ############################################################################### | |
| log "Installing Java (Temurin@${java})..." | |
| brew install --cask "temurin@${java}" || true | |
| ############################################################################### | |
| log "Installing Ansible (pipx)..." | |
| pipx ensurepath >/dev/null 2>&1 || true | |
| export PATH="$HOME/.local/bin:$PATH" | |
| ensure_line 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.zprofile" | |
| ensure_line 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bash_profile" | |
| if command -v ansible >/dev/null 2>&1; then | |
| pipx upgrade --include-injected ansible >/dev/null 2>&1 || true | |
| else | |
| pipx install --include-deps ansible | |
| fi | |
| ############################################################################### | |
| log "Installing AWS CLI v2..." | |
| brew install awscli | |
| ############################################################################### | |
| log "Installing kubectl (kubernetes-cli)..." | |
| brew install kubernetes-cli | |
| ############################################################################### | |
| log "Installing Helm..." | |
| brew install helm | |
| ############################################################################### | |
| log "Installing k9s..." | |
| brew install k9s | |
| ############################################################################### | |
| log "Installing HashiCorp tools (official tap)..." | |
| brew tap hashicorp/tap | |
| brew install hashicorp/tap/terraform | |
| brew install terragrunt | |
| brew install hashicorp/tap/packer | |
| brew install hashicorp/tap/vault | |
| ############################################################################### | |
| log "Installing Postman..." | |
| brew install --cask postman | |
| ############################################################################### | |
| # Rancher Desktop removed per request | |
| ############################################################################### | |
| log "Installing Visual Studio Code (cask) if missing..." | |
| if ! brew list --cask visual-studio-code >/dev/null 2>&1; then | |
| brew install --cask visual-studio-code | |
| fi | |
| # Make `code` available if VS Code is installed but shell command isn't | |
| if ! command -v code >/dev/null 2>&1; then | |
| VS_CODE_BIN="/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" | |
| if [ -x "$VS_CODE_BIN" ]; then | |
| ln -sf "$VS_CODE_BIN" "${BREW_PREFIX}/bin/code" >/dev/null 2>&1 || true | |
| fi | |
| fi | |
| ############################################################################### | |
| log "Installing Visual Studio Code Extensions (if code is available)..." | |
| if command -v code >/dev/null 2>&1; then | |
| code --install-extension daylerees.rainglow \ | |
| --install-extension secanis.jenkinsfile-support \ | |
| --install-extension ms-python.python \ | |
| --install-extension ms-vscode.cpptools \ | |
| --install-extension esbenp.prettier-vscode \ | |
| --install-extension bradgashler.htmltagwrap \ | |
| --install-extension dsznajder.es7-react-js-snippets \ | |
| --install-extension dunstontc.vscode-docker-syntax \ | |
| --install-extension formulahendry.code-runner \ | |
| --install-extension hashicorp.terraform \ | |
| --install-extension 4ops.terraform \ | |
| --install-extension eamodio.gitlens \ | |
| --install-extension vscode-icons-team.vscode-icons \ | |
| --install-extension zxh404.vscode-proto3 \ | |
| --install-extension ms-vscode.vs-keybindings \ | |
| --install-extension twxs.cmake \ | |
| --install-extension ms-vscode.cmake-tools \ | |
| --install-extension github.copilot \ | |
| --install-extension ms-azuretools.vscode-docker \ | |
| --install-extension dtsvet.vscode-wasm | |
| else | |
| echo "VS Code 'code' command not found; skipping extension installation." | |
| fi | |
| ############################################################################### | |
| # AI Coding Agent CLIs | |
| ############################################################################### | |
| if ! is_disabled "$codex_cli"; then | |
| log "Installing Codex CLI (@openai/codex@${codex_cli})..." | |
| if [ "$(lower "$codex_cli")" = "latest" ]; then | |
| npm i -g @openai/codex@latest | |
| else | |
| npm i -g "@openai/codex@${codex_cli}" | |
| fi | |
| fi | |
| if ! is_disabled "$gemini_cli"; then | |
| log "Installing Gemini CLI (@google/gemini-cli@${gemini_cli})..." | |
| if [ "$(lower "$gemini_cli")" = "latest" ]; then | |
| npm i -g @google/gemini-cli@latest | |
| else | |
| npm i -g "@google/gemini-cli@${gemini_cli}" | |
| fi | |
| fi | |
| if ! is_disabled "$claude_code"; then | |
| log "Installing Claude Code (${claude_code})..." | |
| if [ "$(lower "$claude_code") = "latest" ] || [ "$(lower "$claude_code") = "stable" ]; then | |
| curl -fsSL https://claude.ai/install.sh | bash | |
| else | |
| curl -fsSL https://claude.ai/install.sh | bash -s "$claude_code" | |
| fi | |
| fi | |
| if ! is_disabled "$cursor_cli"; then | |
| log "Installing Cursor CLI..." | |
| curl -fsS https://cursor.com/install | bash | |
| fi | |
| # GitHub Spec Kit (Specify CLI) via uv | |
| # IMPORTANT: repo does not have a "latest" tag. For "latest", install from default branch HEAD. | |
| if ! is_disabled "$github_spec_kit"; then | |
| log "Installing uv (for GitHub Spec Kit / Specify CLI)..." | |
| brew install uv >/dev/null 2>&1 || true | |
| log "Installing GitHub Spec Kit (Specify CLI)..." | |
| SPEC_REF="$(lower "$github_spec_kit")" | |
| if [ "$SPEC_REF" = "latest" ]; then | |
| uv tool install specify-cli --from "git+https://github.com/github/spec-kit.git" --force | |
| else | |
| uv tool install specify-cli --from "git+https://github.com/github/spec-kit.git@${github_spec_kit}" --force | |
| fi | |
| fi | |
| ############################################################################### | |
| # SSH key generation (ed25519) | |
| ############################################################################### | |
| if ! is_disabled "$keygen"; then | |
| log "SSH key setup (ed25519)..." | |
| mkdir -p "$HOME/.ssh" | |
| chmod 700 "$HOME/.ssh" | |
| if [ ! -f "$HOME/.ssh/id_ed25519" ]; then | |
| ssh-keygen -q -t ed25519 -a 64 -N "" -C "$(whoami)@$(hostname)" -f "$HOME/.ssh/id_ed25519" <<<y >/dev/null 2>&1 | |
| echo "Generated ~/.ssh/id_ed25519" | |
| else | |
| echo "~/.ssh/id_ed25519 already exists; skipping." | |
| fi | |
| fi | |
| log "Done." | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment