claude-autopm 2.2.2 ā 2.4.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.
- package/bin/autopm.js +2 -2
- package/lib/ai-providers/ClaudeProvider.js +15 -0
- package/lib/cli/commands/epic.js +777 -0
- package/lib/cli/commands/prd.js +186 -1
- package/lib/services/EpicService.js +284 -10
- package/lib/services/PRDService.js +49 -0
- package/package.json +1 -1
package/lib/cli/commands/prd.js
CHANGED
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const PRDService = require('../../services/PRDService');
|
|
16
|
+
const ClaudeProvider = require('../../ai-providers/ClaudeProvider');
|
|
16
17
|
const fs = require('fs-extra');
|
|
17
18
|
const ora = require('ora');
|
|
18
19
|
const chalk = require('chalk');
|
|
19
20
|
const path = require('path');
|
|
20
21
|
const { spawn } = require('child_process');
|
|
22
|
+
const readline = require('readline');
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Get PRD file path
|
|
@@ -304,6 +306,12 @@ async function prdStatus(argv) {
|
|
|
304
306
|
* @param {Object} argv - Command arguments
|
|
305
307
|
*/
|
|
306
308
|
async function prdNew(argv) {
|
|
309
|
+
// Check if AI mode is enabled
|
|
310
|
+
if (argv.ai) {
|
|
311
|
+
return await prdNewWithAI(argv);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Standard mode: spawn prd-new.js
|
|
307
315
|
const spinner = ora(`Creating PRD: ${argv.name}`).start();
|
|
308
316
|
|
|
309
317
|
try {
|
|
@@ -397,6 +405,171 @@ async function prdNew(argv) {
|
|
|
397
405
|
}
|
|
398
406
|
}
|
|
399
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Create new PRD with AI assistance
|
|
410
|
+
* @param {Object} argv - Command arguments
|
|
411
|
+
*/
|
|
412
|
+
async function prdNewWithAI(argv) {
|
|
413
|
+
console.log(chalk.cyan(`\nš¤ AI-Powered PRD Creation: ${argv.name}`));
|
|
414
|
+
console.log(chalk.cyan('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n'));
|
|
415
|
+
|
|
416
|
+
const spinner = ora('Initializing...').start();
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
// Check if PRD already exists
|
|
420
|
+
const prdPath = getPrdPath(argv.name);
|
|
421
|
+
const exists = await fs.pathExists(prdPath);
|
|
422
|
+
if (exists) {
|
|
423
|
+
spinner.fail(chalk.red('PRD already exists'));
|
|
424
|
+
console.error(chalk.red(`\nError: PRD file already exists: ${prdPath}`));
|
|
425
|
+
console.error(chalk.yellow('Use: autopm prd edit ' + argv.name + ' to modify it'));
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Create PRDs directory if needed
|
|
430
|
+
const prdsDir = path.join(process.cwd(), '.claude', 'prds');
|
|
431
|
+
await fs.ensureDir(prdsDir);
|
|
432
|
+
|
|
433
|
+
spinner.stop();
|
|
434
|
+
|
|
435
|
+
// Gather information from user
|
|
436
|
+
const rl = readline.createInterface({
|
|
437
|
+
input: process.stdin,
|
|
438
|
+
output: process.stdout
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const prompt = (question) => new Promise((resolve) => {
|
|
442
|
+
rl.question(question, resolve);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
console.log(chalk.cyan('š§ Let\'s gather some information for AI...\n'));
|
|
446
|
+
|
|
447
|
+
const context = {};
|
|
448
|
+
|
|
449
|
+
// Product Vision
|
|
450
|
+
console.log(chalk.bold('š Product Vision'));
|
|
451
|
+
console.log(chalk.gray('What problem are you solving? What\'s the vision?'));
|
|
452
|
+
context.vision = await prompt(chalk.cyan('Vision: '));
|
|
453
|
+
|
|
454
|
+
// Target Users
|
|
455
|
+
console.log(chalk.bold('\nš„ Target Users'));
|
|
456
|
+
console.log(chalk.gray('Who will use this? What are their needs?'));
|
|
457
|
+
context.users = await prompt(chalk.cyan('Target users: '));
|
|
458
|
+
|
|
459
|
+
// Key Features (simplified - just high level)
|
|
460
|
+
console.log(chalk.bold('\n⨠Key Features'));
|
|
461
|
+
console.log(chalk.gray('What are the main capabilities? (brief description)'));
|
|
462
|
+
context.features = await prompt(chalk.cyan('Features: '));
|
|
463
|
+
|
|
464
|
+
// Success Metrics
|
|
465
|
+
console.log(chalk.bold('\nš Success Metrics'));
|
|
466
|
+
console.log(chalk.gray('How will you measure success?'));
|
|
467
|
+
context.metrics = await prompt(chalk.cyan('Metrics: '));
|
|
468
|
+
|
|
469
|
+
// Priority
|
|
470
|
+
console.log(chalk.bold('\nšÆ Priority'));
|
|
471
|
+
const priority = await prompt(chalk.cyan('Priority (P0/P1/P2/P3) [P2]: '));
|
|
472
|
+
context.priority = priority || 'P2';
|
|
473
|
+
|
|
474
|
+
// Timeline
|
|
475
|
+
console.log(chalk.bold('\nā° Timeline'));
|
|
476
|
+
const timeline = await prompt(chalk.cyan('Timeline [Q1 2025]: '));
|
|
477
|
+
context.timeline = timeline || 'Q1 2025';
|
|
478
|
+
|
|
479
|
+
rl.close();
|
|
480
|
+
|
|
481
|
+
// Build AI prompt
|
|
482
|
+
const aiPrompt = `Generate a comprehensive Product Requirements Document (PRD) based on the following information:
|
|
483
|
+
|
|
484
|
+
**PRD Name**: ${argv.name}
|
|
485
|
+
**Priority**: ${context.priority}
|
|
486
|
+
**Timeline**: ${context.timeline}
|
|
487
|
+
|
|
488
|
+
**Product Vision**:
|
|
489
|
+
${context.vision}
|
|
490
|
+
|
|
491
|
+
**Target Users**:
|
|
492
|
+
${context.users}
|
|
493
|
+
|
|
494
|
+
**Key Features**:
|
|
495
|
+
${context.features}
|
|
496
|
+
|
|
497
|
+
**Success Metrics**:
|
|
498
|
+
${context.metrics}
|
|
499
|
+
|
|
500
|
+
Please generate a complete, professional PRD with the following sections:
|
|
501
|
+
1. Executive Summary
|
|
502
|
+
2. Problem Statement (with Background, Current State, Desired State)
|
|
503
|
+
3. Target Users (with User Personas and User Stories)
|
|
504
|
+
4. Key Features (organized by priority: Must Have, Should Have, Nice to Have)
|
|
505
|
+
5. Success Metrics (with KPIs and Measurement Plan)
|
|
506
|
+
6. Technical Requirements (Architecture, Non-Functional Requirements, Dependencies)
|
|
507
|
+
7. Implementation Plan (broken into phases)
|
|
508
|
+
8. Risks and Mitigation
|
|
509
|
+
9. Open Questions
|
|
510
|
+
10. Appendix (References, Glossary, Changelog)
|
|
511
|
+
|
|
512
|
+
Format the output as a proper markdown document with frontmatter (status, priority, created, author, timeline).
|
|
513
|
+
|
|
514
|
+
Make it comprehensive, actionable, and professional. Expand on the provided information with industry best practices.`;
|
|
515
|
+
|
|
516
|
+
// Initialize AI provider and PRD service
|
|
517
|
+
const provider = new ClaudeProvider();
|
|
518
|
+
const prdService = new PRDService({ provider });
|
|
519
|
+
let prdContent = '';
|
|
520
|
+
|
|
521
|
+
if (argv.stream) {
|
|
522
|
+
// Streaming mode
|
|
523
|
+
const genSpinner = ora('Generating PRD with AI...').start();
|
|
524
|
+
genSpinner.stop();
|
|
525
|
+
|
|
526
|
+
console.log(chalk.cyan('\n\nš¤ AI is generating your PRD...\n'));
|
|
527
|
+
console.log(chalk.gray('ā'.repeat(80)) + '\n');
|
|
528
|
+
|
|
529
|
+
for await (const chunk of prdService.generatePRDStream(aiPrompt)) {
|
|
530
|
+
process.stdout.write(chunk);
|
|
531
|
+
prdContent += chunk;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
console.log('\n\n' + chalk.gray('ā'.repeat(80)));
|
|
535
|
+
} else {
|
|
536
|
+
// Non-streaming mode
|
|
537
|
+
const genSpinner = ora('Generating PRD with AI...').start();
|
|
538
|
+
prdContent = await prdService.generatePRD(aiPrompt);
|
|
539
|
+
genSpinner.succeed(chalk.green('PRD generated'));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Save to file
|
|
543
|
+
const saveSpinner = ora('Saving PRD...').start();
|
|
544
|
+
await fs.writeFile(prdPath, prdContent);
|
|
545
|
+
saveSpinner.succeed(chalk.green('PRD saved'));
|
|
546
|
+
|
|
547
|
+
console.log(chalk.green('\nā
AI-powered PRD created successfully!'));
|
|
548
|
+
console.log(chalk.cyan(`š File: ${prdPath}\n`));
|
|
549
|
+
|
|
550
|
+
// Show next steps
|
|
551
|
+
console.log(chalk.bold('š What You Can Do Next:\n'));
|
|
552
|
+
console.log(` ${chalk.cyan('1.')} Review and edit: ${chalk.yellow('autopm prd edit ' + argv.name)}`);
|
|
553
|
+
console.log(` ${chalk.cyan('2.')} Check status: ${chalk.yellow('autopm prd status ' + argv.name)}`);
|
|
554
|
+
console.log(` ${chalk.cyan('3.')} Parse to epic: ${chalk.yellow('autopm prd parse ' + argv.name + ' --ai')}`);
|
|
555
|
+
console.log(` ${chalk.cyan('4.')} Extract epics: ${chalk.yellow('autopm prd extract-epics ' + argv.name)}\n`);
|
|
556
|
+
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.error(chalk.red(`\nā Error: ${error.message}`));
|
|
559
|
+
|
|
560
|
+
if (error.message.includes('ANTHROPIC_API_KEY') || error.message.includes('API key')) {
|
|
561
|
+
console.error(chalk.red('\nā Error: API key not configured'));
|
|
562
|
+
console.error(chalk.yellow('\nš” Set your API key in .env file:'));
|
|
563
|
+
console.error(chalk.cyan(' ANTHROPIC_API_KEY=sk-ant-api03-...'));
|
|
564
|
+
} else if (process.env.DEBUG) {
|
|
565
|
+
console.error(chalk.gray('\nStack trace:'));
|
|
566
|
+
console.error(chalk.gray(error.stack));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
400
573
|
/**
|
|
401
574
|
* Parse PRD with AI
|
|
402
575
|
* @param {Object} argv - Command arguments
|
|
@@ -642,8 +815,20 @@ function builder(yargs) {
|
|
|
642
815
|
type: 'string',
|
|
643
816
|
alias: 't'
|
|
644
817
|
})
|
|
818
|
+
.option('ai', {
|
|
819
|
+
describe: 'Use AI to generate PRD content (requires ANTHROPIC_API_KEY)',
|
|
820
|
+
type: 'boolean',
|
|
821
|
+
default: false
|
|
822
|
+
})
|
|
823
|
+
.option('stream', {
|
|
824
|
+
describe: 'Stream AI output in real-time (only with --ai)',
|
|
825
|
+
type: 'boolean',
|
|
826
|
+
default: false
|
|
827
|
+
})
|
|
645
828
|
.example('autopm prd new my-feature', 'Create PRD with wizard')
|
|
646
|
-
.example('autopm prd new payment-api --template api-feature', 'Create PRD from template')
|
|
829
|
+
.example('autopm prd new payment-api --template api-feature', 'Create PRD from template')
|
|
830
|
+
.example('autopm prd new my-feature --ai', 'AI-powered PRD generation')
|
|
831
|
+
.example('autopm prd new my-feature --ai --stream', 'AI generation with streaming');
|
|
647
832
|
},
|
|
648
833
|
prdNew // Handler
|
|
649
834
|
)
|
|
@@ -40,26 +40,28 @@ class EpicService {
|
|
|
40
40
|
* Create a new EpicService instance
|
|
41
41
|
*
|
|
42
42
|
* @param {Object} options - Configuration options
|
|
43
|
-
* @param {PRDService} options.prdService - PRDService instance for parsing
|
|
43
|
+
* @param {PRDService} options.prdService - PRDService instance for parsing (optional for CLI operations)
|
|
44
44
|
* @param {ConfigManager} [options.configManager] - Optional ConfigManager instance
|
|
45
45
|
* @param {Object} [options.provider] - Optional AI provider instance for streaming
|
|
46
|
+
* @param {string} [options.epicsDir] - Path to epics directory (default: .claude/epics)
|
|
47
|
+
* @param {string} [options.defaultStatus] - Default epic status (default: backlog)
|
|
46
48
|
*/
|
|
47
49
|
constructor(options = {}) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!(options.prdService instanceof PRDService)) {
|
|
53
|
-
throw new Error('prdService must be an instance of PRDService');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
this.prdService = options.prdService;
|
|
50
|
+
// PRDService is optional now - only required for PRD parsing operations
|
|
51
|
+
this.prdService = options.prdService || null;
|
|
57
52
|
|
|
58
53
|
// Store ConfigManager if provided (for future use)
|
|
59
54
|
this.configManager = options.configManager || undefined;
|
|
60
55
|
|
|
61
56
|
// Store provider if provided (for streaming operations)
|
|
62
57
|
this.provider = options.provider || undefined;
|
|
58
|
+
|
|
59
|
+
// CLI operation options
|
|
60
|
+
this.options = {
|
|
61
|
+
epicsDir: options.epicsDir || '.claude/epics',
|
|
62
|
+
defaultStatus: options.defaultStatus || 'backlog',
|
|
63
|
+
...options
|
|
64
|
+
};
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// ==========================================
|
|
@@ -312,6 +314,10 @@ class EpicService {
|
|
|
312
314
|
* // }
|
|
313
315
|
*/
|
|
314
316
|
analyzePRD(prdContent) {
|
|
317
|
+
if (!this.prdService) {
|
|
318
|
+
throw new Error('PRDService instance is required for PRD analysis operations');
|
|
319
|
+
}
|
|
320
|
+
|
|
315
321
|
const frontmatter = this.prdService.parseFrontmatter(prdContent);
|
|
316
322
|
const sections = this.prdService.extractPrdContent(prdContent);
|
|
317
323
|
|
|
@@ -321,6 +327,59 @@ class EpicService {
|
|
|
321
327
|
};
|
|
322
328
|
}
|
|
323
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Parse YAML frontmatter from markdown content
|
|
332
|
+
*
|
|
333
|
+
* Extracts key-value pairs from YAML frontmatter block.
|
|
334
|
+
* Returns null if frontmatter is missing or malformed.
|
|
335
|
+
*
|
|
336
|
+
* @param {string} content - Markdown content with frontmatter
|
|
337
|
+
* @returns {Object|null} Parsed frontmatter object or null
|
|
338
|
+
*/
|
|
339
|
+
parseFrontmatter(content) {
|
|
340
|
+
if (!content || typeof content !== 'string') {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Match frontmatter block: ---\n...\n--- or ---\n---
|
|
345
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/) ||
|
|
346
|
+
content.match(/^---\n---/);
|
|
347
|
+
|
|
348
|
+
if (!frontmatterMatch) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Empty frontmatter (---\n---)
|
|
353
|
+
if (!frontmatterMatch[1] && content.startsWith('---\n---')) {
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const frontmatter = {};
|
|
358
|
+
const lines = (frontmatterMatch[1] || '').split('\n');
|
|
359
|
+
|
|
360
|
+
for (const line of lines) {
|
|
361
|
+
// Skip empty lines
|
|
362
|
+
if (!line.trim()) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Find first colon to split key and value
|
|
367
|
+
const colonIndex = line.indexOf(':');
|
|
368
|
+
if (colonIndex === -1) {
|
|
369
|
+
continue; // Skip lines without colons
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const key = line.substring(0, colonIndex).trim();
|
|
373
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
374
|
+
|
|
375
|
+
if (key) {
|
|
376
|
+
frontmatter[key] = value;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return frontmatter;
|
|
381
|
+
}
|
|
382
|
+
|
|
324
383
|
/**
|
|
325
384
|
* Determine dependencies between features
|
|
326
385
|
*
|
|
@@ -604,6 +663,221 @@ ${prdContent}`;
|
|
|
604
663
|
yield chunk;
|
|
605
664
|
}
|
|
606
665
|
}
|
|
666
|
+
|
|
667
|
+
// ==========================================
|
|
668
|
+
// 6. CLI OPERATIONS (I/O Methods)
|
|
669
|
+
// ==========================================
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* List all epics with metadata
|
|
673
|
+
*
|
|
674
|
+
* @returns {Promise<Array<Object>>} Array of epic objects
|
|
675
|
+
*/
|
|
676
|
+
async listEpics() {
|
|
677
|
+
const fs = require('fs-extra');
|
|
678
|
+
const path = require('path');
|
|
679
|
+
|
|
680
|
+
const epicsDir = path.join(process.cwd(), this.options.epicsDir);
|
|
681
|
+
|
|
682
|
+
// Check if epics directory exists
|
|
683
|
+
const dirExists = await fs.pathExists(epicsDir);
|
|
684
|
+
if (!dirExists) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Read all epic directories
|
|
689
|
+
let epicDirs;
|
|
690
|
+
try {
|
|
691
|
+
const items = await fs.readdir(epicsDir, { withFileTypes: true });
|
|
692
|
+
epicDirs = items
|
|
693
|
+
.filter(dirent => dirent.isDirectory())
|
|
694
|
+
.map(dirent => dirent.name);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const epics = [];
|
|
700
|
+
|
|
701
|
+
for (const epicDir of epicDirs) {
|
|
702
|
+
const epicPath = path.join(epicsDir, epicDir);
|
|
703
|
+
const epicFilePath = path.join(epicPath, 'epic.md');
|
|
704
|
+
|
|
705
|
+
// Skip directories without epic.md file
|
|
706
|
+
const fileExists = await fs.pathExists(epicFilePath);
|
|
707
|
+
if (!fileExists) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
let metadata;
|
|
712
|
+
try {
|
|
713
|
+
const content = await fs.readFile(epicFilePath, 'utf8');
|
|
714
|
+
metadata = this.parseFrontmatter(content);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
// Skip files that can't be read
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Apply defaults
|
|
721
|
+
const name = (metadata && metadata.name) || epicDir;
|
|
722
|
+
const status = (metadata && metadata.status) || this.options.defaultStatus;
|
|
723
|
+
const progress = (metadata && metadata.progress) || '0%';
|
|
724
|
+
const github = (metadata && metadata.github) || '';
|
|
725
|
+
const created = (metadata && metadata.created) || '';
|
|
726
|
+
|
|
727
|
+
// Count tasks
|
|
728
|
+
const taskCount = await this.countTasks(epicPath);
|
|
729
|
+
|
|
730
|
+
// Extract GitHub issue number
|
|
731
|
+
const githubIssue = this.extractGitHubIssue(github);
|
|
732
|
+
|
|
733
|
+
epics.push({
|
|
734
|
+
name,
|
|
735
|
+
status,
|
|
736
|
+
progress,
|
|
737
|
+
github,
|
|
738
|
+
githubIssue,
|
|
739
|
+
created,
|
|
740
|
+
taskCount,
|
|
741
|
+
epicDir,
|
|
742
|
+
epicPath: path.join(epicPath, 'epic.md')
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return epics;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get detailed epic information
|
|
751
|
+
*
|
|
752
|
+
* @param {string} epicName - Epic directory name
|
|
753
|
+
* @returns {Promise<Object>} Epic data with metadata and task count
|
|
754
|
+
* @throws {Error} If epic not found
|
|
755
|
+
*/
|
|
756
|
+
async getEpic(epicName) {
|
|
757
|
+
const fs = require('fs-extra');
|
|
758
|
+
|
|
759
|
+
const epicPath = this.getEpicPath(epicName);
|
|
760
|
+
const epicFilePath = this.getEpicFilePath(epicName);
|
|
761
|
+
|
|
762
|
+
// Check if epic exists
|
|
763
|
+
const exists = await fs.pathExists(epicFilePath);
|
|
764
|
+
if (!exists) {
|
|
765
|
+
throw new Error(`Epic not found: ${epicName}`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Read epic content
|
|
769
|
+
const content = await fs.readFile(epicFilePath, 'utf8');
|
|
770
|
+
const metadata = this.parseFrontmatter(content);
|
|
771
|
+
|
|
772
|
+
// Apply defaults
|
|
773
|
+
const name = (metadata && metadata.name) || epicName;
|
|
774
|
+
const status = (metadata && metadata.status) || this.options.defaultStatus;
|
|
775
|
+
const progress = (metadata && metadata.progress) || '0%';
|
|
776
|
+
const github = (metadata && metadata.github) || '';
|
|
777
|
+
const created = (metadata && metadata.created) || '';
|
|
778
|
+
|
|
779
|
+
// Count tasks
|
|
780
|
+
const taskCount = await this.countTasks(epicPath);
|
|
781
|
+
|
|
782
|
+
// Extract GitHub issue
|
|
783
|
+
const githubIssue = this.extractGitHubIssue(github);
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
name,
|
|
787
|
+
status,
|
|
788
|
+
progress,
|
|
789
|
+
github,
|
|
790
|
+
githubIssue,
|
|
791
|
+
created,
|
|
792
|
+
taskCount,
|
|
793
|
+
epicDir: epicName,
|
|
794
|
+
epicPath: epicFilePath,
|
|
795
|
+
content
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Validate epic structure and completeness
|
|
801
|
+
*
|
|
802
|
+
* @param {string} epicName - Epic directory name
|
|
803
|
+
* @returns {Promise<Object>} Validation result: { valid: boolean, issues: string[] }
|
|
804
|
+
* @throws {Error} If epic not found
|
|
805
|
+
*/
|
|
806
|
+
async validateEpicStructure(epicName) {
|
|
807
|
+
const fs = require('fs-extra');
|
|
808
|
+
|
|
809
|
+
const epicFilePath = this.getEpicFilePath(epicName);
|
|
810
|
+
|
|
811
|
+
// Check if epic exists
|
|
812
|
+
const exists = await fs.pathExists(epicFilePath);
|
|
813
|
+
if (!exists) {
|
|
814
|
+
throw new Error(`Epic not found: ${epicName}`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const issues = [];
|
|
818
|
+
|
|
819
|
+
// Read epic content
|
|
820
|
+
const content = await fs.readFile(epicFilePath, 'utf8');
|
|
821
|
+
|
|
822
|
+
// Check for frontmatter
|
|
823
|
+
const frontmatter = this.parseFrontmatter(content);
|
|
824
|
+
if (!frontmatter) {
|
|
825
|
+
issues.push('Missing frontmatter');
|
|
826
|
+
} else {
|
|
827
|
+
// Check for required fields
|
|
828
|
+
if (!frontmatter.name) {
|
|
829
|
+
issues.push('Missing name in frontmatter');
|
|
830
|
+
}
|
|
831
|
+
if (!frontmatter.status) {
|
|
832
|
+
issues.push('Missing status in frontmatter');
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
valid: issues.length === 0,
|
|
838
|
+
issues
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Count task files in epic directory
|
|
844
|
+
*
|
|
845
|
+
* @param {string} epicPath - Path to epic directory
|
|
846
|
+
* @returns {Promise<number>} Number of task files
|
|
847
|
+
*/
|
|
848
|
+
async countTasks(epicPath) {
|
|
849
|
+
const fs = require('fs-extra');
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
const files = await fs.readdir(epicPath);
|
|
853
|
+
// Count files that match pattern [0-9]*.md
|
|
854
|
+
return files.filter(file => /^\d+\.md$/.test(file)).length;
|
|
855
|
+
} catch (error) {
|
|
856
|
+
return 0;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Get full path to epic directory
|
|
862
|
+
*
|
|
863
|
+
* @param {string} epicName - Epic directory name
|
|
864
|
+
* @returns {string} Full path to epic directory
|
|
865
|
+
*/
|
|
866
|
+
getEpicPath(epicName) {
|
|
867
|
+
const path = require('path');
|
|
868
|
+
return path.join(process.cwd(), this.options.epicsDir, epicName);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Get full path to epic.md file
|
|
873
|
+
*
|
|
874
|
+
* @param {string} epicName - Epic directory name
|
|
875
|
+
* @returns {string} Full path to epic.md file
|
|
876
|
+
*/
|
|
877
|
+
getEpicFilePath(epicName) {
|
|
878
|
+
const path = require('path');
|
|
879
|
+
return path.join(this.getEpicPath(epicName), 'epic.md');
|
|
880
|
+
}
|
|
607
881
|
}
|
|
608
882
|
|
|
609
883
|
module.exports = EpicService;
|
|
@@ -880,6 +880,55 @@ ${content}`;
|
|
|
880
880
|
// TIER 4: AI STREAMING METHODS
|
|
881
881
|
// ==========================================
|
|
882
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Generate PRD from prompt (non-streaming)
|
|
885
|
+
*
|
|
886
|
+
* Uses AI to generate a complete PRD from a high-level description or prompt.
|
|
887
|
+
* Returns the generated PRD as markdown text.
|
|
888
|
+
*
|
|
889
|
+
* @param {string} prompt - Prompt describing what PRD to generate
|
|
890
|
+
* @param {Object} [options] - Generation options
|
|
891
|
+
* @returns {Promise<string>} Generated PRD markdown
|
|
892
|
+
* @throws {Error} If provider is not available
|
|
893
|
+
*
|
|
894
|
+
* @example
|
|
895
|
+
* const prd = await service.generatePRD('Create a PRD for user authentication');
|
|
896
|
+
* console.log(prd); // Full PRD markdown
|
|
897
|
+
*/
|
|
898
|
+
async generatePRD(prompt, options = {}) {
|
|
899
|
+
if (!this.provider || !this.provider.generate) {
|
|
900
|
+
throw new Error('PRD generation requires an AI provider with generate() support');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return await this.provider.generate(prompt, options);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Generate PRD from prompt with streaming output
|
|
908
|
+
*
|
|
909
|
+
* Uses AI to generate a complete PRD from a high-level description,
|
|
910
|
+
* streaming the output in real-time as it's generated.
|
|
911
|
+
*
|
|
912
|
+
* @param {string} prompt - Prompt describing what PRD to generate
|
|
913
|
+
* @param {Object} [options] - Streaming options
|
|
914
|
+
* @returns {AsyncGenerator<string>} Stream of PRD markdown chunks
|
|
915
|
+
* @throws {Error} If provider is not available or lacks stream() support
|
|
916
|
+
*
|
|
917
|
+
* @example
|
|
918
|
+
* for await (const chunk of service.generatePRDStream('Create a PRD for...')) {
|
|
919
|
+
* process.stdout.write(chunk); // Display PRD as it's generated
|
|
920
|
+
* }
|
|
921
|
+
*/
|
|
922
|
+
async *generatePRDStream(prompt, options = {}) {
|
|
923
|
+
if (!this.provider || !this.provider.stream) {
|
|
924
|
+
throw new Error('Streaming PRD generation requires an AI provider with stream() support');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
for await (const chunk of this.provider.stream(prompt, options)) {
|
|
928
|
+
yield chunk;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
883
932
|
/**
|
|
884
933
|
* Parse PRD with streaming AI analysis
|
|
885
934
|
*
|