Skip to content

Instantly share code, notes, and snippets.

@heyalexej
Last active July 9, 2026 20:25
Show Gist options
  • Select an option

  • Save heyalexej/2419b1bae9170502166fb079fe4cf47b to your computer and use it in GitHub Desktop.

Select an option

Save heyalexej/2419b1bae9170502166fb079fe4cf47b to your computer and use it in GitHub Desktop.
Install Homebrew and iTerm2 on a clean MacBook Air
#!/usr/bin/env bash
# Bootstrap a clean MacBook Air in two stages:
# 1. Run in Apple's Terminal: installs Homebrew + iTerm2, then opens iTerm2.
# 2. Run the same command in iTerm2: installs VS Code, GitHub CLI,
# Pi Coding Agent, logs into GitHub, then optionally stores your DeepSeek API key for Pi.
#
# Usage:
# /bin/bash -c "$(curl -fsSL 'https://gist.githubusercontent.com/heyalexej/2419b1bae9170502166fb079fe4cf47b/raw/macbook-air-homebrew-iterm2.sh?cachebust=1')"
set -euo pipefail
RAW_URL_BASE="https://gist.githubusercontent.com/heyalexej/2419b1bae9170502166fb079fe4cf47b/raw/macbook-air-homebrew-iterm2.sh"
RAW_URL="$RAW_URL_BASE?cachebust=$(date +%s)"
RUN_CMD="/bin/bash -c \"\$(curl -fsSL '$RAW_URL_BASE?cachebust=$(date +%s)')\""
BREW=""
log() {
printf '\n\033[1;34m==> %s\033[0m\n' "$*"
}
warn() {
printf '\n\033[1;33mWARNING: %s\033[0m\n' "$*"
}
fail() {
printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2
exit 1
}
is_macos() {
[[ "$(uname -s)" == "Darwin" ]]
}
in_iterm2() {
[[ "${TERM_PROGRAM:-}" == "iTerm.app" ]]
}
ensure_command_line_tools() {
log "Checking Apple Command Line Tools"
if xcode-select -p >/dev/null 2>&1; then
log "Command Line Tools already installed"
return
fi
warn "Apple Command Line Tools are missing. A macOS installer popup will open."
xcode-select --install >/dev/null 2>&1 || true
printf "Click 'Install' in the popup. Waiting for installation to finish"
attempts=0
until xcode-select -p >/dev/null 2>&1; do
attempts=$((attempts + 1))
if [[ "$attempts" -ge 90 ]]; then
fail "Command Line Tools did not finish within 30 minutes. Re-run this script after installing them."
fi
printf "."
sleep 20
done
printf "\n"
}
find_brew() {
if [[ -x /opt/homebrew/bin/brew ]]; then
BREW=/opt/homebrew/bin/brew
elif [[ -x /usr/local/bin/brew ]]; then
BREW=/usr/local/bin/brew
else
BREW="$(command -v brew || true)"
fi
}
ensure_homebrew() {
if command -v brew >/dev/null 2>&1; then
find_brew
log "Homebrew already installed: $BREW"
else
log "Installing Homebrew"
# The official installer may ask for the Mac login password for sudo.
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
find_brew
fi
if [[ -z "${BREW:-}" || ! -x "$BREW" ]]; then
fail "Homebrew installed, but brew was not found. Open a new Terminal/iTerm2 window and try again."
fi
log "Adding Homebrew to ~/.zprofile"
touch "$HOME/.zprofile"
if ! grep -Fq "$BREW shellenv" "$HOME/.zprofile"; then
{
printf '\n# Homebrew\n'
printf 'eval "$(%s shellenv)"\n' "$BREW"
} >> "$HOME/.zprofile"
fi
eval "$($BREW shellenv)"
log "Updating Homebrew"
"$BREW" update
}
install_cask() {
local cask="$1"
local label="$2"
log "Installing $label"
if "$BREW" list --cask "$cask" >/dev/null 2>&1; then
log "$label already installed"
else
"$BREW" install --cask "$cask"
fi
}
install_formula() {
local formula="$1"
local label="$2"
log "Installing $label"
if "$BREW" list --formula "$formula" >/dev/null 2>&1; then
log "$label already installed"
else
"$BREW" install "$formula"
fi
}
ensure_iterm2() {
install_cask "iterm2" "iTerm2"
}
ensure_vscode() {
install_cask "visual-studio-code" "Visual Studio Code"
}
ensure_gh() {
install_formula "gh" "GitHub CLI"
}
github_auth_present() {
command -v gh >/dev/null 2>&1 && gh auth status --hostname github.com >/dev/null 2>&1
}
configure_git_identity_from_github() {
github_auth_present || return
local login=""
local name=""
local id=""
login="$(gh api user --jq '.login' 2>/dev/null || true)"
name="$(gh api user --jq '.name // .login' 2>/dev/null || true)"
id="$(gh api user --jq '.id' 2>/dev/null || true)"
[[ -n "$login" ]] || return
[[ -n "$name" && "$name" != "null" ]] || name="$login"
if ! git config --global user.name >/dev/null 2>&1; then
git config --global user.name "$name"
fi
if ! git config --global user.email >/dev/null 2>&1 && [[ -n "$id" ]]; then
git config --global user.email "$id+$login@users.noreply.github.com"
fi
git config --global init.defaultBranch main
}
login_github() {
ensure_gh
if github_auth_present; then
log "GitHub CLI already logged in"
else
log "Starting GitHub login"
cat <<'EOF'
A browser window will open.
Sign in with the existing GitHub account and approve "GitHub CLI".
The one-time device code is copied to the clipboard automatically.
EOF
gh auth login \
--hostname github.com \
--web \
--clipboard \
--git-protocol https \
--scopes "repo,read:org,gist,workflow"
fi
gh auth setup-git --hostname github.com || true
configure_git_identity_from_github
}
node_is_new_enough_for_pi() {
command -v node >/dev/null 2>&1 && node -e 'const [maj,min,patch] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && (min > 19 || (min === 19 && patch >= 0))) ? 0 : 1)' >/dev/null 2>&1
}
ensure_node_for_pi() {
if node_is_new_enough_for_pi; then
log "Node.js already new enough for Pi: $(node --version)"
return
fi
log "Installing/updating Node.js for Pi"
if "$BREW" list node >/dev/null 2>&1; then
"$BREW" upgrade node || true
else
"$BREW" install node
fi
hash -r
if ! node_is_new_enough_for_pi; then
fail "Pi requires Node.js 22.19.0 or newer, but the active node is: $(command -v node || echo missing) $(node --version 2>/dev/null || true)"
fi
}
ensure_local_bin_on_path() {
mkdir -p "$HOME/.local/bin"
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
touch "$HOME/.zprofile"
if ! grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.zprofile"; then
{
printf '\n# User-local command line tools\n'
printf 'export PATH="$HOME/.local/bin:$PATH"\n'
} >> "$HOME/.zprofile"
fi
}
install_pi() {
ensure_local_bin_on_path
if command -v pi >/dev/null 2>&1; then
log "Pi Coding Agent already installed: $(command -v pi)"
pi --version || true
return
fi
log "Installing Pi Coding Agent"
curl -fsSL https://pi.dev/install.sh | sh
hash -r
ensure_local_bin_on_path
if ! command -v pi >/dev/null 2>&1; then
fail "Pi installer finished, but 'pi' is not on PATH. Open a new iTerm2 window and run: pi --version"
fi
}
deepseek_auth_present() {
local auth_path="$HOME/.pi/agent/auth.json"
[[ -f "$auth_path" ]] || return 1
node -e 'const fs=require("fs"); const p=process.argv[1]; try { const a=JSON.parse(fs.readFileSync(p,"utf8")); process.exit(a.deepseek && a.deepseek.type === "api_key" && a.deepseek.key ? 0 : 1); } catch { process.exit(1); }' "$auth_path"
}
prompt_for_deepseek_key() {
if [[ -n "${DEEPSEEK_API_KEY:-}" ]]; then
printf '%s' "$DEEPSEEK_API_KEY"
return
fi
if deepseek_auth_present; then
return
fi
if [[ ! -r /dev/tty ]]; then
warn "No interactive terminal available; skipping DeepSeek API key prompt. You can later run: export DEEPSEEK_API_KEY=... && pi"
return
fi
printf '\nPaste your DeepSeek API key for Pi (input hidden; leave empty to skip): ' > /dev/tty
stty -echo < /dev/tty
local key=""
IFS= read -r key < /dev/tty || true
stty echo < /dev/tty
printf '\n' > /dev/tty
printf '%s' "$key"
}
configure_deepseek_for_pi() {
mkdir -p "$HOME/.pi/agent"
chmod 700 "$HOME/.pi" "$HOME/.pi/agent" 2>/dev/null || true
if deepseek_auth_present && [[ -z "${DEEPSEEK_API_KEY:-}" ]]; then
log "DeepSeek API key already configured for Pi"
return
fi
local key
key="$(prompt_for_deepseek_key)"
if [[ -z "$key" ]]; then
warn "Skipping DeepSeek API key setup. Pi also accepts DEEPSEEK_API_KEY in the environment."
return
fi
log "Storing DeepSeek API key in ~/.pi/agent/auth.json"
PI_AUTH_JSON="$HOME/.pi/agent/auth.json" PI_DEEPSEEK_API_KEY="$key" node <<'NODE'
const fs = require('fs');
const path = require('path');
const authPath = process.env.PI_AUTH_JSON;
const key = process.env.PI_DEEPSEEK_API_KEY;
const dir = path.dirname(authPath);
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
let auth = {};
if (fs.existsSync(authPath)) {
const text = fs.readFileSync(authPath, 'utf8').trim();
if (text) {
try {
auth = JSON.parse(text);
} catch (error) {
const backup = `${authPath}.backup-${new Date().toISOString().replace(/[:.]/g, '-')}`;
fs.copyFileSync(authPath, backup);
console.error(`Existing auth.json was invalid; backed up to ${backup}`);
auth = {};
}
}
}
auth.deepseek = { type: 'api_key', key };
fs.writeFileSync(authPath, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
fs.chmodSync(authPath, 0o600);
NODE
}
configure_pi_settings() {
log "Configuring Pi defaults"
local has_deepseek=0
if deepseek_auth_present; then
has_deepseek=1
fi
PI_SETTINGS_JSON="$HOME/.pi/agent/settings.json" PI_HAS_DEEPSEEK="$has_deepseek" node <<'NODE'
const fs = require('fs');
const path = require('path');
const settingsPath = process.env.PI_SETTINGS_JSON;
const hasDeepSeek = process.env.PI_HAS_DEEPSEEK === '1';
const dir = path.dirname(settingsPath);
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
let settings = {};
if (fs.existsSync(settingsPath)) {
const text = fs.readFileSync(settingsPath, 'utf8').trim();
if (text) {
try {
settings = JSON.parse(text);
} catch (error) {
const backup = `${settingsPath}.backup-${new Date().toISOString().replace(/[:.]/g, '-')}`;
fs.copyFileSync(settingsPath, backup);
console.error(`Existing settings.json was invalid; backed up to ${backup}`);
settings = {};
}
}
}
settings.externalEditor ??= 'code --wait';
if (hasDeepSeek) {
settings.defaultProvider ??= 'deepseek';
settings.defaultModel ??= 'deepseek-v4-pro';
settings.defaultThinkingLevel ??= 'medium';
}
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
fs.chmodSync(settingsPath, 0o600);
NODE
}
stop_after_stage_one_if_not_in_iterm2() {
if in_iterm2 || [[ "${RUN_FULL_SETUP:-0}" == "1" ]]; then
return
fi
log "Stage 1 complete: Homebrew and iTerm2 are installed"
if command -v pbcopy >/dev/null 2>&1; then
printf '%s\n' "$RUN_CMD" | pbcopy || true
log "Copied the next command to the clipboard"
fi
open -a iTerm >/dev/null 2>&1 || true
cat <<EOF
Next step:
1. Open iTerm2 (it should be opening now).
2. Run the same command again:
$RUN_CMD
That second run will install Visual Studio Code, GitHub CLI, Pi Coding Agent,
start GitHub login, and ask for your DeepSeek API key.
EOF
exit 0
}
print_summary() {
log "Done"
printf 'Homebrew: %s\n' "$("$BREW" --version | head -n 1)"
printf 'iTerm2: /Applications/iTerm.app\n'
printf 'VS Code: /Applications/Visual Studio Code.app\n'
printf 'GitHub: %s\n' "$(gh api user --jq '.login' 2>/dev/null || echo 'not logged in')"
printf 'Pi: %s\n' "$(command -v pi 2>/dev/null || echo 'not found')"
if deepseek_auth_present; then
printf 'DeepSeek: configured in ~/.pi/agent/auth.json\n'
printf '\nTry Pi now:\n pi --provider deepseek --model deepseek-v4-pro\n'
else
printf 'DeepSeek: not configured; later use /login in Pi or set DEEPSEEK_API_KEY.\n'
fi
}
main() {
is_macos || fail "This script is for macOS only."
ensure_command_line_tools
ensure_homebrew
ensure_iterm2
stop_after_stage_one_if_not_in_iterm2
ensure_vscode
login_github
ensure_node_for_pi
install_pi
configure_deepseek_for_pi
configure_pi_settings
print_summary
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment