Skip to content

Instantly share code, notes, and snippets.

@nsticco
Last active February 8, 2026 04:37
Show Gist options
  • Select an option

  • Save nsticco/db82987b5121f06884de4010072e2cb0 to your computer and use it in GitHub Desktop.

Select an option

Save nsticco/db82987b5121f06884de4010072e2cb0 to your computer and use it in GitHub Desktop.
#!/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