diff --git a/bin/rclaude b/bin/rclaude index b70ab11..6222c6e 100755 --- a/bin/rclaude +++ b/bin/rclaude @@ -659,7 +659,12 @@ cmd_resume() { exit 1 ;; esac migrate_session "$_host" "$_dst" "$_target" "$_session_cwd" "$_dst_cwd" || exit - RCLAUDE_RESUME_ID=$_target exec "$0" "$_dst" "$_dst_cwd" + # Pass src + src_cwd to the launch path so it can rsync the project + # tree if the dst dir doesn't exist (set RCLAUDE_MIGRATE_SYNC=none + # to skip and just mkdir). + RCLAUDE_MIGRATE_FROM=$_host RCLAUDE_MIGRATE_FROM_CWD=$_session_cwd \ + RCLAUDE_RESUME_ID=$_target \ + exec "$0" "$_dst" "$_dst_cwd" ;; esac } @@ -778,15 +783,53 @@ fi # Remote: pre-flight the directory so a typo or missing path fails loudly # here instead of silently killing the tmux pane and closing the ssh # transport (which looks like a generic 'Connection closed' to the user). +# +# Cross-host mirror exception: when invoked via `resume --on`, we've just +# migrated the session JSONL but the project files may not exist on the +# target. In that case auto-mkdir so the conversation can be resumed; the +# user can sync project files separately (rsync / git clone). The session +# state is what matters most for resume. if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" "test -d ${dir}" 2>/dev/null; then - echo "rclaude: directory not found on $host: $dir" >&2 - case $dir in - *@proj/*|*@apps/*|*@pkg/*) - echo " hint: '@proj/@apps/@pkg' are Claude-instruction aliases, not real shell paths." >&2 - echo " See ~/.claude/instructions/project-paths.md for the real ~/Code// mapping." >&2 - ;; - esac - exit 1 + if [ -n "${RCLAUDE_RESUME_ID:-}" ] && [ -n "${RCLAUDE_MIGRATE_FROM:-}" ]; then + # Cross-host mirror landing: try to rsync the project tree from + # source to dst so claude has the files it expects. Falls back to + # mkdir-only when sync is disabled or rsync fails. Both endpoints + # being remote is unsupported (no two-hop relay). + _sync_mode=${RCLAUDE_MIGRATE_SYNC:-rsync} + _src_host=$RCLAUDE_MIGRATE_FROM + _src_dir=$RCLAUDE_MIGRATE_FROM_CWD + _did_rsync=0 + if [ "$_sync_mode" = "rsync" ] && command -v rsync >/dev/null 2>&1; then + _src_local=0; _dst_local=0 + is_local "$_src_host" && _src_local=1 + is_local "$host" && _dst_local=1 + if [ $((_src_local + _dst_local)) -ge 1 ]; then + _src_arg=$([ "$_src_local" = 1 ] && printf '%s/' "$_src_dir" || printf '%s:%s/' "$_src_host" "$_src_dir") + _dst_arg=$([ "$_dst_local" = 1 ] && printf '%s/' "$dir" || printf '%s:%s/' "$host" "$dir") + printf 'rclaude: rsyncing %s → %s ...\n' "$_src_arg" "$_dst_arg" >&2 + if rsync -a --info=stats1 "$_src_arg" "$_dst_arg" >&2; then + _did_rsync=1 + else + echo "rclaude: rsync failed; falling back to empty mkdir" >&2 + fi + fi + fi + if [ "$_did_rsync" = 0 ]; then + echo "rclaude: $dir doesn't exist on $host — creating empty dir for session resume." >&2 + echo " (sync separately if needed: rsync -a $_src_dir/ $host:$dir/)" >&2 + ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" "mkdir -p ${dir}" 2>/dev/null || { + echo "rclaude: mkdir failed on $host: $dir" >&2; exit 1; } + fi + else + echo "rclaude: directory not found on $host: $dir" >&2 + case $dir in + *@proj/*|*@apps/*|*@pkg/*) + echo " hint: '@proj/@apps/@pkg' are Claude-instruction aliases, not real shell paths." >&2 + echo " See ~/.claude/instructions/project-paths.md for the real ~/Code// mapping." >&2 + ;; + esac + exit 1 + fi fi sync_tmux_conf "$host"