Files
platform/scripts/setup/officer-setup/lib/base.sh
T
pastilhasandClaude Opus 5 4d14e11f6c --unattended: every question that has a default answers itself
51 yes/no prompts and ~20 free-text ones, of which about six actually need a human.
The line drawn is "a question with a default answers itself; a question with no
possible default still asks", so it stays attended without being a conversation.

Half of it already existed: ASSUME_YES=1 was implemented and honoured by confirm()
in both scripts, returning each question's OWN default — so a "do the thing you
asked for" question goes yes and a genuine extra goes no. --unattended sets it.

The new part is menu_answer(), for the eight numbered menus. It sets the variable
EMPTY rather than passing a default in, because every menu already consumes its
choice as `${CHOICE:-<n>}` — the default lives next to the options it selects
between, which is the right place, and a second copy in the helper could drift from
the one the prompt advertises. Verified all eight consume that way before touching
them. `read <<<''` rather than eval or `declare -g`, which is bash 4.2+ and rules
out the bash 3.2 macOS still ships.

officer-setup's ask_required takes its default too, except where there is none — the
owning account on a machine machine-setup never ran on, where a guess would install
as the wrong user.

STILL ASKS, deliberately: the username; the Tailscale control plane, login server
and auth key; the git identity; and an SSH public key when the account has none.
That last one is a trap I nearly walked into — on a fresh VPS KEY_COUNT==0 forces
ADD_KEY=true with no confirm, and the menu's default is "[1] paste a public key",
which then prompts with no default at all. Auto-answering that menu would hang or
fail, so it is excluded by name. adduser also still asks for a password; that is
the tool, not us.

Two pre-existing bugs fixed on the way: machine-setup's sudo re-exec passed "$@"
after `shift` had emptied it, so --only and --reask stopped existing the moment it
escalated — same bug as officer-setup had. And UNATTENDED/ASSUME_YES are named in
all three sudo lists, because env_reset would otherwise drop the flag at
escalation, which is now the fourth variable lost that way.

Verified: bash -n on five files, --help on all three, and menu_answer + confirm
under the flag showing a menu resolving to its default and a no-default confirm
correctly answering no.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 06:46:48 +00:00

141 lines
4.1 KiB
Bash

#!/bin/bash
# =============================================================================
# officer-setup — shared foundation
# =============================================================================
#
# Sourced by officer-setup.sh before anything runs. DEFINITIONS ONLY, the same
# rule machine-setup/lib holds to: nothing here installs, writes or restarts.
#
# ── Why this is a separate script from machine-setup ──
#
# They answer different questions. machine-setup asks what a MACHINE should be —
# users, ssh, firewall, runtimes — and is worth running on a box that will never
# see Officer. This one puts Officer on a machine that is already ready, and
# assumes nothing about how it got that way.
#
# The split also means the failure modes stay apart: a broken firewall rule and a
# failed database migration are not the same kind of problem and should not be
# in the same run.
[[ -n "${OFFICER_SETUP_BASE_LOADED:-}" ]] && return 0
OFFICER_SETUP_BASE_LOADED=1
SUMMARY=()
ERRORS=()
CURRENT_STEP=""
SKIP_STEP=false
USERNAME="${SETUP_USERNAME:-}"
USER_HOME=""
OFFICER_ROOT="${OFFICER_ROOT:-}"
MACHINE_ROLE="${MACHINE_ROLE:-}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${CYAN}::${NC} $*"; }
ok() { echo -e " ${GREEN}OK${NC}: $*"; }
warn() { echo -e " ${YELLOW}WARN${NC}: $*"; }
fail() {
echo -e " ${RED}FAIL${NC}: $*"
exit 1
}
ONLY_STEP="${ONLY_STEP:-}"
step() {
CURRENT_STEP="$1"
if [[ -n "$ONLY_STEP" ]]; then
if [[ "${1,,}" == "${ONLY_STEP,,}" ]]; then
SKIP_STEP=false
echo ""
echo -e "${BOLD}── $1 ──${NC}"
else
SKIP_STEP=true
fi
return
fi
if grep -qxF "$1" "$PROGRESS_FILE" 2>/dev/null; then
echo -e " ${GREEN}SKIP${NC}: $1 (already done)"
SKIP_STEP=true
return
fi
SKIP_STEP=false
echo ""
echo -e "${BOLD}── $1 ──${NC}"
}
skip() { [[ "$SKIP_STEP" == true ]]; }
step_ok() {
[[ -n "$ONLY_STEP" ]] && return 0
echo "$CURRENT_STEP" >>"$PROGRESS_FILE"
}
page() {
if [[ -t 1 ]] && command -v more &>/dev/null; then more; else cat; fi
}
confirm() {
local message="${1:-Proceed?}" default="${2:-y}" help_fn="${3:-}" answer prompt
[[ "${ASSUME_YES:-}" == "1" ]] && { [[ "$default" == "y" ]] && return 0 || return 1; }
if [[ "$default" == "y" ]]; then prompt="[Y/n]"; else prompt="[y/N]"; fi
[[ -n "$help_fn" ]] && prompt="${prompt%]}/?]"
while true; do
if ! read -rp " ${message} ${prompt}: " answer; then
echo ""
fail "No answer. Set ASSUME_YES=1 to run without prompts."
fi
[[ -z "$answer" ]] && answer="$default"
case "$answer" in
y | Y | yes | Yes) return 0 ;;
n | N | no | No) return 1 ;;
"?")
if [[ -n "$help_fn" ]]; then
echo ""
"$help_fn" | page
echo ""
else
warn "Answer y or n."
fi
;;
*) warn "Answer y or n${help_fn:+, or ? for what this is}." ;;
esac
done
}
ask_required() {
local __var="$1" message="$2" default="$3" answer=""
# Unattended takes the default where there IS one. Where there is not — the owning
# account on a machine that machine-setup never ran on — it still asks, because
# there is nothing to fall back to and a guess would install as the wrong user.
if [[ "${UNATTENDED:-}" == "1" && -n "$default" ]]; then
printf ' %s [%s] — unattended, taking the default\n' "$message" "$default"
printf -v "$__var" '%s' "$default"
return 0
fi
while [[ -z "$answer" ]]; do
if ! read -rp " ${message}${default:+ [$default]}: " answer; then
echo ""
fail "No answer."
fi
answer="${answer:-$default}"
[[ -z "$answer" ]] && warn "This one cannot be left blank."
done
printf -v "$__var" '%s' "$answer"
}
user_group() { id -gn "${1:-$USERNAME}" 2>/dev/null || echo "${1:-$USERNAME}"; }
# Run something as the account that owns the install. Officer's files, its
# node_modules and its pm2 process list all belong to that account, not to root —
# a repository cloned as root is one the owner cannot pull.
as_owner() { (cd "${2:-/}" && sudo -H -u "$USERNAME" bash -c "$1"); }