From fa5e1b640dc6ef916bc1cc631d030f210b575375 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 6 Jun 2026 20:47:28 -0700 Subject: [PATCH] =?UTF-8?q?fix(@scripts):=20=F0=9F=90=9B=20update=20crc=20?= =?UTF-8?q?to=20run=20headless=20with=20non-persistent=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- bin/crc | 199 ++++++++++++++++++++++++++------------------------------ 1 file changed, 91 insertions(+), 108 deletions(-) diff --git a/bin/crc b/bin/crc index 19f132c..37b5d17 100755 --- a/bin/crc +++ b/bin/crc @@ -1,57 +1,52 @@ #!/bin/sh -# crc — start a `claude rc` (Remote Control) server in a target dir on a target -# host, inside a durable tmux session so transport drops don't kill it. +# 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. # -# `claude rc` (= `claude remote-control`) is a persistent server: you start it -# in a directory, then drive sessions there from claude.ai/code or the Claude -# mobile app. It must keep running after you disconnect — so crc parks it in a -# named tmux session on the host (same durability trick as tssh/remote-run). +# 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. # -# This is for INTERACTIVE, ad-hoc use — you watch it connect, grab the URL, then -# detach (Ctrl-b d) and it keeps running until the host reboots. For servers -# that must survive a reboot, use a systemd --user unit running `claude rc` -# directly instead (see the claude-rc@ units on apricot). -# -# The tmux session name is derived from the directory, so re-running crc for the -# same host+dir RE-ATTACHES the existing server instead of starting a second -# one. Detach with Ctrl-b d; reattach with the same crc command. +# 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 -- # extra args passed to `claude rc` -# crc -n ... # open a NEW iTerm window instead of current tab -# crc -h | --help +# 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 may be any ssh target (alias, user@host, IP), or local/./localhost to run -# on this machine. When is omitted, $PWD is mirrored to the same path -# under the remote's $HOME (like rclaude); paths outside $HOME fall back to ~. +# 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. # -# Env: -# CRC_HOST default host when none given (default: apricot.lan) - +# Options: +# --spawn worktree | same-dir | session (default: worktree) +# +# Env: CRC_HOST default host when none given (default: apricot.lan) set -eu host=${CRC_HOST:-apricot.lan} -new_window=0 -dry_run=0 - -usage() { sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'; } - -# --- arg parse ------------------------------------------------------------- +action=launch # launch | stop | status | log +spawn=worktree have_host=0 dir_set=0 dir='' -rc_args='' # everything after `--`, verbatim +rc_args='' + +usage() { sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//'; } while [ $# -gt 0 ]; do case "$1" in - -h|--help) usage; exit 0 ;; - -n|--new-window) new_window=1; shift ;; - --dry-run) dry_run=1; shift ;; - --) shift; rc_args=$*; break ;; - -*) echo "crc: unknown option: $1" >&2; exit 2 ;; + -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 ;; + --) 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 @@ -61,34 +56,14 @@ while [ $# -gt 0 ]; do esac done -# --- new-window mode: relaunch self (sans -n) in a fresh iTerm window ------- -if [ "$new_window" -eq 1 ]; then - cmd="crc" - [ $have_host -eq 1 ] && cmd="$cmd $(printf %q "$host")" - [ $dir_set -eq 1 ] && cmd="$cmd $(printf %q "$dir")" - [ -n "$rc_args" ] && cmd="$cmd -- $rc_args" - escaped=$(printf %s "$cmd" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') - osascript < "$bootf" </dev/null; then - echo "crc: directory not found on host: \$DIR" >&2 - exit 1 -fi -exec tmux new-session -A -c "\$DIR" -s "\$SESS" "\${SHELL:-/bin/sh} -lc 'exec claude rc \$RC_ARGS'" -BOOT -) -boot_b64=$(printf %s "$boot" | base64 | tr -d '\n') +SD=\${XDG_STATE_HOME:-\$HOME/.local/state}/claude-rc +mkdir -p "\$SD" +LOG="\$SD/\$NAME.log"; PIDF="\$SD/\$NAME.pid" + +alive() { [ -f "\$PIDF" ] && kill -0 "\$(cat "\$PIDF" 2>/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" \$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" -echo "crc: ${session} → claude rc in ${abs:-\$HOME/${rel}} on ${host}" >&2 -[ "$ensure" -eq 0 ] && echo "crc: detach with Ctrl-b d (server keeps running); reattach by re-running this command." >&2 - -if [ "$dry_run" -eq 1 ]; then - echo "--- remote script ($host) ---" >&2 - printf %s "$boot_b64" | base64 -d - echo - exit 0 -fi - case "$host" in - local|localhost|.) - command -v tmux >/dev/null 2>&1 || { echo "crc: tmux not found locally" >&2; exit 127; } - eval "$run_remote" - ;; - *) - if [ "$ensure" -eq 1 ]; then exec ssh "$host" "$run_remote" - else exec ssh -t "$host" "$run_remote"; fi - ;; + local|localhost|.) eval "$run_remote" ;; + *) ssh "$host" "$run_remote" ;; esac