taskmux 0.2.4__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskmux
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Modern tmux-based task manager for LLM development tools
5
5
  Project-URL: Homepage, https://github.com/nc9/taskmux
6
6
  Project-URL: Repository, https://github.com/nc9/taskmux
@@ -104,6 +104,67 @@ command = "docker compose up postgres"
104
104
  auto_start = false
105
105
  ```
106
106
 
107
+ ## Full Example
108
+
109
+ A full-stack app with a database, API server, and frontend — using health checks to ensure each service is ready before starting its dependents:
110
+
111
+ ```toml
112
+ name = "fullstack-app"
113
+
114
+ [tasks.db]
115
+ command = "docker compose up postgres redis"
116
+ health_check = "pg_isready -h localhost -p 5432"
117
+ health_interval = 3
118
+
119
+ [tasks.migrate]
120
+ command = "python manage.py migrate && echo 'done' && sleep infinity"
121
+ cwd = "apps/api"
122
+ depends_on = ["db"]
123
+ health_check = "test -f .migrate-complete"
124
+
125
+ [tasks.api]
126
+ command = "python manage.py runserver 0.0.0.0:8000"
127
+ cwd = "apps/api"
128
+ depends_on = ["migrate"]
129
+ health_check = "curl -sf http://localhost:8000/health"
130
+
131
+ [tasks.worker]
132
+ command = "celery -A myapp worker -l info"
133
+ cwd = "apps/api"
134
+ depends_on = ["db"]
135
+
136
+ [tasks.web]
137
+ command = "bun dev"
138
+ cwd = "apps/web"
139
+ depends_on = ["api"]
140
+ health_check = "curl -sf http://localhost:3000"
141
+
142
+ [tasks.storybook]
143
+ command = "bun storybook"
144
+ cwd = "apps/web"
145
+ auto_start = false
146
+ ```
147
+
148
+ What happens on `taskmux start`:
149
+
150
+ 1. **db** starts first (no dependencies)
151
+ 2. **migrate** and **worker** wait for db's health check (`pg_isready`) to pass
152
+ 3. **api** waits for migrate's health check
153
+ 4. **web** waits for api's health check (`curl localhost:8000/health`)
154
+ 5. **storybook** is skipped (`auto_start = false`) — start it manually with `taskmux start storybook`
155
+
156
+ ```bash
157
+ taskmux start # Starts everything in dependency order
158
+ taskmux logs # Interleaved logs from all tasks
159
+ taskmux logs -g "ERROR" # Grep all tasks for errors
160
+ taskmux logs api # Logs from just the API
161
+ taskmux logs -f api # Follow API logs live
162
+ taskmux health # Health check table
163
+ taskmux inspect api # JSON state for a single task
164
+ taskmux restart worker # Restart just the worker
165
+ taskmux start storybook # Start a manual task
166
+ ```
167
+
107
168
  ## Commands
108
169
 
109
170
  ```bash
@@ -124,10 +185,13 @@ taskmux remove <task> # Remove task from config
124
185
  taskmux inspect <task> # JSON task state (pid, command, health)
125
186
 
126
187
  # Logs
127
- taskmux logs <task> # Show recent logs
128
- taskmux logs -f <task> # Follow logs live
129
- taskmux logs -n 100 <task> # Last N lines
130
- taskmux logs <task> -g "error" # Search logs with grep
188
+ taskmux logs # Interleaved logs from all tasks
189
+ taskmux logs <task> # Show recent logs for a task
190
+ taskmux logs -f # Attach to session (switch windows with tmux keybinds)
191
+ taskmux logs -f <task> # Follow a task's logs live
192
+ taskmux logs -n 200 <task> # Last N lines
193
+ taskmux logs -g "error" # Search all tasks
194
+ taskmux logs <task> -g "error" # Search one task
131
195
  taskmux logs <task> -g "error" -C 5 # Grep with context lines
132
196
 
133
197
  # Init
@@ -161,12 +225,20 @@ after_stop = "echo done"
161
225
 
162
226
  [tasks.server]
163
227
  command = "python manage.py runserver"
