#!/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" 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" # Checked before cloning. An ssh URL with no usable key does not fail # cleanly: git prompts for a password nobody is there to type, or stops on # host-key verification. if [[ "$CLONE_URL" == ssh://* ]] && ! repo_ssh_ok "$CLONE_URL"; then echo "" warn "${USERNAME} cannot authenticate to $(repo_ssh_host "$CLONE_URL") over ssh" echo " Either add that account's public key to the git server, or clone" echo " over https instead — which works without a key if the repository" echo " is readable anonymously." echo "" echo " [1] https — $(repo_https_url "$CLONE_URL")" echo " [2] ssh anyway — will fail if the key is genuinely missing" echo " [3] stop here, and add the key first" echo "" REPO_PICK="" while [[ -z "$REPO_PICK" ]]; do if ! read -rp " Which one? (1/2/3) [1]: " REPO_CHOICE; then echo "" fail "No answer." fi case "${REPO_CHOICE:-1}" in 1) CLONE_URL="$(repo_https_url "$CLONE_URL")" REPO_PICK=go ;; 2) REPO_PICK=go ;; 3) fail "Stopped. Add ${USERNAME}'s public key to the git server and run this again." ;; *) warn "Pick 1, 2 or 3." ;; esac done fi 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 warn "the clone did not complete" ERRORS+=("Repository: clone failed") fail "Nothing below can run without the repository." fi else fail "Nothing below can run without the repository." fi fi step_ok fi # ============================================================================= # NOT BUILT YET # ============================================================================= # 3 Dependencies bun install # 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 ""