bmad-method 6.0.0-alpha.14 → 6.0.0-alpha.15
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/.coderabbit.yaml +36 -0
- package/{CODE_OF_CONDUCT.md → .github/CODE_OF_CONDUCT.md} +4 -4
- package/CHANGELOG.md +136 -408
- package/README.md +4 -1
- package/docs/custom-content-installation.md +245 -0
- package/docs/index.md +2 -2
- package/docs/installers-bundlers/installers-modules-platforms-reference.md +6 -5
- package/docs/web-bundles-gemini-gpt-guide.md +1 -1
- package/example-custom-content/README.md +4 -0
- package/example-custom-content/agents/commit-poet/commit-poet.agent.yaml +1 -1
- package/example-custom-content/agents/toolsmith/toolsmith-sidecar/instructions.md +1 -1
- package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/docs.md +1 -1
- package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/installers.md +1 -1
- package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/modules.md +2 -2
- package/example-custom-content/agents/toolsmith/toolsmith.agent.yaml +1 -1
- package/example-custom-content/{custom.yaml → module.yaml} +1 -0
- package/example-custom-content/workflows/quiz-master/steps/step-01-init.md +2 -2
- package/example-custom-content/workflows/quiz-master/steps/step-02-q1.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-03-q2.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-04-q3.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-05-q4.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-06-q5.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-07-q6.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-08-q7.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-09-q8.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-10-q9.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-11-q10.md +1 -1
- package/example-custom-content/workflows/quiz-master/steps/step-12-results.md +1 -1
- package/example-custom-content/workflows/quiz-master/workflow.md +1 -1
- package/example-custom-module/mwm/README.md +5 -0
- package/example-custom-module/mwm/agents/cbt-coach/cbt-coach.agent.yaml +1 -0
- package/example-custom-module/mwm/agents/crisis-navigator.agent.yaml +3 -2
- package/example-custom-module/mwm/agents/meditation-guide.agent.yaml +3 -2
- package/example-custom-module/mwm/agents/wellness-companion/wellness-companion.agent.yaml +1 -0
- package/example-custom-module/mwm/{_module-installer/install-config.yaml → module.yaml} +1 -0
- package/package.json +1 -1
- package/src/core/_module-installer/installer.js +1 -1
- package/src/modules/bmb/_module-installer/installer.js +1 -1
- package/src/modules/bmb/docs/agents/index.md +1 -1
- package/src/modules/bmb/workflows/create-module/steps/step-04-structure.md +3 -3
- package/src/modules/bmb/workflows/create-module/steps/step-05-config.md +1 -1
- package/src/modules/bmb/workflows/create-module/steps/step-08-installer.md +8 -8
- package/src/modules/bmb/workflows/create-module/steps/step-09-documentation.md +2 -1
- package/src/modules/bmb/workflows/create-module/steps/step-10-roadmap.md +3 -2
- package/src/modules/bmb/workflows/create-module/steps/step-11-validate.md +3 -3
- package/src/modules/bmb/workflows/create-module/templates/installer.template.js +1 -1
- package/src/modules/bmb/workflows/create-module/validation.md +3 -3
- package/src/modules/bmb/workflows/create-workflow/steps/step-01-init.md +1 -1
- package/src/modules/bmb/workflows/create-workflow/steps/step-07-build.md +1 -1
- package/src/modules/bmgd/README.md +2 -1
- package/src/modules/bmm/_module-installer/installer.js +1 -1
- package/src/modules/bmm/_module-installer/platform-specifics/claude-code.js +1 -1
- package/src/modules/bmm/_module-installer/platform-specifics/windsurf.js +1 -1
- package/src/modules/cis/_module-installer/installer.js +1 -1
- package/tools/cli/README.md +4 -4
- package/tools/cli/installers/lib/core/config-collector.js +16 -8
- package/tools/cli/installers/lib/core/custom-module-cache.js +239 -0
- package/tools/cli/installers/lib/core/detector.js +8 -4
- package/tools/cli/installers/lib/core/installer.js +815 -23
- package/tools/cli/installers/lib/core/manifest-generator.js +176 -13
- package/tools/cli/installers/lib/core/manifest.js +47 -0
- package/tools/cli/installers/lib/custom/handler.js +150 -20
- package/tools/cli/installers/lib/modules/manager.js +78 -32
- package/tools/cli/lib/agent/compiler.js +3 -11
- package/tools/cli/lib/agent/installer.js +2 -1
- package/tools/cli/lib/cli-utils.js +21 -4
- package/tools/cli/lib/ui.js +499 -11
- package/tools/maintainer/review-pr-README.md +55 -0
- package/tools/maintainer/review-pr.md +242 -0
- package/tools/migrate-custom-module-paths.js +124 -0
- package/bmad-method-6.0.0-alpha.14.tgz +0 -0
- package/docs/custom-agent-installation.md +0 -137
- package/example-custom-content/workflows/quiz-master/workflow-plan-quiz-master.md +0 -269
- /package/src/core/{_module-installer/install-config.yaml → module.yaml} +0 -0
- /package/src/modules/bmb/{_module-installer/install-config.yaml → module.yaml} +0 -0
- /package/src/modules/bmb/workflows/create-module/templates/{install-config.template.yaml → module.template.yaml} +0 -0
- /package/src/modules/bmgd/{_module-installer/install-config.yaml → module.yaml} +0 -0
- /package/src/modules/bmm/{_module-installer/install-config.yaml → module.yaml} +0 -0
- /package/src/modules/cis/{_module-installer/install-config.yaml → module.yaml} +0 -0
|
@@ -41,7 +41,11 @@ class ManifestGenerator {
|
|
|
41
41
|
// Deduplicate modules list to prevent duplicates
|
|
42
42
|
this.modules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])];
|
|
43
43
|
this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])]; // All installed modules get rescanned
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
// For CSV manifests, we need to include ALL modules that are installed
|
|
46
|
+
// preservedModules controls which modules stay as-is in the CSV (don't get rescanned)
|
|
47
|
+
// But all modules should be included in the final manifest
|
|
48
|
+
this.preservedModules = [...new Set([...preservedModules, ...selectedModules, ...installedModules])]; // Include all installed modules
|
|
45
49
|
this.bmadDir = bmadDir;
|
|
46
50
|
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '.bmad' or 'bmad')
|
|
47
51
|
this.allInstalledFiles = installedFiles;
|
|
@@ -61,14 +65,14 @@ class ManifestGenerator {
|
|
|
61
65
|
// Collect workflow data
|
|
62
66
|
await this.collectWorkflows(selectedModules);
|
|
63
67
|
|
|
64
|
-
// Collect agent data
|
|
65
|
-
await this.collectAgents(
|
|
68
|
+
// Collect agent data - use updatedModules which includes all installed modules
|
|
69
|
+
await this.collectAgents(this.updatedModules);
|
|
66
70
|
|
|
67
71
|
// Collect task data
|
|
68
|
-
await this.collectTasks(
|
|
72
|
+
await this.collectTasks(this.updatedModules);
|
|
69
73
|
|
|
70
74
|
// Collect tool data
|
|
71
|
-
await this.collectTools(
|
|
75
|
+
await this.collectTools(this.updatedModules);
|
|
72
76
|
|
|
73
77
|
// Write manifest files and collect their paths
|
|
74
78
|
const manifestFiles = [
|
|
@@ -450,6 +454,21 @@ class ManifestGenerator {
|
|
|
450
454
|
async writeMainManifest(cfgDir) {
|
|
451
455
|
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
|
452
456
|
|
|
457
|
+
// Read existing manifest to preserve custom modules
|
|
458
|
+
let existingCustomModules = [];
|
|
459
|
+
if (await fs.pathExists(manifestPath)) {
|
|
460
|
+
try {
|
|
461
|
+
const existingContent = await fs.readFile(manifestPath, 'utf8');
|
|
462
|
+
const existingManifest = yaml.load(existingContent);
|
|
463
|
+
if (existingManifest && existingManifest.customModules) {
|
|
464
|
+
existingCustomModules = existingManifest.customModules;
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
// If we can't read the existing manifest, continue without preserving custom modules
|
|
468
|
+
console.warn('Warning: Could not read existing manifest to preserve custom modules');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
453
472
|
const manifest = {
|
|
454
473
|
installation: {
|
|
455
474
|
version: packageJson.version,
|
|
@@ -457,6 +476,7 @@ class ManifestGenerator {
|
|
|
457
476
|
lastUpdated: new Date().toISOString(),
|
|
458
477
|
},
|
|
459
478
|
modules: this.modules,
|
|
479
|
+
customModules: existingCustomModules, // Preserve custom modules
|
|
460
480
|
ides: this.selectedIdes,
|
|
461
481
|
};
|
|
462
482
|
|
|
@@ -562,12 +582,47 @@ class ManifestGenerator {
|
|
|
562
582
|
async writeWorkflowManifest(cfgDir) {
|
|
563
583
|
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
|
564
584
|
|
|
585
|
+
// Read existing manifest to preserve entries
|
|
586
|
+
const existingEntries = new Map();
|
|
587
|
+
if (await fs.pathExists(csvPath)) {
|
|
588
|
+
const content = await fs.readFile(csvPath, 'utf8');
|
|
589
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
590
|
+
|
|
591
|
+
// Skip header
|
|
592
|
+
for (let i = 1; i < lines.length; i++) {
|
|
593
|
+
const line = lines[i];
|
|
594
|
+
if (line) {
|
|
595
|
+
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
|
596
|
+
const parts = line.split('","');
|
|
597
|
+
if (parts.length >= 4) {
|
|
598
|
+
const name = parts[0].replace(/^"/, '');
|
|
599
|
+
const module = parts[2];
|
|
600
|
+
existingEntries.set(`${module}:${name}`, line);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
565
606
|
// Create CSV header - removed standalone column as ALL workflows now generate commands
|
|
566
607
|
let csv = 'name,description,module,path\n';
|
|
567
608
|
|
|
568
|
-
//
|
|
609
|
+
// Combine existing and new workflows
|
|
610
|
+
const allWorkflows = new Map();
|
|
611
|
+
|
|
612
|
+
// Add existing entries
|
|
613
|
+
for (const [key, value] of existingEntries) {
|
|
614
|
+
allWorkflows.set(key, value);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Add/update new workflows
|
|
569
618
|
for (const workflow of this.workflows) {
|
|
570
|
-
|
|
619
|
+
const key = `${workflow.module}:${workflow.name}`;
|
|
620
|
+
allWorkflows.set(key, `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Write all workflows
|
|
624
|
+
for (const [, value] of allWorkflows) {
|
|
625
|
+
csv += value + '\n';
|
|
571
626
|
}
|
|
572
627
|
|
|
573
628
|
await fs.writeFile(csvPath, csv);
|
|
@@ -581,12 +636,50 @@ class ManifestGenerator {
|
|
|
581
636
|
async writeAgentManifest(cfgDir) {
|
|
582
637
|
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
|
583
638
|
|
|
639
|
+
// Read existing manifest to preserve entries
|
|
640
|
+
const existingEntries = new Map();
|
|
641
|
+
if (await fs.pathExists(csvPath)) {
|
|
642
|
+
const content = await fs.readFile(csvPath, 'utf8');
|
|
643
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
644
|
+
|
|
645
|
+
// Skip header
|
|
646
|
+
for (let i = 1; i < lines.length; i++) {
|
|
647
|
+
const line = lines[i];
|
|
648
|
+
if (line) {
|
|
649
|
+
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
|
650
|
+
const parts = line.split('","');
|
|
651
|
+
if (parts.length >= 11) {
|
|
652
|
+
const name = parts[0].replace(/^"/, '');
|
|
653
|
+
const module = parts[8];
|
|
654
|
+
existingEntries.set(`${module}:${name}`, line);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
584
660
|
// Create CSV header with persona fields
|
|
585
661
|
let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
|
586
662
|
|
|
587
|
-
//
|
|
663
|
+
// Combine existing and new agents, preferring new data for duplicates
|
|
664
|
+
const allAgents = new Map();
|
|
665
|
+
|
|
666
|
+
// Add existing entries
|
|
667
|
+
for (const [key, value] of existingEntries) {
|
|
668
|
+
allAgents.set(key, value);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Add/update new agents
|
|
588
672
|
for (const agent of this.agents) {
|
|
589
|
-
|
|
673
|
+
const key = `${agent.module}:${agent.name}`;
|
|
674
|
+
allAgents.set(
|
|
675
|
+
key,
|
|
676
|
+
`"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Write all agents
|
|
681
|
+
for (const [, value] of allAgents) {
|
|
682
|
+
csv += value + '\n';
|
|
590
683
|
}
|
|
591
684
|
|
|
592
685
|
await fs.writeFile(csvPath, csv);
|
|
@@ -600,12 +693,47 @@ class ManifestGenerator {
|
|
|
600
693
|
async writeTaskManifest(cfgDir) {
|
|
601
694
|
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
|
602
695
|
|
|
696
|
+
// Read existing manifest to preserve entries
|
|
697
|
+
const existingEntries = new Map();
|
|
698
|
+
if (await fs.pathExists(csvPath)) {
|
|
699
|
+
const content = await fs.readFile(csvPath, 'utf8');
|
|
700
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
701
|
+
|
|
702
|
+
// Skip header
|
|
703
|
+
for (let i = 1; i < lines.length; i++) {
|
|
704
|
+
const line = lines[i];
|
|
705
|
+
if (line) {
|
|
706
|
+
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
|
707
|
+
const parts = line.split('","');
|
|
708
|
+
if (parts.length >= 6) {
|
|
709
|
+
const name = parts[0].replace(/^"/, '');
|
|
710
|
+
const module = parts[3];
|
|
711
|
+
existingEntries.set(`${module}:${name}`, line);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
603
717
|
// Create CSV header with standalone column
|
|
604
718
|
let csv = 'name,displayName,description,module,path,standalone\n';
|
|
605
719
|
|
|
606
|
-
//
|
|
720
|
+
// Combine existing and new tasks
|
|
721
|
+
const allTasks = new Map();
|
|
722
|
+
|
|
723
|
+
// Add existing entries
|
|
724
|
+
for (const [key, value] of existingEntries) {
|
|
725
|
+
allTasks.set(key, value);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Add/update new tasks
|
|
607
729
|
for (const task of this.tasks) {
|
|
608
|
-
|
|
730
|
+
const key = `${task.module}:${task.name}`;
|
|
731
|
+
allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Write all tasks
|
|
735
|
+
for (const [, value] of allTasks) {
|
|
736
|
+
csv += value + '\n';
|
|
609
737
|
}
|
|
610
738
|
|
|
611
739
|
await fs.writeFile(csvPath, csv);
|
|
@@ -619,12 +747,47 @@ class ManifestGenerator {
|
|
|
619
747
|
async writeToolManifest(cfgDir) {
|
|
620
748
|
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
|
621
749
|
|
|
750
|
+
// Read existing manifest to preserve entries
|
|
751
|
+
const existingEntries = new Map();
|
|
752
|
+
if (await fs.pathExists(csvPath)) {
|
|
753
|
+
const content = await fs.readFile(csvPath, 'utf8');
|
|
754
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
755
|
+
|
|
756
|
+
// Skip header
|
|
757
|
+
for (let i = 1; i < lines.length; i++) {
|
|
758
|
+
const line = lines[i];
|
|
759
|
+
if (line) {
|
|
760
|
+
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
|
761
|
+
const parts = line.split('","');
|
|
762
|
+
if (parts.length >= 6) {
|
|
763
|
+
const name = parts[0].replace(/^"/, '');
|
|
764
|
+
const module = parts[3];
|
|
765
|
+
existingEntries.set(`${module}:${name}`, line);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
622
771
|
// Create CSV header with standalone column
|
|
623
772
|
let csv = 'name,displayName,description,module,path,standalone\n';
|
|
624
773
|
|
|
625
|
-
//
|
|
774
|
+
// Combine existing and new tools
|
|
775
|
+
const allTools = new Map();
|
|
776
|
+
|
|
777
|
+
// Add existing entries
|
|
778
|
+
for (const [key, value] of existingEntries) {
|
|
779
|
+
allTools.set(key, value);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Add/update new tools
|
|
626
783
|
for (const tool of this.tools) {
|
|
627
|
-
|
|
784
|
+
const key = `${tool.module}:${tool.name}`;
|
|
785
|
+
allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Write all tools
|
|
789
|
+
for (const [, value] of allTools) {
|
|
790
|
+
csv += value + '\n';
|
|
628
791
|
}
|
|
629
792
|
|
|
630
793
|
await fs.writeFile(csvPath, csv);
|
|
@@ -61,6 +61,7 @@ class Manifest {
|
|
|
61
61
|
installDate: manifestData.installation?.installDate,
|
|
62
62
|
lastUpdated: manifestData.installation?.lastUpdated,
|
|
63
63
|
modules: manifestData.modules || [],
|
|
64
|
+
customModules: manifestData.customModules || [],
|
|
64
65
|
ides: manifestData.ides || [],
|
|
65
66
|
};
|
|
66
67
|
} catch (error) {
|
|
@@ -93,6 +94,7 @@ class Manifest {
|
|
|
93
94
|
lastUpdated: manifest.lastUpdated,
|
|
94
95
|
},
|
|
95
96
|
modules: manifest.modules || [],
|
|
97
|
+
customModules: manifest.customModules || [],
|
|
96
98
|
ides: manifest.ides || [],
|
|
97
99
|
};
|
|
98
100
|
|
|
@@ -535,6 +537,51 @@ class Manifest {
|
|
|
535
537
|
|
|
536
538
|
return configs;
|
|
537
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Add a custom module to the manifest with its source path
|
|
542
|
+
* @param {string} bmadDir - Path to bmad directory
|
|
543
|
+
* @param {Object} customModule - Custom module info
|
|
544
|
+
*/
|
|
545
|
+
async addCustomModule(bmadDir, customModule) {
|
|
546
|
+
const manifest = await this.read(bmadDir);
|
|
547
|
+
if (!manifest) {
|
|
548
|
+
throw new Error('No manifest found');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!manifest.customModules) {
|
|
552
|
+
manifest.customModules = [];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Check if custom module already exists
|
|
556
|
+
const existingIndex = manifest.customModules.findIndex((m) => m.id === customModule.id);
|
|
557
|
+
if (existingIndex === -1) {
|
|
558
|
+
// Add new entry
|
|
559
|
+
manifest.customModules.push(customModule);
|
|
560
|
+
} else {
|
|
561
|
+
// Update existing entry
|
|
562
|
+
manifest.customModules[existingIndex] = customModule;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
await this.update(bmadDir, { customModules: manifest.customModules });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Remove a custom module from the manifest
|
|
570
|
+
* @param {string} bmadDir - Path to bmad directory
|
|
571
|
+
* @param {string} moduleId - Module ID to remove
|
|
572
|
+
*/
|
|
573
|
+
async removeCustomModule(bmadDir, moduleId) {
|
|
574
|
+
const manifest = await this.read(bmadDir);
|
|
575
|
+
if (!manifest || !manifest.customModules) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const index = manifest.customModules.findIndex((m) => m.id === moduleId);
|
|
580
|
+
if (index !== -1) {
|
|
581
|
+
manifest.customModules.splice(index, 1);
|
|
582
|
+
await this.update(bmadDir, { customModules: manifest.customModules });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
538
585
|
}
|
|
539
586
|
|
|
540
587
|
module.exports = { Manifest };
|
|
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const yaml = require('js-yaml');
|
|
5
5
|
const { FileOps } = require('../../../lib/file-ops');
|
|
6
|
+
const { XmlHandler } = require('../../../lib/xml-handler');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Handler for custom content (custom.yaml)
|
|
@@ -11,6 +12,7 @@ const { FileOps } = require('../../../lib/file-ops');
|
|
|
11
12
|
class CustomHandler {
|
|
12
13
|
constructor() {
|
|
13
14
|
this.fileOps = new FileOps();
|
|
15
|
+
this.xmlHandler = new XmlHandler();
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -52,6 +54,12 @@ class CustomHandler {
|
|
|
52
54
|
} else if (entry.name === 'custom.yaml') {
|
|
53
55
|
// Found a custom.yaml file
|
|
54
56
|
customPaths.push(fullPath);
|
|
57
|
+
} else if (
|
|
58
|
+
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
|
|
59
|
+
// Skip if it's in src/modules (those are standard modules)
|
|
60
|
+
!fullPath.includes(path.join('src', 'modules'))
|
|
61
|
+
) {
|
|
62
|
+
customPaths.push(fullPath);
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
} catch {
|
|
@@ -66,37 +74,44 @@ class CustomHandler {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
69
|
-
* Get custom content info from a custom.yaml file
|
|
70
|
-
* @param {string}
|
|
77
|
+
* Get custom content info from a custom.yaml or module.yaml file
|
|
78
|
+
* @param {string} configPath - Path to config file
|
|
79
|
+
* @param {string} projectRoot - Project root directory for calculating relative paths
|
|
71
80
|
* @returns {Object|null} Custom content info
|
|
72
81
|
*/
|
|
73
|
-
async getCustomInfo(
|
|
82
|
+
async getCustomInfo(configPath, projectRoot = null) {
|
|
74
83
|
try {
|
|
75
|
-
const configContent = await fs.readFile(
|
|
84
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
76
85
|
|
|
77
86
|
// Try to parse YAML with error handling
|
|
78
87
|
let config;
|
|
79
88
|
try {
|
|
80
89
|
config = yaml.load(configContent);
|
|
81
90
|
} catch (parseError) {
|
|
82
|
-
console.warn(chalk.yellow(`Warning: YAML parse error in ${
|
|
91
|
+
console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
|
|
83
92
|
return null;
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
const
|
|
95
|
+
// Check if this is an module.yaml (module) or custom.yaml (custom content)
|
|
96
|
+
const isInstallConfig = configPath.endsWith('module.yaml');
|
|
97
|
+
const configDir = path.dirname(configPath);
|
|
98
|
+
|
|
99
|
+
// Use provided projectRoot or fall back to process.cwd()
|
|
100
|
+
const basePath = projectRoot || process.cwd();
|
|
101
|
+
const relativePath = path.relative(basePath, configDir);
|
|
88
102
|
|
|
89
103
|
return {
|
|
90
|
-
id: config.code ||
|
|
91
|
-
name: config.name
|
|
92
|
-
description: config.description || '
|
|
93
|
-
path:
|
|
104
|
+
id: config.code || 'unknown-code',
|
|
105
|
+
name: config.name,
|
|
106
|
+
description: config.description || '',
|
|
107
|
+
path: configDir,
|
|
94
108
|
relativePath: relativePath,
|
|
95
109
|
defaultSelected: config.default_selected === true,
|
|
96
110
|
config: config,
|
|
111
|
+
isInstallConfig: isInstallConfig, // Track which type this is
|
|
97
112
|
};
|
|
98
113
|
} catch (error) {
|
|
99
|
-
console.warn(chalk.yellow(`Warning: Failed to read ${
|
|
114
|
+
console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
|
|
100
115
|
return null;
|
|
101
116
|
}
|
|
102
117
|
}
|
|
@@ -128,10 +143,10 @@ class CustomHandler {
|
|
|
128
143
|
await fs.ensureDir(bmadAgentsDir);
|
|
129
144
|
await fs.ensureDir(bmadWorkflowsDir);
|
|
130
145
|
|
|
131
|
-
// Process agents - copy
|
|
146
|
+
// Process agents - compile and copy agents
|
|
132
147
|
const agentsDir = path.join(customPath, 'agents');
|
|
133
148
|
if (await fs.pathExists(agentsDir)) {
|
|
134
|
-
await this.
|
|
149
|
+
await this.compileAndCopyAgents(agentsDir, bmadAgentsDir, bmadDir, config, fileTrackingCallback, results);
|
|
135
150
|
|
|
136
151
|
// Count agent files
|
|
137
152
|
const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']);
|
|
@@ -236,13 +251,20 @@ class CustomHandler {
|
|
|
236
251
|
// Copy with placeholder replacement for text files
|
|
237
252
|
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json'];
|
|
238
253
|
if (textExtensions.some((ext) => entry.name.endsWith(ext))) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
});
|
|
254
|
+
// Read source content
|
|
255
|
+
let content = await fs.readFile(sourcePath, 'utf8');
|
|
256
|
+
|
|
257
|
+
// Replace placeholders
|
|
258
|
+
content = content.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
|
|
259
|
+
content = content.replaceAll('{user_name}', config.user_name || 'User');
|
|
260
|
+
content = content.replaceAll('{communication_language}', config.communication_language || 'English');
|
|
261
|
+
content = content.replaceAll('{output_folder}', config.output_folder || 'docs');
|
|
262
|
+
|
|
263
|
+
// Write to target
|
|
264
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
265
|
+
await fs.writeFile(targetPath, content, 'utf8');
|
|
245
266
|
} else {
|
|
267
|
+
// Copy binary files as-is
|
|
246
268
|
await fs.copy(sourcePath, targetPath);
|
|
247
269
|
}
|
|
248
270
|
|
|
@@ -261,6 +283,114 @@ class CustomHandler {
|
|
|
261
283
|
}
|
|
262
284
|
}
|
|
263
285
|
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Compile .agent.yaml files to .md format and handle sidecars
|
|
289
|
+
* @param {string} sourceAgentsPath - Source agents directory
|
|
290
|
+
* @param {string} targetAgentsPath - Target agents directory
|
|
291
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
292
|
+
* @param {Object} config - Configuration for placeholder replacement
|
|
293
|
+
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
|
294
|
+
* @param {Object} results - Results object to update
|
|
295
|
+
*/
|
|
296
|
+
async compileAndCopyAgents(sourceAgentsPath, targetAgentsPath, bmadDir, config, fileTrackingCallback, results) {
|
|
297
|
+
// Get all .agent.yaml files recursively
|
|
298
|
+
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
|
|
299
|
+
|
|
300
|
+
for (const agentFile of agentFiles) {
|
|
301
|
+
const relativePath = path.relative(sourceAgentsPath, agentFile);
|
|
302
|
+
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
|
303
|
+
|
|
304
|
+
await fs.ensureDir(targetDir);
|
|
305
|
+
|
|
306
|
+
const agentName = path.basename(agentFile, '.agent.yaml');
|
|
307
|
+
const targetMdPath = path.join(targetDir, `${agentName}.md`);
|
|
308
|
+
// Use the actual bmadDir if available (for when installing to temp dir)
|
|
309
|
+
const actualBmadDir = config._bmadDir || bmadDir;
|
|
310
|
+
const customizePath = path.join(actualBmadDir, '_cfg', 'agents', `custom-${agentName}.customize.yaml`);
|
|
311
|
+
|
|
312
|
+
// Read and compile the YAML
|
|
313
|
+
try {
|
|
314
|
+
const yamlContent = await fs.readFile(agentFile, 'utf8');
|
|
315
|
+
const { compileAgent } = require('../../../lib/agent/compiler');
|
|
316
|
+
|
|
317
|
+
// Create customize template if it doesn't exist
|
|
318
|
+
if (!(await fs.pathExists(customizePath))) {
|
|
319
|
+
const { getSourcePath } = require('../../../lib/project-root');
|
|
320
|
+
const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
|
|
321
|
+
if (await fs.pathExists(genericTemplatePath)) {
|
|
322
|
+
// Copy with placeholder replacement
|
|
323
|
+
let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
|
|
324
|
+
templateContent = templateContent.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
|
|
325
|
+
await fs.writeFile(customizePath, templateContent, 'utf8');
|
|
326
|
+
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Compile the agent
|
|
331
|
+
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config });
|
|
332
|
+
|
|
333
|
+
// Replace placeholders in the compiled content
|
|
334
|
+
let processedXml = xml;
|
|
335
|
+
processedXml = processedXml.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
|
|
336
|
+
processedXml = processedXml.replaceAll('{user_name}', config.user_name || 'User');
|
|
337
|
+
processedXml = processedXml.replaceAll('{communication_language}', config.communication_language || 'English');
|
|
338
|
+
processedXml = processedXml.replaceAll('{output_folder}', config.output_folder || 'docs');
|
|
339
|
+
|
|
340
|
+
// Write the compiled MD file
|
|
341
|
+
await fs.writeFile(targetMdPath, processedXml, 'utf8');
|
|
342
|
+
|
|
343
|
+
// Check if agent has sidecar
|
|
344
|
+
let hasSidecar = false;
|
|
345
|
+
try {
|
|
346
|
+
const yamlLib = require('yaml');
|
|
347
|
+
const agentYaml = yamlLib.parse(yamlContent);
|
|
348
|
+
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
|
|
349
|
+
} catch {
|
|
350
|
+
// Continue without sidecar processing
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Copy sidecar files if agent has hasSidecar flag
|
|
354
|
+
if (hasSidecar && config.agent_sidecar_folder) {
|
|
355
|
+
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
|
|
356
|
+
|
|
357
|
+
// Resolve agent sidecar folder path
|
|
358
|
+
const projectDir = path.dirname(bmadDir);
|
|
359
|
+
const resolvedSidecarFolder = config.agent_sidecar_folder
|
|
360
|
+
.replaceAll('{project-root}', projectDir)
|
|
361
|
+
.replaceAll('{bmad_folder}', path.basename(bmadDir));
|
|
362
|
+
|
|
363
|
+
// Create sidecar directory for this agent
|
|
364
|
+
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
|
|
365
|
+
await fs.ensureDir(agentSidecarDir);
|
|
366
|
+
|
|
367
|
+
// Copy sidecar files
|
|
368
|
+
const sidecarResult = copyAgentSidecarFiles(path.dirname(agentFile), agentSidecarDir, agentFile);
|
|
369
|
+
|
|
370
|
+
if (sidecarResult.copied.length > 0) {
|
|
371
|
+
console.log(chalk.dim(` Copied ${sidecarResult.copied.length} sidecar file(s) to: ${agentSidecarDir}`));
|
|
372
|
+
}
|
|
373
|
+
if (sidecarResult.preserved.length > 0) {
|
|
374
|
+
console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Track the file
|
|
379
|
+
if (fileTrackingCallback) {
|
|
380
|
+
fileTrackingCallback(targetMdPath);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
console.log(
|
|
384
|
+
chalk.dim(
|
|
385
|
+
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
|
|
390
|
+
results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
264
394
|
}
|
|
265
395
|
|
|
266
396
|
module.exports = { CustomHandler };
|