Initial commit
This commit is contained in:
11
.claude-plugin/plugin.json
Normal file
11
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "tmux",
|
||||||
|
"description": "Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, or when user mentions tmux, debugging, or interactive shells.",
|
||||||
|
"version": "1.4.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Alberto Leal"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tmux
|
||||||
|
|
||||||
|
Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, or when user mentions tmux, debugging, or interactive shells.
|
||||||
622
SKILL.md
Normal file
622
SKILL.md
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
---
|
||||||
|
name: tmux
|
||||||
|
description: "Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, or when user mentions tmux, debugging, or interactive shells."
|
||||||
|
license: Vibecoded
|
||||||
|
---
|
||||||
|
|
||||||
|
# tmux Skill
|
||||||
|
|
||||||
|
Use tmux as a programmable terminal multiplexer for interactive work. Works on Linux and macOS with stock tmux; avoid custom config by using a private socket.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
The session registry eliminates repetitive socket/target specification through automatic session tracking (~80% reduction in boilerplate):
|
||||||
|
|
||||||
|
**IMPORTANT**: Before creating a new session, ALWAYS check existing sessions first to avoid name conflicts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check existing sessions to ensure name is available
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
# Create and register a Python REPL session (choose a unique name)
|
||||||
|
./tools/create-session.sh -n claude-python --python
|
||||||
|
|
||||||
|
# Send commands using session name (auto-lookup socket/target)
|
||||||
|
./tools/safe-send.sh -s claude-python -c "print(2+2)" -w ">>>"
|
||||||
|
|
||||||
|
# Or with a single session, omit -s entirely (auto-detect)
|
||||||
|
./tools/safe-send.sh -c "print('hello world')" -w ">>>"
|
||||||
|
|
||||||
|
# List all registered sessions with health status
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
# Clean up dead sessions
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
After starting a session, ALWAYS tell the user how to monitor it by giving them a command to copy/paste (substitute actual values from the session you created):
|
||||||
|
|
||||||
|
```
|
||||||
|
To monitor this session yourself:
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
Or attach directly:
|
||||||
|
tmux -S <socket> attach -t <session-name>
|
||||||
|
|
||||||
|
Or to capture the output once:
|
||||||
|
tmux -S <socket> capture-pane -p -J -t <session-name>:0.0 -S -200
|
||||||
|
```
|
||||||
|
|
||||||
|
This must ALWAYS be printed right after a session was started (i.e. right before you start using the session) and once again at the end of the tool loop. But the earlier you send it, the happier the user will be.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The session registry provides three ways to reference sessions:
|
||||||
|
|
||||||
|
1. **By name** using `-s session-name` (looks up socket/target in registry)
|
||||||
|
2. **Auto-detect** when only one session exists (omit `-s`)
|
||||||
|
3. **Explicit** using `-S socket -t target` (backward compatible)
|
||||||
|
|
||||||
|
Tools automatically choose the right session using this priority order:
|
||||||
|
1. Explicit `-S` and `-t` flags (highest priority)
|
||||||
|
2. Session name `-s` flag (registry lookup)
|
||||||
|
3. Auto-detect single session (if only one exists)
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- No more repeating `-S socket -t target` on every command
|
||||||
|
- Automatic session discovery
|
||||||
|
- Built-in health tracking
|
||||||
|
- Activity timestamps for cleanup decisions
|
||||||
|
- Fully backward compatible
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
For practical examples of managing tmux sessions through their lifecycle, see the [Session Lifecycle Guide](./references/session-lifecycle.md).
|
||||||
|
|
||||||
|
This guide covers:
|
||||||
|
- **Daily workflows**: Ephemeral sessions, long-running analysis, crash recovery, multi-session workspaces
|
||||||
|
- **Decision trees**: Create vs reuse, cleanup timing, error handling
|
||||||
|
- **Tool reference matrix**: Which tools to use at each lifecycle stage
|
||||||
|
- **Troubleshooting**: Quick fixes for common problems (session not found, commands not executing, cleanup issues)
|
||||||
|
- **Best practices**: 10 DO's and 10 DON'Ts with examples
|
||||||
|
|
||||||
|
## Finding sessions
|
||||||
|
|
||||||
|
List all registered sessions with health status:
|
||||||
|
```bash
|
||||||
|
./tools/list-sessions.sh # Table format
|
||||||
|
./tools/list-sessions.sh --json # JSON format
|
||||||
|
```
|
||||||
|
|
||||||
|
Output shows session name, socket, target, health status, PID, and creation time.
|
||||||
|
|
||||||
|
## Sending input safely
|
||||||
|
|
||||||
|
The `./tools/safe-send.sh` helper provides automatic retries, readiness checks, and optional prompt waiting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using session name (looks up socket/target from registry)
|
||||||
|
./tools/safe-send.sh -s claude-python -c "print('hello')" -w ">>>"
|
||||||
|
|
||||||
|
# Auto-detect single session (omit -s)
|
||||||
|
./tools/safe-send.sh -c "print('world')" -w ">>>"
|
||||||
|
|
||||||
|
# Explicit socket/target (backward compatible)
|
||||||
|
./tools/safe-send.sh -S "$SOCKET" -t "$SESSION":0.0 -c "print('hello')" -w ">>>"
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Helper: safe-send.sh](#helper-safe-sendsh) section below for full documentation.
|
||||||
|
|
||||||
|
## Watching output
|
||||||
|
|
||||||
|
- Capture recent history (joined lines to avoid wrapping artifacts): `tmux -S "$SOCKET" capture-pane -p -J -t target -S -200`.
|
||||||
|
- For continuous monitoring, poll with the helper script (below) instead of `tmux wait-for` (which does not watch pane output).
|
||||||
|
- You can also temporarily attach to observe: `tmux -S "$SOCKET" attach -t "$SESSION"`; detach with `Ctrl+b d`.
|
||||||
|
- When giving instructions to a user, **explicitly print a copy/paste monitor command** alongside the action—don't assume they remembered the command.
|
||||||
|
|
||||||
|
## Spawning Processes
|
||||||
|
|
||||||
|
Some special rules for processes:
|
||||||
|
|
||||||
|
- when asked to debug, use lldb by default
|
||||||
|
- **CRITICAL**: When starting a Python interactive shell, **always** set the `PYTHON_BASIC_REPL=1` environment variable before launching Python. This is **essential** - the non-basic console (fancy REPL with syntax highlighting) interferes with send-keys and will cause commands to fail silently.
|
||||||
|
```bash
|
||||||
|
# When using create-session.sh, this is automatic with --python flag
|
||||||
|
./tools/create-session.sh -n my-python --python
|
||||||
|
|
||||||
|
# When creating manually:
|
||||||
|
tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Synchronizing / waiting for prompts
|
||||||
|
|
||||||
|
Use timed polling to avoid races with interactive tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Wait for Python prompt
|
||||||
|
./tools/wait-for-text.sh -s claude-python -p '^>>>' -T 15 -l 4000
|
||||||
|
|
||||||
|
# Auto-detect single session
|
||||||
|
./tools/wait-for-text.sh -p '^>>>' -T 15
|
||||||
|
|
||||||
|
# Explicit socket/target
|
||||||
|
./tools/wait-for-text.sh -S "$SOCKET" -t "$SESSION":0.0 -p '^>>>' -T 15 -l 4000
|
||||||
|
```
|
||||||
|
|
||||||
|
For long-running commands, poll for completion text (`"Type quit to exit"`, `"Program exited"`, etc.) before proceeding.
|
||||||
|
|
||||||
|
## Interactive tool recipes
|
||||||
|
|
||||||
|
- **Python REPL**: Use `./tools/create-session.sh -n my-python --python`; wait for `^>>>`; send code; interrupt with `C-c`. The `--python` flag automatically sets `PYTHON_BASIC_REPL=1`.
|
||||||
|
- **gdb**: Use `./tools/create-session.sh -n my-gdb --gdb`; disable paging with safe-send; break with `C-c`; issue `bt`, `info locals`, etc.; exit via `quit` then confirm `y`.
|
||||||
|
- **Other TTY apps** (ipdb, psql, mysql, node, bash): Use `./tools/create-session.sh -n my-session --shell`; poll for prompt; send literal text and Enter.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
Killing sessions (recommended - removes both tmux session and registry entry):
|
||||||
|
```bash
|
||||||
|
# Kill a specific session by name
|
||||||
|
./tools/kill-session.sh -s session-name
|
||||||
|
|
||||||
|
# Auto-detect and kill single session
|
||||||
|
./tools/kill-session.sh
|
||||||
|
|
||||||
|
# Dry-run to see what would be killed
|
||||||
|
./tools/kill-session.sh -s session-name --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Registry cleanup (removes registry entries only, doesn't kill tmux sessions):
|
||||||
|
```bash
|
||||||
|
# Remove dead sessions from registry
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
|
||||||
|
# Remove sessions older than 1 hour
|
||||||
|
./tools/cleanup-sessions.sh --older-than 1h
|
||||||
|
|
||||||
|
# See what would be removed (dry-run)
|
||||||
|
./tools/cleanup-sessions.sh --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual cleanup (when not using registry):
|
||||||
|
- Kill a session when done: `tmux -S "$SOCKET" kill-session -t "$SESSION"`.
|
||||||
|
- Kill all sessions on a socket: `tmux -S "$SOCKET" list-sessions -F '#{session_name}' | xargs -r -n1 tmux -S "$SOCKET" kill-session -t`.
|
||||||
|
- Remove everything on the private socket: `tmux -S "$SOCKET" kill-server`.
|
||||||
|
|
||||||
|
## Helper: create-session.sh
|
||||||
|
|
||||||
|
`./tools/create-session.sh` creates and registers new tmux sessions with automatic registry integration.
|
||||||
|
|
||||||
|
**IMPORTANT**: Before creating a session, ALWAYS run `./tools/list-sessions.sh` to check for existing sessions and ensure your chosen name is unique.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/create-session.sh -n <name> [--python|--gdb|--shell] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key options:**
|
||||||
|
- `-n`/`--name` session name (required)
|
||||||
|
- `--python` launch Python REPL with PYTHON_BASIC_REPL=1
|
||||||
|
- `--gdb` launch gdb debugger
|
||||||
|
- `--shell` launch bash shell (default)
|
||||||
|
- `-S`/`--socket` custom socket path (optional, uses default)
|
||||||
|
- `-w`/`--window` window name (default: "shell")
|
||||||
|
- `--no-register` don't add to registry
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create Python REPL session
|
||||||
|
./tools/create-session.sh -n claude-python --python
|
||||||
|
|
||||||
|
# Create gdb session
|
||||||
|
./tools/create-session.sh -n claude-gdb --gdb
|
||||||
|
|
||||||
|
# Create session without registering
|
||||||
|
./tools/create-session.sh -n temp-session --shell --no-register
|
||||||
|
|
||||||
|
# Create session with custom socket
|
||||||
|
./tools/create-session.sh -n my-session -S /tmp/custom.sock --python
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns JSON with session info:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "claude-python",
|
||||||
|
"socket": "/tmp/claude-tmux-sockets/claude.sock",
|
||||||
|
"target": "claude-python:0.0",
|
||||||
|
"type": "python-repl",
|
||||||
|
"pid": 12345,
|
||||||
|
"registered": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper: list-sessions.sh
|
||||||
|
|
||||||
|
`./tools/list-sessions.sh` lists all registered sessions with health status.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/list-sessions.sh [--json]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--json` output as JSON instead of table format
|
||||||
|
|
||||||
|
**Table output (default):**
|
||||||
|
```
|
||||||
|
NAME SOCKET TARGET STATUS PID CREATED
|
||||||
|
claude-python claude.sock :0.0 alive 1234 2h ago
|
||||||
|
claude-gdb claude.sock :0.0 dead - 1h ago
|
||||||
|
|
||||||
|
Total: 2 | Alive: 1 | Dead: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON output:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sessions": [
|
||||||
|
{"name": "claude-python", "status": "alive", ...}
|
||||||
|
],
|
||||||
|
"total": 2,
|
||||||
|
"alive": 1,
|
||||||
|
"dead": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Health statuses:**
|
||||||
|
- `alive` - Session running and healthy
|
||||||
|
- `dead` - Pane marked as dead
|
||||||
|
- `missing` - Session/pane not found
|
||||||
|
- `zombie` - Process exited but pane exists
|
||||||
|
- `server` - Tmux server not running
|
||||||
|
|
||||||
|
## Helper: cleanup-sessions.sh
|
||||||
|
|
||||||
|
`./tools/cleanup-sessions.sh` removes dead or old sessions from the registry.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/cleanup-sessions.sh [--dry-run] [--all] [--older-than <duration>]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--dry-run` show what would be cleaned without removing
|
||||||
|
- `--all` remove all sessions (even alive ones)
|
||||||
|
- `--older-than <duration>` remove sessions older than threshold (e.g., "1h", "2d")
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove dead sessions
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
|
||||||
|
# Dry-run to see what would be removed
|
||||||
|
./tools/cleanup-sessions.sh --dry-run
|
||||||
|
|
||||||
|
# Remove sessions inactive for more than 1 hour
|
||||||
|
./tools/cleanup-sessions.sh --older-than 1h
|
||||||
|
|
||||||
|
# Remove all sessions
|
||||||
|
./tools/cleanup-sessions.sh --all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Duration format:** `30m`, `2h`, `1d`, `3600s`
|
||||||
|
|
||||||
|
## Helper: kill-session.sh
|
||||||
|
|
||||||
|
Kill tmux session and remove from registry (atomic operation).
|
||||||
|
|
||||||
|
**Purpose**: Provides a single operation to fully clean up a session by both killing the tmux session and removing it from the registry.
|
||||||
|
|
||||||
|
**Key features**:
|
||||||
|
- Atomic operation (kills session AND deregisters)
|
||||||
|
- Three operation modes: registry lookup, explicit socket/target, auto-detect
|
||||||
|
- Dry-run support for safety
|
||||||
|
- Proper exit codes for all scenarios
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```bash
|
||||||
|
# Kill session by name (registry lookup)
|
||||||
|
tools/kill-session.sh -s claude-python
|
||||||
|
|
||||||
|
# Kill with explicit socket and target
|
||||||
|
tools/kill-session.sh -S /tmp/claude.sock -t my-session:0.0
|
||||||
|
|
||||||
|
# Auto-detect single session
|
||||||
|
tools/kill-session.sh
|
||||||
|
|
||||||
|
# Dry-run to see what would happen
|
||||||
|
tools/kill-session.sh -s claude-python --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
- `-s NAME` - Session name (uses registry lookup)
|
||||||
|
- `-S PATH` - Socket path (explicit mode, requires -t)
|
||||||
|
- `-t TARGET` - Target pane (explicit mode, requires -S)
|
||||||
|
- `--dry-run` - Show operations without executing
|
||||||
|
- `-v` - Verbose output
|
||||||
|
- `-h` - Show help
|
||||||
|
|
||||||
|
**Exit codes**:
|
||||||
|
- 0 - Complete success (killed AND deregistered)
|
||||||
|
- 1 - Partial success (one operation succeeded)
|
||||||
|
- 2 - Complete failure (both failed or not found)
|
||||||
|
- 3 - Invalid arguments
|
||||||
|
|
||||||
|
**Priority order** (when multiple methods specified):
|
||||||
|
1. Explicit -S and -t (highest priority)
|
||||||
|
2. Session name -s (registry lookup)
|
||||||
|
3. Auto-detect (if no flags and only one session exists)
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- Cleaning up after interactive debugging sessions
|
||||||
|
- Removing sessions that are no longer needed
|
||||||
|
- Ensuring complete cleanup (both tmux and registry)
|
||||||
|
- Batch operations with proper error handling
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- Unlike `cleanup-sessions.sh` (which only removes registry entries), this tool also kills the actual tmux session
|
||||||
|
- Use auto-detect mode when you have only one session and want quick cleanup
|
||||||
|
- Dry-run mode is helpful to verify what will be cleaned up before executing
|
||||||
|
|
||||||
|
## Helper: safe-send.sh
|
||||||
|
|
||||||
|
`./tools/safe-send.sh` sends keystrokes to tmux panes with automatic retries, readiness checks, and optional prompt waiting. Prevents dropped commands that can occur when sending to busy or not-yet-ready panes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Session registry mode
|
||||||
|
./tools/safe-send.sh -s session-name -c "command" [-w pattern]
|
||||||
|
|
||||||
|
# Auto-detect mode (single session)
|
||||||
|
./tools/safe-send.sh -c "command" [-w pattern]
|
||||||
|
|
||||||
|
# Explicit mode (backward compatible)
|
||||||
|
./tools/safe-send.sh -t session:0.0 -c "command" [-S socket] [-w pattern]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target selection (priority order):**
|
||||||
|
- `-s`/`--session` session name (looks up socket/target in registry)
|
||||||
|
- `-t`/`--target` explicit pane target (session:window.pane)
|
||||||
|
- (no flags) auto-detect if only one session exists
|
||||||
|
|
||||||
|
**Key options:**
|
||||||
|
- `-c`/`--command` command to send (required; empty string sends just Enter)
|
||||||
|
- `-S`/`--socket` tmux socket path (for custom sockets via -S)
|
||||||
|
- `-L`/`--socket-name` tmux socket name (for named sockets via -L)
|
||||||
|
- `-l`/`--literal` use literal mode (send text without executing)
|
||||||
|
- `-m`/`--multiline` use multiline mode (paste-buffer for code blocks)
|
||||||
|
- `-w`/`--wait` wait for this pattern after sending
|
||||||
|
- `-T`/`--timeout` timeout in seconds (default: 30)
|
||||||
|
- `-r`/`--retries` max retry attempts (default: 3)
|
||||||
|
- `-i`/`--interval` base retry interval in seconds (default: 0.5)
|
||||||
|
- `-v`/`--verbose` verbose output for debugging
|
||||||
|
|
||||||
|
**Exit codes:**
|
||||||
|
- `0` - Command sent successfully
|
||||||
|
- `1` - Failed to send after retries
|
||||||
|
- `2` - Timeout waiting for prompt
|
||||||
|
- `3` - Pane not ready
|
||||||
|
- `4` - Invalid arguments
|
||||||
|
|
||||||
|
**Modes:**
|
||||||
|
- **Normal mode (default):** Sends command and presses Enter (executes in shell/REPL)
|
||||||
|
- **Multiline mode (-m):** Sends multiline code blocks via paste-buffer (~10x faster than line-by-line). Auto-appends blank line for Python REPL execution. Incompatible with `-l`.
|
||||||
|
- **Literal mode (-l):** Sends exact characters without Enter (typing text). Incompatible with `-m`.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Send commands to Python REPL with automatic retry and prompt waiting
|
||||||
|
- Send gdb commands and wait for the gdb prompt
|
||||||
|
- Critical commands that must not be dropped
|
||||||
|
- Send commands immediately after session creation
|
||||||
|
- Automate interactions with any interactive CLI tool
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send Python command using session registry
|
||||||
|
./tools/safe-send.sh -s claude-python -c "print('hello')" -w ">>>" -T 10
|
||||||
|
|
||||||
|
# Auto-detect single session
|
||||||
|
./tools/safe-send.sh -c "print('world')" -w ">>>"
|
||||||
|
|
||||||
|
# Send text in literal mode (no Enter)
|
||||||
|
./tools/safe-send.sh -s claude-python -c "some text" -l
|
||||||
|
|
||||||
|
# Send with custom retry settings
|
||||||
|
./tools/safe-send.sh -s claude-python -c "ls" -r 5 -i 1.0
|
||||||
|
|
||||||
|
# Send control sequence
|
||||||
|
./tools/safe-send.sh -s claude-python -c "C-c"
|
||||||
|
|
||||||
|
# Send multiline Python function (fast, preserves indentation)
|
||||||
|
./tools/safe-send.sh -s claude-python -m -c "def fibonacci(n):
|
||||||
|
if n <= 1:
|
||||||
|
return n
|
||||||
|
return fibonacci(n-1) + fibonacci(n-2)" -w ">>>" -T 10
|
||||||
|
|
||||||
|
# Send multiline class definition
|
||||||
|
./tools/safe-send.sh -s claude-python -m -c "class Calculator:
|
||||||
|
def __init__(self):
|
||||||
|
self.result = 0
|
||||||
|
|
||||||
|
def add(self, x):
|
||||||
|
self.result += x
|
||||||
|
return self" -w ">>>"
|
||||||
|
|
||||||
|
# Explicit socket/target (backward compatible)
|
||||||
|
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets
|
||||||
|
SOCKET="$SOCKET_DIR/claude.sock"
|
||||||
|
./tools/safe-send.sh -S "$SOCKET" -t "$SESSION":0.0 -c "print('hello')" -w ">>>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multiline mode benefits:**
|
||||||
|
- **~10x faster** than sending line-by-line (single operation vs N separate calls)
|
||||||
|
- **Preserves indentation** perfectly (important for Python)
|
||||||
|
- **Auto-executes** in Python REPL (blank line appended automatically)
|
||||||
|
- **Cleaner logs** (one operation instead of many)
|
||||||
|
- **Best for:** Function definitions, class definitions, complex code blocks
|
||||||
|
|
||||||
|
## Helper: wait-for-text.sh
|
||||||
|
|
||||||
|
`./tools/wait-for-text.sh` polls a pane for a regex (or fixed string) with a timeout. Works on Linux/macOS with bash + tmux + grep.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using session name (looks up socket/target from registry)
|
||||||
|
./tools/wait-for-text.sh -s claude-python -p '^>>>' -T 15
|
||||||
|
|
||||||
|
# Auto-detect single session (omit -s)
|
||||||
|
./tools/wait-for-text.sh -p '^>>>' -T 15
|
||||||
|
|
||||||
|
# Explicit socket/target (backward compatible)
|
||||||
|
./tools/wait-for-text.sh -S "$SOCKET" -t "$SESSION":0.0 -p '^>>>' -T 15
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target selection (priority order):**
|
||||||
|
- `-s`/`--session` session name (looks up socket/target in registry)
|
||||||
|
- `-t`/`--target` explicit pane target (session:window.pane)
|
||||||
|
- (no flags) auto-detect if only one session exists
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `-p`/`--pattern` regex to match (required); add `-F` for fixed string
|
||||||
|
- `-S`/`--socket` tmux socket path (for custom sockets via -S)
|
||||||
|
- `-T` timeout seconds (integer, default 15)
|
||||||
|
- `-i` poll interval seconds (default 0.5)
|
||||||
|
- `-l` history lines to search from the pane (integer, default 1000)
|
||||||
|
- Exits 0 on first match, 1 on timeout. On failure prints the last captured text to stderr to aid debugging.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Wait for Python prompt using session name
|
||||||
|
./tools/wait-for-text.sh -s claude-python -p '^>>>' -T 10
|
||||||
|
|
||||||
|
# Wait for gdb prompt with auto-detect
|
||||||
|
./tools/wait-for-text.sh -p '(gdb)' -T 10
|
||||||
|
|
||||||
|
# Explicit socket/target (backward compatible)
|
||||||
|
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets
|
||||||
|
SOCKET="$SOCKET_DIR/claude.sock"
|
||||||
|
./tools/wait-for-text.sh -S "$SOCKET" -t "$SESSION":0.0 -p '^>>>' -T 15
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper: pane-health.sh
|
||||||
|
|
||||||
|
`./tools/pane-health.sh` checks the health status of a tmux pane before operations to prevent "pane not found" errors and detect failures early. Essential for reliable automation.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using session name (looks up socket/target from registry)
|
||||||
|
./tools/pane-health.sh -s claude-python [--format json|text]
|
||||||
|
|
||||||
|
# Auto-detect single session (omit -s)
|
||||||
|
./tools/pane-health.sh --format text
|
||||||
|
|
||||||
|
# Explicit socket/target (backward compatible)
|
||||||
|
./tools/pane-health.sh -S "$SOCKET" -t "$SESSION":0.0 [--format json|text]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target selection (priority order):**
|
||||||
|
- `-s`/`--session` session name (looks up socket/target in registry)
|
||||||
|
- `-t`/`--target` explicit pane target (session:window.pane)
|
||||||
|
- (no flags) auto-detect if only one session exists
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `-S`/`--socket` tmux socket path (for custom sockets via -S)
|
||||||
|
- `--format` output format: `json` (default) or `text`
|
||||||
|
- Exits with status codes indicating health state
|
||||||
|
|
||||||
|
**Exit codes:**
|
||||||
|
- `0` - Healthy (pane alive, process running)
|
||||||
|
- `1` - Dead (pane marked as dead)
|
||||||
|
- `2` - Missing (pane/session doesn't exist)
|
||||||
|
- `3` - Zombie (process exited but pane still exists)
|
||||||
|
- `4` - Server not running
|
||||||
|
|
||||||
|
**JSON output includes:**
|
||||||
|
- `status`: overall health (`healthy`, `dead`, `missing`, `zombie`, `server_not_running`)
|
||||||
|
- `server_running`: boolean
|
||||||
|
- `session_exists`: boolean
|
||||||
|
- `pane_exists`: boolean
|
||||||
|
- `pane_dead`: boolean
|
||||||
|
- `pid`: process ID (or null)
|
||||||
|
- `process_running`: boolean
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Before sending commands: verify pane is ready
|
||||||
|
- After errors: determine if pane crashed
|
||||||
|
- Periodic health checks during long operations
|
||||||
|
- Cleanup decision: which panes to kill vs keep
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check health using session name (JSON output)
|
||||||
|
./tools/pane-health.sh -s claude-python
|
||||||
|
# Output: {"status": "healthy", "server_running": true, ...}
|
||||||
|
|
||||||
|
# Check health with auto-detect (text output)
|
||||||
|
./tools/pane-health.sh --format text
|
||||||
|
# Output: Pane claude-python:0.0 is healthy (PID: 12345, process running)
|
||||||
|
|
||||||
|
# Conditional logic with session registry
|
||||||
|
if ./tools/pane-health.sh -s my-session --format text; then
|
||||||
|
echo "Pane is ready for commands"
|
||||||
|
./tools/safe-send.sh -s my-session -c "print('hello')"
|
||||||
|
else
|
||||||
|
echo "Pane is not healthy (exit code: $?)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Explicit socket/target (backward compatible)
|
||||||
|
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets
|
||||||
|
SOCKET="$SOCKET_DIR/claude.sock"
|
||||||
|
./tools/pane-health.sh -S "$SOCKET" -t "$SESSION":0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced: Direct Socket Control
|
||||||
|
|
||||||
|
For advanced users who need explicit control over socket paths without using the session registry, see the [Direct Socket Control](references/direct-socket-control.md) reference.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- Custom socket isolation requirements
|
||||||
|
- Integration with existing tmux workflows
|
||||||
|
- Testing or debugging tmux configuration
|
||||||
|
|
||||||
|
Most workflows should use the session registry tools described above.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
For comprehensive guidance on using the session registry effectively, see:
|
||||||
|
|
||||||
|
- **[Session Registry Reference](references/session-registry.md)** - Complete documentation including:
|
||||||
|
- Registry architecture and file format
|
||||||
|
- Advanced usage patterns
|
||||||
|
- Troubleshooting guide
|
||||||
|
- Migration from manual socket management
|
||||||
|
- Best practices for session naming, cleanup strategies, and error handling
|
||||||
|
- When to use registry vs. manual approach
|
||||||
|
|
||||||
|
Key recommendations:
|
||||||
|
- Use descriptive session names (e.g., `claude-python-analysis`, not `session1`)
|
||||||
|
- Run `./tools/cleanup-sessions.sh` periodically to remove dead sessions
|
||||||
|
- Use `./tools/list-sessions.sh` to verify session health before long operations
|
||||||
|
- For single-session workflows, omit `-s` flag to leverage auto-detection
|
||||||
|
- For multiple sessions, always use `-s session-name` for clarity
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Session not found in registry:**
|
||||||
|
- Use `./tools/list-sessions.sh` to see all registered sessions
|
||||||
|
- Session may have been created with `--no-register` flag
|
||||||
|
- Registry file may be corrupted (check `$CLAUDE_TMUX_SOCKET_DIR/.sessions.json`)
|
||||||
|
|
||||||
|
**Auto-detection fails with "Multiple sessions found":**
|
||||||
|
- Specify session name explicitly with `-s my-session`
|
||||||
|
- Or clean up unused sessions with `./tools/cleanup-sessions.sh`
|
||||||
|
|
||||||
|
**Pane health check fails:**
|
||||||
|
- Session may have crashed - check with `./tools/list-sessions.sh`
|
||||||
|
- Tmux server may not be running - verify socket exists
|
||||||
|
- Use `./tools/pane-health.sh -s session-name --format text` for detailed diagnostics
|
||||||
|
|
||||||
|
**Registry lock timeout:**
|
||||||
|
- Another process may be writing to registry
|
||||||
|
- Wait a moment and retry
|
||||||
|
- Check for stale lock file: `$CLAUDE_TMUX_SOCKET_DIR/.sessions.lock`
|
||||||
|
|
||||||
|
For more detailed troubleshooting, see the [Session Registry Reference](references/session-registry.md#troubleshooting).
|
||||||
97
plugin.lock.json
Normal file
97
plugin.lock.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:dashed/claude-marketplace:plugins/tmux",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "b791a79cde3dc378f5f48d0f4768846c62bc5109",
|
||||||
|
"treeHash": "c812bb0ddafed26011d6f61daa44be29967ae60526e682cb764e5e47dcc9fe6d",
|
||||||
|
"generatedAt": "2025-11-28T10:16:03.271236Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "tmux",
|
||||||
|
"description": "Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, or when user mentions tmux, debugging, or interactive shells.",
|
||||||
|
"version": "1.4.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "f2fdf0bf02e8605e10c4599cb22e8ad567c432cb2edab0ed69bd41d03ef7b8a8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "SKILL.md",
|
||||||
|
"sha256": "8d54919da5b2f19c4c5f56f70f78d1a911f87b648b91cab68f9a2e1b9871c70b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/list-sessions.sh",
|
||||||
|
"sha256": "5cf212b88d78ba58f3d43b9c36d2479ba37819d4ce7fdd4f2ac3af75f8565d40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/kill-session.sh",
|
||||||
|
"sha256": "42a2b18ca43402e0e9880edb97c56cb49a0ee90501a904b357a6267dc8a15e02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/wait-for-text.sh",
|
||||||
|
"sha256": "7fa8851b3be0d49abb49c4fbdaf31e687036ebf1d40030e66e1d5a7d3d72298a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/pane-health.sh",
|
||||||
|
"sha256": "c43f562af38359e26558c2a5548753c0b245549f828082ad4473db734f795a95"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/safe-send.sh",
|
||||||
|
"sha256": "f505adb563854400da13f8b59f5de782292301e7736849d9d2a2766b1eb9ab2d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/create-session.sh",
|
||||||
|
"sha256": "066417e810ff7b84200bba96edba07257c920f9d2bfa4eaa34e95bddb2c23cb5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/find-sessions.sh",
|
||||||
|
"sha256": "ecfad81dbb7c9aaecb03fbd01f5175902916be18d68184589145c2ebd5585b35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/cleanup-sessions.sh",
|
||||||
|
"sha256": "2b9dd4b78738b341579c34b032c3898284571cc418bac04d25493c69fb64264c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/lib/registry.sh",
|
||||||
|
"sha256": "3ce5fcf343910795e492a6e7b952fd12ba59624be0aaa36271da69ad77201739"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/lib/time_utils.sh",
|
||||||
|
"sha256": "526ffe7bcc4506b0b810d1c6a023bcce8fda22d3e9086eddb926e354729ba869"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/session-lifecycle.md",
|
||||||
|
"sha256": "489dd66ffbada2c49bb4b3b0ddc20daf2edddba1bf6af01957a4ea5719aa658c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/session-registry.md",
|
||||||
|
"sha256": "b2834deabbaabc2ed2b2e8a6e1ee3abb2371776461b9ca01a120a36522636785"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/direct-socket-control.md",
|
||||||
|
"sha256": "936d7dabdc75bd8e8b10ff4513740d3a9e54757892c283c828557c86fc3b2b4b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "59f1b332b5913768c9aa80aa3f12f622fef6daf082e2b8ef9053191ec3ea3b50"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "c812bb0ddafed26011d6f61daa44be29967ae60526e682cb764e5e47dcc9fe6d"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
108
references/direct-socket-control.md
Normal file
108
references/direct-socket-control.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Direct Socket Control
|
||||||
|
|
||||||
|
This guide covers using direct tmux commands for session management without the session registry. This is an advanced approach for users who need explicit control over socket paths and session management.
|
||||||
|
|
||||||
|
**Note:** The session registry is recommended for most workflows. It eliminates ~80% of boilerplate and provides automatic session tracking, health checking, and cleanup. See [Session Registry Reference](session-registry.md) for the standard approach.
|
||||||
|
|
||||||
|
## When to Use This Approach
|
||||||
|
|
||||||
|
Use direct socket control when you need:
|
||||||
|
- Custom socket isolation requirements
|
||||||
|
- Integration with existing tmux workflows
|
||||||
|
- Multiple sessions on different sockets with complex routing
|
||||||
|
- Testing or debugging tmux configuration
|
||||||
|
|
||||||
|
For most interactive development, debugging, and REPL workflows, use the session registry tools instead.
|
||||||
|
|
||||||
|
## Manual Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets # well-known dir for all agent sockets
|
||||||
|
mkdir -p "$SOCKET_DIR"
|
||||||
|
SOCKET="$SOCKET_DIR/claude.sock" # keep agent sessions separate from your personal tmux
|
||||||
|
SESSION=claude-python # slug-like names; avoid spaces
|
||||||
|
tmux -S "$SOCKET" new -d -s "$SESSION" -n shell
|
||||||
|
tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200 # watch output
|
||||||
|
tmux -S "$SOCKET" kill-session -t "$SESSION" # clean up
|
||||||
|
```
|
||||||
|
|
||||||
|
After starting a session, ALWAYS tell the user how to monitor it:
|
||||||
|
|
||||||
|
```
|
||||||
|
To monitor this session yourself:
|
||||||
|
tmux -S "$SOCKET" attach -t claude-python
|
||||||
|
|
||||||
|
Or to capture the output once:
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -J -t claude-python:0.0 -S -200
|
||||||
|
```
|
||||||
|
|
||||||
|
## Socket Convention
|
||||||
|
|
||||||
|
- Agents MUST place tmux sockets under `CLAUDE_TMUX_SOCKET_DIR` (defaults to `${TMPDIR:-/tmp}/claude-tmux-sockets`) and use `tmux -S "$SOCKET"` so we can enumerate/clean them. Create the dir first: `mkdir -p "$CLAUDE_TMUX_SOCKET_DIR"`.
|
||||||
|
- Default socket path to use unless you must isolate further: `SOCKET="$CLAUDE_TMUX_SOCKET_DIR/claude.sock"`.
|
||||||
|
|
||||||
|
## Targeting Panes and Naming
|
||||||
|
|
||||||
|
- Target format: `{session}:{window}.{pane}`, defaults to `:0.0` if omitted. Keep names short (e.g., `claude-py`, `claude-gdb`).
|
||||||
|
- Use `-S "$SOCKET"` consistently to stay on the private socket path. If you need user config, drop `-f /dev/null`; otherwise `-f /dev/null` gives a clean config.
|
||||||
|
- Inspect: `tmux -S "$SOCKET" list-sessions`, `tmux -S "$SOCKET" list-panes -a`.
|
||||||
|
|
||||||
|
## Finding Sessions Manually
|
||||||
|
|
||||||
|
- List sessions on a specific socket: `./tools/find-sessions.sh -S "$SOCKET"`; add `-q partial-name` to filter.
|
||||||
|
- Scan all sockets: `./tools/find-sessions.sh --all` (uses `CLAUDE_TMUX_SOCKET_DIR`)
|
||||||
|
|
||||||
|
## Direct tmux send-keys
|
||||||
|
|
||||||
|
- Prefer literal sends to avoid shell splitting: `tmux -S "$SOCKET" send-keys -t target -l -- "$cmd"`
|
||||||
|
- When composing inline commands, use single quotes or ANSI C quoting to avoid expansion: `tmux ... send-keys -t target -- $'python3 -m http.server 8000'`.
|
||||||
|
- To send control keys: `tmux ... send-keys -t target C-c`, `C-d`, `C-z`, `Escape`, etc.
|
||||||
|
|
||||||
|
## Comparison with Session Registry
|
||||||
|
|
||||||
|
| Feature | Direct Socket Control | Session Registry |
|
||||||
|
|---------|----------------------|------------------|
|
||||||
|
| Setup complexity | Manual, verbose | Automated with tools |
|
||||||
|
| Socket/target specification | Required every time | Once at creation |
|
||||||
|
| Session discovery | Manual enumeration | Automatic tracking |
|
||||||
|
| Health checking | Manual verification | Built-in health status |
|
||||||
|
| Cleanup | Manual kill commands | Automated cleanup tools |
|
||||||
|
| Auto-detection | Not available | Single session auto-detect |
|
||||||
|
| Best for | Custom isolation, CI/CD | Interactive workflows |
|
||||||
|
|
||||||
|
## Migrating to Session Registry
|
||||||
|
|
||||||
|
If you're using direct socket control and want to migrate to the registry approach:
|
||||||
|
|
||||||
|
1. **Create sessions with registry tools:**
|
||||||
|
```bash
|
||||||
|
# Instead of:
|
||||||
|
# tmux -S "$SOCKET" new -d -s my-session -n shell
|
||||||
|
# Use:
|
||||||
|
./tools/create-session.sh -n my-session --shell
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Replace socket/target with session name:**
|
||||||
|
```bash
|
||||||
|
# Instead of:
|
||||||
|
# ./tools/safe-send.sh -S "$SOCKET" -t "$SESSION":0.0 -c "command"
|
||||||
|
# Use:
|
||||||
|
./tools/safe-send.sh -s my-session -c "command"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use registry for cleanup:**
|
||||||
|
```bash
|
||||||
|
# Instead of:
|
||||||
|
# tmux -S "$SOCKET" kill-session -t "$SESSION"
|
||||||
|
# Use:
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Migration Guide](session-registry.md#migration-guide) in the Session Registry Reference for complete migration instructions.
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Session Registry Reference](session-registry.md) - The recommended approach for most workflows
|
||||||
|
- [Session Registry: Best Practices](session-registry.md#best-practices) - When to use registry vs. manual
|
||||||
|
- [Migration Guide](session-registry.md#migration-guide) - Transition from manual to registry approach
|
||||||
503
references/session-lifecycle.md
Normal file
503
references/session-lifecycle.md
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
# Session Lifecycle Guide
|
||||||
|
|
||||||
|
> Practical guide for managing tmux sessions through their complete lifecycle - from creation to cleanup
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Tmux sessions managed by this skill follow a predictable lifecycle. Understanding these stages helps you make better decisions about when to create, reuse, or clean up sessions. This guide provides real-world workflows and decision trees for daily use.
|
||||||
|
|
||||||
|
**When to use this guide:**
|
||||||
|
- You're unsure whether to create a new session or reuse an existing one
|
||||||
|
- You need to debug a crashed or stuck session
|
||||||
|
- You want to understand proper cleanup workflows
|
||||||
|
- You're managing multiple parallel sessions
|
||||||
|
|
||||||
|
## Lifecycle Stages
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ PRE-CHECK │ Check existing sessions
|
||||||
|
│ (optional) │ Tool: list-sessions.sh
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ CREATE │ Spawn new tmux session
|
||||||
|
│ │ Tool: create-session.sh
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ INIT │ Wait for startup (prompt appears)
|
||||||
|
│ │ Tool: wait-for-text.sh
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ ACTIVE USE │ Send commands, capture output
|
||||||
|
│ │ Tools: safe-send.sh, wait-for-text.sh
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ IDLE │ Session exists but unused
|
||||||
|
│ │ Tool: list-sessions.sh (check status)
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ CLEANUP │ Remove dead/stale sessions
|
||||||
|
│ │ Tools: cleanup-sessions.sh, tmux kill-session
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error recovery:**
|
||||||
|
- If session crashes → Check with `pane-health.sh`
|
||||||
|
- If session becomes zombie → Remove with `cleanup-sessions.sh`
|
||||||
|
- If network interruption → Reconnect using `tmux attach` or resume with session name
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### 1. Quick Python Calculation (Ephemeral Session)
|
||||||
|
|
||||||
|
**Use case:** Run a one-off calculation, get the result, and cleanup immediately.
|
||||||
|
|
||||||
|
**Duration:** < 5 minutes
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
```bash
|
||||||
|
# 1. Check no conflicts
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
# 2. Create session
|
||||||
|
./tools/create-session.sh -n quick-calc --python
|
||||||
|
|
||||||
|
# 3. Send calculation
|
||||||
|
./tools/safe-send.sh -s quick-calc -c "import math; print(math.factorial(100))" -w ">>>" -T 10
|
||||||
|
|
||||||
|
# 4. Capture output
|
||||||
|
SOCKET=$(jq -r '.sessions["quick-calc"].socket' ~/.local/state/claude-tmux/.sessions.json)
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -t quick-calc:0.0 -S -10
|
||||||
|
|
||||||
|
# 5. Cleanup immediately
|
||||||
|
tmux -S "$SOCKET" kill-session -t quick-calc
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:** One-time calculations, quick tests, disposable experiments.
|
||||||
|
|
||||||
|
### 2. Long-Running Analysis (Persistent Session)
|
||||||
|
|
||||||
|
**Use case:** Hours or days of interactive REPL work with state preservation.
|
||||||
|
|
||||||
|
**Duration:** Hours to days
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
```bash
|
||||||
|
# 1. Check existing sessions first
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
# 2. Create persistent session with descriptive name
|
||||||
|
./tools/create-session.sh -n data-analysis-2025-11 --python
|
||||||
|
|
||||||
|
# 3. Load data (may take time)
|
||||||
|
./tools/safe-send.sh -s data-analysis-2025-11 -c "import pandas as pd" -w ">>>"
|
||||||
|
./tools/safe-send.sh -s data-analysis-2025-11 -c "df = pd.read_csv('large_dataset.csv')" -w ">>>" -T 300
|
||||||
|
|
||||||
|
# 4. Work interactively over time
|
||||||
|
./tools/safe-send.sh -s data-analysis-2025-11 -c "df.describe()" -w ">>>"
|
||||||
|
# ... hours later ...
|
||||||
|
./tools/safe-send.sh -s data-analysis-2025-11 -c "df.groupby('category').mean()" -w ">>>"
|
||||||
|
|
||||||
|
# 5. Check session health periodically
|
||||||
|
./tools/pane-health.sh -s data-analysis-2025-11
|
||||||
|
|
||||||
|
# 6. Cleanup only when done (days later)
|
||||||
|
./tools/list-sessions.sh # Verify it's the right session
|
||||||
|
tmux -S "$(jq -r '.sessions["data-analysis-2025-11"].socket' ~/.local/state/claude-tmux/.sessions.json)" kill-session -t data-analysis-2025-11
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:** Long-running analysis, state preservation, multiple work sessions.
|
||||||
|
|
||||||
|
**Best practices:**
|
||||||
|
- Use descriptive names (include date or project name)
|
||||||
|
- Check health before resuming work
|
||||||
|
- Don't cleanup until completely done
|
||||||
|
- Capture important output early
|
||||||
|
|
||||||
|
### 3. Recovering from Crashed Session
|
||||||
|
|
||||||
|
**Use case:** Session died unexpectedly, need to investigate and restart.
|
||||||
|
|
||||||
|
**Duration:** 5-10 minutes
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
```bash
|
||||||
|
# 1. Check session status
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
# Output shows: status "dead" or "zombie"
|
||||||
|
|
||||||
|
# 2. Diagnose the issue
|
||||||
|
./tools/pane-health.sh -s crashed-session --format text
|
||||||
|
# Check exit code: 1=dead, 2=missing, 3=zombie
|
||||||
|
|
||||||
|
# 3. Capture any remaining output for debugging
|
||||||
|
SOCKET=$(jq -r '.sessions["crashed-session"].socket' ~/.local/state/claude-tmux/.sessions.json)
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -t crashed-session:0.0 -S -200 > crash-output.txt 2>/dev/null || echo "Pane unavailable"
|
||||||
|
|
||||||
|
# 4. Remove dead session from registry
|
||||||
|
./tools/cleanup-sessions.sh # Removes dead sessions automatically
|
||||||
|
|
||||||
|
# 5. Create replacement session
|
||||||
|
./tools/create-session.sh -n crashed-session-recovery --python
|
||||||
|
|
||||||
|
# 6. Resume work with new session
|
||||||
|
./tools/safe-send.sh -s crashed-session-recovery -c "# Resuming from crash..." -w ">>>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:** Session crashed, process died, debugging needed.
|
||||||
|
|
||||||
|
**Troubleshooting tips:**
|
||||||
|
- Always capture output before cleanup (may contain error messages)
|
||||||
|
- Check system logs if crash is mysterious: `dmesg`, `journalctl`
|
||||||
|
- Verify PYTHON_BASIC_REPL=1 was set (common cause of silent failures)
|
||||||
|
|
||||||
|
### 4. Multi-Session Workspace (Parallel Tasks)
|
||||||
|
|
||||||
|
**Use case:** Multiple parallel tasks (data loading, analysis, monitoring) in separate sessions.
|
||||||
|
|
||||||
|
**Duration:** Variable
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
```bash
|
||||||
|
# 1. Check existing sessions
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
# 2. Create multiple sessions with clear names
|
||||||
|
./tools/create-session.sh -n loader-session --python
|
||||||
|
./tools/create-session.sh -n analysis-session --python
|
||||||
|
./tools/create-session.sh -n monitor-session --python
|
||||||
|
|
||||||
|
# 3. Start parallel work
|
||||||
|
# Session 1: Load large dataset
|
||||||
|
./tools/safe-send.sh -s loader-session -c "import pandas as pd; df = pd.read_csv('huge.csv')" -w ">>>" -T 600 &
|
||||||
|
|
||||||
|
# Session 2: Run analysis on existing data
|
||||||
|
./tools/safe-send.sh -s analysis-session -c "results = analyze_data()" -w ">>>" -T 300 &
|
||||||
|
|
||||||
|
# Session 3: Monitor system
|
||||||
|
./tools/safe-send.sh -s monitor-session -c "import psutil; psutil.cpu_percent()" -w ">>>"
|
||||||
|
|
||||||
|
# 4. Check all sessions status
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
# 5. Cleanup when done
|
||||||
|
./tools/cleanup-sessions.sh --all # Or cleanup individually
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:** Parallel independent tasks, workspace organization, resource isolation.
|
||||||
|
|
||||||
|
**Best practices:**
|
||||||
|
- Use descriptive session names (purpose-based: loader-*, analysis-*, monitor-*)
|
||||||
|
- One session per major task (don't overload single session)
|
||||||
|
- Check `list-sessions.sh` frequently to monitor all sessions
|
||||||
|
- Cleanup all related sessions together when project complete
|
||||||
|
|
||||||
|
## Decision Trees
|
||||||
|
|
||||||
|
### Should I Create a New Session or Reuse?
|
||||||
|
|
||||||
|
```
|
||||||
|
Start
|
||||||
|
│
|
||||||
|
├─→ Is this a quick one-off task?
|
||||||
|
│ └─→ YES → Create new ephemeral session
|
||||||
|
│ (cleanup immediately after)
|
||||||
|
│
|
||||||
|
├─→ Do I have an existing session for this work?
|
||||||
|
│ ├─→ YES → Is it still alive?
|
||||||
|
│ │ ├─→ YES → Reuse existing session
|
||||||
|
│ │ └─→ NO → Create new session
|
||||||
|
│ └─→ NO → Create new session
|
||||||
|
│
|
||||||
|
└─→ Am I starting a new long-running project?
|
||||||
|
└─→ YES → Create new persistent session
|
||||||
|
(descriptive name with date/project)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules of thumb:**
|
||||||
|
- ✅ **Create new** for: Different projects, parallel tasks, isolation needed
|
||||||
|
- ✅ **Reuse** for: Continuing same work, state preservation needed
|
||||||
|
- ❌ **Don't reuse** for: Unrelated tasks (state pollution), different projects
|
||||||
|
|
||||||
|
### When Should I Clean Up Sessions?
|
||||||
|
|
||||||
|
```
|
||||||
|
Start
|
||||||
|
│
|
||||||
|
├─→ Is session marked as "dead" or "zombie"?
|
||||||
|
│ └─→ YES → Cleanup immediately
|
||||||
|
│ (./tools/cleanup-sessions.sh)
|
||||||
|
│
|
||||||
|
├─→ Has the task completed?
|
||||||
|
│ └─→ YES → Cleanup immediately
|
||||||
|
│ (tmux kill-session + cleanup-sessions.sh)
|
||||||
|
│
|
||||||
|
├─→ Is session idle for > 1 hour?
|
||||||
|
│ ├─→ AND no state preservation needed?
|
||||||
|
│ │ └─→ YES → Cleanup
|
||||||
|
│ └─→ OR state needed?
|
||||||
|
│ └─→ NO → Keep (long-running analysis)
|
||||||
|
│
|
||||||
|
└─→ Am I done for the day but continuing tomorrow?
|
||||||
|
└─→ Keep session (resume later)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cleanup commands:**
|
||||||
|
```bash
|
||||||
|
# Remove only dead/zombie sessions (safe, default)
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
|
||||||
|
# Remove sessions older than 1 hour
|
||||||
|
./tools/cleanup-sessions.sh --older-than 1h
|
||||||
|
|
||||||
|
# Remove all sessions (careful!)
|
||||||
|
./tools/cleanup-sessions.sh --all
|
||||||
|
|
||||||
|
# Preview what would be removed (dry-run)
|
||||||
|
./tools/cleanup-sessions.sh --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### How Should I Handle Session Errors?
|
||||||
|
|
||||||
|
```
|
||||||
|
Error Detected
|
||||||
|
│
|
||||||
|
├─→ Is session responding to commands?
|
||||||
|
│ ├─→ NO → Check health (pane-health.sh)
|
||||||
|
│ │ ├─→ Dead/zombie → Capture output → Cleanup → Recreate
|
||||||
|
│ │ └─→ Alive but hung → Send C-c → Retry command
|
||||||
|
│ └─→ YES → Problem is elsewhere (not session)
|
||||||
|
│
|
||||||
|
├─→ Are commands being executed but output wrong?
|
||||||
|
│ └─→ Check PYTHON_BASIC_REPL=1 was set
|
||||||
|
│ └─→ Not set? Restart session with env var
|
||||||
|
│
|
||||||
|
└─→ Is prompt not appearing?
|
||||||
|
└─→ Increase timeout (-T flag)
|
||||||
|
└─→ Still failing? Check pane content manually
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool Reference Matrix
|
||||||
|
|
||||||
|
| Lifecycle Stage | Tool | Purpose | Example Command |
|
||||||
|
|-----------------|------|---------|-----------------|
|
||||||
|
| **Pre-check** | `list-sessions.sh` | Check existing sessions | `./tools/list-sessions.sh` |
|
||||||
|
| **Create** | `create-session.sh` | Spawn new session | `./tools/create-session.sh -n my-session --python` |
|
||||||
|
| **Init** | `wait-for-text.sh` | Wait for startup prompt | `./tools/wait-for-text.sh -s my-session -p ">>>" -T 15` |
|
||||||
|
| **Active Use** | `safe-send.sh` | Send commands safely | `./tools/safe-send.sh -s my-session -c "print(42)" -w ">>>"` |
|
||||||
|
| **Active Use** | `safe-send.sh` (multiline) | Send code blocks | `./tools/safe-send.sh -s my-session -m -c "def foo():\n return 42" -w ">>>"` |
|
||||||
|
| **Monitoring** | `pane-health.sh` | Check session health | `./tools/pane-health.sh -s my-session` |
|
||||||
|
| **Monitoring** | `list-sessions.sh` | View all sessions | `./tools/list-sessions.sh --json` |
|
||||||
|
| **Capture** | `tmux capture-pane` | Get session output | `tmux -S $SOCKET capture-pane -p -t session:0.0 -S -100` |
|
||||||
|
| **Cleanup** | `cleanup-sessions.sh` | Remove dead sessions | `./tools/cleanup-sessions.sh` |
|
||||||
|
| **Cleanup** | `tmux kill-session` | Terminate specific session | `tmux -S $SOCKET kill-session -t my-session` |
|
||||||
|
|
||||||
|
## Troubleshooting Quick Reference
|
||||||
|
|
||||||
|
### Session Not Found
|
||||||
|
|
||||||
|
**Symptom:** `./tools/safe-send.sh -s my-session -c "cmd"` fails with "Session 'my-session' not found in registry"
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Session was never created
|
||||||
|
2. Session was created but not registered (`--no-register` flag used)
|
||||||
|
3. Session was cleaned up by mistake
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
```bash
|
||||||
|
# Check what sessions exist
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
# Check if session exists outside registry
|
||||||
|
./tools/find-sessions.sh --all
|
||||||
|
|
||||||
|
# Recreate session
|
||||||
|
./tools/create-session.sh -n my-session --python
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands Not Executing
|
||||||
|
|
||||||
|
**Symptom:** Commands sent but REPL shows no activity
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. PYTHON_BASIC_REPL=1 not set (most common!)
|
||||||
|
2. Session crashed/dead
|
||||||
|
3. Prompt not detected correctly
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
```bash
|
||||||
|
# Check session health
|
||||||
|
./tools/pane-health.sh -s my-session --format text
|
||||||
|
|
||||||
|
# Manually check what's in the pane
|
||||||
|
SOCKET=$(jq -r '.sessions["my-session"].socket' ~/.local/state/claude-tmux/.sessions.json)
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -t my-session:0.0 -S -20
|
||||||
|
|
||||||
|
# If Python REPL, verify PYTHON_BASIC_REPL=1 was set
|
||||||
|
# Symptom: You'll see fancy colored prompts instead of plain >>>
|
||||||
|
# Fix: Kill and recreate with correct env var
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Capture Issues
|
||||||
|
|
||||||
|
**Symptom:** `tmux capture-pane` returns incomplete or truncated output
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Pane history buffer too small
|
||||||
|
2. Lines wrapped due to terminal width
|
||||||
|
3. ANSI color codes interfering
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
```bash
|
||||||
|
# Capture more history (-S flag = start line)
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -t session:0.0 -S -500
|
||||||
|
|
||||||
|
# Join wrapped lines (-J flag)
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -J -t session:0.0 -S -200
|
||||||
|
|
||||||
|
# Strip ANSI color codes
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -t session:0.0 | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dead/Zombie Sessions
|
||||||
|
|
||||||
|
**Symptom:** `./tools/list-sessions.sh` shows status "dead" or "zombie"
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Process crashed
|
||||||
|
2. Session was killed manually
|
||||||
|
3. Out of memory / system issue
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
```bash
|
||||||
|
# Capture any remaining output for debugging
|
||||||
|
SOCKET=$(jq -r '.sessions["dead-session"].socket' ~/.local/state/claude-tmux/.sessions.json)
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -t dead-session:0.0 -S -500 > debug.txt 2>/dev/null
|
||||||
|
|
||||||
|
# Remove from registry
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
|
||||||
|
# Recreate if needed
|
||||||
|
./tools/create-session.sh -n replacement-session --python
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Sessions with Same Name
|
||||||
|
|
||||||
|
**Symptom:** `./tools/create-session.sh -n my-session --python` fails with "session already exists"
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Forgot to check existing sessions first
|
||||||
|
2. Previous session not cleaned up
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
```bash
|
||||||
|
# Check existing sessions
|
||||||
|
./tools/list-sessions.sh
|
||||||
|
|
||||||
|
# Use different name OR cleanup old session first
|
||||||
|
tmux -S "$SOCKET" kill-session -t my-session
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
|
||||||
|
# Then create new session
|
||||||
|
./tools/create-session.sh -n my-session --python
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### ✅ DO
|
||||||
|
|
||||||
|
1. **Always check before creating**
|
||||||
|
```bash
|
||||||
|
./tools/list-sessions.sh # Check for conflicts first
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use descriptive session names**
|
||||||
|
```bash
|
||||||
|
# Good: data-analysis-2025-11, debug-api-auth, monitor-prod
|
||||||
|
# Bad: session1, test, tmp
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set PYTHON_BASIC_REPL=1 for Python**
|
||||||
|
```bash
|
||||||
|
# This is handled by create-session.sh --python automatically
|
||||||
|
# But verify if creating manually
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Wait for prompts after every command**
|
||||||
|
```bash
|
||||||
|
./tools/safe-send.sh -s session -c "command" -w ">>>" -T 10
|
||||||
|
# Always include -w flag for synchronization
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Use session registry (-s flag)**
|
||||||
|
```bash
|
||||||
|
# Preferred: ./tools/safe-send.sh -s my-session -c "cmd"
|
||||||
|
# Avoid: ./tools/safe-send.sh -S $SOCKET -t session:0.0 -c "cmd"
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Check health before critical operations**
|
||||||
|
```bash
|
||||||
|
if ./tools/pane-health.sh -s session; then
|
||||||
|
./tools/safe-send.sh -s session -c "critical_command"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Use multiline mode for code blocks**
|
||||||
|
```bash
|
||||||
|
# 10x faster than line-by-line
|
||||||
|
./tools/safe-send.sh -s session -m -c "def foo():\n return 42" -w ">>>"
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Cleanup dead sessions regularly**
|
||||||
|
```bash
|
||||||
|
# Run periodically or in cleanup scripts
|
||||||
|
./tools/cleanup-sessions.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
9. **Capture output early and often**
|
||||||
|
```bash
|
||||||
|
# Before making destructive changes
|
||||||
|
tmux -S "$SOCKET" capture-pane -p -t session:0.0 -S -200 > backup.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
10. **Use --dry-run for cleanup preview**
|
||||||
|
```bash
|
||||||
|
# See what would be removed before executing
|
||||||
|
./tools/cleanup-sessions.sh --dry-run
|
||||||
|
./tools/cleanup-sessions.sh --older-than 2d --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T
|
||||||
|
|
||||||
|
1. **Don't create sessions without checking first** - Leads to name conflicts
|
||||||
|
2. **Don't reuse sessions for unrelated work** - State pollution causes bugs
|
||||||
|
3. **Don't forget -w flag when sending commands** - Race conditions
|
||||||
|
4. **Don't skip health checks before critical operations** - Pane might be dead
|
||||||
|
5. **Don't use generic session names** - Hard to manage multiple sessions
|
||||||
|
6. **Don't leave dead sessions in registry** - Clutters output, wastes resources
|
||||||
|
7. **Don't forget to cleanup ephemeral sessions** - Resource leaks
|
||||||
|
8. **Don't send commands too fast** - Wait for prompts between commands
|
||||||
|
9. **Don't ignore session health warnings** - Investigate issues early
|
||||||
|
10. **Don't use line-by-line for large code blocks** - Use multiline mode instead
|
||||||
|
|
||||||
|
## Related References
|
||||||
|
|
||||||
|
- [Session Registry Guide](./session-registry.md) - Deep dive on registry system
|
||||||
|
- [Direct Socket Control](./direct-socket-control.md) - Advanced manual socket management
|
||||||
|
- [Main SKILL.md](../SKILL.md) - Complete tmux skill documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version:** Documented for tmux skill v1.3.0+ (includes multiline support)
|
||||||
1484
references/session-registry.md
Normal file
1484
references/session-registry.md
Normal file
File diff suppressed because it is too large
Load Diff
263
tools/cleanup-sessions.sh
Executable file
263
tools/cleanup-sessions.sh
Executable file
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# cleanup-sessions.sh - Clean up dead or old tmux sessions
|
||||||
|
#
|
||||||
|
# Removes dead sessions from the registry or optionally cleans up
|
||||||
|
# all sessions or sessions older than a specified threshold.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./cleanup-sessions.sh [options]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --dry-run Show what would be cleaned without doing it
|
||||||
|
# --all Remove all sessions (even alive ones)
|
||||||
|
# --older-than Remove sessions older than duration (e.g., "1h", "2d")
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - Success
|
||||||
|
# 1 - Invalid arguments
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get script directory to source registry library
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/registry.sh
|
||||||
|
source "$SCRIPT_DIR/lib/registry.sh"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
dry_run=false
|
||||||
|
clean_all=false
|
||||||
|
older_than=""
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Functions
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat << EOF
|
||||||
|
Usage: $(basename "$0") [options]
|
||||||
|
|
||||||
|
Clean up dead or old tmux sessions from the registry.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dry-run Show what would be cleaned without actually removing
|
||||||
|
--all Remove all sessions (even alive ones)
|
||||||
|
--older-than DUR Remove sessions older than duration
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Duration Format:
|
||||||
|
Supported units: s (seconds), m (minutes), h (hours), d (days)
|
||||||
|
Examples: 30m, 2h, 1d, 3600s
|
||||||
|
|
||||||
|
Cleanup Modes:
|
||||||
|
Default: Remove only dead/missing/zombie sessions
|
||||||
|
--all: Remove all sessions regardless of health
|
||||||
|
--older-than: Remove sessions older than specified duration
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Show what dead sessions would be removed (dry-run)
|
||||||
|
$(basename "$0") --dry-run
|
||||||
|
|
||||||
|
# Remove all dead sessions
|
||||||
|
$(basename "$0")
|
||||||
|
|
||||||
|
# Remove sessions inactive for more than 1 hour
|
||||||
|
$(basename "$0") --older-than 1h
|
||||||
|
|
||||||
|
# Remove all sessions (even alive ones)
|
||||||
|
$(basename "$0") --all
|
||||||
|
|
||||||
|
# Dry-run: show sessions older than 2 days
|
||||||
|
$(basename "$0") --dry-run --older-than 2d
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 - Success
|
||||||
|
1 - Invalid arguments
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse duration string to seconds
|
||||||
|
# Args: duration string (e.g., "1h", "30m", "2d")
|
||||||
|
# Returns: seconds as integer
|
||||||
|
parse_duration() {
|
||||||
|
local dur="$1"
|
||||||
|
|
||||||
|
if [[ "$dur" =~ ^([0-9]+)([smhd])$ ]]; then
|
||||||
|
local value="${BASH_REMATCH[1]}"
|
||||||
|
local unit="${BASH_REMATCH[2]}"
|
||||||
|
|
||||||
|
case "$unit" in
|
||||||
|
s) echo "$value" ;;
|
||||||
|
m) echo "$((value * 60))" ;;
|
||||||
|
h) echo "$((value * 3600))" ;;
|
||||||
|
d) echo "$((value * 86400))" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "Error: Invalid duration format: $dur" >&2
|
||||||
|
echo "Use format like: 30m, 2h, 1d" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if session is older than threshold
|
||||||
|
# Args: created_at timestamp, threshold in seconds
|
||||||
|
# Returns: 0 if older, 1 if newer
|
||||||
|
is_older_than() {
|
||||||
|
local created="$1"
|
||||||
|
local threshold_secs="$2"
|
||||||
|
|
||||||
|
# Convert ISO8601 to epoch (cross-platform)
|
||||||
|
local created_epoch
|
||||||
|
created_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$created" "+%s" 2>/dev/null || \
|
||||||
|
date -d "$created" "+%s" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [[ "$created_epoch" == "0" ]]; then
|
||||||
|
# Can't parse date, assume it's old
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local now_epoch
|
||||||
|
now_epoch=$(date "+%s")
|
||||||
|
local age=$((now_epoch - created_epoch))
|
||||||
|
|
||||||
|
[[ $age -gt $threshold_secs ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
dry_run=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--all)
|
||||||
|
clean_all=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--older-than)
|
||||||
|
older_than="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown option: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Validate arguments
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
threshold_secs=0
|
||||||
|
if [[ -n "$older_than" ]]; then
|
||||||
|
threshold_secs=$(parse_duration "$older_than") || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Get sessions from registry
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
registry_data=$(registry_list_sessions)
|
||||||
|
session_names=$(echo "$registry_data" | jq -r '.sessions | keys[]' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -z "$session_names" ]]; then
|
||||||
|
echo "No sessions in registry."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Find sessions to remove
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Path to pane-health tool
|
||||||
|
PANE_HEALTH="$SCRIPT_DIR/pane-health.sh"
|
||||||
|
|
||||||
|
sessions_to_remove=()
|
||||||
|
removed_count=0
|
||||||
|
|
||||||
|
while IFS= read -r name; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
|
||||||
|
# Get session data
|
||||||
|
socket=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].socket")
|
||||||
|
target=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].target")
|
||||||
|
created=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].created_at")
|
||||||
|
|
||||||
|
# Determine if session should be removed
|
||||||
|
should_remove=false
|
||||||
|
reason=""
|
||||||
|
|
||||||
|
if [[ "$clean_all" == true ]]; then
|
||||||
|
should_remove=true
|
||||||
|
reason="all sessions mode"
|
||||||
|
else
|
||||||
|
# Check health status
|
||||||
|
if [[ -x "$PANE_HEALTH" ]]; then
|
||||||
|
if ! "$PANE_HEALTH" -S "$socket" -t "$target" --format text >/dev/null 2>&1; then
|
||||||
|
should_remove=true
|
||||||
|
reason="dead/missing/zombie"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check age if threshold specified
|
||||||
|
if [[ -n "$older_than" ]] && [[ "$threshold_secs" -gt 0 ]]; then
|
||||||
|
if is_older_than "$created" "$threshold_secs"; then
|
||||||
|
should_remove=true
|
||||||
|
if [[ -n "$reason" ]]; then
|
||||||
|
reason="$reason + older than $older_than"
|
||||||
|
else
|
||||||
|
reason="older than $older_than"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add to removal list if needed
|
||||||
|
if [[ "$should_remove" == true ]]; then
|
||||||
|
sessions_to_remove+=("$name|$reason")
|
||||||
|
fi
|
||||||
|
done <<< "$session_names"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Remove sessions
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ ${#sessions_to_remove[@]} -eq 0 ]]; then
|
||||||
|
echo "No sessions to clean up."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$dry_run" == true ]]; then
|
||||||
|
echo "Dry-run mode: Would remove ${#sessions_to_remove[@]} session(s):"
|
||||||
|
for session_info in "${sessions_to_remove[@]}"; do
|
||||||
|
IFS='|' read -r name reason <<< "$session_info"
|
||||||
|
echo " - $name ($reason)"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Removing ${#sessions_to_remove[@]} session(s):"
|
||||||
|
for session_info in "${sessions_to_remove[@]}"; do
|
||||||
|
IFS='|' read -r name reason <<< "$session_info"
|
||||||
|
echo " - $name ($reason)"
|
||||||
|
if registry_remove_session "$name"; then
|
||||||
|
removed_count=$((removed_count + 1))
|
||||||
|
else
|
||||||
|
echo " Warning: Failed to remove $name" >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Removed $removed_count session(s) successfully."
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
224
tools/create-session.sh
Executable file
224
tools/create-session.sh
Executable file
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# create-session.sh - Create and register tmux sessions
|
||||||
|
#
|
||||||
|
# Creates a new tmux session and optionally registers it in the session registry.
|
||||||
|
# Supports launching different types of sessions (Python REPL, gdb, shell).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./create-session.sh -n <name> [options]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -n, --name Session name (required)
|
||||||
|
# -S, --socket Custom socket path (optional, uses default)
|
||||||
|
# -w, --window Window name (default: "shell")
|
||||||
|
# --python Launch Python REPL with PYTHON_BASIC_REPL=1
|
||||||
|
# --gdb Launch gdb
|
||||||
|
# --shell Launch shell (default)
|
||||||
|
# --no-register Don't add to registry
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - Success
|
||||||
|
# 1 - Invalid arguments
|
||||||
|
# 2 - Session already exists
|
||||||
|
# 3 - Tmux command failed
|
||||||
|
# 4 - Registry operation failed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get script directory to source registry library
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/registry.sh
|
||||||
|
source "$SCRIPT_DIR/lib/registry.sh"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
session_name=""
|
||||||
|
socket=""
|
||||||
|
window_name="shell"
|
||||||
|
session_type="shell"
|
||||||
|
launch_command="bash"
|
||||||
|
register_session=true
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Functions
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat << EOF
|
||||||
|
Usage: $(basename "$0") -n <name> [options]
|
||||||
|
|
||||||
|
Create a new tmux session and optionally register it in the session registry.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-n, --name Session name (required)
|
||||||
|
-S, --socket Custom socket path (optional, uses default)
|
||||||
|
-w, --window Window name (default: "shell")
|
||||||
|
--python Launch Python REPL with PYTHON_BASIC_REPL=1
|
||||||
|
--gdb Launch gdb
|
||||||
|
--shell Launch shell (default)
|
||||||
|
--no-register Don't add to registry
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Session Types:
|
||||||
|
--shell Launches bash (default)
|
||||||
|
--python Launches Python REPL with PYTHON_BASIC_REPL=1
|
||||||
|
--gdb Launches gdb debugger
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Create Python REPL session (auto-registered)
|
||||||
|
$(basename "$0") -n my-python --python
|
||||||
|
|
||||||
|
# Create session with custom socket
|
||||||
|
$(basename "$0") -n my-session -S /tmp/custom.sock --shell
|
||||||
|
|
||||||
|
# Create session without registering
|
||||||
|
$(basename "$0") -n temp-session --no-register
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 - Success
|
||||||
|
1 - Invalid arguments
|
||||||
|
2 - Session already exists
|
||||||
|
3 - Tmux command failed
|
||||||
|
4 - Registry operation failed
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-n|--name)
|
||||||
|
session_name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-S|--socket)
|
||||||
|
socket="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-w|--window)
|
||||||
|
window_name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--python)
|
||||||
|
session_type="python-repl"
|
||||||
|
launch_command="PYTHON_BASIC_REPL=1 python3 -q"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--gdb)
|
||||||
|
session_type="debugger"
|
||||||
|
launch_command="gdb"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--shell)
|
||||||
|
session_type="shell"
|
||||||
|
launch_command="bash"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-register)
|
||||||
|
register_session=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown option: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Validation
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ -z "$session_name" ]]; then
|
||||||
|
echo "Error: Session name is required (use -n <name>)" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set default socket if not provided
|
||||||
|
if [[ -z "$socket" ]]; then
|
||||||
|
socket="$CLAUDE_TMUX_SOCKET_DIR/claude.sock"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure socket directory exists
|
||||||
|
socket_dir="$(dirname "$socket")"
|
||||||
|
mkdir -p "$socket_dir"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Check if session already exists
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Check in registry first
|
||||||
|
if [[ "$register_session" == true ]] && registry_session_exists "$session_name"; then
|
||||||
|
echo "Error: Session '$session_name' already exists in registry" >&2
|
||||||
|
echo "Use a different name or remove the existing session first" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if tmux session actually exists on this socket
|
||||||
|
if tmux -S "$socket" has-session -t "$session_name" 2>/dev/null; then
|
||||||
|
echo "Error: Tmux session '$session_name' already exists on socket $socket" >&2
|
||||||
|
echo "Use a different name or kill the existing session first" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Create tmux session
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if ! tmux -S "$socket" new-session -d -s "$session_name" -n "$window_name" "$launch_command" 2>/dev/null; then
|
||||||
|
echo "Error: Failed to create tmux session '$session_name'" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the session PID
|
||||||
|
session_pid=$(tmux -S "$socket" display-message -p -t "$session_name" '#{pane_pid}' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Build target (session:window.pane)
|
||||||
|
target="$session_name:0.0"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Register session
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "$register_session" == true ]]; then
|
||||||
|
if ! registry_add_session "$session_name" "$socket" "$target" "$session_type" "$session_pid"; then
|
||||||
|
echo "Warning: Session created but failed to register in registry" >&2
|
||||||
|
# Don't fail completely, session was created successfully
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Output session info as JSON
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
{
|
||||||
|
"name": "$session_name",
|
||||||
|
"socket": "$socket",
|
||||||
|
"target": "$target",
|
||||||
|
"type": "$session_type",
|
||||||
|
"pid": ${session_pid:-null},
|
||||||
|
"window": "$window_name",
|
||||||
|
"registered": $register_session
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
exit 0
|
||||||
262
tools/find-sessions.sh
Executable file
262
tools/find-sessions.sh
Executable file
@@ -0,0 +1,262 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# find-sessions.sh - Discover and list tmux sessions on sockets
|
||||||
|
#
|
||||||
|
# PURPOSE:
|
||||||
|
# Find and display information about tmux sessions, either on a specific
|
||||||
|
# socket or by scanning all sockets in a directory. Useful for discovering
|
||||||
|
# what agent sessions are currently running.
|
||||||
|
#
|
||||||
|
# HOW IT WORKS:
|
||||||
|
# 1. Identify target socket(s) based on command-line options
|
||||||
|
# 2. Query each socket for running tmux sessions
|
||||||
|
# 3. Display session info: name, attach status, creation time
|
||||||
|
# 4. Optionally filter by session name substring
|
||||||
|
#
|
||||||
|
# USE CASES:
|
||||||
|
# - List all agent sessions across multiple sockets
|
||||||
|
# - Find a specific session by name (partial matching)
|
||||||
|
# - Check if a session is attached or detached
|
||||||
|
# - See when sessions were created
|
||||||
|
# - Enumerate sessions before cleanup
|
||||||
|
#
|
||||||
|
# EXAMPLES:
|
||||||
|
# # List sessions on default tmux socket
|
||||||
|
# ./find-sessions.sh
|
||||||
|
#
|
||||||
|
# # List sessions on specific socket by name
|
||||||
|
# ./find-sessions.sh -L mysocket
|
||||||
|
#
|
||||||
|
# # List sessions on specific socket by path
|
||||||
|
# ./find-sessions.sh -S /tmp/claude-tmux-sockets/claude.sock
|
||||||
|
#
|
||||||
|
# # Scan all sockets in directory
|
||||||
|
# ./find-sessions.sh --all
|
||||||
|
#
|
||||||
|
# # Find sessions with "python" in the name
|
||||||
|
# ./find-sessions.sh --all -q python
|
||||||
|
#
|
||||||
|
# DEPENDENCIES:
|
||||||
|
# - bash (with arrays, [[, functions)
|
||||||
|
# - tmux (for list-sessions)
|
||||||
|
# - grep (for filtering by query)
|
||||||
|
#
|
||||||
|
|
||||||
|
# Bash strict mode:
|
||||||
|
# -e: Exit immediately if any command fails
|
||||||
|
# -u: Treat unset variables as errors
|
||||||
|
# -o pipefail: Pipe fails if any command in pipeline fails
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: find-sessions.sh [-L socket-name|-S socket-path|-A] [-q pattern]
|
||||||
|
|
||||||
|
List tmux sessions on a socket (default tmux socket if none provided).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-L, --socket tmux socket name (passed to tmux -L)
|
||||||
|
-S, --socket-path tmux socket path (passed to tmux -S)
|
||||||
|
-A, --all scan all sockets under CLAUDE_TMUX_SOCKET_DIR
|
||||||
|
-q, --query case-insensitive substring to filter session names
|
||||||
|
-h, --help show this help
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Default Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Socket specification (mutually exclusive)
|
||||||
|
socket_name="" # tmux socket name (for tmux -L)
|
||||||
|
socket_path="" # tmux socket path (for tmux -S)
|
||||||
|
|
||||||
|
# Filtering and scanning options
|
||||||
|
query="" # substring to filter session names (case-insensitive)
|
||||||
|
scan_all=false # whether to scan all sockets in socket_dir
|
||||||
|
|
||||||
|
# Directory containing agent tmux sockets
|
||||||
|
# Priority: CLAUDE_TMUX_SOCKET_DIR env var > TMPDIR/claude-tmux-sockets > /tmp/claude-tmux-sockets
|
||||||
|
socket_dir="${CLAUDE_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/claude-tmux-sockets}"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Parse Command-Line Arguments
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-L|--socket) socket_name="${2-}"; shift 2 ;; # Socket name mode
|
||||||
|
-S|--socket-path) socket_path="${2-}"; shift 2 ;; # Socket path mode
|
||||||
|
-A|--all) scan_all=true; shift ;; # Scan all mode
|
||||||
|
-q|--query) query="${2-}"; shift 2 ;; # Filter by name
|
||||||
|
-h|--help) usage; exit 0 ;; # Show help
|
||||||
|
*) echo "Unknown option: $1" >&2; usage; exit 1 ;; # Error on unknown
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Validate Options
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Cannot use --all with specific socket options (they're mutually exclusive)
|
||||||
|
if [[ "$scan_all" == true && ( -n "$socket_name" || -n "$socket_path" ) ]]; then
|
||||||
|
echo "Cannot combine --all with -L or -S" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cannot use both -L and -S at the same time (different socket types)
|
||||||
|
if [[ -n "$socket_name" && -n "$socket_path" ]]; then
|
||||||
|
echo "Use either -L or -S, not both" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that tmux is installed and available in PATH
|
||||||
|
if ! command -v tmux >/dev/null 2>&1; then
|
||||||
|
echo "tmux not found in PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Function: list_sessions
|
||||||
|
# ============================================================================
|
||||||
|
# Query a tmux socket for sessions and display formatted output
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# $1: Label describing the socket (for display purposes)
|
||||||
|
# $@: Remaining args are passed to tmux command (e.g., -L name or -S path)
|
||||||
|
#
|
||||||
|
# Returns:
|
||||||
|
# 0 if sessions found (or no sessions after filtering)
|
||||||
|
# 1 if tmux server not running on this socket
|
||||||
|
#
|
||||||
|
# Output format:
|
||||||
|
# Sessions on <label>:
|
||||||
|
# - session-name (attached|detached, started <timestamp>)
|
||||||
|
#
|
||||||
|
list_sessions() {
|
||||||
|
# Store label for display, then shift to get remaining args
|
||||||
|
local label="$1"; shift
|
||||||
|
# Build tmux command array with remaining args (socket options)
|
||||||
|
local tmux_cmd=(tmux "$@")
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Query tmux for session information
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# tmux list-sessions -F specifies output format:
|
||||||
|
# #{session_name}: Name of the session
|
||||||
|
# #{session_attached}: 1 if attached, 0 if detached
|
||||||
|
# #{session_created_string}: Human-readable creation timestamp
|
||||||
|
# Tab-separated output for easy parsing
|
||||||
|
# 2>/dev/null: Suppress errors if no server running
|
||||||
|
# if !: Check if command failed (no server = exit code 1)
|
||||||
|
#
|
||||||
|
if ! sessions="$("${tmux_cmd[@]}" list-sessions -F '#{session_name}\t#{session_attached}\t#{session_created_string}' 2>/dev/null)"; then
|
||||||
|
echo "No tmux server found on $label" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Filter sessions by query if provided
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# -i: Case-insensitive search
|
||||||
|
# --: End of options (allows query starting with -)
|
||||||
|
# || true: Don't fail if grep finds no matches (returns exit 1)
|
||||||
|
#
|
||||||
|
if [[ -n "$query" ]]; then
|
||||||
|
sessions="$(printf '%s\n' "$sessions" | grep -i -- "$query" || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Handle case where no sessions match query
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
if [[ -z "$sessions" ]]; then
|
||||||
|
echo "No sessions found on $label"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Format and display session information
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
echo "Sessions on $label:"
|
||||||
|
# Parse tab-separated values into name, attached, created
|
||||||
|
# IFS=$'\t': Set field separator to tab character
|
||||||
|
# read -r: Don't interpret backslashes (raw input)
|
||||||
|
printf '%s\n' "$sessions" | while IFS=$'\t' read -r name attached created; do
|
||||||
|
# Convert attached flag (1/0) to human-readable label
|
||||||
|
attached_label=$([[ "$attached" == "1" ]] && echo "attached" || echo "detached")
|
||||||
|
# Display formatted session info
|
||||||
|
printf ' - %s (%s, started %s)\n' "$name" "$attached_label" "$created"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Execution: Scan All Mode
|
||||||
|
# ============================================================================
|
||||||
|
# Scan all socket files in socket_dir and list sessions on each
|
||||||
|
|
||||||
|
if [[ "$scan_all" == true ]]; then
|
||||||
|
# Verify socket directory exists
|
||||||
|
if [[ ! -d "$socket_dir" ]]; then
|
||||||
|
echo "Socket directory not found: $socket_dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Enumerate all files in socket directory
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# shopt -s nullglob: If no matches, glob expands to empty array (not literal *)
|
||||||
|
# This prevents errors when directory is empty
|
||||||
|
shopt -s nullglob
|
||||||
|
sockets=("$socket_dir"/*)
|
||||||
|
shopt -u nullglob # Restore default behavior
|
||||||
|
|
||||||
|
# Check if any files were found
|
||||||
|
if [[ "${#sockets[@]}" -eq 0 ]]; then
|
||||||
|
echo "No sockets found under $socket_dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Iterate through all socket files and list sessions
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Track exit code: 0 = all succeeded, 1 = at least one failed
|
||||||
|
exit_code=0
|
||||||
|
for sock in "${sockets[@]}"; do
|
||||||
|
# -S test: Check if file is a socket (not a regular file or directory)
|
||||||
|
# Skip non-socket files (e.g., .DS_Store, temp files)
|
||||||
|
if [[ ! -S "$sock" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Call list_sessions for this socket
|
||||||
|
# || exit_code=$?: Capture failure exit code but continue loop
|
||||||
|
list_sessions "socket path '$sock'" -S "$sock" || exit_code=$?
|
||||||
|
done
|
||||||
|
# Exit with captured exit code (0 if all succeeded, 1 if any failed)
|
||||||
|
exit "$exit_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Execution: Single Socket Mode
|
||||||
|
# ============================================================================
|
||||||
|
# List sessions on a specific socket (or default socket)
|
||||||
|
|
||||||
|
# Start with base tmux command
|
||||||
|
tmux_cmd=(tmux)
|
||||||
|
socket_label="default socket"
|
||||||
|
|
||||||
|
# Add socket-specific options based on user input
|
||||||
|
if [[ -n "$socket_name" ]]; then
|
||||||
|
# -L mode: Named socket (e.g., tmux -L mysocket)
|
||||||
|
tmux_cmd+=(-L "$socket_name")
|
||||||
|
socket_label="socket name '$socket_name'"
|
||||||
|
elif [[ -n "$socket_path" ]]; then
|
||||||
|
# -S mode: Socket path (e.g., tmux -S /tmp/my.sock)
|
||||||
|
tmux_cmd+=(-S "$socket_path")
|
||||||
|
socket_label="socket path '$socket_path'"
|
||||||
|
fi
|
||||||
|
# If neither set, use default tmux socket (no additional flags)
|
||||||
|
|
||||||
|
# Call list_sessions with constructed command
|
||||||
|
# ${tmux_cmd[@]:1}: Array slice starting at index 1 (skips "tmux" itself)
|
||||||
|
# This passes only the flags (e.g., "-L mysocket" or "-S /path")
|
||||||
|
list_sessions "$socket_label" "${tmux_cmd[@]:1}"
|
||||||
308
tools/kill-session.sh
Executable file
308
tools/kill-session.sh
Executable file
@@ -0,0 +1,308 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# kill-session.sh - Kill tmux session and remove from registry
|
||||||
|
#
|
||||||
|
# PURPOSE:
|
||||||
|
# Atomically kill a tmux session and remove it from the session registry.
|
||||||
|
# Provides a single operation to fully clean up a session.
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# ./kill-session.sh [options]
|
||||||
|
#
|
||||||
|
# OPTIONS:
|
||||||
|
# -s, --session NAME Session name (uses registry lookup)
|
||||||
|
# -S, --socket PATH Socket path (explicit mode, requires -t)
|
||||||
|
# -t, --target TARGET Target pane (explicit mode, requires -S)
|
||||||
|
# --dry-run Show what would be done without executing
|
||||||
|
# -v, --verbose Verbose output
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
# EXIT CODES:
|
||||||
|
# 0 - Complete success (tmux session killed AND deregistered)
|
||||||
|
# 1 - Partial success (one operation succeeded, one failed)
|
||||||
|
# 2 - Complete failure (both operations failed or session not found)
|
||||||
|
# 3 - Invalid arguments
|
||||||
|
#
|
||||||
|
# EXAMPLES:
|
||||||
|
# # Kill session by name (registry lookup)
|
||||||
|
# ./kill-session.sh -s claude-python
|
||||||
|
#
|
||||||
|
# # Kill with explicit socket/target
|
||||||
|
# ./kill-session.sh -S /tmp/claude.sock -t my-session:0.0
|
||||||
|
#
|
||||||
|
# # Dry-run to see what would happen
|
||||||
|
# ./kill-session.sh -s claude-python --dry-run
|
||||||
|
#
|
||||||
|
# # Auto-detect single session
|
||||||
|
# ./kill-session.sh
|
||||||
|
#
|
||||||
|
# DEPENDENCIES:
|
||||||
|
# - bash, tmux, jq
|
||||||
|
# - lib/registry.sh (for session registry operations)
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get script directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Source registry library
|
||||||
|
# shellcheck source=lib/registry.sh
|
||||||
|
source "$SCRIPT_DIR/lib/registry.sh"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
session_name=""
|
||||||
|
socket=""
|
||||||
|
target=""
|
||||||
|
dry_run=false
|
||||||
|
verbose=false
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Functions
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: kill-session.sh [options]
|
||||||
|
|
||||||
|
Kill tmux session and remove from registry (atomic operation).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-s, --session NAME Session name (uses registry lookup)
|
||||||
|
-S, --socket PATH Socket path (explicit mode, requires -t)
|
||||||
|
-t, --target TARGET Target pane (explicit mode, requires -S)
|
||||||
|
--dry-run Show what would be done without executing
|
||||||
|
-v, --verbose Verbose output
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 - Complete success (killed AND deregistered)
|
||||||
|
1 - Partial success (one operation succeeded)
|
||||||
|
2 - Complete failure (both failed or not found)
|
||||||
|
3 - Invalid arguments
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Kill session by name
|
||||||
|
kill-session.sh -s claude-python
|
||||||
|
|
||||||
|
# Kill with explicit socket/target
|
||||||
|
kill-session.sh -S /tmp/claude.sock -t session:0.0
|
||||||
|
|
||||||
|
# Dry-run
|
||||||
|
kill-session.sh -s claude-python --dry-run
|
||||||
|
|
||||||
|
# Auto-detect (if only one session exists)
|
||||||
|
kill-session.sh
|
||||||
|
|
||||||
|
Priority order (if multiple methods specified):
|
||||||
|
1. Explicit -S and -t (highest priority)
|
||||||
|
2. Session name -s (registry lookup)
|
||||||
|
3. Auto-detect (if no flags and only one session exists)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log_verbose() {
|
||||||
|
if [[ "$verbose" == true ]]; then
|
||||||
|
echo "$@" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto-detect session if only one exists in registry
|
||||||
|
# Sets session_name global variable
|
||||||
|
# Returns: 0 if detected, 1 if cannot auto-detect
|
||||||
|
auto_detect_session() {
|
||||||
|
local registry_data session_count session_names
|
||||||
|
|
||||||
|
registry_data=$(registry_list_sessions)
|
||||||
|
session_names=$(echo "$registry_data" | jq -r '.sessions | keys[]' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -z "$session_names" ]]; then
|
||||||
|
echo "Error: No sessions in registry" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
session_count=$(echo "$session_names" | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
if [[ "$session_count" -eq 1 ]]; then
|
||||||
|
session_name="$session_names"
|
||||||
|
log_verbose "Auto-detected session: $session_name"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Error: Multiple sessions found, specify -s session-name:" >&2
|
||||||
|
# shellcheck disable=SC2001 # sed needed for adding prefix to multiple lines
|
||||||
|
echo "$session_names" | sed 's/^/ - /' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-s|--session)
|
||||||
|
session_name="${2:-}"
|
||||||
|
if [[ -z "$session_name" ]]; then
|
||||||
|
echo "Error: -s requires a session name" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-S|--socket)
|
||||||
|
socket="${2:-}"
|
||||||
|
if [[ -z "$socket" ]]; then
|
||||||
|
echo "Error: -S requires a socket path" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-t|--target)
|
||||||
|
target="${2:-}"
|
||||||
|
if [[ -z "$target" ]]; then
|
||||||
|
echo "Error: -t requires a target pane" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
dry_run=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-v|--verbose)
|
||||||
|
verbose=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown option: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 3
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Validate arguments
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Priority 1: Explicit socket/target mode
|
||||||
|
if [[ -n "$socket" || -n "$target" ]]; then
|
||||||
|
if [[ -z "$socket" || -z "$target" ]]; then
|
||||||
|
echo "Error: Both -S and -t must be specified together" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
log_verbose "Using explicit mode: socket=$socket, target=$target"
|
||||||
|
|
||||||
|
# Priority 2: Session name mode (registry lookup)
|
||||||
|
elif [[ -n "$session_name" ]]; then
|
||||||
|
log_verbose "Using registry mode: session=$session_name"
|
||||||
|
|
||||||
|
# Look up socket and target from registry
|
||||||
|
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
|
||||||
|
echo "Error: Session '$session_name' not found in registry" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
socket=$(echo "$session_data" | jq -r '.socket')
|
||||||
|
target=$(echo "$session_data" | jq -r '.target')
|
||||||
|
|
||||||
|
if [[ -z "$socket" || -z "$target" ]]; then
|
||||||
|
echo "Error: Invalid session data in registry for '$session_name'" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_verbose "Resolved from registry: socket=$socket, target=$target"
|
||||||
|
|
||||||
|
# Priority 3: Auto-detect mode
|
||||||
|
else
|
||||||
|
log_verbose "Attempting auto-detect..."
|
||||||
|
if ! auto_detect_session; then
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Look up socket and target
|
||||||
|
session_data=$(registry_get_session "$session_name")
|
||||||
|
socket=$(echo "$session_data" | jq -r '.socket')
|
||||||
|
target=$(echo "$session_data" | jq -r '.target')
|
||||||
|
|
||||||
|
log_verbose "Auto-detected: socket=$socket, target=$target"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract session name from target if not already set
|
||||||
|
if [[ -z "$session_name" ]]; then
|
||||||
|
# Target format is typically "session-name:0.0"
|
||||||
|
session_name="${target%%:*}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Execute kill operations
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "$dry_run" == true ]]; then
|
||||||
|
echo "Dry-run mode: Would perform the following operations:"
|
||||||
|
echo " 1. Kill tmux session: tmux -S \"$socket\" kill-session -t \"$session_name\""
|
||||||
|
if registry_session_exists "$session_name"; then
|
||||||
|
echo " 2. Remove from registry: $session_name"
|
||||||
|
else
|
||||||
|
echo " 2. Session not in registry, skip deregistration"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Actual execution
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
tmux_killed=false
|
||||||
|
registry_removed=false
|
||||||
|
|
||||||
|
# Step 1: Kill tmux session
|
||||||
|
echo "Killing tmux session: $session_name"
|
||||||
|
if tmux -S "$socket" kill-session -t "$session_name" 2>/dev/null; then
|
||||||
|
echo " ✓ Tmux session killed successfully"
|
||||||
|
tmux_killed=true
|
||||||
|
log_verbose "Tmux kill-session succeeded"
|
||||||
|
else
|
||||||
|
exit_code=$?
|
||||||
|
echo " ✗ Failed to kill tmux session (exit code: $exit_code)" >&2
|
||||||
|
log_verbose "Tmux kill-session failed, session may not exist"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: Remove from registry
|
||||||
|
if registry_session_exists "$session_name"; then
|
||||||
|
echo "Removing from registry: $session_name"
|
||||||
|
if registry_remove_session "$session_name"; then
|
||||||
|
echo " ✓ Removed from registry successfully"
|
||||||
|
registry_removed=true
|
||||||
|
log_verbose "Registry removal succeeded"
|
||||||
|
else
|
||||||
|
echo " ✗ Failed to remove from registry" >&2
|
||||||
|
log_verbose "Registry removal failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_verbose "Session not in registry, skipping deregistration"
|
||||||
|
# Not in registry is OK if we killed the tmux session
|
||||||
|
registry_removed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Determine final exit code
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "$tmux_killed" == true && "$registry_removed" == true ]]; then
|
||||||
|
echo "Session '$session_name' fully removed"
|
||||||
|
exit 0
|
||||||
|
elif [[ "$tmux_killed" == true || "$registry_removed" == true ]]; then
|
||||||
|
echo "Warning: Partial removal of session '$session_name'" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Error: Failed to remove session '$session_name'" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
255
tools/list-sessions.sh
Executable file
255
tools/list-sessions.sh
Executable file
@@ -0,0 +1,255 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# list-sessions.sh - List all registered tmux sessions
|
||||||
|
#
|
||||||
|
# Lists all sessions in the registry with health status information.
|
||||||
|
# Supports both human-readable table format and JSON output.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./list-sessions.sh [--json]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --json Output as JSON instead of table
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - Success
|
||||||
|
# 1 - Error
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get script directory to source libraries
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/registry.sh
|
||||||
|
source "$SCRIPT_DIR/lib/registry.sh"
|
||||||
|
# shellcheck source=lib/time_utils.sh
|
||||||
|
source "$SCRIPT_DIR/lib/time_utils.sh"
|
||||||
|
|
||||||
|
# Path to pane-health tool
|
||||||
|
PANE_HEALTH="$SCRIPT_DIR/pane-health.sh"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
output_format="table"
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Functions
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat << EOF
|
||||||
|
Usage: $(basename "$0") [--json]
|
||||||
|
|
||||||
|
List all registered tmux sessions with health status.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--json Output as JSON instead of table format
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Output Formats:
|
||||||
|
Table (default):
|
||||||
|
NAME SOCKET TARGET STATUS PID CREATED
|
||||||
|
my-python claude.sock my-python:0.0 alive 1234 2h ago
|
||||||
|
my-gdb claude.sock my-gdb:0.0 dead - 1h ago
|
||||||
|
|
||||||
|
JSON (--json):
|
||||||
|
{
|
||||||
|
"sessions": [
|
||||||
|
{"name": "my-python", "socket": "...", "status": "alive", ...}
|
||||||
|
],
|
||||||
|
"total": 2,
|
||||||
|
"alive": 1,
|
||||||
|
"dead": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Health Status:
|
||||||
|
alive - Session is running and healthy
|
||||||
|
dead - Pane is marked as dead
|
||||||
|
missing - Session/pane not found
|
||||||
|
zombie - Process not running
|
||||||
|
server - Tmux server not running
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# List sessions in table format
|
||||||
|
$(basename "$0")
|
||||||
|
|
||||||
|
# List sessions as JSON
|
||||||
|
$(basename "$0") --json
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 - Success
|
||||||
|
1 - Error
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get health status for a session
|
||||||
|
# Returns: status string (alive, dead, missing, zombie, server)
|
||||||
|
get_health_status() {
|
||||||
|
local socket="$1"
|
||||||
|
local target="$2"
|
||||||
|
|
||||||
|
if [[ ! -x "$PANE_HEALTH" ]]; then
|
||||||
|
echo "unknown"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call pane-health.sh and interpret exit code
|
||||||
|
if "$PANE_HEALTH" -S "$socket" -t "$target" --format text >/dev/null 2>&1; then
|
||||||
|
echo "alive"
|
||||||
|
else
|
||||||
|
local exit_code=$?
|
||||||
|
case $exit_code in
|
||||||
|
1) echo "dead" ;;
|
||||||
|
2) echo "missing" ;;
|
||||||
|
3) echo "zombie" ;;
|
||||||
|
4) echo "server" ;;
|
||||||
|
*) echo "unknown" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--json)
|
||||||
|
output_format="json"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown option: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Get sessions from registry
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
registry_data=$(registry_list_sessions)
|
||||||
|
session_names=$(echo "$registry_data" | jq -r '.sessions | keys[]' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Count sessions
|
||||||
|
total_count=0
|
||||||
|
alive_count=0
|
||||||
|
dead_count=0
|
||||||
|
|
||||||
|
# Build session list with health info
|
||||||
|
sessions_with_health=()
|
||||||
|
|
||||||
|
if [[ -n "$session_names" ]]; then
|
||||||
|
while IFS= read -r name; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
|
||||||
|
# Get session data
|
||||||
|
socket=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].socket")
|
||||||
|
target=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].target")
|
||||||
|
type=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].type")
|
||||||
|
pid=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].pid // \"\"")
|
||||||
|
created=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].created_at")
|
||||||
|
|
||||||
|
# Get health status
|
||||||
|
status=$(get_health_status "$socket" "$target")
|
||||||
|
|
||||||
|
# Update counters
|
||||||
|
total_count=$((total_count + 1))
|
||||||
|
if [[ "$status" == "alive" ]]; then
|
||||||
|
alive_count=$((alive_count + 1))
|
||||||
|
else
|
||||||
|
dead_count=$((dead_count + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Store session info
|
||||||
|
sessions_with_health+=("$name|$socket|$target|$status|$pid|$created|$type")
|
||||||
|
done <<< "$session_names"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
# Output results
|
||||||
|
#------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ "$output_format" == "json" ]]; then
|
||||||
|
# JSON output
|
||||||
|
echo "{"
|
||||||
|
echo " \"sessions\": ["
|
||||||
|
|
||||||
|
first=true
|
||||||
|
for session_info in "${sessions_with_health[@]+"${sessions_with_health[@]}"}"; do
|
||||||
|
IFS='|' read -r name socket target status pid created type <<< "$session_info"
|
||||||
|
|
||||||
|
if [[ "$first" == false ]]; then
|
||||||
|
echo ","
|
||||||
|
fi
|
||||||
|
first=false
|
||||||
|
|
||||||
|
# Get basename of socket for cleaner output
|
||||||
|
socket_basename=$(basename "$socket")
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
{
|
||||||
|
"name": "$name",
|
||||||
|
"socket": "$socket",
|
||||||
|
"socket_basename": "$socket_basename",
|
||||||
|
"target": "$target",
|
||||||
|
"type": "$type",
|
||||||
|
"status": "$status",
|
||||||
|
"pid": ${pid:-null},
|
||||||
|
"created_at": "$created"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ],"
|
||||||
|
echo " \"total\": $total_count,"
|
||||||
|
echo " \"alive\": $alive_count,"
|
||||||
|
echo " \"dead\": $dead_count"
|
||||||
|
echo "}"
|
||||||
|
else
|
||||||
|
# Table output
|
||||||
|
if [[ $total_count -eq 0 ]]; then
|
||||||
|
echo "No sessions registered."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print header
|
||||||
|
printf "%-20s %-20s %-20s %-10s %-8s %-15s\n" \
|
||||||
|
"NAME" "SOCKET" "TARGET" "STATUS" "PID" "CREATED"
|
||||||
|
printf "%-20s %-20s %-20s %-10s %-8s %-15s\n" \
|
||||||
|
"----" "------" "------" "------" "---" "-------"
|
||||||
|
|
||||||
|
# Print sessions
|
||||||
|
for session_info in "${sessions_with_health[@]+"${sessions_with_health[@]}"}"; do
|
||||||
|
IFS='|' read -r name socket target status pid created type <<< "$session_info"
|
||||||
|
|
||||||
|
# Get basename of socket for cleaner output
|
||||||
|
socket_basename=$(basename "$socket")
|
||||||
|
|
||||||
|
# Format time ago
|
||||||
|
time_str=$(time_ago "$created")
|
||||||
|
|
||||||
|
# Truncate long values
|
||||||
|
name_trunc="${name:0:20}"
|
||||||
|
socket_trunc="${socket_basename:0:20}"
|
||||||
|
target_trunc="${target:0:20}"
|
||||||
|
|
||||||
|
printf "%-20s %-20s %-20s %-10s %-8s %-15s\n" \
|
||||||
|
"$name_trunc" "$socket_trunc" "$target_trunc" "$status" "${pid:--}" "$time_str"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
echo ""
|
||||||
|
echo "Total: $total_count | Alive: $alive_count | Dead: $dead_count"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
424
tools/pane-health.sh
Executable file
424
tools/pane-health.sh
Executable file
@@ -0,0 +1,424 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# pane-health.sh - Check health status of a tmux pane
|
||||||
|
#
|
||||||
|
# PURPOSE:
|
||||||
|
# Verify pane and session state before operations to prevent "pane not found"
|
||||||
|
# errors and detect failures early. Essential for reliable tmux automation.
|
||||||
|
#
|
||||||
|
# HOW IT WORKS:
|
||||||
|
# 1. Check if tmux server is running on the specified socket
|
||||||
|
# 2. Verify session exists using tmux has-session
|
||||||
|
# 3. Check if pane exists and get its state (dead/alive, PID)
|
||||||
|
# 4. Validate process is running via ps command
|
||||||
|
# 5. Determine overall health status and return structured output
|
||||||
|
#
|
||||||
|
# USE CASES:
|
||||||
|
# - Before sending commands: verify pane is ready
|
||||||
|
# - After errors: determine if pane crashed
|
||||||
|
# - Periodic health checks during long operations
|
||||||
|
# - Cleanup decision: which panes to kill vs keep
|
||||||
|
# - Integration with other tools (safe-send.sh, etc.)
|
||||||
|
#
|
||||||
|
# EXAMPLES:
|
||||||
|
# # Check pane health in JSON format
|
||||||
|
# ./pane-health.sh -S /tmp/my.sock -t session:0.0
|
||||||
|
#
|
||||||
|
# # Check pane health in text format
|
||||||
|
# ./pane-health.sh -t myapp:0.0 --format text
|
||||||
|
#
|
||||||
|
# # Use in conditional logic
|
||||||
|
# if ./pane-health.sh -t session:0.0 --format text; then
|
||||||
|
# echo "Pane is healthy"
|
||||||
|
# else
|
||||||
|
# echo "Pane has issues (exit code: $?)"
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# EXIT CODES:
|
||||||
|
# 0 - Healthy (pane alive, process running)
|
||||||
|
# 1 - Dead (pane marked as dead)
|
||||||
|
# 2 - Missing (pane/session doesn't exist)
|
||||||
|
# 3 - Zombie (process exited but pane still exists)
|
||||||
|
# 4 - Server not running
|
||||||
|
#
|
||||||
|
# DEPENDENCIES:
|
||||||
|
# - bash (with [[, printf, functions)
|
||||||
|
# - tmux (for has-session, list-panes)
|
||||||
|
# - ps (for process state validation)
|
||||||
|
#
|
||||||
|
|
||||||
|
# Bash strict mode:
|
||||||
|
# -e: Exit immediately if any command fails
|
||||||
|
# -u: Treat unset variables as errors
|
||||||
|
# -o pipefail: Pipe fails if any command in pipeline fails
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get script directory to source registry library
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/registry.sh
|
||||||
|
source "$SCRIPT_DIR/lib/registry.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: pane-health.sh -t target [options]
|
||||||
|
OR: pane-health.sh -s session [options]
|
||||||
|
OR: pane-health.sh [options] # auto-detect single session
|
||||||
|
|
||||||
|
Check health status of a tmux pane and report structured results.
|
||||||
|
|
||||||
|
Target Selection (priority order):
|
||||||
|
-s, --session session name (looks up socket/target in registry)
|
||||||
|
-t, --target tmux target (session:window.pane), explicit
|
||||||
|
(no flags) auto-detect if only one session in registry
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-S, --socket tmux socket path (for custom sockets via -S)
|
||||||
|
--format output format: json|text (default: json)
|
||||||
|
-h, --help show this help
|
||||||
|
|
||||||
|
Exit Codes:
|
||||||
|
0 - Healthy (pane alive, process running)
|
||||||
|
1 - Dead (pane marked as dead)
|
||||||
|
2 - Missing (pane/session doesn't exist)
|
||||||
|
3 - Zombie (process exited but pane still exists)
|
||||||
|
4 - Server not running
|
||||||
|
|
||||||
|
Output Formats:
|
||||||
|
json - Structured JSON with all health information
|
||||||
|
text - Human-readable status message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Using session name
|
||||||
|
./pane-health.sh -s my-python
|
||||||
|
|
||||||
|
# Auto-detect single session
|
||||||
|
./pane-health.sh --format text
|
||||||
|
|
||||||
|
# Explicit socket/target (backward compatible)
|
||||||
|
./pane-health.sh -t session:0.0 -S /tmp/claude.sock
|
||||||
|
|
||||||
|
# Use in script with session registry
|
||||||
|
if ./pane-health.sh -s my-session; then
|
||||||
|
echo "Pane is ready for commands"
|
||||||
|
fi
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Default Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Required parameters (must be provided by user)
|
||||||
|
target="" # tmux target pane (format: session:window.pane)
|
||||||
|
|
||||||
|
# Optional parameters
|
||||||
|
session_name="" # session name for registry lookup
|
||||||
|
socket="" # tmux socket path (empty = use default tmux socket)
|
||||||
|
output_format="json" # output format: json or text
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Parse Command-Line Arguments
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-s|--session) session_name="${2-}"; shift 2 ;; # Set session name for registry lookup
|
||||||
|
-t|--target) target="${2-}"; shift 2 ;; # Set target pane
|
||||||
|
-S|--socket) socket="${2-}"; shift 2 ;; # Set custom socket path
|
||||||
|
--format) output_format="${2-}"; shift 2 ;; # Set output format
|
||||||
|
-h|--help) usage; exit 0 ;; # Show help and exit
|
||||||
|
*) echo "Unknown option: $1" >&2; usage; exit 1 ;; # Error on unknown option
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Session Resolution
|
||||||
|
# ============================================================================
|
||||||
|
# Resolve session name to socket/target if provided
|
||||||
|
# Priority: 1) Explicit -S/-t, 2) Session name -s, 3) Auto-detect single session
|
||||||
|
|
||||||
|
if [[ -n "$socket" && -n "$target" ]]; then
|
||||||
|
# Priority 1: Explicit socket and target provided (backward compatible)
|
||||||
|
: # Use as-is, no resolution needed
|
||||||
|
elif [[ -n "$session_name" ]]; then
|
||||||
|
# Priority 2: Session name provided, look up in registry
|
||||||
|
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
|
||||||
|
echo "Error: Session '$session_name' not found in registry" >&2
|
||||||
|
echo "Use 'list-sessions.sh' to see available sessions" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract socket and target from session data
|
||||||
|
socket=$(echo "$session_data" | jq -r '.socket')
|
||||||
|
target=$(echo "$session_data" | jq -r '.target')
|
||||||
|
|
||||||
|
# Update activity timestamp
|
||||||
|
registry_update_activity "$session_name" 2>/dev/null || true
|
||||||
|
elif [[ -z "$socket" && -z "$target" ]]; then
|
||||||
|
# Priority 3: No explicit params, try auto-detect single session
|
||||||
|
session_count=$(registry_list_sessions 2>/dev/null | jq '.sessions | length' 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [[ "$session_count" == "1" ]]; then
|
||||||
|
# Single session exists, auto-use it
|
||||||
|
auto_session_name=$(registry_list_sessions | jq -r '.sessions | keys[0]')
|
||||||
|
session_data=$(registry_get_session "$auto_session_name")
|
||||||
|
socket=$(echo "$session_data" | jq -r '.socket')
|
||||||
|
target=$(echo "$session_data" | jq -r '.target')
|
||||||
|
|
||||||
|
# Update activity timestamp
|
||||||
|
registry_update_activity "$auto_session_name" 2>/dev/null || true
|
||||||
|
elif [[ "$session_count" == "0" ]]; then
|
||||||
|
echo "Error: No sessions found in registry" >&2
|
||||||
|
echo "Create a session with 'create-session.sh' or specify -t and -S explicitly" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Error: Multiple sessions found ($session_count total)" >&2
|
||||||
|
echo "Please specify session name with -s or use -t/-S explicitly" >&2
|
||||||
|
echo "Use 'list-sessions.sh' to see available sessions" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Validate Required Parameters and Dependencies
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check that required parameters were provided (after resolution)
|
||||||
|
if [[ -z "$target" ]]; then
|
||||||
|
echo "target is required" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate output format
|
||||||
|
if [[ "$output_format" != "json" && "$output_format" != "text" ]]; then
|
||||||
|
echo "format must be 'json' or 'text'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that tmux is installed and available in PATH
|
||||||
|
if ! command -v tmux >/dev/null 2>&1; then
|
||||||
|
echo "tmux not found in PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that ps is installed and available in PATH
|
||||||
|
if ! command -v ps >/dev/null 2>&1; then
|
||||||
|
echo "ps not found in PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Build tmux Command Array
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Build base tmux command with optional socket parameter
|
||||||
|
# ${socket:+-S "$socket"} expands to "-S $socket" if socket is set, empty otherwise
|
||||||
|
tmux_cmd=(tmux ${socket:+-S "$socket"})
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Initialize Health State Variables
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Health check results (will be populated during checks)
|
||||||
|
server_running=false # Is tmux server running on the socket?
|
||||||
|
session_exists=false # Does the target session exist?
|
||||||
|
pane_exists=false # Does the target pane exist?
|
||||||
|
pane_dead=false # Is the pane marked as dead by tmux?
|
||||||
|
pid="" # Process ID running in the pane
|
||||||
|
process_running=false # Is the process actually running?
|
||||||
|
status="unknown" # Overall health status
|
||||||
|
exit_code=0 # Script exit code
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Function: output_result
|
||||||
|
# ============================================================================
|
||||||
|
# Output health check results in requested format (JSON or text)
|
||||||
|
#
|
||||||
|
# Arguments: None (uses global variables)
|
||||||
|
# Returns: None (outputs to stdout)
|
||||||
|
#
|
||||||
|
output_result() {
|
||||||
|
if [[ "$output_format" == "json" ]]; then
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# JSON Output Format
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Structured output with all health information
|
||||||
|
# Boolean values: "true" or "false" (JSON strings)
|
||||||
|
# PID: numeric value or null if not available
|
||||||
|
#
|
||||||
|
cat <<JSON
|
||||||
|
{
|
||||||
|
"status": "$status",
|
||||||
|
"server_running": $([[ "$server_running" == true ]] && echo "true" || echo "false"),
|
||||||
|
"session_exists": $([[ "$session_exists" == true ]] && echo "true" || echo "false"),
|
||||||
|
"pane_exists": $([[ "$pane_exists" == true ]] && echo "true" || echo "false"),
|
||||||
|
"pane_dead": $([[ "$pane_dead" == true ]] && echo "true" || echo "false"),
|
||||||
|
"pid": $([[ -n "$pid" ]] && echo "$pid" || echo "null"),
|
||||||
|
"process_running": $([[ "$process_running" == true ]] && echo "true" || echo "false")
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
else
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Text Output Format
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Human-readable status message
|
||||||
|
#
|
||||||
|
case "$status" in
|
||||||
|
healthy)
|
||||||
|
echo "Pane $target is healthy (PID: $pid, process running)"
|
||||||
|
;;
|
||||||
|
dead)
|
||||||
|
echo "Pane $target is dead (marked as dead by tmux)"
|
||||||
|
;;
|
||||||
|
zombie)
|
||||||
|
echo "Pane $target is a zombie (pane exists but process $pid exited)"
|
||||||
|
;;
|
||||||
|
missing)
|
||||||
|
if [[ "$session_exists" == false ]]; then
|
||||||
|
echo "Session does not exist (target: $target)"
|
||||||
|
else
|
||||||
|
echo "Pane does not exist (target: $target)"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
server_not_running)
|
||||||
|
echo "tmux server is not running on socket${socket:+: $socket}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown status: $status"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Health Check: Step 1 - Check if tmux server is running
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Try to list sessions to verify server is running
|
||||||
|
# Redirect all output to /dev/null (we only care about exit code)
|
||||||
|
# Exit code 0 = server running, non-zero = server not running
|
||||||
|
if "${tmux_cmd[@]}" list-sessions >/dev/null 2>&1; then
|
||||||
|
server_running=true
|
||||||
|
else
|
||||||
|
# Server is not running - this is the most fundamental failure
|
||||||
|
status="server_not_running"
|
||||||
|
exit_code=4
|
||||||
|
output_result
|
||||||
|
exit "$exit_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Health Check: Step 2 - Extract session name from target
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Target format: session:window.pane or session:window or just session
|
||||||
|
# Extract session name (everything before first colon, or entire string if no colon)
|
||||||
|
# ${target%%:*} means: remove longest match of ":*" from the end
|
||||||
|
session_name="${target%%:*}"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Health Check: Step 3 - Check if session exists
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Use tmux has-session to check if session exists
|
||||||
|
# -t: target session name
|
||||||
|
# Exit code 0 = session exists, non-zero = session doesn't exist
|
||||||
|
if "${tmux_cmd[@]}" has-session -t "$session_name" 2>/dev/null; then
|
||||||
|
session_exists=true
|
||||||
|
else
|
||||||
|
# Session doesn't exist - can't check pane without session
|
||||||
|
status="missing"
|
||||||
|
exit_code=2
|
||||||
|
output_result
|
||||||
|
exit "$exit_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Health Check: Step 4 - Check if pane exists and get pane state
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Query tmux for pane information
|
||||||
|
# list-panes -F: Format output with specific variables
|
||||||
|
# #{pane_dead}: 1 if pane is dead, 0 if alive
|
||||||
|
# #{pane_pid}: Process ID running in the pane
|
||||||
|
# -t: target (can be session, session:window, or session:window.pane)
|
||||||
|
# 2>/dev/null: Suppress errors if pane doesn't exist
|
||||||
|
#
|
||||||
|
# Note: If target is session:0.0 but pane doesn't exist, list-panes returns empty
|
||||||
|
# If target is just session, it lists all panes in session
|
||||||
|
#
|
||||||
|
if ! pane_info="$("${tmux_cmd[@]}" list-panes -F '#{pane_dead} #{pane_pid}' -t "$target" 2>/dev/null)"; then
|
||||||
|
# list-panes failed - pane doesn't exist
|
||||||
|
status="missing"
|
||||||
|
exit_code=2
|
||||||
|
output_result
|
||||||
|
exit "$exit_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we got any output (pane exists)
|
||||||
|
if [[ -z "$pane_info" ]]; then
|
||||||
|
# No output means pane doesn't exist
|
||||||
|
status="missing"
|
||||||
|
exit_code=2
|
||||||
|
output_result
|
||||||
|
exit "$exit_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pane exists - mark it and parse the info
|
||||||
|
pane_exists=true
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Parse pane information
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# pane_info format: "0 12345" or "1 12345"
|
||||||
|
# First field: pane_dead flag (0 = alive, 1 = dead)
|
||||||
|
# Second field: pane_pid
|
||||||
|
#
|
||||||
|
# Read into variables using read command
|
||||||
|
# IFS=' ': Use space as field separator
|
||||||
|
# read -r: Don't interpret backslashes
|
||||||
|
read -r pane_dead_flag pid_value <<< "$pane_info"
|
||||||
|
|
||||||
|
# Set pane_dead boolean based on flag
|
||||||
|
if [[ "$pane_dead_flag" == "1" ]]; then
|
||||||
|
pane_dead=true
|
||||||
|
status="dead"
|
||||||
|
exit_code=1
|
||||||
|
output_result
|
||||||
|
exit "$exit_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Store PID value
|
||||||
|
pid="$pid_value"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Health Check: Step 5 - Validate process is running
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Use ps to check if process with this PID is actually running
|
||||||
|
# -p: Specify process ID to check
|
||||||
|
# -o pid=: Output only PID (with no header)
|
||||||
|
# 2>/dev/null: Suppress errors if PID doesn't exist
|
||||||
|
# grep -q: Quiet mode, just check if pattern matches (exit code 0 = match)
|
||||||
|
#
|
||||||
|
if ps -p "$pid" -o pid= >/dev/null 2>&1; then
|
||||||
|
# Process is running - pane is healthy!
|
||||||
|
process_running=true
|
||||||
|
status="healthy"
|
||||||
|
exit_code=0
|
||||||
|
else
|
||||||
|
# Process is not running but pane still exists - this is a zombie
|
||||||
|
# This can happen when a process exits but tmux keeps the pane open
|
||||||
|
# (depending on remain-on-exit setting)
|
||||||
|
process_running=false
|
||||||
|
status="zombie"
|
||||||
|
exit_code=3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Output Results and Exit
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
output_result
|
||||||
|
exit "$exit_code"
|
||||||
503
tools/safe-send.sh
Executable file
503
tools/safe-send.sh
Executable file
@@ -0,0 +1,503 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# safe-send.sh - Send keystrokes to tmux pane with retries and readiness checking
|
||||||
|
#
|
||||||
|
# PURPOSE:
|
||||||
|
# Reliably send commands to tmux panes with automatic retries, readiness checks,
|
||||||
|
# and optional prompt waiting. Prevents dropped keystrokes that can occur when
|
||||||
|
# sending to busy or not-yet-ready panes.
|
||||||
|
#
|
||||||
|
# HOW IT WORKS:
|
||||||
|
# 1. Verify pane is healthy using pane-health.sh (if available)
|
||||||
|
# 2. Attempt to send keystrokes using tmux send-keys
|
||||||
|
# 3. On failure: retry with exponential backoff (0.5s, 1s, 2s, ...)
|
||||||
|
# 4. Optionally wait for prompt pattern after sending (using wait-for-text.sh)
|
||||||
|
# 5. Return success or failure with appropriate exit code
|
||||||
|
#
|
||||||
|
# USE CASES:
|
||||||
|
# - Send commands to Python REPL with automatic retry
|
||||||
|
# - Send gdb commands and wait for prompt
|
||||||
|
# - Critical commands that must not be dropped
|
||||||
|
# - Send commands immediately after session creation
|
||||||
|
# - Integrate into automation scripts requiring reliability
|
||||||
|
#
|
||||||
|
# EXAMPLES:
|
||||||
|
# # Send Python command and wait for prompt
|
||||||
|
# ./safe-send.sh -S /tmp/my.sock -t session:0.0 -c "print('hello')" -w ">>>"
|
||||||
|
#
|
||||||
|
# # Send literal text without executing (no Enter)
|
||||||
|
# ./safe-send.sh -t myapp:0.0 -c "some text" -l
|
||||||
|
#
|
||||||
|
# # Send with custom retry settings
|
||||||
|
# ./safe-send.sh -t session:0.0 -c "ls" -r 5 -i 1.0 -T 60
|
||||||
|
#
|
||||||
|
# # Send control sequence
|
||||||
|
# ./safe-send.sh -t session:0.0 -c "C-c"
|
||||||
|
#
|
||||||
|
# EXIT CODES:
|
||||||
|
# 0 - Command sent successfully
|
||||||
|
# 1 - Failed to send after retries
|
||||||
|
# 2 - Timeout waiting for prompt
|
||||||
|
# 3 - Pane not ready
|
||||||
|
# 4 - Invalid arguments
|
||||||
|
#
|
||||||
|
# DEPENDENCIES:
|
||||||
|
# - bash (with [[, printf, sleep, bc for exponential backoff)
|
||||||
|
# - tmux (for send-keys)
|
||||||
|
# - pane-health.sh (optional, for readiness check)
|
||||||
|
# - wait-for-text.sh (optional, for prompt waiting)
|
||||||
|
#
|
||||||
|
|
||||||
|
# Bash strict mode:
|
||||||
|
# -e: Exit immediately if any command fails
|
||||||
|
# -u: Treat unset variables as errors
|
||||||
|
# -o pipefail: Pipe fails if any command in pipeline fails
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get script directory to source registry library
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/registry.sh
|
||||||
|
source "$SCRIPT_DIR/lib/registry.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: safe-send.sh -t target -c command [options]
|
||||||
|
OR: safe-send.sh -s session -c command [options]
|
||||||
|
OR: safe-send.sh -c command [options] # auto-detect single session
|
||||||
|
|
||||||
|
Send keystrokes to a tmux pane with automatic retries and readiness checking.
|
||||||
|
|
||||||
|
Target Selection (priority order):
|
||||||
|
-s, --session session name (looks up socket/target in registry)
|
||||||
|
-t, --target tmux target (session:window.pane), explicit
|
||||||
|
(no flags) auto-detect if only one session in registry
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-c, --command command to send (empty = just Enter)
|
||||||
|
-S, --socket tmux socket path (for custom sockets via -S)
|
||||||
|
-L, --socket-name tmux socket name (for named sockets via -L)
|
||||||
|
-l, --literal use literal mode (send-keys -l, no Enter)
|
||||||
|
-m, --multiline use multiline mode (paste-buffer for code blocks)
|
||||||
|
-w, --wait wait for this pattern after sending
|
||||||
|
-T, --timeout timeout in seconds (default: 30)
|
||||||
|
-r, --retries max retry attempts (default: 3)
|
||||||
|
-i, --interval base retry interval in seconds (default: 0.5)
|
||||||
|
-v, --verbose verbose output for debugging
|
||||||
|
-h, --help show this help
|
||||||
|
|
||||||
|
Exit Codes:
|
||||||
|
0 - Command sent successfully
|
||||||
|
1 - Failed to send after retries
|
||||||
|
2 - Timeout waiting for prompt
|
||||||
|
3 - Pane not ready
|
||||||
|
4 - Invalid arguments
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
Normal mode (default):
|
||||||
|
Sends command and presses Enter (executes in shell/REPL)
|
||||||
|
Example: safe-send.sh -c "print('hello')"
|
||||||
|
|
||||||
|
Multiline mode (-m):
|
||||||
|
Sends multiline code blocks via paste-buffer
|
||||||
|
Auto-appends blank line for REPL execution
|
||||||
|
Example: safe-send.sh -m -c "def foo():
|
||||||
|
return 42"
|
||||||
|
(Incompatible with --literal)
|
||||||
|
|
||||||
|
Literal mode (-l):
|
||||||
|
Sends exact characters without Enter (typing text)
|
||||||
|
Example: safe-send.sh -l -c "some text"
|
||||||
|
(Incompatible with --multiline)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Send Python command and wait for prompt
|
||||||
|
safe-send.sh -t session:0.0 -c "2+2" -w ">>>" -T 10
|
||||||
|
|
||||||
|
# Send multiline Python function
|
||||||
|
safe-send.sh -t session:0.0 -m -c "def foo():
|
||||||
|
return 42" -w ">>>"
|
||||||
|
|
||||||
|
# Send gdb command
|
||||||
|
safe-send.sh -t debug:0.0 -c "break main" -w "(gdb)"
|
||||||
|
|
||||||
|
# Send with literal mode (no Enter)
|
||||||
|
safe-send.sh -t session:0.0 -c "text" -l
|
||||||
|
|
||||||
|
# Send with custom retry settings
|
||||||
|
safe-send.sh -t session:0.0 -c "ls" -r 5 -i 1.0
|
||||||
|
|
||||||
|
# Send on named socket
|
||||||
|
safe-send.sh -L my-socket -t session:0.0 -c "echo test"
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Default Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Required parameters (must be provided by user)
|
||||||
|
target="" # tmux target pane (format: session:window.pane)
|
||||||
|
command="__NOT_SET__" # command to send to the pane (sentinel value = not provided)
|
||||||
|
|
||||||
|
# Optional parameters
|
||||||
|
session_name="" # session name for registry lookup
|
||||||
|
socket="" # tmux socket path (empty = use default tmux socket)
|
||||||
|
socket_name="" # tmux socket name (for -L option)
|
||||||
|
literal_mode=false # use send-keys -l (literal mode)
|
||||||
|
multiline_mode=false # use paste-buffer for multiline code blocks
|
||||||
|
wait_pattern="" # pattern to wait for after sending (optional)
|
||||||
|
timeout=30 # timeout in seconds for prompt waiting
|
||||||
|
max_retries=3 # maximum number of send attempts
|
||||||
|
base_interval=0.5 # base interval for exponential backoff
|
||||||
|
verbose=false # enable verbose logging
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Parse Command-Line Arguments
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-s|--session) session_name="${2-}"; shift 2 ;;
|
||||||
|
-t|--target) target="${2-}"; shift 2 ;;
|
||||||
|
-c|--command) command="${2-}"; shift 2 ;;
|
||||||
|
-S|--socket) socket="${2-}"; shift 2 ;;
|
||||||
|
-L|--socket-name) socket_name="${2-}"; shift 2 ;;
|
||||||
|
-l|--literal) literal_mode=true; shift ;;
|
||||||
|
-m|--multiline) multiline_mode=true; shift ;;
|
||||||
|
-w|--wait) wait_pattern="${2-}"; shift 2 ;;
|
||||||
|
-T|--timeout) timeout="${2-}"; shift 2 ;;
|
||||||
|
-r|--retries) max_retries="${2-}"; shift 2 ;;
|
||||||
|
-i|--interval) base_interval="${2-}"; shift 2 ;;
|
||||||
|
-v|--verbose) verbose=true; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; usage; exit 4 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Session Resolution
|
||||||
|
# ============================================================================
|
||||||
|
# Resolve session name to socket/target if provided
|
||||||
|
# Priority: 1) Explicit -S/-t, 2) Session name -s, 3) Auto-detect single session
|
||||||
|
|
||||||
|
if [[ -n "$socket" && -n "$target" ]]; then
|
||||||
|
# Priority 1: Explicit socket and target provided (backward compatible)
|
||||||
|
: # Use as-is, no resolution needed
|
||||||
|
elif [[ -n "$session_name" ]]; then
|
||||||
|
# Priority 2: Session name provided, look up in registry
|
||||||
|
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
|
||||||
|
echo "Error: Session '$session_name' not found in registry" >&2
|
||||||
|
echo "Use 'list-sessions.sh' to see available sessions" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract socket and target from session data
|
||||||
|
socket=$(echo "$session_data" | jq -r '.socket')
|
||||||
|
target=$(echo "$session_data" | jq -r '.target')
|
||||||
|
|
||||||
|
# Update activity timestamp
|
||||||
|
registry_update_activity "$session_name" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ "$verbose" == true ]]; then
|
||||||
|
echo "Resolved session '$session_name': socket=$socket, target=$target" >&2
|
||||||
|
fi
|
||||||
|
elif [[ -z "$socket" && -z "$target" ]]; then
|
||||||
|
# Priority 3: No explicit params, try auto-detect single session
|
||||||
|
session_count=$(registry_list_sessions 2>/dev/null | jq '.sessions | length' 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [[ "$session_count" == "1" ]]; then
|
||||||
|
# Single session exists, auto-use it
|
||||||
|
auto_session_name=$(registry_list_sessions | jq -r '.sessions | keys[0]')
|
||||||
|
session_data=$(registry_get_session "$auto_session_name")
|
||||||
|
socket=$(echo "$session_data" | jq -r '.socket')
|
||||||
|
target=$(echo "$session_data" | jq -r '.target')
|
||||||
|
|
||||||
|
# Update activity timestamp
|
||||||
|
registry_update_activity "$auto_session_name" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ "$verbose" == true ]]; then
|
||||||
|
echo "Auto-detected single session '$auto_session_name': socket=$socket, target=$target" >&2
|
||||||
|
fi
|
||||||
|
elif [[ "$session_count" == "0" ]]; then
|
||||||
|
echo "Error: No sessions found in registry" >&2
|
||||||
|
echo "Create a session with 'create-session.sh' or specify -t and -S explicitly" >&2
|
||||||
|
exit 4
|
||||||
|
else
|
||||||
|
echo "Error: Multiple sessions found ($session_count total)" >&2
|
||||||
|
echo "Please specify session name with -s or use -t/-S explicitly" >&2
|
||||||
|
echo "Use 'list-sessions.sh' to see available sessions" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Validate Required Parameters and Dependencies
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check that required parameters were provided (after resolution)
|
||||||
|
if [[ -z "$target" ]]; then
|
||||||
|
echo "target is required" >&2
|
||||||
|
usage
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that -c was provided (but empty string is allowed)
|
||||||
|
if [[ "$command" == "__NOT_SET__" ]]; then
|
||||||
|
echo "command is required (use -c \"\" to send just Enter)" >&2
|
||||||
|
usage
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Note: Empty command is allowed - it just sends Enter (useful for prompts)
|
||||||
|
|
||||||
|
# Validate that timeout is a positive number
|
||||||
|
if ! [[ "$timeout" =~ ^[0-9]+\.?[0-9]*$ ]]; then
|
||||||
|
echo "timeout must be a positive number" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate that max_retries is a positive integer
|
||||||
|
if ! [[ "$max_retries" =~ ^[0-9]+$ ]] || [[ "$max_retries" -lt 1 ]]; then
|
||||||
|
echo "retries must be a positive integer (>= 1)" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate that base_interval is a positive number
|
||||||
|
if ! [[ "$base_interval" =~ ^[0-9]+\.?[0-9]*$ ]]; then
|
||||||
|
echo "interval must be a positive number" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that both socket options are not specified
|
||||||
|
if [[ -n "$socket" && -n "$socket_name" ]]; then
|
||||||
|
echo "Cannot specify both -S and -L options" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that multiline and literal modes are not both specified
|
||||||
|
if [[ "$multiline_mode" == true && "$literal_mode" == true ]]; then
|
||||||
|
echo "Error: --multiline and --literal are mutually exclusive" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that tmux is installed and available in PATH
|
||||||
|
if ! command -v tmux >/dev/null 2>&1; then
|
||||||
|
echo "tmux not found in PATH" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Helper Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# verbose_log: Log message if verbose mode is enabled
|
||||||
|
# Arguments: message string
|
||||||
|
# Returns: None (outputs to stderr)
|
||||||
|
verbose_log() {
|
||||||
|
if [[ "$verbose" == true ]]; then
|
||||||
|
echo "[safe-send] $*" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Build tmux Command Array
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Build base tmux command with optional socket parameter
|
||||||
|
tmux_cmd=(tmux)
|
||||||
|
if [[ -n "$socket" ]]; then
|
||||||
|
tmux_cmd+=(-S "$socket")
|
||||||
|
verbose_log "Using socket path: $socket"
|
||||||
|
elif [[ -n "$socket_name" ]]; then
|
||||||
|
tmux_cmd+=(-L "$socket_name")
|
||||||
|
verbose_log "Using socket name: $socket_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Get Tool Directory for Optional Dependencies
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Pre-flight Check: Verify Pane Health
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check if pane-health.sh is available
|
||||||
|
pane_health_tool="$SCRIPT_DIR/pane-health.sh"
|
||||||
|
if [[ -x "$pane_health_tool" ]]; then
|
||||||
|
verbose_log "Checking pane health before sending..."
|
||||||
|
|
||||||
|
# Build socket args for pane-health.sh
|
||||||
|
pane_health_args=()
|
||||||
|
if [[ -n "$socket" ]]; then
|
||||||
|
pane_health_args+=(-S "$socket")
|
||||||
|
elif [[ -n "$socket_name" ]]; then
|
||||||
|
# pane-health.sh doesn't support -L, so use default socket when -L is used
|
||||||
|
# This means health check won't work perfectly with -L, but we'll skip it gracefully
|
||||||
|
verbose_log "Warning: pane-health.sh doesn't support -L option, skipping health check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check pane health (exit codes: 0=healthy, 1=dead, 2=missing, 3=zombie, 4=server not running)
|
||||||
|
# Only run health check if we have socket args (not using -L)
|
||||||
|
if [[ ${#pane_health_args[@]} -gt 0 ]]; then
|
||||||
|
if ! "$pane_health_tool" "${pane_health_args[@]}" -t "$target" --format text >/dev/null 2>&1; then
|
||||||
|
health_exit=$?
|
||||||
|
echo "Error: Pane not ready (health check failed with exit code $health_exit)" >&2
|
||||||
|
verbose_log "Run '$pane_health_tool -t $target --format text' for details"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
verbose_log "Pane health check passed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
verbose_log "pane-health.sh not found, skipping health check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Logic: Send Command with Retry
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
send_success=false
|
||||||
|
|
||||||
|
for attempt in $(seq 1 "$max_retries"); do
|
||||||
|
verbose_log "Attempt $attempt/$max_retries: Sending command to $target"
|
||||||
|
|
||||||
|
if [[ "$multiline_mode" == true ]]; then
|
||||||
|
# ============================================================
|
||||||
|
# Multiline mode: use paste-buffer
|
||||||
|
# ============================================================
|
||||||
|
verbose_log "Using multiline mode (paste-buffer)"
|
||||||
|
|
||||||
|
# Auto-append blank line if not present (for Python REPL execution)
|
||||||
|
processed_command="$command"
|
||||||
|
if [[ ! "$processed_command" =~ $'\n\n'$ ]]; then
|
||||||
|
processed_command="${processed_command}"$'\n\n'
|
||||||
|
verbose_log "Auto-appended blank line for REPL execution"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set buffer
|
||||||
|
if ! "${tmux_cmd[@]}" set-buffer "$processed_command" 2>/dev/null; then
|
||||||
|
verbose_log "set-buffer failed on attempt $attempt"
|
||||||
|
# Continue to retry logic below (don't break early)
|
||||||
|
else
|
||||||
|
# Paste buffer to target pane
|
||||||
|
if "${tmux_cmd[@]}" paste-buffer -t "$target" 2>/dev/null; then
|
||||||
|
verbose_log "paste-buffer successful on attempt $attempt"
|
||||||
|
send_success=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
verbose_log "paste-buffer failed on attempt $attempt"
|
||||||
|
# Continue to retry logic below
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$literal_mode" == true ]]; then
|
||||||
|
# ============================================================
|
||||||
|
# Literal mode: send exact characters, no Enter
|
||||||
|
# ============================================================
|
||||||
|
verbose_log "Using literal mode (-l)"
|
||||||
|
send_cmd=("${tmux_cmd[@]}" send-keys -t "$target")
|
||||||
|
send_cmd+=(-l "$command")
|
||||||
|
|
||||||
|
# Attempt to send the command
|
||||||
|
if "${send_cmd[@]}" 2>/dev/null; then
|
||||||
|
verbose_log "Send successful on attempt $attempt"
|
||||||
|
send_success=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
verbose_log "Send failed on attempt $attempt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
# ============================================================
|
||||||
|
# Normal mode: send command and press Enter
|
||||||
|
# ============================================================
|
||||||
|
verbose_log "Using normal mode (with Enter)"
|
||||||
|
send_cmd=("${tmux_cmd[@]}" send-keys -t "$target")
|
||||||
|
send_cmd+=("$command" Enter)
|
||||||
|
|
||||||
|
# Attempt to send the command
|
||||||
|
if "${send_cmd[@]}" 2>/dev/null; then
|
||||||
|
verbose_log "Send successful on attempt $attempt"
|
||||||
|
send_success=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
verbose_log "Send failed on attempt $attempt"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Retry logic with exponential backoff
|
||||||
|
# ============================================================
|
||||||
|
# If this is not the last attempt, wait before retrying
|
||||||
|
if [[ $attempt -lt $max_retries ]]; then
|
||||||
|
# Calculate exponential backoff: base_interval * (2 ^ (attempt - 1))
|
||||||
|
# For base_interval=0.5: 0.5s, 1s, 2s, 4s, ...
|
||||||
|
# Using bc for floating-point arithmetic
|
||||||
|
if command -v bc >/dev/null 2>&1; then
|
||||||
|
sleep_duration=$(echo "$base_interval * (2 ^ ($attempt - 1))" | bc -l)
|
||||||
|
else
|
||||||
|
# Fallback if bc is not available: use integer arithmetic
|
||||||
|
multiplier=$((2 ** (attempt - 1)))
|
||||||
|
sleep_duration=$(echo "$base_interval * $multiplier" | awk '{print $1 * $3}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
verbose_log "Waiting ${sleep_duration}s before retry..."
|
||||||
|
sleep "$sleep_duration"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if send was successful
|
||||||
|
if [[ "$send_success" == false ]]; then
|
||||||
|
echo "Error: Failed to send command after $max_retries attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Optional: Wait for Prompt Pattern
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# If wait pattern is specified, wait for it using wait-for-text.sh
|
||||||
|
if [[ -n "$wait_pattern" ]]; then
|
||||||
|
wait_tool="$SCRIPT_DIR/wait-for-text.sh"
|
||||||
|
|
||||||
|
if [[ -x "$wait_tool" ]]; then
|
||||||
|
verbose_log "Waiting for pattern: $wait_pattern (timeout: ${timeout}s)"
|
||||||
|
|
||||||
|
# Build socket args for wait-for-text.sh
|
||||||
|
wait_args=()
|
||||||
|
if [[ -n "$socket" ]]; then
|
||||||
|
wait_args+=(-S "$socket")
|
||||||
|
elif [[ -n "$socket_name" ]]; then
|
||||||
|
# wait-for-text.sh doesn't support -L, skip waiting with warning
|
||||||
|
echo "Warning: wait-for-text.sh doesn't support -L option, cannot wait for pattern" >&2
|
||||||
|
verbose_log "Skipping pattern wait due to -L usage"
|
||||||
|
# Exit successfully since the send was successful
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for pattern (only if using -S or default socket)
|
||||||
|
if [[ ${#wait_args[@]} -ge 0 ]]; then
|
||||||
|
if "$wait_tool" "${wait_args[@]}" -t "$target" -p "$wait_pattern" -T "$timeout" >/dev/null 2>&1; then
|
||||||
|
verbose_log "Pattern found"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
wait_exit=$?
|
||||||
|
echo "Error: Timeout waiting for pattern '$wait_pattern'" >&2
|
||||||
|
verbose_log "wait-for-text.sh exited with code $wait_exit"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: wait-for-text.sh not found, cannot wait for pattern" >&2
|
||||||
|
verbose_log "Continuing without waiting for pattern"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Success
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
verbose_log "Command sent successfully"
|
||||||
|
exit 0
|
||||||
260
tools/wait-for-text.sh
Executable file
260
tools/wait-for-text.sh
Executable file
@@ -0,0 +1,260 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# wait-for-text.sh - Poll a tmux pane for text pattern and exit when found
|
||||||
|
#
|
||||||
|
# PURPOSE:
|
||||||
|
# Synchronize with interactive programs running in tmux by waiting for specific
|
||||||
|
# output patterns (e.g., shell prompts, program completion messages).
|
||||||
|
#
|
||||||
|
# HOW IT WORKS:
|
||||||
|
# 1. Captures pane output at regular intervals (default: 0.5s)
|
||||||
|
# 2. Searches captured text for the specified pattern using grep
|
||||||
|
# 3. Exits successfully (0) when pattern is found
|
||||||
|
# 4. Exits with error (1) if timeout is reached
|
||||||
|
#
|
||||||
|
# USE CASES:
|
||||||
|
# - Wait for Python REPL prompt (>>>) before sending commands
|
||||||
|
# - Wait for gdb prompt before issuing breakpoint commands
|
||||||
|
# - Wait for "compilation complete" before running tests
|
||||||
|
# - Synchronize with any interactive CLI tool in tmux
|
||||||
|
#
|
||||||
|
# EXAMPLE:
|
||||||
|
# # Wait for Python prompt on custom socket
|
||||||
|
# ./wait-for-text.sh -S /tmp/my.sock -t session:0.0 -p '^>>>' -T 10
|
||||||
|
#
|
||||||
|
# # Wait for exact string "Ready" (fixed string, not regex)
|
||||||
|
# ./wait-for-text.sh -t myapp:0.0 -p 'Ready' -F -T 30
|
||||||
|
#
|
||||||
|
# DEPENDENCIES:
|
||||||
|
# - bash (with [[, printf, sleep)
|
||||||
|
# - tmux (for capture-pane)
|
||||||
|
# - grep (for pattern matching)
|
||||||
|
# - date (for timeout calculation)
|
||||||
|
#
|
||||||
|
|
||||||
|
# Bash strict mode:
|
||||||
|
# -e: Exit immediately if any command fails
|
||||||
|
# -u: Treat unset variables as errors
|
||||||
|
# -o pipefail: Pipe fails if any command in pipeline fails
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get script directory to source registry library
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/registry.sh
|
||||||
|
source "$SCRIPT_DIR/lib/registry.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: wait-for-text.sh -t target -p pattern [options]
|
||||||
|
OR: wait-for-text.sh -s session -p pattern [options]
|
||||||
|
OR: wait-for-text.sh -p pattern [options] # auto-detect single session
|
||||||
|
|
||||||
|
Poll a tmux pane for text and exit when found.
|
||||||
|
|
||||||
|
Target Selection (priority order):
|
||||||
|
-s, --session session name (looks up socket/target in registry)
|
||||||
|
-t, --target tmux target (session:window.pane), explicit
|
||||||
|
(no flags) auto-detect if only one session in registry
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-p, --pattern regex pattern to look for, required
|
||||||
|
-S, --socket tmux socket path (for custom sockets via -S)
|
||||||
|
-F, --fixed treat pattern as a fixed string (grep -F)
|
||||||
|
-T, --timeout seconds to wait (integer, default: 15)
|
||||||
|
-i, --interval poll interval in seconds (default: 0.5)
|
||||||
|
-l, --lines number of history lines to inspect (integer, default: 1000)
|
||||||
|
-h, --help show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Using session name
|
||||||
|
wait-for-text.sh -s my-python -p '>>>' -T 10
|
||||||
|
|
||||||
|
# Auto-detect single session
|
||||||
|
wait-for-text.sh -p '>>>' -T 10
|
||||||
|
|
||||||
|
# Explicit socket/target (backward compatible)
|
||||||
|
wait-for-text.sh -S /tmp/my.sock -t session:0.0 -p '>>>'
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Default Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Required parameters (must be provided by user)
|
||||||
|
target="" # tmux target pane (format: session:window.pane)
|
||||||
|
pattern="" # regex pattern or fixed string to search for
|
||||||
|
|
||||||
|
# Optional parameters
|
||||||
|
session_name="" # session name for registry lookup
|
||||||
|
socket="" # tmux socket path (empty = use default tmux socket)
|
||||||
|
grep_flag="-E" # grep mode: -E (extended regex, default) or -F (fixed string)
|
||||||
|
timeout=15 # seconds to wait before giving up
|
||||||
|
interval=0.5 # seconds between polling attempts
|
||||||
|
lines=1000 # number of pane history lines to capture and search
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Parse Command-Line Arguments
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-s|--session) session_name="${2-}"; shift 2 ;; # Set session name for registry lookup
|
||||||
|
-t|--target) target="${2-}"; shift 2 ;; # Set target pane
|
||||||
|
-p|--pattern) pattern="${2-}"; shift 2 ;; # Set search pattern
|
||||||
|
-S|--socket) socket="${2-}"; shift 2 ;; # Set custom socket path
|
||||||
|
-F|--fixed) grep_flag="-F"; shift ;; # Use fixed string matching
|
||||||
|
-T|--timeout) timeout="${2-}"; shift 2 ;; # Set timeout duration
|
||||||
|
-i|--interval) interval="${2-}"; shift 2 ;; # Set poll interval
|
||||||
|
-l|--lines) lines="${2-}"; shift 2 ;; # Set history depth
|
||||||
|
-h|--help) usage; exit 0 ;; # Show help and exit
|
||||||
|
*) echo "Unknown option: $1" >&2; usage; exit 1 ;; # Error on unknown option
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Session Resolution
|
||||||
|
# ============================================================================
|
||||||
|
# Resolve session name to socket/target if provided
|
||||||
|
# Priority: 1) Explicit -S/-t, 2) Session name -s, 3) Auto-detect single session
|
||||||
|
|
||||||
|
if [[ -n "$socket" && -n "$target" ]]; then
|
||||||
|
# Priority 1: Explicit socket and target provided (backward compatible)
|
||||||
|
: # Use as-is, no resolution needed
|
||||||
|
elif [[ -n "$session_name" ]]; then
|
||||||
|
# Priority 2: Session name provided, look up in registry
|
||||||
|
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
|
||||||
|
echo "Error: Session '$session_name' not found in registry" >&2
|
||||||
|
echo "Use 'list-sessions.sh' to see available sessions" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract socket and target from session data
|
||||||
|
socket=$(echo "$session_data" | jq -r '.socket')
|
||||||
|
target=$(echo "$session_data" | jq -r '.target')
|
||||||
|
|
||||||
|
# Update activity timestamp
|
||||||
|
registry_update_activity "$session_name" 2>/dev/null || true
|
||||||
|
elif [[ -z "$socket" && -z "$target" ]]; then
|
||||||
|
# Priority 3: No explicit params, try auto-detect single session
|
||||||
|
session_count=$(registry_list_sessions 2>/dev/null | jq '.sessions | length' 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [[ "$session_count" == "1" ]]; then
|
||||||
|
# Single session exists, auto-use it
|
||||||
|
auto_session_name=$(registry_list_sessions | jq -r '.sessions | keys[0]')
|
||||||
|
session_data=$(registry_get_session "$auto_session_name")
|
||||||
|
socket=$(echo "$session_data" | jq -r '.socket')
|
||||||
|
target=$(echo "$session_data" | jq -r '.target')
|
||||||
|
|
||||||
|
# Update activity timestamp
|
||||||
|
registry_update_activity "$auto_session_name" 2>/dev/null || true
|
||||||
|
elif [[ "$session_count" == "0" ]]; then
|
||||||
|
echo "Error: No sessions found in registry" >&2
|
||||||
|
echo "Create a session with 'create-session.sh' or specify -t and -S explicitly" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Error: Multiple sessions found ($session_count total)" >&2
|
||||||
|
echo "Please specify session name with -s or use -t/-S explicitly" >&2
|
||||||
|
echo "Use 'list-sessions.sh' to see available sessions" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Validate Required Parameters and Dependencies
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check that required parameters were provided (after resolution)
|
||||||
|
if [[ -z "$target" || -z "$pattern" ]]; then
|
||||||
|
echo "target and pattern are required" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate that timeout is a positive integer (regex: one or more digits)
|
||||||
|
if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "timeout must be an integer number of seconds" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate that lines is a positive integer
|
||||||
|
if ! [[ "$lines" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "lines must be an integer" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that tmux is installed and available in PATH
|
||||||
|
if ! command -v tmux >/dev/null 2>&1; then
|
||||||
|
echo "tmux not found in PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Calculate Deadline for Timeout
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get current time in epoch seconds (Unix timestamp)
|
||||||
|
start_epoch=$(date +%s)
|
||||||
|
# Calculate deadline: current time + timeout duration
|
||||||
|
deadline=$((start_epoch + timeout))
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Polling Loop
|
||||||
|
# ============================================================================
|
||||||
|
# Repeatedly capture pane output and search for pattern until found or timeout
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Step 1: Capture pane output from tmux
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# tmux capture-pane options:
|
||||||
|
# -p: Print to stdout (instead of saving to paste buffer)
|
||||||
|
# -J: Join wrapped lines (prevents false line breaks from terminal width)
|
||||||
|
# -t: Target pane to capture from
|
||||||
|
# -S: Start line (negative = relative to end, e.g., -1000 = last 1000 lines)
|
||||||
|
#
|
||||||
|
# ${socket:+-S "$socket"} syntax explanation:
|
||||||
|
# - If $socket is set: expands to -S "$socket"
|
||||||
|
# - If $socket is empty: expands to nothing
|
||||||
|
# This allows optional socket parameter without breaking the command
|
||||||
|
#
|
||||||
|
# Error handling:
|
||||||
|
# 2>/dev/null: Suppress error messages if pane doesn't exist
|
||||||
|
# || true: Don't fail script if capture fails (exit 0 instead)
|
||||||
|
#
|
||||||
|
pane_text="$(tmux ${socket:+-S "$socket"} capture-pane -p -J -t "$target" -S "-${lines}" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Step 2: Search captured text for pattern
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Use printf to safely output text (handles special characters correctly)
|
||||||
|
# Pipe to grep to search for pattern
|
||||||
|
# $grep_flag: Either -E (regex) or -F (fixed string), set by --fixed flag
|
||||||
|
# --: Marks end of options (allows patterns starting with -)
|
||||||
|
# >/dev/null: Discard grep output (we only care about exit code)
|
||||||
|
# Exit code 0 = pattern found, 1 = not found
|
||||||
|
#
|
||||||
|
if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then
|
||||||
|
# SUCCESS: Pattern found in pane output
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Step 3: Check if timeout has been reached
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
now=$(date +%s)
|
||||||
|
if (( now >= deadline )); then
|
||||||
|
# TIMEOUT: Pattern not found within specified time
|
||||||
|
echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2
|
||||||
|
echo "Last ${lines} lines from $target:" >&2
|
||||||
|
printf '%s\n' "$pane_text" >&2 # Show what was captured (for debugging)
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Step 4: Wait before next poll attempt
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Sleep for specified interval before checking again
|
||||||
|
# Default: 0.5 seconds (configurable via --interval)
|
||||||
|
sleep "$interval"
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user