Initial commit
This commit is contained in:
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