2026-06-06 18:59:46 -07:00
|
|
|
#!/bin/sh
|
2026-06-06 19:09:18 -07:00
|
|
|
# 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.
|
2026-06-06 18:59:46 -07:00
|
|
|
#
|
2026-06-06 19:09:18 -07:00
|
|
|
# `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.
|
2026-06-06 18:59:46 -07:00
|
|
|
#
|
|
|
|
|
# Usage:
|
2026-06-06 19:09:18 -07:00
|
|
|
# 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
|
2026-06-06 18:59:46 -07:00
|
|
|
# crc -h | --help
|
|
|
|
|
#
|
2026-06-06 19:09:18 -07:00
|
|
|
# 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 ~.
|
|
|
|
|
#
|
2026-06-06 18:59:46 -07:00
|
|
|
# Env:
|
|
|
|
|
# CRC_HOST default host when none given (default: apricot.lan)
|
|
|
|
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
host=${CRC_HOST:-apricot.lan}
|
2026-06-06 19:09:18 -07:00
|
|
|
new_window=0
|
|
|
|
|
dry_run=0
|
2026-06-06 18:59:46 -07:00
|
|
|
|
2026-06-06 19:09:18 -07:00
|
|
|
usage() { sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//'; }
|
2026-06-06 18:59:46 -07:00
|
|
|
|
2026-06-06 19:09:18 -07:00
|
|
|
# --- 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
|
2026-06-06 18:59:46 -07:00
|
|
|
|
2026-06-06 19:09:18 -07:00
|
|
|
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
|
2026-06-06 18:59:46 -07:00
|
|
|
|
2026-06-06 19:09:18 -07:00
|
|
|
# --- 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
|
2026-06-06 18:59:46 -07:00
|
|
|
tell application "iTerm"
|
|
|
|
|
activate
|
|
|
|
|
create window with default profile
|
2026-06-06 19:09:18 -07:00
|
|
|
tell current session of current window to write text "${escaped}"
|
2026-06-06 18:59:46 -07:00
|
|
|
end tell
|
|
|
|
|
OSA
|
2026-06-06 19:09:18 -07:00
|
|
|
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")
|
2026-06-06 19:49:55 -07:00
|
|
|
if [ -n "\$ABS" ]; then
|
|
|
|
|
case "\$ABS" in
|
|
|
|
|
"~") DIR=\$HOME ;;
|
|
|
|
|
"~/"*) DIR="\$HOME/\${ABS#~/}" ;;
|
|
|
|
|
*) DIR=\$ABS ;;
|
|
|
|
|
esac
|
|
|
|
|
else
|
|
|
|
|
DIR="\$HOME\${REL:+/\$REL}"
|
|
|
|
|
fi
|
2026-06-06 19:09:18 -07:00
|
|
|
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
|