sharp does the images now; ffmpeg keeps the video. each does the job it is for — sharp is libvips in-process, no process to spawn and no pipe, which is an order of magnitude less work for the common case of a folder of photographs. a video has no frame until something decodes one, and that is ffmpeg's and nobody else's. added through the procedure bunfig.toml describes, since there is no --no-frozen-lockfile flag in bun 1.3.14: package.json edited, frozenLockfile commented out for ONE command, lockfile diff read, setting put back, and a frozen install confirmed clean afterwards. WHAT THE LOCKFILE DIFF ACTUALLY SAID, because the friction is only worth anything if somebody reads it. everything is inside sharp's tree: sharp 0.33.5 -> 0.35.4 at top level, @img/colour added, and the per-platform libvips binaries moved 1.2.4 -> 1.3.3. two SHARED packages moved with it — semver 7.7.4 -> 7.8.5 and @emnapi/runtime 1.8.1 -> 1.11.3 — both forced by sharp's own ranges, and both previous consumers (next, editorconfig) kept pinned copies of what they had. next keeps its own sharp at 0.33.5 entirely. nothing outside that moved. ^0.35.3 to match what av-club declares. 0.x caret pins the minor, so ^0.33 and ^0.35 do not overlap — the plugin intersection would have resolved two native libvips trees onto one machine for no reason. THE IMAGE LIST GREW: libvips reads HEIC and SVG and ffmpeg does not, and neither is exotic — HEIC is what every recent iPhone writes, and a folder of them drawing generic file icons was the most visible gap this had. `failOn: 'none'` and `limitInputPixels: false` are both deliberate. libvips rejects a file with any warning in it, and a truncated JPEG — an interrupted copy, a phone that died mid-write — is exactly that; refusing it would make this browser stricter than the viewer somebody opens the file with. the pixel ceiling is a DoS guard for an upload endpoint, and these are the owner's own files already on the owner's own disk: a panorama is not an attack. SETUP, both halves: ffmpeg was never in the package list at all, on any of the four package managers, despite this router shelling out to it for subtitles, audio extraction and audio tracks long before thumbnails existed. it worked because every machine set up by hand already had it. now declared. sharp gets a LOAD CHECK beside node-pty's build check, and reported separately from it. same failure shape — an install that succeeds while the native module does not — but the opposite cause: node-pty compiles and can fail to, sharp downloads a libvips binary for this exact platform and arch and can fail to resolve one. a combined "native modules" verdict would send somebody to rebuild the wrong thing. loading it is the only test worth running; a directory proves nothing, and `versions.vips` proves libvips is bound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
1008 lines
42 KiB
Bash
Executable File
1008 lines
42 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=""
|
|
|
|
# Kept before the loop consumes them. This script re-executes itself through sudo
|
|
# further down and was passing `"$@"`, which `shift` had already emptied — so
|
|
# `officer-setup.sh --only build` run as a normal user silently became a FULL run
|
|
# the moment it escalated. Nothing said so; the flag just stopped existing.
|
|
ORIGINAL_ARGS=(${@+"$@"})
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--only)
|
|
ONLY_STEP="${2:-}"
|
|
shift 2
|
|
;;
|
|
--only=*)
|
|
ONLY_STEP="${1#*=}"
|
|
shift
|
|
;;
|
|
# Set before lib/repo.sh is sourced below, which reads it as
|
|
# `${OFFICER_REPO:-<default>}` — so this wins and an absent flag still defaults.
|
|
--repo)
|
|
[[ -n "${2:-}" ]] || {
|
|
echo "--repo needs a URL" >&2
|
|
exit 2
|
|
}
|
|
OFFICER_REPO="$2"
|
|
shift 2
|
|
;;
|
|
--repo=*)
|
|
OFFICER_REPO="${1#*=}"
|
|
shift
|
|
;;
|
|
--unattended | -y)
|
|
export UNATTENDED=1 ASSUME_YES=1
|
|
shift
|
|
;;
|
|
-l | --list)
|
|
# Portable ERE + sed rather than GNU grep's -P/\K — BSD grep on macOS has no -P at
|
|
# all and errors immediately ("invalid option -- P"), confirmed live.
|
|
grep -E '^step "' "${BASH_SOURCE[0]}" | sed -E 's/^step "([^"]+)".*/\1/'
|
|
exit 0
|
|
;;
|
|
-h | --help)
|
|
echo "usage: officer-setup.sh [--only <step>] [--list] [--repo <url>] [--unattended]"
|
|
echo ""
|
|
echo " --only <step> run one step; --list names them"
|
|
echo " --unattended take the default for every question that has one (-y)"
|
|
echo " --repo <url> clone from here instead of the default, which is a"
|
|
echo " private Gitea over SSH and only authenticates on a"
|
|
echo " machine whose key it already knows. Same as exporting"
|
|
echo " OFFICER_REPO. Ignored once the repo is checked out."
|
|
exit 0
|
|
;;
|
|
*) echo "unknown option: $1" >&2 && exit 2 ;;
|
|
esac
|
|
done
|
|
export OFFICER_REPO="${OFFICER_REPO:-}"
|
|
|
|
# shellcheck source=officer-setup/lib/base.sh
|
|
source "$SCRIPT_DIR/report.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"
|
|
# shellcheck source=officer-setup/lib/postgres.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/postgres.sh"
|
|
# shellcheck source=officer-setup/lib/env.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/env.sh"
|
|
# shellcheck source=officer-setup/lib/secrets.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/secrets.sh"
|
|
# shellcheck source=officer-setup/lib/build.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/build.sh"
|
|
# shellcheck source=officer-setup/lib/services.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/services.sh"
|
|
# shellcheck source=officer-setup/lib/proxy.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/proxy.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}"
|
|
|
|
# ── Privileges: asked for, not demanded ──
|
|
#
|
|
# Run this as YOURSELF. It needs root on Linux, so it asks through sudo and
|
|
# re-executes itself rather than making you type it. Variables are passed to sudo
|
|
# by name rather than with -E, because `env_reset` is the sudoers default and
|
|
# strips the environment — which is how DATA_PATH was lost once already.
|
|
#
|
|
# macOS never escalates: Homebrew refuses to run as root, and the account running
|
|
# this IS the owner, so there is nothing to chown and nothing to drop to.
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
if [[ "$EUID" -eq 0 ]]; then
|
|
fail "Do not run this with sudo on macOS — run it as yourself."
|
|
fi
|
|
elif [[ "$EUID" -ne 0 ]]; then
|
|
command -v sudo >/dev/null 2>&1 || fail "This needs root and sudo is not installed — run it as root."
|
|
echo ""
|
|
echo " This needs administrator rights. You will be asked for your password."
|
|
echo ""
|
|
exec sudo \
|
|
OFFICER_ROOT="${OFFICER_ROOT:-}" \
|
|
SETUP_USERNAME="${SETUP_USERNAME:-}" \
|
|
MACHINE_ROLE="${MACHINE_ROLE:-}" \
|
|
REPORT_FILE="${REPORT_FILE:-}" \
|
|
UNATTENDED="${UNATTENDED:-}" \
|
|
ASSUME_YES="${ASSUME_YES:-}" \
|
|
OFFICER_REPO="${OFFICER_REPO:-}" \
|
|
bash "$SCRIPT_DIR/officer-setup.sh" ${ORIGINAL_ARGS[@]+"${ORIGINAL_ARGS[@]}"}
|
|
fi
|
|
|
|
trap report_flush EXIT
|
|
|
|
# ── 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
|
|
|
|
# proxy.sh set PROXY_NET from $SETUP_DOCKER_NETWORK when it was SOURCED, before the answers
|
|
# file above could have populated that variable — so it needs re-deriving now the same way
|
|
# ENV_PORT/ENV_PUBLIC_URL/POSTGRES_URL are recovered below. Without this a customised bridge
|
|
# name from machine-setup silently would not carry over, and Proxy would default back to
|
|
# "services" regardless of what Docker was actually told to use.
|
|
PROXY_NET="${SETUP_DOCKER_NETWORK:-$PROXY_NET}"
|
|
|
|
# ── 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}"
|
|
|
|
# ── recover what earlier runs already decided ──
|
|
#
|
|
# A skipped section leaves its variables unset, and later sections read them. On
|
|
# a resume that is every section before the one it stopped at, so Build announced
|
|
# "PUBLIC_URL <not set — run the Environment section first>" on a machine whose
|
|
# .env had been written twenty minutes earlier.
|
|
#
|
|
# Read back here, once, from the file that already holds the answers, rather than
|
|
# per-section — three variables cross a section boundary (ENV_PORT and
|
|
# ENV_PUBLIC_URL from Environment, POSTGRES_URL from Database) and the next one
|
|
# added would have to remember to do this again.
|
|
#
|
|
# Only fills what is EMPTY, so a variable passed in on the command line still
|
|
# wins, and a section that runs for real still overwrites it with its own answer.
|
|
if [[ -f "$(env_file)" ]]; then
|
|
ENV_PORT="${ENV_PORT:-$(env_get PORT)}"
|
|
ENV_PUBLIC_URL="${ENV_PUBLIC_URL:-$(env_get PUBLIC_URL)}"
|
|
POSTGRES_URL="${POSTGRES_URL:-$(env_get POSTGRES_URL)}"
|
|
fi
|
|
|
|
# ── 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 is bash 4+; macOS's stock bash is 3.2, so this reads line-by-line instead.
|
|
MISSING=()
|
|
while IFS= read -r _line; do MISSING+=("$_line"); done < <(missing_tools)
|
|
MISSING_OPT=()
|
|
while IFS= read -r _line; do MISSING_OPT+=("$_line"); done < <(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.
|
|
|
|
report_section "Layout"
|
|
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 is bash 4+; macOS's stock bash is 3.2.
|
|
WRONG_OWNER=()
|
|
while IFS= read -r _line; do WRONG_OWNER+=("$_line"); done < <(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)")
|
|
|
|
# The affordances an agent needs to know about this platform. Not fatal: an
|
|
# install without them works, agents just go back to guessing what the client
|
|
# on the other end will render — which is what they did before this existed.
|
|
if capabilities_sync; then
|
|
ok "capabilities synced from ${CAPABILITIES_REPO}"
|
|
SUMMARY+=("Capabilities: synced")
|
|
else
|
|
warn "capabilities not synced — agents will have no affordances. Run 'git pull' in $(layout_items_dir), or press Update all on the marketplace screen."
|
|
SUMMARY+=("Capabilities: NOT synced")
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 3. Repository
|
|
# =============================================================================
|
|
|
|
report_section "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
|
|
# =============================================================================
|
|
|
|
report_section "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 " sharp: $(sharp_loads && echo "loads (libvips $(sharp_version))" || echo 'does not load')"
|
|
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."
|
|
echo ""
|
|
echo " sharp is the opposite problem: it does not compile, it downloads a"
|
|
echo " libvips binary for this exact platform and architecture. So the"
|
|
echo " install can succeed and the module still not load, and the way that"
|
|
echo " shows up is a file browser drawing generic icons over a folder of"
|
|
echo " photographs. Loading it is the only check worth running."
|
|
|
|
if deps_installed && node_pty_built && sharp_loads; then
|
|
ok "already installed; node-pty built and sharp loads"
|
|
SUMMARY+=("Dependencies: already installed")
|
|
elif confirm "Install them?"; then
|
|
if install_deps; then
|
|
# Reported separately from node-pty rather than as one "native modules"
|
|
# verdict: they fail for different reasons and want different fixes, and a
|
|
# combined line sends somebody to rebuild the wrong one.
|
|
if ! sharp_loads; then
|
|
warn "installed, but sharp does not load"
|
|
echo " Image thumbnails in the file browser will silently fall back to"
|
|
echo " file icons. Usually the platform-specific binary did not resolve:"
|
|
echo " cd $(platform_dir) && bun install --force"
|
|
ERRORS+=("Dependencies: sharp does not load")
|
|
fi
|
|
|
|
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
|
|
|
|
# =============================================================================
|
|
# 5. Database
|
|
# =============================================================================
|
|
#
|
|
# POSTGRES_URL is set here and written by the environment section below.
|
|
|
|
report_section "Database"
|
|
step "Database"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Database — Postgres, the only one Officer has"
|
|
echo " It holds the account, passkeys, settings, dashboards, email"
|
|
echo " accounts and the job queue. Nothing else in the platform is a"
|
|
echo " database."
|
|
echo ""
|
|
# One network for everything Officer provisions. Created before the compose
|
|
# file references it, since it is declared external there.
|
|
if ensure_docker_network; then
|
|
ok "docker network '${OFFICER_NETWORK}' created"
|
|
SUMMARY+=("Docker network: ${OFFICER_NETWORK} created")
|
|
elif docker_network_exists; then
|
|
echo " network: ${OFFICER_NETWORK} (already there)"
|
|
fi
|
|
|
|
# The client goes on the HOST, before any of the container work, because it is the half
|
|
# that is not in the container. A member has their own Postgres role and no access to the
|
|
# owner's Docker socket, so `docker exec … psql` is the owner's tool, not theirs.
|
|
install_pg_client || ERRORS+=("psql: client not installed — members have no Postgres CLI")
|
|
|
|
echo " compose file: $(pg_compose_exists && echo "$(pg_compose_file)" || echo 'not written yet')"
|
|
echo " container: $(pg_container_running && echo "${PG_CONTAINER} running" || echo 'not running')"
|
|
echo " port ${PG_PORT}: $(pg_port_in_use && echo 'something is listening' || echo 'free')"
|
|
|
|
POSTGRES_URL=""
|
|
|
|
# An existing compose file means this ran before. Reuse its password rather
|
|
# than minting a new one, which would leave the container and the URL
|
|
# disagreeing about the credential.
|
|
if pg_compose_exists && PG_EXISTING_PASSWORD="$(pg_password_from_env_file)"; then
|
|
POSTGRES_URL="$(pg_url "$PG_EXISTING_PASSWORD")"
|
|
echo ""
|
|
echo " already provisioned here — reusing the password from $(pg_env_file)"
|
|
pg_container_running || {
|
|
info " starting it"
|
|
pg_compose_up >/dev/null 2>&1 || true
|
|
}
|
|
if pg_wait_ready; then
|
|
ok "postgres answering on 127.0.0.1:${PG_PORT}"
|
|
SUMMARY+=("Database: existing Postgres at ${PG_CONTAINER}")
|
|
else
|
|
warn "the container is not answering — check: docker logs ${PG_CONTAINER}"
|
|
ERRORS+=("Database: provisioned but not answering")
|
|
fi
|
|
|
|
else
|
|
echo ""
|
|
info "Which Postgres should Officer use?"
|
|
echo ""
|
|
echo " [1] provision one here"
|
|
echo " ${PG_IMAGE} in $(pg_service_dir), bound to 127.0.0.1 only."
|
|
echo " Docker publishes ports by writing iptables rules beneath ufw,"
|
|
echo " so a database published to every interface is reachable from"
|
|
echo " the internet whatever the firewall says. Loopback is all the"
|
|
echo " platform needs — it runs on this machine."
|
|
echo ""
|
|
echo " [2] use one you already run"
|
|
echo " Give the connection URL. Nothing is provisioned."
|
|
echo ""
|
|
|
|
DB_PICK=""
|
|
while [[ -z "$DB_PICK" ]]; do
|
|
if ! read -rp " Which one? (1/2) [1]: " DB_CHOICE; then
|
|
echo ""
|
|
fail "No answer."
|
|
fi
|
|
case "${DB_CHOICE:-1}" in
|
|
1)
|
|
if ! command -v docker &>/dev/null; then
|
|
warn "Docker is not installed, so there is nothing to provision into."
|
|
continue
|
|
fi
|
|
if pg_port_in_use; then
|
|
warn "something is already listening on ${PG_PORT} — provisioning here would fail to bind"
|
|
echo " If that is a Postgres you already run, pick 2 and give its URL."
|
|
continue
|
|
fi
|
|
DB_PICK=provision
|
|
;;
|
|
2) DB_PICK=existing ;;
|
|
*) warn "Pick 1 or 2." ;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$DB_PICK" == provision ]]; then
|
|
PG_PASSWORD="$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)"
|
|
write_pg_compose "$PG_PASSWORD"
|
|
ok "compose written to $(pg_compose_file)"
|
|
if pg_compose_up && pg_wait_ready; then
|
|
POSTGRES_URL="$(pg_url "$PG_PASSWORD")"
|
|
ok "postgres answering on 127.0.0.1:${PG_PORT}, database '${PG_DATABASE}'"
|
|
SUMMARY+=("Database: provisioned at $(pg_service_dir)")
|
|
else
|
|
warn "the container did not come up — check: docker logs ${PG_CONTAINER}"
|
|
ERRORS+=("Database: container did not start")
|
|
SUMMARY+=("Database: provisioning FAILED")
|
|
fi
|
|
else
|
|
echo ""
|
|
ask_required POSTGRES_URL "Connection URL" "postgresql://user:password@host:5432/officer"
|
|
if pg_url_works "$POSTGRES_URL"; then
|
|
ok "reachable"
|
|
SUMMARY+=("Database: existing, ${POSTGRES_URL%%:*}://…")
|
|
else
|
|
# Not fatal. The URL may be right and the database not started yet, and
|
|
# refusing to continue over that would be worse than saying so.
|
|
warn "could not connect with that URL"
|
|
echo " Kept anyway — check it before running the schema step."
|
|
ERRORS+=("Database: the given URL did not answer")
|
|
SUMMARY+=("Database: existing URL kept, did not answer")
|
|
fi
|
|
fi
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 6. Environment
|
|
# =============================================================================
|
|
|
|
report_section "Environment"
|
|
step "Environment"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Environment — $(env_file)"
|
|
|
|
# Read back before anything is asked; existing values become the defaults.
|
|
ENV_PORT="$(env_get PORT)"
|
|
ENV_PUBLIC_URL="$(env_get PUBLIC_URL)"
|
|
|
|
if env_exists; then
|
|
echo " exists — its values are the defaults below"
|
|
else
|
|
echo " does not exist yet"
|
|
fi
|
|
|
|
# ── what is asked ──
|
|
echo ""
|
|
ask_required ENV_PORT "Port Officer listens on" "${ENV_PORT:-9000}"
|
|
|
|
echo ""
|
|
echo " PUBLIC_URL is where Officer is reached from a browser. It is the one"
|
|
echo " thing this machine cannot work out for itself, and three things need"
|
|
echo " it: the OpenGraph tags baked into the page by 'bun gen:index', the"
|
|
echo " host the task API hands to scripts, and the CalDAV profile an iPhone"
|
|
echo " installs — that last one requires https."
|
|
echo ""
|
|
echo " Defaulting to this machine's tailnet address, not localhost: the"
|
|
echo " tailnet is where Officer is actually reached from, and localhost"
|
|
echo " works from here and nowhere else."
|
|
ask_required ENV_PUBLIC_URL "Public URL" "${ENV_PUBLIC_URL:-$(default_public_url "$ENV_PORT")}"
|
|
|
|
echo ""
|
|
echo " to write:"
|
|
echo " PORT=${ENV_PORT}"
|
|
echo " PUBLIC_URL=${ENV_PUBLIC_URL}"
|
|
echo " POSTGRES_URL=${POSTGRES_URL%%:*}://…"
|
|
echo ""
|
|
echo " the install root is not written here — the platform derives it as the"
|
|
echo " parent of the repo, so data/, capabilities/ and dockers/ follow from"
|
|
echo " ${OFFICER_ROOT} without anything having to agree with anything."
|
|
echo ""
|
|
|
|
if confirm "Write it?"; then
|
|
write_env
|
|
ok "written, 0600, owned by ${USERNAME}"
|
|
report_changed "wrote $(env_file) (0600, owner ${USERNAME}) — PORT, PUBLIC_URL, POSTGRES_URL. No secrets: every key lives in the secret store."
|
|
[[ -f "$(env_file).before-officer-setup" ]] && echo " previous kept as $(env_file).before-officer-setup"
|
|
SUMMARY+=("Environment: $(env_file)")
|
|
else
|
|
warn "skipped by request"
|
|
SUMMARY+=("Environment: SKIPPED by request")
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 7. Secrets
|
|
# =============================================================================
|
|
#
|
|
# The store creates keys on demand, so this section is not strictly required —
|
|
# the first `sign()` would mint the jwt key by itself. It runs anyway for two
|
|
# reasons: the file should exist with the right owner and mode before anything
|
|
# races to create it, and an install that finishes without ever saying the words
|
|
# "back this up" is one where nobody learns the file matters until it is gone.
|
|
|
|
report_section "Secrets"
|
|
step "Secrets"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Secret store — $(secret_store_path)"
|
|
echo " Every encryption and signing key the platform holds, one SQLite file,"
|
|
echo " one key per purpose. Nothing goes in .env."
|
|
echo ""
|
|
echo " bootstrapped now:"
|
|
echo " jwt signs every session token"
|
|
echo ""
|
|
echo " Every other purpose — wallet, photos, jellyfin, invoiceshelf, vault,"
|
|
echo " service-connections — is created when its plugin is installed. A"
|
|
echo " plugin cannot read another plugin's key."
|
|
echo ""
|
|
|
|
if confirm "Create it?"; then
|
|
if bootstrap_secret_store; then
|
|
ok "created, 0600, owned by ${USERNAME}"
|
|
report_changed "created $(secret_store_path) (0600, dir 0700, owner ${USERNAME}) with keys for: jwt. Generated locally, never transmitted."
|
|
echo ""
|
|
warn "back up $(secret_store_path) — and keep it OUT of the backup that holds your database dump."
|
|
echo " Losing it signs everyone out and makes every encrypted column in"
|
|
echo " Postgres unreadable. For the wallet seed that is unrecoverable:"
|
|
echo " the passphrase opens the inner envelope, this is the outer one."
|
|
echo ""
|
|
echo " Keeping it beside a dump defeats it — the dump is the ciphertext"
|
|
echo " and this is the key. Separate backups, or it is one theft."
|
|
SUMMARY+=("Secrets: $(secret_store_path)")
|
|
else
|
|
warn "could not create the store — the platform will create it on first use"
|
|
SUMMARY+=("Secrets: NOT created; the platform will do it on first use")
|
|
fi
|
|
else
|
|
warn "skipped by request — the platform will create it on first use"
|
|
SUMMARY+=("Secrets: SKIPPED; the platform will create it on first use")
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 8. Schema
|
|
# =============================================================================
|
|
|
|
report_section "Schema"
|
|
step "Schema"
|
|
if ! skip; then
|
|
echo ""
|
|
# Counted from the aggregator rather than hardcoded, so the number is the truth
|
|
# even when a plugin line is uncommented. It was written as ${SCHEMA_TABLES:-?}
|
|
# and never assigned, so the section said "? tables" — a placeholder that looked
|
|
# like the count could not be determined rather than like nobody had set it.
|
|
SCHEMA_TABLES="$(schema_table_count)"
|
|
info "Database schema"
|
|
echo " ${SCHEMA_TABLES:-?} tables, applied with 'bun db:push' — drizzle-kit"
|
|
echo " diffs the schema code against Postgres and alters it directly. There"
|
|
echo " are no migration files and no migration table; the code is the source"
|
|
echo " of truth."
|
|
echo ""
|
|
echo " Only the CORE tables. Every plugin's tables are commented out in"
|
|
echo " src/databases/officer_db/src/schema.ts and get created when the"
|
|
echo " plugin is installed."
|
|
echo ""
|
|
|
|
if confirm "Push it?"; then
|
|
if OUT="$(push_schema)"; then
|
|
ok "schema applied"
|
|
report_changed "applied ${SCHEMA_TABLES} tables to Postgres with 'bun db:push' (drizzle-kit; no migration files)"
|
|
SUMMARY+=("Schema: ${SCHEMA_TABLES:-?} tables pushed")
|
|
else
|
|
warn "db:push failed"
|
|
echo "$OUT" | tail -12 | sed 's/^/ /'
|
|
SUMMARY+=("Schema: FAILED — see the output above")
|
|
fi
|
|
else
|
|
warn "skipped by request — the platform will not start without it"
|
|
SUMMARY+=("Schema: SKIPPED by request")
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 9. Build
|
|
# =============================================================================
|
|
|
|
report_section "Build"
|
|
step "Build"
|
|
if ! skip; then
|
|
echo ""
|
|
info "index.gen.html"
|
|
echo " 'bun gen:index' substitutes your public URL into index.html and"
|
|
echo " writes index.gen.html, which is the file the server imports. It is"
|
|
echo " gitignored, so a fresh clone never has one and the server has no page"
|
|
echo " to serve until this runs."
|
|
echo ""
|
|
echo " URL: ${ENV_PUBLIC_URL:-<not set — run the Environment section first>}"
|
|
echo ""
|
|
echo " To change it later: bun gen:index https://your.new.url"
|
|
echo ""
|
|
|
|
if [[ -z "$ENV_PUBLIC_URL" ]]; then
|
|
warn "PUBLIC_URL is not in $(env_file) — run the Environment section, then this one"
|
|
SUMMARY+=("Build: SKIPPED — no PUBLIC_URL")
|
|
elif confirm "Generate it?"; then
|
|
if OUT="$(gen_index)"; then
|
|
ok "$(gen_index_output)"
|
|
report_changed "generated $(gen_index_output) from index.html, substituting PUBLIC_URL=${ENV_PUBLIC_URL}"
|
|
SUMMARY+=("Build: index.gen.html for ${ENV_PUBLIC_URL}")
|
|
else
|
|
warn "gen:index failed"
|
|
echo "$OUT" | tail -8 | sed 's/^/ /'
|
|
SUMMARY+=("Build: FAILED — see the output above")
|
|
fi
|
|
else
|
|
warn "skipped by request — the server has no page to serve without it"
|
|
SUMMARY+=("Build: SKIPPED by request")
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 10. Services
|
|
# =============================================================================
|
|
|
|
report_section "Services"
|
|
step "Services"
|
|
if ! skip; then
|
|
echo ""
|
|
info "pm2 — $(ecosystem_file)"
|
|
echo " The ecosystem file is GENERATED, not checked in. It describes this"
|
|
echo " install and nothing else, so nothing in git can drift from it."
|
|
echo ""
|
|
echo " ${#CORE_PROCESSES[@]} processes:"
|
|
for entry in "${CORE_PROCESSES[@]}"; do
|
|
IFS='|' read -r _name _script _args <<<"$entry"
|
|
printf " %-24s %s %s\n" "$_name" "$_script" "$_args"
|
|
done
|
|
echo ""
|
|
echo " Nothing else. Every plugin adds its own entry when it is installed."
|
|
echo ""
|
|
|
|
if confirm "Write it and start them?"; then
|
|
write_ecosystem
|
|
ok "written — $(ecosystem_file)"
|
|
report_changed "wrote $(ecosystem_file) — ${#CORE_PROCESSES[@]} pm2 apps: $(printf '%s ' "${CORE_PROCESSES[@]%%|*}")"
|
|
|
|
# Starting against a database that is not answering is not fatal — the server
|
|
# waits and the agent retries forever — but it makes the Verify section below
|
|
# report a failure that is really just a race, and that is the kind of noise
|
|
# that teaches people to ignore a red line.
|
|
if pg_container_running && ! pg_wait_ready 30; then
|
|
warn "Postgres is not answering — starting anyway, but Verify may report failures"
|
|
fi
|
|
|
|
if OUT="$(pm2_start)"; then
|
|
ok "processes started"
|
|
report_started "pm2 startOrRestart: $(printf '%s ' "${CORE_PROCESSES[@]%%|*}")"
|
|
pm2_save >/dev/null 2>&1 && ok "process list saved (survives a pm2 restart)"
|
|
|
|
echo ""
|
|
if confirm "Start them on boot too?"; then
|
|
if pm2_enable_startup; then
|
|
ok "pm2 will resurrect them at boot"
|
|
report_ran "pm2 startup systemd — installed a systemd unit so pm2 resurrects these at boot"
|
|
SUMMARY+=("Services: ${#CORE_PROCESSES[@]} processes started, enabled at boot")
|
|
else
|
|
warn "could not enable the boot hook — run 'pm2 startup' yourself and follow it"
|
|
SUMMARY+=("Services: ${#CORE_PROCESSES[@]} processes started; boot hook NOT enabled")
|
|
fi
|
|
else
|
|
SUMMARY+=("Services: ${#CORE_PROCESSES[@]} processes started; not enabled at boot")
|
|
fi
|
|
else
|
|
warn "pm2 did not start cleanly"
|
|
echo "$OUT" | tail -12 | sed 's/^/ /'
|
|
SUMMARY+=("Services: FAILED to start — see the output above")
|
|
fi
|
|
else
|
|
warn "skipped by request"
|
|
SUMMARY+=("Services: SKIPPED by request")
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 11. Verify
|
|
# =============================================================================
|
|
|
|
report_section "Verify"
|
|
step "Verify"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Are the processes actually up?"
|
|
echo ""
|
|
|
|
VERIFY_BAD=0
|
|
while IFS='|' read -r vname vstatus vrestarts; do
|
|
[[ -z "$vname" ]] && continue
|
|
if [[ "$vstatus" == "online" ]]; then
|
|
if (( vrestarts > 3 )); then
|
|
warn "$(printf '%-24s online, but restarted %s times — check: pm2 logs %s' "$vname" "$vrestarts" "$vname")"
|
|
VERIFY_BAD=$((VERIFY_BAD + 1))
|
|
else
|
|
ok "$(printf '%-24s online' "$vname")"
|
|
fi
|
|
else
|
|
warn "$(printf '%-24s %s — check: pm2 logs %s' "$vname" "$vstatus" "$vname")"
|
|
VERIFY_BAD=$((VERIFY_BAD + 1))
|
|
fi
|
|
done < <(pm2_status_lines)
|
|
|
|
echo ""
|
|
# A process can be `online` and still be failing to serve — a restart loop takes
|
|
# a few seconds to show up in the counter, and the app can be up with a broken
|
|
# database. So the port is asked directly.
|
|
if curl -fsS --max-time 5 "http://127.0.0.1:${ENV_PORT:-9000}/api" >/dev/null 2>&1; then
|
|
ok "the API answers on 127.0.0.1:${ENV_PORT:-9000}"
|
|
SUMMARY+=("Verify: API answering on port ${ENV_PORT:-9000}")
|
|
else
|
|
warn "nothing answered on 127.0.0.1:${ENV_PORT:-9000}/api"
|
|
echo " pm2 logs officer is where the reason will be."
|
|
VERIFY_BAD=$((VERIFY_BAD + 1))
|
|
SUMMARY+=("Verify: the API did NOT answer on port ${ENV_PORT:-9000}")
|
|
fi
|
|
|
|
if (( VERIFY_BAD == 0 )); then
|
|
echo ""
|
|
ok "Officer is running. Open ${ENV_PUBLIC_URL:-http://localhost:${ENV_PORT:-9000}} and the"
|
|
echo " first-run screen will create the owner account."
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
|
|
# =============================================================================
|
|
# 12. Proxy
|
|
# =============================================================================
|
|
#
|
|
# Optional, and last, because it is the only step that needs Officer to be already
|
|
# running: NPM proxies to it, and the gate below checks the bind address rather than
|
|
# taking a curl to loopback as proof.
|
|
#
|
|
# ── Why this section ignores --unattended ──
|
|
#
|
|
# Every other question in this script has a defensible default. None of these do — a
|
|
# domain name, a DNS provider and that provider's API credentials cannot be guessed —
|
|
# and the step is opt-in besides. So its prompts read stdin directly instead of going
|
|
# through confirm()/ask_required(), which honour ASSUME_YES.
|
|
#
|
|
# The valve is a TTY check, not the flag: with no terminal there is nobody to ask, so
|
|
# it skips and prints the manual instructions. That keeps a cron-driven install working
|
|
# without letting --unattended silently agree to publishing a public hostname.
|
|
|
|
report_section "Proxy"
|
|
step "Proxy"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Reverse proxy — a real hostname and an HTTPS certificate"
|
|
echo " Optional. Skip it if you already run a proxy elsewhere, or if you"
|
|
echo " reach this instance over the tailnet and are happy with that."
|
|
echo ""
|
|
|
|
PROXY_PORT="${ENV_PORT:-9000}"
|
|
|
|
if [[ ! -t 0 ]]; then
|
|
warn "no terminal — skipping the proxy, which cannot be answered unattended"
|
|
proxy_skip_instructions "$PROXY_PORT"
|
|
SUMMARY+=("Proxy: skipped (no terminal)")
|
|
elif ! proxy_require_listening "$PROXY_PORT"; then
|
|
warn "skipping the proxy — Officer is not reachable the way NPM would reach it"
|
|
echo " Fix the bind address, then: officer-setup.sh --only Proxy"
|
|
SUMMARY+=("Proxy: skipped (Officer not listening on 0.0.0.0)")
|
|
elif ! proxy_confirm "Set up Nginx Proxy Manager now?"; then
|
|
proxy_skip_instructions "$PROXY_PORT"
|
|
SUMMARY+=("Proxy: skipped by request")
|
|
else
|
|
# One failure path for all of it: every function warns and returns non-zero rather
|
|
# than exiting, so a proxy that does not come up leaves a finished Officer install
|
|
# behind rather than a failed one. It is the last section for that reason.
|
|
PROXY_DOMAIN="$(proxy_ask 'Domain for this instance (e.g. officer.example.com)')"
|
|
if [[ -z "$PROXY_DOMAIN" ]]; then
|
|
warn "no domain given — skipping"
|
|
SUMMARY+=("Proxy: skipped (no domain)")
|
|
elif
|
|
proxy_detect_target &&
|
|
proxy_ensure_network &&
|
|
proxy_order_docker_after_tailscaled &&
|
|
proxy_write_compose &&
|
|
proxy_start &&
|
|
proxy_claim_admin &&
|
|
proxy_get_token &&
|
|
{ [[ "$CHALLENGE" != "dns" ]] || proxy_prompt_dns_credentials; } &&
|
|
proxy_wait_for_dns "$PROXY_DOMAIN" "$TARGET_IP" &&
|
|
proxy_allow_bridge_to_host "$PROXY_PORT" &&
|
|
proxy_create_host "$PROXY_DOMAIN" "$PROXY_PORT" &&
|
|
proxy_issue_certificate "$PROXY_DOMAIN" &&
|
|
proxy_attach_certificate
|
|
then
|
|
proxy_verify "$PROXY_DOMAIN"
|
|
echo ""
|
|
ok "Officer is published at https://${PROXY_DOMAIN}"
|
|
echo " NPM admin: http://127.0.0.1:81$([[ "$CHALLENGE" == "dns" ]] && echo " or http://${TARGET_IP}:81")"
|
|
SUMMARY+=("Proxy: https://${PROXY_DOMAIN}")
|
|
else
|
|
warn "the proxy did not finish — Officer itself is unaffected and still running"
|
|
echo " Retry just this part with: officer-setup.sh --only Proxy"
|
|
ERRORS+=("Proxy: did not finish")
|
|
SUMMARY+=("Proxy: FAILED — retry with --only Proxy")
|
|
fi
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# ── Who you are when this exits ──
|
|
#
|
|
# Root, and that surprises people — reasonably, because everything this script just
|
|
# installed belongs to somebody else. The platform runs as ${USERNAME}: the checkout,
|
|
# node_modules, .env, the secret store and every pm2 process are theirs. Root was
|
|
# the installer's privilege, never the platform's.
|
|
#
|
|
# Saying so matters for two things that are invisible until they bite:
|
|
#
|
|
# - group membership is fixed at login. ${USERNAME} was added to `docker` during
|
|
# machine setup, and a session that started before that does not have it — so
|
|
# `docker ps` fails for a reason that has nothing to do with docker.
|
|
# - the shell config was written into THEIR home. Staying as root means none of it
|
|
# is loaded, and the machine looks unconfigured.
|
|
if [[ "$EUID" -eq 0 ]]; then
|
|
echo ""
|
|
echo -e "${BOLD} One more thing — you are still root.${NC}"
|
|
echo ""
|
|
echo " Officer runs as ${USERNAME}, and everything it installed is theirs."
|
|
echo " Nothing here needs root any more. To carry on as them:"
|
|
echo ""
|
|
echo -e " ${BOLD}su - ${USERNAME}${NC} from this session"
|
|
echo -e " ${BOLD}ssh ${USERNAME}@<this machine>${NC} or log in fresh"
|
|
echo ""
|
|
echo " Either gives a new session, which is what makes their docker group"
|
|
echo " membership and their shell configuration take effect. Staying as root"
|
|
echo " means neither does, and the machine will look half-configured."
|
|
fi
|
|
|
|
echo ""
|
|
|
|
report_mark_complete
|