claude-autopm 2.3.0 → 2.5.0

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.
@@ -0,0 +1,777 @@
1
+ /**
2
+ * CLI Epic Commands
3
+ *
4
+ * Provides Epic management commands.
5
+ * Implements subcommands for epic lifecycle management.
6
+ *
7
+ * @module cli/commands/epic
8
+ * @requires ../../services/EpicService
9
+ * @requires fs-extra
10
+ * @requires ora
11
+ * @requires chalk
12
+ * @requires path
13
+ */
14
+
15
+ const EpicService = require('../../services/EpicService');
16
+ const fs = require('fs-extra');
17
+ const ora = require('ora');
18
+ const chalk = require('chalk');
19
+ const path = require('path');
20
+ const { spawn } = require('child_process');
21
+ const readline = require('readline');
22
+
23
+ /**
24
+ * Get epic directory path
25
+ * @param {string} name - Epic name
26
+ * @returns {string} Full path to epic directory
27
+ */
28
+ function getEpicPath(name) {
29
+ return path.join(process.cwd(), '.claude', 'epics', name);
30
+ }
31
+
32
+ /**
33
+ * Read epic file
34
+ * @param {string} name - Epic name
35
+ * @returns {Promise<string>} Epic content
36
+ * @throws {Error} If file doesn't exist or can't be read
37
+ */
38
+ async function readEpicFile(name) {
39
+ const epicPath = path.join(getEpicPath(name), 'epic.md');
40
+
41
+ const exists = await fs.pathExists(epicPath);
42
+ if (!exists) {
43
+ throw new Error(`Epic file not found: ${epicPath}`);
44
+ }
45
+
46
+ return await fs.readFile(epicPath, 'utf8');
47
+ }
48
+
49
+ /**
50
+ * List all epics
51
+ * @param {Object} argv - Command arguments
52
+ */
53
+ async function epicList(argv) {
54
+ const spinner = ora('Loading epics...').start();
55
+
56
+ try {
57
+ const epicService = new EpicService();
58
+ const epics = await epicService.listEpics();
59
+
60
+ if (epics.length === 0) {
61
+ spinner.info(chalk.yellow('No epics found'));
62
+ console.log(chalk.yellow('\nCreate your first epic with: autopm epic new <name>'));
63
+ return;
64
+ }
65
+
66
+ spinner.succeed(chalk.green(`Found ${epics.length} epic(s)`));
67
+
68
+ // Group by status
69
+ const grouped = {
70
+ 'Planning': [],
71
+ 'In Progress': [],
72
+ 'Completed': []
73
+ };
74
+
75
+ epics.forEach(epic => {
76
+ const category = epicService.categorizeStatus(epic.status);
77
+ if (category === 'planning') {
78
+ grouped['Planning'].push(epic);
79
+ } else if (category === 'in_progress') {
80
+ grouped['In Progress'].push(epic);
81
+ } else if (category === 'done') {
82
+ grouped['Completed'].push(epic);
83
+ } else {
84
+ grouped['Planning'].push(epic);
85
+ }
86
+ });
87
+
88
+ // Display grouped epics
89
+ console.log(chalk.green('\nšŸ“‹ Epics:\n'));
90
+
91
+ Object.entries(grouped).forEach(([status, statusEpics]) => {
92
+ if (statusEpics.length > 0) {
93
+ const statusColor = status === 'Completed' ? chalk.green :
94
+ status === 'In Progress' ? chalk.yellow :
95
+ chalk.blue;
96
+
97
+ console.log(statusColor(`\n${status}:`));
98
+
99
+ statusEpics.forEach((epic, index) => {
100
+ console.log(` ${index + 1}. ${chalk.bold(epic.name)}`);
101
+ console.log(` ${chalk.dim('Progress:')} ${epic.progress.padEnd(10)} ${chalk.dim('Tasks:')} ${epic.taskCount}`);
102
+
103
+ if (epic.githubIssue) {
104
+ console.log(` ${chalk.dim('GitHub:')} #${epic.githubIssue}`);
105
+ }
106
+
107
+ if (epic.created) {
108
+ console.log(` ${chalk.dim('Created:')} ${epic.created.split('T')[0]}`);
109
+ }
110
+ });
111
+ }
112
+ });
113
+
114
+ console.log(chalk.dim(`\nTotal: ${epics.length} epic(s)`));
115
+ console.log(chalk.dim('Use: autopm epic show <name> to view details\n'));
116
+
117
+ } catch (error) {
118
+ spinner.fail(chalk.red('Failed to list epics'));
119
+ console.error(chalk.red(`\nError: ${error.message}`));
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Show epic content
125
+ * @param {Object} argv - Command arguments
126
+ */
127
+ async function epicShow(argv) {
128
+ const spinner = ora(`Loading epic: ${argv.name}`).start();
129
+
130
+ try {
131
+ const content = await readEpicFile(argv.name);
132
+ const epicService = new EpicService();
133
+ const metadata = epicService.parseFrontmatter(content);
134
+
135
+ spinner.succeed(chalk.green('Epic loaded'));
136
+
137
+ // Display metadata table
138
+ console.log('\n' + chalk.bold('šŸ“Š Epic Metadata') + '\n');
139
+ console.log(chalk.gray('─'.repeat(50)) + '\n');
140
+
141
+ if (metadata) {
142
+ console.log(chalk.bold('Name: ') + (metadata.name || argv.name));
143
+ console.log(chalk.bold('Status: ') + (metadata.status || 'N/A'));
144
+ console.log(chalk.bold('Progress: ') + (metadata.progress || 'N/A'));
145
+ console.log(chalk.bold('Priority: ') + (metadata.priority || 'N/A'));
146
+ console.log(chalk.bold('Created: ') + (metadata.created ? metadata.created.split('T')[0] : 'N/A'));
147
+
148
+ if (metadata.github) {
149
+ console.log(chalk.bold('GitHub: ') + metadata.github);
150
+ }
151
+ }
152
+
153
+ console.log('\n' + chalk.gray('─'.repeat(80)) + '\n');
154
+ console.log(content);
155
+ console.log('\n' + chalk.gray('─'.repeat(80)) + '\n');
156
+
157
+ const epicPath = path.join(getEpicPath(argv.name), 'epic.md');
158
+ console.log(chalk.dim(`File: ${epicPath}\n`));
159
+
160
+ } catch (error) {
161
+ spinner.fail(chalk.red('Failed to show epic'));
162
+
163
+ if (error.message.includes('not found')) {
164
+ console.error(chalk.red(`\nError: ${error.message}`));
165
+ console.error(chalk.yellow('Use: autopm epic list to see available epics'));
166
+ } else {
167
+ console.error(chalk.red(`\nError: ${error.message}`));
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Create new epic
174
+ * @param {Object} argv - Command arguments
175
+ */
176
+ async function epicNew(argv) {
177
+ const spinner = ora(`Creating epic: ${argv.name}`).start();
178
+
179
+ try {
180
+ const epicPath = getEpicPath(argv.name);
181
+ const epicFilePath = path.join(epicPath, 'epic.md');
182
+
183
+ // Check if epic already exists
184
+ const exists = await fs.pathExists(epicFilePath);
185
+ if (exists) {
186
+ spinner.fail(chalk.red('Epic already exists'));
187
+ console.error(chalk.red(`\nError: Epic file already exists: ${epicFilePath}`));
188
+ console.error(chalk.yellow('Use: autopm epic edit ' + argv.name + ' to modify it'));
189
+ return;
190
+ }
191
+
192
+ // Create epic directory
193
+ await fs.ensureDir(epicPath);
194
+
195
+ spinner.stop();
196
+
197
+ // Handle --from-prd flag
198
+ if (argv['from-prd']) {
199
+ return await epicNewFromPRD(argv, epicPath, epicFilePath);
200
+ }
201
+
202
+ // Interactive prompts
203
+ const rl = readline.createInterface({
204
+ input: process.stdin,
205
+ output: process.stdout
206
+ });
207
+
208
+ const prompt = (question) => new Promise((resolve) => {
209
+ rl.question(question, resolve);
210
+ });
211
+
212
+ console.log(chalk.cyan('\nšŸš€ Creating new epic...\n'));
213
+
214
+ const title = await prompt(chalk.cyan('Title [' + argv.name + ']: ')) || argv.name;
215
+ const description = await prompt(chalk.cyan('Description: '));
216
+ const priority = await prompt(chalk.cyan('Priority (P0/P1/P2/P3) [P2]: ')) || 'P2';
217
+
218
+ rl.close();
219
+
220
+ // Build epic content
221
+ const now = new Date().toISOString();
222
+ const frontmatter = `---
223
+ name: ${argv.name}
224
+ status: backlog
225
+ created: ${now}
226
+ progress: 0%
227
+ prd: .claude/prds/${argv.name}.md
228
+ github: [Will be updated when synced to GitHub]
229
+ priority: ${priority}
230
+ ---`;
231
+
232
+ const content = `${frontmatter}
233
+
234
+ # Epic: ${title}
235
+
236
+ ## Overview
237
+ ${description || 'Epic description goes here.'}
238
+
239
+ ## Task Breakdown
240
+
241
+ Tasks will be added as epic progresses.
242
+ `;
243
+
244
+ // Write epic file
245
+ await fs.writeFile(epicFilePath, content);
246
+
247
+ console.log(chalk.green('\nāœ… Epic created successfully!'));
248
+ console.log(chalk.cyan(`šŸ“„ File: ${epicFilePath}\n`));
249
+
250
+ // Show next steps
251
+ console.log(chalk.bold('šŸ“‹ What You Can Do Next:\n'));
252
+ console.log(` ${chalk.cyan('1.')} Edit epic: ${chalk.yellow('autopm epic edit ' + argv.name)}`);
253
+ console.log(` ${chalk.cyan('2.')} Check status: ${chalk.yellow('autopm epic status ' + argv.name)}`);
254
+ console.log(` ${chalk.cyan('3.')} Start working: ${chalk.yellow('autopm epic start ' + argv.name)}`);
255
+ console.log(` ${chalk.cyan('4.')} Sync to GitHub: ${chalk.yellow('autopm epic sync ' + argv.name)}\n`);
256
+
257
+ } catch (error) {
258
+ spinner.fail(chalk.red('Failed to create epic'));
259
+ console.error(chalk.red(`\nError: ${error.message}`));
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Create new epic from PRD
265
+ * @param {Object} argv - Command arguments
266
+ * @param {string} epicPath - Path to epic directory
267
+ * @param {string} epicFilePath - Path to epic.md file
268
+ */
269
+ async function epicNewFromPRD(argv, epicPath, epicFilePath) {
270
+ const spinner = ora('Creating epic from PRD...').start();
271
+
272
+ try {
273
+ const prdPath = path.join(process.cwd(), '.claude', 'prds', `${argv['from-prd']}.md`);
274
+ const prdExists = await fs.pathExists(prdPath);
275
+
276
+ if (!prdExists) {
277
+ spinner.fail(chalk.red('PRD not found'));
278
+ console.error(chalk.red(`\nError: PRD file not found: ${prdPath}`));
279
+ console.error(chalk.yellow('Use: autopm prd list to see available PRDs'));
280
+ return;
281
+ }
282
+
283
+ const prdContent = await fs.readFile(prdPath, 'utf8');
284
+ const epicService = new EpicService();
285
+ const prdMetadata = epicService.parseFrontmatter(prdContent);
286
+
287
+ // Build epic from PRD
288
+ const now = new Date().toISOString();
289
+ const frontmatter = `---
290
+ name: ${argv.name}
291
+ status: backlog
292
+ created: ${now}
293
+ progress: 0%
294
+ prd: .claude/prds/${argv['from-prd']}.md
295
+ github: [Will be updated when synced to GitHub]
296
+ priority: ${prdMetadata?.priority || 'P2'}
297
+ ---`;
298
+
299
+ const content = `${frontmatter}
300
+
301
+ # Epic: ${argv.name}
302
+
303
+ ## Overview
304
+ Created from PRD: ${argv['from-prd']}
305
+
306
+ ${prdContent.split('## Problem Statement')[1]?.split('##')[0] || ''}
307
+
308
+ ## Task Breakdown
309
+
310
+ Tasks will be generated from PRD analysis.
311
+ `;
312
+
313
+ await fs.writeFile(epicFilePath, content);
314
+
315
+ spinner.succeed(chalk.green('Epic created from PRD'));
316
+
317
+ console.log(chalk.green('\nāœ… Epic created successfully!'));
318
+ console.log(chalk.cyan(`šŸ“„ File: ${epicFilePath}`));
319
+ console.log(chalk.cyan(`šŸ“‹ Source PRD: ${prdPath}\n`));
320
+
321
+ } catch (error) {
322
+ spinner.fail(chalk.red('Failed to create epic from PRD'));
323
+ console.error(chalk.red(`\nError: ${error.message}`));
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Edit epic in editor
329
+ * @param {Object} argv - Command arguments
330
+ */
331
+ async function epicEdit(argv) {
332
+ const spinner = ora(`Opening epic: ${argv.name}`).start();
333
+
334
+ try {
335
+ const epicFilePath = path.join(getEpicPath(argv.name), 'epic.md');
336
+
337
+ // Check if file exists
338
+ const exists = await fs.pathExists(epicFilePath);
339
+ if (!exists) {
340
+ spinner.fail(chalk.red('Epic not found'));
341
+ console.error(chalk.red(`\nError: Epic file not found: ${epicFilePath}`));
342
+ console.error(chalk.yellow('Use: autopm epic list to see available epics'));
343
+ return;
344
+ }
345
+
346
+ spinner.succeed(chalk.green('Opening editor...'));
347
+
348
+ // Determine editor
349
+ const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
350
+
351
+ // Spawn editor
352
+ const child = spawn(editor, [epicFilePath], {
353
+ stdio: 'inherit',
354
+ cwd: process.cwd()
355
+ });
356
+
357
+ // Wait for editor to close
358
+ await new Promise((resolve, reject) => {
359
+ child.on('close', (code) => {
360
+ if (code === 0) {
361
+ console.log(chalk.green('\nāœ“ Epic saved'));
362
+ resolve();
363
+ } else {
364
+ reject(new Error(`Editor exited with code ${code}`));
365
+ }
366
+ });
367
+ child.on('error', reject);
368
+ });
369
+
370
+ } catch (error) {
371
+ spinner.fail(chalk.red('Failed to edit epic'));
372
+ console.error(chalk.red(`\nError: ${error.message}`));
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Show epic status
378
+ * @param {Object} argv - Command arguments
379
+ */
380
+ async function epicStatus(argv) {
381
+ const spinner = ora(`Analyzing epic: ${argv.name}`).start();
382
+
383
+ try {
384
+ const epicService = new EpicService();
385
+ const epic = await epicService.getEpic(argv.name);
386
+
387
+ spinner.succeed(chalk.green('Status analyzed'));
388
+
389
+ // Display status
390
+ console.log('\n' + chalk.bold('šŸ“Š Epic Status Report') + '\n');
391
+ console.log(chalk.gray('─'.repeat(50)) + '\n');
392
+
393
+ console.log(chalk.bold('Metadata:'));
394
+ console.log(` Name: ${epic.name}`);
395
+ console.log(` Status: ${chalk.yellow(epic.status)}`);
396
+ console.log(` Priority: ${chalk.red(epic.priority || 'P2')}`);
397
+ console.log(` Created: ${epic.created ? epic.created.split('T')[0] : 'N/A'}`);
398
+
399
+ if (epic.github && epic.githubIssue) {
400
+ console.log(` GitHub: #${epic.githubIssue}`);
401
+ }
402
+
403
+ // Progress bar
404
+ const progressPercent = parseInt(epic.progress) || 0;
405
+ const progressBar = epicService.generateProgressBar(progressPercent, 20);
406
+
407
+ console.log('\n' + chalk.bold('Progress:') + ` ${progressPercent}%`);
408
+ console.log(` [${progressPercent >= 80 ? chalk.green(progressBar.bar.slice(1, -1)) :
409
+ progressPercent >= 50 ? chalk.yellow(progressBar.bar.slice(1, -1)) :
410
+ chalk.red(progressBar.bar.slice(1, -1))}]`);
411
+
412
+ // Task statistics
413
+ console.log('\n' + chalk.bold('Tasks:'));
414
+ console.log(` Total: ${epic.taskCount}`);
415
+
416
+ console.log('\n' + chalk.gray('─'.repeat(50)) + '\n');
417
+
418
+ const epicPath = path.join(getEpicPath(argv.name), 'epic.md');
419
+ console.log(chalk.dim(`File: ${epicPath}\n`));
420
+
421
+ } catch (error) {
422
+ spinner.fail(chalk.red('Failed to analyze status'));
423
+
424
+ if (error.message.includes('not found')) {
425
+ console.error(chalk.red(`\nError: ${error.message}`));
426
+ } else {
427
+ console.error(chalk.red(`\nError: ${error.message}`));
428
+ }
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Validate epic structure
434
+ * @param {Object} argv - Command arguments
435
+ */
436
+ async function epicValidate(argv) {
437
+ const spinner = ora(`Validating epic: ${argv.name}`).start();
438
+
439
+ try {
440
+ const epicService = new EpicService();
441
+ const result = await epicService.validateEpicStructure(argv.name);
442
+
443
+ if (result.valid) {
444
+ spinner.succeed(chalk.green('Epic is valid'));
445
+ console.log(chalk.green('\nāœ“ Validation passed - Epic structure is correct'));
446
+ } else {
447
+ spinner.fail(chalk.red(`Epic validation failed - ${result.issues.length} issues found`));
448
+ console.error(chalk.red(`\nāœ— Validation failed - ${result.issues.length} issue(s):`));
449
+ result.issues.forEach((issue, index) => {
450
+ console.error(chalk.red(` ${index + 1}. ${issue}`));
451
+ });
452
+ process.exit(1);
453
+ }
454
+ } catch (error) {
455
+ spinner.fail(chalk.red('Failed to validate epic'));
456
+
457
+ if (error.message.includes('not found')) {
458
+ console.error(chalk.red(`\nāœ— Error: Epic not found`));
459
+ console.error(chalk.red(` ${error.message}`));
460
+ console.error(chalk.yellow('\nšŸ’” Use: autopm epic list to see available epics'));
461
+ } else {
462
+ console.error(chalk.red(`\nāœ— Error: ${error.message}`));
463
+ }
464
+ process.exit(1);
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Start working on epic
470
+ * @param {Object} argv - Command arguments
471
+ */
472
+ async function epicStart(argv) {
473
+ const spinner = ora(`Starting epic: ${argv.name}`).start();
474
+
475
+ try {
476
+ const epicFilePath = path.join(getEpicPath(argv.name), 'epic.md');
477
+
478
+ // Check if file exists
479
+ const exists = await fs.pathExists(epicFilePath);
480
+ if (!exists) {
481
+ spinner.fail(chalk.red('Epic not found'));
482
+ console.error(chalk.red(`\nError: Epic file not found: ${epicFilePath}`));
483
+ console.error(chalk.yellow('Use: autopm epic list to see available epics'));
484
+ return;
485
+ }
486
+
487
+ // Read current content
488
+ let content = await fs.readFile(epicFilePath, 'utf8');
489
+
490
+ // Update status to in-progress
491
+ content = content.replace(/^status:\s*.+$/m, 'status: in-progress');
492
+
493
+ // Add started date if not present
494
+ if (!content.includes('started:')) {
495
+ const now = new Date().toISOString();
496
+ content = content.replace(/^(created:.+)$/m, `$1\nstarted: ${now}`);
497
+ }
498
+
499
+ // Write updated content
500
+ await fs.writeFile(epicFilePath, content);
501
+
502
+ spinner.succeed(chalk.green('Epic started'));
503
+
504
+ console.log(chalk.green('\nāœ… Epic is now in progress!'));
505
+ console.log(chalk.cyan(`šŸ“„ File: ${epicFilePath}\n`));
506
+
507
+ console.log(chalk.bold('šŸ“‹ What You Can Do Next:\n'));
508
+ console.log(` ${chalk.cyan('1.')} Check status: ${chalk.yellow('autopm epic status ' + argv.name)}`);
509
+ console.log(` ${chalk.cyan('2.')} Edit epic: ${chalk.yellow('autopm epic edit ' + argv.name)}`);
510
+ console.log(` ${chalk.cyan('3.')} Close when done: ${chalk.yellow('autopm epic close ' + argv.name)}\n`);
511
+
512
+ } catch (error) {
513
+ spinner.fail(chalk.red('Failed to start epic'));
514
+ console.error(chalk.red(`\nError: ${error.message}`));
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Close epic
520
+ * @param {Object} argv - Command arguments
521
+ */
522
+ async function epicClose(argv) {
523
+ const spinner = ora(`Closing epic: ${argv.name}`).start();
524
+
525
+ try {
526
+ const epicFilePath = path.join(getEpicPath(argv.name), 'epic.md');
527
+
528
+ // Check if file exists
529
+ const exists = await fs.pathExists(epicFilePath);
530
+ if (!exists) {
531
+ spinner.fail(chalk.red('Epic not found'));
532
+ console.error(chalk.red(`\nError: Epic file not found: ${epicFilePath}`));
533
+ console.error(chalk.yellow('Use: autopm epic list to see available epics'));
534
+ return;
535
+ }
536
+
537
+ // Read current content
538
+ let content = await fs.readFile(epicFilePath, 'utf8');
539
+
540
+ // Update status to completed
541
+ content = content.replace(/^status:\s*.+$/m, 'status: completed');
542
+
543
+ // Update progress to 100%
544
+ content = content.replace(/^progress:\s*.+$/m, 'progress: 100%');
545
+
546
+ // Add completed date if not present
547
+ if (!content.includes('completed:')) {
548
+ const now = new Date().toISOString();
549
+ content = content.replace(/^(created:.+)$/m, `$1\ncompleted: ${now}`);
550
+ }
551
+
552
+ // Write updated content
553
+ await fs.writeFile(epicFilePath, content);
554
+
555
+ spinner.succeed(chalk.green('Epic closed'));
556
+
557
+ console.log(chalk.green('\nāœ… Epic completed!'));
558
+ console.log(chalk.cyan(`šŸ“„ File: ${epicFilePath}\n`));
559
+
560
+ console.log(chalk.bold('šŸ“‹ What You Can Do Next:\n'));
561
+ console.log(` ${chalk.cyan('1.')} Check status: ${chalk.yellow('autopm epic status ' + argv.name)}`);
562
+ console.log(` ${chalk.cyan('2.')} View epic: ${chalk.yellow('autopm epic show ' + argv.name)}\n`);
563
+
564
+ } catch (error) {
565
+ spinner.fail(chalk.red('Failed to close epic'));
566
+ console.error(chalk.red(`\nError: ${error.message}`));
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Sync epic with GitHub
572
+ * @param {Object} argv - Command arguments
573
+ */
574
+ async function epicSync(argv) {
575
+ const spinner = ora(`Syncing epic: ${argv.name}`).start();
576
+
577
+ try {
578
+ const epicService = new EpicService();
579
+ const epic = await epicService.getEpic(argv.name);
580
+
581
+ // TODO: Implement GitHub integration
582
+ // For now, just show a message
583
+ spinner.info(chalk.yellow('GitHub sync not yet implemented'));
584
+
585
+ console.log(chalk.yellow('\nāš ļø GitHub sync feature coming soon!\n'));
586
+
587
+ console.log(chalk.dim('This feature will:'));
588
+ console.log(chalk.dim(' - Create GitHub issue if not exists'));
589
+ console.log(chalk.dim(' - Update existing GitHub issue'));
590
+ console.log(chalk.dim(' - Sync epic status and progress\n'));
591
+
592
+ console.log(chalk.bold('For now, you can:'));
593
+ console.log(` ${chalk.cyan('1.')} View epic: ${chalk.yellow('autopm epic show ' + argv.name)}`);
594
+ console.log(` ${chalk.cyan('2.')} Check status: ${chalk.yellow('autopm epic status ' + argv.name)}\n`);
595
+
596
+ } catch (error) {
597
+ spinner.fail(chalk.red('Failed to sync epic'));
598
+ console.error(chalk.red(`\nError: ${error.message}`));
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Command builder - registers all subcommands
604
+ * @param {Object} yargs - Yargs instance
605
+ * @returns {Object} Configured yargs instance
606
+ */
607
+ function builder(yargs) {
608
+ return yargs
609
+ .command(
610
+ 'list',
611
+ 'List all epics',
612
+ (yargs) => {
613
+ return yargs
614
+ .option('status', {
615
+ describe: 'Filter by status (planning/in-progress/completed)',
616
+ type: 'string'
617
+ })
618
+ .example('autopm epic list', 'Show all epics')
619
+ .example('autopm epic list --status in-progress', 'Show active epics');
620
+ },
621
+ epicList
622
+ )
623
+ .command(
624
+ 'show <name>',
625
+ 'Display epic content',
626
+ (yargs) => {
627
+ return yargs
628
+ .positional('name', {
629
+ describe: 'Epic name',
630
+ type: 'string'
631
+ })
632
+ .example('autopm epic show user-auth', 'Display epic details');
633
+ },
634
+ epicShow
635
+ )
636
+ .command(
637
+ 'new <name>',
638
+ 'Create new epic',
639
+ (yargs) => {
640
+ return yargs
641
+ .positional('name', {
642
+ describe: 'Epic name (use-kebab-case)',
643
+ type: 'string'
644
+ })
645
+ .option('from-prd', {
646
+ describe: 'Create from PRD',
647
+ type: 'string'
648
+ })
649
+ .option('template', {
650
+ describe: 'Template to use',
651
+ type: 'string'
652
+ })
653
+ .example('autopm epic new user-auth', 'Create new epic')
654
+ .example('autopm epic new user-auth --from-prd my-feature', 'Create from PRD');
655
+ },
656
+ epicNew
657
+ )
658
+ .command(
659
+ 'edit <name>',
660
+ 'Edit epic in your editor',
661
+ (yargs) => {
662
+ return yargs
663
+ .positional('name', {
664
+ describe: 'Epic name',
665
+ type: 'string'
666
+ })
667
+ .example('autopm epic edit user-auth', 'Open epic in editor')
668
+ .example('EDITOR=code autopm epic edit user-auth', 'Open in VS Code');
669
+ },
670
+ epicEdit
671
+ )
672
+ .command(
673
+ 'status <name>',
674
+ 'Show epic status and progress',
675
+ (yargs) => {
676
+ return yargs
677
+ .positional('name', {
678
+ describe: 'Epic name',
679
+ type: 'string'
680
+ })
681
+ .example('autopm epic status user-auth', 'Show epic status report');
682
+ },
683
+ epicStatus
684
+ )
685
+ .command(
686
+ 'validate <name>',
687
+ 'Validate epic structure',
688
+ (yargs) => {
689
+ return yargs
690
+ .positional('name', {
691
+ describe: 'Epic name',
692
+ type: 'string'
693
+ })
694
+ .example('autopm epic validate user-auth', 'Validate epic structure');
695
+ },
696
+ epicValidate
697
+ )
698
+ .command(
699
+ 'start <name>',
700
+ 'Start working on epic',
701
+ (yargs) => {
702
+ return yargs
703
+ .positional('name', {
704
+ describe: 'Epic name',
705
+ type: 'string'
706
+ })
707
+ .example('autopm epic start user-auth', 'Mark epic as in-progress');
708
+ },
709
+ epicStart
710
+ )
711
+ .command(
712
+ 'close <name>',
713
+ 'Close epic',
714
+ (yargs) => {
715
+ return yargs
716
+ .positional('name', {
717
+ describe: 'Epic name',
718
+ type: 'string'
719
+ })
720
+ .example('autopm epic close user-auth', 'Mark epic as completed');
721
+ },
722
+ epicClose
723
+ )
724
+ .command(
725
+ 'sync <name>',
726
+ 'Sync epic with GitHub',
727
+ (yargs) => {
728
+ return yargs
729
+ .positional('name', {
730
+ describe: 'Epic name',
731
+ type: 'string'
732
+ })
733
+ .example('autopm epic sync user-auth', 'Sync epic to GitHub issue');
734
+ },
735
+ epicSync
736
+ )
737
+ .demandCommand(1, 'You must specify an epic action')
738
+ .strictCommands()
739
+ .help();
740
+ }
741
+
742
+ /**
743
+ * Command export
744
+ */
745
+ module.exports = {
746
+ command: 'epic',
747
+ describe: 'Manage epics and epic lifecycle',
748
+ builder,
749
+ handler: (argv) => {
750
+ if (!argv._.includes('epic') || argv._.length === 1) {
751
+ console.log(chalk.yellow('\nPlease specify an epic command\n'));
752
+ console.log('Usage: autopm epic <command>\n');
753
+ console.log('Available commands:');
754
+ console.log(' list List all epics');
755
+ console.log(' show <name> Display epic');
756
+ console.log(' new <name> Create new epic');
757
+ console.log(' edit <name> Edit epic');
758
+ console.log(' status <name> Show epic status');
759
+ console.log(' validate <name> Validate epic');
760
+ console.log(' start <name> Start working on epic');
761
+ console.log(' close <name> Close epic');
762
+ console.log(' sync <name> Sync with GitHub');
763
+ console.log('\nUse: autopm epic <command> --help for more info\n');
764
+ }
765
+ },
766
+ handlers: {
767
+ list: epicList,
768
+ show: epicShow,
769
+ new: epicNew,
770
+ edit: epicEdit,
771
+ status: epicStatus,
772
+ validate: epicValidate,
773
+ start: epicStart,
774
+ close: epicClose,
775
+ sync: epicSync
776
+ }
777
+ };