diff --git a/bin/claude-rc b/bin/claude-rc deleted file mode 100755 index 08feff2..0000000 --- a/bin/claude-rc +++ /dev/null @@ -1,147 +0,0 @@ -#!/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; } - # --spawn is mandatory for headless operation: without it `claude rc` - # prompts "Choose [1/2]" for spawn mode and blocks forever. Default to - # worktree (isolated session per spawn — safe for concurrent agents); - # override per-instance with CLAUDE_RC_SPAWN=same-dir|session. - # --permission-mode sets the mode for spawned sessions; bypassPermissions - # so phone/web sessions run without permission prompts (override with - # CLAUDE_RC_PERM=default|acceptEdits|plan|...). - exec claude rc --name "$name" \ - --spawn "${CLAUDE_RC_SPAWN:-worktree}" \ - --permission-mode "${CLAUDE_RC_PERM:-bypassPermissions}" - ;; - 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 diff --git a/bin/crc b/bin/crc deleted file mode 100755 index b993ab5..0000000 --- a/bin/crc +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/sh -# crc — launch a non-persistent, HEADLESS `claude rc` (Remote Control) server in -# a target dir on a target host, and print its claude.ai/code URL. -# -# Headless: claude rc runs detached with output logged (no tmux, no tty) and -# `--spawn` preset, so it never blocks on the interactive spawn-mode prompt. It -# survives terminal/ssh drops but NOT a host reboot (non-persistent). For servers -# that must survive reboot, register them with the `claude-rc` manager instead. -# -# Drive the session from claude.ai/code or the Claude mobile app via the printed -# URL. Re-running just reports the existing server (never duplicates it). -# -# Usage: -# crc # apricot.lan, mirror of $PWD -# crc # , mirror of $PWD -# crc # , explicit dir (abs path or ~/...) -# crc --stop # stop that server -# crc --status # status + URL only (don't start) -# crc --log # tail its log -# crc ... -- # extra args passed to `claude rc` -# -# host: any ssh target (alias, user@host, IP), or local/./localhost. When -# is omitted, $PWD is mirrored to the same path under the remote's $HOME. -# -# Options: -# --spawn worktree | same-dir | session (default: worktree) -# --perm permission mode for spawned sessions: -# bypassPermissions | default | acceptEdits | plan | dontAsk | auto -# (default: bypassPermissions) -# -# Env: CRC_HOST default host when none given (default: apricot.lan) -set -eu - -host=${CRC_HOST:-apricot.lan} -action=launch # launch | stop | status | log -spawn=worktree -perm=bypassPermissions -have_host=0 -dir_set=0 -dir='' -rc_args='' - -usage() { sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'; } - -while [ $# -gt 0 ]; do - case "$1" in - -h|--help) usage; exit 0 ;; - --stop) action=stop; shift ;; - --status) action=status; shift ;; - --log) action=log; shift ;; - --spawn) [ $# -ge 2 ] || { echo "crc: --spawn needs a value" >&2; exit 2; }; spawn=$2; shift 2 ;; - --perm|--permission-mode) [ $# -ge 2 ] || { echo "crc: $1 needs a value" >&2; exit 2; }; perm=$2; shift 2 ;; - --) shift; rc_args=$*; break ;; - -*) echo "crc: unknown option: $1" >&2; exit 2 ;; - *) - if [ $have_host -eq 0 ]; then host=$1; have_host=1 - elif [ $dir_set -eq 0 ]; then dir=$1; dir_set=1 - else echo "crc: too many arguments: $1" >&2; exit 2 - fi - shift ;; - esac -done - -# --- resolve dir + a stable session name (slug) ---------------------------- -rel='' -abs='' -slug_src='' -if [ $dir_set -eq 1 ]; then - abs=$dir - # Normalize so ~/x, $HOME/x and /abs/$HOME/x map to the same slug (the shell - # may have expanded ~ to $HOME before crc saw it). - case "$dir" in - "$HOME"/*) slug_src=${dir#"$HOME"/} ;; - '~/'*) slug_src=${dir#\~/} ;; - *) slug_src=$dir ;; - esac -else - case "$PWD" in - "$HOME") rel='' ; slug_src='home' ;; - "$HOME"/*) rel=${PWD#"$HOME"/} ; slug_src=$rel ;; - *) rel='' ; slug_src='home' ;; - esac -fi -slug=$(printf %s "$slug_src" | tr '[:upper:]' '[:lower:]' | sed -e 's#[^a-z0-9]\{1,\}#-#g' -e 's#^-##' -e 's#-$##') -[ -n "$slug" ] || slug='home' -name="crc-${slug}" - -# --- remote bootstrap (base64'd to survive every quoting layer) ------------ -# Runs on the target (local or via ssh). Manages a detached, logged claude rc -# keyed by $name under the state dir; idempotent (won't double-launch). -bootf=$(mktemp "${TMPDIR:-/tmp}/crc.XXXXXX") -trap 'rm -f "$bootf"' EXIT INT TERM -cat > "$bootf" </dev/null)" 2>/dev/null; } -envid() { grep -aoE 'env_[A-Za-z0-9]+' "\$LOG" 2>/dev/null | tail -1; } -report() { - if alive; then - e=\$(envid) - echo "\$NAME: running (pid \$(cat "\$PIDF")) in \$DIR" - if [ -n "\$e" ]; then echo " https://claude.ai/code?environment=\$e" - else echo " (URL not in log yet — retry: crc ... --status)"; fi - else - echo "\$NAME: not running" - fi -} - -case "\$ACTION" in - stop) - if alive; then kill "\$(cat "\$PIDF")" 2>/dev/null && echo "stopped \$NAME"; else echo "\$NAME not running"; fi - rm -f "\$PIDF" ;; - status) report ;; - log) [ -f "\$LOG" ] && tail -n 40 "\$LOG" || echo "no log: \$LOG" ;; - launch) - if alive; then - echo "crc: \$NAME already running — reporting existing server" - else - cd "\$DIR" 2>/dev/null || { echo "crc: directory not found: \$DIR" >&2; exit 1; } - : > "\$LOG" - nohup claude rc --name "\$NAME" --spawn "\$SPAWN" --permission-mode "\$PERM" \$RC_ARGS >"\$LOG" 2>&1 & - echo \$! > "\$PIDF" - i=0; while [ \$i -lt 20 ] && [ -z "\$(envid)" ] && alive; do sleep 1; i=\$((i+1)); done - fi - report ;; -esac -BOOT -boot_b64=$(base64 < "$bootf" | tr -d '\n') -run_remote="printf %s '${boot_b64}' | base64 -d | sh" - -case "$host" in - local|localhost|.) eval "$run_remote" ;; - *) ssh "$host" "$run_remote" ;; -esac