#!/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 ] [--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" # shellcheck source=officer-setup/lib/layout.sh source "$SCRIPT_DIR/officer-setup/lib/layout.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. Layout # ============================================================================= # # Before the repository, because the repository is cloned into it. step "Layout" if ! skip; then echo "" info "Layout — everything Officer owns, under one root" echo " ${OFFICER_ROOT}/" echo " platform/ the app" echo " data/ managed homes, attachments, job logs" echo " dockers/ anything the app store provisions" echo " capabilities/ skills, tools, tasks, processes" echo "" echo " Nothing here is configurable. The original asked separately for the" echo " data directory and the item store, which were two answers that had" echo " to agree with each other. One root now, and the rest follows." echo "" echo " To put data/ on a bigger volume later, symlink it — that is a" echo " decision about storage rather than about how Officer is laid out." mapfile -t WRONG_OWNER < <(layout_wrong_owner) if ((${#WRONG_OWNER[@]} > 0)); then echo "" warn "these exist but do not belong to ${USERNAME}:" printf ' %s\n' "${WRONG_OWNER[@]}" echo " Everything that writes into them runs as ${USERNAME} — the platform" echo " under pm2, the app store's compose files, the item store the agent" echo " authors into. Left as they are, those writes fail in a way that" echo " reads as a bug in the platform." if confirm "Give them to ${USERNAME}?"; then for d in "${WRONG_OWNER[@]}"; do chown -R "${USERNAME}:$(user_group)" "$d"; done ok "ownership corrected" SUMMARY+=("Layout: ownership corrected on ${#WRONG_OWNER[@]} directory(ies)") fi fi create_layout ok "layout in place under ${OFFICER_ROOT}" SUMMARY+=("Layout: ${OFFICER_ROOT} (data, dockers, capabilities)") step_ok fi # ============================================================================= # 3. 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 # ============================================================================= # 4. 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 ""