fix(@scripts): 🐛 update crc to run headless with non-persistent mode
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
953fbc2ee5
commit
fa5e1b640d
1 changed files with 91 additions and 108 deletions
199
bin/crc
199
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 <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
|
||||
# crc # apricot.lan, mirror of $PWD
|
||||
# crc <host> # <host>, mirror of $PWD
|
||||
# crc <host> <dir> # <host>, explicit dir (abs path or ~/...)
|
||||
# crc <host> <dir> --stop # stop that server
|
||||
# crc <host> <dir> --status # status + URL only (don't start)
|
||||
# crc <host> <dir> --log # tail its log
|
||||
# crc ... -- <args> # 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 <dir> 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 <dir>
|
||||
# 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 <m> 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 <<OSA
|
||||
tell application "iTerm"
|
||||
activate
|
||||
create window with default profile
|
||||
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 ~/...)
|
||||
# --- resolve dir + a stable session name (slug) ----------------------------
|
||||
rel=''
|
||||
abs=''
|
||||
slug_src=''
|
||||
if [ $dir_set -eq 1 ]; then
|
||||
abs=$dir
|
||||
# Normalize the slug source so ~/x, $HOME/x and /abs/$HOME/x all yield the
|
||||
# same session name (the shell may have already expanded ~ to $HOME before
|
||||
# crc saw it) — otherwise attach-or-create can't find an existing session.
|
||||
# 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#\~/} ;;
|
||||
|
|
@ -96,67 +71,75 @@ if [ $dir_set -eq 1 ]; then
|
|||
esac
|
||||
else
|
||||
case "$PWD" in
|
||||
"$HOME") rel='' ; slug_src='home' ;;
|
||||
"$HOME") rel='' ; slug_src='home' ;;
|
||||
"$HOME"/*) rel=${PWD#"$HOME"/} ; slug_src=$rel ;;
|
||||
*) rel='' ; slug_src='home' ;; # outside $HOME → remote home
|
||||
*) 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}"
|
||||
|
||||
# tmux session name: a --session override (sanitized — tmux forbids '.' and
|
||||
# ':'), else stable and derived from the dir.
|
||||
if [ -n "$session_override" ]; then
|
||||
session=$(printf %s "$session_override" | tr '.:' '--')
|
||||
else
|
||||
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}"
|
||||
fi
|
||||
|
||||
# --- build the remote bootstrap (base64'd to survive all quoting layers) ----
|
||||
# Decoded and run by `sh` on the far side. Resolves 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). tmux -c sets
|
||||
# the session start-dir, so no fragile inner `cd "$DIR"` is needed.
|
||||
boot=$(cat <<BOOT
|
||||
# --- 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" <<BOOT
|
||||
set -eu
|
||||
NAME=$(printf %q "$name")
|
||||
REL=$(printf %q "$rel")
|
||||
ABS=$(printf %q "$abs")
|
||||
SPAWN=$(printf %q "$spawn")
|
||||
ACTION=$(printf %q "$action")
|
||||
RC_ARGS=$(printf %q "$rc_args")
|
||||
SESS=$(printf %q "$session")
|
||||
ENSURE=$ensure
|
||||
RESPAWN=$respawn
|
||||
|
||||
if [ "\$ABS" = "~" ]; then DIR=\$HOME
|
||||
elif [ -n "\$ABS" ] && [ "\${ABS#~/}" != "\$ABS" ]; then DIR="\$HOME/\${ABS#~/}"
|
||||
elif [ -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 -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 </dev/null >>"\$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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue