start officer-setup: pre-flight, inheriting what machine-setup already asked
The second half of the install, and a much smaller script than the original: of the old setup.sh's thirteen sections, seven are machine-setup's job now and two more were already removed. What is left is the repository, dependencies, the database, .env, the schema, the build and pm2. Pre-flight asks nothing on a normal run. machine-setup saves the account, the Officer path and the role beside itself, and this reads the same file — so machine-setup then officer-setup is two scripts and one set of answers. It prompts only where that file is absent, which is a supported case rather than an error: somebody may have provisioned the box their own way. It then checks the machine is actually ready — git, node, bun and pm2 required, docker optional — and reports all of them together with what each is for. 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. A missing required tool stops the run and names machine-setup. Found by running it: a remembered answer can go stale. My own earlier testing had left SETUP_USERNAME=gitfresh in that file, for a throwaway account I then deleted, and the run dead-ended on it. A remembered account that no longer exists is a reason to ask again, not a reason to stop — so it is checked before it is trusted, reported, and replaced. Docker being absent is a warning rather than a failure: Postgres can be one you already run, and the app store simply cannot provision until Docker is there. Sections 2 to 9 are listed and not built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,3 +55,6 @@ public/plugins/
|
|||||||
# Written by machine-setup.sh to record completed steps; per-machine, never shared.
|
# Written by machine-setup.sh to record completed steps; per-machine, never shared.
|
||||||
.setup-progress
|
.setup-progress
|
||||||
.setup-answers
|
.setup-answers
|
||||||
|
|
||||||
|
# Written by officer-setup.sh; per-machine.
|
||||||
|
scripts/setup/officer-setup/.setup-progress
|
||||||
|
|||||||
+157
-16
@@ -5,26 +5,167 @@ set -e
|
|||||||
# officer-setup — the platform, on a machine that is already provisioned
|
# 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
|
# The second half of the install. machine-setup/ brings a blank box up to a
|
||||||
# usable machine; this puts Officer on top of it: dependencies, .env, the
|
# usable machine; this puts Officer on top of it.
|
||||||
# database schema, and the pm2 process list.
|
|
||||||
#
|
#
|
||||||
# Run: bash scripts/setup/officer-setup.sh
|
# 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"
|
||||||
|
|
||||||
|
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
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
#
|
|
||||||
# NOT WRITTEN YET. It is being rebuilt from scripts/setup-old/setup.sh, which
|
|
||||||
# still works and is still the one to run.
|
|
||||||
#
|
|
||||||
# This file exists ahead of its contents so that `bun setup` points at where the
|
|
||||||
# installer is going rather than at a path that is about to move again. It fails
|
|
||||||
# loudly rather than sitting empty, because an empty script exits 0 and would
|
|
||||||
# make `bun setup` report success while doing nothing at all.
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " officer-setup.sh is not written yet."
|
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 ""
|
echo ""
|
||||||
echo " It is being rebuilt from scripts/setup-old/setup.sh, which still works:"
|
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 ""
|
echo ""
|
||||||
echo " bash scripts/setup-old/setup.sh # full install"
|
info "What Officer needs from this machine"
|
||||||
echo " OFFICER_PROFILE=light bash scripts/setup-old/setup.sh # light install"
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# NOT BUILT YET
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# 2 Repository clone or locate the platform at $OFFICER_ROOT/platform
|
||||||
|
# 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 ""
|
echo ""
|
||||||
exit 1
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#!/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=""
|
||||||
|
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"); }
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# officer-setup — is this machine ready
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# Definitions only.
|
||||||
|
#
|
||||||
|
# ── Inherited, not re-asked ──
|
||||||
|
#
|
||||||
|
# machine-setup saves the account, the Officer path and the machine role beside
|
||||||
|
# itself. This reads the same file, so a normal run — machine-setup, then this —
|
||||||
|
# asks nothing at all. It only prompts on a machine where machine-setup never
|
||||||
|
# ran, which is a supported case rather than an error: somebody may have
|
||||||
|
# provisioned the box their own way.
|
||||||
|
|
||||||
|
[[ -n "${OFFICER_SETUP_PREFLIGHT_LOADED:-}" ]] && return 0
|
||||||
|
OFFICER_SETUP_PREFLIGHT_LOADED=1
|
||||||
|
|
||||||
|
# Where machine-setup keeps what it was told. Beside this script, one directory
|
||||||
|
# across.
|
||||||
|
MACHINE_ANSWERS="${MACHINE_ANSWERS:-${SCRIPT_DIR}/machine-setup/.setup-answers}"
|
||||||
|
|
||||||
|
# Read as assignments rather than sourced: the file is read by a root run and
|
||||||
|
# sourcing it would make it executable content.
|
||||||
|
load_machine_answers() {
|
||||||
|
[[ -r "$MACHINE_ANSWERS" ]] || return 1
|
||||||
|
local key value
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
[[ "$key" =~ ^[A-Z_]+$ ]] || continue
|
||||||
|
[[ -n "$value" ]] || continue
|
||||||
|
case "$key" in
|
||||||
|
MACHINE_ROLE) if [[ -z "$MACHINE_ROLE" ]]; then MACHINE_ROLE="$value"; fi ;;
|
||||||
|
SETUP_USERNAME) if [[ -z "$USERNAME" ]]; then USERNAME="$value"; fi ;;
|
||||||
|
OFFICER_ROOT) if [[ -z "$OFFICER_ROOT" ]]; then OFFICER_ROOT="$value"; fi ;;
|
||||||
|
esac
|
||||||
|
done <"$MACHINE_ANSWERS"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# What Officer needs to already be here, and what installs it.
|
||||||
|
#
|
||||||
|
# Checked together and reported together: finding out about a missing bun three
|
||||||
|
# sections in, after the repository has been cloned and a database started, is a
|
||||||
|
# worse way to learn it than being told at the start.
|
||||||
|
REQUIRED_TOOLS=(git node bun pm2)
|
||||||
|
OPTIONAL_TOOLS=(docker)
|
||||||
|
|
||||||
|
missing_tools() {
|
||||||
|
local t
|
||||||
|
for t in "${REQUIRED_TOOLS[@]}"; do command -v "$t" &>/dev/null || echo "$t"; done
|
||||||
|
}
|
||||||
|
|
||||||
|
missing_optional_tools() {
|
||||||
|
local t
|
||||||
|
for t in "${OPTIONAL_TOOLS[@]}"; do command -v "$t" &>/dev/null || echo "$t"; done
|
||||||
|
}
|
||||||
|
|
||||||
|
tool_why() {
|
||||||
|
case "$1" in
|
||||||
|
git) echo "to clone and update the platform" ;;
|
||||||
|
node) echo "pm2 runs on it, and the terminal sidecar builds node-pty against it" ;;
|
||||||
|
bun) echo "the platform itself and nineteen of the twenty processes" ;;
|
||||||
|
pm2) echo "supervises every process; the ecosystem files are written for it" ;;
|
||||||
|
docker) echo "Postgres, and anything the app store provisions" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The account has to exist before anything is written to its home.
|
||||||
|
owner_exists() { id "$USERNAME" &>/dev/null; }
|
||||||
|
|
||||||
|
resolve_user_home() {
|
||||||
|
USER_HOME="$(getent passwd "$USERNAME" 2>/dev/null | cut -d: -f6)"
|
||||||
|
[[ -n "$USER_HOME" ]] || USER_HOME="/home/${USERNAME}"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user