tmux-team 2.2.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.
- package/README.md +21 -191
- package/package.json +1 -1
- package/src/cli.ts +0 -5
- package/src/commands/config.ts +2 -44
- package/src/commands/help.ts +0 -2
- package/src/commands/talk.test.ts +296 -46
- package/src/commands/talk.ts +69 -63
- package/src/config.test.ts +0 -1
- package/src/config.ts +0 -1
- package/src/identity.ts +89 -0
- package/src/types.ts +2 -2
- package/src/version.ts +1 -1
- package/src/pm/commands.test.ts +0 -1462
- package/src/pm/commands.ts +0 -1011
- package/src/pm/manager.test.ts +0 -377
- package/src/pm/manager.ts +0 -146
- package/src/pm/permissions.test.ts +0 -444
- package/src/pm/permissions.ts +0 -293
- package/src/pm/storage/adapter.ts +0 -57
- package/src/pm/storage/fs.test.ts +0 -512
- package/src/pm/storage/fs.ts +0 -290
- package/src/pm/storage/github.ts +0 -842
- package/src/pm/types.ts +0 -91
package/src/pm/commands.ts
DELETED
|
@@ -1,1011 +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
|
-
import fs from 'fs';
|
|
28
|
-
import os from 'os';
|
|
29
|
-
|
|
30
|
-
// ─────────────────────────────────────────────────────────────
|
|
31
|
-
// Helpers
|
|
32
|
-
// ─────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
async function requireTeam(ctx: Context): Promise<{ teamId: string; storage: StorageAdapter }> {
|
|
35
|
-
const teamId = findCurrentTeamId(process.cwd(), ctx.paths.globalDir);
|
|
36
|
-
if (!teamId) {
|
|
37
|
-
ctx.ui.error("No team found. Run 'tmux-team pm init' first or navigate to a linked directory.");
|
|
38
|
-
ctx.exit(ExitCodes.CONFIG_MISSING);
|
|
39
|
-
}
|
|
40
|
-
const storage = getStorageAdapter(teamId, ctx.paths.globalDir);
|
|
41
|
-
|
|
42
|
-
// Validate team exists
|
|
43
|
-
const team = await storage.getTeam();
|
|
44
|
-
if (!team) {
|
|
45
|
-
ctx.ui.error(`Team ${teamId} not found. The .tmux-team-id file may be stale.`);
|
|
46
|
-
ctx.exit(ExitCodes.CONFIG_MISSING);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return { teamId, storage };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function formatStatus(status: TaskStatus | MilestoneStatus): string {
|
|
53
|
-
switch (status) {
|
|
54
|
-
case 'pending':
|
|
55
|
-
return colors.yellow('pending');
|
|
56
|
-
case 'in_progress':
|
|
57
|
-
return colors.blue('in_progress');
|
|
58
|
-
case 'done':
|
|
59
|
-
return colors.green('done');
|
|
60
|
-
default:
|
|
61
|
-
return status;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseStatus(s: string): TaskStatus {
|
|
66
|
-
const normalized = s.toLowerCase().replace(/-/g, '_');
|
|
67
|
-
if (normalized === 'pending' || normalized === 'in_progress' || normalized === 'done') {
|
|
68
|
-
return normalized as TaskStatus;
|
|
69
|
-
}
|
|
70
|
-
throw new Error(`Invalid status: ${s}. Use: pending, in_progress, done`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function requirePermission(ctx: Context, check: PermissionCheck): void {
|
|
74
|
-
const result = checkPermission(ctx.config, check);
|
|
75
|
-
|
|
76
|
-
// Display warning if there's an identity conflict
|
|
77
|
-
if (result.warning) {
|
|
78
|
-
ctx.ui.warn(result.warning);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!result.allowed) {
|
|
82
|
-
const permPath = buildPermissionPath(check);
|
|
83
|
-
ctx.ui.error(`Permission denied: ${result.actor} cannot perform ${permPath}`);
|
|
84
|
-
ctx.exit(ExitCodes.ERROR);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ─────────────────────────────────────────────────────────────
|
|
89
|
-
// Commands
|
|
90
|
-
// ─────────────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
export async function cmdPmInit(ctx: Context, args: string[]): Promise<void> {
|
|
93
|
-
requirePermission(ctx, PermissionChecks.teamCreate());
|
|
94
|
-
|
|
95
|
-
const { ui, flags, paths } = ctx;
|
|
96
|
-
|
|
97
|
-
// Parse flags: --name, --backend, --repo
|
|
98
|
-
let name = 'Unnamed Project';
|
|
99
|
-
let backend: StorageBackend = 'fs';
|
|
100
|
-
let repo: string | undefined;
|
|
101
|
-
|
|
102
|
-
for (let i = 0; i < args.length; i++) {
|
|
103
|
-
if (args[i] === '--name' && args[i + 1]) {
|
|
104
|
-
name = args[++i];
|
|
105
|
-
} else if (args[i].startsWith('--name=')) {
|
|
106
|
-
name = args[i].slice(7);
|
|
107
|
-
} else if (args[i] === '--backend' && args[i + 1]) {
|
|
108
|
-
const b = args[++i];
|
|
109
|
-
if (b !== 'fs' && b !== 'github') {
|
|
110
|
-
ui.error(`Invalid backend: ${b}. Use: fs, github`);
|
|
111
|
-
ctx.exit(ExitCodes.ERROR);
|
|
112
|
-
}
|
|
113
|
-
backend = b;
|
|
114
|
-
} else if (args[i].startsWith('--backend=')) {
|
|
115
|
-
const b = args[i].slice(10);
|
|
116
|
-
if (b !== 'fs' && b !== 'github') {
|
|
117
|
-
ui.error(`Invalid backend: ${b}. Use: fs, github`);
|
|
118
|
-
ctx.exit(ExitCodes.ERROR);
|
|
119
|
-
}
|
|
120
|
-
backend = b as StorageBackend;
|
|
121
|
-
} else if (args[i] === '--repo' && args[i + 1]) {
|
|
122
|
-
repo = args[++i];
|
|
123
|
-
} else if (args[i].startsWith('--repo=')) {
|
|
124
|
-
repo = args[i].slice(7);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Validate GitHub backend requires repo
|
|
129
|
-
if (backend === 'github' && !repo) {
|
|
130
|
-
ui.error('GitHub backend requires --repo flag (e.g., --repo owner/repo)');
|
|
131
|
-
ctx.exit(ExitCodes.ERROR);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const teamId = generateTeamId();
|
|
135
|
-
const teamDir = path.join(getTeamsDir(paths.globalDir), teamId);
|
|
136
|
-
|
|
137
|
-
// Save config first
|
|
138
|
-
saveTeamConfig(teamDir, { backend, repo });
|
|
139
|
-
|
|
140
|
-
// Create storage adapter
|
|
141
|
-
const storage = createStorageAdapter(teamDir, backend, repo);
|
|
142
|
-
|
|
143
|
-
const team = await storage.initTeam(name);
|
|
144
|
-
linkTeam(process.cwd(), teamId);
|
|
145
|
-
|
|
146
|
-
await storage.appendEvent({
|
|
147
|
-
event: 'team_created',
|
|
148
|
-
id: teamId,
|
|
149
|
-
name,
|
|
150
|
-
backend,
|
|
151
|
-
repo,
|
|
152
|
-
actor: 'human',
|
|
153
|
-
ts: new Date().toISOString(),
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
if (flags.json) {
|
|
157
|
-
ui.json({ team, backend, repo, linked: process.cwd() });
|
|
158
|
-
} else {
|
|
159
|
-
ui.success(`Created team '${name}' (${teamId})`);
|
|
160
|
-
if (backend === 'github') {
|
|
161
|
-
ui.info(`Backend: GitHub (${repo})`);
|
|
162
|
-
}
|
|
163
|
-
ui.info(`Linked to ${process.cwd()}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export async function cmdPmMilestone(ctx: Context, args: string[]): Promise<void> {
|
|
168
|
-
const [subcommand, ...rest] = args;
|
|
169
|
-
|
|
170
|
-
switch (subcommand) {
|
|
171
|
-
case 'add':
|
|
172
|
-
return cmdMilestoneAdd(ctx, rest);
|
|
173
|
-
case 'list':
|
|
174
|
-
case 'ls':
|
|
175
|
-
case undefined:
|
|
176
|
-
return cmdMilestoneList(ctx, rest);
|
|
177
|
-
case 'done':
|
|
178
|
-
return cmdMilestoneDone(ctx, rest);
|
|
179
|
-
case 'delete':
|
|
180
|
-
case 'rm':
|
|
181
|
-
return cmdMilestoneDelete(ctx, rest);
|
|
182
|
-
case 'doc':
|
|
183
|
-
return cmdMilestoneDoc(ctx, rest);
|
|
184
|
-
default:
|
|
185
|
-
ctx.ui.error(`Unknown milestone command: ${subcommand}. Use: add, list, done, delete, doc`);
|
|
186
|
-
ctx.exit(ExitCodes.ERROR);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async function cmdMilestoneAdd(ctx: Context, args: string[]): Promise<void> {
|
|
191
|
-
requirePermission(ctx, PermissionChecks.milestoneCreate());
|
|
192
|
-
|
|
193
|
-
const { ui, flags } = ctx;
|
|
194
|
-
const { storage } = await requireTeam(ctx);
|
|
195
|
-
|
|
196
|
-
// Parse args: <name> [--description <text>]
|
|
197
|
-
let name = '';
|
|
198
|
-
let description: string | undefined;
|
|
199
|
-
|
|
200
|
-
for (let i = 0; i < args.length; i++) {
|
|
201
|
-
if (args[i] === '--description' || args[i] === '-d') {
|
|
202
|
-
description = args[++i];
|
|
203
|
-
} else if (args[i].startsWith('--description=')) {
|
|
204
|
-
description = args[i].slice(14);
|
|
205
|
-
} else if (!name) {
|
|
206
|
-
name = args[i];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!name) {
|
|
211
|
-
ui.error('Usage: tmux-team pm milestone add <name> [--description <text>]');
|
|
212
|
-
ctx.exit(ExitCodes.ERROR);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const milestone = await storage.createMilestone({ name, description });
|
|
216
|
-
|
|
217
|
-
await storage.appendEvent({
|
|
218
|
-
event: 'milestone_created',
|
|
219
|
-
id: milestone.id,
|
|
220
|
-
name,
|
|
221
|
-
actor: 'human',
|
|
222
|
-
ts: new Date().toISOString(),
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
if (flags.json) {
|
|
226
|
-
ui.json(milestone);
|
|
227
|
-
} else {
|
|
228
|
-
ui.success(`Created milestone #${milestone.id}: ${name}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function cmdMilestoneList(ctx: Context, _args: string[]): Promise<void> {
|
|
233
|
-
requirePermission(ctx, PermissionChecks.milestoneList());
|
|
234
|
-
|
|
235
|
-
const { ui, flags } = ctx;
|
|
236
|
-
const { storage } = await requireTeam(ctx);
|
|
237
|
-
|
|
238
|
-
const milestones = await storage.listMilestones();
|
|
239
|
-
|
|
240
|
-
if (flags.json) {
|
|
241
|
-
ui.json(milestones);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (milestones.length === 0) {
|
|
246
|
-
ui.info('No milestones. Use: tmux-team pm milestone add <name>');
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
console.log();
|
|
251
|
-
ui.table(
|
|
252
|
-
['ID', 'NAME', 'STATUS'],
|
|
253
|
-
milestones.map((m) => [m.id, m.name, formatStatus(m.status)])
|
|
254
|
-
);
|
|
255
|
-
console.log();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function cmdMilestoneDone(ctx: Context, args: string[]): Promise<void> {
|
|
259
|
-
requirePermission(ctx, PermissionChecks.milestoneUpdate(['status']));
|
|
260
|
-
|
|
261
|
-
const { ui, flags } = ctx;
|
|
262
|
-
const { storage } = await requireTeam(ctx);
|
|
263
|
-
|
|
264
|
-
const id = args[0];
|
|
265
|
-
if (!id) {
|
|
266
|
-
ui.error('Usage: tmux-team pm milestone done <id>');
|
|
267
|
-
ctx.exit(ExitCodes.ERROR);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const milestone = await storage.getMilestone(id);
|
|
271
|
-
if (!milestone) {
|
|
272
|
-
ui.error(`Milestone ${id} not found`);
|
|
273
|
-
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const updated = await storage.updateMilestone(id, { status: 'done' });
|
|
277
|
-
|
|
278
|
-
await storage.appendEvent({
|
|
279
|
-
event: 'milestone_updated',
|
|
280
|
-
id,
|
|
281
|
-
field: 'status',
|
|
282
|
-
from: milestone.status,
|
|
283
|
-
to: 'done',
|
|
284
|
-
actor: 'human',
|
|
285
|
-
ts: new Date().toISOString(),
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
if (flags.json) {
|
|
289
|
-
ui.json(updated);
|
|
290
|
-
} else {
|
|
291
|
-
ui.success(`Milestone #${id} marked as done`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function cmdMilestoneDelete(ctx: Context, args: string[]): Promise<void> {
|
|
296
|
-
requirePermission(ctx, PermissionChecks.milestoneDelete());
|
|
297
|
-
|
|
298
|
-
const { ui, flags } = ctx;
|
|
299
|
-
const { storage } = await requireTeam(ctx);
|
|
300
|
-
|
|
301
|
-
const id = args[0];
|
|
302
|
-
if (!id) {
|
|
303
|
-
ui.error('Usage: tmux-team pm milestone delete <id>');
|
|
304
|
-
ctx.exit(ExitCodes.ERROR);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const milestone = await storage.getMilestone(id);
|
|
308
|
-
if (!milestone) {
|
|
309
|
-
ui.error(`Milestone ${id} not found`);
|
|
310
|
-
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
await storage.deleteMilestone(id);
|
|
314
|
-
|
|
315
|
-
await storage.appendEvent({
|
|
316
|
-
event: 'milestone_deleted',
|
|
317
|
-
id,
|
|
318
|
-
name: milestone.name,
|
|
319
|
-
actor: 'human',
|
|
320
|
-
ts: new Date().toISOString(),
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
if (flags.json) {
|
|
324
|
-
ui.json({ deleted: true, id, name: milestone.name });
|
|
325
|
-
} else {
|
|
326
|
-
ui.success(`Milestone #${id} "${milestone.name}" deleted`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async function cmdMilestoneDoc(ctx: Context, args: string[]): Promise<void> {
|
|
331
|
-
const { ui, flags } = ctx;
|
|
332
|
-
|
|
333
|
-
// Parse arguments
|
|
334
|
-
let id: string | undefined;
|
|
335
|
-
let body: string | undefined;
|
|
336
|
-
let bodyFile: string | undefined;
|
|
337
|
-
let showRef = false;
|
|
338
|
-
let editMode = false;
|
|
339
|
-
|
|
340
|
-
for (let i = 0; i < args.length; i++) {
|
|
341
|
-
const arg = args[i];
|
|
342
|
-
if (arg === 'ref') {
|
|
343
|
-
showRef = true;
|
|
344
|
-
} else if (arg === '--edit' || arg === '-e') {
|
|
345
|
-
editMode = true;
|
|
346
|
-
} else if (arg === '--body' || arg === '-b') {
|
|
347
|
-
body = args[++i];
|
|
348
|
-
if (body === undefined) {
|
|
349
|
-
ui.error('--body requires a value');
|
|
350
|
-
ctx.exit(ExitCodes.ERROR);
|
|
351
|
-
}
|
|
352
|
-
} else if (arg.startsWith('--body=')) {
|
|
353
|
-
body = arg.slice(7);
|
|
354
|
-
} else if (arg === '--body-file' || arg === '-f') {
|
|
355
|
-
bodyFile = args[++i];
|
|
356
|
-
if (bodyFile === undefined) {
|
|
357
|
-
ui.error('--body-file requires a value');
|
|
358
|
-
ctx.exit(ExitCodes.ERROR);
|
|
359
|
-
}
|
|
360
|
-
} else if (arg.startsWith('--body-file=')) {
|
|
361
|
-
bodyFile = arg.slice(12);
|
|
362
|
-
} else if (!id) {
|
|
363
|
-
id = arg;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (!id) {
|
|
368
|
-
ui.error(
|
|
369
|
-
'Usage: tmux-team pm milestone doc <id> [ref | --edit | --body <text> | --body-file <path>]'
|
|
370
|
-
);
|
|
371
|
-
ctx.exit(ExitCodes.ERROR);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const isWriteMode = editMode || body !== undefined || bodyFile !== undefined;
|
|
375
|
-
|
|
376
|
-
// Check permission based on mode
|
|
377
|
-
if (isWriteMode) {
|
|
378
|
-
requirePermission(ctx, PermissionChecks.docUpdate());
|
|
379
|
-
} else {
|
|
380
|
-
requirePermission(ctx, PermissionChecks.docRead());
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const { storage } = await requireTeam(ctx);
|
|
384
|
-
const milestone = await storage.getMilestone(id);
|
|
385
|
-
if (!milestone) {
|
|
386
|
-
ui.error(`Milestone ${id} not found`);
|
|
387
|
-
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Show reference (docPath)
|
|
391
|
-
if (showRef) {
|
|
392
|
-
if (flags.json) {
|
|
393
|
-
ui.json({ id, docPath: milestone.docPath });
|
|
394
|
-
} else {
|
|
395
|
-
console.log(milestone.docPath || '(no docPath)');
|
|
396
|
-
}
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// --body: set content directly
|
|
401
|
-
if (body !== undefined) {
|
|
402
|
-
await storage.setMilestoneDoc(id, body);
|
|
403
|
-
ui.success(`Saved documentation for milestone #${id}`);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// --body-file: read content from file
|
|
408
|
-
if (bodyFile !== undefined) {
|
|
409
|
-
if (!fs.existsSync(bodyFile)) {
|
|
410
|
-
ui.error(`File not found: ${bodyFile}`);
|
|
411
|
-
ctx.exit(ExitCodes.ERROR);
|
|
412
|
-
}
|
|
413
|
-
const content = fs.readFileSync(bodyFile, 'utf-8');
|
|
414
|
-
await storage.setMilestoneDoc(id, content);
|
|
415
|
-
ui.success(`Saved documentation for milestone #${id} (from ${bodyFile})`);
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const doc = await storage.getMilestoneDoc(id);
|
|
420
|
-
|
|
421
|
-
// Default: print doc content
|
|
422
|
-
if (!editMode) {
|
|
423
|
-
if (flags.json) {
|
|
424
|
-
ui.json({ id, doc });
|
|
425
|
-
} else {
|
|
426
|
-
console.log(doc || '(empty)');
|
|
427
|
-
}
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Edit mode: open in editor using temp file
|
|
432
|
-
const editor = process.env.EDITOR || 'vim';
|
|
433
|
-
const tempDir = os.tmpdir();
|
|
434
|
-
const tempFile = path.join(tempDir, `tmux-team-milestone-${id}.md`);
|
|
435
|
-
|
|
436
|
-
// Write current content to temp file
|
|
437
|
-
fs.writeFileSync(tempFile, doc || `# ${milestone.name}\n\n`);
|
|
438
|
-
|
|
439
|
-
const { spawnSync } = await import('child_process');
|
|
440
|
-
spawnSync(editor, [tempFile], { stdio: 'inherit' });
|
|
441
|
-
|
|
442
|
-
// Read edited content and sync back to storage
|
|
443
|
-
const newContent = fs.readFileSync(tempFile, 'utf-8');
|
|
444
|
-
await storage.setMilestoneDoc(id, newContent);
|
|
445
|
-
|
|
446
|
-
// Clean up temp file
|
|
447
|
-
try {
|
|
448
|
-
fs.unlinkSync(tempFile);
|
|
449
|
-
} catch {
|
|
450
|
-
// Ignore cleanup errors
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
ui.success(`Saved documentation for milestone #${id}`);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export async function cmdPmTask(ctx: Context, args: string[]): Promise<void> {
|
|
457
|
-
const [subcommand, ...rest] = args;
|
|
458
|
-
|
|
459
|
-
switch (subcommand) {
|
|
460
|
-
case 'add':
|
|
461
|
-
return cmdTaskAdd(ctx, rest);
|
|
462
|
-
case 'list':
|
|
463
|
-
case 'ls':
|
|
464
|
-
case undefined:
|
|
465
|
-
return cmdTaskList(ctx, rest);
|
|
466
|
-
case 'show':
|
|
467
|
-
return cmdTaskShow(ctx, rest);
|
|
468
|
-
case 'update':
|
|
469
|
-
return cmdTaskUpdate(ctx, rest);
|
|
470
|
-
case 'done':
|
|
471
|
-
return cmdTaskDone(ctx, rest);
|
|
472
|
-
case 'doc':
|
|
473
|
-
return cmdTaskDoc(ctx, rest);
|
|
474
|
-
default:
|
|
475
|
-
ctx.ui.error(`Unknown task command: ${subcommand}. Use: add, list, show, update, done, doc`);
|
|
476
|
-
ctx.exit(ExitCodes.ERROR);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
async function cmdTaskAdd(ctx: Context, args: string[]): Promise<void> {
|
|
481
|
-
requirePermission(ctx, PermissionChecks.taskCreate());
|
|
482
|
-
|
|
483
|
-
const { ui, flags } = ctx;
|
|
484
|
-
const { storage } = await requireTeam(ctx);
|
|
485
|
-
|
|
486
|
-
// Parse args: <title> [--milestone <id>] [--assignee <name>] [--body <text>]
|
|
487
|
-
let title = '';
|
|
488
|
-
let body: string | undefined;
|
|
489
|
-
let milestone: string | undefined;
|
|
490
|
-
let assignee: string | undefined;
|
|
491
|
-
|
|
492
|
-
for (let i = 0; i < args.length; i++) {
|
|
493
|
-
if (args[i] === '--milestone' || args[i] === '-m') {
|
|
494
|
-
milestone = args[++i];
|
|
495
|
-
} else if (args[i].startsWith('--milestone=')) {
|
|
496
|
-
milestone = args[i].slice(12);
|
|
497
|
-
} else if (args[i] === '--assignee' || args[i] === '-a') {
|
|
498
|
-
assignee = args[++i];
|
|
499
|
-
} else if (args[i].startsWith('--assignee=')) {
|
|
500
|
-
assignee = args[i].slice(11);
|
|
501
|
-
} else if (args[i] === '--body' || args[i] === '-b') {
|
|
502
|
-
body = args[++i];
|
|
503
|
-
} else if (args[i].startsWith('--body=')) {
|
|
504
|
-
body = args[i].slice(7);
|
|
505
|
-
} else if (!title) {
|
|
506
|
-
title = args[i];
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (!title) {
|
|
511
|
-
ui.error('Usage: tmux-team pm task add <title> [--milestone <id>]');
|
|
512
|
-
ctx.exit(ExitCodes.ERROR);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const task = await storage.createTask({ title, body, milestone, assignee });
|
|
516
|
-
|
|
517
|
-
await storage.appendEvent({
|
|
518
|
-
event: 'task_created',
|
|
519
|
-
id: task.id,
|
|
520
|
-
title,
|
|
521
|
-
milestone,
|
|
522
|
-
actor: 'human',
|
|
523
|
-
ts: new Date().toISOString(),
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
if (flags.json) {
|
|
527
|
-
ui.json(task);
|
|
528
|
-
} else {
|
|
529
|
-
ui.success(`Created task #${task.id}: ${title}`);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
async function cmdTaskList(ctx: Context, args: string[]): Promise<void> {
|
|
534
|
-
requirePermission(ctx, PermissionChecks.taskList());
|
|
535
|
-
|
|
536
|
-
const { ui, flags, config } = ctx;
|
|
537
|
-
const { storage } = await requireTeam(ctx);
|
|
538
|
-
|
|
539
|
-
// Parse filters
|
|
540
|
-
let milestone: string | undefined;
|
|
541
|
-
let status: TaskStatus | undefined;
|
|
542
|
-
let showAll = false;
|
|
543
|
-
|
|
544
|
-
for (let i = 0; i < args.length; i++) {
|
|
545
|
-
if (args[i] === '--milestone' || args[i] === '-m') {
|
|
546
|
-
milestone = args[++i];
|
|
547
|
-
} else if (args[i].startsWith('--milestone=')) {
|
|
548
|
-
milestone = args[i].slice(12);
|
|
549
|
-
} else if (args[i] === '--status' || args[i] === '-s') {
|
|
550
|
-
status = parseStatus(args[++i]);
|
|
551
|
-
} else if (args[i].startsWith('--status=')) {
|
|
552
|
-
status = parseStatus(args[i].slice(9));
|
|
553
|
-
} else if (args[i] === '--all' || args[i] === '-a') {
|
|
554
|
-
showAll = true;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const hideOrphanTasks = config.defaults.hideOrphanTasks;
|
|
559
|
-
|
|
560
|
-
// By default, exclude tasks in completed milestones (unless --all)
|
|
561
|
-
const tasks = await storage.listTasks({
|
|
562
|
-
milestone,
|
|
563
|
-
status,
|
|
564
|
-
excludeCompletedMilestones: !showAll,
|
|
565
|
-
hideOrphanTasks,
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
if (flags.json) {
|
|
569
|
-
ui.json(tasks);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (tasks.length === 0) {
|
|
574
|
-
ui.info('No tasks. Use: tmux-team pm task add <title>');
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
console.log();
|
|
579
|
-
ui.table(
|
|
580
|
-
['ID', 'TITLE', 'STATUS', 'MILESTONE'],
|
|
581
|
-
tasks.map((t) => [t.id, t.title.slice(0, 40), formatStatus(t.status), t.milestone || '-'])
|
|
582
|
-
);
|
|
583
|
-
console.log();
|
|
584
|
-
|
|
585
|
-
if (!flags.json) {
|
|
586
|
-
const modeHint = hideOrphanTasks
|
|
587
|
-
? 'hiding tasks without milestones'
|
|
588
|
-
: 'showing tasks without milestones';
|
|
589
|
-
const toggleHint = hideOrphanTasks ? 'false' : 'true';
|
|
590
|
-
ui.info(
|
|
591
|
-
`List mode: ${modeHint}. Use: ${colors.cyan(`tmt config set hideOrphanTasks ${toggleHint}`)}`
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
async function cmdTaskShow(ctx: Context, args: string[]): Promise<void> {
|
|
597
|
-
requirePermission(ctx, PermissionChecks.taskShow());
|
|
598
|
-
|
|
599
|
-
const { ui, flags } = ctx;
|
|
600
|
-
const { storage } = await requireTeam(ctx);
|
|
601
|
-
|
|
602
|
-
const id = args[0];
|
|
603
|
-
if (!id) {
|
|
604
|
-
ui.error('Usage: tmux-team pm task show <id>');
|
|
605
|
-
ctx.exit(ExitCodes.ERROR);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const task = await storage.getTask(id);
|
|
609
|
-
if (!task) {
|
|
610
|
-
ui.error(`Task ${id} not found`);
|
|
611
|
-
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
if (flags.json) {
|
|
615
|
-
ui.json(task);
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
console.log();
|
|
620
|
-
console.log(colors.cyan(`Task #${task.id}: ${task.title}`));
|
|
621
|
-
console.log(`Status: ${formatStatus(task.status)}`);
|
|
622
|
-
if (task.milestone) console.log(`Milestone: #${task.milestone}`);
|
|
623
|
-
if (task.assignee) console.log(`Assignee: ${task.assignee}`);
|
|
624
|
-
console.log(`Created: ${task.createdAt}`);
|
|
625
|
-
console.log(`Updated: ${task.updatedAt}`);
|
|
626
|
-
console.log();
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
async function cmdTaskUpdate(ctx: Context, args: string[]): Promise<void> {
|
|
630
|
-
const { ui, flags } = ctx;
|
|
631
|
-
|
|
632
|
-
// Parse: <id> --status <status> [--assignee <name>]
|
|
633
|
-
const id = args[0];
|
|
634
|
-
if (!id) {
|
|
635
|
-
ui.error('Usage: tmux-team pm task update <id> --status <status>');
|
|
636
|
-
ctx.exit(ExitCodes.ERROR);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
let status: TaskStatus | undefined;
|
|
640
|
-
let assignee: string | undefined;
|
|
641
|
-
|
|
642
|
-
for (let i = 1; i < args.length; i++) {
|
|
643
|
-
if (args[i] === '--status' || args[i] === '-s') {
|
|
644
|
-
status = parseStatus(args[++i]);
|
|
645
|
-
} else if (args[i].startsWith('--status=')) {
|
|
646
|
-
status = parseStatus(args[i].slice(9));
|
|
647
|
-
} else if (args[i] === '--assignee' || args[i] === '-a') {
|
|
648
|
-
assignee = args[++i];
|
|
649
|
-
} else if (args[i].startsWith('--assignee=')) {
|
|
650
|
-
assignee = args[i].slice(11);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Check permissions based on which fields are being updated
|
|
655
|
-
const fields: string[] = [];
|
|
656
|
-
if (status) fields.push('status');
|
|
657
|
-
if (assignee) fields.push('assignee');
|
|
658
|
-
if (fields.length > 0) {
|
|
659
|
-
requirePermission(ctx, PermissionChecks.taskUpdate(fields));
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const { storage } = await requireTeam(ctx);
|
|
663
|
-
const task = await storage.getTask(id);
|
|
664
|
-
if (!task) {
|
|
665
|
-
ui.error(`Task ${id} not found`);
|
|
666
|
-
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const updates: { status?: TaskStatus; assignee?: string } = {};
|
|
670
|
-
if (status) updates.status = status;
|
|
671
|
-
if (assignee) updates.assignee = assignee;
|
|
672
|
-
|
|
673
|
-
if (Object.keys(updates).length === 0) {
|
|
674
|
-
ui.error('No updates specified. Use --status or --assignee');
|
|
675
|
-
ctx.exit(ExitCodes.ERROR);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const updated = await storage.updateTask(id, updates);
|
|
679
|
-
|
|
680
|
-
if (status) {
|
|
681
|
-
await storage.appendEvent({
|
|
682
|
-
event: 'task_updated',
|
|
683
|
-
id,
|
|
684
|
-
field: 'status',
|
|
685
|
-
from: task.status,
|
|
686
|
-
to: status,
|
|
687
|
-
actor: 'human',
|
|
688
|
-
ts: new Date().toISOString(),
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (flags.json) {
|
|
693
|
-
ui.json(updated);
|
|
694
|
-
} else {
|
|
695
|
-
ui.success(`Updated task #${id}`);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
async function cmdTaskDone(ctx: Context, args: string[]): Promise<void> {
|
|
700
|
-
requirePermission(ctx, PermissionChecks.taskUpdate(['status']));
|
|
701
|
-
|
|
702
|
-
const { ui, flags } = ctx;
|
|
703
|
-
const { storage } = await requireTeam(ctx);
|
|
704
|
-
|
|
705
|
-
const id = args[0];
|
|
706
|
-
if (!id) {
|
|
707
|
-
ui.error('Usage: tmux-team pm task done <id>');
|
|
708
|
-
ctx.exit(ExitCodes.ERROR);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const task = await storage.getTask(id);
|
|
712
|
-
if (!task) {
|
|
713
|
-
ui.error(`Task ${id} not found`);
|
|
714
|
-
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const updated = await storage.updateTask(id, { status: 'done' });
|
|
718
|
-
|
|
719
|
-
await storage.appendEvent({
|
|
720
|
-
event: 'task_updated',
|
|
721
|
-
id,
|
|
722
|
-
field: 'status',
|
|
723
|
-
from: task.status,
|
|
724
|
-
to: 'done',
|
|
725
|
-
actor: 'human',
|
|
726
|
-
ts: new Date().toISOString(),
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
if (flags.json) {
|
|
730
|
-
ui.json(updated);
|
|
731
|
-
} else {
|
|
732
|
-
ui.success(`Task #${id} marked as done`);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
async function cmdTaskDoc(ctx: Context, args: string[]): Promise<void> {
|
|
737
|
-
const { ui, flags } = ctx;
|
|
738
|
-
|
|
739
|
-
// Parse arguments
|
|
740
|
-
let id: string | undefined;
|
|
741
|
-
let body: string | undefined;
|
|
742
|
-
let bodyFile: string | undefined;
|
|
743
|
-
let showRef = false;
|
|
744
|
-
let editMode = false;
|
|
745
|
-
|
|
746
|
-
for (let i = 0; i < args.length; i++) {
|
|
747
|
-
const arg = args[i];
|
|
748
|
-
if (arg === 'ref') {
|
|
749
|
-
showRef = true;
|
|
750
|
-
} else if (arg === '--edit' || arg === '-e') {
|
|
751
|
-
editMode = true;
|
|
752
|
-
} else if (arg === '--body' || arg === '-b') {
|
|
753
|
-
body = args[++i];
|
|
754
|
-
if (body === undefined) {
|
|
755
|
-
ui.error('--body requires a value');
|
|
756
|
-
ctx.exit(ExitCodes.ERROR);
|
|
757
|
-
}
|
|
758
|
-
} else if (arg.startsWith('--body=')) {
|
|
759
|
-
body = arg.slice(7);
|
|
760
|
-
} else if (arg === '--body-file' || arg === '-f') {
|
|
761
|
-
bodyFile = args[++i];
|
|
762
|
-
if (bodyFile === undefined) {
|
|
763
|
-
ui.error('--body-file requires a value');
|
|
764
|
-
ctx.exit(ExitCodes.ERROR);
|
|
765
|
-
}
|
|
766
|
-
} else if (arg.startsWith('--body-file=')) {
|
|
767
|
-
bodyFile = arg.slice(12);
|
|
768
|
-
} else if (!id) {
|
|
769
|
-
id = arg;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (!id) {
|
|
774
|
-
ui.error(
|
|
775
|
-
'Usage: tmux-team pm task doc <id> [ref | --edit | --body <text> | --body-file <path>]'
|
|
776
|
-
);
|
|
777
|
-
ctx.exit(ExitCodes.ERROR);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const isWriteMode = editMode || body !== undefined || bodyFile !== undefined;
|
|
781
|
-
|
|
782
|
-
// Check permission based on mode
|
|
783
|
-
if (isWriteMode) {
|
|
784
|
-
requirePermission(ctx, PermissionChecks.docUpdate());
|
|
785
|
-
} else {
|
|
786
|
-
requirePermission(ctx, PermissionChecks.docRead());
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const { storage } = await requireTeam(ctx);
|
|
790
|
-
const task = await storage.getTask(id);
|
|
791
|
-
if (!task) {
|
|
792
|
-
ui.error(`Task ${id} not found`);
|
|
793
|
-
ctx.exit(ExitCodes.PANE_NOT_FOUND);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Show reference (docPath)
|
|
797
|
-
if (showRef) {
|
|
798
|
-
if (flags.json) {
|
|
799
|
-
ui.json({ id, docPath: task.docPath });
|
|
800
|
-
} else {
|
|
801
|
-
console.log(task.docPath || '(no docPath)');
|
|
802
|
-
}
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// --body: set content directly
|
|
807
|
-
if (body !== undefined) {
|
|
808
|
-
await storage.setTaskDoc(id, body);
|
|
809
|
-
ui.success(`Saved documentation for task #${id}`);
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// --body-file: read content from file
|
|
814
|
-
if (bodyFile !== undefined) {
|
|
815
|
-
if (!fs.existsSync(bodyFile)) {
|
|
816
|
-
ui.error(`File not found: ${bodyFile}`);
|
|
817
|
-
ctx.exit(ExitCodes.ERROR);
|
|
818
|
-
}
|
|
819
|
-
const content = fs.readFileSync(bodyFile, 'utf-8');
|
|
820
|
-
await storage.setTaskDoc(id, content);
|
|
821
|
-
ui.success(`Saved documentation for task #${id} (from ${bodyFile})`);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const doc = await storage.getTaskDoc(id);
|
|
826
|
-
|
|
827
|
-
// Default: print doc content
|
|
828
|
-
if (!editMode) {
|
|
829
|
-
if (flags.json) {
|
|
830
|
-
ui.json({ id, doc });
|
|
831
|
-
} else {
|
|
832
|
-
console.log(doc || '(empty)');
|
|
833
|
-
}
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Edit mode: open in editor using temp file
|
|
838
|
-
const editor = process.env.EDITOR || 'vim';
|
|
839
|
-
const tempDir = os.tmpdir();
|
|
840
|
-
const tempFile = path.join(tempDir, `tmux-team-task-${id}.md`);
|
|
841
|
-
|
|
842
|
-
// Write current content to temp file
|
|
843
|
-
fs.writeFileSync(tempFile, doc || `# ${task.title}\n\n`);
|
|
844
|
-
|
|
845
|
-
const { spawnSync } = await import('child_process');
|
|
846
|
-
spawnSync(editor, [tempFile], { stdio: 'inherit' });
|
|
847
|
-
|
|
848
|
-
// Read edited content and sync back to storage
|
|
849
|
-
const newContent = fs.readFileSync(tempFile, 'utf-8');
|
|
850
|
-
await storage.setTaskDoc(id, newContent);
|
|
851
|
-
|
|
852
|
-
// Clean up temp file
|
|
853
|
-
try {
|
|
854
|
-
fs.unlinkSync(tempFile);
|
|
855
|
-
} catch {
|
|
856
|
-
// Ignore cleanup errors
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
ui.success(`Saved documentation for task #${id}`);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
export async function cmdPmLog(ctx: Context, args: string[]): Promise<void> {
|
|
863
|
-
requirePermission(ctx, PermissionChecks.logRead());
|
|
864
|
-
|
|
865
|
-
const { ui, flags } = ctx;
|
|
866
|
-
const { storage } = await requireTeam(ctx);
|
|
867
|
-
|
|
868
|
-
// Parse --limit flag
|
|
869
|
-
let limit: number | undefined;
|
|
870
|
-
for (let i = 0; i < args.length; i++) {
|
|
871
|
-
if (args[i] === '--limit' || args[i] === '-n') {
|
|
872
|
-
limit = parseInt(args[++i], 10);
|
|
873
|
-
} else if (args[i].startsWith('--limit=')) {
|
|
874
|
-
limit = parseInt(args[i].slice(8), 10);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
const events = await storage.getEvents(limit);
|
|
879
|
-
|
|
880
|
-
if (flags.json) {
|
|
881
|
-
ui.json(events);
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
if (events.length === 0) {
|
|
886
|
-
ui.info('No events logged yet.');
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
console.log();
|
|
891
|
-
for (const event of events) {
|
|
892
|
-
const time = event.ts.slice(0, 19).replace('T', ' ');
|
|
893
|
-
const actor = colors.cyan(event.actor);
|
|
894
|
-
const action = colors.yellow(event.event);
|
|
895
|
-
const id = event.id ? `#${event.id}` : '';
|
|
896
|
-
console.log(`${colors.dim(time)} ${actor} ${action} ${id}`);
|
|
897
|
-
}
|
|
898
|
-
console.log();
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
export async function cmdPmList(ctx: Context, _args: string[]): Promise<void> {
|
|
902
|
-
requirePermission(ctx, PermissionChecks.teamList());
|
|
903
|
-
|
|
904
|
-
const { ui, flags, paths } = ctx;
|
|
905
|
-
|
|
906
|
-
const teams = listTeams(paths.globalDir);
|
|
907
|
-
const currentTeamId = findCurrentTeamId(process.cwd(), paths.globalDir);
|
|
908
|
-
|
|
909
|
-
if (flags.json) {
|
|
910
|
-
ui.json({ teams, currentTeamId });
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
if (teams.length === 0) {
|
|
915
|
-
ui.info("No teams. Use: tmux-team pm init --name 'My Project'");
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
console.log();
|
|
920
|
-
ui.table(
|
|
921
|
-
['', 'ID', 'NAME', 'BACKEND', 'CREATED'],
|
|
922
|
-
teams.map((t) => [
|
|
923
|
-
t.id === currentTeamId ? colors.green('→') : ' ',
|
|
924
|
-
t.id.slice(0, 8) + '...',
|
|
925
|
-
t.name,
|
|
926
|
-
t.backend === 'github' ? colors.cyan('github') : colors.dim('fs'),
|
|
927
|
-
t.createdAt.slice(0, 10),
|
|
928
|
-
])
|
|
929
|
-
);
|
|
930
|
-
console.log();
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// ─────────────────────────────────────────────────────────────
|
|
934
|
-
// Main PM router
|
|
935
|
-
// ─────────────────────────────────────────────────────────────
|
|
936
|
-
|
|
937
|
-
export async function cmdPm(ctx: Context, args: string[]): Promise<void> {
|
|
938
|
-
const [subcommand, ...rest] = args;
|
|
939
|
-
|
|
940
|
-
// Handle shorthands
|
|
941
|
-
const cmd = subcommand === 'm' ? 'milestone' : subcommand === 't' ? 'task' : subcommand;
|
|
942
|
-
|
|
943
|
-
switch (cmd) {
|
|
944
|
-
case 'init':
|
|
945
|
-
return cmdPmInit(ctx, rest);
|
|
946
|
-
case 'milestone':
|
|
947
|
-
return cmdPmMilestone(ctx, rest);
|
|
948
|
-
case 'task':
|
|
949
|
-
return cmdPmTask(ctx, rest);
|
|
950
|
-
case 'log':
|
|
951
|
-
return cmdPmLog(ctx, rest);
|
|
952
|
-
case 'list':
|
|
953
|
-
case 'ls':
|
|
954
|
-
return cmdPmList(ctx, rest);
|
|
955
|
-
case undefined:
|
|
956
|
-
case 'help':
|
|
957
|
-
return cmdPmHelp(ctx);
|
|
958
|
-
default:
|
|
959
|
-
ctx.ui.error(`Unknown pm command: ${subcommand}. Run 'tmux-team pm help'`);
|
|
960
|
-
ctx.exit(ExitCodes.ERROR);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function cmdPmHelp(_ctx: Context): void {
|
|
965
|
-
console.log(`
|
|
966
|
-
${colors.cyan('tmux-team pm')} - Project management
|
|
967
|
-
|
|
968
|
-
${colors.yellow('COMMANDS')}
|
|
969
|
-
${colors.green('init')} [options] Create a new team/project
|
|
970
|
-
--name <name> Project name
|
|
971
|
-
--backend <fs|github> Storage backend (default: fs)
|
|
972
|
-
--repo <owner/repo> GitHub repo (required for github backend)
|
|
973
|
-
${colors.green('list')} List all teams
|
|
974
|
-
${colors.green('milestone')} add <name> [-d <desc>] Add milestone (shorthand: m)
|
|
975
|
-
${colors.green('milestone')} list List milestones
|
|
976
|
-
${colors.green('milestone')} done <id> Mark milestone complete
|
|
977
|
-
${colors.green('milestone')} delete <id> Delete milestone (rm)
|
|
978
|
-
${colors.green('milestone')} doc <id> [options] Print/update doc
|
|
979
|
-
ref: show path, --edit: edit, --body: set text, --body-file: set from file
|
|
980
|
-
${colors.green('task')} add <title> [--milestone] Add task (shorthand: t)
|
|
981
|
-
${colors.green('task')} list [options] List tasks (hides done milestones by default)
|
|
982
|
-
--all: include tasks in completed milestones
|
|
983
|
-
${colors.green('task')} show <id> Show task details
|
|
984
|
-
${colors.green('task')} update <id> --status <s> Update task status
|
|
985
|
-
${colors.green('task')} done <id> Mark task complete
|
|
986
|
-
${colors.green('task')} doc <id> [options] Print/update doc (same options as milestone doc)
|
|
987
|
-
${colors.green('log')} [--limit <n>] Show audit event log
|
|
988
|
-
|
|
989
|
-
${colors.yellow('BACKENDS')}
|
|
990
|
-
${colors.cyan('fs')} Local filesystem (default) - tasks in ~/.config/tmux-team/teams/
|
|
991
|
-
${colors.cyan('github')} GitHub Issues - tasks become issues, milestones sync with GH
|
|
992
|
-
|
|
993
|
-
${colors.yellow('SHORTHANDS')}
|
|
994
|
-
pm m = pm milestone
|
|
995
|
-
pm t = pm task
|
|
996
|
-
pm ls = pm list
|
|
997
|
-
|
|
998
|
-
${colors.yellow('EXAMPLES')}
|
|
999
|
-
# Local filesystem backend (default)
|
|
1000
|
-
tmux-team pm init --name "Auth Refactor"
|
|
1001
|
-
|
|
1002
|
-
# GitHub backend - uses gh CLI for auth
|
|
1003
|
-
tmux-team pm init --name "Sprint 1" --backend github --repo owner/repo
|
|
1004
|
-
|
|
1005
|
-
tmux-team pm m add "Phase 1"
|
|
1006
|
-
tmux-team pm t add "Implement login" --milestone 1
|
|
1007
|
-
tmux-team pm t list --status pending
|
|
1008
|
-
tmux-team pm t done 1
|
|
1009
|
-
tmux-team pm log --limit 10
|
|
1010
|
-
`);
|
|
1011
|
-
}
|