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.
Files changed (79) hide show
  1. package/.coderabbit.yaml +36 -0
  2. package/{CODE_OF_CONDUCT.md → .github/CODE_OF_CONDUCT.md} +4 -4
  3. package/CHANGELOG.md +136 -408
  4. package/README.md +4 -1
  5. package/docs/custom-content-installation.md +245 -0
  6. package/docs/index.md +2 -2
  7. package/docs/installers-bundlers/installers-modules-platforms-reference.md +6 -5
  8. package/docs/web-bundles-gemini-gpt-guide.md +1 -1
  9. package/example-custom-content/README.md +4 -0
  10. package/example-custom-content/agents/commit-poet/commit-poet.agent.yaml +1 -1
  11. package/example-custom-content/agents/toolsmith/toolsmith-sidecar/instructions.md +1 -1
  12. package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/docs.md +1 -1
  13. package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/installers.md +1 -1
  14. package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/modules.md +2 -2
  15. package/example-custom-content/agents/toolsmith/toolsmith.agent.yaml +1 -1
  16. package/example-custom-content/{custom.yaml → module.yaml} +1 -0
  17. package/example-custom-content/workflows/quiz-master/steps/step-01-init.md +2 -2
  18. package/example-custom-content/workflows/quiz-master/steps/step-02-q1.md +1 -1
  19. package/example-custom-content/workflows/quiz-master/steps/step-03-q2.md +1 -1
  20. package/example-custom-content/workflows/quiz-master/steps/step-04-q3.md +1 -1
  21. package/example-custom-content/workflows/quiz-master/steps/step-05-q4.md +1 -1
  22. package/example-custom-content/workflows/quiz-master/steps/step-06-q5.md +1 -1
  23. package/example-custom-content/workflows/quiz-master/steps/step-07-q6.md +1 -1
  24. package/example-custom-content/workflows/quiz-master/steps/step-08-q7.md +1 -1
  25. package/example-custom-content/workflows/quiz-master/steps/step-09-q8.md +1 -1
  26. package/example-custom-content/workflows/quiz-master/steps/step-10-q9.md +1 -1
  27. package/example-custom-content/workflows/quiz-master/steps/step-11-q10.md +1 -1
  28. package/example-custom-content/workflows/quiz-master/steps/step-12-results.md +1 -1
  29. package/example-custom-content/workflows/quiz-master/workflow.md +1 -1
  30. package/example-custom-module/mwm/README.md +5 -0
  31. package/example-custom-module/mwm/agents/cbt-coach/cbt-coach.agent.yaml +1 -0
  32. package/example-custom-module/mwm/agents/crisis-navigator.agent.yaml +3 -2
  33. package/example-custom-module/mwm/agents/meditation-guide.agent.yaml +3 -2
  34. package/example-custom-module/mwm/agents/wellness-companion/wellness-companion.agent.yaml +1 -0
  35. package/example-custom-module/mwm/{_module-installer/install-config.yaml → module.yaml} +1 -0
  36. package/package.json +1 -1
  37. package/src/core/_module-installer/installer.js +1 -1
  38. package/src/modules/bmb/_module-installer/installer.js +1 -1
  39. package/src/modules/bmb/docs/agents/index.md +1 -1
  40. package/src/modules/bmb/workflows/create-module/steps/step-04-structure.md +3 -3
  41. package/src/modules/bmb/workflows/create-module/steps/step-05-config.md +1 -1
  42. package/src/modules/bmb/workflows/create-module/steps/step-08-installer.md +8 -8
  43. package/src/modules/bmb/workflows/create-module/steps/step-09-documentation.md +2 -1
  44. package/src/modules/bmb/workflows/create-module/steps/step-10-roadmap.md +3 -2
  45. package/src/modules/bmb/workflows/create-module/steps/step-11-validate.md +3 -3
  46. package/src/modules/bmb/workflows/create-module/templates/installer.template.js +1 -1
  47. package/src/modules/bmb/workflows/create-module/validation.md +3 -3
  48. package/src/modules/bmb/workflows/create-workflow/steps/step-01-init.md +1 -1
  49. package/src/modules/bmb/workflows/create-workflow/steps/step-07-build.md +1 -1
  50. package/src/modules/bmgd/README.md +2 -1
  51. package/src/modules/bmm/_module-installer/installer.js +1 -1
  52. package/src/modules/bmm/_module-installer/platform-specifics/claude-code.js +1 -1
  53. package/src/modules/bmm/_module-installer/platform-specifics/windsurf.js +1 -1
  54. package/src/modules/cis/_module-installer/installer.js +1 -1
  55. package/tools/cli/README.md +4 -4
  56. package/tools/cli/installers/lib/core/config-collector.js +16 -8
  57. package/tools/cli/installers/lib/core/custom-module-cache.js +239 -0
  58. package/tools/cli/installers/lib/core/detector.js +8 -4
  59. package/tools/cli/installers/lib/core/installer.js +815 -23
  60. package/tools/cli/installers/lib/core/manifest-generator.js +176 -13
  61. package/tools/cli/installers/lib/core/manifest.js +47 -0
  62. package/tools/cli/installers/lib/custom/handler.js +150 -20
  63. package/tools/cli/installers/lib/modules/manager.js +78 -32
  64. package/tools/cli/lib/agent/compiler.js +3 -11
  65. package/tools/cli/lib/agent/installer.js +2 -1
  66. package/tools/cli/lib/cli-utils.js +21 -4
  67. package/tools/cli/lib/ui.js +499 -11
  68. package/tools/maintainer/review-pr-README.md +55 -0
  69. package/tools/maintainer/review-pr.md +242 -0
  70. package/tools/migrate-custom-module-paths.js +124 -0
  71. package/bmad-method-6.0.0-alpha.14.tgz +0 -0
  72. package/docs/custom-agent-installation.md +0 -137
  73. package/example-custom-content/workflows/quiz-master/workflow-plan-quiz-master.md +0 -269
  74. /package/src/core/{_module-installer/install-config.yaml → module.yaml} +0 -0
  75. /package/src/modules/bmb/{_module-installer/install-config.yaml → module.yaml} +0 -0
  76. /package/src/modules/bmb/workflows/create-module/templates/{install-config.template.yaml → module.template.yaml} +0 -0
  77. /package/src/modules/bmgd/{_module-installer/install-config.yaml → module.yaml} +0 -0
  78. /package/src/modules/bmm/{_module-installer/install-config.yaml → module.yaml} +0 -0
  79. /package/src/modules/cis/{_module-installer/install-config.yaml → module.yaml} +0 -0
