#!/usr/bin/env bash # `scripts/bus-service.sh render|install|uninstall|status []`: # the systemd user unit that supervises `scripts/mosaic bus start ` # (lead decision 70). The template is # packages/cli/systemd/mosaic-bus.service.in; the rendered unit is # mosaic-bus@.service in the systemd user directory, one instance per # business (mosaic-bus@). Nothing here reads a capability, the # binding or the token. See packages/cli/README.md, "Bus host". # # render [--claude PATH] print the rendered unit on stdout # install [--dir DIR] [--no-reload] [--claude PATH] # write the unit (write to a temp file, then # rename) and run `systemctl --user daemon-reload` # uninstall [--dir DIR] [--no-reload] # remove the unit; refuses while an instance is active # status unit state, then `mosaic bus status` # # --claude PATH pins the Claude Code binary the unit's sessions run # (MOSAIC_CLAUDE_BIN); without it, `command -v claude` here. The path is # kept as given, so a symlink that tracks updates keeps tracking them. # # Exit codes: 0 ok, 1 operation failed, 4 usage. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TEMPLATE="$REPO/packages/cli/systemd/mosaic-bus.service.in" UNIT_NAME="mosaic-bus@.service" USAGE="usage: scripts/bus-service.sh render [--claude PATH] | install [--dir DIR] [--no-reload] [--claude PATH] | uninstall [--dir DIR] [--no-reload] | status " die() { echo "bus-service: $*" >&2; exit 1; } usage() { echo "$USAGE" >&2; exit 4; } CLAUDE_BIN="" CLAUDE_SET=0 # The unit's MOSAIC_CLAUDE_BIN line, or a comment when there is no claude to # pin. systemd expands % and splits Environment= on spaces, so the path is # held to a plain character set. claude_line() { local bin if [ "$CLAUDE_SET" = 1 ]; then bin="$CLAUDE_BIN" else bin="$(command -v claude || true)" if [ -z "$bin" ]; then echo "bus-service: no claude on PATH; MOSAIC_CLAUDE_BIN left unset (--claude PATH pins one)" >&2 echo "# MOSAIC_CLAUDE_BIN unset: no claude on PATH at install time" return 0 fi fi case "$bin" in /*) ;; *) die "claude path must be absolute: $bin" ;; esac case "$bin" in *[!A-Za-z0-9/._+-]*) die "claude path contains a character the unit cannot carry: $bin" ;; esac [ -f "$bin" ] && [ -x "$bin" ] || die "claude path is not an executable file: $bin" echo "Environment=MOSAIC_CLAUDE_BIN=$bin" } render() { [ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE" local node_dir node_dir="$(dirname "$(command -v node || true)")" [ -n "$node_dir" ] && [ "$node_dir" != "." ] || die "node not found on PATH" local path="/usr/local/bin:/usr/bin:/bin" case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac case "$REPO" in *@*|*'|'*) die "repository path contains a character the template cannot carry: $REPO" ;; esac local claude claude="$(claude_line)" || exit 1 sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" -e "s|^@CLAUDE_ENV@$|$claude|" "$TEMPLATE" } UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" RELOAD=1 parse_flags() { while [ $# -gt 0 ]; do case "$1" in --dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; shift 2 ;; --no-reload) RELOAD=0; shift ;; --claude) [ $# -ge 2 ] || usage; CLAUDE_BIN="$2"; CLAUDE_SET=1; shift 2 ;; *) usage ;; esac done } install_unit() { parse_flags "$@" mkdir -p "$UNIT_DIR" local target="$UNIT_DIR/$UNIT_NAME" tmp unit unit="$(render)" || exit 1 tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")" printf '%s\n' "$unit" > "$tmp" chmod 0644 "$tmp" if [ -f "$target" ] && cmp -s "$tmp" "$target"; then rm -f "$tmp" echo "unchanged: $target" else mv -f "$tmp" "$target" echo "written: $target" fi local bin bin="$(sed -n 's/^Environment=MOSAIC_CLAUDE_BIN=//p' "$target")" if [ -n "$bin" ]; then echo "claude: $bin ($("$bin" --version 2>/dev/null | head -n 1 || true))" else echo "claude: MOSAIC_CLAUDE_BIN unset; sessions run claude from the unit's PATH" fi if [ "$RELOAD" = 1 ]; then systemctl --user daemon-reload || die "daemon-reload failed" echo "daemon-reload done" fi cat </notify/ the notifier refuses a looser directory write /notify//notify.json, mode 0600: {"notifyVersion": 1, "binding": ""} or "binding": null for no DMs systemctl --user enable --now mosaic-bus@ start now and at login systemctl --user status mosaic-bus@ journalctl --user -u mosaic-bus@ -f the host's log systemctl --user stop mosaic-bus@ SIGTERM; restartable survive logout and reboot only with lingering on: loginctl enable-linger ${USER:-$(id -un)} MSG } uninstall_unit() { parse_flags "$@" [ "$CLAUDE_SET" = 0 ] || usage local target="$UNIT_DIR/$UNIT_NAME" if [ "$RELOAD" = 1 ]; then local active active="$(systemctl --user list-units --no-legend --plain 'mosaic-bus@*' 2>/dev/null | awk '$3 == "active" || $3 == "activating" || $3 == "deactivating" {print $1}')" [ -z "$active" ] || die "refusing: instance(s) still running: $(echo "$active" | tr '\n' ' ')stop them first" fi if [ -f "$target" ]; then rm -f "$target" echo "removed: $target" else echo "absent: $target" fi if [ "$RELOAD" = 1 ]; then systemctl --user daemon-reload || die "daemon-reload failed" echo "daemon-reload done" fi } status_unit() { [ $# -eq 1 ] || usage local business="$1" case "$business" in ''|*[!a-z0-9-]*) die "business id must be [a-z0-9-]+" ;; esac local unit="mosaic-bus@$business.service" echo "unit: $unit" echo " enabled: $(systemctl --user is-enabled "$unit" 2>&1 || true)" echo " active: $(systemctl --user is-active "$unit" 2>&1 || true)" systemctl --user show "$unit" -p MainPID -p NRestarts -p ExecMainStartTimestamp -p Result -p Environment 2>/dev/null | sed 's/^/ /' "$REPO/scripts/mosaic" bus status | sed 's/^/ /' || true } [ $# -ge 1 ] || usage cmd="$1"; shift case "$cmd" in render) while [ $# -gt 0 ]; do case "$1" in --claude) [ $# -ge 2 ] || usage; CLAUDE_BIN="$2"; CLAUDE_SET=1; shift 2 ;; *) usage ;; esac done render ;; install) install_unit "$@" ;; uninstall) uninstall_unit "$@" ;; status) status_unit "$@" ;; -h|--help) echo "$USAGE" ;; *) usage ;; esac