macOS is a dev machine, and machine-setup now treats it as one

Officer on a Mac is a dev helper on a laptop somebody sits at. It is never the
homelab or VPS case, so the role is not asked for there — it is `dev`, and every
section that exists to make a machine a good server is skipped.

Seventeen of twenty-six sections skip, listed once in MACOS_SKIP in lib/base.sh
with a reason each, rather than an `if macos` threaded through each section. Most
would simply fail — no systemd, no ufw, no netplan, no useradd, no
/etc/ssh/sshd_config.d — but a few would SUCCEED and be wrong, which is worse:
stopping a laptop from sleeping, or freezing the address of a machine that moves
between networks daily.

Nine run: System update, Core utils, Tailscale, Command-line tools, Git, Docker,
Neovim, JavaScript runtimes, Agent CLIs.

The blocker was root. Linux needs it for nearly everything; Homebrew REFUSES to
run as root and says so, so the whole script under sudo would have failed at the
first brew install having already taken a password. It is now required on Linux
and refused on macOS, which works precisely because the macOS path skips
everything that needed it.

Docker is checked, not installed. Docker Desktop is a GUI app that wants opening,
permissions and a running window — not a shell script's business — and colima and
lima both cost an evening the first time something does not resolve. So the step
reports whether the daemon answers and points at the download otherwise. The
group-vs-rootless choice below it is Linux only: Desktop runs containers in a VM
owned by whoever is logged in, so there is no group to join.

Added the Xcode command line tools as a macOS-only step, before anything that
builds. node-pty ships no prebuilt binary on any platform and always falls
through to node-gyp, so `bun install` cannot finish without a compiler — and it
fails deep in a dependency tree naming neither Xcode nor node-pty. `xcode-select
--install` opens a dialogue and returns immediately, so the step says to come
back rather than pretending to have waited.

Tailscale takes the cask, not install.sh — that script is a Linux package-manager
wrapper. The cask ships a usable CLI; the Mac App Store build is sandboxed and
does not.

Not run on a Mac. There isn't one here, so this is read from the code and from
what each tool documents, not observed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 02:29:56 +00:00
co-authored by Claude Opus 5
parent 7280d13b93
commit b075f1f882
7 changed files with 316 additions and 16 deletions
+97 -9
View File
@@ -119,15 +119,39 @@ else
echo " so coming back costs nothing."
fi
if [[ "$EUID" -ne 0 ]]; then
fail "Please run as root: sudo ./machine-setup.sh"
# ── root on Linux, NOT root on macOS ──
#
# The two are opposites and it is not a preference. On Linux nearly every section
# needs root — apt, systemd units, useradd, netplan, ufw. On macOS Homebrew
# REFUSES to run as root and says so; running the whole script under sudo there
# would fail at the first `brew install` having already asked for a password.
#
# It works out because the macOS path skips everything that needed root in the
# first place (see MACOS_SKIP in lib/base.sh). What is left — brew, the Xcode
# command line tools, the agent CLIs, bun — is all per-user by design.
if [[ "$OS" == "macos" ]]; then
if [[ "$EUID" -eq 0 ]]; then
fail "Do not run this with sudo on macOS — Homebrew refuses to run as root. Run it as yourself."
fi
else
if [[ "$EUID" -ne 0 ]]; then
fail "Please run as root: sudo ./machine-setup.sh"
fi
fi
ask_username
if id "$USERNAME" &>/dev/null; then
info "Account: ${USERNAME} (exists, home ${USER_HOME})"
# On macOS the account running the script IS the account, and there is nothing to
# create — the User account step is skipped entirely.
if [[ "$OS" == "macos" ]]; then
USERNAME="$(id -un)"
USER_HOME="$HOME"
info "Account: ${USERNAME} (you — macOS creates no accounts here)"
else
info "Account: ${USERNAME} (will be created, home ${USER_HOME})"
ask_username
if id "$USERNAME" &>/dev/null; then
info "Account: ${USERNAME} (exists, home ${USER_HOME})"
else
info "Account: ${USERNAME} (will be created, home ${USER_HOME})"
fi
fi
ask_officer_root
@@ -374,6 +398,34 @@ fi
# What the distribution provides: the six this script would break without, and
# the command-line tools that make a machine worth sitting at.
# macOS's compilers, before anything that might need to build a native module.
if [[ "$OS" == "macos" ]]; then
step "Xcode command line tools"
if ! skip; then
echo ""
if xcode_clt_installed; then
ok "already installed ($(xcode-select -p))"
SUMMARY+=("Xcode CLT: already installed")
else
info "Xcode command line tools — macOS's compilers"
echo " Needed because node-pty ships no prebuilt binary and compiles"
echo " from source on every machine, so 'bun install' cannot finish"
echo " without a compiler."
echo ""
if confirm "Start the install?"; then
xcode_clt_install
warn "a macOS dialogue has opened — finish it there, then re-run this step"
echo " ./machine-setup.sh --only 'Xcode command line tools'"
SUMMARY+=("Xcode CLT: install started in a GUI dialogue — finish it, then re-run")
else
warn "skipped — 'bun install' will fail on node-pty without it"
SUMMARY+=("Xcode CLT: SKIPPED by request")
fi
fi
step_ok
fi
fi
step "Core utils"
if ! skip; then
# shellcheck disable=SC2046 # word splitting is how the list is passed
@@ -1644,9 +1696,42 @@ if ! skip; then
info "Docker — containers, and how ${USERNAME} is allowed to talk to them"
echo " engine: $(docker_is_installed && docker --version 2>/dev/null | cut -d, -f1 || echo 'not installed')"
echo " daemon: $(docker_daemon_ok && echo 'reachable' || echo 'not reachable from here')"
echo " ${USERNAME}: $(user_in_docker_group && echo 'in the docker group' || echo 'not in the docker group')"
if [[ "$OS" != "macos" ]]; then
echo " ${USERNAME}: $(user_in_docker_group && echo 'in the docker group' || echo 'not in the docker group')"
fi
if ! docker_is_installed; then
# ── macOS: we do not install Docker, we check for it ──
#
# Docker Desktop is the only thing that works here without a fight. Lima and
# colima both technically run containers on a Mac and both cost an evening the
# first time something does not resolve, so this asks for Desktop by name
# rather than installing an alternative that will disappoint later.
#
# Not installed by the script either: it is a GUI app that wants to be opened,
# granted permissions and left running, none of which a shell script should be
# doing on somebody's laptop.
if [[ "$OS" == "macos" ]]; then
if docker_daemon_ok; then
ok "Docker Desktop is running"
SUMMARY+=("Docker: Docker Desktop running")
elif docker_is_installed; then
warn "the docker CLI is here but the daemon is not answering"
echo " Open Docker Desktop from Applications and let it finish starting."
SUMMARY+=("Docker: installed but not running — open Docker Desktop")
else
warn "Docker is not installed"
echo ""
echo " Officer needs it for Postgres and for anything the app store"
echo " installs. Get Docker Desktop:"
echo ""
echo " https://www.docker.com/products/docker-desktop/"
echo ""
echo " Open it once after installing, then run this step again:"
echo " ./machine-setup.sh --only Docker"
SUMMARY+=("Docker: NOT installed — install Docker Desktop, then re-run this step")
fi
step_ok
elif ! docker_is_installed; then
echo ""
echo " to install: docker-ce, the CLI, containerd, buildx and compose,"
echo " from Docker's own repository"
@@ -1665,7 +1750,10 @@ if ! skip; then
fi
fi
if docker_is_installed; then
# The group-vs-rootless choice below is Linux only: Docker Desktop runs
# containers in a VM owned by whoever is logged in, so there is no group to
# join and no rootless variant to pick.
if [[ "$OS" != "macos" ]] && docker_is_installed; then
# ── how this account reaches the daemon ──
if user_in_docker_group || docker_rootless_installed; then
echo ""