bmad-method 6.2.3-next.26 → 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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.2.3-next.26",
4
+ "version": "6.2.3-next.27",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -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 };
@@ -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
@@ -569,77 +569,36 @@ class UI {
569
569
  * @returns {Array} Selected module codes (excluding core)
570
570
  */
571
571
  async selectAllModules(installedModuleIds = new Set()) {
572
- const { OfficialModules } = require('./modules/official-modules');
573
- const officialModulesSource = new OfficialModules();
574
- const { modules: localModules } = await officialModulesSource.listAvailable();
575
-
576
- // Get external modules
572
+ // Registry is the single source of truth for the module list
577
573
  const externalManager = new ExternalModuleManager();
578
- const externalModules = await externalManager.listAvailable();
574
+ const registryModules = await externalManager.listAvailable();
579
575
 
580
576
  // Build flat options list with group hints for autocompleteMultiselect
581
577
  const allOptions = [];
582
578
  const initialValues = [];
583
579
  const lockedValues = ['core'];
584
580
 
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
581
  // 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);
582
+ const buildModuleEntry = async (mod) => {
583
+ const isInstalled = installedModuleIds.has(mod.code);
584
+ const version = await getMarketplaceVersion(mod.code);
595
585
  const label = version ? `${mod.name} (v${version})` : mod.name;
596
586
  return {
597
587
  label,
598
- value,
599
- hint: mod.description || group,
600
- // Pre-select only if already installed (not on fresh install)
588
+ value: mod.code,
589
+ hint: mod.description,
601
590
  selected: isInstalled,
602
591
  };
603
592
  };
604
593
 
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
- }
614
- }
615
- }
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
- }
594
+ // Registry order is display order; core is always locked
595
+ for (const mod of registryModules) {
596
+ const entry = await buildModuleEntry(mod);
597
+ allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
598
+ if (entry.selected) {
599
+ initialValues.push(mod.code);
640
600
  }
641
601
  }
642
- allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
643
602
 
644
603
  const selected = await prompts.autocompleteMultiselect({
645
604
  message: 'Select modules to install:',
@@ -670,16 +629,14 @@ class UI {
670
629
  * @returns {Array} Default module codes
671
630
  */
672
631
  async getDefaultModules(installedModuleIds = new Set()) {
673
- const { OfficialModules } = require('./modules/official-modules');
674
- const officialModules = new OfficialModules();
675
- const { modules: localModules } = await officialModules.listAvailable();
632
+ const externalManager = new ExternalModuleManager();
633
+ const registryModules = await externalManager.listAvailable();
676
634
 
677
635
  const defaultModules = [];
678
636
 
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);
637
+ for (const mod of registryModules) {
638
+ if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
639
+ defaultModules.push(mod.code);
683
640
  }
684
641
  }
685
642