138 lines
5.2 KiB
Bash
Executable file
138 lines
5.2 KiB
Bash
Executable file
#!/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@<name>.service` running `claude rc --name
|
|
# <name>` in <dir>. 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 <name> print the environment URL
|
|
# add <name> <dir> register + enable --now
|
|
# rm <name> disable + unregister
|
|
# sync reconcile units to the registry (boot/after edits)
|
|
# logs <name> [journalctl-args…]
|
|
# restart|stop|start <name>
|
|
# _run <name> 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 <name>" >&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 <name> <dir>" >&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
|