@@ -22,6 +22,7 @@ const path = require('node:path');
22
22
  const fs = require('fs-extra');
23
23
  const chalk = require('chalk');
24
24
  const ora = require('ora');
25
+ const inquirer = require('inquirer');
25
26
  const { Detector } = require('./detector');
26
27
  const { Manifest } = require('./manifest');
27
28
  const { ModuleManager } = require('../modules/manager');
@@ -129,7 +130,7 @@ class Installer {
129
130
  */
130
131
  async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
131
132
  // List of text file extensions that should have placeholder replacement
132
- const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv'];
133
+ const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
133
134
  const ext = path.extname(sourcePath).toLowerCase();
134
135
 
135
136
  // Check if this is a text file that might contain placeholders
@@ -750,13 +751,81 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
750
751
  spinner.text = 'Creating directory structure...';
751
752
  await this.createDirectoryStructure(bmadDir);
752
753
 
753
- // Resolve dependencies for selected modules
754
- spinner.text = 'Resolving dependencies...';
754
+ // Get project root
755
755
  const projectRoot = getProjectRoot();
756
- const modulesToInstall = config.installCore ? ['core', ...config.modules] : config.modules;
756
+
757
+ // Step 1: Install core module first (if requested)
758
+ if (config.installCore) {
759
+ spinner.start('Installing BMAD core...');
760
+ await this.installCoreWithDependencies(bmadDir, { core: {} });
761
+ spinner.succeed('Core installed');
762
+
763
+ // Generate core config file
764
+ await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
765
+ }
766
+
767
+ // Custom content is already handled in UI before module selection
768
+ let finalCustomContent = config.customContent;
769
+
770
+ // Step 3: Prepare modules list including cached custom modules
771
+ let allModules = [...(config.modules || [])];
772
+
773
+ // During quick update, we might have custom module sources from the manifest
774
+ if (config._customModuleSources) {
775
+ // Add custom modules from stored sources
776
+ for (const [moduleId, customInfo] of config._customModuleSources) {
777
+ if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
778
+ allModules.push(moduleId);
779
+ }
780
+ }
781
+ }
782
+
783
+ // Add cached custom modules
784
+ if (finalCustomContent && finalCustomContent.cachedModules) {
785
+ for (const cachedModule of finalCustomContent.cachedModules) {
786
+ if (!allModules.includes(cachedModule.id)) {
787
+ allModules.push(cachedModule.id);
788
+ }
789
+ }
790
+ }
791
+
792
+ // Regular custom content from user input (non-cached)
793
+ if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
794
+ // Add custom modules to the installation list
795
+ for (const customFile of finalCustomContent.selectedFiles) {
796
+ const { CustomHandler } = require('../custom/handler');
797
+ const customHandler = new CustomHandler();
798
+ const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
799
+ if (customInfo && customInfo.id) {
800
+ allModules.push(customInfo.id);
801
+ }
802
+ }
803
+ }
804
+
805
+ // Don't include core again if already installed
806
+ if (config.installCore) {
807
+ allModules = allModules.filter((m) => m !== 'core');
808
+ }
809
+
810
+ const modulesToInstall = allModules;
757
811
 
758
812
  // For dependency resolution, we need to pass the project root
759
- const resolution = await this.dependencyResolver.resolve(projectRoot, config.modules || [], { verbose: config.verbose });
813
+ // Create a temporary module manager that knows about custom content locations
814
+ const tempModuleManager = new ModuleManager({
815
+ scanProjectForModules: true,
816
+ bmadDir: bmadDir, // Pass bmadDir so we can check cache
817
+ });
818
+
819
+ // Make sure custom modules are discoverable
820
+ if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
821
+ // The dependency resolver needs to know about these modules
822
+ // We'll handle custom modules separately in the installation loop
823
+ }
824
+
825
+ const resolution = await this.dependencyResolver.resolve(projectRoot, allModules, {
826
+ verbose: config.verbose,
827
+ moduleManager: tempModuleManager,
828
+ });
760
829
 
761
830
  if (config.verbose) {
762
831
  spinner.succeed('Dependencies resolved');
@@ -764,24 +833,159 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
764
833
  spinner.succeed('Dependencies resolved');
765
834
  }
766
835
 
767
- // Install core if requested or if dependencies require it
768
- if (config.installCore || resolution.byModule.core) {
769
- spinner.start('Installing BMAD core...');
770
- await this.installCoreWithDependencies(bmadDir, resolution.byModule.core);
771
- spinner.succeed('Core installed');
772
- }
836
+ // Core is already installed above, skip if included in resolution
773
837
 
