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.
@@ -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
- if (!options.prdService) {
49
- throw new Error('PRDService instance is required');
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;