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
|
@@ -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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
10
|
-
*
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
22
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
85
|
-
|
|
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
|
|
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
|
-
#
|
|
2
|
-
#
|
|
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
|
package/tools/installer/ui.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
593
|
-
const isInstalled = installedModuleIds.has(
|
|
594
|
-
const version = await getMarketplaceVersion(
|
|
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
|
|
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
|
-
//
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
|
674
|
-
const
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
|