Files
platform/scripts/setup/officer-setup.sh
T
pastilhasandClaude Opus 5 8a0946ea39 officer-setup section 3: dependencies
bun install, as the account, in the checkout.

Two things stated because a failure here is otherwise opaque.

The lockfile is frozen — bunfig.toml sets [install] frozenLockfile = true — so
bun resolves from bun.lock and nothing else. A package.json that disagrees with
it is a hard failure rather than a quiet resolution, which is deliberate: the
friction exists so an unexplained lockfile change shows up in a diff. If the
install fails complaining about the lockfile, the section says that is the
frozen lockfile working and that it wants a human to read the diff, rather than
reporting a generic failure.

node-pty has no Linux prebuild, so this compiles it from source on every machine.
That is what build-essential and python3 are in machine-setup's core utils for,
and the section says so — the failure would otherwise surface much later as a
terminal that never starts.

Success is checked by the artefact rather than by the exit status: bun can
complete while the native module is not built, because it skips a dependency's
lifecycle scripts unless it trusts the package. So the section looks for
node_modules/node-pty/build/Release/*.node and, when it is missing, names the
consequence and the command that fixes it instead of reporting success.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 21:55:04 +00:00

295 lines
11 KiB
Bash
Executable File

#!/bin/bash
set -e
# =============================================================================
# officer-setup — the platform, on a machine that is already provisioned
#
# The second half of the install. machine-setup/ brings a blank box up to a
# usable machine; this puts Officer on top of it.
#
# Run as root: sudo scripts/setup/officer-setup.sh
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROGRESS_FILE="$SCRIPT_DIR/officer-setup/.setup-progress"
ONLY_STEP=""
while [[ $# -gt 0 ]]; do
case "$1" in
--only)
ONLY_STEP="${2:-}"
shift 2
;;
--only=*)
ONLY_STEP="${1#*=}"
shift
;;
-l | --list)
grep -oP '^step "\K[^"]+' "${BASH_SOURCE[0]}"
exit 0
;;
-h | --help)
echo "usage: officer-setup.sh [--only <step>] [--list]"
exit 0
;;
*) echo "unknown option: $1" >&2 && exit 2 ;;
esac
done
# shellcheck source=officer-setup/lib/base.sh
source "$SCRIPT_DIR/officer-setup/lib/base.sh"
# shellcheck source=officer-setup/lib/preflight.sh
source "$SCRIPT_DIR/officer-setup/lib/preflight.sh"
# shellcheck source=officer-setup/lib/repo.sh
source "$SCRIPT_DIR/officer-setup/lib/repo.sh"
trap 'echo ""; echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}"; echo -e "${RED}║ OFFICER SETUP FAILED${NC}"; echo -e "${RED}║ Step: ${CURRENT_STEP:-unknown}${NC}"; echo -e "${RED}║ Line: $LINENO${NC}"; echo -e "${RED}║ Command: $BASH_COMMAND${NC}"; echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}"' ERR
# =============================================================================
# 1. Pre-flight
# =============================================================================
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ Officer Setup ║${NC}"
echo -e "${BOLD}╚══════════════════════════════════════════════════╝${NC}"
if [[ "$EUID" -ne 0 ]]; then
fail "Please run as root: sudo ./officer-setup.sh"
fi
# ── what machine-setup already established ──
echo ""
if load_machine_answers; then
info "Read from machine-setup: ${MACHINE_ANSWERS}"
else
warn "machine-setup has not run on this machine"
echo " That is fine if you provisioned it another way — the questions it"
echo " would have answered are asked below instead."
fi
# ── the account ──
#
# A remembered answer can go stale: the account it names may have been renamed or
# removed since machine-setup ran. That is a reason to ask again, not a reason to
# stop — so the remembered value is checked before it is trusted, and a bad one
# is reported and replaced rather than ending the run.
if [[ -n "$USERNAME" ]] && ! owner_exists; then
warn "the remembered account '${USERNAME}' does not exist on this machine any more"
USERNAME=""
fi
while [[ -z "$USERNAME" ]] || ! owner_exists; do
echo ""
info "Which account owns this Officer install?"
echo " Its files, its node_modules and its pm2 process list all belong to"
echo " this account rather than to root."
echo ""
ask_required USERNAME "Username" "${SUDO_USER:-}"
owner_exists || warn "There is no account called '${USERNAME}' on this machine."
done
resolve_user_home
# ── where it goes ──
if [[ -z "$OFFICER_ROOT" ]]; then
echo ""
info "Where should Officer be installed?"
echo " One directory holding the app, its data, the item store and any"
echo " containers the app store provisions."
echo ""
ask_required OFFICER_ROOT "Path" "${USER_HOME}/officerdev"
fi
OFFICER_ROOT="${OFFICER_ROOT/#\~/$USER_HOME}"
[[ "$OFFICER_ROOT" == /* ]] || fail "That needs to be an absolute path — got '${OFFICER_ROOT}'"
OFFICER_ROOT="${OFFICER_ROOT%/}"
info "Account: ${USERNAME} (home ${USER_HOME})"
info "Officer: ${OFFICER_ROOT}"
[[ -n "$MACHINE_ROLE" ]] && info "Role: ${MACHINE_ROLE}"
# ── is the machine actually ready ──
#
# Checked and reported together. Finding out about a missing bun three sections
# in, after a repository has been cloned and a database started, is a worse way
# to learn it.
echo ""
info "What Officer needs from this machine"
mapfile -t MISSING < <(missing_tools)
mapfile -t MISSING_OPT < <(missing_optional_tools)
for t in "${REQUIRED_TOOLS[@]}"; do
if command -v "$t" &>/dev/null; then
printf ' %-6s %-10s %s\n' "$t" "ok" "$(tool_why "$t")"
else
printf ' %-6s %-10s %s\n' "$t" "MISSING" "$(tool_why "$t")"
fi
done
for t in "${OPTIONAL_TOOLS[@]}"; do
if command -v "$t" &>/dev/null; then
printf ' %-6s %-10s %s\n' "$t" "ok" "$(tool_why "$t")"
else
printf ' %-6s %-10s %s\n' "$t" "absent" "$(tool_why "$t") — optional"
fi
done
if ((${#MISSING[@]} > 0)); then
echo ""
fail "Missing: ${MISSING[*]}. Run scripts/setup/machine-setup/machine-setup.sh first, or install them yourself."
fi
if ((${#MISSING_OPT[@]} > 0)); then
echo ""
warn "No Docker. Postgres will have to be one you already run, and the app"
echo " store cannot provision anything until Docker is installed."
fi
if [[ -f "$PROGRESS_FILE" ]]; then
echo ""
info "Resuming — $(wc -l <"$PROGRESS_FILE") step(s) already done, and they will be skipped"
echo " To start over instead: sudo rm ${PROGRESS_FILE}"
else
echo ""
echo " This can be stopped at any point and run again later. Completed"
echo " steps are remembered and skipped."
fi
# =============================================================================
# 2. Repository
# =============================================================================
step "Repository"
if ! skip; then
PLATFORM_DIR="$(platform_dir)"
echo ""
info "Repository — where the platform's code lives"
echo " path: ${PLATFORM_DIR}"
if repo_exists; then
echo " remote: $(repo_remote)"
echo " branch: $(repo_branch)"
echo " working: $(repo_is_dirty && echo 'has uncommitted changes' || echo 'clean')"
# Reported, never silently corrected. Repointing somebody's remote is a
# decision about where their work goes, and this script is not entitled to
# make it quietly.
if [[ -n "$(repo_remote)" && "$(repo_remote)" != "$OFFICER_REPO" ]]; then
echo ""
warn "this checkout points somewhere other than ${OFFICER_REPO}"
echo " Left alone. To move it:"
echo " git -C ${PLATFORM_DIR} remote set-url origin ${OFFICER_REPO}"
fi
if repo_is_dirty; then
echo ""
echo " not pulling — there are uncommitted changes here, and a pull"
echo " would either fail or bury them"
SUMMARY+=("Repository: present at ${PLATFORM_DIR}, left alone (uncommitted changes)")
elif confirm "Pull the latest changes?"; then
if pull_repo; then
ok "up to date on $(repo_branch)"
SUMMARY+=("Repository: pulled, on $(repo_branch)")
else
# --ff-only, so this means the branch has diverged rather than that the
# network failed. Saying which matters.
warn "could not fast-forward — the local branch has diverged from the remote"
ERRORS+=("Repository: pull refused, branch diverged")
SUMMARY+=("Repository: present, pull refused (diverged)")
fi
else
SUMMARY+=("Repository: present at ${PLATFORM_DIR}")
fi
else
echo " nothing there yet"
echo ""
info "Clone from ${OFFICER_REPO}?"
echo " Cloned as ${USERNAME}, not as root — a repository owned by root is"
echo " one you cannot pull, commit in, or install into."
CLONE_URL="$OFFICER_REPO"
if confirm "Clone it now?"; then
if clone_repo "$CLONE_URL"; then
ok "cloned to ${PLATFORM_DIR} on $(repo_branch)"
SUMMARY+=("Repository: cloned from ${CLONE_URL}")
else
# GIT_TERMINAL_PROMPT=0 in clone_repo means this is a real failure rather
# than a prompt nobody answered.
fail "could not clone ${CLONE_URL} — nothing below can run without it."
fi
else
fail "Nothing below can run without the repository."
fi
fi
step_ok
fi
# =============================================================================
# 3. Dependencies
# =============================================================================
step "Dependencies"
if ! skip; then
echo ""
info "Dependencies — bun install, as ${USERNAME}"
echo " node_modules: $(deps_installed && echo present || echo 'not there')"
echo " node-pty: $(node_pty_built && echo built || echo 'not built')"
echo ""
echo " The lockfile is frozen: bun resolves from bun.lock and nothing else,"
echo " so a package.json that disagrees with it fails rather than quietly"
echo " picking newer versions. That friction is deliberate."
echo ""
echo " node-pty has no Linux prebuild, so this compiles it from source"
echo " every time — which is what build-essential and python3 are for."
if deps_installed && node_pty_built; then
ok "already installed, and node-pty is built"
SUMMARY+=("Dependencies: already installed")
elif confirm "Install them?"; then
if install_deps; then
if node_pty_built; then
ok "installed, node-pty built"
SUMMARY+=("Dependencies: installed")
else
# The install can succeed while the native module does not get built —
# bun skips a dependency's lifecycle scripts unless it trusts the
# package. Worth naming, because the symptom is a terminal that never
# comes up rather than an install error.
warn "installed, but node-pty has no built module at node_modules/node-pty/build/Release/"
echo " The terminal sidecar cannot start without it. Try:"
echo " cd $(platform_dir) && bun install --force"
ERRORS+=("Dependencies: node-pty not built")
SUMMARY+=("Dependencies: installed, node-pty NOT built")
fi
else
warn "bun install failed"
echo " If it complained about the lockfile, package.json and bun.lock"
echo " disagree — that is the frozen lockfile doing its job, and it"
echo " wants a human to look at the diff."
ERRORS+=("Dependencies: bun install failed")
SUMMARY+=("Dependencies: FAILED")
fi
else
warn "skipped by request"
SUMMARY+=("Dependencies: SKIPPED by request")
fi
step_ok
fi
# =============================================================================
# NOT BUILT YET
# =============================================================================
# 4 Database Postgres in docker, or one you already run
# 5 Environment .env
# 6 Schema db:push
# 7 Build gen:index
# 8 Services pm2 startOrRestart · save · startup
# 9 Verify are the processes actually up
echo ""
echo -e "${BOLD} Pre-flight complete.${NC} The remaining sections are not built yet."
echo ""