tmux-team 2.1.0 → 3.0.0-alpha.1

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,723 +0,0 @@
1
- // ─────────────────────────────────────────────────────────────
2
- // PM Commands - project management CLI
3
- // ─────────────────────────────────────────────────────────────
4
-
5
- import type { Context } from '../types.js';
6
- import { ExitCodes } from '../exits.js';
7
- import { colors } from '../ui.js';
8
- import {
9
- checkPermission,
10
- buildPermissionPath,
11
- PermissionChecks,
12
- type PermissionCheck,
13
- } from './permissions.js';
14
- import {
15
- findCurrentTeamId,
16
- getStorageAdapter,
17
- generateTeamId,
18
- getTeamsDir,
19
- linkTeam,
20
- listTeams,
21
- createStorageAdapter,
22
- saveTeamConfig,
23
- } from './manager.js';
24
- import type { StorageAdapter } from './storage/adapter.js';
25
- import type { TaskStatus, MilestoneStatus, StorageBackend } from './types.js';
26
- import path from 'path';
27
-
28
- // ─────────────────────────────────────────────────────────────
29
- // Helpers
30
- // ─────────────────────────────────────────────────────────────
31
-
32
- async function requireTeam(ctx: Context): Promise<{ teamId: string; storage: StorageAdapter }> {
33
- const teamId = findCurrentTeamId(process.cwd(), ctx.paths.globalDir);
34
- if (!teamId) {
35
- ctx.ui.error("No team found. Run 'tmux-team pm init' first or navigate to a linked directory.");
36
- ctx.exit(ExitCodes.CONFIG_MISSING);
37
- }
38
- const storage = getStorageAdapter(teamId, ctx.paths.globalDir);
39
-
40
- // Validate team exists
41
- const team = await storage.getTeam();
42
- if (!team) {
43
- ctx.ui.error(`Team ${teamId} not found. The .tmux-team-id file may be stale.`);
44
- ctx.exit(ExitCodes.CONFIG_MISSING);
45
- }
46
-
47
- return { teamId, storage };
48
- }
49
-
50
- function formatStatus(status: TaskStatus | MilestoneStatus): string {
51
- switch (status) {
52
- case 'pending':
53
- return colors.yellow('pending');
54
- case 'in_progress':
55
- return colors.blue('in_progress');
56
- case 'done':
57
- return colors.green('done');
58
- default:
59
- return status;
60
- }
61
- }
62
-
63
- function parseStatus(s: string): TaskStatus {
64
- const normalized = s.toLowerCase().replace(/-/g, '_');
65
- if (normalized === 'pending' || normalized === 'in_progress' || normalized === 'done') {
66
- return normalized as TaskStatus;
67
- }
68
- throw new Error(`Invalid status: ${s}. Use: pending, in_progress, done`);
69
- }
70
-
71
- function requirePermission(ctx: Context, check: PermissionCheck): void {
72
- const result = checkPermission(ctx.config, check);
73
-
74
- // Display warning if there's an identity conflict
75
- if (result.warning) {
76
- ctx.ui.warn(result.warning);
77
- }
78
-
79
- if (!result.allowed) {
80
- const permPath = buildPermissionPath(check);
81
- ctx.ui.error(`Permission denied: ${result.actor} cannot perform ${permPath}`);
82
- ctx.exit(ExitCodes.ERROR);
83
- }
84
- }
85
-
86
- // ─────────────────────────────────────────────────────────────
87
- // Commands
88
- // ─────────────────────────────────────────────────────────────
89
-
90
- export async function cmdPmInit(ctx: Context, args: string[]): Promise<void> {
91
- requirePermission(ctx, PermissionChecks.teamCreate());
92
-
93
- const { ui, flags, paths } = ctx;
94
-
95
- // Parse flags: --name, --backend, --repo
96
- let name = 'Unnamed Project';
97
- let backend: StorageBackend = 'fs';
98
- let repo: string | undefined;
99
-
100
- for (let i = 0; i < args.length; i++) {
101
- if (args[i] === '--name' && args[i + 1]) {
102
- name = args[++i];
103
- } else if (args[i].startsWith('--name=')) {
104
- name = args[i].slice(7);
105
- } else if (args[i] === '--backend' && args[i + 1]) {
106
- const b = args[++i];
107
- if (b !== 'fs' && b !== 'github') {
108
- ui.error(`Invalid backend: ${b}. Use: fs, github`);
109
- ctx.exit(ExitCodes.ERROR);
110
- }
111
- backend = b;
112
- } else if (args[i].startsWith('--backend=')) {
113
- const b = args[i].slice(10);
114
- if (b !== 'fs' && b !== 'github') {
115
- ui.error(`Invalid backend: ${b}. Use: fs, github`);
116
- ctx.exit(ExitCodes.ERROR);
117
- }
118
- backend = b as StorageBackend;
119
- } else if (args[i] === '--repo' && args[i + 1]) {
120
- repo = args[++i];
121
- } else if (args[i].startsWith('--repo=')) {
122
- repo = args[i].slice(7);
123
- }
124
- }
125
-
126
- // Validate GitHub backend requires repo
127
- if (backend === 'github' && !repo) {
128
- ui.error('GitHub backend requires --repo flag (e.g., --repo owner/repo)');
129
- ctx.exit(ExitCodes.ERROR);
130
- }
131
-
132
- const teamId = generateTeamId();
133
- const teamDir = path.join(getTeamsDir(paths.globalDir), teamId);
134
-
135
- // Save config first
136
- saveTeamConfig(teamDir, { backend, repo });
137
-
138
- // Create storage adapter
139
- const storage = createStorageAdapter(teamDir, backend, repo);
140
-
141
- const team = await storage.initTeam(name);
142
- linkTeam(process.cwd(), teamId);
143
-
144
- await storage.appendEvent({
145
- event: 'team_created',
146
- id: teamId,
147
- name,
148
- backend,
149
- repo,
150
- actor: 'human',
151
- ts: new Date().toISOString(),
152
- });
153
-
154
- if (flags.json) {
155
- ui.json({ team, backend, repo, linked: process.cwd() });
156
- } else {
157
- ui.success(`Created team '${name}' (${teamId})`);
158
- if (backend === 'github') {
159
- ui.info(`Backend: GitHub (${repo})`);
160
- }
161
- ui.info(`Linked to ${process.cwd()}`);
162
- }
163
- }
164
-
165
- export async function cmdPmMilestone(ctx: Context, args: string[]): Promise<void> {
166
- const [subcommand, ...rest] = args;
167
-
168
- switch (subcommand) {
169
- case 'add':
170
- return cmdMilestoneAdd(ctx, rest);
171
- case 'list':
172
- case 'ls':
173
- return cmdMilestoneList(ctx, rest);
174
- case 'done':
175
- return cmdMilestoneDone(ctx, rest);
176
- default:
177
- ctx.ui.error(`Unknown milestone command: ${subcommand}. Use: add, list, done`);
178
- ctx.exit(ExitCodes.ERROR);
179
- }
180
- }
181
-
182
- async function cmdMilestoneAdd(ctx: Context, args: string[]): Promise<void> {
183
- requirePermission(ctx, PermissionChecks.milestoneCreate());
184
-
185
- const { ui, flags } = ctx;
186
- const { storage } = await requireTeam(ctx);
187
-
188
- const name = args[0];
189
- if (!name) {
190
- ui.error('Usage: tmux-team pm milestone add <name>');
191
- ctx.exit(ExitCodes.ERROR);
192
- }
193
-
194
- const milestone = await storage.createMilestone({ name });
195
-
196
- await storage.appendEvent({
197
- event: 'milestone_created',
198
- id: milestone.id,
199
- name,
200
- actor: 'human',
201
- ts: new Date().toISOString(),
202
- });
203
-
204
- if (flags.json) {
205
- ui.json(milestone);
206
- } else {
207
- ui.success(`Created milestone #${milestone.id}: ${name}`);
208
- }
209
- }
210
-
211
- async function cmdMilestoneList(ctx: Context, _args: string[]): Promise<void> {
212
- requirePermission(ctx, PermissionChecks.milestoneList());
213
-
214
- const { ui, flags } = ctx;
215
- const { storage } = await requireTeam(ctx);
216
-
217
- const milestones = await storage.listMilestones();
218
-
219
- if (flags.json) {
220
- ui.json(milestones);
221
- return;
222
- }
223
-
224
- if (milestones.length === 0) {
225
- ui.info('No milestones. Use: tmux-team pm milestone add <name>');
226
- return;
227
- }
228
-
229
- console.log();
230
- ui.table(
231
- ['ID', 'NAME', 'STATUS'],
232
- milestones.map((m) => [m.id, m.name, formatStatus(m.status)])
233
- );
234
- console.log();
235
- }
236
-
237
- async function cmdMilestoneDone(ctx: Context, args: string[]): Promise<void> {
238
- requirePermission(ctx, PermissionChecks.milestoneUpdate(['status']));
239
-
240
- const { ui, flags } = ctx;
241
- const { storage } = await requireTeam(ctx);
242
-
243
- const id = args[0];
244
- if (!id) {
245
- ui.error('Usage: tmux-team pm milestone done <id>');
246
- ctx.exit(ExitCodes.ERROR);
247
- }
248
-
249
- const milestone = await storage.getMilestone(id);
250
- if (!milestone) {
251
- ui.error(`Milestone ${id} not found`);
252
- ctx.exit(ExitCodes.PANE_NOT_FOUND);
253
- }
254
-
255
- const updated = await storage.updateMilestone(id, { status: 'done' });
256
-
257
- await storage.appendEvent({
258
- event: 'milestone_updated',
259
- id,
260
- field: 'status',
261
- from: milestone.status,
262
- to: 'done',
263
- actor: 'human',
264
- ts: new Date().toISOString(),
265
- });
266
-
267
- if (flags.json) {
268
- ui.json(updated);
269
- } else {
270
- ui.success(`Milestone #${id} marked as done`);
271
- }
272
- }
273
-
274
- export async function cmdPmTask(ctx: Context, args: string[]): Promise<void> {
275
- const [subcommand, ...rest] = args;
276
-
277
- switch (subcommand) {
278
- case 'add':
279
- return cmdTaskAdd(ctx, rest);
280
- case 'list':
281
- case 'ls':
282
- return cmdTaskList(ctx, rest);
283
- case 'show':
284
- return cmdTaskShow(ctx, rest);
285
- case 'update':
286
- return cmdTaskUpdate(ctx, rest);
287
- case 'done':
288
- return cmdTaskDone(ctx, rest);
289
- default:
290
- ctx.ui.error(`Unknown task command: ${subcommand}. Use: add, list, show, update, done`);
291
- ctx.exit(ExitCodes.ERROR);
292
- }
293
- }
294
-
295
- async function cmdTaskAdd(ctx: Context, args: string[]): Promise<void> {
296
- requirePermission(ctx, PermissionChecks.taskCreate());
297
-
298
- const { ui, flags } = ctx;
299
- const { storage } = await requireTeam(ctx);
300
-
301
- // Parse args: <title> [--milestone <id>] [--assignee <name>] [--body <text>]
302
- let title = '';
303
- let body: string | undefined;
304
- let milestone: string | undefined;
305
- let assignee: string | undefined;
306
-
307
- for (let i = 0; i < args.length; i++) {
308
- if (args[i] === '--milestone' || args[i] === '-m') {
309
- milestone = args[++i];
310
- } else if (args[i].startsWith('--milestone=')) {
311
- milestone = args[i].slice(12);
312
- } else if (args[i] === '--assignee' || args[i] === '-a') {
313
- assignee = args[++i];
314
- } else if (args[i].startsWith('--assignee=')) {
315
- assignee = args[i].slice(11);
316
- } else if (args[i] === '--body' || args[i] === '-b') {
317
- body = args[++i];
318
- } else if (args[i].startsWith('--body=')) {
319
- body = args[i].slice(7);
320
- } else if (!title) {
321
- title = args[i];
322
- }
323
- }
324
-
325
- if (!title) {
326
- ui.error('Usage: tmux-team pm task add <title> [--milestone <id>]');
327
- ctx.exit(ExitCodes.ERROR);
328
- }
329
-
330
- const task = await storage.createTask({ title, body, milestone, assignee });
331
-
332
- await storage.appendEvent({
333
- event: 'task_created',
334
- id: task.id,
335
- title,
336
- milestone,
337
- actor: 'human',
338
- ts: new Date().toISOString(),
339
- });
340
-
341
- if (flags.json) {
342
- ui.json(task);
343
- } else {
344
- ui.success(`Created task #${task.id}: ${title}`);
345
- }
346
- }
347
-
348
- async function cmdTaskList(ctx: Context, args: string[]): Promise<void> {
349
- requirePermission(ctx, PermissionChecks.taskList());
350
-
351
- const { ui, flags } = ctx;
352
- const { storage } = await requireTeam(ctx);
353
-
354
- // Parse filters
355
- let milestone: string | undefined;
356
- let status: TaskStatus | undefined;
357
-
358
- for (let i = 0; i < args.length; i++) {
359
- if (args[i] === '--milestone' || args[i] === '-m') {
360
- milestone = args[++i];
361
- } else if (args[i].startsWith('--milestone=')) {
362
- milestone = args[i].slice(12);
363
- } else if (args[i] === '--status' || args[i] === '-s') {
364
- status = parseStatus(args[++i]);
365
- } else if (args[i].startsWith('--status=')) {
366
- status = parseStatus(args[i].slice(9));
367
- }
368
- }
369
-
370
- const tasks = await storage.listTasks({ milestone, status });
371
-
372
- if (flags.json) {
373
- ui.json(tasks);
374
- return;
375
- }
376
-
377
- if (tasks.length === 0) {
378
- ui.info('No tasks. Use: tmux-team pm task add <title>');
379
- return;
380
- }
381
-
382
- console.log();
383
- ui.table(
384
- ['ID', 'TITLE', 'STATUS', 'MILESTONE'],
385
- tasks.map((t) => [t.id, t.title.slice(0, 40), formatStatus(t.status), t.milestone || '-'])
386
- );
387
- console.log();
388
- }
389
-
390
- async function cmdTaskShow(ctx: Context, args: string[]): Promise<void> {
391
- requirePermission(ctx, PermissionChecks.taskShow());
392
-
393
- const { ui, flags } = ctx;
394
- const { storage } = await requireTeam(ctx);
395
-
396
- const id = args[0];
397
- if (!id) {
398
- ui.error('Usage: tmux-team pm task show <id>');
399
- ctx.exit(ExitCodes.ERROR);
400
- }
401
-
402
- const task = await storage.getTask(id);
403
- if (!task) {
404
- ui.error(`Task ${id} not found`);
405
- ctx.exit(ExitCodes.PANE_NOT_FOUND);
406
- }
407
-
408
- if (flags.json) {
409
- ui.json(task);
410
- return;
411
- }
412
-
413
- console.log();
414
- console.log(colors.cyan(`Task #${task.id}: ${task.title}`));
415
- console.log(`Status: ${formatStatus(task.status)}`);
416
- if (task.milestone) console.log(`Milestone: #${task.milestone}`);
417
- if (task.assignee) console.log(`Assignee: ${task.assignee}`);
418
- console.log(`Created: ${task.createdAt}`);
419
- console.log(`Updated: ${task.updatedAt}`);
420
- console.log();
421
- }
422
-
423
- async function cmdTaskUpdate(ctx: Context, args: string[]): Promise<void> {
424
- const { ui, flags } = ctx;
425
-
426
- // Parse: <id> --status <status> [--assignee <name>]
427
- const id = args[0];
428
- if (!id) {
429
- ui.error('Usage: tmux-team pm task update <id> --status <status>');
430
- ctx.exit(ExitCodes.ERROR);
431
- }
432
-
433
- let status: TaskStatus | undefined;
434
- let assignee: string | undefined;
435
-
436
- for (let i = 1; i < args.length; i++) {
437
- if (args[i] === '--status' || args[i] === '-s') {
438
- status = parseStatus(args[++i]);
439
- } else if (args[i].startsWith('--status=')) {
440
- status = parseStatus(args[i].slice(9));
441
- } else if (args[i] === '--assignee' || args[i] === '-a') {
442
- assignee = args[++i];
443
- } else if (args[i].startsWith('--assignee=')) {
444
- assignee = args[i].slice(11);
445
- }
446
- }
447
-
448
- // Check permissions based on which fields are being updated
449
- const fields: string[] = [];
450
- if (status) fields.push('status');
451
- if (assignee) fields.push('assignee');
452
- if (fields.length > 0) {
453
- requirePermission(ctx, PermissionChecks.taskUpdate(fields));
454
- }
455
-
456
- const { storage } = await requireTeam(ctx);
457
- const task = await storage.getTask(id);
458
- if (!task) {
459
- ui.error(`Task ${id} not found`);
460
- ctx.exit(ExitCodes.PANE_NOT_FOUND);
461
- }
462
-
463
- const updates: { status?: TaskStatus; assignee?: string } = {};
464
- if (status) updates.status = status;
465
- if (assignee) updates.assignee = assignee;
466
-
467
- if (Object.keys(updates).length === 0) {
468
- ui.error('No updates specified. Use --status or --assignee');
469
- ctx.exit(ExitCodes.ERROR);
470
- }
471
-
472
- const updated = await storage.updateTask(id, updates);
473
-
474
- if (status) {
475
- await storage.appendEvent({
476
- event: 'task_updated',
477
- id,
478
- field: 'status',
479
- from: task.status,
480
- to: status,
481
- actor: 'human',
482
- ts: new Date().toISOString(),
483
- });
484
- }
485
-
486
- if (flags.json) {
487
- ui.json(updated);
488
- } else {
489
- ui.success(`Updated task #${id}`);
490
- }
491
- }
492
-
493
- async function cmdTaskDone(ctx: Context, args: string[]): Promise<void> {
494
- requirePermission(ctx, PermissionChecks.taskUpdate(['status']));
495
-
496
- const { ui, flags } = ctx;
497
- const { storage } = await requireTeam(ctx);
498
-
499
- const id = args[0];
500
- if (!id) {
501
- ui.error('Usage: tmux-team pm task done <id>');
502
- ctx.exit(ExitCodes.ERROR);
503
- }
504
-
505
- const task = await storage.getTask(id);
506
- if (!task) {
507
- ui.error(`Task ${id} not found`);
508
- ctx.exit(ExitCodes.PANE_NOT_FOUND);
509
- }
510
-
511
- const updated = await storage.updateTask(id, { status: 'done' });
512
-
513
- await storage.appendEvent({
514
- event: 'task_updated',
515
- id,
516
- field: 'status',
517
- from: task.status,
518
- to: 'done',
519
- actor: 'human',
520
- ts: new Date().toISOString(),
521
- });
522
-
523
- if (flags.json) {
524
- ui.json(updated);
525
- } else {
526
- ui.success(`Task #${id} marked as done`);
527
- }
528
- }
529
-
530
- export async function cmdPmDoc(ctx: Context, args: string[]): Promise<void> {
531
- const { ui, flags } = ctx;
532
-
533
- const id = args[0];
534
- if (!id) {
535
- ui.error('Usage: tmux-team pm doc <id> [--print]');
536
- ctx.exit(ExitCodes.ERROR);
537
- }
538
-
539
- const printOnly = args.includes('--print') || args.includes('-p');
540
-
541
- // Check permission based on mode
542
- if (printOnly || flags.json) {
543
- requirePermission(ctx, PermissionChecks.docRead());
544
- } else {
545
- requirePermission(ctx, PermissionChecks.docUpdate());
546
- }
547
-
548
- const { teamId, storage } = await requireTeam(ctx);
549
- const task = await storage.getTask(id);
550
- if (!task) {
551
- ui.error(`Task ${id} not found`);
552
- ctx.exit(ExitCodes.PANE_NOT_FOUND);
553
- }
554
-
555
- const doc = await storage.getTaskDoc(id);
556
-
557
- if (printOnly || flags.json) {
558
- if (flags.json) {
559
- ui.json({ id, doc });
560
- } else {
561
- console.log(doc || '(empty)');
562
- }
563
- return;
564
- }
565
-
566
- // Open in editor
567
- const editor = process.env.EDITOR || 'vim';
568
- const docPath = path.join(getTeamsDir(ctx.paths.globalDir), teamId, 'tasks', `${id}.md`);
569
-
570
- const { spawnSync } = await import('child_process');
571
- spawnSync(editor, [docPath], { stdio: 'inherit' });
572
-
573
- ui.success(`Saved documentation for task #${id}`);
574
- }
575
-
576
- export async function cmdPmLog(ctx: Context, args: string[]): Promise<void> {
577
- requirePermission(ctx, PermissionChecks.logRead());
578
-
579
- const { ui, flags } = ctx;
580
- const { storage } = await requireTeam(ctx);
581
-
582
- // Parse --limit flag
583
- let limit: number | undefined;
584
- for (let i = 0; i < args.length; i++) {
585
- if (args[i] === '--limit' || args[i] === '-n') {
586
- limit = parseInt(args[++i], 10);
587
- } else if (args[i].startsWith('--limit=')) {
588
- limit = parseInt(args[i].slice(8), 10);
589
- }
590
- }
591
-
592
- const events = await storage.getEvents(limit);
593
-
594
- if (flags.json) {
595
- ui.json(events);
596
- return;
597
- }
598
-
599
- if (events.length === 0) {
600
- ui.info('No events logged yet.');
601
- return;
602
- }
603
-
604
- console.log();
605
- for (const event of events) {
606
- const time = event.ts.slice(0, 19).replace('T', ' ');
607
- const actor = colors.cyan(event.actor);
608
- const action = colors.yellow(event.event);
609
- const id = event.id ? `#${event.id}` : '';
610
- console.log(`${colors.dim(time)} ${actor} ${action} ${id}`);
611
- }
612
- console.log();
613
- }
614
-
615
- export async function cmdPmList(ctx: Context, _args: string[]): Promise<void> {
616
- requirePermission(ctx, PermissionChecks.teamList());
617
-
618
- const { ui, flags, paths } = ctx;
619
-
620
- const teams = listTeams(paths.globalDir);
621
- const currentTeamId = findCurrentTeamId(process.cwd(), paths.globalDir);
622
-
623
- if (flags.json) {
624
- ui.json({ teams, currentTeamId });
625
- return;
626
- }
627
-
628
- if (teams.length === 0) {
629
- ui.info("No teams. Use: tmux-team pm init --name 'My Project'");
630
- return;
631
- }
632
-
633
- console.log();
634
- ui.table(
635
- ['', 'ID', 'NAME', 'BACKEND', 'CREATED'],
636
- teams.map((t) => [
637
- t.id === currentTeamId ? colors.green('→') : ' ',
638
- t.id.slice(0, 8) + '...',
639
- t.name,
640
- t.backend === 'github' ? colors.cyan('github') : colors.dim('fs'),
641
- t.createdAt.slice(0, 10),
642
- ])
643
- );
644
- console.log();
645
- }
646
-
647
- // ─────────────────────────────────────────────────────────────
648
- // Main PM router
649
- // ─────────────────────────────────────────────────────────────
650
-
651
- export async function cmdPm(ctx: Context, args: string[]): Promise<void> {
652
- const [subcommand, ...rest] = args;
653
-
654
- // Handle shorthands
655
- const cmd = subcommand === 'm' ? 'milestone' : subcommand === 't' ? 'task' : subcommand;
656
-
657
- switch (cmd) {
658
- case 'init':
659
- return cmdPmInit(ctx, rest);
660
- case 'milestone':
661
- return cmdPmMilestone(ctx, rest);
662
- case 'task':
663
- return cmdPmTask(ctx, rest);
664
- case 'doc':
665
- return cmdPmDoc(ctx, rest);
666
- case 'log':
667
- return cmdPmLog(ctx, rest);
668
- case 'list':
669
- case 'ls':
670
- return cmdPmList(ctx, rest);
671
- case undefined:
672
- case 'help':
673
- return cmdPmHelp(ctx);
674
- default:
675
- ctx.ui.error(`Unknown pm command: ${subcommand}. Run 'tmux-team pm help'`);
676
- ctx.exit(ExitCodes.ERROR);
677
- }
678
- }
679
-
680
- function cmdPmHelp(_ctx: Context): void {
681
- console.log(`
682
- ${colors.cyan('tmux-team pm')} - Project management
683
-
684
- ${colors.yellow('COMMANDS')}
685
- ${colors.green('init')} [options] Create a new team/project
686
- --name <name> Project name
687
- --backend <fs|github> Storage backend (default: fs)
688
- --repo <owner/repo> GitHub repo (required for github backend)
689
- ${colors.green('list')} List all teams
690
- ${colors.green('milestone')} add <name> Add milestone (shorthand: m)
691
- ${colors.green('milestone')} list List milestones
692
- ${colors.green('milestone')} done <id> Mark milestone complete
693
- ${colors.green('task')} add <title> [--milestone] Add task (shorthand: t)
694
- ${colors.green('task')} list [--status] [--milestone] List tasks
695
- ${colors.green('task')} show <id> Show task details
696
- ${colors.green('task')} update <id> --status <s> Update task status
697
- ${colors.green('task')} done <id> Mark task complete
698
- ${colors.green('doc')} <id> [--print] View/edit task documentation
699
- ${colors.green('log')} [--limit <n>] Show audit event log
700
-
701
- ${colors.yellow('BACKENDS')}
702
- ${colors.cyan('fs')} Local filesystem (default) - tasks in ~/.config/tmux-team/teams/
703
- ${colors.cyan('github')} GitHub Issues - tasks become issues, milestones sync with GH
704
-
705
- ${colors.yellow('SHORTHANDS')}
706
- pm m = pm milestone
707
- pm t = pm task
708
- pm ls = pm list
709
-
710
- ${colors.yellow('EXAMPLES')}
711
- # Local filesystem backend (default)
712
- tmux-team pm init --name "Auth Refactor"
713
-
714
- # GitHub backend - uses gh CLI for auth
715
- tmux-team pm init --name "Sprint 1" --backend github --repo owner/repo
716
-
717
- tmux-team pm m add "Phase 1"
718
- tmux-team pm t add "Implement login" --milestone 1
719
- tmux-team pm t list --status pending
720
- tmux-team pm t done 1
721
- tmux-team pm log --limit 10
722
- `);
723
- }