774
838
  // Install modules with their dependencies
775
- if (config.modules && config.modules.length > 0) {
776
- for (const moduleName of config.modules) {
839
+ if (allModules && allModules.length > 0) {
840
+ const installedModuleNames = new Set();
841
+
842
+ for (const moduleName of allModules) {
843
+ // Skip if already installed
844
+ if (installedModuleNames.has(moduleName)) {
845
+ continue;
846
+ }
847
+ installedModuleNames.add(moduleName);
848
+
777
849
  spinner.start(`Installing module: ${moduleName}...`);
778
- await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
850
+
851
+ // Check if this is a custom module
852
+ let isCustomModule = false;
853
+ let customInfo = null;
854
+ let useCache = false;
855
+
856
+ // First check if we have a cached version
857
+ if (finalCustomContent && finalCustomContent.cachedModules) {
858
+ const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
859
+ if (cachedModule) {
860
+ isCustomModule = true;
861
+ customInfo = {
862
+ id: moduleName,
863
+ path: cachedModule.cachePath,
864
+ config: {},
865
+ };
866
+ useCache = true;
867
+ }
868
+ }
869
+
870
+ // Then check if we have custom module sources from the manifest (for quick update)
871
+ if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
872
+ customInfo = config._customModuleSources.get(moduleName);
873
+ isCustomModule = true;
874
+
875
+ // Check if this is a cached module (source path starts with _cfg)
876
+ if (customInfo.sourcePath && (customInfo.sourcePath.startsWith('_cfg') || customInfo.sourcePath.includes('_cfg/custom'))) {
877
+ useCache = true;
878
+ // Make sure we have the right path structure
879
+ if (!customInfo.path) {
880
+ customInfo.path = customInfo.sourcePath;
881
+ }
882
+ }
883
+ }
884
+
885
+ // Finally check regular custom content
886
+ if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
887
+ const { CustomHandler } = require('../custom/handler');
888
+ const customHandler = new CustomHandler();
889
+ for (const customFile of finalCustomContent.selectedFiles) {
890
+ const info = await customHandler.getCustomInfo(customFile, projectDir);
891
+ if (info && info.id === moduleName) {
892
+ isCustomModule = true;
893
+ customInfo = info;
894
+ break;
895
+ }
896
+ }
897
+ }
898
+
899
+ if (isCustomModule && customInfo) {
900
+ // Install custom module using CustomHandler but as a proper module
901
+ const { CustomHandler } = require('../custom/handler');
902
+ const customHandler = new CustomHandler();
903
+
904
+ // Install to module directory instead of custom directory
905
+ const moduleTargetPath = path.join(bmadDir, moduleName);
906
+ await fs.ensureDir(moduleTargetPath);
907
+
908
+ const result = await customHandler.install(
909
+ customInfo.path,
910
+ path.join(bmadDir, 'temp-custom'),
911
+ { ...config.coreConfig, ...customInfo.config, _bmadDir: bmadDir },
912
+ (filePath) => {
913
+ // Track installed files with correct path
914
+ const relativePath = path.relative(path.join(bmadDir, 'temp-custom'), filePath);
915
+ const finalPath = path.join(moduleTargetPath, relativePath);
916
+ this.installedFiles.push(finalPath);
917
+ },
918
+ );
919
+
920
+ // Move from temp-custom to actual module directory
921
+ const tempCustomPath = path.join(bmadDir, 'temp-custom');
922
+ if (await fs.pathExists(tempCustomPath)) {
923
+ const customDir = path.join(tempCustomPath, 'custom');
924
+ if (await fs.pathExists(customDir)) {
925
+ // Move contents to module directory
926
+ const items = await fs.readdir(customDir);
927
+ for (const item of items) {
928
+ const srcPath = path.join(customDir, item);
929
+ const destPath = path.join(moduleTargetPath, item);
930
+
931
+ // If destination exists, remove it first (or we could merge)
932
+ if (await fs.pathExists(destPath)) {
933
+ await fs.remove(destPath);
934
+ }
935
+
936
+ await fs.move(srcPath, destPath);
937
+ }
938
+ }
939
+ await fs.remove(tempCustomPath);
940
+ }
941
+
942
+ // Create module config
943
+ await this.generateModuleConfigs(bmadDir, { [moduleName]: { ...config.coreConfig, ...customInfo.config } });
944
+
945
+ // Store custom module info for later manifest update
946
+ if (!config._customModulesToTrack) {
947
+ config._customModulesToTrack = [];
948
+ }
949
+
950
+ // For cached modules, use appropriate path handling
951
+ let sourcePath;
952
+ if (useCache) {
953
+ // Check if we have cached modules info (from initial install)
954
+ if (finalCustomContent && finalCustomContent.cachedModules) {
955
+ sourcePath = finalCustomContent.cachedModules.find((m) => m.id === moduleName)?.relativePath;
956
+ } else {
957
+ // During update, the sourcePath is already cache-relative if it starts with _cfg
958
+ sourcePath =
959
+ customInfo.sourcePath && customInfo.sourcePath.startsWith('_cfg')
960
+ ? customInfo.sourcePath
961
+ : path.relative(bmadDir, customInfo.path || customInfo.sourcePath);
962
+ }
963
+ } else {
964
+ sourcePath = path.resolve(customInfo.path || customInfo.sourcePath);
965
+ }
966
+
967
+ config._customModulesToTrack.push({
968
+ id: customInfo.id,
969
+ name: customInfo.name,
970
+ sourcePath: sourcePath,
971
+ installDate: new Date().toISOString(),
972
+ });
973
+ } else {
974
+ // Regular module installation
975
+ // Special case for core module
976
+ if (moduleName === 'core') {
977
+ await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
978
+ } else {
979
+ await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
980
+ }
981
+ }
982
+
779
983
  spinner.succeed(`Module installed: ${moduleName}`);
780
984
  }
781
985
 
782
986
  // Install partial modules (only dependencies)
783
987
  for (const [module, files] of Object.entries(resolution.byModule)) {
784
- if (!config.modules.includes(module) && module !== 'core') {
988
+ if (!allModules.includes(module) && module !== 'core') {
785
989
  const totalFiles =
786
990
  files.agents.length +
787
991
  files.tasks.length +
@@ -798,6 +1002,72 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
798
1002
  }
799
1003
  }
800
1004
 
1005
+ // Install custom content if provided AND selected
1006
+ // Process custom content that wasn't installed as modules
1007
+ // This is now handled in the module installation loop above
1008
+ // This section is kept for backward compatibility with any custom content
1009
+ // that doesn't have a module structure
1010
+ const remainingCustomContent = [];
1011
+ if (
1012
+ config.customContent &&
1013
+ config.customContent.hasCustomContent &&
1014
+ config.customContent.customPath &&
1015
+ config.customContent.selected &&
1016
+ config.customContent.selectedFiles
1017
+ ) {
1018
+ // Filter out custom modules that were already installed
1019
+ for (const customFile of config.customContent.selectedFiles) {
1020
+ const { CustomHandler } = require('../custom/handler');
1021
+ const customHandler = new CustomHandler();
1022
+ const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
1023
+
1024
+ // Skip if this was installed as a module
1025
+ if (!customInfo || !customInfo.id || !allModules.includes(customInfo.id)) {
1026
+ remainingCustomContent.push(customFile);
1027
+ }
1028
+ }
1029
+ }
1030
+
1031
+ if (remainingCustomContent.length > 0) {
1032
+ spinner.start('Installing remaining custom content...');
1033
+ const { CustomHandler } = require('../custom/handler');
1034
+ const customHandler = new CustomHandler();
1035
+
1036
+ // Use the remaining files
1037
+ const customFiles = remainingCustomContent;
1038
+
1039
+ if (customFiles.length > 0) {
1040
+ console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
1041
+ for (const customFile of customFiles) {
1042
+ const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
1043
+ if (customInfo) {
1044
+ console.log(chalk.dim(` • ${customInfo.name} (${customInfo.relativePath})`));
1045
+
1046
+ // Install the custom content
1047
+ const result = await customHandler.install(
1048
+ customInfo.path,
1049
+ bmadDir,
1050
+ { ...config.coreConfig, ...customInfo.config },
1051
+ (filePath) => {
1052
+ // Track installed files
1053
+ this.installedFiles.push(filePath);
1054
+ },
1055
+ );
1056
+
1057
+ if (result.errors.length > 0) {
1058
+ console.log(chalk.yellow(` ⚠️ ${result.errors.length} error(s) occurred`));
1059
+ for (const error of result.errors) {
1060
+ console.log(chalk.dim(` - ${error}`));
1061
+ }
1062
+ } else {
1063
+ console.log(chalk.green(` ✓ Installed ${result.agentsInstalled} agents, ${result.workflowsInstalled} workflows`));
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+ spinner.succeed('Custom content installed');
1069
+ }
1070
+
801
1071
  // Generate clean config.yaml files for each installed module
802
1072
  spinner.start('Generating module configurations...');
803
1073
  await this.generateModuleConfigs(bmadDir, moduleConfigs);
@@ -820,14 +1090,37 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
820
1090
  spinner.start('Generating workflow and agent manifests...');
821
1091
  const manifestGen = new ManifestGenerator();
822
1092
 
823
- // Include preserved modules (from quick update) in the manifest
824
- const allModulesToList = config._preserveModules ? [...(config.modules || []), ...config._preserveModules] : config.modules || [];
1093
+ // For quick update, we need ALL installed modules in the manifest
1094
+ // Not just the ones being updated
1095
+ const allModulesForManifest = config._quickUpdate
1096
+ ? config._existingModules || allModules || []
1097
+ : config._preserveModules
1098
+ ? [...allModules, ...config._preserveModules]
1099
+ : allModules || [];
825
1100
 
826
- const manifestStats = await manifestGen.generateManifests(bmadDir, config.modules || [], this.installedFiles, {
1101
+ // For regular installs (including when called from quick update), use what we have
1102
+ let modulesForCsvPreserve;
1103
+ if (config._quickUpdate) {
1104
+ // Quick update - use existing modules or fall back to modules being updated
1105
+ modulesForCsvPreserve = config._existingModules || allModules || [];
1106
+ } else {
1107
+ // Regular install - use the modules we're installing plus any preserved ones
1108
+ modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
1109
+ }
1110
+
1111
+ const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, this.installedFiles, {
827
1112
  ides: config.ides || [],
828
- preservedModules: config._preserveModules || [], // Scan these from installed bmad/ dir
1113
+ preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
829
1114
  });
830
1115
 
1116
+ // Add custom modules to manifest (now that it exists)
1117
+ if (config._customModulesToTrack && config._customModulesToTrack.length > 0) {
1118
+ spinner.text = 'Storing custom module sources...';
1119
+ for (const customModule of config._customModulesToTrack) {
1120
+ await this.manifest.addCustomModule(bmadDir, customModule);
1121
+ }
1122
+ }
1123
+
831
1124
  spinner.succeed(
832
1125
  `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
833
1126
  );
@@ -1090,6 +1383,30 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
1090
1383
  const currentVersion = existingInstall.version;
1091
1384
  const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
1092
1385
 
1386
+ // Check for custom modules with missing sources before update
1387
+ const customModuleSources = new Map();
1388
+ if (existingInstall.customModules) {
1389
+ for (const customModule of existingInstall.customModules) {
1390
+ customModuleSources.set(customModule.id, customModule);
1391
+ }
1392
+ }
1393
+
1394
+ if (customModuleSources.size > 0) {
1395
+ spinner.stop();
1396
+ console.log(chalk.yellow('\nChecking custom module sources before update...'));
1397
+
1398
+ const projectRoot = getProjectRoot();
1399
+ await this.handleMissingCustomSources(
1400
+ customModuleSources,
1401
+ bmadDir,
1402
+ projectRoot,
1403
+ 'update',
1404
+ existingInstall.modules.map((m) => m.id),
1405
+ );
1406
+
1407
+ spinner.start('Preparing update...');
1408
+ }
1409
+
1093
1410
  if (config.dryRun) {
1094
1411
  spinner.stop();
1095
1412
  console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n'));
@@ -1547,6 +1864,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
1547
1864
  // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
1548
1865
  // const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
1549
1866
 
1867
+ // Replace {bmad_folder} with actual folder name
1868
+ xmlContent = xmlContent.replaceAll('{bmad_folder}', this.bmadFolderName || 'bmad');
1869
+
1550
1870
  // Replace {agent_sidecar_folder} if configured
1551
1871
  const coreConfig = this.configCollector.collectedConfig.core || {};
1552
1872
  if (coreConfig.agent_sidecar_folder && xmlContent.includes('{agent_sidecar_folder}')) {
@@ -1858,6 +2178,24 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
1858
2178
  throw new Error(`BMAD not installed at ${bmadDir}`);
1859
2179
  }
1860
2180
 
2181
+ // Check for custom modules with missing sources
2182
+ const manifest = await this.manifest.read(bmadDir);
2183
+ if (manifest && manifest.customModules && manifest.customModules.length > 0) {
2184
+ spinner.stop();
2185
+ console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
2186
+
2187
+ const customModuleSources = new Map();
2188
+ for (const customModule of manifest.customModules) {
2189
+ customModuleSources.set(customModule.id, customModule);
2190
+ }
2191
+
2192
+ const projectRoot = getProjectRoot();
2193
+ const installedModules = manifest.modules || [];
2194
+ await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
2195
+
2196
+ spinner.start('Rebuilding agent files...');
2197
+ }
2198
+
1861
2199
  let agentCount = 0;
1862
2200
  let taskCount = 0;
1863
2201
 
@@ -2002,17 +2340,245 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
2002
2340
  const existingInstall = await this.detector.detect(bmadDir);
2003
2341
  const installedModules = existingInstall.modules.map((m) => m.id);
2004
2342
  const configuredIdes = existingInstall.ides || [];
2343
+ const projectRoot = path.dirname(bmadDir);
2344
+
2345
+ // Get custom module sources from manifest
2346
+ const customModuleSources = new Map();
2347
+ if (existingInstall.customModules) {
2348
+ for (const customModule of existingInstall.customModules) {
2349
+ // Ensure we have an absolute sourcePath
2350
+ let absoluteSourcePath = customModule.sourcePath;
2351
+
2352
+ // Check if sourcePath is a cache-relative path (starts with _cfg/)
2353
+ if (absoluteSourcePath && absoluteSourcePath.startsWith('_cfg')) {
2354
+ // Convert cache-relative path to absolute path
2355
+ absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
2356
+ }
2357
+ // If no sourcePath but we have relativePath, convert it
2358
+ else if (!absoluteSourcePath && customModule.relativePath) {
2359
+ // relativePath is relative to the project root (parent of bmad dir)
2360
+ absoluteSourcePath = path.resolve(projectRoot, customModule.relativePath);
2361
+ }
2362
+ // Ensure sourcePath is absolute for anything else
2363
+ else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
2364
+ absoluteSourcePath = path.resolve(absoluteSourcePath);
2365
+ }
2366
+
2367
+ // Update the custom module object with the absolute path
2368
+ const updatedModule = {
2369
+ ...customModule,
2370
+ sourcePath: absoluteSourcePath,
2371
+ };
2372
+
2373
+ customModuleSources.set(customModule.id, updatedModule);
2374
+ }
2375
+ }
2005
2376
 
2006
2377
  // Load saved IDE configurations
2007
2378
  const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
2008
2379
 
2009
2380
  // Get available modules (what we have source for)
2010
- const availableModules = await this.moduleManager.listAvailable();
2011
- const availableModuleIds = new Set(availableModules.map((m) => m.id));
2381
+ const availableModulesData = await this.moduleManager.listAvailable();
2382
+ const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
2383
+
2384
+ // Add custom modules from manifest if their sources exist
2385
+ for (const [moduleId, customModule] of customModuleSources) {
2386
+ // Use the absolute sourcePath
2387
+ const sourcePath = customModule.sourcePath;
2388
+
2389
+ // Check if source exists at the recorded path
2390
+ if (
2391
+ sourcePath &&
2392
+ (await fs.pathExists(sourcePath)) && // Add to available modules if not already there
2393
+ !availableModules.some((m) => m.id === moduleId)
2394
+ ) {
2395
+ availableModules.push({
2396
+ id: moduleId,
2397
+ name: customModule.name || moduleId,
2398
+ path: sourcePath,
2399
+ isCustom: true,
2400
+ fromManifest: true,
2401
+ });
2402
+ }
2403
+ }
2404
+
2405
+ // Check for untracked custom modules (installed but not in manifest)
2406
+ const untrackedCustomModules = [];
2407
+ for (const installedModule of installedModules) {
2408
+ // Skip standard modules and core
2409
+ const standardModuleIds = ['bmb', 'bmgd', 'bmm', 'cis', 'core'];
2410
+ if (standardModuleIds.includes(installedModule)) {
2411
+ continue;
2412
+ }
2413
+
2414
+ // Check if this installed module is not tracked in customModules
2415
+ if (!customModuleSources.has(installedModule)) {
2416
+ const modulePath = path.join(bmadDir, installedModule);
2417
+ if (await fs.pathExists(modulePath)) {
2418
+ untrackedCustomModules.push({
2419
+ id: installedModule,
2420
+ name: installedModule, // We don't have the original name
2421
+ path: modulePath,
2422
+ untracked: true,
2423
+ });
2424
+ }
2425
+ }
2426
+ }
2427
+
2428
+ // If we found untracked custom modules, offer to track them
2429
+ if (untrackedCustomModules.length > 0) {
2430
+ spinner.stop();
2431
+ console.log(chalk.yellow(`\n⚠️ Found ${untrackedCustomModules.length} custom module(s) not tracked in manifest:`));
2432
+
2433
+ for (const untracked of untrackedCustomModules) {
2434
+ console.log(chalk.dim(` • ${untracked.id} (installed at ${path.relative(projectRoot, untracked.path)})`));
2435
+ }
2436
+
2437
+ const { trackModules } = await inquirer.prompt([
2438
+ {
2439
+ type: 'confirm',
2440
+ name: 'trackModules',
2441
+ message: chalk.cyan('Would you like to scan for their source locations?'),
2442
+ default: true,
2443
+ },
2444
+ ]);
2445
+
2446
+ if (trackModules) {
2447
+ const { scanDirectory } = await inquirer.prompt([
2448
+ {
2449
+ type: 'input',
2450
+ name: 'scanDirectory',
2451
+ message: 'Enter directory to scan for custom module sources (or leave blank to skip):',
2452
+ default: projectRoot,
2453
+ validate: async (input) => {
2454
+ if (input && input.trim() !== '') {
2455
+ const expandedPath = path.resolve(input.trim());
2456
+ if (!(await fs.pathExists(expandedPath))) {
2457
+ return 'Directory does not exist';
2458
+ }
2459
+ const stats = await fs.stat(expandedPath);
2460
+ if (!stats.isDirectory()) {
2461
+ return 'Path must be a directory';
2462
+ }
2463
+ }
2464
+ return true;
2465
+ },
2466
+ },
2467
+ ]);
2468
+
2469
+ if (scanDirectory && scanDirectory.trim() !== '') {
2470
+ console.log(chalk.dim('\nScanning for custom module sources...'));
2471
+
2472
+ // Scan for all module.yaml files
2473
+ const allModulePaths = await this.moduleManager.findModulesInProject(scanDirectory);
2474
+ const { ModuleManager } = require('../modules/manager');
2475
+ const mm = new ModuleManager({ scanProjectForModules: true });
2476
+
2477
+ for (const untracked of untrackedCustomModules) {
2478
+ let foundSource = null;
2479
+
2480
+ // Try to find by module ID
2481
+ for (const modulePath of allModulePaths) {
2482
+ try {
2483
+ const moduleInfo = await mm.getModuleInfo(modulePath);
2484
+ if (moduleInfo && moduleInfo.id === untracked.id) {
2485
+ foundSource = {
2486
+ path: modulePath,
2487
+ info: moduleInfo,
2488
+ };
2489
+ break;
2490
+ }
2491
+ } catch {
2492
+ // Continue searching
2493
+ }
2494
+ }
2495
+
2496
+ if (foundSource) {
2497
+ console.log(chalk.green(` ✓ Found source for ${untracked.id}: ${path.relative(projectRoot, foundSource.path)}`));
2498
+
2499
+ // Add to manifest
2500
+ await this.manifest.addCustomModule(bmadDir, {
2501
+ id: untracked.id,
2502
+ name: foundSource.info.name || untracked.name,
2503
+ sourcePath: path.resolve(foundSource.path),
2504
+ installDate: new Date().toISOString(),
2505
+ tracked: true,
2506
+ });
2507
+
2508
+ // Add to customModuleSources for processing
2509
+ customModuleSources.set(untracked.id, {
2510
+ id: untracked.id,
2511
+ name: foundSource.info.name || untracked.name,
2512
+ sourcePath: path.resolve(foundSource.path),
2513
+ });
2514
+ } else {
2515
+ console.log(chalk.yellow(` ⚠ Could not find source for ${untracked.id}`));
2516
+ }
2517
+ }
2518
+ }
2519
+ }
2520
+
2521
+ console.log(chalk.dim('\nUntracked custom modules will remain installed but cannot be updated without their source.'));
2522
+ spinner.start('Preparing update...');
2523
+ }
2524
+
2525
+ // Handle missing custom module sources using shared method
2526
+ const customModuleResult = await this.handleMissingCustomSources(
2527
+ customModuleSources,
2528
+ bmadDir,
2529
+ projectRoot,
2530
+ 'update',
2531
+ installedModules,
2532
+ );
2533
+
2534
+ // Handle both old return format (array) and new format (object)
2535
+ let validCustomModules = [];
2536
+ let keptModulesWithoutSources = [];
2537
+
2538
+ if (Array.isArray(customModuleResult)) {
2539
+ // Old format - just an array
2540
+ validCustomModules = customModuleResult;
2541
+ } else if (customModuleResult && typeof customModuleResult === 'object') {
2542
+ // New format - object with two arrays
2543
+ validCustomModules = customModuleResult.validCustomModules || [];
2544
+ keptModulesWithoutSources = customModuleResult.keptModulesWithoutSources || [];
2545
+ }
2546
+
2547
+ const customModulesFromManifest = validCustomModules.map((m) => ({
2548
+ ...m,
2549
+ isCustom: true,
2550
+ hasUpdate: true,
2551
+ }));
2552
+
2553
+ // Add untracked modules to the update list but mark them as untrackable
2554
+ for (const untracked of untrackedCustomModules) {
2555
+ if (!customModuleSources.has(untracked.id)) {
2556
+ customModulesFromManifest.push({
2557
+ ...untracked,
2558
+ isCustom: true,
2559
+ hasUpdate: false, // Can't update without source
2560
+ untracked: true,
2561
+ });
2562
+ }
2563
+ }
2564
+
2565
+ const allAvailableModules = [...availableModules, ...customModulesFromManifest];
2566
+ const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
2567
+
2568
+ // Core module is special - never include it in update flow
2569
+ const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core');
2012
2570
 
2013
2571
  // Only update modules that are BOTH installed AND available (we have source for)
2014
- const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
2015
- const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
2572
+ const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
2573
+ const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id));
2574
+
2575
+ // Add custom modules that were kept without sources to the skipped modules
2576
+ // This ensures their agents are preserved in the manifest
2577
+ for (const keptModule of keptModulesWithoutSources) {
2578
+ if (!skippedModules.includes(keptModule)) {
2579
+ skippedModules.push(keptModule);
2580
+ }
2581
+ }
2016
2582
 
2017
2583
  spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
2018
2584
 
@@ -2077,6 +2643,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
2077
2643
  _quickUpdate: true, // Flag to skip certain prompts
2078
2644
  _preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
2079
2645
  _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
2646
+ _customModuleSources: customModuleSources, // Pass custom module sources for updates
2647
+ _existingModules: installedModules, // Pass all installed modules for manifest generation
2080
2648
  };
2081
2649
 
2082
2650
  // Call the standard install method
@@ -2716,6 +3284,230 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
2716
3284
  }
2717
3285
  }
2718
3286
  }
3287
+
3288
+ /**
3289
+ * Handle missing custom module sources interactively
3290
+ * @param {Map} customModuleSources - Map of custom module ID to info
3291
+ * @param {string} bmadDir - BMAD directory
3292
+ * @param {string} projectRoot - Project root directory
3293
+ * @param {string} operation - Current operation ('update', 'compile', etc.)
3294
+ * @param {Array} installedModules - Array of installed module IDs (will be modified)
3295
+ * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
3296
+ */
3297
+ async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) {
3298
+ const validCustomModules = [];
3299
+ const keptModulesWithoutSources = []; // Track modules kept without sources
3300
+ const customModulesWithMissingSources = [];
3301
+
3302
+ // Check which sources exist
3303
+ for (const [moduleId, customInfo] of customModuleSources) {
3304
+ if (await fs.pathExists(customInfo.sourcePath)) {
3305
+ validCustomModules.push({
3306
+ id: moduleId,
3307
+ name: customInfo.name,
3308
+ path: customInfo.sourcePath,
3309
+ info: customInfo,
3310
+ });
3311
+ } else {
3312
+ customModulesWithMissingSources.push({
3313
+ id: moduleId,
3314
+ name: customInfo.name,
3315
+ sourcePath: customInfo.sourcePath,
3316
+ relativePath: customInfo.relativePath,
3317
+ info: customInfo,
3318
+ });
3319
+ }
3320
+ }
3321
+
3322
+ // If no missing sources, return immediately
3323
+ if (customModulesWithMissingSources.length === 0) {
3324
+ return validCustomModules;
3325
+ }
3326
+
3327
+ // Stop any spinner for interactive prompts
3328
+ const currentSpinner = ora();
3329
+ if (currentSpinner.isSpinning) {
3330
+ currentSpinner.stop();
3331
+ }
3332
+
3333
+ console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
3334
+
3335
+ const inquirer = require('inquirer');
3336
+ let keptCount = 0;
3337
+ let updatedCount = 0;
3338
+ let removedCount = 0;
3339
+
3340
+ for (const missing of customModulesWithMissingSources) {
3341
+ console.log(chalk.dim(` • ${missing.name} (${missing.id})`));
3342
+ console.log(chalk.dim(` Original source: ${missing.relativePath}`));
3343
+ console.log(chalk.dim(` Full path: ${missing.sourcePath}`));
3344
+
3345
+ const choices = [
3346
+ {
3347
+ name: 'Keep installed (will not be processed)',
3348
+ value: 'keep',
3349
+ short: 'Keep',
3350
+ },
3351
+ {
3352
+ name: 'Specify new source location',
3353
+ value: 'update',
3354
+ short: 'Update',
3355
+ },
3356
+ ];
3357
+
3358
+ // Only add remove option if not just compiling agents
3359
+ if (operation !== 'compile-agents') {
3360
+ choices.push({
3361
+ name: '⚠️ REMOVE module completely (destructive!)',
3362
+ value: 'remove',
3363
+ short: 'Remove',
3364
+ });
3365
+ }
3366
+
3367
+ const { action } = await inquirer.prompt([
3368
+ {
3369
+ type: 'list',
3370
+ name: 'action',
3371
+ message: `How would you like to handle "${missing.name}"?`,
3372
+ choices,
3373
+ },
3374
+ ]);
3375
+
3376
+ switch (action) {
3377
+ case 'update': {
3378
+ const { newSourcePath } = await inquirer.prompt([
3379
+ {
3380
+ type: 'input',
3381
+ name: 'newSourcePath',
3382
+ message: 'Enter the new path to the custom module:',
3383
+ default: missing.sourcePath,
3384
+ validate: async (input) => {
3385
+ if (!input || input.trim() === '') {
3386
+ return 'Please enter a path';
3387
+ }
3388
+ const expandedPath = path.resolve(input.trim());
3389
+ if (!(await fs.pathExists(expandedPath))) {
3390
+ return 'Path does not exist';
3391
+ }
3392
+ // Check if it looks like a valid module
3393
+ const moduleYamlPath = path.join(expandedPath, 'module.yaml');
3394
+ const agentsPath = path.join(expandedPath, 'agents');
3395
+ const workflowsPath = path.join(expandedPath, 'workflows');
3396
+
3397
+ if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
3398
+ return 'Path does not appear to contain a valid custom module';
3399
+ }
3400
+ return true;
3401
+ },
3402
+ },
3403
+ ]);
3404
+
3405
+ // Update the source in manifest
3406
+ const resolvedPath = path.resolve(newSourcePath.trim());
3407
+ missing.info.sourcePath = resolvedPath;
3408
+ // Remove relativePath - we only store absolute sourcePath now
3409
+ delete missing.info.relativePath;
3410
+ await this.manifest.addCustomModule(bmadDir, missing.info);
3411
+
3412
+ validCustomModules.push({
3413
+ id: moduleId,
3414
+ name: missing.name,
3415
+ path: resolvedPath,
3416
+ info: missing.info,
3417
+ });
3418
+
3419
+ updatedCount++;
3420
+ console.log(chalk.green(`✓ Updated source location`));
3421
+
3422
+ break;
3423
+ }
3424
+ case 'remove': {
3425
+ // Extra confirmation for destructive remove
3426
+ console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
3427
+ console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`));
3428
+
3429
+ const { confirm } = await inquirer.prompt([
3430
+ {
3431
+ type: 'confirm',
3432
+ name: 'confirm',
3433
+ message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
3434
+ default: false,
3435
+ },
3436
+ ]);
3437
+
3438
+ if (confirm) {
3439
+ const { typedConfirm } = await inquirer.prompt([
3440
+ {
3441
+ type: 'input',
3442
+ name: 'typedConfirm',
3443
+ message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
3444
+ validate: (input) => {
3445
+ if (input !== 'DELETE') {
3446
+ return chalk.red('You must type "DELETE" exactly to proceed');
3447
+ }
3448
+ return true;
3449
+ },
3450
+ },
3451
+ ]);
3452
+
3453
+ if (typedConfirm === 'DELETE') {
3454
+ // Remove the module from filesystem and manifest
3455
+ const modulePath = path.join(bmadDir, moduleId);
3456
+ if (await fs.pathExists(modulePath)) {
3457
+ const fsExtra = require('fs-extra');
3458
+ await fsExtra.remove(modulePath);
3459
+ console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
3460
+ }
3461
+
3462
+ await this.manifest.removeModule(bmadDir, moduleId);
3463
+ await this.manifest.removeCustomModule(bmadDir, moduleId);
3464
+ console.log(chalk.yellow(` ✓ Removed from manifest`));
3465
+
3466
+ // Also remove from installedModules list
3467
+ if (installedModules && installedModules.includes(moduleId)) {
3468
+ const index = installedModules.indexOf(moduleId);
3469
+ if (index !== -1) {
3470
+ installedModules.splice(index, 1);
3471
+ }
3472
+ }
3473
+
3474
+ removedCount++;
3475
+ console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`));
3476
+ } else {
3477
+ console.log(chalk.dim(' Removal cancelled - module will be kept'));
3478
+ keptCount++;
3479
+ }
3480
+ } else {
3481
+ console.log(chalk.dim(' Removal cancelled - module will be kept'));
3482
+ keptCount++;
3483
+ }
3484
+
3485
+ break;
3486
+ }
3487
+ case 'keep': {
3488
+ keptCount++;
3489
+ keptModulesWithoutSources.push(moduleId);
3490
+ console.log(chalk.dim(` Module will be kept as-is`));
3491
+
3492
+ break;
3493
+ }
3494
+ // No default
3495
+ }
3496
+ }
3497
+
3498
+ // Show summary
3499
+ if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
3500
+ console.log(chalk.dim(`\nSummary for custom modules with missing sources:`));
3501
+ if (keptCount > 0) console.log(chalk.dim(` • ${keptCount} module(s) kept as-is`));
3502
+ if (updatedCount > 0) console.log(chalk.dim(` • ${updatedCount} module(s) updated with new sources`));
3503
+ if (removedCount > 0) console.log(chalk.red(` • ${removedCount} module(s) permanently deleted`));
3504
+ }
3505
+
3506
+ return {
3507
+ validCustomModules,
3508
+ keptModulesWithoutSources,
3509
+ };
3510
+ }
2719
3511
  }
2720
3512
 
2721
3513
  module.exports = { Installer };