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.
@@ -563,86 +563,80 @@ class UI {
563
563
  }
564
564
 
565
565
  /**
566
- * Select all modules (official + community) using grouped multiselect.
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
- const { OfficialModules } = require('./modules/official-modules');
573
- const officialModulesSource = new OfficialModules();
574
- const { modules: localModules } = await officialModulesSource.listAvailable();
571
+ // Phase 1: Official modules
572
+ const officialSelected = await this._selectOfficialModules(installedModuleIds);
575
573
 
576
- // Get external modules
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 externalModules = await externalManager.listAvailable();
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
- // Core module is always installed — show it locked at the top
586
- const coreVersion = await getMarketplaceVersion('core');
587
- const coreLabel = coreVersion ? `BMad Core Module (v${coreVersion})` : 'BMad Core Module';
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 || group,
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
- // Local modules (BMM, BMB, etc.)
606
- const localEntries = [];
607
- for (const mod of localModules) {
608
- if (mod.id !== 'core') {
609
- const entry = await buildModuleEntry(mod, mod.id, 'Local');
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 { OfficialModules } = require('./modules/official-modules');
674
- const officialModules = new OfficialModules();
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
- // Add default-selected local modules (typically BMM)
680
- for (const mod of localModules) {
681
- if (mod.defaultSelected === true || installedModuleIds.has(mod.id)) {
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