228
+ cwd = "apps/api"
229
+ health_check = "curl -sf http://localhost:8000/health"
230
+ depends_on = ["db"]
164
231
 
165
232
  [tasks.server.hooks]
166
233
  before_start = "python manage.py migrate"
167
234
 
235
+ [tasks.db]
236
+ command = "docker compose up postgres"
237
+ health_check = "pg_isready -h localhost"
238
+
168
239
  [tasks.worker]
169
240
  command = "celery worker -A myapp"
241
+ depends_on = ["db"]
170
242
 
171
243
  [tasks.tailwind]
172
244
  command = "npx tailwindcss -w"
@@ -185,8 +257,31 @@ auto_start = false
185
257
  | `hooks.after_stop` | — | Run after stopping tasks |
186
258
  | `tasks.<name>.command` | — | Shell command to run |
187
259
  | `tasks.<name>.auto_start` | `true` | Start with `taskmux start` |
260
+ | `tasks.<name>.cwd` | — | Working directory for the task |
261
+ | `tasks.<name>.health_check` | — | Shell command to check health (exit 0 = healthy) |
262
+ | `tasks.<name>.health_interval` | `10` | Seconds between health checks |
263
+ | `tasks.<name>.health_timeout` | `5` | Seconds before health check times out |
264
+ | `tasks.<name>.health_retries` | `3` | Consecutive failures before "unhealthy" |
265
+ | `tasks.<name>.depends_on` | `[]` | Task names that must be healthy before this task starts |
188
266
  | `tasks.<name>.hooks.*` | — | Per-task lifecycle hooks (same fields as global) |
189
267
 
268
+ ### Dependency Ordering
269
+
270
+ Tasks with `depends_on` are started in topological order. Before starting a task, taskmux waits for each dependency's health check to pass (up to `health_retries * health_interval` seconds). If a dependency never becomes healthy, the dependent task is skipped with a warning.
271
+
272
+ Circular dependencies and references to nonexistent tasks are rejected at config load time.
273
+
274
+ When starting a single task with `taskmux start <task>`, dependencies are not auto-started — you get a warning if they aren't running.
275
+
276
+ ### Health Checks
277
+
278
+ If `health_check` is set, taskmux runs it as a shell command. Exit code 0 means healthy. If not set, taskmux falls back to checking if the tmux pane has a running process (not just a shell prompt).
279
+
280
+ Health checks are used by:
281
+ - `taskmux health` — shows a table of all task health
282
+ - `taskmux start` — waits for dependencies to be healthy before starting dependents
283
+ - `taskmux daemon` — continuously monitors and auto-restarts unhealthy tasks
284
+
190
285
  ### Hook Cascade
191
286
 
192
287
  Hooks fire in this order:
@@ -212,14 +307,17 @@ Use `--defaults` to skip prompts (CI/automation).
212
307
 
213
308
  ```json
214
309
  {
215
- "name": "server",
216
- "command": "npm run dev",
310
+ "name": "api",
311
+ "command": "python manage.py runserver 0.0.0.0:8000",
217
312
  "auto_start": true,
313
+ "cwd": "apps/api",
314
+ "health_check": "curl -sf http://localhost:8000/health",
315
+ "depends_on": ["db"],
218
316
  "running": true,
219
317
  "healthy": true,
220
318
  "pid": "12345",
221
- "pane_current_command": "node",
222
- "pane_current_path": "/home/user/project",
319
+ "pane_current_command": "python",
320
+ "pane_current_path": "/home/user/project/apps/api",
223
321
  "window_id": "@1",
224
322
  "pane_id": "%1"
225
323
  }
@@ -69,6 +69,67 @@ command = "docker compose up postgres"
69
69
  auto_start = false
70
70
  ```
71
71
 
