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.
- package/bin/autopm.js +4 -2
- package/lib/cli/commands/epic.js +777 -0
- package/lib/cli/commands/issue.js +520 -0
- package/lib/services/EpicService.js +284 -10
- package/lib/services/IssueService.js +591 -0
- package/package.json +1 -1
|
@@ -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;
|