feat(@scripts): ✨ enhance crc to support tmux sessions and persistent remote control
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
fe0cc983f3
commit
dfbdb194e2
1 changed files with 119 additions and 35 deletions
154
bin/crc
154
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 <host> # <host>, mirror of $PWD
|
||||
# crc <host> <dir> # <host>, explicit dir
|
||||
# crc # apricot.lan, mirror of $PWD
|
||||
# crc <host> # <host>, mirror of $PWD
|
||||
# crc <host> <dir> # <host>, explicit dir (abs path or ~/...)
|
||||
# crc <host> <dir> -- <args> # 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 <dir> 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 <<OSA
|
||||
# --- 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 <<OSA
|
||||
tell application "iTerm"
|
||||
activate
|
||||
create window with default profile
|
||||
tell current session of current window
|
||||
write text "${escaped}"
|
||||
end tell
|
||||
tell current session of current window to write text "${escaped}"
|
||||
end tell
|
||||
OSA
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- resolve dir into a remote-evaluable shell expression ------------------
|
||||
# rel → mirror of $PWD relative to $HOME (remote expands $HOME)
|
||||
# abs → explicit path, used verbatim (handles absolute and ~/...)
|
||||
rel=''
|
||||
abs=''
|
||||
slug_src=''
|
||||
if [ $dir_set -eq 1 ]; then
|
||||
abs=$dir
|
||||
slug_src=$dir
|
||||
else
|
||||
case "$PWD" in
|
||||
"$HOME") rel='' ; slug_src='home' ;;
|
||||
"$HOME"/*) rel=${PWD#"$HOME"/} ; slug_src=$rel ;;
|
||||
*) rel='' ; slug_src='home' ;; # outside $HOME → remote home
|
||||
esac
|
||||
fi
|
||||
|
||||
# tmux session name: stable, derived from the dir. tmux forbids '.' and ':'.
|
||||
slug=$(printf %s "$slug_src" | tr '[:upper:]' '[:lower:]' | sed -e 's#[^a-z0-9]\{1,\}#-#g' -e 's#^-##' -e 's#-$##')
|
||||
[ -n "$slug" ] || slug='home'
|
||||
session="claude-rc-${slug}"
|
||||
|
||||
# --- build the remote bootstrap (base64'd to survive all quoting layers) ----
|
||||
# Decoded and run by `sh` on the far side. Computes DIR, validates it, then
|
||||
# exec's tmux new-session -A (attach-or-create) running `claude rc` under a
|
||||
# login shell so ~/.local/bin (where claude lives) is on PATH.
|
||||
boot=$(cat <<BOOT
|
||||
REL=$(printf %q "$rel")
|
||||
ABS=$(printf %q "$abs")
|
||||
RC_ARGS=$(printf %q "$rc_args")
|
||||
SESS=$(printf %q "$session")
|
||||
if [ -n "\$ABS" ]; then DIR=\$ABS; else DIR="\$HOME\${REL:+/\$REL}"; fi
|
||||
if ! cd "\$DIR" 2>/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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue