bmad-method 6.2.3-next.25 → 6.2.3-next.27

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.
@@ -97,7 +97,6 @@ class Manifest {
97
97
  lastUpdated: manifestData.installation?.lastUpdated,
98
98
  modules: moduleNames, // Simple array of module names for backward compatibility
99
99
  modulesDetailed: hasDetailedModules ? modules : null, // New detailed format
100
- customModules: manifestData.customModules || [], // Keep for backward compatibility
101
100
  ides: manifestData.ides || [],
102
101
  };
103
102
  } catch (error) {
@@ -254,7 +253,6 @@ class Manifest {
254
253
  lastUpdated: manifest.installation?.lastUpdated,
255
254
  modules: moduleNames,
256
255
  modulesDetailed: hasDetailedModules ? modules : null,
257
- customModules: manifest.customModules || [],
258
256
  ides: manifest.ides || [],
259
257
  };
260
258
  }
@@ -783,52 +781,6 @@ class Manifest {
783
781
 
784
782
  return configs;
785
783
  }
786
- /**
787
- * Add a custom module to the manifest with its source path
788
- * @param {string} bmadDir - Path to bmad directory
789
- * @param {Object} customModule - Custom module info
790
- */
791
- async addCustomModule(bmadDir, customModule) {
792
- const manifest = await this.read(bmadDir);
793
- if (!manifest) {
794
- throw new Error('No manifest found');
795
- }
796
-
797
- if (!manifest.customModules) {
798
- manifest.customModules = [];
799
- }
800
-
801
- // Check if custom module already exists
802
- const existingIndex = manifest.customModules.findIndex((m) => m.id === customModule.id);
803
- if (existingIndex === -1) {
804
- // Add new entry
805
- manifest.customModules.push(customModule);
806
- } else {
807
- // Update existing entry
808
- manifest.customModules[existingIndex] = customModule;
809
- }
810
-
811
- await this.update(bmadDir, { customModules: manifest.customModules });
812
- }
813
-
814
- /**
815
- * Remove a custom module from the manifest
816
- * @param {string} bmadDir - Path to bmad directory
817
- * @param {string} moduleId - Module ID to remove
818
- */
819
- async removeCustomModule(bmadDir, moduleId) {
820
- const manifest = await this.read(bmadDir);
821
- if (!manifest || !manifest.customModules) {
822
- return;
823
- }
824
-
825
- const index = manifest.customModules.findIndex((m) => m.id === moduleId);
826
- if (index !== -1) {
827
- manifest.customModules.splice(index, 1);
828
- await this.update(bmadDir, { customModules: manifest.customModules });
829
- }
830
- }
831
-
832
784
  /**
833
785
  * Get module version info from source
834
786
  * @param {string} moduleName - Module name/code
@@ -866,29 +818,8 @@ class Manifest {
866
818
  };
867
819
  }
868
820
 
869
- // Custom module: resolve path from source or cache before reading version
870
- const customSourcePath = moduleSourcePath || path.join(bmadDir, '_config', 'custom', moduleName);
871
- const version = await this._readMarketplaceVersion(moduleName, customSourcePath);
872
-
873
- const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
874
- const moduleYamlPath = path.join(cacheDir, 'module.yaml');
875
-
876
- if (await fs.pathExists(moduleYamlPath)) {
877
- try {
878
- const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
879
- const moduleConfig = yaml.parse(yamlContent);
880
- return {
881
- version: version || moduleConfig.version || null,
882
- source: 'custom',
883
- npmPackage: moduleConfig.npmPackage || null,
884
- repoUrl: moduleConfig.repoUrl || null,
885
- };
886
- } catch (error) {
887
- await prompts.log.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`);
888
- }
889
- }
890
-
891
821
  // Unknown module
822
+ const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
892
823
  return {
893
824
  version,
894
825
  source: 'unknown',
@@ -225,13 +225,20 @@ class ConfigDrivenIdeSetup {
225
225
  // Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
226
226
  // Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
227
227
  if (this.installerConfig?.legacy_targets) {
228
- if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
229
- for (const legacyDir of this.installerConfig.legacy_targets) {
230
- if (this.isGlobalPath(legacyDir)) {
231
- await this.warnGlobalLegacy(legacyDir, options);
232
- } else {
233
- await this.cleanupTarget(projectDir, legacyDir, options, null);
234
- await this.removeEmptyParents(projectDir, legacyDir);
228
+ const legacyDirsExist = await Promise.all(
229
+ this.installerConfig.legacy_targets.map((d) =>
230
+ this.isGlobalPath(d) ? fs.pathExists(d.replace(/^~/, os.homedir())) : fs.pathExists(path.join(projectDir, d)),
231
+ ),
232
+ );
233
+ if (legacyDirsExist.some(Boolean)) {
234
+ if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
235
+ for (const legacyDir of this.installerConfig.legacy_targets) {
236
+ if (this.isGlobalPath(legacyDir)) {
237
+ await this.warnGlobalLegacy(legacyDir, options);
238
+ } else {
239
+ await this.cleanupTarget(projectDir, legacyDir, options, null);
240
+ await this.removeEmptyParents(projectDir, legacyDir);
241
+ }
235
242
  }
236
243
  }
237
244
  }
@@ -1,67 +1,128 @@
1
1
  const fs = require('fs-extra');
2
2
  const os = require('node:os');
3
3
  const path = require('node:path');
4
+ const https = require('node:https');
4
5
  const { execSync } = require('node:child_process');
5
6
  const yaml = require('yaml');
6
7
  const prompts = require('../prompts');
7
8
 
9
+ const REGISTRY_RAW_URL = 'https://raw.githubusercontent.com/bmad-code-org/bmad-plugins-marketplace/main/registry/official.yaml';
10
+ const FALLBACK_CONFIG_PATH = path.join(__dirname, 'registry-fallback.yaml');
11
+
8
12
  /**
9
- * Manages external official modules defined in external-official-modules.yaml
10
- * These are modules hosted in external repositories that can be installed
13
+ * Manages official modules from the remote BMad marketplace registry.
14
+ * Fetches registry/official.yaml from GitHub; falls back to the bundled
15
+ * external-official-modules.yaml when the network is unavailable.
11
16
  *
12
17
  * @class ExternalModuleManager
13
18
  */
14
19
  class ExternalModuleManager {
15
- constructor() {
16
- this.externalModulesConfigPath = path.join(__dirname, '../external-official-modules.yaml');
17
- this.cachedModules = null;
20
+ constructor() {}
21
+
22
+ /**
23
+ * Fetch a URL and return the response body as a string.
24
+ * @param {string} url - URL to fetch
25
+ * @param {number} timeout - Timeout in ms (default 10s)
26
+ * @returns {Promise<string>} Response body
27
+ */
28
+ _fetch(url, timeout = 10_000) {
29
+ return new Promise((resolve, reject) => {
30
+ const req = https
31
+ .get(url, { timeout }, (res) => {
32
+ // Follow one redirect (GitHub sometimes 301s)
33
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
34
+ return this._fetch(res.headers.location, timeout).then(resolve, reject);
35
+ }
36
+ if (res.statusCode !== 200) {
37
+ return reject(new Error(`HTTP ${res.statusCode}`));
38
+ }
39
+ let data = '';
40
+ res.on('data', (chunk) => (data += chunk));
41
+ res.on('end', () => resolve(data));
42
+ })
43
+ .on('error', reject)
44
+ .on('timeout', () => {
45
+ req.destroy();
46
+ reject(new Error('Request timed out'));
47
+ });
48
+ });
18
49
  }
19
50
 
20
51
  /**
21
- * Load and parse the external-official-modules.yaml file
22
- * @returns {Object} Parsed YAML content with modules object
52
+ * Load the official modules registry from GitHub, falling back to the
53
+ * bundled YAML file if the fetch fails.
54
+ * @returns {Object} Parsed YAML content with modules array
23
55
  */
24
56
  async loadExternalModulesConfig() {
25
57
  if (this.cachedModules) {
26
58
  return this.cachedModules;
27
59
  }
28
60
 
61
+ // Try remote registry first
29
62
  try {
30
- const content = await fs.readFile(this.externalModulesConfigPath, 'utf8');
63
+ const content = await this._fetch(REGISTRY_RAW_URL);
64
+ const config = yaml.parse(content);
65
+ if (config?.modules?.length) {
66
+ this.cachedModules = config;
67
+ return config;
68
+ }
69
+ } catch {
70
+ // Fall through to local fallback
71
+ }
72
+
73
+ // Fallback to bundled file
74
+ try {
75
+ const content = await fs.readFile(FALLBACK_CONFIG_PATH, 'utf8');
31
76
  const config = yaml.parse(content);
32
77
  this.cachedModules = config;
78
+ await prompts.log.warn('Could not reach BMad registry; using bundled module list.');
33
79
  return config;
34
80
  } catch (error) {
35
- await prompts.log.warn(`Failed to load external modules config: ${error.message}`);
36
- return { modules: {} };
81
+ await prompts.log.warn(`Failed to load modules config: ${error.message}`);
82
+ return { modules: [] };
37
83
  }
38
84
  }
39
85
 
40
86
  /**
41
- * Get list of available external modules
87
+ * Normalize a module entry from either the remote registry format
88
+ * (snake_case, array) or the legacy bundled format (kebab-case, object map).
89
+ * @param {Object} mod - Raw module config from YAML
90
+ * @param {string} [key] - Key name (only for legacy map format)
91
+ * @returns {Object} Normalized module info
92
+ */
93
+ _normalizeModule(mod, key) {
94
+ return {
95
+ key: key || mod.name,
96
+ url: mod.repository || mod.url,
97
+ moduleDefinition: mod.module_definition || mod['module-definition'],
98
+ code: mod.code,
99
+ name: mod.display_name || mod.name,
100
+ description: mod.description || '',
101
+ defaultSelected: mod.default_selected === true || mod.defaultSelected === true,
102
+ type: mod.type || 'bmad-org',
103
+ npmPackage: mod.npm_package || mod.npmPackage || null,
104
+ builtIn: mod.built_in === true,
105
+ isExternal: mod.built_in !== true,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Get list of available modules from the registry
42
111
  * @returns {Array<Object>} Array of module info objects
43
112
  */
44
113
  async listAvailable() {
45
114
  const config = await this.loadExternalModulesConfig();
46
- const modules = [];
47
115
 
48
- for (const [key, moduleConfig] of Object.entries(config.modules || {})) {
49
- modules.push({
50
- key,
51
- url: moduleConfig.url,
52
- moduleDefinition: moduleConfig['module-definition'],
53
- code: moduleConfig.code,
54
- name: moduleConfig.name,
55
- header: moduleConfig.header,
56
- subheader: moduleConfig.subheader,
57
- description: moduleConfig.description || '',
58
- defaultSelected: moduleConfig.defaultSelected === true,
59
- type: moduleConfig.type || 'community', // bmad-org or community
60
- npmPackage: moduleConfig.npmPackage || null, // Include npm package name
61
- isExternal: true,
62
- });
116
+ // Remote format: modules is an array
117
+ if (Array.isArray(config.modules)) {
118
+ return config.modules.map((mod) => this._normalizeModule(mod));
63
119
  }
64
120
 
121
+ // Legacy bundled format: modules is an object map
122
+ const modules = [];
123
+ for (const [key, mod] of Object.entries(config.modules || {})) {
124
+ modules.push(this._normalizeModule(mod, key));
125
+ }
65
126
  return modules;
66
127
  }
67
128
 
@@ -81,27 +142,8 @@ class ExternalModuleManager {
81
142
  * @returns {Object|null} Module info or null if not found
82
143
  */
83
144
  async getModuleByKey(key) {
84
- const config = await this.loadExternalModulesConfig();
85
- const moduleConfig = config.modules?.[key];
86
-
87
- if (!moduleConfig) {
88
- return null;
89
- }
90
-
91
- return {
92
- key,
93
- url: moduleConfig.url,
94
- moduleDefinition: moduleConfig['module-definition'],
95
- code: moduleConfig.code,
96
- name: moduleConfig.name,
97
- header: moduleConfig.header,
98
- subheader: moduleConfig.subheader,
99
- description: moduleConfig.description || '',
100
- defaultSelected: moduleConfig.defaultSelected === true,
101
- type: moduleConfig.type || 'community', // bmad-org or community
102
- npmPackage: moduleConfig.npmPackage || null, // Include npm package name
103
- isExternal: true,
104
- };
145
+ const modules = await this.listAvailable();
146
+ return modules.find((m) => m.key === key) || null;
105
147
  }
106
148
 
107
149
  /**
@@ -154,7 +196,7 @@ class ExternalModuleManager {
154
196
  const moduleInfo = await this.getModuleByCode(moduleCode);
155
197
 
156
198
  if (!moduleInfo) {
157
- throw new Error(`External module '${moduleCode}' not found in external-official-modules.yaml`);
199
+ throw new Error(`External module '${moduleCode}' not found in the BMad registry`);
158
200
  }
159
201
 
160
202
  const cacheDir = this.getExternalCacheDir();
@@ -304,7 +346,7 @@ class ExternalModuleManager {
304
346
  async findExternalModuleSource(moduleCode, options = {}) {
305
347
  const moduleInfo = await this.getModuleByCode(moduleCode);
306
348
 
307
- if (!moduleInfo) {
349
+ if (!moduleInfo || moduleInfo.builtIn) {
308
350
  return null;
309
351
  }
310
352
 
@@ -349,6 +391,7 @@ class ExternalModuleManager {
349
391
  // Nothing found: return configured path (preserves old behavior for error messaging)
350
392
  return path.dirname(configuredPath);
351
393
  }
394
+ cachedModules = null;
352
395
  }
353
396
 
354
397
  module.exports = { ExternalModuleManager };
@@ -98,11 +98,10 @@ class OfficialModules {
98
98
  /**
99
99
  * List all available built-in modules (core and bmm).
100
100
  * All other modules come from external-official-modules.yaml
101
- * @returns {Object} Object with modules array and customModules array
101
+ * @returns {Object} Object with modules array
102
102
  */
103
103
  async listAvailable() {
104
104
  const modules = [];
105
- const customModules = [];
106
105
 
107
106
  // Add built-in core module (directly under src/core-skills)
108
107
  const corePath = getSourcePath('core-skills');
@@ -122,7 +121,7 @@ class OfficialModules {
122
121
  }
123
122
  }
124
123
 
125
- return { modules, customModules };
124
+ return { modules };
126
125
  }
127
126
 
128
127
  /**
@@ -133,25 +132,12 @@ class OfficialModules {
133
132
  * @returns {Object|null} Module info or null if not a valid module
134
133
  */
135
134
  async getModuleInfo(modulePath, defaultName, sourceDescription) {
136
- // Check for module structure (module.yaml OR custom.yaml)
137
135
  const moduleConfigPath = path.join(modulePath, 'module.yaml');
138
- const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
139
- let configPath = null;
140
136
 
141
- if (await fs.pathExists(moduleConfigPath)) {
142
- configPath = moduleConfigPath;
143
- } else if (await fs.pathExists(rootCustomConfigPath)) {
144
- configPath = rootCustomConfigPath;
145
- }
146
-
147
- // Skip if this doesn't look like a module
148
- if (!configPath) {
137
+ if (!(await fs.pathExists(moduleConfigPath))) {
149
138
  return null;
150
139
  }
151
140
 
152
- // Mark as custom if it's using custom.yaml OR if it's outside src/bmm or src/core
153
- const isCustomSource =
154
- sourceDescription !== 'src/bmm-skills' && sourceDescription !== 'src/core-skills' && sourceDescription !== 'src/modules';
155
141
  const moduleInfo = {
156
142
  id: defaultName,
157
143
  path: modulePath,
@@ -162,12 +148,11 @@ class OfficialModules {
162
148
  description: 'BMAD Module',
163
149
  version: '5.0.0',
164
150
  source: sourceDescription,
165
- isCustom: configPath === rootCustomConfigPath || isCustomSource,
166
151
  };
167
152
 
168
153
  // Read module config for metadata
169
154
  try {
170
- const configContent = await fs.readFile(configPath, 'utf8');
155
+ const configContent = await fs.readFile(moduleConfigPath, 'utf8');
171
156
  const config = yaml.parse(configContent);
172
157
 
173
158
  // Use the code property as the id if available
@@ -824,20 +809,15 @@ class OfficialModules {
824
809
  const results = [];
825
810
 
826
811
  for (const moduleName of modules) {
827
- // Resolve module.yaml path - custom paths first, then standard location, then OfficialModules search
812
+ // Resolve module.yaml path - standard location first, then OfficialModules search
828
813
  let moduleConfigPath = null;
829
- const customPath = this.customModulePaths?.get(moduleName);
830
- if (customPath) {
831
- moduleConfigPath = path.join(customPath, 'module.yaml');
814
+ const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
815
+ if (await fs.pathExists(standardPath)) {
816
+ moduleConfigPath = standardPath;
832
817
  } else {
833
- const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
834
- if (await fs.pathExists(standardPath)) {
835
- moduleConfigPath = standardPath;
836
- } else {
837
- const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
838
- if (moduleSourcePath) {
839
- moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
840
- }
818
+ const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
819
+ if (moduleSourcePath) {
820
+ moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
841
821
  }
842
822
  }
843
823
 
@@ -882,12 +862,9 @@ class OfficialModules {
882
862
  * @param {Array} modules - List of modules to configure (including 'core')
883
863
  * @param {string} projectDir - Target project directory
884
864
  * @param {Object} options - Additional options
885
- * @param {Map} options.customModulePaths - Map of module ID to source path for custom modules
886
865
  * @param {boolean} options.skipPrompts - Skip prompts and use defaults (for --yes flag)
887
866
  */
888
867
  async collectAllConfigurations(modules, projectDir, options = {}) {
889
- // Store custom module paths for use in collectModuleConfig
890
- this.customModulePaths = options.customModulePaths || new Map();
891
868
  this.skipPrompts = options.skipPrompts || false;
892
869
  this.modulesToCustomize = undefined;
893
870
  await this.loadExistingConfig(projectDir);
@@ -1042,25 +1019,7 @@ class OfficialModules {
1042
1019
  }
1043
1020
  }
1044
1021
 
1045
- let configPath = null;
1046
- let isCustomModule = false;
1047
-
1048
- if (await fs.pathExists(moduleConfigPath)) {
1049
- configPath = moduleConfigPath;
1050
- } else {
1051
- // Check if this is a custom module with custom.yaml
1052
- const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
1053
-
1054
- if (moduleSourcePath) {
1055
- const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
1056
-
1057
- if (await fs.pathExists(rootCustomConfigPath)) {
1058
- isCustomModule = true;
1059
- // For custom modules, we don't have an install-config schema, so just use existing values
1060
- // The custom.yaml values will be loaded and merged during installation
1061
- }
1062
- }
1063
-
1022
+ if (!(await fs.pathExists(moduleConfigPath))) {
1064
1023
  // No config schema for this module - use existing values
1065
1024
  if (this._existingConfig && this._existingConfig[moduleName]) {
1066
1025
  if (!this.collectedConfig[moduleName]) {
@@ -1071,7 +1030,7 @@ class OfficialModules {
1071
1030
  return false;
1072
1031
  }
1073
1032
 
1074
- const configContent = await fs.readFile(configPath, 'utf8');
1033
+ const configContent = await fs.readFile(moduleConfigPath, 'utf8');
1075
1034
  const moduleConfig = yaml.parse(configContent);
1076
1035
 
1077
1036
  if (!moduleConfig) {
@@ -1332,16 +1291,7 @@ class OfficialModules {
1332
1291
  this.allAnswers = {};
1333
1292
  }
1334
1293
  // Load module's config
1335
- // First, check if we have a custom module path for this module
1336
- let moduleConfigPath = null;
1337
-
1338
- if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
1339
- const customPath = this.customModulePaths.get(moduleName);
1340
- moduleConfigPath = path.join(customPath, 'module.yaml');
1341
- } else {
1342
- // Try the standard src/modules location
1343
- moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
1344
- }
1294
+ let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
1345
1295
 
1346
1296
  // If not found in src/modules or custom paths, search the project
1347
1297
  if (!(await fs.pathExists(moduleConfigPath))) {
@@ -1,5 +1,6 @@
1
- # This file allows these modules under bmad-code-org to also be installed with the bmad method installer, while
2
- # allowing us to keep the source of these projects in separate repos.
1
+ # Fallback module registry used only when the BMad Marketplace repo
2
+ # (bmad-code-org/bmad-plugins-marketplace) is unreachable.
3
+ # The remote registry/official.yaml is the source of truth.
3
4
 
4
5
  modules:
5
6
  bmad-builder:
@@ -41,13 +42,3 @@ modules:
41
42
  defaultSelected: false
42
43
  type: bmad-org
43
44
  npmPackage: bmad-method-test-architecture-enterprise
44
-
45
- whiteport-design-studio:
46
- url: https://github.com/bmad-code-org/bmad-method-wds-expansion
47
- module-definition: src/module.yaml
48
- code: wds
49
- name: "Whiteport Design Studio (For UX Professionals)"
50
- description: "Whiteport Design Studio (For UX Professionals)"
51
- defaultSelected: false
52
- type: community
53
- npmPackage: bmad-method-wds-expansion