#!/usr/bin/env bash # # Install MAESTRO as a systemd service so it starts automatically on boot. # # Fills in deploy/maestro.service (a template) from the current checkout — node # path, project directory and run user are auto-detected — then installs and # enables the unit. systemd supervises `node dist/main.js` directly; it does not # build, so build first (`scripts/server.sh start` or `npm run build`). # # scripts/install-systemd.sh # user service (no root, runs as you) [default] # scripts/install-systemd.sh --print # preview generated unit, install nothing # scripts/install-systemd.sh --mode system # system-wide service (needs sudo) # scripts/install-systemd.sh --run-as bot # run as a specific user (system mode only) # scripts/install-systemd.sh --name maestro # override service name # scripts/install-systemd.sh --no-start # enable for boot but don't start now # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" TEMPLATE="$PROJECT_DIR/deploy/maestro.service" MODE="user" # user | system (default: user — no root, runs as you) NAME="maestro" RUN_AS="" # system mode only; default resolved below PRINT_ONLY=0 NO_START=0 die() { echo "error: $*" >&2; exit 1; } # Consume the value of a `--opt value` flag, erroring cleanly if it's missing # (a bare `shift 2` on the last arg would abort under `set -e` with no message). need_val() { [[ $# -ge 2 ]] || die "$1 requires a value"; } while [[ $# -gt 0 ]]; do case "$1" in --mode) need_val "$@"; MODE="$2"; shift 2 ;; --mode=*) MODE="${1#*=}"; shift ;; --name) need_val "$@"; NAME="$2"; shift 2 ;; --name=*) NAME="${1#*=}"; shift ;; --run-as) need_val "$@"; RUN_AS="$2"; shift 2 ;; --run-as=*) RUN_AS="${1#*=}"; shift ;; --print|--dry-run) PRINT_ONLY=1; shift ;; --no-start) NO_START=1; shift ;; -h|--help) # Print the leading comment block (after the shebang) as usage text. awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0" exit 0 ;; *) die "unknown argument: $1" ;; esac done [[ "$MODE" == "system" || "$MODE" == "user" ]] || die "--mode must be 'system' or 'user' (got '$MODE')" [[ "$NAME" =~ ^[A-Za-z0-9_.-]+$ ]] || die "invalid --name '$NAME'" [[ -f "$TEMPLATE" ]] || die "template not found: $TEMPLATE" # --- Auto-detect substitution values ------------------------------------------ NODE_BIN="$(command -v node || true)" [[ -n "$NODE_BIN" ]] || die "node not found in PATH" if [[ "$MODE" == "system" ]]; then # Run user: explicit flag > invoking user under sudo > current user. # Never default to root (the app should not build/run as root). RUN_USER="${RUN_AS:-${SUDO_USER:-$USER}}" [[ "$RUN_USER" != "root" ]] || echo "warning: run user resolves to 'root'; pass --run-as to run the app as a non-root user." >&2 # Existence is enforced at install time (below), not for --print previews. else # user mode: the service belongs to the invoking user's systemd manager. # Running under sudo would resolve $USER to root and install a stray unit # under /root — almost never intended. Steer to the right path instead. if [[ $EUID -eq 0 ]]; then die "user mode as root would install under /root. Run WITHOUT sudo, or for a machine-wide service use: --mode system --run-as " fi [[ -z "$RUN_AS" ]] || echo "warning: --run-as is ignored in user mode (the service always runs as '$USER'); use --mode system to run as another user." >&2 RUN_USER="$USER" fi # --- Preflight (skipped for --print) ------------------------------------------ if [[ "$PRINT_ONLY" -eq 0 ]]; then [[ -f "$PROJECT_DIR/dist/main.js" ]] || die "dist/main.js missing — build first: scripts/server.sh start (or npm run build)" if [[ "$MODE" == "system" ]]; then id "$RUN_USER" >/dev/null 2>&1 || die "run user '$RUN_USER' does not exist (pass --run-as )" fi fi # --- Generate the unit from the template -------------------------------------- generate_unit() { # Substitute placeholders. PROJECT_DIR/NODE_BIN are absolute paths without # '|' or '&', safe as sed replacements. sed \ -e "s|@PROJECT_DIR@|$PROJECT_DIR|g" \ -e "s|@NODE@|$NODE_BIN|g" \ -e "s|@RUN_USER@|$RUN_USER|g" \ "$TEMPLATE" | grep -v '^#' | grep -v '^$' \ | if [[ "$MODE" == "user" ]]; then # A user manager owns the process — drop User=, use user-session targets, # and don't wait on the system-only network-online.target. sed \ -e '/^User=/d' \ -e '/^Wants=network-online.target/d' \ -e 's|^After=network-online.target|After=default.target|' \ -e 's|^WantedBy=multi-user.target|WantedBy=default.target|' else cat fi } UNIT_TEXT="$(generate_unit)" if [[ "$PRINT_ONLY" -eq 1 ]]; then echo "# --- generated ${NAME}.service (mode=${MODE}) ---" echo "$UNIT_TEXT" exit 0 fi # --- Install ------------------------------------------------------------------ if [[ "$MODE" == "system" ]]; then UNIT_PATH="/etc/systemd/system/${NAME}.service" if [[ $EUID -eq 0 ]]; then priv() { "$@"; } else command -v sudo >/dev/null 2>&1 || die "system install needs root; re-run with sudo" priv() { sudo "$@"; } fi echo "Installing $UNIT_PATH (run user: $RUN_USER)…" printf '%s\n' "$UNIT_TEXT" | priv tee "$UNIT_PATH" >/dev/null priv systemctl daemon-reload if [[ "$NO_START" -eq 1 ]]; then priv systemctl enable "$NAME" echo "Enabled for boot. Start later with: sudo systemctl start $NAME" else priv systemctl enable --now "$NAME" fi echo "Done. Status: systemctl status $NAME" echo " Logs: journalctl -u $NAME -f" else UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" UNIT_PATH="$UNIT_DIR/${NAME}.service" mkdir -p "$UNIT_DIR" echo "Installing $UNIT_PATH …" printf '%s\n' "$UNIT_TEXT" > "$UNIT_PATH" systemctl --user daemon-reload # Linger lets the user service run at boot without an active login session. if ! loginctl enable-linger "$USER" 2>/dev/null; then echo "warning: could not enable linger for '$USER'; the service may not start until you log in." >&2 echo " Retry with: sudo loginctl enable-linger $USER" >&2 fi if [[ "$NO_START" -eq 1 ]]; then systemctl --user enable "$NAME" echo "Enabled for boot. Start later with: systemctl --user start $NAME" else systemctl --user enable --now "$NAME" fi echo "Done. Status: systemctl --user status $NAME" echo " Logs: journalctl --user -u $NAME -f" fi