72
+ ## Full Example
73
+
74
+ A full-stack app with a database, API server, and frontend — using health checks to ensure each service is ready before starting its dependents:
75
+
76
+ ```toml
77
+ name = "fullstack-app"
78
+
79
+ [tasks.db]
80
+ command = "docker compose up postgres redis"
81
+ health_check = "pg_isready -h localhost -p 5432"
82
+ health_interval = 3
83
+
84
+ [tasks.migrate]
85
+ command = "python manage.py migrate && echo 'done' && sleep infinity"
86
+ cwd = "apps/api"
87
+ depends_on = ["db"]
88
+ health_check = "test -f .migrate-complete"
89
+
90
+ [tasks.api]
91
+ command = "python manage.py runserver 0.0.0.0:8000"
92
+ cwd = "apps/api"
93
+ depends_on = ["migrate"]
94
+ health_check = "curl -sf http://localhost:8000/health"
95
+
96
+ [tasks.worker]
97
+ command = "celery -A myapp worker -l info"
98
+ cwd = "apps/api"
99
+ depends_on = ["db"]
100
+
101
+ [tasks.web]
102
+ command = "bun dev"
103
+ cwd = "apps/web"
104
+ depends_on = ["api"]
105
+ health_check = "curl -sf http://localhost:3000"
106
+
107
+ [tasks.storybook]
108
+ command = "bun storybook"
109
+ cwd = "apps/web"
110
+ auto_start = false
111
+ ```
112
+
113
+ What happens on `taskmux start`:
114
+
115
+ 1. **db** starts first (no dependencies)
116
+ 2. **migrate** and **worker** wait for db's health check (`pg_isready`) to pass
117
+ 3. **api** waits for migrate's health check
118
+ 4. **web** waits for api's health check (`curl localhost:8000/health`)
119
+ 5. **storybook** is skipped (`auto_start = false`) — start it manually with `taskmux start storybook`
120
+
121
+ ```bash
122
+ taskmux start # Starts everything in dependency order
123
+ taskmux logs # Interleaved logs from all tasks
124
+ taskmux logs -g "ERROR" # Grep all tasks for errors
125
+ taskmux logs api # Logs from just the API
126
+ taskmux logs -f api # Follow API logs live
127
+ taskmux health # Health check table
128
+ taskmux inspect api # JSON state for a single task
129
+ taskmux restart worker # Restart just the worker
130
+ taskmux start storybook # Start a manual task
131
+ ```
132
+
72
133
  ## Commands
73
134
 
74
135
  ```bash
@@ -89,10 +150,13 @@ taskmux remove <task> # Remove task from config
89
150
  taskmux inspect <task> # JSON task state (pid, command, health)
90
151
 
91
152
  # Logs
92
- taskmux logs <task> # Show recent logs
93
- taskmux logs -f <task> # Follow logs live
94
- taskmux logs -n 100 <task> # Last N lines
95
- taskmux logs <task> -g "error" # Search logs with grep
153
+ taskmux logs # Interleaved logs from all tasks
154
+ taskmux logs <task> # Show recent logs for a task
155
+ taskmux logs -f # Attach to session (switch windows with tmux keybinds)
156
+ taskmux logs -f <task> # Follow a task's logs live
157
+ taskmux logs -n 200 <task> # Last N lines
158
+ taskmux logs -g "error" # Search all tasks
159
+ taskmux logs <task> -g "error" # Search one task
96
160
  taskmux logs <task> -g "error" -C 5 # Grep with context lines
97
161
 
98
162
  # Init
@@ -126,12 +190,20 @@ after_stop = "echo done"
126
190
 
127
191
  [tasks.server]
128
192
  command = "python manage.py runserver"
193
+ cwd = "apps/api"
194
+ health_check = "curl -sf http://localhost:8000/health"
195
+ depends_on = ["db"]
129
196
 
130
197
  [tasks.server.hooks]
131
198
  before_start = "python manage.py migrate"
132
199
 
200
+ [tasks.db]
201
+ command = "docker compose up postgres"
202
+ health_check = "pg_isready -h localhost"
203
+
133
204
  [tasks.worker]
134
205
  command = "celery worker -A myapp"
206
+ depends_on = ["db"]
135
207
 
136
208
  [tasks.tailwind]
137
209
  command = "npx tailwindcss -w"
@@ -150,8 +222,31 @@ auto_start = false
150
222
  | `hooks.after_stop` | — | Run after stopping tasks |
151
223
  | `tasks.<name>.command` | — | Shell command to run |
152
224
  | `tasks.<name>.auto_start` | `true` | Start with `taskmux start` |
225
+ | `tasks.<name>.cwd` | — | Working directory for the task |
226
+ | `tasks.<name>.health_check` | — | Shell command to check health (exit 0 = healthy) |
227
+ | `tasks.<name>.health_interval` | `10` | Seconds between health checks |
228
+ | `tasks.<name>.health_timeout` | `5` | Seconds before health check times out |
229
+ | `tasks.<name>.health_retries` | `3` | Consecutive failures before "unhealthy" |
230
+ | `tasks.<name>.depends_on` | `[]` | Task names that must be healthy before this task starts |
153
231
  | `tasks.<name>.hooks.*` | — | Per-task lifecycle hooks (same fields as global) |
154
232
 
233
+ ### Dependency Ordering
234
+
235
+ Tasks with `depends_on` are started in topological order. Before starting a task, taskmux waits for each dependency's health check to pass (up to `health_retries * health_interval` seconds). If a dependency never becomes healthy, the dependent task is skipped with a warning.
236
+
237
+ Circular dependencies and references to nonexistent tasks are rejected at config load time.
238
+
239
+ When starting a single task with `taskmux start <task>`, dependencies are not auto-started — you get a warning if they aren't running.
240
+
241
+ ### Health Checks
242
+
243
+ If `health_check` is set, taskmux runs it as a shell command. Exit code 0 means healthy. If not set, taskmux falls back to checking if the tmux pane has a running process (not just a shell prompt).
244
+
245
+ Health checks are used by:
246
+ - `taskmux health` — shows a table of all task health
247
+ - `taskmux start` — waits for dependencies to be healthy before starting dependents
248
+ - `taskmux daemon` — continuously monitors and auto-restarts unhealthy tasks
249
+
155
250
  ### Hook Cascade
156
251
 
157
252
  Hooks fire in this order:
@@ -177,14 +272,17 @@ Use `--defaults` to skip prompts (CI/automation).
177
272
 
178
273
  ```json
