Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tmux-task-runner
|
||||||
|
|
||||||
|
Run build processes, test suites, deployments, and development servers in monitored tmux sessions with persistent logging
|
||||||
64
plugin.lock.json
Normal file
64
plugin.lock.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
325
skills/tmux-task-runner/EXAMPLES.md
Normal file
325
skills/tmux-task-runner/EXAMPLES.md
Normal file
@@ -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 <session>`).
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
154
skills/tmux-task-runner/README.md
Normal file
154
skills/tmux-task-runner/README.md
Normal file
@@ -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 <session>` and kill it if needed: `./run.sh kill <session>`
|
||||||
430
skills/tmux-task-runner/SKILL.md
Normal file
430
skills/tmux-task-runner/SKILL.md
Normal file
@@ -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 <path>` 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 <session>` 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 <session> [--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
|
||||||
9
skills/tmux-task-runner/TODO.md
Normal file
9
skills/tmux-task-runner/TODO.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
- [x] Add support for `run --workdir <path>` 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.
|
||||||
19
skills/tmux-task-runner/package.json
Normal file
19
skills/tmux-task-runner/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
789
skills/tmux-task-runner/run.sh
Executable file
789
skills/tmux-task-runner/run.sh
Executable file
@@ -0,0 +1,789 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Tmux Task Runner - Execute commands in managed tmux sessions
|
||||||
|
# Usage: ./run.sh <task-type> <command...>
|
||||||
|
|
||||||
|
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 <task-type> [--workdir <dir>] [--env KEY=VALUE ...] [--notify] [--] <command...>"
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
command_args+=("$1")
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
echo "Usage: $0 run <task-type> [--workdir <dir>] [--env KEY=VALUE ...] [--notify] [--] <command...>"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
command_args+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#command_args[@]} -eq 0 ]; then
|
||||||
|
echo "Error: No command provided"
|
||||||
|
echo "Usage: $0 run <task-type> [--workdir <dir>] [--env KEY=VALUE ...] [--notify] [--] <command...>"
|
||||||
|
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 <<EOF
|
||||||
|
set -o pipefail
|
||||||
|
SESSION_NAME="$session"
|
||||||
|
LOG_FILE="$logfile"
|
||||||
|
STATUS_FILE="$status_file"
|
||||||
|
COMMAND_DISPLAY=$command_display_escaped
|
||||||
|
ENV_DISPLAY=${env_display_escaped:-''}
|
||||||
|
WORKDIR_VALUE=$workdir_escaped
|
||||||
|
NOTIFY_FLAG=$notify_flag
|
||||||
|
NOTIFY_MESSAGE_SUCCESS=$notify_message_success_escaped
|
||||||
|
NOTIFY_MESSAGE_FAILURE=$notify_message_failure_escaped
|
||||||
|
${env_exports}
|
||||||
|
START_EPOCH=\$(date +%s)
|
||||||
|
START_ISO=\$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
${command_string} 2>&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 <session-name>"
|
||||||
|
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 <session-name>"
|
||||||
|
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 <session-name>"
|
||||||
|
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 <session-name> [--interval seconds] [--lines count]"
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -z "$session" ]; then
|
||||||
|
session=$1
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
echo "Unexpected argument: $1"
|
||||||
|
echo "Usage: $0 tail <session-name> [--interval seconds] [--lines count]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$session" ]; then
|
||||||
|
echo "Error: No session name provided"
|
||||||
|
echo "Usage: $0 tail <session-name> [--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 <task-type> [options] <command...>
|
||||||
|
$0 check <session-name> 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 <session-name> Poll output from a session without attaching
|
||||||
|
$0 attach <session-name> Attach to an interactive session
|
||||||
|
$0 kill <session-name> 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 <dir> 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
|
||||||
Reference in New Issue
Block a user