bmad-method 6.2.3-next.26 → 6.2.3-next.28
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/package.json +1 -1
- package/tools/installer/core/installer.js +32 -0
- package/tools/installer/core/manifest.js +28 -0
- package/tools/installer/ide/_config-driven.js +14 -7
- package/tools/installer/modules/community-manager.js +377 -0
- package/tools/installer/modules/custom-module-manager.js +308 -0
- package/tools/installer/modules/external-manager.js +65 -49
- package/tools/installer/modules/official-modules.js +23 -1
- package/tools/installer/modules/registry-client.js +66 -0
- package/tools/installer/{external-official-modules.yaml → modules/registry-fallback.yaml} +3 -12
- package/tools/installer/ui.js +304 -67
package/tools/installer/ui.js
CHANGED
|
@@ -563,86 +563,80 @@ class UI {
|
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
/**
|
|
566
|
-
* Select all modules
|
|
567
|
-
* Core is shown as locked but filtered from the result since it's always installed separately.
|
|
566
|
+
* Select all modules across three tiers: official, community, and custom URL.
|
|
568
567
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
569
568
|
* @returns {Array} Selected module codes (excluding core)
|
|
570
569
|
*/
|
|
571
570
|
async selectAllModules(installedModuleIds = new Set()) {
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
const { modules: localModules } = await officialModulesSource.listAvailable();
|
|
571
|
+
// Phase 1: Official modules
|
|
572
|
+
const officialSelected = await this._selectOfficialModules(installedModuleIds);
|
|
575
573
|
|
|
576
|
-
//
|
|
574
|
+
// Determine which installed modules are NOT official (community or custom).
|
|
575
|
+
// These must be preserved even if the user declines to browse community/custom.
|
|
576
|
+
const officialCodes = new Set(officialSelected);
|
|
577
577
|
const externalManager = new ExternalModuleManager();
|
|
578
|
-
const
|
|
578
|
+
const registryModules = await externalManager.listAvailable();
|
|
579
|
+
const officialRegistryCodes = new Set(registryModules.map((m) => m.code));
|
|
580
|
+
const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
|
|
581
|
+
|
|
582
|
+
// Phase 2: Community modules (category drill-down)
|
|
583
|
+
// Returns { codes, didBrowse } so we know if the user entered the flow
|
|
584
|
+
const communityResult = await this._browseCommunityModules(installedModuleIds);
|
|
585
|
+
|
|
586
|
+
// Phase 3: Custom URL modules
|
|
587
|
+
const customSelected = await this._addCustomUrlModules(installedModuleIds);
|
|
588
|
+
|
|
589
|
+
// Merge all selections
|
|
590
|
+
const allSelected = new Set([...officialSelected, ...communityResult.codes, ...customSelected]);
|
|
591
|
+
|
|
592
|
+
// Auto-include installed non-official modules that the user didn't get
|
|
593
|
+
// a chance to manage (they declined to browse). If they did browse,
|
|
594
|
+
// trust their selections - they could have deselected intentionally.
|
|
595
|
+
if (!communityResult.didBrowse) {
|
|
596
|
+
for (const code of installedNonOfficial) {
|
|
597
|
+
allSelected.add(code);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return [...allSelected];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Select official modules using autocompleteMultiselect.
|
|
606
|
+
* Extracted from the original selectAllModules - unchanged behavior.
|
|
607
|
+
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
608
|
+
* @returns {Array} Selected official module codes
|
|
609
|
+
*/
|
|
610
|
+
async _selectOfficialModules(installedModuleIds = new Set()) {
|
|
611
|
+
const externalManager = new ExternalModuleManager();
|
|
612
|
+
const registryModules = await externalManager.listAvailable();
|
|
579
613
|
|
|
580
|
-
// Build flat options list with group hints for autocompleteMultiselect
|
|
581
614
|
const allOptions = [];
|
|
582
615
|
const initialValues = [];
|
|
583
616
|
const lockedValues = ['core'];
|
|
584
617
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
allOptions.push({ label: coreLabel, value: 'core', hint: 'Core configuration and shared resources' });
|
|
589
|
-
initialValues.push('core');
|
|
590
|
-
|
|
591
|
-
// Helper to build module entry with proper sorting and selection
|
|
592
|
-
const buildModuleEntry = async (mod, value, group) => {
|
|
593
|
-
const isInstalled = installedModuleIds.has(value);
|
|
594
|
-
const version = await getMarketplaceVersion(value);
|
|
618
|
+
const buildModuleEntry = async (mod) => {
|
|
619
|
+
const isInstalled = installedModuleIds.has(mod.code);
|
|
620
|
+
const version = await getMarketplaceVersion(mod.code);
|
|
595
621
|
const label = version ? `${mod.name} (v${version})` : mod.name;
|
|
596
622
|
return {
|
|
597
623
|
label,
|
|
598
|
-
value,
|
|
599
|
-
hint: mod.description
|
|
600
|
-
// Pre-select only if already installed (not on fresh install)
|
|
624
|
+
value: mod.code,
|
|
625
|
+
hint: mod.description,
|
|
601
626
|
selected: isInstalled,
|
|
602
627
|
};
|
|
603
628
|
};
|
|
604
629
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
localEntries.push(entry);
|
|
611
|
-
if (entry.selected) {
|
|
612
|
-
initialValues.push(mod.id);
|
|
613
|
-
}
|
|
630
|
+
for (const mod of registryModules) {
|
|
631
|
+
const entry = await buildModuleEntry(mod);
|
|
632
|
+
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
|
633
|
+
if (entry.selected) {
|
|
634
|
+
initialValues.push(mod.code);
|
|
614
635
|
}
|
|
615
636
|
}
|
|
616
|
-
allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint })));
|
|
617
|
-
|
|
618
|
-
// Group 2: BMad Official Modules (type: bmad-org)
|
|
619
|
-
const officialModules = [];
|
|
620
|
-
for (const mod of externalModules) {
|
|
621
|
-
if (mod.type === 'bmad-org') {
|
|
622
|
-
const entry = await buildModuleEntry(mod, mod.code, 'Official');
|
|
623
|
-
officialModules.push(entry);
|
|
624
|
-
if (entry.selected) {
|
|
625
|
-
initialValues.push(mod.code);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint })));
|
|
630
|
-
|
|
631
|
-
// Group 3: Community Modules (type: community)
|
|
632
|
-
const communityModules = [];
|
|
633
|
-
for (const mod of externalModules) {
|
|
634
|
-
if (mod.type === 'community') {
|
|
635
|
-
const entry = await buildModuleEntry(mod, mod.code, 'Community');
|
|
636
|
-
communityModules.push(entry);
|
|
637
|
-
if (entry.selected) {
|
|
638
|
-
initialValues.push(mod.code);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
|
|
643
637
|
|
|
644
638
|
const selected = await prompts.autocompleteMultiselect({
|
|
645
|
-
message: 'Select modules to install:',
|
|
639
|
+
message: 'Select official modules to install:',
|
|
646
640
|
options: allOptions,
|
|
647
641
|
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
|
648
642
|
lockedValues,
|
|
@@ -652,34 +646,275 @@ class UI {
|
|
|
652
646
|
|
|
653
647
|
const result = selected ? [...selected] : [];
|
|
654
648
|
|
|
655
|
-
// Display selected modules as bulleted list
|
|
656
649
|
if (result.length > 0) {
|
|
657
650
|
const moduleLines = result.map((moduleId) => {
|
|
658
651
|
const opt = allOptions.find((o) => o.value === moduleId);
|
|
659
652
|
return ` \u2022 ${opt?.label || moduleId}`;
|
|
660
653
|
});
|
|
661
|
-
await prompts.log.message('Selected modules:\n' + moduleLines.join('\n'));
|
|
654
|
+
await prompts.log.message('Selected official modules:\n' + moduleLines.join('\n'));
|
|
662
655
|
}
|
|
663
656
|
|
|
664
657
|
return result;
|
|
665
658
|
}
|
|
666
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Browse and select community modules using category drill-down.
|
|
662
|
+
* Featured/promoted modules appear at the top.
|
|
663
|
+
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
664
|
+
* @returns {Object} { codes: string[], didBrowse: boolean }
|
|
665
|
+
*/
|
|
666
|
+
async _browseCommunityModules(installedModuleIds = new Set()) {
|
|
667
|
+
const browseCommunity = await prompts.confirm({
|
|
668
|
+
message: 'Would you like to browse community modules?',
|
|
669
|
+
default: false,
|
|
670
|
+
});
|
|
671
|
+
if (!browseCommunity) return { codes: [], didBrowse: false };
|
|
672
|
+
|
|
673
|
+
const { CommunityModuleManager } = require('./modules/community-manager');
|
|
674
|
+
const communityMgr = new CommunityModuleManager();
|
|
675
|
+
|
|
676
|
+
const s = await prompts.spinner();
|
|
677
|
+
s.start('Loading community module catalog...');
|
|
678
|
+
|
|
679
|
+
let categories, featured, allCommunity;
|
|
680
|
+
try {
|
|
681
|
+
[categories, featured, allCommunity] = await Promise.all([
|
|
682
|
+
communityMgr.getCategoryList(),
|
|
683
|
+
communityMgr.listFeatured(),
|
|
684
|
+
communityMgr.listAll(),
|
|
685
|
+
]);
|
|
686
|
+
s.stop(`Community catalog loaded (${allCommunity.length} modules)`);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
s.error('Failed to load community catalog');
|
|
689
|
+
await prompts.log.warn(` ${error.message}`);
|
|
690
|
+
return { codes: [], didBrowse: false };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (allCommunity.length === 0) {
|
|
694
|
+
await prompts.log.info('No community modules are currently available.');
|
|
695
|
+
return { codes: [], didBrowse: false };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const selectedCodes = new Set();
|
|
699
|
+
let browsing = true;
|
|
700
|
+
|
|
701
|
+
while (browsing) {
|
|
702
|
+
const categoryChoices = [];
|
|
703
|
+
|
|
704
|
+
// Featured section at top
|
|
705
|
+
if (featured.length > 0) {
|
|
706
|
+
categoryChoices.push({
|
|
707
|
+
value: '__featured__',
|
|
708
|
+
label: `\u2605 Featured (${featured.length} module${featured.length === 1 ? '' : 's'})`,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Categories with module counts
|
|
713
|
+
for (const cat of categories) {
|
|
714
|
+
categoryChoices.push({
|
|
715
|
+
value: cat.slug,
|
|
716
|
+
label: `${cat.name} (${cat.moduleCount} module${cat.moduleCount === 1 ? '' : 's'})`,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Special actions at bottom
|
|
721
|
+
categoryChoices.push(
|
|
722
|
+
{ value: '__all__', label: '\u25CE View all community modules' },
|
|
723
|
+
{ value: '__search__', label: '\u25CE Search by keyword' },
|
|
724
|
+
{ value: '__done__', label: '\u2713 Done browsing' },
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
const selectedCount = selectedCodes.size;
|
|
728
|
+
const categoryChoice = await prompts.select({
|
|
729
|
+
message: `Browse community modules${selectedCount > 0 ? ` (${selectedCount} selected)` : ''}:`,
|
|
730
|
+
choices: categoryChoices,
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
if (categoryChoice === '__done__') {
|
|
734
|
+
browsing = false;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let modulesToShow;
|
|
739
|
+
switch (categoryChoice) {
|
|
740
|
+
case '__featured__': {
|
|
741
|
+
modulesToShow = featured;
|
|
742
|
+
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case '__all__': {
|
|
746
|
+
modulesToShow = allCommunity;
|
|
747
|
+
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case '__search__': {
|
|
751
|
+
const query = await prompts.text({
|
|
752
|
+
message: 'Search community modules:',
|
|
753
|
+
placeholder: 'e.g., design, testing, game',
|
|
754
|
+
});
|
|
755
|
+
if (!query || query.trim() === '') continue;
|
|
756
|
+
modulesToShow = await communityMgr.searchByKeyword(query.trim());
|
|
757
|
+
if (modulesToShow.length === 0) {
|
|
758
|
+
await prompts.log.warn('No matching modules found.');
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
default: {
|
|
765
|
+
modulesToShow = await communityMgr.listByCategory(categoryChoice);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Build options for autocompleteMultiselect
|
|
770
|
+
const trustBadge = (tier) => {
|
|
771
|
+
if (tier === 'bmad-certified') return '\u2713';
|
|
772
|
+
if (tier === 'community-reviewed') return '\u25CB';
|
|
773
|
+
return '\u26A0';
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const options = modulesToShow.map((mod) => {
|
|
777
|
+
const versionStr = mod.version ? ` (v${mod.version})` : '';
|
|
778
|
+
const badge = trustBadge(mod.trustTier);
|
|
779
|
+
return {
|
|
780
|
+
label: `${mod.displayName}${versionStr} [${badge}]`,
|
|
781
|
+
value: mod.code,
|
|
782
|
+
hint: mod.description,
|
|
783
|
+
};
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Pre-check modules that are already selected or installed
|
|
787
|
+
const initialValues = modulesToShow.filter((m) => selectedCodes.has(m.code) || installedModuleIds.has(m.code)).map((m) => m.code);
|
|
788
|
+
|
|
789
|
+
const selected = await prompts.autocompleteMultiselect({
|
|
790
|
+
message: 'Select community modules:',
|
|
791
|
+
options,
|
|
792
|
+
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
|
793
|
+
required: false,
|
|
794
|
+
maxItems: Math.min(options.length, 10),
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Update accumulated selections: sync with what user selected in this view
|
|
798
|
+
const shownCodes = new Set(modulesToShow.map((m) => m.code));
|
|
799
|
+
for (const code of shownCodes) {
|
|
800
|
+
if (selected && selected.includes(code)) {
|
|
801
|
+
selectedCodes.add(code);
|
|
802
|
+
} else {
|
|
803
|
+
selectedCodes.delete(code);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (selectedCodes.size > 0) {
|
|
809
|
+
const moduleLines = [];
|
|
810
|
+
for (const code of selectedCodes) {
|
|
811
|
+
const mod = await communityMgr.getModuleByCode(code);
|
|
812
|
+
moduleLines.push(` \u2022 ${mod?.displayName || code}`);
|
|
813
|
+
}
|
|
814
|
+
await prompts.log.message('Selected community modules:\n' + moduleLines.join('\n'));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return { codes: [...selectedCodes], didBrowse: true };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Prompt user to install modules from custom GitHub URLs.
|
|
822
|
+
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
823
|
+
* @returns {Array} Selected custom module code strings
|
|
824
|
+
*/
|
|
825
|
+
async _addCustomUrlModules(installedModuleIds = new Set()) {
|
|
826
|
+
const addCustom = await prompts.confirm({
|
|
827
|
+
message: 'Would you like to install from a custom GitHub URL?',
|
|
828
|
+
default: false,
|
|
829
|
+
});
|
|
830
|
+
if (!addCustom) return [];
|
|
831
|
+
|
|
832
|
+
const { CustomModuleManager } = require('./modules/custom-module-manager');
|
|
833
|
+
const customMgr = new CustomModuleManager();
|
|
834
|
+
const selectedModules = [];
|
|
835
|
+
|
|
836
|
+
let addMore = true;
|
|
837
|
+
while (addMore) {
|
|
838
|
+
const url = await prompts.text({
|
|
839
|
+
message: 'GitHub repository URL:',
|
|
840
|
+
placeholder: 'https://github.com/owner/repo',
|
|
841
|
+
validate: (input) => {
|
|
842
|
+
if (!input || input.trim() === '') return 'URL is required';
|
|
843
|
+
const result = customMgr.validateGitHubUrl(input.trim());
|
|
844
|
+
return result.isValid ? undefined : result.error;
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const s = await prompts.spinner();
|
|
849
|
+
s.start('Fetching module info...');
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
const plugins = await customMgr.discoverModules(url.trim());
|
|
853
|
+
s.stop('Module info loaded');
|
|
854
|
+
|
|
855
|
+
await prompts.log.warn(
|
|
856
|
+
'UNVERIFIED MODULE: This module has not been reviewed by the BMad team.\n' + ' Only install modules from sources you trust.',
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
for (const plugin of plugins) {
|
|
860
|
+
const versionStr = plugin.version ? ` v${plugin.version}` : '';
|
|
861
|
+
await prompts.log.info(` ${plugin.name}${versionStr}\n ${plugin.description}\n Author: ${plugin.author}`);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const confirmInstall = await prompts.confirm({
|
|
865
|
+
message: `Install ${plugins.length} plugin${plugins.length === 1 ? '' : 's'} from ${url.trim()}?`,
|
|
866
|
+
default: false,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
if (confirmInstall) {
|
|
870
|
+
// Pre-clone the repo so it's cached for the install pipeline
|
|
871
|
+
s.start('Cloning repository...');
|
|
872
|
+
try {
|
|
873
|
+
await customMgr.cloneRepo(url.trim());
|
|
874
|
+
s.stop('Repository cloned');
|
|
875
|
+
} catch (cloneError) {
|
|
876
|
+
s.error('Failed to clone repository');
|
|
877
|
+
await prompts.log.error(` ${cloneError.message}`);
|
|
878
|
+
addMore = await prompts.confirm({ message: 'Try another URL?', default: false });
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
for (const plugin of plugins) {
|
|
883
|
+
selectedModules.push(plugin.code);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
} catch (error) {
|
|
887
|
+
s.error('Failed to load module info');
|
|
888
|
+
await prompts.log.error(` ${error.message}`);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
addMore = await prompts.confirm({
|
|
892
|
+
message: 'Add another custom module?',
|
|
893
|
+
default: false,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (selectedModules.length > 0) {
|
|
898
|
+
await prompts.log.message('Selected custom modules:\n' + selectedModules.map((c) => ` \u2022 ${c}`).join('\n'));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return selectedModules;
|
|
902
|
+
}
|
|
903
|
+
|
|
667
904
|
/**
|
|
668
905
|
* Get default modules for non-interactive mode
|
|
669
906
|
* @param {Set} installedModuleIds - Already installed module IDs
|
|
670
907
|
* @returns {Array} Default module codes
|
|
671
908
|
*/
|
|
672
909
|
async getDefaultModules(installedModuleIds = new Set()) {
|
|
673
|
-
const
|
|
674
|
-
const
|
|
675
|
-
const { modules: localModules } = await officialModules.listAvailable();
|
|
910
|
+
const externalManager = new ExternalModuleManager();
|
|
911
|
+
const registryModules = await externalManager.listAvailable();
|
|
676
912
|
|
|
677
913
|
const defaultModules = [];
|
|
678
914
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
defaultModules.push(mod.id);
|
|
915
|
+
for (const mod of registryModules) {
|
|
916
|
+
if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
|
|
917
|
+
defaultModules.push(mod.code);
|
|
683
918
|
}
|
|
684
919
|
}
|
|
685
920
|
|
|
@@ -989,6 +1224,7 @@ class UI {
|
|
|
989
1224
|
// Group modules by source
|
|
990
1225
|
const builtIn = modules.filter((m) => m.source === 'built-in');
|
|
991
1226
|
const external = modules.filter((m) => m.source === 'external');
|
|
1227
|
+
const community = modules.filter((m) => m.source === 'community');
|
|
992
1228
|
const custom = modules.filter((m) => m.source === 'custom');
|
|
993
1229
|
const unknown = modules.filter((m) => m.source === 'unknown');
|
|
994
1230
|
|
|
@@ -1009,6 +1245,7 @@ class UI {
|
|
|
1009
1245
|
|
|
1010
1246
|
formatGroup(builtIn, 'Built-in Modules');
|
|
1011
1247
|
formatGroup(external, 'External Modules (Official)');
|
|
1248
|
+
formatGroup(community, 'Community Modules');
|
|
1012
1249
|
formatGroup(custom, 'Custom Modules');
|
|
1013
1250
|
formatGroup(unknown, 'Other Modules');
|
|
1014
1251
|
|