commit 74e7f5529cb5af1519a6a41042cf0a4f5b2ce9bf Author: Natalie Date: Sat Apr 25 22:13:25 2026 -0700 initial: remote-run + tssh + installer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f31b3e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..28def7d --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# session-tools + +Resilient remote-execution wrappers for SSH/tmux patterns across the lilith +host fleet (plum, apricot, black, quinn-vps, ...). + +The premise: a bare `ssh host cmd` dies the moment the transport hiccups, +killing whatever was running on the remote. These wrappers run commands inside +a detached tmux session on the remote so the work survives the SSH drop. + +## Tools + +- **`bin/remote-run `** — One-shot command runner. Spawns a + detached tmux session on ``, streams stdout/stderr back to your + terminal, propagates the exit code. If the local ssh dies mid-run, the tmux + session continues; reattach with `ssh tmux ls` then + `ssh tmux attach -t `. + +- **`bin/tssh `** — Interactive shell wrapper. Auto-attaches to (or + creates) a per-user tmux session on `` named `claude-$(whoami)`. + Detach with `Ctrl-b d`; transport drops don't kill the shell. + +## Install + +On every host that should have these on `$PATH`: + +```sh +git clone http://forge.black.local/lilith/session-tools.git ~/Code/@scripts/session-tools +~/Code/@scripts/session-tools/install.sh +``` + +Symlinks `bin/remote-run` and `bin/tssh` into `~/bin`. Pulls future updates +via plain `git pull` — symlinks track the repo automatically. + +## When to use what + +| Scenario | Use | +|--------------------------------------------|----------------------------------------------| +| Interactive shell on a remote | `tssh ` | +| One-off command (build, test, query) | `remote-run ""` | +| Long-running job (>1h, must survive reboot)| `systemd --user` unit on the remote, not ssh | + +## Per-host shims (optional) + +If a particular host gets used a lot, drop a one-liner into `~/bin/`: + +```sh +# ~/bin/apricot-run +#!/bin/sh +exec remote-run apricot "$@" +``` diff --git a/bin/remote-run b/bin/remote-run new file mode 100755 index 0000000..9e82264 --- /dev/null +++ b/bin/remote-run @@ -0,0 +1,57 @@ +#!/bin/sh +# remote-run +# +# Run a command on inside a detached tmux session, stream output back, +# propagate exit code. If the local ssh dies mid-run, the tmux session keeps +# going on the remote — recover with: +# ssh tmux ls +# ssh tmux attach -t +# +# is whatever ssh accepts: a Host alias from ~/.ssh/config, a +# user@hostname, an IP, etc. + +set -eu + +if [ $# -lt 2 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +host=$1 +shift + +session="claude-$(whoami)-$$-$(date +%s)" + +# Single-quote-escape the user command for safe embedding in remote bootstrap. +user_cmd=$* +quoted_cmd=$(printf %s "$user_cmd" | sed "s/'/'\\\\''/g") + +# Remote bootstrap — runs the user command in its own bash subshell so that +# any `exit` or `set -e` inside it does NOT short-circuit our exit-capture. +remote_cmd=$(cat < "\$log" +tmux new-session -d -s "\$session" "bash -c '${quoted_cmd}' > \$log 2>&1; echo \\\$? > \$exitf; tmux wait-for -S done-\$session" 2>/tmp/\${session}.tmuxerr +if [ \$? -ne 0 ]; then + echo "tmux failed to start session:" >&2 + cat /tmp/\${session}.tmuxerr >&2 + rm -f /tmp/\${session}.tmuxerr + exit 127 +fi +rm -f /tmp/\${session}.tmuxerr +( tail -F "\$log" 2>/dev/null ) & +tail_pid=\$! +tmux wait-for done-\$session 2>/dev/null +sleep 0.2 +kill \$tail_pid 2>/dev/null || true +wait \$tail_pid 2>/dev/null || true +code=\$(cat "\$exitf" 2>/dev/null || echo 1) +tmux kill-session -t "\$session" 2>/dev/null || true +rm -f "\$log" "\$exitf" +exit \$code +REMOTE +) + +ssh "$host" "$remote_cmd" diff --git a/bin/tssh b/bin/tssh new file mode 100755 index 0000000..7d31dcd --- /dev/null +++ b/bin/tssh @@ -0,0 +1,20 @@ +#!/bin/sh +# tssh +# +# Interactive ssh that auto-attaches to (or creates) a per-user tmux session +# on the remote, so transport drops don't kill your shell. +# +# Detach with Ctrl-b d. Reattach by re-running this command. +# Session name: claude-$LOCAL_USER on the remote box. + +set -eu + +if [ $# -lt 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +host=$1 +session="claude-$(whoami)" + +exec ssh -t "$host" "tmux new-session -A -s '${session}'" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1c09e54 --- /dev/null +++ b/install.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# install.sh — symlink session-tools/bin/* into ~/bin (idempotent). +# +# Run this on every host that should have remote-run / tssh available. + +set -eu + +repo_dir=$(cd "$(dirname "$0")" && pwd) +target=${HOME}/bin + +mkdir -p "$target" + +for src in "$repo_dir"/bin/*; do + name=$(basename "$src") + link="$target/$name" + if [ -L "$link" ] && [ "$(readlink "$link")" = "$src" ]; then + echo "ok: $link -> $src" + continue + fi + if [ -e "$link" ] && [ ! -L "$link" ]; then + echo "skip: $link exists and is not a symlink — leaving alone" >&2 + continue + fi + ln -sfn "$src" "$link" + echo "link: $link -> $src" +done + +case ":$PATH:" in + *":$target:"*) ;; + *) echo "note: add $target to PATH if it isn't already" ;; +esac