179
274
  {
180
- "name": "server",
181
- "command": "npm run dev",
275
+ "name": "api",
276
+ "command": "python manage.py runserver 0.0.0.0:8000",
182
277
  "auto_start": true,
278
+ "cwd": "apps/api",
279
+ "health_check": "curl -sf http://localhost:8000/health",
280
+ "depends_on": ["db"],
183
281
  "running": true,
184
282
  "healthy": true,
185
283
  "pid": "12345",
186
- "pane_current_command": "node",
187
- "pane_current_path": "/home/user/project",
284
+ "pane_current_command": "python",
285
+ "pane_current_path": "/home/user/project/apps/api",
188
286
  "window_id": "@1",
189
287
  "pane_id": "%1"
190
288
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskmux"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "Modern tmux-based task manager for LLM development tools"
5
5
  readme = "README.md"
6
6
  license-file = "LICENSE"
@@ -8,10 +8,27 @@ from collections import deque
8
8
  from datetime import datetime
9
9
 
10
10
  import libtmux
11
+ from rich.console import Console
12
+ from rich.markup import escape
11
13
 
12
14
  from .hooks import runHook
13
15
  from .models import TaskmuxConfig
14
16
 
17
+ TASK_COLORS = ["cyan", "green", "yellow", "magenta", "blue", "red"]
18
+
19
+
20
+ def _find_new_lines(current: list[str], prev_tail: list[str]) -> list[str]:
21
+ """Return lines in current that are new since prev_tail."""
22
+ if not prev_tail:
23
+ return current
24
+ target = prev_tail[-1]
25
+ for i in range(len(current) - 1, -1, -1):
26
+ if current[i] == target:
27
+ ctx = min(len(prev_tail), i + 1)
28
+ if current[i - ctx + 1 : i + 1] == prev_tail[-ctx:]:
29
+ return current[i + 1 :]
30
+ return current # no match, prev scrolled away — return all
31
+
15
32
 
16
33
  class TmuxManager:
17
34
  """Manages tmux sessions and tasks using libtmux API."""
@@ -433,6 +450,54 @@ class TmuxManager:
433
450
  info["healthy"] = self.is_task_healthy(task_name)
434
451
  return info
435
452
 
453
+ def _tail_panes(
454
+ self,
455
+ panes: list[tuple[str, libtmux.Pane, str]],
456
+ lines: int = 100,
457
+ grep: str | None = None,
458
+ ) -> None:
459
+ """Poll capture-pane and print new lines with colored task prefixes."""
460
+ console = Console()
461
+ state: dict[str, list[str]] = {}
462
+
463
+ try:
464
+ while True:
465
+ for task_name, pane, color in panes:
466
+ output = pane.cmd("capture-pane", "-p", "-S", f"-{lines}").stdout
467
+ while output and not output[-1].strip():
468
+ output.pop()
469
+
470
+ prev = state.get(task_name, [])
471
+ new = _find_new_lines(output, prev)
472
+
473
+ if grep:
474
+ new = [ln for ln in new if grep.lower() in ln.lower()]
475
+
476
+ for line in new:
477
+ prefix = escape(f"[{task_name}]")
478
+ console.print(f"[{color}]{prefix}[/{color}] {escape(line)}")
479
+
480
+ if output:
481
+ state[task_name] = output[-50:]
482
+
483
+ time.sleep(0.5)
484
+ except KeyboardInterrupt:
485
+ console.print("\n[dim]Stopped following logs[/dim]")
486
+
487
+ def _collect_panes(self, task_names: list[str]) -> list[tuple[str, libtmux.Pane, str]]:
488
+ """Collect (name, pane, color) tuples for running tasks."""
489
+ sess = self._get_session()
490
+ result: list[tuple[str, libtmux.Pane, str]] = []
491
+ for i, name in enumerate(task_names):
492
+ window = sess.windows.get(window_name=name, default=None)
493
+ if not window:
494
+ continue
495
+ pane = window.active_pane
496
+ if pane:
497
+ color = TASK_COLORS[i % len(TASK_COLORS)]
498
+ result.append((name, pane, color))
499
+ return result
500
+
436
501
  def show_logs(
437
502
  self,
438
503
  task_name: str | None,
@@ -461,8 +526,9 @@ class TmuxManager:
461
526
  return
462
527
 
463
528
  if follow:
464
- window.select_window()
465
- sess.attach()
529
+ panes = self._collect_panes([task_name])
530
+ if panes:
531
+ self._tail_panes(panes, lines=lines, grep=grep)
466
532
  else:
467
533
  pane = window.active_pane
468
534
  if pane:
@@ -482,26 +548,33 @@ class TmuxManager:
482
548
  ) -> None:
483
549
  """Show logs from all running tasks."""
484
550
  sess = self._get_session()
551
+ console = Console()
552
+ task_names = list(self.config.tasks.keys())
485
553
 
486
554
  if follow:
487
- sess.attach()
555
+ panes = self._collect_panes(task_names)
556
+ if panes:
557
+ self._tail_panes(panes, lines=lines, grep=grep)
488
558
  return
489
559
 
490
- for task_name in self.config.tasks:
560
+ for i, task_name in enumerate(task_names):
491
561
  window = sess.windows.get(window_name=task_name, default=None)
492
562
  if not window:
493
563
  continue
494
564
  pane = window.active_pane
495
565
  if not pane:
496
566
  continue
567
+ color = TASK_COLORS[i % len(TASK_COLORS)]
497
568
  output = pane.cmd("capture-pane", "-p", "-S", f"-{lines}").stdout
498
569
  if grep:
499
570
  matching = [line for line in output if grep.lower() in line.lower()]
500
571
  for line in matching:
501
- print(f"[{task_name}] {line}")
572
+ prefix = escape(f"[{task_name}]")
573
+ console.print(f"[{color}]{prefix}[/{color}] {escape(line)}")
502
574
  else:
503
575
  for line in output:
504
- print(f"[{task_name}] {line}")
576
+ prefix = escape(f"[{task_name}]")
577
+ console.print(f"[{color}]{prefix}[/{color}] {escape(line)}")
505
578
 
506
579
  def list_tasks(self) -> None:
507
580
  """List all tasks and their status"""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes