From 3cca07187e6e421f09a83533afd68aba89c11849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 13 Aug 2026 02:49:44 +0000 Subject: [PATCH] =?UTF-8?q?scripts/install.sh=20=E2=80=94=20one=20command?= =?UTF-8?q?=20for=20both=20halves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bun setup` runs it. Machine setup first, then officer setup, stopping if the first does not finish rather than running the second against a machine that is not ready. They stay two scripts because they answer two different questions and are worth running apart — a machine you already trust needs only the second, one you are rebuilding needs only the first. --machine-only and --officer-only say so directly, and both halves remain runnable by path. Privileges are checked here, before anything is done, because the two systems want opposite things: Linux needs root for apt, systemd, useradd, netplan and ufw and for creating directories owned by the service account; macOS must NOT be root, since Homebrew refuses to run as one. Each script already enforces its own rule, so this is only about failing early instead of halfway. officer-setup.sh gained the same OS-aware check. It required root unconditionally, which on macOS would have failed immediately after machine-setup — which must run as the user — and for no reason: there the account running it IS the owner, so there is nothing to chown and nothing to drop privileges to. Arguments are parsed before privileges, so --help works without sudo and an unknown option is rejected before anybody is asked for a password. It did not, first time round. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 2 +- scripts/install.sh | 103 +++++++++++++++++++++++++++++++++ scripts/setup/officer-setup.sh | 12 +++- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100755 scripts/install.sh diff --git a/package.json b/package.json index cc98348a..e2e11a57 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "format": "{ git diff --name-only HEAD -- 'src/**/*.ts' 'src/**/*.tsx'; git ls-files --others --exclude-standard -- 'src/**/*.ts' 'src/**/*.tsx'; } | xargs -r prettier --write", "format:all": "prettier --write \"src/**/*.{ts,tsx}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", - "setup": "bash scripts/setup/officer-setup.sh" + "setup": "bash scripts/install.sh" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.41", diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..558b8543 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# ============================================================================= +# Officer — install +# ============================================================================= +# +# One command, blank machine to running platform. It runs the two halves in +# order and does nothing else itself: +# +# setup/machine-setup/machine-setup.sh a usable machine — packages, tailnet, +# runtimes, docker, shell +# setup/officer-setup.sh the platform on top of it — repo, +# dependencies, postgres, .env, secret +# store, schema, build, pm2 +# +# They stay two scripts because they answer two different questions and are worth +# running separately: a machine you already trust needs only the second, and a +# machine you are rebuilding needs only the first. This is the wrapper for the +# case where you want both, which is most first runs. +# +# Both are re-runnable. Each remembers the steps it finished and skips them, so +# stopping halfway and coming back costs nothing. +# +# ── Privileges ── +# +# Linux needs root: apt, systemd units, useradd, netplan, ufw, and creating +# directories owned by the service account. macOS must NOT be root: Homebrew +# refuses to run as one, and there is nothing to chown because the account +# running this IS the owner. Both scripts enforce that themselves; this checks +# first so the failure arrives before anything has been done. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MACHINE="$SCRIPT_DIR/setup/machine-setup/machine-setup.sh" +OFFICER="$SCRIPT_DIR/setup/officer-setup.sh" + +BOLD='\033[1m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +say() { echo -e "$*"; } +die() { + echo -e "${YELLOW}error:${NC} $*" >&2 + exit 1 +} + +[[ -r "$MACHINE" ]] || die "missing $MACHINE" +[[ -r "$OFFICER" ]] || die "missing $OFFICER" + +# Which halves to run. Both by default. +RUN_MACHINE=true +RUN_OFFICER=true +for arg in "$@"; do + case "$arg" in + --machine-only) RUN_OFFICER=false ;; + --officer-only) RUN_MACHINE=false ;; + -h | --help) + say "usage: install.sh [--machine-only | --officer-only]" + say "" + say " no flags both halves, machine first" + say " --machine-only stop after the machine is provisioned" + say " --officer-only the platform only, on a machine you already trust" + exit 0 + ;; + *) die "unknown option: $arg" ;; + esac +done + +KERNEL="$(uname -s)" +case "$KERNEL" in + Darwin) + [[ "$EUID" -eq 0 ]] && die "do not run this with sudo on macOS — Homebrew refuses to run as root. Run it as yourself." + ;; + Linux) + [[ "$EUID" -ne 0 ]] && die "please run as root: sudo ./scripts/install.sh" + ;; + *) die "unsupported system: $KERNEL. Officer installs on Linux and macOS." ;; +esac + +say "" +say "${BOLD}Officer install${NC}" +say " system: $KERNEL" +$RUN_MACHINE && say " 1/2 machine setup" +$RUN_OFFICER && say " $($RUN_MACHINE && echo 2/2 || echo 1/1) officer setup" +say "" +say " Either half can be run on its own later:" +say " scripts/setup/machine-setup/machine-setup.sh" +say " scripts/setup/officer-setup.sh" +say "" + +# Not `set -e`'s job: a half that exits non-zero should say which half, and stop +# before the next one starts on a machine that is not ready for it. +if $RUN_MACHINE; then + bash "$MACHINE" || die "machine setup did not finish — fix what it reported, then run this again" +fi + +if $RUN_OFFICER; then + bash "$OFFICER" || die "officer setup did not finish — fix what it reported, then run this again" +fi + +say "" +say "${GREEN}Done.${NC}" diff --git a/scripts/setup/officer-setup.sh b/scripts/setup/officer-setup.sh index 946f4303..ef6c5501 100755 --- a/scripts/setup/officer-setup.sh +++ b/scripts/setup/officer-setup.sh @@ -66,7 +66,17 @@ echo -e "${BOLD}╔════════════════════ echo -e "${BOLD}║ Officer Setup ║${NC}" echo -e "${BOLD}╚══════════════════════════════════════════════════╝${NC}" -if [[ "$EUID" -ne 0 ]]; then +# root on Linux, NOT root on macOS — the same split machine-setup makes, for the +# same reason. On Linux this creates directories owned by another account and +# drops privileges with `sudo -u`. On macOS the account running the script IS the +# owner, so there is nothing to chown and nothing to drop to — and Homebrew, which +# machine-setup ran just before this, refuses to run as root at all. +OFFICER_OS="$(uname -s)" +if [[ "$OFFICER_OS" == "Darwin" ]]; then + if [[ "$EUID" -eq 0 ]]; then + fail "Do not run this with sudo on macOS — run it as yourself." + fi +elif [[ "$EUID" -ne 0 ]]; then fail "Please run as root: sudo ./officer-setup.sh" fi