env_get() never matched its own file. write_env() always writes OFFICERDEV_-prefixed keys, but env_get() compared against the bare name — verified against a real written .env: `env_get PORT` -> [], `env_get OFFICERDEV_PORT` -> [9000]. Every one of its 5 call sites uses the bare name, so this always came back empty. Two consequences: the "recover what earlier runs already decided" block never recovers anything — its own comment says it exists to fix "Build announced PUBLIC_URL not set on a machine whose .env had been written twenty minutes earlier", and that exact bug was still live — and re-running the Environment step showed the wrong default (9000, tailnet URL) instead of the machine's actual current config. Fixed to check the prefixed key first, falling back to the bare one for an old unprefixed file, matching the runtime's own `OFFICERDEV_X ?? X` priority. pg_password_from_env_file() returned success with an empty string when the compose .env existed but had no POSTGRES_PASSWORD= line. The caller (`pg_compose_exists && PG_EXISTING_PASSWORD="$(pg_password_from_env_file)"`) would then treat that as "reuse this password" and build POSTGRES_URL with a blank credential rather than falling through to provision fresh. Only reachable via a hand-edited or corrupted file — write_pg_compose always writes the line — but "silently reuse a blank password" was the wrong direction to fail in. Now returns 1 when the line is missing or empty. SETUP_DOCKER_NETWORK was read independently by both scripts, each defaulting to "services", but never actually persisted across the machine-setup -> officer-setup boundary — not one of the three keys load_machine_answers recognised. A customised bridge name from machine-setup would silently not carry over, and Proxy would land back on "services" regardless of what Docker was actually told to use. Now saved by machine-setup's save_answers(), recognised by officer-setup's load_machine_answers(), and PROXY_NET is re-derived from it right after that load — proxy.sh's own top-level assignment runs at source time, before the answers file could have populated the variable, same ordering issue ENV_PORT/ENV_PUBLIC_URL/POSTGRES_URL already had to solve. All three verified with isolated reproductions before and after: env_get against a real prefixed .env and a legacy bare one, pg_password_from_env_file against a normal and a corrupted compose .env, and the full save/load/re-derive chain for SETUP_DOCKER_NETWORK. bash -n clean on every touched file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
4.2 KiB
Bash
95 lines
4.2 KiB
Bash
#!/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 ;;
|
|
# The bridge machine-setup told Docker to use — proxy.sh's PROXY_NET otherwise
|
|
# defaults to "services" independently and can only agree with this by coincidence.
|
|
# PROXY_NET is a top-level assignment evaluated when proxy.sh is SOURCED, which
|
|
# happens before this function is ever called — officer-setup.sh re-derives it from
|
|
# SETUP_DOCKER_NETWORK right after calling this, same pattern as ENV_PORT/
|
|
# ENV_PUBLIC_URL/POSTGRES_URL below it.
|
|
SETUP_DOCKER_NETWORK) if [[ -z "${SETUP_DOCKER_NETWORK:-}" ]]; then SETUP_DOCKER_NETWORK="$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 every process except the pty sidecar" ;;
|
|
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; }
|
|
|
|
# `getent` is Linux/glibc — not present on stock macOS at all (confirmed: "command not
|
|
# found" on a real machine; it only appeared to work in earlier testing because this dev
|
|
# session's shell profile shims it, which a fresh install does not have). Reproduced live:
|
|
# this returned the WRONG, hardcoded `/home/<user>` fallback on a real macOS run just now,
|
|
# and `install -d` then failed on it outright ("Operation not supported") since /home isn't
|
|
# a real, writable path on macOS.
|
|
#
|
|
# `eval echo ~"$USERNAME"` is the portable replacement — tilde expansion resolves through
|
|
# whatever the OS's own user database is (NSS on Linux, Directory Services on macOS)
|
|
# without naming either. A nonexistent user leaves the tilde word unexpanded (`~name`
|
|
# rather than a path), which is how the fallback below still triggers for that case.
|
|
resolve_user_home() {
|
|
USER_HOME="$(eval echo ~"$USERNAME" 2>/dev/null)"
|
|
[[ "$USER_HOME" == "~"* ]] && USER_HOME=""
|
|
[[ -n "$USER_HOME" ]] || USER_HOME="/home/${USERNAME}"
|
|
}
|