#!/bin/sh # claude-rc — central manager for always-on `claude rc` (Remote Control) servers, # supervised by systemd --user on this host. Runs ON the host (apricot); drive it # from elsewhere over ssh (plum's `rc` function forwards here). # # Single source of truth is the registry: # ~/.config/claude-rc/projects # lines: name=dir ('#' comments) # `dir` is relative to $HOME (or absolute, or ~/...). Each entry maps to a # systemd template instance `claude-rc@.service` running `claude rc --name # ` in . systemd + Linger give boot-persistence and Restart=always. # # Commands: # list registry + unit state + dir # status [name] state + claude.ai/code URL (all, or one) # url print the environment URL # add register + enable --now # rm disable + unregister # sync reconcile units to the registry (boot/after edits) # logs [journalctl-args…] # restart|stop|start # _run internal: exec'd by the template unit set -eu REG=${CLAUDE_RC_REGISTRY:-$HOME/.config/claude-rc/projects} TPL=claude-rc@ # systemd template prefix uc() { systemctl --user "$@"; } # name -> absolute dir (resolves rel-to-HOME, ~/ and absolute). Empty if absent. reg_dir() { [ -f "$REG" ] || return 0 while IFS= read -r line || [ -n "$line" ]; do case "$line" in ''|\#*) continue ;; esac [ "${line%%=*}" = "$1" ] || continue d=${line#*=} case "$d" in /*) printf %s "$d" ;; '~/'*) printf %s "$HOME/${d#\~/}" ;; *) printf %s "$HOME/$d" ;; esac return 0 done < "$REG" } reg_names() { [ -f "$REG" ] || return 0 while IFS= read -r line || [ -n "$line" ]; do case "$line" in ''|\#*) continue ;; esac printf '%s\n' "${line%%=*}" done < "$REG" } url_of() { journalctl --user -u "$TPL$1" -o cat -n 120 2>/dev/null \ | grep -oE 'env_[A-Za-z0-9]+' | tail -1 } require() { [ -n "${1:-}" ] || { echo "claude-rc: missing " >&2; exit 2; }; } cmd=${1:-list}; [ $# -gt 0 ] && shift || true case "$cmd" in _run) name=${1:-}; require "$name" dir=$(reg_dir "$name") [ -n "$dir" ] || { echo "claude-rc: '$name' not in $REG" >&2; exit 1; } cd "$dir" || { echo "claude-rc: dir missing: $dir" >&2; exit 1; } exec claude rc --name "$name" ;; list|ls) printf '%-16s %-10s %s\n' NAME STATE DIR reg_names | while read -r n; do printf '%-16s %-10s %s\n' "$n" "$(uc is-active "$TPL$n" 2>/dev/null || echo -)" "$(reg_dir "$n")" done ;; status|st) show() { env=$(url_of "$1") printf '%-16s %-10s %s\n' "$1" "$(uc is-active "$TPL$1" 2>/dev/null || echo -)" \ "${env:+https://claude.ai/code?environment=$env}" } if [ -n "${1:-}" ]; then show "$1"; else reg_names | while read -r n; do show "$n"; done; fi ;; url) name=${1:-}; require "$name" env=$(url_of "$name") [ -n "$env" ] && echo "https://claude.ai/code?environment=$env" || { echo "claude-rc: no URL for $name" >&2; exit 1; } ;; add) name=${1:-}; dir=${2:-} [ -n "$name" ] && [ -n "$dir" ] || { echo "usage: claude-rc add " >&2; exit 2; } mkdir -p "$(dirname "$REG")"; touch "$REG" if reg_names | grep -qx "$name"; then echo "claude-rc: '$name' already registered ($(reg_dir "$name"))" else printf '%s=%s\n' "$name" "$dir" >> "$REG" echo "registered $name=$dir" fi uc enable --now "$TPL$name" && echo "enabled+started $TPL$name" ;; rm|remove) name=${1:-}; require "$name" uc disable --now "$TPL$name" 2>/dev/null && echo "disabled $TPL$name" || true if [ -f "$REG" ]; then tmp=$(mktemp); grep -v "^$name=" "$REG" > "$tmp" 2>/dev/null || true; mv "$tmp" "$REG" fi echo "unregistered $name" ;; sync) # Bring every registered project up… reg_names | while read -r n; do uc enable --now "$TPL$n" >/dev/null 2>&1 && echo "up: $n" || echo "FAIL: $n" done # …and tear down any enabled instance no longer in the registry. uc list-unit-files "${TPL}*.service" --no-legend 2>/dev/null | awk '{print $1}' | while read -r uf; do inst=${uf#"$TPL"}; inst=${inst%.service} [ -n "$inst" ] || continue if ! reg_names | grep -qx "$inst"; then uc disable --now "$TPL$inst" >/dev/null 2>&1 && echo "down: $inst (orphan)" fi done ;; logs) name=${1:-}; require "$name"; shift || true set -- "$@" [ $# -gt 0 ] || set -- -n 40 --no-pager exec journalctl --user -u "$TPL$name" "$@" ;; restart|stop|start) name=${1:-}; require "$name" uc "$cmd" "$TPL$name" && uc is-active "$TPL$name" 2>/dev/null || true ;; -h|--help|help) sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//' ;; *) echo "claude-rc: unknown command '$cmd' (try: claude-rc help)" >&2; exit 2 ;; esac