initial: remote-run + tssh + installer
This commit is contained in:
commit
74e7f5529c
5 changed files with 160 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.DS_Store
|
||||
*.swp
|
||||
50
README.md
Normal file
50
README.md
Normal file
|
|
@ -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 <host> <cmd...>`** — One-shot command runner. Spawns a
|
||||
detached tmux session on `<host>`, 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 <host> tmux ls` then
|
||||
`ssh <host> tmux attach -t <session>`.
|
||||
|
||||
- **`bin/tssh <host>`** — Interactive shell wrapper. Auto-attaches to (or
|
||||
creates) a per-user tmux session on `<host>` 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 <host>` |
|
||||
| One-off command (build, test, query) | `remote-run <host> "<cmd>"` |
|
||||
| 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 "$@"
|
||||
```
|
||||
57
bin/remote-run
Executable file
57
bin/remote-run
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
#!/bin/sh
|
||||
# remote-run <host> <cmd...>
|
||||
#
|
||||
# Run a command on <host> 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 <host> tmux ls
|
||||
# ssh <host> tmux attach -t <session>
|
||||
#
|
||||
# <host> 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 <host> <cmd...>" >&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 <<REMOTE
|
||||
session='${session}'
|
||||
log="/tmp/\${session}.log"
|
||||
exitf="/tmp/\${session}.exit"
|
||||
: > "\$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"
|
||||
20
bin/tssh
Executable file
20
bin/tssh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
# tssh <host>
|
||||
#
|
||||
# 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 <host>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
host=$1
|
||||
session="claude-$(whoami)"
|
||||
|
||||
exec ssh -t "$host" "tmux new-session -A -s '${session}'"
|
||||
31
install.sh
Executable file
31
install.sh
Executable file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue