feat(v0.3): H1 — tmux keepalive (sessions survive a server restart)

- src/session/tmux.ts: sync tmux CLI wrappers (available/has/kill), best-effort
- config: useTmux from USE_TMUX env (1/0/auto→detect tmux on PATH)
- session: when useTmux, spawn 'tmux new-session -A -s web_<id> <shell>' (the
  node-pty proc is a tmux CLIENT); createSession takes an optional id for
  re-attach; Session.tmuxName; kill() runs tmux kill-session
- manager: handleAttach re-attaches to a surviving 'web_<id>' tmux session after
  a restart (Case 3.5); shutdown kills only the client pty for tmux sessions so
  the shell keeps running
- tests: USE_TMUX:0 in shared integration cfg (no tmux leakage); unit CFGs
  useTmux:false; integration 'H1' (real tmux): set var → restart server →
  re-attach → var survived. 208 tests green.
This commit is contained in:
Yaojia Wang
2026-06-17 19:48:39 +02:00
parent af2a0879e3
commit 9099f73534
8 changed files with 198 additions and 9 deletions

46
src/session/tmux.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* src/session/tmux.ts (H1) — thin synchronous wrappers around the tmux CLI.
*
* Used only when cfg.useTmux is on. Running the shell inside a tmux session
* lets it survive a server/host restart: the node-pty process is just a tmux
* *client*; killing it detaches, and the tmux server keeps the shell alive.
*
* Every call is best-effort and never throws (tmux missing / session gone are
* treated as "false"/no-op).
*/
import { execFileSync } from 'node:child_process'
/** Is the tmux binary available on PATH? */
export function tmuxAvailable(): boolean {
try {
execFileSync('tmux', ['-V'], { stdio: 'ignore' })
return true
} catch {
return false
}
}
/** tmux session name for a web-terminal session id (UUIDs are tmux-safe). */
export function tmuxName(sessionId: string): string {
return `web_${sessionId}`
}
/** Does a tmux session with this name already exist (e.g. after a restart)? */
export function hasSession(name: string): boolean {
try {
execFileSync('tmux', ['has-session', '-t', name], { stdio: 'ignore' })
return true
} catch {
return false
}
}
/** Kill a tmux session (ends the shell). No-op if it's already gone. */
export function killSession(name: string): void {
try {
execFileSync('tmux', ['kill-session', '-t', name], { stdio: 'ignore' })
} catch {
// already gone — fine
}
}