Last active
February 18, 2026 21:01
-
-
Save fxstein/10693ecc34c80fb7c4cf056d850bddf1 to your computer and use it in GitHub Desktop.
Pi Mac Mini bootstrap — fresh machine to running Pi instance
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 | |
| # ============================================================ | |
| # PI BOOTSTRAP — Set up a fresh Mac Mini as a Pi instance | |
| # Usage: curl -fsSL <gist-url> | bash | |
| # | |
| # Prerequisites: macOS, admin user, internet | |
| # Everything else is installed by this script. | |
| # ============================================================ | |
| set -euo pipefail | |
| echo "🥧 Pi Mac Mini Bootstrap" | |
| echo "========================" | |
| echo "" | |
| # ============================================================= | |
| # 1. HOMEBREW | |
| # ============================================================= | |
| if command -v brew &>/dev/null; then | |
| echo "✅ Homebrew installed" | |
| else | |
| echo "📦 Installing Homebrew..." | |
| # shellcheck disable=SC2312 # curl piped install is standard for Homebrew | |
| /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | |
| # Configure shell environment for Homebrew (Apple Silicon or Intel) | |
| BREW_BIN="" | |
| if [[ -f /opt/homebrew/bin/brew ]]; then | |
| BREW_BIN="/opt/homebrew/bin/brew" | |
| elif [[ -f /usr/local/bin/brew ]]; then | |
| BREW_BIN="/usr/local/bin/brew" | |
| fi | |
| if [[ -n "${BREW_BIN}" ]]; then | |
| # shellcheck disable=SC2312 # Command substitution in eval is expected | |
| eval "$("${BREW_BIN}" shellenv)" | |
| if ! grep -q 'brew shellenv' "${HOME}/.zprofile" 2>/dev/null; then | |
| # shellcheck disable=SC2016 # Single quotes intentional - write literal string to file | |
| echo "eval \"\$(${BREW_BIN} shellenv)\"" >> "${HOME}/.zprofile" | |
| fi | |
| fi | |
| fi | |
| echo "" | |
| # ============================================================= | |
| # 2. CORE TOOLS | |
| # ============================================================= | |
| echo "📦 Installing tools..." | |
| TOOLS=(git gh cloudflared 1password-cli chezmoi) | |
| # OrbStack for Apple Silicon, Docker Desktop for Intel | |
| # shellcheck disable=SC2312 # uname in command substitution is standard | |
| if [[ "$(uname -m)" == "arm64" ]]; then | |
| CASKS=(orbstack) | |
| else | |
| CASKS=(docker) | |
| fi | |
| for tool in "${TOOLS[@]}"; do | |
| if command -v "${tool}" &>/dev/null; then | |
| echo " ✅ ${tool}" | |
| else | |
| echo " 📦 ${tool}..." | |
| brew install "${tool}" | |
| fi | |
| done | |
| for cask in "${CASKS[@]}"; do | |
| # Check both brew and whether the app is already installed manually | |
| if brew list --cask "${cask}" &>/dev/null 2>&1; then | |
| echo " ✅ ${cask} (brew)" | |
| elif [[ "${cask}" == "docker" ]] && [[ -d "/Applications/Docker.app" ]]; then | |
| echo " ✅ ${cask} (manual install)" | |
| elif [[ "${cask}" == "orbstack" ]] && [[ -d "/Applications/OrbStack.app" ]]; then | |
| echo " ✅ ${cask} (manual install)" | |
| else | |
| echo " 📦 ${cask}..." | |
| brew install --cask "${cask}" | |
| fi | |
| done | |
| echo "" | |
| # ============================================================= | |
| # 3. ENABLE SSH (Remote Login) | |
| # ============================================================= | |
| echo "🔐 Enabling Remote Login (SSH)..." | |
| if sudo systemsetup -getremotelogin 2>/dev/null | grep -q "On"; then | |
| echo " ✅ Remote Login already enabled" | |
| else | |
| if sudo systemsetup -setremotelogin on 2>/dev/null; then | |
| echo " ✅ Remote Login enabled" | |
| else | |
| echo " ⚠️ Could not enable Remote Login (needs Full Disk Access)." | |
| echo " Enable manually: System Settings → General → Sharing → Remote Login" | |
| fi | |
| fi | |
| echo "" | |
| # ============================================================= | |
| # 4. GITHUB AUTH | |
| # ============================================================= | |
| echo "🔑 GitHub authentication..." | |
| if gh auth status &>/dev/null 2>&1; then | |
| # shellcheck disable=SC2312 # Command substitution expected here | |
| echo " ✅ Already authenticated as $(gh api user --jq .login)" | |
| else | |
| echo " Please authenticate with GitHub (need access to pionizer org):" | |
| gh auth login | |
| fi | |
| echo "" | |
| # ============================================================= | |
| # 5. CLONE REPOS | |
| # ============================================================= | |
| echo "📂 Cloning repositories..." | |
| OPENCLAW_DIR="${HOME}/openclaw" | |
| PIINFRA_DIR="${HOME}/pi-infra" | |
| if [[ -d "${OPENCLAW_DIR}/.git" ]]; then | |
| echo " ✅ ~/openclaw exists" | |
| else | |
| echo " 📦 Cloning OpenClaw..." | |
| gh repo clone openclaw/openclaw "${OPENCLAW_DIR}" | |
| fi | |
| if [[ -d "${PIINFRA_DIR}/.git" ]]; then | |
| echo " ✅ ~/pi-infra exists" | |
| cd "${PIINFRA_DIR}" && git pull --quiet | |
| else | |
| echo " 📦 Cloning pi-infra..." | |
| gh repo clone pionizer/pi-infra "${PIINFRA_DIR}" | |
| fi | |
| PICORE_DIR="${HOME}/pi-core" | |
| if [[ -d "${PICORE_DIR}/.git" ]]; then | |
| echo " ✅ ~/pi-core exists" | |
| cd "${PICORE_DIR}" && git pull --quiet | |
| else | |
| echo " 📦 Cloning pi-core..." | |
| gh repo clone pionizer/pi-core "${PICORE_DIR}" | |
| fi | |
| NUCLEUS_DIR="${HOME}/nucleus" | |
| if [[ -d "${NUCLEUS_DIR}/.git" ]]; then | |
| echo " ✅ ~/nucleus exists" | |
| cd "${NUCLEUS_DIR}" && git pull --quiet | |
| else | |
| echo " 📦 Cloning nucleus..." | |
| gh repo clone pionizer/nucleus "${NUCLEUS_DIR}" | |
| fi | |
| echo "" | |
| # ============================================================= | |
| # 6. INSTALL OPENCLAW CLI | |
| # ============================================================= | |
| echo "🔧 Installing openclaw CLI..." | |
| mkdir -p "${HOME}/.local/bin" | |
| cp "${PIINFRA_DIR}/pion-wrapper.sh" "${HOME}/.local/bin/pion" | |
| chmod +x "${HOME}/.local/bin/pion" | |
| ln -sf "${HOME}/.local/bin/pion" "${HOME}/.local/bin/openclaw" | |
| # Ensure ~/.local/bin is on PATH for this session and future shells | |
| export PATH="${HOME}/.local/bin:${PATH}" | |
| if ! grep -q '\.local/bin' "${HOME}/.zshenv" 2>/dev/null; then | |
| echo 'export PATH="${HOME}/.local/bin:${PATH}"' >> "${HOME}/.zshenv" | |
| echo " ✅ Added ~/.local/bin to PATH in ~/.zshenv" | |
| fi | |
| echo " ✅ ~/.local/bin/pion + openclaw" | |
| echo "" | |
| # ============================================================= | |
| # 7. SECRETS DIRECTORY | |
| # ============================================================= | |
| echo "📁 Setting up directories..." | |
| mkdir -p "${HOME}/.openclaw/secrets" | |
| mkdir -p "${HOME}/.openclaw/workspace" | |
| echo " ✅ ~/.openclaw/" | |
| echo "" | |
| # ============================================================= | |
| # 8. 1PASSWORD SERVICE ACCOUNT | |
| # ============================================================= | |
| echo "🔐 1Password setup..." | |
| if [[ -n "${OP_SERVICE_ACCOUNT_TOKEN:-}" ]]; then | |
| echo " ✅ OP_SERVICE_ACCOUNT_TOKEN set" | |
| else | |
| echo "" | |
| echo " ⚠️ OP_SERVICE_ACCOUNT_TOKEN not set." | |
| echo " Get it from 1Password → Settings → Service Accounts → pi-infra" | |
| echo "" | |
| read -r -p " Paste token (or Enter to skip): " OP_TOKEN | |
| if [[ -n "${OP_TOKEN}" ]]; then | |
| export OP_SERVICE_ACCOUNT_TOKEN="${OP_TOKEN}" | |
| # Add to shell profile | |
| # Write to .zshenv (loaded by ALL shells, including non-interactive SSH) | |
| if ! grep -q "OP_SERVICE_ACCOUNT_TOKEN" "${HOME}/.zshenv" 2>/dev/null; then | |
| echo "export OP_SERVICE_ACCOUNT_TOKEN=\"${OP_TOKEN}\"" >> "${HOME}/.zshenv" | |
| echo " ✅ Added to ~/.zshenv" | |
| fi | |
| else | |
| echo " ⏭️ Skipped — run 'openclaw setup' after setting it" | |
| fi | |
| fi | |
| echo "" | |
| # ============================================================= | |
| # 9. INSTANCE CONFIGURATION (must run before chezmoi) | |
| # ============================================================= | |
| echo "🏷️ Instance setup..." | |
| echo "" | |
| echo " Each Pi instance needs an ID (single letter):" | |
| echo " o = Oliver" | |
| echo " m = Markus" | |
| echo " s = Susanne" | |
| echo "" | |
| read -r -p " Instance ID [o]: " INSTANCE_ID | |
| INSTANCE_ID="${INSTANCE_ID:-o}" | |
| echo "" | |
| # Map single letter to full instance name for chezmoi | |
| # Note: no associative arrays — macOS ships with Bash 3.2 | |
| case "${INSTANCE_ID}" in | |
| o) PION_INSTANCE="oliver" ;; | |
| m) PION_INSTANCE="markus" ;; | |
| s) PION_INSTANCE="susanne" ;; | |
| *) PION_INSTANCE="${INSTANCE_ID}" ;; | |
| esac | |
| export PION_INSTANCE | |
| # Store instance identity | |
| if ! grep -q "^export PI_INSTANCE_ID=" "${HOME}/.zshrc" 2>/dev/null; then | |
| echo "export PI_INSTANCE_ID=\"${INSTANCE_ID}\"" >> "${HOME}/.zshrc" | |
| fi | |
| # PION_INSTANCE in .zshenv (needed for SSH + chezmoi) | |
| if ! grep -q "^export PION_INSTANCE=" ~/.zshenv 2>/dev/null; then | |
| echo "export PION_INSTANCE=\"${PION_INSTANCE}\"" >> ~/.zshenv | |
| echo " ✅ PION_INSTANCE=${PION_INSTANCE} added to ~/.zshenv" | |
| fi | |
| if ! grep -q "${INSTANCE_ID}-gluon" "${HOME}/.zshrc" 2>/dev/null; then | |
| echo "PROMPT=\"🥧 ${INSTANCE_ID}-gluon %1~ \$ \"" >> "${HOME}/.zshrc" | |
| echo " ✅ Shell prompt set: 🥧 ${INSTANCE_ID}-gluon" | |
| fi | |
| # ============================================================= | |
| # 10. WORKSPACE — Clone instance repo | |
| # ============================================================= | |
| WORKSPACE_DIR="${HOME}/openclaw/workspace" | |
| INSTANCE_REPO="pionizer/pi-${PION_INSTANCE}" | |
| echo "📂 Setting up workspace..." | |
| if [[ -d "${WORKSPACE_DIR}/.git" ]]; then | |
| echo " ✅ ${WORKSPACE_DIR} exists" | |
| cd "${WORKSPACE_DIR}" && git pull --quiet | |
| else | |
| echo " 📦 Cloning ${INSTANCE_REPO}..." | |
| rm -rf "${WORKSPACE_DIR}" | |
| gh repo clone "${INSTANCE_REPO}" "${WORKSPACE_DIR}" | |
| fi | |
| echo "" | |
| # ============================================================= | |
| # 11. CHEZMOI — Generate secrets | |
| # ============================================================= | |
| if [[ -n "${OP_SERVICE_ACCOUNT_TOKEN:-}" ]]; then | |
| echo "🔑 Generating secrets via chezmoi..." | |
| rm -rf "${HOME}/.config/chezmoi" "${HOME}/.local/share/chezmoi" | |
| cp -r "${PIINFRA_DIR}/chezmoi" "${HOME}/.local/share/chezmoi" | |
| chezmoi init --force | |
| chezmoi apply --force | |
| echo " ✅ ~/.openclaw/.env generated" | |
| else | |
| echo "⏭️ Skipping chezmoi (no 1Password token)" | |
| fi | |
| # ============================================================= | |
| # 11. BUILD & START | |
| # ============================================================= | |
| echo "🏗️ Building Pi..." | |
| echo "" | |
| # OrbStack needs to be running | |
| if ! docker info &>/dev/null 2>&1; then | |
| # shellcheck disable=SC2312 # uname in command substitution is standard | |
| DOCKER_APP=$([[ "$(uname -m)" == "arm64" ]] && echo "OrbStack" || echo "Docker Desktop") | |
| echo " ⚠️ Docker not running. Start ${DOCKER_APP} first, then run:" | |
| echo " openclaw build" | |
| echo " openclaw tunnel setup --instance ${INSTANCE_ID}" | |
| echo "" | |
| else | |
| openclaw build | |
| echo "" | |
| # ============================================================= | |
| # 12. TUNNEL | |
| # ============================================================= | |
| echo "🔐 Setting up Cloudflare tunnel..." | |
| echo "" | |
| echo " ⚠️ This needs a tunnel credential file." | |
| echo " If this is a NEW instance, create a tunnel first:" | |
| echo " cloudflared tunnel create pi-${INSTANCE_ID}" | |
| echo "" | |
| read -r -p " Run tunnel setup now? [y/N]: " REPLY | |
| if [[ "${REPLY}" =~ ^[Yy]$ ]]; then | |
| openclaw tunnel setup --instance "${INSTANCE_ID}" | |
| fi | |
| fi | |
| echo "" | |
| echo "============================================" | |
| echo "✅ Pi bootstrap complete!" | |
| echo "============================================" | |
| echo "" | |
| echo " Instance: ${INSTANCE_ID}" | |
| echo " CLI: openclaw help" | |
| echo " Start: openclaw gateway start" | |
| echo " Build: openclaw build" | |
| echo " SSH: openclaw tunnel setup --instance ${INSTANCE_ID}" | |
| echo "" | |
| echo " If you skipped any steps, run: openclaw setup" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment