From dfbdb194e21d63628a69a6e0e785688a1817196f Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 6 Jun 2026 19:09:18 -0700 Subject: [PATCH] =?UTF-8?q?feat(@scripts):=20=E2=9C=A8=20enhance=20crc=20t?= =?UTF-8?q?o=20support=20tmux=20sessions=20and=20persistent=20remote=20con?= =?UTF-8?q?trol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- bin/crc | 154 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 119 insertions(+), 35 deletions(-) diff --git a/bin/crc b/bin/crc index 5bbed4f..be4069d 100755 --- a/bin/crc +++ b/bin/crc @@ -1,56 +1,140 @@ #!/bin/sh -# crc — "claude remote", in a fresh iTerm window. +# 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. # -# Thin launcher over `rclaude`: opens a NEW iTerm2 window and starts a durable -# remote Claude Code session in a target directory on a target host. Unlike the -# `cc` alias (which takes over the current tab), this spawns its own window so -# the current shell is left alone. +# `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). +# +# 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; the server keeps running. Reattach with the same +# crc command. # # Usage: -# crc # apricot.lan, mirror of $PWD -# crc # , mirror of $PWD -# crc # , explicit dir +# 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 # +# 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 ~. +# # Env: # CRC_HOST default host when none given (default: apricot.lan) -# -# Everything host/dir/mirror/tmux-related is delegated to `rclaude`; this script -# only owns the "new iTerm window" part. set -eu host=${CRC_HOST:-apricot.lan} -dir=$PWD +new_window=0 +dry_run=0 -case "${1:-}" in - -h|--help) - sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' - exit 0 - ;; -esac +usage() { sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//'; } -[ $# -ge 1 ] && host=$1 -[ $# -ge 2 ] && dir=$2 +# --- arg parse ------------------------------------------------------------- +positional='' # collected host/dir (max 2), space-free tokens unsafe so + # we track count explicitly +have_host=0 +dir_set=0 +dir='' +rc_args='' # everything after `--`, verbatim -command -v rclaude >/dev/null 2>&1 || { - echo "crc: rclaude not found on PATH" >&2 - exit 127 -} +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 ;; + *) + 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 -# Build the command the new window will run. rclaude resolves the local->remote -# mirror itself, so we just hand it host + dir verbatim. -inner="rclaude $(printf %q "$host") $(printf %q "$dir")" - -# Escape for embedding inside an AppleScript double-quoted string. -escaped=$(printf %s "$inner" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') - -osascript </dev/null; then + echo "crc: directory not found on host: \$DIR" >&2 + exit 1 +fi +exec tmux new-session -A -s "\$SESS" "\${SHELL:-/bin/sh} -lc 'cd \"\$DIR\" && exec claude rc \$RC_ARGS'" +BOOT +) +boot_b64=$(printf %s "$boot" | base64 | tr -d '\n') + +run_remote="printf %s '${boot_b64}' | base64 -d | sh" + +echo "crc: ${session} → claude rc in ${abs:-\$HOME/${rel}} on ${host}" >&2 +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" + ;; + *) + exec ssh -t "$host" "$run_remote" + ;; +esac