make machine-setup.sh and officer-setup.sh actually run on macOS
Both scripts had real macOS branches already — MACOS_SKIP, Xcode CLT, Docker Desktop
detection — but neither had ever run past its own library load on a real Mac, and
verifying that live is what this commit is. This machine is the Mac.
The root cause was the interpreter, not missing OS coverage: macOS's stock /bin/bash is
3.2.57 (Apple stopped shipping newer once bash went GPLv3) and this machine has no newer
one installed. Both scripts used bash 4+/4.3+ syntax with zero version guard, so a real
run died in seconds — declare -A (base.sh, associative array), local -n (packages.sh,
nameref), mapfile (5 sites), ${var,,} (both scripts' own step() — confirmed --only killed
the ENTIRE run), and machine-setup.sh's own pre-flight hard-refused brew outright ("this
script does not implement yet") even once the bash-4 crash was fixed. Each got rewritten
to its bash-3.2-safe equivalent rather than requiring a newer bash and re-executing under
it — nine call sites, a bounded fix, and one less thing that can go wrong before the
script can report what's wrong.
Past that gate, a live probe run (real account/path prompts, killed before Database/
Services could touch this machine's actual state) reproduced a second bug in the first
few seconds: resolve_user_home used getent (Linux/glibc, absent on macOS) with a
hardcoded /home/<user> fallback, so Layout tried `install -d /home/pastilhas` and failed
with "Operation not supported". Fixed with the portable `eval echo ~user` tilde-expansion
idiom, re-ran the same probe, and it correctly created /Users/pastilhas/officerdev/ and
started a real git clone.
Everything else follows the same shape — an unguarded Linux-only tool with a portable or
brew-based macOS equivalent:
- grep -oP (4 sites: both --list flags, env.sh's tailnet lookup, build.sh's schema
table count) — BSD grep has no -P at all. Portable ERE+sed where regex was enough;
jq where the input was already JSON (tailnet lookup) — jq is already an unconditional
dependency elsewhere in this script, so that's a correctness fix, not just a port.
- ss (postgres.sh, proxy.sh — is anything listening on this port, and is it bound wide
enough for NPM's container to reach) — both silently mis-reported on macOS, since the
"command not found" was swallowed by 2>/dev/null and read as "nothing's listening".
New shared `listening_addrs()` in officer-setup's base.sh, branching to lsof.
- getent hosts (proxy.sh's DNS-wait loop) — same silent-failure shape, would have spun
its full 30-minute timeout regardless of whether the record actually resolved. macOS
branch uses dscacheutil.
- stat -c (layout.sh's ownership check) — BSD stat has no -c, confirmed live ("illegal
option -- c"), and the failure was VISIBLE and made every layout directory report as
wrong-owner. Portable file_owner() tries -c then falls back to -f %Su.
- apt-only Postgres client, NodeSource's Debian-only setup script, lazydocker/lazygit's
installers (one literally named install_update_linux.sh, the other hardcoding
"_Linux_" into the release asset URL) — each gets a brew branch, formula names
verified against the live Homebrew catalog rather than assumed.
- sudo -u "$USERNAME" to become the SAME account the process already is — real on
Linux, where this whole script runs as root and dropping to the owner needs no
password; on macOS the process never escalates in the first place, so re-invoking
sudo for yourself asks for a real password on every call, or fails outright without
sudo rights. machine-setup's dev.sh had ~10 inline call sites (git, bun, deno, claude,
opencode, editor detection) rerouted through a new shared run_as_owner(); officer-
setup's own as_owner() (already the shared entry point there) got the same branch,
and six direct sudo call sites in build.sh/secrets.sh/services.sh that bypassed it
were rerouted through it too.
Three things stay Linux-only rather than getting an unverified macOS implementation,
each skipped with a stated reason at the point it would otherwise run: pm2's boot-
persistence hook (needs a launchd plist generated and loaded, not a systemd unit
printed and eval'd — real, separate work), the Tailscale exit-node NIC/sysctl tuning
(no macOS equivalent concept, and advertising a laptop as an exit node is already the
edge case MACOS_SKIP treats it as), and ufw's bridge-to-host firewall rule (pf is a
different rule language entirely, and Docker Desktop's own VM-based networking may not
even have the problem this rule exists to solve — guessing at a firewall rule felt like
the wrong place to be wrong).
Verified: both --list flags print step names instead of a grep usage error. A bounded
officer-setup.sh run (killed before Database) resolved the real macOS home directory,
created the layout with correct ownership, and began a real git clone — cleaned up
after (rm -rf the test directory, the gitignored .setup-progress file, the stray
install-report). machine-setup.sh's pre-flight now reports "Packages: brew" and reaches
the account/path prompts instead of refusing the OS outright. bash -n clean on all 16
files under bash 3.2, the actual interpreter these scripts run under on a Mac.
Not run further live: Database, Secrets, Services and Proxy would mutate this machine's
real state (a live Postgres, a live set of pm2 processes this session runs through) and
were left for a deliberate, supervised run rather than folded into validation.
This commit is contained in:
@@ -165,25 +165,39 @@ ONLY_STEP="${ONLY_STEP:-}"
|
||||
# Keyed on the step title, so the sections themselves stay Linux code with no
|
||||
# `if macos` branches threaded through them. The reason is printed, because a
|
||||
# silent skip and a missing step look identical.
|
||||
declare -A MACOS_SKIP=(
|
||||
["User account"]="accounts are System Settings' business on a Mac, not a script's"
|
||||
["Disk space"]="ballast and swap tuning are server concerns"
|
||||
["Locale"]="macOS manages locale itself"
|
||||
["Timezone"]="macOS manages the timezone itself"
|
||||
["Swap"]="macOS sizes its own swap dynamically"
|
||||
["Emergency disk ballast"]="a server trick for a machine nobody is sitting at"
|
||||
["earlyoom"]="Linux OOM killer tuning; macOS has its own memory pressure handling"
|
||||
["inotify watch limit"]="Linux inotify; macOS watches files through FSEvents"
|
||||
["Sleep and suspend"]="a laptop SHOULD sleep — this stops a server from doing it"
|
||||
["Boot hang"]="a systemd boot ordering fix"
|
||||
["SSH access"]="hardening a door a dev machine should not be opening"
|
||||
["DNS"]="systemd-resolved"
|
||||
["Network address"]="netplan, and a laptop moves between networks by design"
|
||||
["fail2ban"]="brute-force protection for an exposed SSH port"
|
||||
["Unattended upgrades"]="apt; macOS updates through Software Update"
|
||||
["Firewall"]="ufw; macOS has its own application firewall"
|
||||
["Shell"]="zsh is already the default, and tmux is a choice you make yourself"
|
||||
)
|
||||
#
|
||||
# A `case` rather than an associative array — `declare -A` is bash 4+, and macOS's own
|
||||
# stock `/bin/bash` is 3.2 (Apple hasn't shipped a newer one since bash went GPLv3), with
|
||||
# nothing newer installed on a fresh machine. An array keyed on the step title would parse-
|
||||
# fail the instant this file is sourced, before a single step could run — which is exactly
|
||||
# what happened here until it was measured. Prints the reason on stdout, so the caller reads
|
||||
# it with `$(...)`; exit status says whether the title matched anything at all.
|
||||
macos_skip_reason() {
|
||||
case "$1" in
|
||||
"User account") echo "accounts are System Settings' business on a Mac, not a script's" ;;
|
||||
"Disk space") echo "ballast and swap tuning are server concerns" ;;
|
||||
"Locale") echo "macOS manages locale itself" ;;
|
||||
"Timezone") echo "macOS manages the timezone itself" ;;
|
||||
"Swap") echo "macOS sizes its own swap dynamically" ;;
|
||||
"Emergency disk ballast") echo "a server trick for a machine nobody is sitting at" ;;
|
||||
"earlyoom") echo "Linux OOM killer tuning; macOS has its own memory pressure handling" ;;
|
||||
"inotify watch limit") echo "Linux inotify; macOS watches files through FSEvents" ;;
|
||||
"Sleep and suspend") echo "a laptop SHOULD sleep — this stops a server from doing it" ;;
|
||||
"Boot hang") echo "a systemd boot ordering fix" ;;
|
||||
"SSH access") echo "hardening a door a dev machine should not be opening" ;;
|
||||
"DNS") echo "systemd-resolved" ;;
|
||||
"Network address") echo "netplan, and a laptop moves between networks by design" ;;
|
||||
"fail2ban") echo "brute-force protection for an exposed SSH port" ;;
|
||||
"Unattended upgrades") echo "apt; macOS updates through Software Update" ;;
|
||||
"Firewall") echo "ufw; macOS has its own application firewall" ;;
|
||||
"Shell") echo "zsh is already the default, and tmux is a choice you make yourself" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# `${var,,}` (case-insensitive compare below) is bash 4+ too — this is the portable
|
||||
# pre-4 substitute, not a style choice.
|
||||
lower() { tr '[:upper:]' '[:lower:]' <<<"$1"; }
|
||||
|
||||
step() {
|
||||
CURRENT_STEP="$1"
|
||||
@@ -191,16 +205,19 @@ step() {
|
||||
# which one it is. Twenty-six sections, one place.
|
||||
declare -F report_section >/dev/null && report_section "$1"
|
||||
|
||||
if [[ "${OS:-}" == "macos" && -n "${MACOS_SKIP[$1]:-}" ]]; then
|
||||
echo ""
|
||||
echo -e "${BOLD}── $1 ──${NC}"
|
||||
echo -e " ${GREEN}SKIP${NC}: not on macOS — ${MACOS_SKIP[$1]}"
|
||||
SKIP_STEP=true
|
||||
return
|
||||
if [[ "${OS:-}" == "macos" ]]; then
|
||||
local _skip_reason
|
||||
if _skip_reason="$(macos_skip_reason "$1")"; then
|
||||
echo ""
|
||||
echo -e "${BOLD}── $1 ──${NC}"
|
||||
echo -e " ${GREEN}SKIP${NC}: not on macOS — ${_skip_reason}"
|
||||
SKIP_STEP=true
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$ONLY_STEP" ]]; then
|
||||
if [[ "${1,,}" == "${ONLY_STEP,,}" ]]; then
|
||||
if [[ "$(lower "$1")" == "$(lower "$ONLY_STEP")" ]]; then
|
||||
SKIP_STEP=false
|
||||
echo ""
|
||||
echo -e "${BOLD}── $1 ──${NC}"
|
||||
@@ -508,6 +525,24 @@ ask_officer_root() {
|
||||
# both of which abort the step.
|
||||
user_group() { id -gn "${1:-$USERNAME}" 2>/dev/null || echo "${1:-$USERNAME}"; }
|
||||
|
||||
# Run a command as the created account, e.g. `run_as_owner git clone ...`.
|
||||
#
|
||||
# On Linux this whole script runs as root — refused otherwise, see the pre-flight check
|
||||
# above — so `sudo -u "$USERNAME"` from root needs no password; sudo lets root become
|
||||
# anyone freely. On macOS the script deliberately never escalates (Homebrew refuses to run
|
||||
# as root), so the process already IS "$USERNAME", and re-invoking sudo for the SAME
|
||||
# account asks for a real password — potentially many times in one run, dev.sh calls this
|
||||
# shape roughly ten times — or fails outright on an account with no sudo rights at all.
|
||||
# `dev.sh`'s git/bun/deno/claude/opencode/editor helpers all route through this rather than
|
||||
# hand-rolling `sudo -H -u "$USERNAME" ...` each time, so macOS behaviour lives in one place.
|
||||
run_as_owner() {
|
||||
if [[ "${OS:-}" == "macos" ]]; then
|
||||
"$@"
|
||||
else
|
||||
sudo -H -u "$USERNAME" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run a block as the created user (login shell, inherits HOME)
|
||||
as_user() {
|
||||
sudo -u "$USERNAME" -i bash -c "$1"
|
||||
|
||||
@@ -47,8 +47,8 @@ MACHINE_SETUP_DEV_LOADED=1
|
||||
# has never been configured — the fresh machine this script exists for — reading
|
||||
# the current value aborted the run before the section had printed anything.
|
||||
# Missing a value is an answer here, not a failure.
|
||||
git_get() { (cd / && sudo -H -u "$USERNAME" git config --global --get "$1" 2>/dev/null) || true; }
|
||||
git_set() { (cd / && sudo -H -u "$USERNAME" git config --global "$1" "$2"); }
|
||||
git_get() { (cd / && run_as_owner git config --global --get "$1" 2>/dev/null) || true; }
|
||||
git_set() { (cd / && run_as_owner git config --global "$1" "$2"); }
|
||||
|
||||
# Is there anything configured at all?
|
||||
git_has_identity() { [[ -n "$(git_get user.name)" || -n "$(git_get user.email)" ]]; }
|
||||
@@ -102,7 +102,7 @@ install_oh_my_zsh() {
|
||||
# failed died here, silently, because the output is redirected: the run just
|
||||
# stopped after apt finished installing zsh, with nothing said. Observed on a
|
||||
# fresh Hetzner VPS, 2026-08-14.
|
||||
sudo -H -u "$USERNAME" sh -c \
|
||||
run_as_owner sh -c \
|
||||
"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended >/dev/null 2>&1 ||
|
||||
true
|
||||
# Belt and braces: `|| true` above already makes the last command succeed, and
|
||||
@@ -187,11 +187,11 @@ nvim_clone_config() {
|
||||
local repo="$1" dest="${USER_HOME}/.config/nvim"
|
||||
|
||||
install -d -m 0755 -o "$USERNAME" -g "$(user_group)" "${USER_HOME}/.config"
|
||||
(cd / && sudo -H -u "$USERNAME" git clone --depth 1 "$repo" "$dest" >/dev/null 2>&1) || return 1
|
||||
(cd / && run_as_owner git clone --depth 1 "$repo" "$dest" >/dev/null 2>&1) || return 1
|
||||
|
||||
# The starter is a template, not something to track. Left in place for a
|
||||
# config of the user's own, which they will want to keep pulling.
|
||||
[[ "$repo" == *LazyVim/starter* ]] && sudo -u "$USERNAME" rm -rf "${dest}/.git"
|
||||
[[ "$repo" == *LazyVim/starter* ]] && run_as_owner rm -rf "${dest}/.git"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -230,6 +230,18 @@ node_installed_major() { node -v 2>/dev/null | sed 's/^v//; s/\..*//'; }
|
||||
|
||||
install_node() {
|
||||
local major="$1"
|
||||
|
||||
# NodeSource's setup script adds an apt repository — it is Debian/Ubuntu-only by name
|
||||
# and by mechanism (checks for dpkg internally), so there is nothing to point it at on
|
||||
# macOS. Homebrew's `node`/`nodejs` formula (both names resolve, confirmed) tracks only
|
||||
# the current release rather than an arbitrary major the way NodeSource's per-major
|
||||
# scripts do — same tradeoff this codebase already accepts for Docker and Xcode CLT on
|
||||
# macOS: brew's own version, not a pinned one.
|
||||
if [[ "${OS:-}" == "macos" ]]; then
|
||||
pkg_install_now nodejs
|
||||
return
|
||||
fi
|
||||
|
||||
# NodeSource publishes one setup script per major. Checked before it is piped
|
||||
# into a shell, because a 404 page piped to bash is a confusing way to fail.
|
||||
curl -fsS -o /dev/null "https://deb.nodesource.com/setup_${major}.x" || {
|
||||
@@ -358,7 +370,7 @@ ensure_bun_symlink() {
|
||||
install_bun() {
|
||||
local tag="${1:-}"
|
||||
[[ -n "$tag" ]] || return 1
|
||||
(cd / && sudo -H -u "$USERNAME" bash -c "curl -fsSL https://bun.sh/install | bash -s '${tag}'") >/dev/null 2>&1
|
||||
(cd / && run_as_owner bash -c "curl -fsSL https://bun.sh/install | bash -s '${tag}'") >/dev/null 2>&1
|
||||
[[ -x "${USER_HOME}/.bun/bin/bun" ]]
|
||||
}
|
||||
|
||||
@@ -367,7 +379,7 @@ install_pm2() { npm install -g pm2 >/dev/null 2>&1; }
|
||||
|
||||
deno_installed() { command -v deno &>/dev/null || [[ -x "${USER_HOME}/.deno/bin/deno" ]]; }
|
||||
install_deno() {
|
||||
(cd / && sudo -H -u "$USERNAME" bash -c 'curl -fsSL https://deno.land/install.sh | sh') >/dev/null 2>&1
|
||||
(cd / && run_as_owner bash -c 'curl -fsSL https://deno.land/install.sh | sh') >/dev/null 2>&1
|
||||
[[ -x "${USER_HOME}/.deno/bin/deno" ]]
|
||||
}
|
||||
|
||||
@@ -434,16 +446,16 @@ agent_is_npm_install() { [[ "$(readlink -f "$(agent_path "$1")" 2>/dev/null)" ==
|
||||
agent_version() {
|
||||
local bin
|
||||
bin="$(agent_path "$1")"
|
||||
[[ -n "$bin" ]] && (cd / && sudo -H -u "$USERNAME" "$bin" --version 2>/dev/null | head -1)
|
||||
[[ -n "$bin" ]] && (cd / && run_as_owner "$bin" --version 2>/dev/null | head -1)
|
||||
}
|
||||
|
||||
install_claude_code() {
|
||||
(cd / && sudo -H -u "$USERNAME" bash -c "set -e; curl -fsSL ${CLAUDE_INSTALL_URL} | bash") >/dev/null 2>&1
|
||||
(cd / && run_as_owner bash -c "set -e; curl -fsSL ${CLAUDE_INSTALL_URL} | bash") >/dev/null 2>&1
|
||||
[[ -x "$(agent_bin claude)" ]]
|
||||
}
|
||||
|
||||
install_opencode() {
|
||||
(cd / && sudo -H -u "$USERNAME" bash -c "set -e; curl -fsSL ${OPENCODE_INSTALL_URL} | bash") >/dev/null 2>&1
|
||||
(cd / && run_as_owner bash -c "set -e; curl -fsSL ${OPENCODE_INSTALL_URL} | bash") >/dev/null 2>&1
|
||||
[[ -x "$(agent_bin opencode)" ]]
|
||||
}
|
||||
|
||||
@@ -471,7 +483,7 @@ editor_candidates() {
|
||||
|
||||
# `|| true` for the same reason git_get has it: "not set" is an answer, and an
|
||||
# assignment from a function that exits non-zero aborts the run under `set -e`.
|
||||
current_editor() { (cd / && sudo -H -u "$USERNAME" bash -lc 'echo "${EDITOR:-}"' 2>/dev/null) || true; }
|
||||
current_editor() { (cd / && run_as_owner bash -lc 'echo "${EDITOR:-}"' 2>/dev/null) || true; }
|
||||
|
||||
set_system_editor() {
|
||||
local editor="$1" path
|
||||
|
||||
@@ -253,13 +253,20 @@ pkg_install() {
|
||||
# passed by value cannot be told apart from an empty one once it has been through
|
||||
# word splitting.
|
||||
#
|
||||
# Copies by name via eval rather than `local -n` — namerefs are bash 4.3+, and macOS's
|
||||
# stock bash is 3.2. This is the idiom namerefs replaced: build the local array by
|
||||
# expanding the CALLER's array name inside a double-quoted eval, which bash 3.2 has always
|
||||
# supported. `$2`/`$3` are trusted call-site literals (`present`, `missing`), never
|
||||
# user input, so the eval is not taking anything untrusted.
|
||||
#
|
||||
# Returns non-zero when there is nothing to do, or when the answer was no — in
|
||||
# both cases the caller should skip its action. LAST_INSTALLED is cleared on a
|
||||
# refusal so the summary does not claim work that never happened.
|
||||
announce_plan() {
|
||||
local label="$1"
|
||||
local -n _present="$2"
|
||||
local -n _missing="$3"
|
||||
local -a _present _missing
|
||||
eval "_present=(\"\${$2[@]}\")"
|
||||
eval "_missing=(\"\${$3[@]}\")"
|
||||
|
||||
echo ""
|
||||
info "${label} — installs what is missing, keeps what you already have"
|
||||
|
||||
@@ -245,7 +245,18 @@ tailscale_needs_logout() {
|
||||
# Routing has to be on before this machine can forward anyone else's packets,
|
||||
# whether as an exit node or as a subnet router. Written as a drop-in so it is
|
||||
# visible as this script's doing.
|
||||
# Both this and install_exit_node_tuning below are Linux-only — sysctl.d, systemd's
|
||||
# networkd-dispatcher, ethtool NIC offload flags, none of which macOS has an equivalent
|
||||
# concept for. Tailscale itself DOES support acting as an exit node from macOS (that's the
|
||||
# `--advertise-exit-node` flag on `tailscale up`, untouched by this guard) — what's
|
||||
# unavailable is specifically this forwarding-throughput tuning, which is also a much
|
||||
# narrower loss on a Mac: the file's own MACOS_SKIP already treats a laptop advertising as
|
||||
# an exit node as an edge case, not the common path a homelab box is.
|
||||
enable_ip_forwarding() {
|
||||
if [[ "${OS:-}" == "macos" ]]; then
|
||||
warn "IP-forwarding tuning is Linux-only (sysctl.d) — skipping; the exit node still works, just untuned"
|
||||
return 0
|
||||
fi
|
||||
cat >"$TS_EXIT_SYSCTL" <<'EOF'
|
||||
# Written by machine-setup: required to forward traffic for other tailnet nodes,
|
||||
# as an exit node or as a subnet router.
|
||||
@@ -265,6 +276,10 @@ EOF
|
||||
# once, because the settings are per-interface and do not survive the link going
|
||||
# down and back up.
|
||||
install_exit_node_tuning() {
|
||||
if [[ "${OS:-}" == "macos" ]]; then
|
||||
warn "NIC offload tuning is Linux-only (networkd-dispatcher, ethtool) — skipping"
|
||||
return 0
|
||||
fi
|
||||
pkg_is_installed networkd-dispatcher || pkg_install_now networkd-dispatcher
|
||||
|
||||
mkdir -p "$(dirname "$TS_DISPATCHER")"
|
||||
|
||||
@@ -48,7 +48,15 @@ tool_is_installed() { command -v "$(tool_command "$1")" &>/dev/null; }
|
||||
# The installers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# The installer script this used is literally named install_update_linux.sh — there is no
|
||||
# upstream equivalent for macOS to swap in, so this is brew there instead of a raw fetch.
|
||||
# All three tools are real Homebrew formulas (confirmed against the live catalogue), which
|
||||
# also means macOS gets `brew upgrade` maintenance the raw-fetch path never had.
|
||||
tool_install_lazydocker() {
|
||||
if [[ "${OS:-}" == "macos" ]]; then
|
||||
brew install lazydocker
|
||||
return
|
||||
fi
|
||||
curl -fsSL https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh |
|
||||
DIR=/usr/local/bin bash
|
||||
}
|
||||
@@ -57,7 +65,16 @@ tool_install_lazydocker() {
|
||||
# x86_64, so an arm machine downloaded a 404 and tar failed halfway through the
|
||||
# run. lazygit spells the architectures x86_64 and arm64, which is neither of the
|
||||
# two spellings ARCH uses, hence the mapping.
|
||||
#
|
||||
# The URL was also hardcoded to `_Linux_` — brew handles macOS instead of this needing a
|
||||
# second literal-string branch, see tool_install_lazydocker above for why brew and not a
|
||||
# Darwin asset name here.
|
||||
tool_install_lazygit() {
|
||||
if [[ "${OS:-}" == "macos" ]]; then
|
||||
brew install lazygit
|
||||
return
|
||||
fi
|
||||
|
||||
local version asset url
|
||||
case "$ARCH" in
|
||||
amd64) asset="x86_64" ;;
|
||||
@@ -82,7 +99,14 @@ tool_install_lazygit() {
|
||||
# Quiet on purpose. The installer ends by printing how to add starship to bash,
|
||||
# zsh, ion, tcsh and xonsh — five shells' worth of instructions for a step that
|
||||
# already writes the zsh config itself. Errors still come through.
|
||||
#
|
||||
# starship's own installer IS cross-platform (unlike the two above), but brew on macOS
|
||||
# keeps it consistent with the other two tools and gets it `brew upgrade` maintenance.
|
||||
tool_install_starship() {
|
||||
if [[ "${OS:-}" == "macos" ]]; then
|
||||
brew install starship
|
||||
return
|
||||
fi
|
||||
curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b /usr/local/bin >/dev/null
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
;;
|
||||
-l | --list)
|
||||
grep -oP '^step "\K[^"]+' "${BASH_SOURCE[0]}"
|
||||
# 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)
|
||||
@@ -114,15 +116,17 @@ info "Machine: ${OS_NAME} (${ARCH})"
|
||||
info "Packages: ${PM:-none detected}"
|
||||
[[ "$IS_WSL" == true ]] && warn "WSL detected — the suspend, logind and boot-hang steps do not apply here"
|
||||
|
||||
# Everything below this line is written against apt and systemd. Detection above
|
||||
# recognises pacman, dnf and brew so the branches have somewhere to hang, but
|
||||
# nothing implements them yet — and running the apt path on Arch would half-build
|
||||
# a machine and stop somewhere unhelpful. Refuse clearly instead, and relax this
|
||||
# list one entry at a time as each package manager grows a real path.
|
||||
# Everything below this line is written against apt and systemd, with real macOS/brew
|
||||
# branches threaded through where a Mac needs different behaviour (MACOS_SKIP for whole
|
||||
# steps that are server-only, `$OS == macos` for the rest). Detection above also recognises
|
||||
# pacman and dnf so THEIR branches have somewhere to hang, but nothing implements those yet
|
||||
# — running the apt path on Arch would half-build a machine and stop somewhere unhelpful.
|
||||
# Refuse clearly instead, and relax this list one entry at a time as each package manager
|
||||
# grows a real path.
|
||||
case "$PM" in
|
||||
apt) ;;
|
||||
apt | brew) ;;
|
||||
"") fail "Could not find a package manager for '${OS_NAME}'." ;;
|
||||
*) fail "${OS_NAME} uses ${PM}, which this script does not implement yet — apt-based systems only, so far." ;;
|
||||
*) fail "${OS_NAME} uses ${PM}, which this script does not implement yet — apt and brew only, so far." ;;
|
||||
esac
|
||||
|
||||
ask_machine_role
|
||||
@@ -404,7 +408,9 @@ fi
|
||||
|
||||
step "System update"
|
||||
if ! skip; then
|
||||
mapfile -t UPGRADABLE < <(pkg_upgradable)
|
||||
# `mapfile` is bash 4+; macOS's stock bash is 3.2. This is the portable substitute.
|
||||
UPGRADABLE=()
|
||||
while IFS= read -r _line; do UPGRADABLE+=("$_line"); done < <(pkg_upgradable)
|
||||
|
||||
echo ""
|
||||
info "System update — upgrades installed packages to their latest versions"
|
||||
@@ -2479,7 +2485,9 @@ EOF
|
||||
echo " which is why git's own core.editor is deliberately not set: git"
|
||||
echo " falls back to \$EDITOR, so setting it here covers everything."
|
||||
echo ""
|
||||
mapfile -t EDITORS < <(editor_candidates)
|
||||
# mapfile is bash 4+; macOS's stock bash is 3.2.
|
||||
EDITORS=()
|
||||
while IFS= read -r _line; do EDITORS+=("$_line"); done < <(editor_candidates)
|
||||
if ((${#EDITORS[@]} == 0)); then
|
||||
warn "no editor found to offer — skipping"
|
||||
else
|
||||
|
||||
@@ -50,7 +50,9 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
;;
|
||||
-l | --list)
|
||||
grep -oP '^step "\K[^"]+' "${BASH_SOURCE[0]}"
|
||||
# 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)
|
||||
@@ -211,8 +213,11 @@ fi
|
||||
echo ""
|
||||
info "What Officer needs from this machine"
|
||||
|
||||
mapfile -t MISSING < <(missing_tools)
|
||||
mapfile -t MISSING_OPT < <(missing_optional_tools)
|
||||
# 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
|
||||
@@ -274,7 +279,9 @@ if ! skip; then
|
||||
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)
|
||||
# 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}:"
|
||||
|
||||
@@ -47,10 +47,16 @@ fail() {
|
||||
|
||||
ONLY_STEP="${ONLY_STEP:-}"
|
||||
|
||||
# `${var,,}` (case-insensitive --only compare below) is bash 4+. macOS's stock /bin/bash is
|
||||
# 3.2 — confirmed live: "bad substitution" — and under this script's `set -e` that means
|
||||
# `--only <step>` killed the ENTIRE run on macOS, not just the comparison. This is the
|
||||
# portable pre-4 substitute.
|
||||
lower() { tr '[:upper:]' '[:lower:]' <<<"$1"; }
|
||||
|
||||
step() {
|
||||
CURRENT_STEP="$1"
|
||||
if [[ -n "$ONLY_STEP" ]]; then
|
||||
if [[ "${1,,}" == "${ONLY_STEP,,}" ]]; then
|
||||
if [[ "$(lower "$1")" == "$(lower "$ONLY_STEP")" ]]; then
|
||||
SKIP_STEP=false
|
||||
echo ""
|
||||
echo -e "${BOLD}── $1 ──${NC}"
|
||||
@@ -134,7 +140,39 @@ ask_required() {
|
||||
|
||||
user_group() { id -gn "${1:-$USERNAME}" 2>/dev/null || echo "${1:-$USERNAME}"; }
|
||||
|
||||
# The bound address(es) of whatever is LISTENing on a TCP port — one per line, formatted
|
||||
# like `127.0.0.1:5432` or `*:5432`. Used both to ask "is anything on this port at all" and
|
||||
# "is it bound wide enough for a container to reach" (Postgres and the Proxy section ask
|
||||
# opposite versions of that question).
|
||||
#
|
||||
# `ss` (iproute2) is Linux-only — no equivalent ships with it on macOS. `lsof` is the
|
||||
# portable answer: present on every macOS install by default, and common enough on Linux
|
||||
# (already a REQUIRED_TOOL nowhere here, so this still prefers `ss` there since that is the
|
||||
# surface this was written and tested against) that branching by OS rather than picking one
|
||||
# tool for both keeps each path exactly as capable as it always was.
|
||||
listening_addrs() {
|
||||
local port="$1"
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | awk -v p=":${port}\$" '$9 ~ p {print $9}'
|
||||
else
|
||||
ss -ltn 2>/dev/null | awk -v p=":${port}\$" '$4 ~ p {print $4}'
|
||||
fi
|
||||
}
|
||||
|
||||
# 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"); }
|
||||
#
|
||||
# On Linux this script runs as root (that's the whole point of sudo -H -u here: dropping
|
||||
# from root down to the owning account). On macOS it never escalates — officer-setup.sh
|
||||
# refuses to even start under sudo there, "the account running this IS the owner" — so the
|
||||
# process already IS $USERNAME, and re-invoking sudo for the SAME account asks for a real
|
||||
# password on every single call (Dependencies, Schema, Build, Secrets, Services, Repository
|
||||
# all go through this) or fails outright without sudo rights at all.
|
||||
as_owner() {
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
(cd "${2:-/}" && bash -c "$1")
|
||||
else
|
||||
(cd "${2:-/}" && sudo -H -u "$USERNAME" bash -c "$1")
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -24,15 +24,20 @@ OFFICER_SETUP_BUILD_LOADED=1
|
||||
# attached. drizzle-kit then fails instead of waiting, which is the outcome you
|
||||
# want at 3am.
|
||||
push_schema() {
|
||||
sudo -u "$USERNAME" bash -c "cd '$(platform_dir)' && bun db:push </dev/null" 2>&1
|
||||
as_owner "cd '$(platform_dir)' && bun db:push </dev/null" 2>&1
|
||||
}
|
||||
|
||||
# What tables the schema will create, read from the aggregator rather than
|
||||
# guessed. This is what makes the section able to say what it is about to do.
|
||||
#
|
||||
# Portable ERE + sed rather than GNU grep's -P/lookbehind: BSD grep on macOS has no -P at
|
||||
# all (confirmed live — errors immediately), and its 2>/dev/null here would have hidden that
|
||||
# and reported "0 tables" on every macOS run instead, cosmetic but silently wrong.
|
||||
schema_table_count() {
|
||||
local dir
|
||||
dir="$(platform_dir)/src/databases/officer_db/src"
|
||||
grep -oP "^export \* from '\./\K[\w-]+(?=/schema')" "$dir/schema.ts" 2>/dev/null | while read -r f; do
|
||||
grep -E "^export \* from '\./" "$dir/schema.ts" 2>/dev/null |
|
||||
sed -E "s/^export \* from '\.\/([A-Za-z0-9_-]+)\/schema'.*/\1/" | while read -r f; do
|
||||
grep -c "pgTable(" "$dir/$f/schema.ts" 2>/dev/null || true
|
||||
done | awk '{s+=$1} END {print s+0}'
|
||||
}
|
||||
@@ -43,7 +48,7 @@ schema_table_count() {
|
||||
# Not optional and not cosmetic: without it the server has no page to serve. It
|
||||
# is gitignored, so a fresh clone never has one.
|
||||
gen_index() {
|
||||
sudo -u "$USERNAME" bash -c "cd '$(platform_dir)' && bun gen:index '$ENV_PUBLIC_URL'" 2>&1
|
||||
as_owner "cd '$(platform_dir)' && bun gen:index '$ENV_PUBLIC_URL'" 2>&1
|
||||
}
|
||||
|
||||
gen_index_output() { echo "$(platform_dir)/src/apps/officer-web/index.gen.html"; }
|
||||
|
||||
@@ -111,7 +111,12 @@ ENVF
|
||||
# is any better a guess.
|
||||
tailnet_hostname() {
|
||||
local dns ip
|
||||
dns="$(tailscale status --json 2>/dev/null | grep -oP '"DNSName":\s*"\K[^"]+' | head -1)"
|
||||
# jq over grep -oP: this output is real JSON, and GNU grep's -P/\K doesn't exist on BSD
|
||||
# grep at all (confirmed live — errors visibly), which used to leak straight to the
|
||||
# terminal since only the `tailscale` call's stderr was suppressed, not the grep's. jq is
|
||||
# already an unconditional dependency elsewhere in this script (proxy.sh), so this is
|
||||
# strictly a correctness improvement even setting the portability problem aside.
|
||||
dns="$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName // empty')"
|
||||
dns="${dns%.}" # MagicDNS reports it fully qualified, with a trailing dot
|
||||
dns="${dns%%.*}" # and we want the short name
|
||||
if [[ -n "$dns" ]]; then
|
||||
|
||||
@@ -39,6 +39,13 @@ layout_dirs() {
|
||||
echo "$(layout_items_dir)"
|
||||
}
|
||||
|
||||
# A file's owning username, portably: GNU stat (Linux) takes `-c %U`, BSD stat (macOS) has
|
||||
# no `-c` at all and wants `-f %Su` instead — confirmed live, GNU form exits
|
||||
# "illegal option -- c" under macOS's stat. Try GNU first since that is the common case,
|
||||
# fall back to BSD rather than branching on an OS variable this file does not otherwise
|
||||
# track.
|
||||
file_owner() { stat -c %U "$1" 2>/dev/null || stat -f %Su "$1"; }
|
||||
|
||||
# Created owned by the account, because everything that writes into them runs as
|
||||
# the account: the platform under pm2, the app store's compose files, the item
|
||||
# store the agent authors into.
|
||||
@@ -58,7 +65,7 @@ layout_wrong_owner() {
|
||||
local dir
|
||||
while read -r dir; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
[[ "$(stat -c %U "$dir")" == "$USERNAME" ]] || echo "$dir"
|
||||
[[ "$(file_owner "$dir")" == "$USERNAME" ]] || echo "$dir"
|
||||
done < <(layout_dirs)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ pg_client_installed() {
|
||||
# entry, no add-apt-repository. Non-fatal — an install without psql is a working platform,
|
||||
# just a more annoying one to operate.
|
||||
install_pg_client() {
|
||||
local major codename
|
||||
local major
|
||||
major="$(pg_client_major)"
|
||||
[[ -n "$major" ]] || {
|
||||
warn "could not read a major version out of PG_IMAGE=${PG_IMAGE} — skipping the client"
|
||||
@@ -74,6 +74,49 @@ install_pg_client() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
install_pg_client_macos "$major"
|
||||
return $?
|
||||
fi
|
||||
|
||||
install_pg_client_linux "$major"
|
||||
}
|
||||
|
||||
# Homebrew's `postgresql@<major>` matches the apt path's own logic: the version has to agree
|
||||
# with the server's, not just be "a psql". It is keg-only — Homebrew never links more than one
|
||||
# Postgres onto PATH by default, because a dev machine plausibly has several majors installed
|
||||
# for different projects — so `brew install` alone does not make `psql` resolve. Symlinking
|
||||
# only the four client binaries this platform actually uses, rather than `brew link --force`ing
|
||||
# the whole keg, is the same caveat Homebrew's own postgresql formula prints; force-linking
|
||||
# would silently steal `psql` from whatever other major was linked before this ran.
|
||||
install_pg_client_macos() {
|
||||
local major="$1" prefix
|
||||
command -v brew &>/dev/null || {
|
||||
warn "Homebrew is not installed — cannot install postgresql@${major}. Run machine-setup.sh first."
|
||||
return 1
|
||||
}
|
||||
prefix="$(brew --prefix)"
|
||||
|
||||
brew install "postgresql@${major}" &>/dev/null || {
|
||||
warn "brew install postgresql@${major} failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
local bin
|
||||
for bin in psql pg_dump pg_dumpall pg_restore; do
|
||||
[[ -x "${prefix}/opt/postgresql@${major}/bin/${bin}" ]] || continue
|
||||
ln -sf "${prefix}/opt/postgresql@${major}/bin/${bin}" "${prefix}/bin/${bin}"
|
||||
done
|
||||
|
||||
pg_client_installed || {
|
||||
warn "psql is not version ${major} after installing — check: brew list postgresql@${major}"
|
||||
return 1
|
||||
}
|
||||
ok "psql $(psql --version | grep -oE '[0-9]+\.[0-9]+' | head -1) installed via Homebrew"
|
||||
}
|
||||
|
||||
install_pg_client_linux() {
|
||||
local major="$1" codename
|
||||
codename="$(. /etc/os-release && echo "${VERSION_CODENAME:-}")"
|
||||
[[ -n "$codename" ]] || {
|
||||
warn "could not work out this release's codename — cannot add the PostgreSQL repository"
|
||||
@@ -121,7 +164,12 @@ pg_container_running() { docker ps --filter "name=^${PG_CONTAINER}$" --format '{
|
||||
|
||||
# Is something already answering on the port? A Postgres the user runs their own
|
||||
# way is a perfectly good answer, and finding out by failing to bind is not.
|
||||
pg_port_in_use() { ss -ltn 2>/dev/null | grep -qE "127\.0\.0\.1:${PG_PORT}\b|\*:${PG_PORT}\b|0\.0\.0\.0:${PG_PORT}\b"; }
|
||||
#
|
||||
# `ss` is Linux-only; `listening_addrs` (lib/base.sh) is the portable version, branching to
|
||||
# `lsof` on macOS. Confirmed empirically to fail silently the old way: `ss` doesn't exist on
|
||||
# macOS, its "command not found" was swallowed by `2>/dev/null`, and an empty grep result
|
||||
# reported the port as free even when something WAS listening.
|
||||
pg_port_in_use() { [[ -n "$(listening_addrs "$PG_PORT")" ]]; }
|
||||
|
||||
# Bound to loopback, deliberately, and the reason is worth keeping next to the
|
||||
# line it explains.
|
||||
|
||||
@@ -69,7 +69,19 @@ tool_why() {
|
||||
# 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="$(getent passwd "$USERNAME" 2>/dev/null | cut -d: -f6)"
|
||||
USER_HOME="$(eval echo ~"$USERNAME" 2>/dev/null)"
|
||||
[[ "$USER_HOME" == "~"* ]] && USER_HOME=""
|
||||
[[ -n "$USER_HOME" ]] || USER_HOME="/home/${USERNAME}"
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ proxy_ask() {
|
||||
# reads like a firewall fault. So the bind ADDRESS is what gets checked.
|
||||
proxy_require_listening() {
|
||||
local port="$1" listen
|
||||
listen="$(ss -ltnH "sport = :$port" 2>/dev/null | awk '{print $4}')"
|
||||
# `ss` is Linux-only; `listening_addrs` (lib/base.sh) branches to `lsof` on macOS. Same
|
||||
# silent-empty-result failure mode as postgres.sh's pg_port_in_use if this were left as-is.
|
||||
listen="$(listening_addrs "$port" | head -1)"
|
||||
[[ -n "$listen" ]] || {
|
||||
warn "nothing is listening on port ${port} — start Officer first"
|
||||
return 1
|
||||
@@ -170,7 +172,15 @@ proxy_wait_for_dns() {
|
||||
info "(Tailnet: the certificate can issue without this, but the name will not resolve until it exists.)"
|
||||
|
||||
while ((elapsed < timeout)); do
|
||||
got="$(getent hosts "$domain" 2>/dev/null | awk '{print $1}' | head -1)"
|
||||
# `getent` is glibc/Linux-only — absent on macOS (confirmed: "command not found" on a
|
||||
# real machine), silently swallowed by 2>/dev/null, which would have spun this through
|
||||
# its full 30-minute timeout regardless of whether the record actually resolved.
|
||||
# `dscacheutil` is macOS's own equivalent, consulting the same system resolver.
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
got="$(dscacheutil -q host -a name "$domain" 2>/dev/null | awk '/^ip_address:/ {print $2; exit}')"
|
||||
else
|
||||
got="$(getent hosts "$domain" 2>/dev/null | awk '{print $1}' | head -1)"
|
||||
fi
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
ok "${domain} resolves to ${want}"
|
||||
return 0
|
||||
@@ -194,8 +204,18 @@ proxy_ensure_network() {
|
||||
|
||||
# NPM binds its admin UI to the tailnet IP. If docker starts before tailscaled that
|
||||
# address does not exist yet and the WHOLE container fails to start, not just that port.
|
||||
#
|
||||
# Docker Desktop on macOS has no systemd unit to order — it is a GUI app the OS launches, not
|
||||
# a service tailscaled could be declared a dependency of — and this account does not run as
|
||||
# root there either, so `mkdir -p /etc/systemd/...` would fail on permissions rather than do
|
||||
# something merely unnecessary. Same shape as machine-setup's MACOS_SKIP: skip with a reason
|
||||
# rather than let a Linux-only action fail confusingly.
|
||||
proxy_order_docker_after_tailscaled() {
|
||||
[[ "$CHALLENGE" == "dns" ]] || return 0
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
ok "Docker Desktop has no systemd unit to order — skipping"
|
||||
return 0
|
||||
fi
|
||||
local f=/etc/systemd/system/docker.service.d/10-after-tailscaled.conf
|
||||
[[ -f "$f" ]] && return 0
|
||||
mkdir -p "$(dirname "$f")"
|
||||
@@ -378,8 +398,20 @@ proxy_get_token() {
|
||||
# bypass ufw entirely. The symptom is a 504 that looks like a network fault. A container
|
||||
# upstream would need none of this, which is why container upstreams are preferable when
|
||||
# there is a choice.
|
||||
# `ufw` is Debian/Ubuntu — no macOS equivalent through this interface (pf/pfctl is a
|
||||
# different rule syntax entirely, and the Application Firewall machine-setup's own
|
||||
# MACOS_SKIP already documents for "Firewall" is per-APP, not per-subnet/per-port). Skipped
|
||||
# rather than guessed at: Docker Desktop for Mac also runs containers inside its own VM, so
|
||||
# bridge-to-host traffic does not cross the real host firewall the same way it does when
|
||||
# Docker runs directly on a Linux kernel — an unverified pf rule here could be solving a
|
||||
# problem macOS's Docker networking model does not actually have.
|
||||
proxy_allow_bridge_to_host() {
|
||||
local port="$1" subnet
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
ok "no ufw on macOS — Docker Desktop's own networking model handles bridge-to-host, skipping"
|
||||
BRIDGE_GATEWAY="$(docker network inspect "$PROXY_NET" -f '{{(index .IPAM.Config 0).Gateway}}')"
|
||||
return 0
|
||||
fi
|
||||
subnet="$(docker network inspect "$PROXY_NET" -f '{{(index .IPAM.Config 0).Subnet}}')"
|
||||
if ufw status 2>/dev/null | grep -q "${port}.*${subnet%%/*}"; then
|
||||
ok "ufw already allows the bridge to reach port ${port}"
|
||||
|
||||
@@ -35,7 +35,7 @@ bootstrap_secret_store() {
|
||||
|
||||
# From the repo, because the module derives the install root as the parent of
|
||||
# the working directory — the same rule as src/servers/data-path.ts.
|
||||
sudo -u "$USERNAME" bash -c "cd '$(platform_dir)' && bun --eval \"
|
||||
as_owner "cd '$(platform_dir)' && bun --eval \"
|
||||
const { getKey } = await import('officerdb/secret-store');
|
||||
getKey('jwt');
|
||||
\"" >/dev/null 2>&1 || return 1
|
||||
|
||||
@@ -101,24 +101,36 @@ FOOTER
|
||||
}
|
||||
|
||||
pm2_start() {
|
||||
sudo -u "$USERNAME" bash -c "cd '$(platform_dir)' && pm2 startOrRestart '$(ecosystem_file)' --update-env" 2>&1
|
||||
as_owner "cd '$(platform_dir)' && pm2 startOrRestart '$(ecosystem_file)' --update-env" 2>&1
|
||||
}
|
||||
|
||||
pm2_save() { sudo -u "$USERNAME" pm2 save 2>&1; }
|
||||
pm2_save() { as_owner "pm2 save" 2>&1; }
|
||||
|
||||
# Survive a reboot. `pm2 startup` PRINTS a command for root to run rather than
|
||||
# doing it — so this runs what it prints, which is the whole point of already
|
||||
# being root here.
|
||||
#
|
||||
# `systemd` is hardcoded because that IS the only path today — pm2's own "platform" argument
|
||||
# has to be `launchd` on macOS instead, generating a LaunchAgent plist rather than a systemd
|
||||
# unit, and running it is a separate mechanism (`launchctl load`, not "eval a printed sudo
|
||||
# command"). That is real, separate work rather than a one-line swap, so this skips with a
|
||||
# stated reason on macOS instead of pretending the systemd path could ever produce anything
|
||||
# usable there — matches the same call this codebase already made for the Tailscale exit-node
|
||||
# NIC tuning and the proxy's systemd docker-ordering fix.
|
||||
pm2_enable_startup() {
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
warn "pm2 boot-persistence needs a launchd plist on macOS, not a systemd unit — not implemented yet"
|
||||
return 1
|
||||
fi
|
||||
local cmd
|
||||
cmd="$(sudo -u "$USERNAME" bash -c "cd '$(platform_dir)' && pm2 startup systemd -u '$USERNAME' --hp '$USER_HOME'" 2>/dev/null | grep -E '^sudo ' | tail -1)"
|
||||
cmd="$(as_owner "cd '$(platform_dir)' && pm2 startup systemd -u '$USERNAME' --hp '$USER_HOME'" 2>/dev/null | grep -E '^sudo ' | tail -1)"
|
||||
[[ -z "$cmd" ]] && return 1
|
||||
eval "${cmd#sudo }"
|
||||
}
|
||||
|
||||
# One line per process: name, status, restarts.
|
||||
pm2_status_lines() {
|
||||
sudo -u "$USERNAME" pm2 jlist 2>/dev/null |
|
||||
as_owner "pm2 jlist" 2>/dev/null |
|
||||
node -e '
|
||||
let s = ""; process.stdin.on("data", (d) => (s += d)).on("end", () => {
|
||||
let apps = []; try { apps = JSON.parse(s); } catch { }
|
||||
|
||||
Reference in New Issue
Block a user