commit 966e739b00d249ef8fe47482cb1830941c6a9965 Author: Zhongwei Li Date: Sun Nov 30 08:56:13 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..0811aaf --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "tmux-task-runner", + "description": "Run build processes, test suites, deployments, and development servers in monitored tmux sessions with persistent logging", + "version": "0.0.0-2025.11.28", + "author": { + "name": "Shane Vitarana", + "email": "zhongweili@tubi.tv" + }, + "skills": [ + "./skills/tmux-task-runner" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..320d5d6 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# tmux-task-runner + +Run build processes, test suites, deployments, and development servers in monitored tmux sessions with persistent logging diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..5525fbc --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,64 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:shanev/skills:tmux-task-runner", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "0a2116610e71a88dc331c9382a228b5b7d9f1892", + "treeHash": "e74eccb15e2cb8683d84aba7401097a98d5fc013426e808b2b76cc8ebe1c6e38", + "generatedAt": "2025-11-28T10:28:17.724055Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "tmux-task-runner", + "description": "Run build processes, test suites, deployments, and development servers in monitored tmux sessions with persistent logging" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "46771df779bcf6978fb21dc111055ccab6aaca776ac8ecb06eec2c54f22888b1" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "10a94d8c4f52b1c86e43e231a1da99c30f86721ba301f3337e2e3bad0d64d32d" + }, + { + "path": "skills/tmux-task-runner/EXAMPLES.md", + "sha256": "a1620f2e82ba585f295c7d80b5150a21594cd37c4e56133e3311b820ceea73cb" + }, + { + "path": "skills/tmux-task-runner/TODO.md", + "sha256": "5b136c498c9d3c4d19d88515fd497de55b410544cd656d75e3dea1252ca4fd28" + }, + { + "path": "skills/tmux-task-runner/run.sh", + "sha256": "15440ef90e5b4ef00cade44bfe72546a38312039152ccb54488260953436e8d6" + }, + { + "path": "skills/tmux-task-runner/README.md", + "sha256": "3b5103a6289ea785acfef99ade5a6b2452236a1016a69ad2283b5e1d1bce599a" + }, + { + "path": "skills/tmux-task-runner/package.json", + "sha256": "1a31d34c40ecddd3d900a3b2b111b773301b8e7d09dd0599cfbe90901d98ba9e" + }, + { + "path": "skills/tmux-task-runner/SKILL.md", + "sha256": "dfeec76c48f6198b496af15b795bfbe12461d5cd93a6e7ee3a3e213c2306051b" + } + ], + "dirSha256": "e74eccb15e2cb8683d84aba7401097a98d5fc013426e808b2b76cc8ebe1c6e38" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/tmux-task-runner/EXAMPLES.md b/skills/tmux-task-runner/EXAMPLES.md new file mode 100644 index 0000000..31bc5e8 --- /dev/null +++ b/skills/tmux-task-runner/EXAMPLES.md @@ -0,0 +1,325 @@ +# Tmux Task Runner - Examples + +This document provides practical examples of using the tmux-task-runner skill. + +## Quick Start + +### Example 1: Running a Build Process + +```bash +# Start a build in a tmux session +./run.sh run build "npm run build" + +# Output: +# ✓ Task started in tmux session +# +# Session: task-build-1729519263 +# Log file: $LOG_DIR/task-build-1729519263.log (LOG_DIR defaults to /tmp) +# Status file: $STATUS_DIR/task-build-1729519263.status +# Workdir: /Users/you/project +# +# Monitoring commands: +# tail -f $LOG_DIR/task-build-1729519263.log +# tmux attach-session -t task-build-1729519263 +# ./run.sh tail task-build-1729519263 +# tmux kill-session -t task-build-1729519263 +``` + +Monitor the build: +```bash +tail -f "$LOG_DIR"/task-build-1729519263.log +``` + +### Example 2: Running Tests + +```bash +# Run pytest with verbose output and CI environment +./run.sh run test --env NODE_ENV=ci "pytest tests/ --verbose --cov" + +# Or with npm/yarn (preserve exit status with coverage) +./run.sh run test --notify "npm test -- --coverage" +``` + +Check test status: +```bash +./run.sh check task-test-1729519263 +# Or review the recorded metadata: +./run.sh status task-test-1729519263 +``` + +### Example 3: Starting a Development Server + +```bash +# Start a Python HTTP server +./run.sh run server "python -m http.server 8000" + +# Start a Node.js dev server +./run.sh run server --workdir ./apps/frontend "npm run dev" + +# Start a Django dev server +./run.sh run server "python manage.py runserver" +``` + +Attach to the server session to see real-time requests: +```bash +./run.sh attach task-server-1729519263 +# Press Ctrl+b then d to detach +``` + +### Example 4: Running Deployment Scripts + +```bash +# Deploy to production +./run.sh run deploy --notify "./deploy.sh production" + +# Or with a deployment tool +./run.sh run deploy "terraform apply -auto-approve" +``` + +Monitor deployment progress: +```bash +# Poll the last 80 lines every 5 seconds +./run.sh tail task-deploy-1729519263 --interval 5 --lines 80 +``` + +## Advanced Usage + +### Example 5: Managing Multiple Concurrent Tasks + +```bash +# Start multiple tasks +./run.sh run build "npm run build" +./run.sh run test "npm test" +./run.sh run lint "npm run lint" + +# List all active tasks +./run.sh list + +# Output: +# Active task sessions: +# task-build-1729519263: 1 windows (created Tue Oct 21 10:30:00 2025) +# task-test-1729519264: 1 windows (created Tue Oct 21 10:30:05 2025) +# task-lint-1729519265: 1 windows (created Tue Oct 21 10:30:10 2025) +``` + +### Example 6: Long-Running Data Processing + +```bash +# Process large dataset +./run.sh run process "python scripts/process_data.py --input large_dataset.csv" + +# Machine learning training +./run.sh run train "python train_model.py --epochs 100" +``` + +Check progress periodically: +```bash +# Every 30 seconds, check the last 20 lines +./run.sh tail task-train-1729519263 --interval 30 --lines 20 +``` + +### Example 7: Database Migrations + +```bash +# Run database migrations +./run.sh run migrate "alembic upgrade head" + +# Or with Django +./run.sh run migrate "python manage.py migrate" +``` + +### Example 8: Cleanup Operations + +```bash +# Check all running tasks +./run.sh list + +# Kill a specific task +./run.sh kill task-build-1729519263 + +# Kill all task sessions +./run.sh kill all +``` + +## Real-World Scenarios + +### Scenario 1: CI/CD Pipeline Simulation + +```bash +# Run a complete CI/CD pipeline locally +./run.sh run ci "bash -c ' + echo \"=== Installing dependencies ===\" && + npm install && + echo \"=== Running linter ===\" && + npm run lint && + echo \"=== Running tests ===\" && + npm test && + echo \"=== Building application ===\" && + npm run build && + echo \"=== CI Pipeline Complete ===\" +'" + +# Monitor the pipeline +tail -f "$LOG_DIR"/task-ci-*.log +``` + +### Scenario 2: Monitoring API Load Testing + +```bash +# Start API load test +./run.sh run loadtest "ab -n 10000 -c 100 http://localhost:8000/api/endpoint" + +# Or with a more advanced tool +./run.sh run loadtest "artillery run load-test-config.yml" + +# Attach to watch real-time metrics +./run.sh attach task-loadtest-1729519263 +``` + +### Scenario 3: File Processing Pipeline + +```bash +# Process video files +./run.sh run encode "ffmpeg -i input.mp4 -c:v libx264 -preset slow output.mp4" + +# Batch image optimization +./run.sh run optimize "find ./images -name '*.jpg' -exec mogrify -quality 85 {} \;" +``` + +### Scenario 4: Development Workflow + +```bash +# Terminal 1: Start backend server +./run.sh run backend "python manage.py runserver" + +# Terminal 2: Start frontend dev server +./run.sh run frontend "npm run dev" + +# Terminal 3: Watch tests +./run.sh run test-watch "npm run test:watch" + +# Terminal 4: Monitor all logs +./run.sh list +# Then tail specific logs as needed +``` + +## Integration with Claude Code + +When working with Claude Code, you can request task execution like this: + +**User:** "Run the test suite in the background so I can continue working" + +**Claude:** I'll run your test suite in a tmux session using the tmux-task-runner skill. + +```bash +./skills/tmux-task-runner/run.sh run test "npm test -- --coverage" +``` + +Your tests are now running in session `task-test-1729519263`. + +Monitor progress: +```bash +./skills/tmux-task-runner/run.sh tail task-test-1729519263 --lines 80 +# Or follow the log directly: +tail -f "$LOG_DIR"/task-test-1729519263.log +``` + +I'll check the initial output: +```bash +tmux capture-pane -t task-test-1729519263 -p -S -20 +``` + +The tests are running. You can continue working, and I'll check back on the results in a few moments. + +--- + +**User:** "Check on the test results" + +**Claude:** Let me check the test session status: + +```bash +./skills/tmux-task-runner/run.sh check task-test-1729519263 +``` + +[Shows output with test results] + +The tests have completed successfully! All 127 tests passed with 92% coverage. + +## Tips and Tricks + +### Tip 1: Creating Task Aliases + +Add to your `~/.bashrc` or `~/.zshrc`: + +```bash +alias tmux-task='/path/to/skills/tmux-task-runner/run.sh' + +# Now you can use: +tmux-task run build "npm run build" +tmux-task list +``` + +### Tip 2: Monitoring Multiple Sessions + +```bash +# Create a monitoring dashboard using tmux itself +tmux new-session -s monitoring \; \ + send-keys "tail -f \"$LOG_DIR\"/task-build-*.log" C-m \; \ + split-window -v \; \ + send-keys "tail -f \"$LOG_DIR\"/task-test-*.log" C-m \; \ + split-window -h \; \ + send-keys "watch -n 5 './run.sh list'" C-m +``` + +### Tip 3: Saving Output + +```bash +# Save complete session output to file +tmux capture-pane -t task-build-1729519263 -p -S - > build-output.txt + +# Or just use the log file +cp "$LOG_DIR"/task-build-1729519263.log ./build-output.txt +``` + +### Tip 4: Setting Up Notifications + +```bash +# Built-in flag handles macOS/Linux automatically +./run.sh run build --notify "npm run build" +``` + +## Troubleshooting + +### Issue: Can't find session + +```bash +# List all sessions to find the correct name +tmux list-sessions + +# Or check available logs +ls -lt "$LOG_DIR"/task-*.log +``` + +### Issue: Session closed unexpectedly + +```bash +# Check the log file for errors +cat "$LOG_DIR"/task-build-1729519263.log + +# Look for error messages or exit codes +tail -50 "$LOG_DIR"/task-build-1729519263.log +``` + +### Issue: Want to keep session after task completes + +Sessions exit automatically once the command completes, but the log and status files remain for review (`./run.sh status `). + +### Issue: Too many old sessions + +```bash +# Kill all active task sessions +./run.sh kill all + +# Clean up old log files (older than 7 days) +find "$LOG_DIR" -name "task-*.log" -mtime +7 -delete +``` diff --git a/skills/tmux-task-runner/README.md b/skills/tmux-task-runner/README.md new file mode 100644 index 0000000..6ad97e5 --- /dev/null +++ b/skills/tmux-task-runner/README.md @@ -0,0 +1,154 @@ +# `tmux` Task Runner + +Execute long-running tasks in tmux sessions with real-time monitoring. Tasks run in detached sessions with persistent logging and easy monitoring. + +## Features + +- Detached tmux sessions for long-running tasks +- Persistent logs and status metadata (configurable via `LOG_DIR` / `STATUS_DIR`) +- Flexible run options: `--workdir`, repeatable `--env` overrides, and optional notifications +- Real-time output monitoring via `tmux`, the built-in `tail` helper, or log files +- Session management (list, status, check, attach, kill) +- Clean output optimized for Claude Code + +## Prerequisites + +This skill requires tmux to be installed: + +```bash +# macOS +brew install tmux + +# Ubuntu/Debian +sudo apt-get install tmux + +# Fedora/RHEL +sudo dnf install tmux +``` + +## Usage + +Once installed, Claude automatically invokes this skill when you request long-running tasks. + +**Examples:** + +- **You:** "Run the test suite in the background" +- **Claude:** Executes tests in a tmux session with monitoring commands + +- **You:** "Start the build process and let me monitor it" +- **Claude:** Creates a tmux session with the build command and provides monitoring instructions + +## Manual Commands + +You can also use the run script directly: + +```bash +# Run a task +./run.sh run build "npm run build" + +# Provide a working directory and environment overrides +./run.sh run test --workdir ./services/api --env NODE_ENV=ci --env DEBUG=1 "npm test -- --runInBand" + +# Enable notifications when the command finishes (best effort) +./run.sh run deploy --notify "./scripts/deploy.sh production" + +# Check status +./run.sh check task-build-1729519263 + +# List all tasks +./run.sh list + +# Summarize recent runs or inspect a specific session +./run.sh status +./run.sh status task-build-1729519263 + +# Tail output without attaching +./run.sh tail task-build-1729519263 --interval 5 --lines 80 + +# Attach to session +./run.sh attach task-build-1729519263 + +# Kill a task +./run.sh kill task-build-1729519263 + +# Get help +./run.sh help +``` + +## Use Cases + +- Build processes (webpack, npm build, etc.) +- Test suites (jest, pytest, etc.) +- Development servers +- Deployment scripts +- Any command requiring background execution with monitoring + +## Examples + +See [EXAMPLES.md](EXAMPLES.md) for detailed usage examples including: +- CI/CD pipeline simulation +- Running multiple concurrent tasks +- Database migrations +- Load testing +- And more + +## How It Works + +1. Creates a uniquely-named tmux session (e.g., `task-build-1729519263`) +2. Runs your command in the detached session +3. Captures all output to `${LOG_DIR:-/tmp}/task-{name}.log` +4. Provides monitoring commands for real-time output +5. Session persists until task completes or you kill it + +## Monitoring Sessions + +**View logs in real-time (`LOG_DIR` defaults to `/tmp`):** +```bash +tail -f "$LOG_DIR"/task-build-1729519263.log +``` + +**Attach to session interactively:** +```bash +tmux attach-session -t task-build-1729519263 +# Press Ctrl+b then d to detach +``` + +**Quick output snapshot:** +```bash +tmux capture-pane -t task-build-1729519263 -p +``` + +**Poll output without attaching:** +```bash +./run.sh tail task-build-1729519263 --interval 5 --lines 80 +``` + +## Configuration + +Set the following environment variables before invoking `run.sh` (or exporting them in your shell) to customize behavior: + +- `LOG_DIR` (default `/tmp`): where log files are written +- `STATUS_DIR` (default `/tmp`): stores status metadata for each task +- `PRUNE_RETENTION_DAYS` (default `7`): automatically removes logs/status files older than this many days +- `STATUS_SUMMARY_LIMIT` (default `10`): number of entries shown by `list`/`status` +- `TAIL_DEFAULT_LINES` (default `50`): lines captured per refresh by `tail` +- `TAIL_DEFAULT_INTERVAL` (default `2`): seconds between refreshes in `tail` + +Directories are created automatically if they do not exist. + +## Troubleshooting + +**tmux not found:** +Install tmux using your system package manager (see Prerequisites) + +**Session closed unexpectedly:** +Check the log file for errors: `cat "$LOG_DIR"/task-{name}.log` + +**Can't find session:** +List all sessions: `tmux list-sessions` or `./run.sh list` + +**Too many old sessions:** +Kill all task sessions: `./run.sh kill all` + +**Task appears hung:** +Inspect the session with `./run.sh check ` and kill it if needed: `./run.sh kill ` diff --git a/skills/tmux-task-runner/SKILL.md b/skills/tmux-task-runner/SKILL.md new file mode 100644 index 0000000..31a2cb3 --- /dev/null +++ b/skills/tmux-task-runner/SKILL.md @@ -0,0 +1,430 @@ +--- +name: tmux-task-runner +description: Run build processes, test suites, deployments, and development servers in monitored tmux sessions with persistent logging. Use when executing long-running tasks that need background execution with real-time monitoring, or when running commands like npm build, pytest, deployment scripts, or dev servers that should continue running while you work on other tasks. +--- + +# Tmux Long-Running Task Skill + +## Overview + +This skill provides a robust solution for running long-running tasks in tmux sessions, offering superior flexibility compared to standard background process execution. It enables: + +- **Detached execution**: Tasks run in isolated tmux sessions +- **Real-time monitoring**: Capture and analyze logs via `tmux capture-pane` +- **Developer control**: Attach to sessions for interactive debugging +- **Persistent logging**: All output saved to timestamped log files (configurable via `LOG_DIR`) +- **Status metadata**: Exit codes, timings, and environment details saved for later review +- **Session management**: List, monitor, summarize, and clean up active sessions + +## Critical Workflow + +When a user requests execution of a long-running task (builds, tests, deployments, servers, etc.): + +1. **Detect task type**: Identify if the task is long-running (>30s expected duration) +2. **Create session**: Generate unique session name (e.g., `task-build-1729519263`) +3. **Execute in tmux**: Run command in detached tmux session with logging +4. **Monitor output**: Use `tmux capture-pane` to read session output +5. **Report status**: Inform user of session name, log/status file locations, and monitoring options +6. **Provide access**: Give user commands to tail logs, use the `tail` helper, or attach to the session + +**CRITICAL:** Always check if tmux is installed before proceeding. If not found, inform the user to install it first. + +## How It Works + +1. User requests a long-running task execution +2. Skill creates a tmux session with descriptive name +3. Task runs in a detached session with output captured to a timestamped log in `${LOG_DIR:-/tmp}` +4. Skill records status metadata (exit code, timings, command, overrides) in `${STATUS_DIR:-/tmp}` +5. User can monitor via log tailing, the built-in `tail` helper, the `status` summary, or by attaching +6. Session persists until task completes or user kills it + +## Setup Instructions + +Ensure tmux is installed on your system: + +```bash +# macOS +brew install tmux + +# Ubuntu/Debian +sudo apt-get install tmux + +# Fedora/RHEL +sudo dnf install tmux +``` + +Verify installation: +```bash +tmux -V +``` + +## Execution Pattern + +### Step 1: Validate tmux availability + +```javascript +// Check if tmux is installed +const { execSync } = require('child_process'); + +try { + execSync('which tmux', { stdio: 'pipe' }); +} catch (error) { + throw new Error('tmux is not installed. Please install it first.'); +} +``` + +### Step 2: Prepare session metadata + +```bash +# Generate unique session name and resolve directories +SESSION_NAME="task-${TASK_TYPE}-$(date +%s)" +LOG_DIR="${LOG_DIR:-/tmp}" +STATUS_DIR="${STATUS_DIR:-/tmp}" +LOG_FILE="$LOG_DIR/${SESSION_NAME}.log" +STATUS_FILE="$STATUS_DIR/${SESSION_NAME}.status" + +# Optional overrides +WORKDIR="${WORKDIR:-$PWD}" # run command from a different folder +ENV_OVERRIDES=("NODE_ENV=ci" "DEBUG=1") # repeatable KEY=VALUE pairs +NOTIFY=1 # enable desktop notifications if available +``` + +### Step 3: Launch tmux session with logging and status tracking + +```bash +tmux new-session -d -s "$SESSION_NAME" -c "$WORKDIR" bash -lc ' + set -o pipefail + + # Export requested environment overrides + export NODE_ENV=ci DEBUG=1 + + START_EPOCH=$(date +%s) + START_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + your-command-here 2>&1 | tee "'"$LOG_FILE"'" + exit_code=${PIPESTATUS[0]} + + END_EPOCH=$(date +%s) + END_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + DURATION=$((END_EPOCH - START_EPOCH)) + + { + printf "exit_code=%q\n" "$exit_code" + printf "command=%q\n" "your-command-here" + printf "started_at_iso=%q\n" "$START_ISO" + printf "finished_at_iso=%q\n" "$END_ISO" + printf "duration_seconds=%q\n" "$DURATION" + printf "log_file=%q\n" "'"$LOG_FILE"'" + printf "workdir=%q\n" "'"$WORKDIR"'" + printf "env_vars=%q\n" "NODE_ENV=ci; DEBUG=1" + } > "'"$STATUS_FILE"'" + + if [ '"$NOTIFY"' -eq 1 ]; then + message="[$SESSION_NAME] " + if [ "$exit_code" -eq 0 ]; then + message+="completed successfully" + else + message+="failed (exit $exit_code)" + fi + command -v notify-send >/dev/null 2>&1 && notify-send "tmux-task-runner" "$message" + fi + + exit "$exit_code" +' +``` + +### Step 4: Provide monitoring and summary commands + +```bash +# Tail the log file directly +tail -f "$LOG_FILE" + +# Poll output without attaching +./run.sh tail "$SESSION_NAME" --interval 5 --lines 80 + +# Summarize session metadata (exit status, duration, timestamps) +./run.sh status "$SESSION_NAME" + +# Attach to the session (interactive) +tmux attach-session -t "$SESSION_NAME" +``` + +## Common Patterns + +### Build Tasks + +```bash +./run.sh run build "npm run build" +# Monitor tail: ./run.sh tail task-build-... --interval 5 +# Summarize: ./run.sh status task-build-... +``` + +### Test Suites + +```bash +./run.sh run test --env NODE_ENV=ci "npm test -- --coverage" +``` + +### Development Server + +```bash +./run.sh run server --workdir ./services/web "npm run dev" +``` + +### Deployment Scripts + +```bash +./run.sh run deploy --notify "./deploy.sh production" +``` + +## Run Script Enhancements + +### Working Directory Overrides +- Use `--workdir ` to run the command from a different directory without wrapping the command in `cd ... &&`. +- The script attempts to use `tmux new-session -c` when available and falls back to an inline `cd` if necessary. + +### Environment Variable Injection +- Repeatable `--env KEY=VALUE` flags export additional environment variables for the task. +- Variable names are validated to prevent invalid shell identifiers. + +### Completion Notifications +- `--notify` triggers a best-effort desktop notification (`notify-send`, `osascript`, or `terminal-notifier`) when the job ends. +- Notifications include the session name and whether the command succeeded. + +### Status Metadata and Summaries +- Each run writes a status file to `${STATUS_DIR:-/tmp}` containing exit code, timestamps, duration, command, workdir, and env overrides. +- `./run.sh status` lists recent runs; `./run.sh status ` prints detailed metadata and the latest log lines. +- Status files enable tooling to expose job history without attaching to tmux directly. + +### Tail Helper +- `./run.sh tail [--interval N] [--lines M]` polls `tmux capture-pane` on a timer for lightweight monitoring. +- Falls back to the log file if the session has already exited. + +### Configurable Directories and Pruning +- Override `LOG_DIR` and `STATUS_DIR` to store artifacts outside `/tmp`. +- `PRUNE_RETENTION_DAYS` (default 7) automatically removes stale log/status files. +- `STATUS_SUMMARY_LIMIT`, `TAIL_DEFAULT_LINES`, and `TAIL_DEFAULT_INTERVAL` tune command defaults without editing the script. + +## Session Management + +### List Active Sessions + +```bash +# List all tmux sessions +tmux list-sessions + +# List only task sessions +tmux list-sessions 2>/dev/null | grep "^task-" || echo "No active task sessions" +``` + +### Monitor Session Output + +```bash +# Capture last 100 lines +tmux capture-pane -t SESSION_NAME -p -S -100 + +# Capture entire scrollback buffer +tmux capture-pane -t SESSION_NAME -p -S - + +# Save to file +tmux capture-pane -t SESSION_NAME -p -S - > session-capture.txt +``` + +### Kill Sessions + +```bash +# Kill specific session +tmux kill-session -t SESSION_NAME + +# Kill all task sessions +tmux list-sessions -F "#{session_name}" | grep "^task-" | xargs -I {} tmux kill-session -t {} +``` + +## Helper Script Example + +Create a reusable helper for common operations: + +```bash +#!/bin/bash +# tmux-task.sh - Tmux task runner helper + +run_task() { + local task_type=$1 + shift + local command="$@" + + local session="task-${task_type}-$(date +%s)" + local logfile="/tmp/${session}.log" + + # Create session + tmux new-session -d -s "$session" "$command 2>&1 | tee $logfile" + + # Output info + echo "✓ Task started in session: $session" + echo " Log file: $logfile" + echo "" + echo "Monitoring commands:" + echo " tail -f $logfile # Follow log output" + echo " tmux attach-session -t $session # Attach to session" + echo " tmux kill-session -t $session # Stop task" +} + +check_session() { + local session=$1 + + if tmux has-session -t "$session" 2>/dev/null; then + echo "Session '$session' is running" + echo "" + echo "Recent output:" + tmux capture-pane -t "$session" -p -S -20 + else + echo "Session '$session' has completed or doesn't exist" + fi +} + +list_tasks() { + echo "Active task sessions:" + tmux list-sessions 2>/dev/null | grep "^task-" || echo " No active tasks" +} + +# Usage example +case "$1" in + run) + run_task "$2" "${@:3}" + ;; + check) + check_session "$2" + ;; + list) + list_tasks + ;; + *) + echo "Usage: $0 {run|check|list} [args...]" + exit 1 +esac +``` + +## Tips + +- **CRITICAL:** Always use unique session names to avoid conflicts +- **CRITICAL:** Include timestamps in session names for easy identification +- **CRITICAL:** Always provide log file paths to users for monitoring +- Use descriptive task type prefixes (build, test, deploy, server, etc.) +- Use `LOG_DIR` (defaults to `/tmp`) to avoid cluttering project directories +- Use `tmux capture-pane -p -S -100` to get reasonable output chunks +- For very long output, save to file instead of displaying inline +- Remind users they can attach to sessions for interactive debugging +- Clean up completed sessions periodically to avoid clutter + +## Troubleshooting + +**Problem:** "session not found" error +- **Solution:** Session may have completed. Check if process finished in log file. + +**Problem:** Unable to capture pane output +- **Solution:** Ensure session name is correct: `tmux list-sessions` + +**Problem:** Log file not updating +- **Solution:** Verify command includes `2>&1 | tee $LOG_FILE` for proper output capture + +**Problem:** tmux not found +- **Solution:** Install tmux using system package manager (brew, apt, dnf, etc.) + +**Problem:** Session already exists +- **Solution:** Use unique names with timestamps or kill existing session first + +## Example Usage + +**User:** "Run the full test suite in the background and let me monitor it" + +**Claude:** I'll run your test suite in a tmux session so you can monitor it easily. + +```bash +./run.sh run test --env NODE_ENV=ci "npm test -- --runInBand" +``` + +Your tests are now running in the background. Here's how to monitor: + +**Monitor the logs:** +```bash +./run.sh tail task-test-1729519263 --interval 5 --lines 80 +# or tail -f "$LOG_DIR"/task-test-1729519263.log +``` + +**Attach to the session:** +```bash +tmux attach-session -t task-test-1729519263 +``` +(Press `Ctrl+b` then `d` to detach without stopping the tests) + +**Check current status summary:** +```bash +./run.sh status task-test-1729519263 +``` + +The summary includes exit code, duration, timestamps, workdir, and environment overrides once the command finishes. + +--- + +## Advanced Features + +### Multi-Pane Monitoring + +Run multiple related tasks in split panes: + +```bash +SESSION="task-multiview-$(date +%s)" + +# Create session with first task +tmux new-session -d -s "$SESSION" "npm run build" + +# Split horizontally and run tests +tmux split-window -h -t "$SESSION" "npm test" + +# Split first pane vertically for logs +tmux select-pane -t 0 +tmux split-window -v -t "$SESSION" "tail -f /tmp/app.log" + +echo "Multi-pane session created: $SESSION" +echo "Attach with: tmux attach-session -t $SESSION" +``` + +### Persistent Sessions + +Configure tmux to save sessions across reboots (requires tmux-resurrect plugin): + +```bash +# Install tmux plugin manager (TPM) +git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm + +# Add to ~/.tmux.conf +echo "set -g @plugin 'tmux-plugins/tmux-resurrect'" >> ~/.tmux.conf +echo "run '~/.tmux/plugins/tpm/tpm'" >> ~/.tmux.conf +``` + +### Integration with CI/CD + +Use tmux sessions for local CI/CD simulation: + +```bash +SESSION="task-ci-$(date +%s)" +LOG="/tmp/${SESSION}.log" + +tmux new-session -d -s "$SESSION" bash -c " + echo '=== Linting ===' && npm run lint && + echo '=== Testing ===' && npm test && + echo '=== Building ===' && npm run build && + echo '=== CI Complete ===' || echo '=== CI Failed ===' +" 2>&1 | tee $LOG +``` + +## Best Practices + +1. **Always provide monitoring commands** to users after starting a session +2. **Use descriptive task types** in session names (build, test, deploy, etc.) +3. **Capture initial output** after starting to confirm task began successfully +4. **Use LOG_DIR (defaults to /tmp)** to keep project directories clean +5. **Include cleanup instructions** for when tasks complete +6. **Check session status** before attempting operations +7. **Provide both tail and attach options** for different monitoring preferences +8. **Use tee for logging** to enable both file and real-time capture diff --git a/skills/tmux-task-runner/TODO.md b/skills/tmux-task-runner/TODO.md new file mode 100644 index 0000000..95d14d9 --- /dev/null +++ b/skills/tmux-task-runner/TODO.md @@ -0,0 +1,9 @@ +# TODO + +- [x] Add support for `run --workdir ` by wiring tmux's `-c` flag (fall back to `cd` wrapper if tmux is too old) so tasks can run from alternate directories safely. +- [x] Support per-task environment overrides (`run --env KEY=VALUE`) without forcing users to inline exports. +- [x] Allow overriding log/status directories via environment variables (e.g. `LOG_DIR`, `STATUS_DIR`) and automatically prune old artifacts. +- [x] Record start/end timestamps and duration in the status file for easier post-run analysis. +- [x] Provide a `status` subcommand that summarizes recent sessions (exit code, duration, log path) instead of manually inspecting `.status` files. +- [x] Add a lightweight `tail` helper command that periodically captures pane output for users who want polling without attaching. +- [x] Offer optional completion notifications (desktop notify, sound, etc.) so long-running tasks can signal finish/failure automatically. diff --git a/skills/tmux-task-runner/package.json b/skills/tmux-task-runner/package.json new file mode 100644 index 0000000..d79141b --- /dev/null +++ b/skills/tmux-task-runner/package.json @@ -0,0 +1,19 @@ +{ + "name": "tmux-task-runner-skill", + "version": "1.0.0", + "description": "Claude Code skill for running long tasks in tmux sessions with monitoring", + "main": "run.sh", + "scripts": { + "setup": "bash -c 'command -v tmux &> /dev/null || echo \"Please install tmux: brew install tmux (macOS) or apt-get install tmux (Linux)\"'", + "check": "tmux -V" + }, + "keywords": [ + "tmux", + "background-tasks", + "long-running", + "monitoring", + "claude-skill" + ], + "author": "Claude Code", + "license": "MIT" +} diff --git a/skills/tmux-task-runner/run.sh b/skills/tmux-task-runner/run.sh new file mode 100755 index 0000000..823628f --- /dev/null +++ b/skills/tmux-task-runner/run.sh @@ -0,0 +1,789 @@ +#!/bin/bash +# Tmux Task Runner - Execute commands in managed tmux sessions +# Usage: ./run.sh + +set -e + +# Check if tmux is installed +if ! command -v tmux &> /dev/null; then + echo "Error: tmux is not installed" + echo "Install tmux:" + echo " macOS: brew install tmux" + echo " Ubuntu/Debian: sudo apt-get install tmux" + echo " Fedora/RHEL: sudo dnf install tmux" + exit 1 +fi + +LOG_DIR="${LOG_DIR:-/tmp}" +STATUS_DIR="${STATUS_DIR:-/tmp}" +PRUNE_RETENTION_DAYS="${PRUNE_RETENTION_DAYS:-7}" +STATUS_SUMMARY_LIMIT="${STATUS_SUMMARY_LIMIT:-10}" +TAIL_DEFAULT_LINES="${TAIL_DEFAULT_LINES:-50}" +TAIL_DEFAULT_INTERVAL="${TAIL_DEFAULT_INTERVAL:-2}" + +# Normalize directory paths (strip trailing slashes) +LOG_DIR="${LOG_DIR%/}" +STATUS_DIR="${STATUS_DIR%/}" + +mkdir -p "$LOG_DIR" "$STATUS_DIR" + +# Prune artifacts older than retention window +prune_artifacts() { + local target_dir=$1 + local pattern=$2 + + find "$target_dir" -maxdepth 1 -type f -name "$pattern" -mtime +"$PRUNE_RETENTION_DAYS" -print0 2>/dev/null | xargs -0 rm -f 2>/dev/null || true +} + +format_duration() { + local seconds=$1 + if [ -z "$seconds" ] || [ "$seconds" -lt 0 ] 2>/dev/null; then + echo "n/a" + return + fi + + local hrs=$((seconds / 3600)) + local mins=$(((seconds % 3600) / 60)) + local secs=$((seconds % 60)) + local formatted="" + + if [ "$hrs" -gt 0 ]; then + formatted+="${hrs}h" + fi + + if [ "$mins" -gt 0 ] || [ "$hrs" -gt 0 ]; then + formatted+="${mins}m" + fi + + formatted+="${secs}s" + echo "$formatted" +} + +abbreviate() { + local input=$1 + local max=${2:-60} + if [ -z "$input" ]; then + echo "" + return + fi + if [ "${#input}" -le "$max" ]; then + echo "$input" + else + local truncated="${input:0:$((max - 3))}" + echo "${truncated}..." + fi +} + +load_status() { + local status_file=$1 + local prefix=$2 + + if [ ! -f "$status_file" ]; then + return 1 + fi + + local exit_code="" + local command="" + local started_at_iso="" + local finished_at_iso="" + local duration_seconds="" + local log_file="" + local workdir="" + local env_vars="" + + # shellcheck disable=SC1090 + source "$status_file" + + printf "%s_exit_code=%q\n" "$prefix" "${exit_code:-}" + printf "%s_command=%q\n" "$prefix" "${command:-}" + printf "%s_started_at_iso=%q\n" "$prefix" "${started_at_iso:-}" + printf "%s_finished_at_iso=%q\n" "$prefix" "${finished_at_iso:-}" + printf "%s_duration_seconds=%q\n" "$prefix" "${duration_seconds:-}" + printf "%s_log_file=%q\n" "$prefix" "${log_file:-}" + printf "%s_workdir=%q\n" "$prefix" "${workdir:-}" + printf "%s_env_vars=%q\n" "$prefix" "${env_vars:-}" + return 0 +} + +# Function to run a task in tmux +TMUX_SUPPORTS_C="" + +tmux_supports_c() { + if [ -n "$TMUX_SUPPORTS_C" ]; then + [ "$TMUX_SUPPORTS_C" -eq 1 ] + return + fi + + local test_session="task-tmuxc-test-$$-$RANDOM" + if tmux new-session -d -s "$test_session" -c "$PWD" "true" 2>/dev/null; then + tmux kill-session -t "$test_session" >/dev/null 2>&1 || true + TMUX_SUPPORTS_C=1 + else + TMUX_SUPPORTS_C=0 + fi + + [ "$TMUX_SUPPORTS_C" -eq 1 ] +} + +run_task() { + local task_type=$1 + shift + + local workdir="" + local notify=0 + local -a env_vars=() + local -a command_args=() + + while [ $# -gt 0 ]; do + case "$1" in + --workdir) + if [ $# -lt 2 ]; then + echo "Error: --workdir requires a directory argument" + exit 1 + fi + workdir=$2 + shift 2 + ;; + --env) + if [ $# -lt 2 ]; then + echo "Error: --env requires KEY=VALUE" + exit 1 + fi + env_vars+=("$2") + shift 2 + ;; + --notify) + notify=1 + shift + ;; + --help|-h) + echo "Usage: $0 run [--workdir ] [--env KEY=VALUE ...] [--notify] [--] " + return + ;; + --) + shift + while [ $# -gt 0 ]; do + command_args+=("$1") + shift + done + break + ;; + --*) + echo "Unknown option: $1" + echo "Usage: $0 run [--workdir ] [--env KEY=VALUE ...] [--notify] [--] " + exit 1 + ;; + *) + command_args+=("$1") + shift + ;; + esac + done + + if [ ${#command_args[@]} -eq 0 ]; then + echo "Error: No command provided" + echo "Usage: $0 run [--workdir ] [--env KEY=VALUE ...] [--notify] [--] " + exit 1 + fi + + if [ -n "$workdir" ] && [ ! -d "$workdir" ]; then + echo "Error: workdir not found: $workdir" + exit 1 + fi + + prune_artifacts "$LOG_DIR" "task-*.log" + prune_artifacts "$STATUS_DIR" "task-*.status" + + local session="task-${task_type}-$(date +%s)" + local logfile="$LOG_DIR/${session}.log" + local status_file="$STATUS_DIR/${session}.status" + local resolved_workdir="${workdir:-$PWD}" + + rm -f "$status_file" + + local command_string + command_string=$(printf "%q " "${command_args[@]}") + command_string="${command_string% }" + + local command_display + command_display=$(printf "%s " "${command_args[@]}") + command_display="${command_display% }" + + local command_display_escaped + command_display_escaped=$(printf "%q" "$command_display") + + local workdir_escaped + workdir_escaped=$(printf "%q" "$resolved_workdir") + + local env_exports="" + local env_display="" + local env_display_escaped="" + if [ ${#env_vars[@]} -gt 0 ]; then + local first=1 + for kv in "${env_vars[@]}"; do + if [[ "$kv" != *=* ]]; then + echo "Invalid --env value (expected KEY=VALUE): $kv" + exit 1 + fi + local key=${kv%%=*} + local value=${kv#*=} + if [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then + echo "Invalid environment variable name: $key" + exit 1 + fi + env_exports+="export $key=$(printf "%q" "$value")"$'\n' + if [ $first -eq 1 ]; then + env_display="$key=$value" + first=0 + else + env_display="$env_display; $key=$value" + fi + done + env_display_escaped=$(printf "%q" "$env_display") + fi + + local notify_flag=$notify + local notify_message_success_escaped + local notify_message_failure_escaped + notify_message_success_escaped=$(printf "%q" "[$session] completed successfully") + notify_message_failure_escaped=$(printf "%q" "[$session] failed with exit") + + local tmux_command + tmux_command=$(cat <&1 | tee "$logfile" +exit_code=\${PIPESTATUS[0]} +END_EPOCH=\$(date +%s) +END_ISO=\$(date -u +"%Y-%m-%dT%H:%M:%SZ") +DURATION=\$((END_EPOCH - START_EPOCH)) +if [ \$exit_code -eq 0 ]; then + echo "Task completed successfully." | tee -a "$logfile" +else + echo "Task failed with exit code \$exit_code." | tee -a "$logfile" +fi +{ + printf "exit_code=%q\\n" "\$exit_code" + printf "command=%q\\n" "\$COMMAND_DISPLAY" + printf "started_at_iso=%q\\n" "\$START_ISO" + printf "started_at_epoch=%q\\n" "\$START_EPOCH" + printf "finished_at_iso=%q\\n" "\$END_ISO" + printf "finished_at_epoch=%q\\n" "\$END_EPOCH" + printf "duration_seconds=%q\\n" "\$DURATION" + printf "log_file=%q\\n" "\$LOG_FILE" + printf "workdir=%q\\n" "\$WORKDIR_VALUE" + printf "env_vars=%q\\n" "\$ENV_DISPLAY" +} > "$STATUS_FILE" +if [ \$NOTIFY_FLAG -eq 1 ]; then + message="" + if [ \$exit_code -eq 0 ]; then + message="$NOTIFY_MESSAGE_SUCCESS" + else + message="$NOTIFY_MESSAGE_FAILURE \$exit_code" + fi + if command -v notify-send >/dev/null 2>&1; then + notify-send "tmux-task-runner" "\$message" + elif command -v osascript >/dev/null 2>&1; then + osascript -e "display notification \"\$message\" with title \"tmux-task-runner\"" + elif command -v terminal-notifier >/dev/null 2>&1; then + terminal-notifier -title "tmux-task-runner" -message "\$message" + fi +fi +exit \$exit_code +EOF +) + + local wrapped_command + wrapped_command=$(printf "%q" "$tmux_command") + + local tmux_args=(-d -s "$session") + if [ -n "$workdir" ] && tmux_supports_c; then + tmux_args+=(-c "$workdir") + fi + + if ! tmux new-session "${tmux_args[@]}" bash -lc "$wrapped_command"; then + if [ -n "$workdir" ]; then + local cd_prefix command_with_cd + cd_prefix=$(printf "cd %q" "$workdir") + command_with_cd=$(printf "%s\n%s" "$cd_prefix" "$tmux_command") + wrapped_command=$(printf "%q" "$command_with_cd") + if ! tmux new-session -d -s "$session" bash -lc "$wrapped_command"; then + echo "Failed to create tmux session (workdir: $workdir)" >&2 + exit 1 + fi + else + echo "Failed to create tmux session" >&2 + exit 1 + fi + fi + + echo "Task started in tmux session" + echo "Session: $session" + echo "Log file: $logfile" + echo "Status file: $status_file" + echo "Workdir: $resolved_workdir" + if [ -n "$env_display" ]; then + echo "Environment overrides: $env_display" + fi + if [ "$notify" -eq 1 ]; then + echo "Notifications: enabled (best effort)" + fi + echo "Monitoring commands:" + echo " tail -f $logfile # Follow log output" + echo " tmux attach-session -t $session # Attach to session (Ctrl+b, d to detach)" + echo " tmux capture-pane -t $session -p # View current output" + echo " $0 tail $session # Poll output without attaching" + echo " tmux kill-session -t $session # Stop task and close session" + + sleep 1 + + echo "Initial output:" + if tmux has-session -t "$session" 2>/dev/null; then + tmux capture-pane -t "$session" -p -S -20 2>/dev/null || echo "(Waiting for output...)" + else + echo "Session ended immediately - check if command is valid" + [ -f "$logfile" ] && tail -20 "$logfile" + fi +} + +# Function to check session status +check_session() { + local session=$1 + local status_file="$STATUS_DIR/${session}.status" + local logfile="$LOG_DIR/${session}.log" + + if [ -z "$session" ]; then + echo "Error: No session name provided" + echo "Usage: $0 check " + exit 1 + fi + + local status_data="" + local status_exit_code="" + local status_command="" + local status_started_at_iso="" + local status_finished_at_iso="" + local status_duration_seconds="" + local status_log_file="" + local status_workdir="" + local status_env_vars="" + + if status_data=$(load_status "$status_file" status 2>/dev/null); then + eval "$status_data" + fi + + if tmux has-session -t "$session" 2>/dev/null; then + echo "Session '$session' is running" + if [ -n "$status_started_at_iso" ]; then + echo "Started at: $status_started_at_iso" + fi + if [ -n "$status_workdir" ]; then + echo "Working directory: $status_workdir" + fi + if [ -n "$status_env_vars" ]; then + echo "Environment overrides: $status_env_vars" + fi + [ -n "$status_command" ] && echo "Command: $status_command" + echo "Recent output (last 30 lines):" + tmux capture-pane -t "$session" -p -S -30 + echo "Session info:" + tmux list-sessions | grep "^$session" || true + else + echo "Session '$session' has completed or doesn't exist" + + if [ -n "$status_exit_code" ]; then + if [ "$status_exit_code" -eq 0 ] 2>/dev/null; then + echo "Recorded exit status: success (0)" + else + echo "Recorded exit status: failure ($status_exit_code)" + fi + [ -n "$status_command" ] && echo "Command: $status_command" + [ -n "$status_workdir" ] && echo "Working directory: $status_workdir" + if [ -n "$status_env_vars" ]; then + echo "Environment overrides: $status_env_vars" + fi + if [ -n "$status_started_at_iso" ]; then + echo "Started at: $status_started_at_iso" + fi + if [ -n "$status_finished_at_iso" ]; then + echo "Finished at: $status_finished_at_iso" + fi + if [ -n "$status_duration_seconds" ]; then + echo "Duration: $(format_duration "$status_duration_seconds")" + fi + fi + + if [ -f "$logfile" ]; then + echo "Log file found: $logfile" + echo "Last 30 lines of log:" + tail -30 "$logfile" + fi + fi +} + +# Function to list all task sessions +print_status_summary() { + local limit=${1:-$STATUS_SUMMARY_LIMIT} + local status_paths + + status_paths=$(ls -t "$STATUS_DIR"/task-*.status 2>/dev/null | head -n "$limit" || true) + + if [ -z "$status_paths" ]; then + echo " No status files found" + return + fi + + while IFS= read -r status_path; do + [ -z "$status_path" ] && continue + local summary_data="" + if summary_data=$(load_status "$status_path" summary 2>/dev/null); then + eval "$summary_data" + local session_name outcome="unknown" duration_display="n/a" command_short="" + session_name=$(basename "$status_path" .status) + if [ -n "$summary_exit_code" ]; then + if [ "$summary_exit_code" -eq 0 ] 2>/dev/null; then + outcome="success" + else + outcome="failed ($summary_exit_code)" + fi + fi + if [ -n "$summary_duration_seconds" ]; then + duration_display=$(format_duration "$summary_duration_seconds") + fi + command_short=$(abbreviate "${summary_command:-}" 70) + + printf " %-35s | %-12s | %s" "$session_name" "$outcome" "$duration_display" + if [ -n "$summary_started_at_iso" ]; then + printf " | %s" "$summary_started_at_iso" + fi + printf "\n" + if [ -n "$command_short" ]; then + printf " cmd: %s\n" "$command_short" + fi + fi + done <<< "$status_paths" +} + +list_tasks() { + echo "Active task sessions:" + local sessions + sessions=$(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^task-" || true) + + if [ -z "$sessions" ]; then + echo " No active task sessions" + else + while IFS= read -r s; do + [ -z "$s" ] && continue + local status_path="$STATUS_DIR/${s}.status" + local display=" $s" + local session_data="" + if session_data=$(load_status "$status_path" current 2>/dev/null); then + eval "$session_data" + if [ -n "$current_started_at_iso" ]; then + display="$display | started $current_started_at_iso" + fi + if [ -n "$current_workdir" ]; then + display="$display | wd $current_workdir" + fi + fi + echo "$display" + done <<< "$sessions" + fi + + echo "Recent log files (from $LOG_DIR):" + local log_glob=("$LOG_DIR"/task-*.log) + if [ "${log_glob[0]}" = "$LOG_DIR/task-*.log" ]; then + echo " No log files found" + else + ls -t "${log_glob[@]}" 2>/dev/null | head -n 10 | while read -r log; do + [ -z "$log" ] && continue + echo " $log" + done + fi + + echo "Recent task statuses (newest first):" + print_status_summary "$STATUS_SUMMARY_LIMIT" +} + +# Function to kill task sessions +kill_task() { + local session=$1 + + if [ -z "$session" ]; then + echo "Error: No session name provided" + echo "Usage: $0 kill " + echo " $0 kill all # Kill all task sessions" + exit 1 + fi + + if [ "$session" = "all" ]; then + local sessions + sessions=$(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^task-" || true) + + if [ -z "$sessions" ]; then + echo "No task sessions to kill" + else + echo "Killing all task sessions:" + while IFS= read -r s; do + [ -z "$s" ] && continue + tmux kill-session -t "$s" 2>/dev/null || true + rm -f "$STATUS_DIR/${s}.status" + echo " Killed: $s" + done <<< "$sessions" + fi + else + if tmux has-session -t "$session" 2>/dev/null; then + tmux kill-session -t "$session" + echo "Killed session: $session" + rm -f "$STATUS_DIR/${session}.status" + else + echo "Session not found: $session" + exit 1 + fi + fi +} + +# Function to attach to session +attach_session() { + local session=$1 + + if [ -z "$session" ]; then + echo "Error: No session name provided" + echo "Usage: $0 attach " + exit 1 + fi + + if tmux has-session -t "$session" 2>/dev/null; then + echo "Attaching to session: $session" + echo "Press Ctrl+b then d to detach without killing the session" + sleep 1 + tmux attach-session -t "$session" + else + echo "Session not found: $session" + exit 1 + fi +} + +status_command() { + local target=$1 + + if [ -z "$target" ]; then + echo "Recent task statuses (limit $STATUS_SUMMARY_LIMIT):" + print_status_summary "$STATUS_SUMMARY_LIMIT" + return + fi + + local status_file="$STATUS_DIR/${target}.status" + if [ ! -f "$status_file" ]; then + echo "Status file not found for session: $target" + echo "Expected at: $status_file" + exit 1 + fi + + local detail_data="" + if ! detail_data=$(load_status "$status_file" detail 2>/dev/null); then + echo "Unable to read status file: $status_file" + exit 1 + fi + eval "$detail_data" + + echo "Session: $target" + [ -n "$detail_command" ] && echo "Command: $detail_command" + [ -n "$detail_workdir" ] && echo "Workdir: $detail_workdir" + if [ -n "$detail_env_vars" ]; then + echo "Environment overrides: $detail_env_vars" + fi + [ -n "$detail_started_at_iso" ] && echo "Started: $detail_started_at_iso" + [ -n "$detail_finished_at_iso" ] && echo "Finished: $detail_finished_at_iso" + if [ -n "$detail_duration_seconds" ]; then + echo "Duration: $(format_duration "$detail_duration_seconds")" + fi + + if [ -n "$detail_exit_code" ]; then + if [ "$detail_exit_code" -eq 0 ] 2>/dev/null; then + echo "Exit code: $detail_exit_code (success)" + else + echo "Exit code: $detail_exit_code (failure)" + fi + else + echo "Exit code: unknown" + fi + + if [ -n "$detail_log_file" ]; then + echo "Log file: $detail_log_file" + if [ -f "$detail_log_file" ]; then + echo "Last 20 lines:" + tail -20 "$detail_log_file" + else + echo "Log file not found on disk." + fi + fi +} + +tail_session() { + local session="" + local interval=$TAIL_DEFAULT_INTERVAL + local lines=$TAIL_DEFAULT_LINES + + while [ $# -gt 0 ]; do + case "$1" in + --interval) + if [ $# -lt 2 ]; then + echo "Error: --interval requires seconds" + exit 1 + fi + interval=$2 + shift 2 + ;; + --lines) + if [ $# -lt 2 ]; then + echo "Error: --lines requires a value" + exit 1 + fi + lines=$2 + shift 2 + ;; + --help|-h) + echo "Usage: $0 tail [--interval seconds] [--lines count]" + return + ;; + *) + if [ -z "$session" ]; then + session=$1 + shift + else + echo "Unexpected argument: $1" + echo "Usage: $0 tail [--interval seconds] [--lines count]" + exit 1 + fi + ;; + esac + done + + if [ -z "$session" ]; then + echo "Error: No session name provided" + echo "Usage: $0 tail [--interval seconds] [--lines count]" + exit 1 + fi + + echo "Tailing session '$session' (Ctrl+C to stop)" + echo "Interval: ${interval}s, Lines: $lines" + + while true; do + if tmux has-session -t "$session" 2>/dev/null; then + echo "=== $(date '+%Y-%m-%d %H:%M:%S') ===" + tmux capture-pane -t "$session" -p -S -"$lines" + echo "" + sleep "$interval" + else + echo "Session '$session' is no longer running." + local logfile="$LOG_DIR/${session}.log" + if [ -f "$logfile" ]; then + echo "Log file available at: $logfile" + echo "Last $lines lines:" + tail -"$lines" "$logfile" + fi + break + fi + done +} + +# Function to show help +show_help() { + cat << EOF +Tmux Task Runner + +Execute long-running tasks in managed tmux sessions with logging. + +Usage: + $0 run [options] + $0 check Check status and output of a session + $0 list List all active task sessions and logs + $0 status [session-name] Show recent session history or a specific session + $0 tail Poll output from a session without attaching + $0 attach Attach to an interactive session + $0 kill Kill a specific session + $0 kill all Kill all task sessions + $0 help Show this help message + +Examples: + $0 run build "npm run build" + $0 run test --notify --env NODE_ENV=ci "npm test -- --runInBand" + $0 run server --workdir ./api "npm run dev" + $0 check task-build-1729519263 + $0 attach task-server-1729519263 + $0 list + $0 status + $0 status task-build-1729519263 + $0 tail task-build-1729519263 --interval 5 --lines 100 + $0 kill task-build-1729519263 + +Task Types: + Common task types: build, test, deploy, server, script, job + Use descriptive names to easily identify sessions later. + +Monitoring: + tail -f $LOG_DIR/task-NAME.log Follow log output in real-time + tmux attach-session -t NAME Interactive session access + tmux capture-pane -t NAME -p Quick output snapshot + $0 tail NAME Periodic polling without attaching + +Run Options: + --workdir Run the command from a specific directory + --env KEY=VALUE Export additional environment variables (repeatable) + --notify Attempt desktop notification on completion + -- Treat the remaining arguments as the command + +Current Directories: + Logs: $LOG_DIR + Status: $STATUS_DIR + +EOF +} + +# Main command handler +case "$1" in + run) + shift + run_task "$@" + ;; + check) + check_session "$2" + ;; + list) + list_tasks + ;; + status) + shift + status_command "$@" + ;; + attach) + attach_session "$2" + ;; + tail) + shift + tail_session "$@" + ;; + kill) + kill_task "$2" + ;; + help|--help|-h|"") + show_help + ;; + *) + echo "Unknown command: $1" + show_help + exit 1 + ;; +esac