fix(setup): make setup.sh self-sufficient on macOS (no admin required) (#354)

Fresh-install testing on macOS 12 Intel surfaced five friction points that
blocked or silently broke installation for users without admin rights.
All fixes are Mac-only, gated behind a new IS_MACOS flag. Linux and
Windows paths are untouched. See #353 for full context.

1. Stock macOS 12 ships /usr/bin/python3 at 3.9.6; setup.sh aborts at
   the 3.10+ minimum check. Fix: probe versioned python3.10-3.13
   binaries before falling back to plain python3.

2. Homebrew auto-install is not a universal fallback. Non-admin Mac
   accounts cannot install Homebrew at all because its installer
   requires admin/sudo. Fix: add uv (astral.sh/uv) as a no-sudo,
   no-admin fallback. uv drops into ~/.local/bin via curl and downloads
   a prebuilt standalone Python 3.11 to ~/.local/share/uv/python; the
   venv is created from that.

3. macOS defaults to zsh since Catalina (2019). Writing AIPASS_HOME to
   ~/.bashrc silently fails because zsh does not source it. Fix: on
   Mac, pick ~/.zshrc (or ~/.bash_profile for bash) based on SHELL.

4. The final symlink used sudo ln -sf /usr/local/bin/drone, which
   prompts for a password on Mac and breaks entirely for non-admin
   users. Fix: on Mac, link into ~/.local/bin (user-writable, no sudo)
   and ensure it is on PATH via the profile rc.

5. HOOK_PYTHON was set to plain python3 on Unix. On Mac that resolves
   to /usr/bin/python3 (3.9.6), which cannot parse hook scripts using
   PEP 604 union syntax (X | None). Hooks silently crash on every
   prompt. Fix: on Mac, point HOOK_PYTHON at the venv python (3.11)
   that setup just built.

Tested end-to-end on a fresh clone, non-admin user, zsh, Intel: all 11
branches bootstrapped, drone CLI works, hooks fire and execute cleanly,
ai_mail delivery verified, sub-agents spawn successfully.

Refs #353

Co-authored-by: Paddy <padddypaddypaddy-boop@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
padddypaddypaddy-boop
2026-04-20 11:16:39 -07:00
committed by GitHub
co-authored by Paddy Claude Opus 4.7
parent 6998cc5ba7
commit c941c3d6ac
+139 -13
View File
@@ -11,14 +11,18 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# --- OS detection ---
# Detect Windows (Git Bash / MSYS2 / Cygwin) — used throughout the script
# Detect Windows (Git Bash / MSYS2 / Cygwin) and macOS — used throughout the script
IS_WINDOWS=0
IS_MACOS=0
case "${OSTYPE:-}" in
msys*|cygwin*|mingw*) IS_WINDOWS=1 ;;
darwin*) IS_MACOS=1 ;;
*)
# Fallback: check uname if OSTYPE is unset
if uname -s 2>/dev/null | grep -qi "mingw\|msys\|cygwin"; then
IS_WINDOWS=1
elif uname -s 2>/dev/null | grep -qi "darwin"; then
IS_MACOS=1
fi
;;
esac
@@ -35,13 +39,25 @@ fi
# --- Find working Python (#292: Windows python3 → MS Store alias) ---
PYTHON=""
# Try python3 first — verify it actually runs (not just exists on PATH)
if command -v python3 &>/dev/null && python3 -c "import sys" &>/dev/null 2>&1; then
PYTHON="python3"
# Fall back to python (Windows installs as 'python' not 'python3')
elif command -v python &>/dev/null && python -c "import sys" &>/dev/null 2>&1; then
PYTHON="python"
else
# Probe versioned binaries first — on macOS, Homebrew installs Python as
# python3.11 and stock python3 may still point to 3.9. Checking versioned
# names first finds a suitable interpreter without auto-install on Mac too.
for v in 3.13 3.12 3.11 3.10; do
if command -v "python$v" &>/dev/null && "python$v" -c "import sys" &>/dev/null 2>&1; then
PYTHON="python$v"
break
fi
done
# Fall back to python3, then python (Windows installs as 'python' not 'python3')
if [ -z "$PYTHON" ]; then
if command -v python3 &>/dev/null && python3 -c "import sys" &>/dev/null 2>&1; then
PYTHON="python3"
elif command -v python &>/dev/null && python -c "import sys" &>/dev/null 2>&1; then
PYTHON="python"
fi
fi
if [ -z "$PYTHON" ]; then
echo "FAIL: No working Python found. Install Python 3.10+ and try again."
echo " Windows: install from python.org, NOT the Microsoft Store."
echo " Then disable the Store alias: Settings > Apps > Advanced app settings > App execution aliases"
@@ -54,8 +70,67 @@ echo "Found $PYTHON $PY_VERSION"
# --- Check minimum version ---
PY_OK=$($PYTHON -c 'import sys; print(int(sys.version_info >= (3, 10)))')
if [ "$PY_OK" != "1" ]; then
echo "FAIL: Python 3.10+ required, found $PY_VERSION"
exit 1
if [ "$IS_MACOS" -eq 1 ]; then
# Auto-install on Mac. Stock macOS 12 ships only python3 3.9 and has no
# versioned binaries. Try Homebrew if it's already installed (no admin
# needed to USE brew — only to install it), then fall back to uv, which
# installs entirely in user-space and works on non-admin Mac accounts.
echo "Python 3.10+ not found on this Mac. Attempting auto-install ..."
# Path 1: existing Homebrew. Don't attempt to install brew itself —
# that step requires admin/sudo and locks out non-admin accounts.
if command -v brew &>/dev/null; then
echo "Homebrew present — installing python@3.11 via brew ..."
if brew install python@3.11; then
if command -v python3.11 &>/dev/null; then
PYTHON="python3.11"
elif [ -x /opt/homebrew/opt/python@3.11/bin/python3.11 ]; then
PYTHON="/opt/homebrew/opt/python@3.11/bin/python3.11"
elif [ -x /usr/local/opt/python@3.11/bin/python3.11 ]; then
PYTHON="/usr/local/opt/python@3.11/bin/python3.11"
fi
fi
if [ -n "$PYTHON" ] && "$PYTHON" -c 'import sys; exit(0 if sys.version_info >= (3, 10) else 1)' 2>/dev/null; then
PY_OK=1
fi
fi
# Path 2: uv — no sudo, no admin, works on any account.
# Installs a prebuilt standalone Python to ~/.local/share/uv/python.
if [ "$PY_OK" != "1" ]; then
echo "Using uv (no-sudo Python installer) ..."
if ! command -v uv &>/dev/null; then
echo "Installing uv to ~/.local/bin ..."
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
fi
if ! command -v uv &>/dev/null; then
echo "FAIL: uv install did not succeed."
echo "Install Python 3.10+ manually from https://www.python.org/downloads/ and retry."
exit 1
fi
echo "Downloading Python 3.11 via uv ..."
uv python install 3.11
# Locate the installed python
UV_PY=$(uv python find 3.11 2>/dev/null || true)
if [ -z "$UV_PY" ] || [ ! -x "$UV_PY" ]; then
UV_PY=$(ls -1 "$HOME/.local/share/uv/python/"*"/bin/python3.11" 2>/dev/null | head -1)
fi
if [ -n "$UV_PY" ] && [ -x "$UV_PY" ]; then
PYTHON="$UV_PY"
PY_OK=1
else
echo "FAIL: uv installed but python3.11 binary not located."
exit 1
fi
fi
PY_VERSION=$("$PYTHON" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
echo "Now using $PYTHON $PY_VERSION"
else
echo "FAIL: Python 3.10+ required, found $PY_VERSION"
exit 1
fi
fi
# --- Create venv ---
@@ -380,9 +455,17 @@ if [ -d "$SCRIPT_DIR/.claude/hooks" ]; then
echo "Installing Claude Code hooks ..."
mkdir -p "$HOME/.claude"
# Determine python command for hooks — venv python on Windows, python3 on Unix
# Determine python command for hooks.
# Linux: keep "python3" — distros ship 3.10+ and hooks import nothing
# version-specific beyond that. Leaving this path unchanged per Linux stability.
# macOS: stock /usr/bin/python3 is 3.9.6 on macOS 12 and cannot parse hook
# scripts that use PEP 604 union syntax (`X | None`). Point at the venv
# python, which setup just built with a 3.10+ interpreter.
# Windows: existing venv-python behavior.
if [ "$IS_WINDOWS" -eq 1 ]; then
HOOK_PYTHON="$SCRIPT_DIR/.venv/Scripts/python.exe"
elif [ "$IS_MACOS" -eq 1 ]; then
HOOK_PYTHON="$SCRIPT_DIR/.venv/bin/python3"
else
HOOK_PYTHON="python3"
fi
@@ -615,8 +698,19 @@ PSWRAP
echo " PowerShell drone wrapper already in $PS_PROFILE"
fi
else
# Linux/macOS: write to ~/.bashrc
PROFILE="$HOME/.bashrc"
# Linux/macOS: write to the user's shell rc.
# macOS default shell has been zsh since Catalina (2019) — stock macOS
# will not source ~/.bashrc, so exports there are invisible. Pick the
# right rc based on $SHELL on Mac; leave Linux behavior as-is (~/.bashrc).
if [ "$IS_MACOS" -eq 1 ]; then
case "${SHELL:-}" in
*/zsh) PROFILE="$HOME/.zshrc" ;;
*/bash) PROFILE="$HOME/.bash_profile" ;; # Mac bash login shell sources .bash_profile, not .bashrc
*) PROFILE="$HOME/.zshrc" ;; # zsh is the macOS default; sensible fallback
esac
else
PROFILE="$HOME/.bashrc"
fi
# AIPASS_HOME
if ! grep -q "AIPASS_HOME" "$PROFILE" 2>/dev/null; then
@@ -628,6 +722,18 @@ else
echo " AIPASS_HOME already in $PROFILE"
fi
# Mac: ensure ~/.local/bin is on PATH so the drone symlink resolves.
# Linux already symlinks into /usr/local/bin (already on PATH everywhere).
if [ "$IS_MACOS" -eq 1 ]; then
if ! grep -q '\.local/bin' "$PROFILE" 2>/dev/null; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$PROFILE"
echo " ~/.local/bin added to PATH in $PROFILE"
else
echo " ~/.local/bin already on PATH in $PROFILE"
fi
export PATH="$HOME/.local/bin:$PATH"
fi
export AIPASS_HOME="$SCRIPT_DIR"
fi
@@ -635,6 +741,24 @@ fi
echo ""
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Windows: drone available via PATH (set above)"
elif [ "$IS_MACOS" -eq 1 ]; then
# Mac: symlink into ~/.local/bin (user-writable, no sudo needed).
# PATH export for ~/.local/bin is handled in the profile block above.
echo "Creating user symlinks in ~/.local/bin ..."
VENV_BIN="$SCRIPT_DIR/.venv/bin"
LOCAL_BIN="$HOME/.local/bin"
mkdir -p "$LOCAL_BIN"
for cmd in drone; do
if [ -f "$VENV_BIN/$cmd" ]; then
if ln -sf "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd"; then
echo " $LOCAL_BIN/$cmd -> $VENV_BIN/$cmd"
else
echo " WARN: Could not create symlink for $cmd"
echo " Manual fix: ln -sf $VENV_BIN/$cmd $LOCAL_BIN/$cmd"
fi
fi
done
else
echo "Creating global symlinks ..."
VENV_BIN="$SCRIPT_DIR/.venv/bin"
@@ -660,6 +784,8 @@ if [ "$FAIL" -eq 0 ]; then
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "drone is available in .venv/Scripts/ (or .venv/bin/ for Git Bash)."
echo "Add the appropriate directory to your PATH (see above)."
elif [ "$IS_MACOS" -eq 1 ]; then
echo "drone is available via ~/.local/bin symlink (on PATH)."
else
echo "drone is available globally via /usr/local/bin symlink."
fi