bmad-method 6.2.3-next.26 → 6.2.3-next.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,308 @@
1
+ const fs = require('fs-extra');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { execSync } = require('node:child_process');
5
+ const prompts = require('../prompts');
6
+ const { RegistryClient } = require('./registry-client');
7
+
8
+ /**
9
+ * Manages custom modules installed from user-provided GitHub URLs.
10
+ * Validates URLs, fetches .claude-plugin/marketplace.json, clones repos.
11
+ */
12
+ class CustomModuleManager {
13
+ constructor() {
14
+ this._client = new RegistryClient();
15
+ }
16
+
17
+ // ─── URL Validation ───────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Parse and validate a GitHub repository URL.
21
+ * Supports HTTPS and SSH formats.
22
+ * @param {string} url - GitHub URL to validate
23
+ * @returns {Object} { owner, repo, isValid, error }
24
+ */
25
+ validateGitHubUrl(url) {
26
+ if (!url || typeof url !== 'string') {
27
+ return { owner: null, repo: null, isValid: false, error: 'URL is required' };
28
+ }
29
+
30
+ const trimmed = url.trim();
31
+
32
+ // HTTPS format: https://github.com/owner/repo[.git]
33
+ const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
34
+ if (httpsMatch) {
35
+ return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null };
36
+ }
37
+
38
+ // SSH format: git@github.com:owner/repo.git
39
+ const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
40
+ if (sshMatch) {
41
+ return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null };
42
+ }
43
+
44
+ return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' };
45
+ }
46
+
47
+ // ─── Discovery ────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Fetch .claude-plugin/marketplace.json from a GitHub repository.
51
+ * @param {string} repoUrl - GitHub repository URL
52
+ * @returns {Object} Parsed marketplace.json content
53
+ */
54
+ async fetchMarketplaceJson(repoUrl) {
55
+ const { owner, repo, isValid, error } = this.validateGitHubUrl(repoUrl);
56
+ if (!isValid) throw new Error(error);
57
+
58
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/.claude-plugin/marketplace.json`;
59
+
60
+ try {
61
+ return await this._client.fetchJson(rawUrl);
62
+ } catch (error_) {
63
+ if (error_.message.includes('404')) {
64
+ throw new Error(`No .claude-plugin/marketplace.json found in ${owner}/${repo}. This repository may not be a BMad module.`);
65
+ }
66
+ if (error_.message.includes('403')) {
67
+ throw new Error(`Repository ${owner}/${repo} is not accessible. Make sure it is public.`);
68
+ }
69
+ throw new Error(`Failed to fetch marketplace.json from ${owner}/${repo}: ${error_.message}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Discover modules from a GitHub repository's marketplace.json.
75
+ * @param {string} repoUrl - GitHub repository URL
76
+ * @returns {Array<Object>} Normalized plugin list
77
+ */
78
+ async discoverModules(repoUrl) {
79
+ const data = await this.fetchMarketplaceJson(repoUrl);
80
+ const plugins = data?.plugins;
81
+
82
+ if (!Array.isArray(plugins) || plugins.length === 0) {
83
+ throw new Error('marketplace.json contains no plugins');
84
+ }
85
+
86
+ return plugins.map((plugin) => this._normalizeCustomModule(plugin, repoUrl, data));
87
+ }
88
+
89
+ // ─── Clone ────────────────────────────────────────────────────────────────
90
+
91
+ /**
92
+ * Get the cache directory for custom modules.
93
+ * @returns {string} Path to the custom modules cache directory
94
+ */
95
+ getCacheDir() {
96
+ return path.join(os.homedir(), '.bmad', 'cache', 'custom-modules');
97
+ }
98
+
99
+ /**
100
+ * Clone a custom module repository to cache.
101
+ * @param {string} repoUrl - GitHub repository URL
102
+ * @param {Object} [options] - Clone options
103
+ * @param {boolean} [options.silent] - Suppress spinner output
104
+ * @returns {string} Path to the cloned repository
105
+ */
106
+ async cloneRepo(repoUrl, options = {}) {
107
+ const { owner, repo, isValid, error } = this.validateGitHubUrl(repoUrl);
108
+ if (!isValid) throw new Error(error);
109
+
110
+ const cacheDir = this.getCacheDir();
111
+ const repoCacheDir = path.join(cacheDir, owner, repo);
112
+ const silent = options.silent || false;
113
+
114
+ await fs.ensureDir(path.join(cacheDir, owner));
115
+
116
+ const createSpinner = async () => {
117
+ if (silent) {
118
+ return { start() {}, stop() {}, error() {} };
119
+ }
120
+ return await prompts.spinner();
121
+ };
122
+
123
+ if (await fs.pathExists(repoCacheDir)) {
124
+ // Update existing clone
125
+ const fetchSpinner = await createSpinner();
126
+ fetchSpinner.start(`Updating ${owner}/${repo}...`);
127
+ try {
128
+ execSync('git fetch origin --depth 1', {
129
+ cwd: repoCacheDir,
130
+ stdio: ['ignore', 'pipe', 'pipe'],
131
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
132
+ });
133
+ execSync('git reset --hard origin/HEAD', {
134
+ cwd: repoCacheDir,
135
+ stdio: ['ignore', 'pipe', 'pipe'],
136
+ });
137
+ fetchSpinner.stop(`Updated ${owner}/${repo}`);
138
+ } catch {
139
+ fetchSpinner.error(`Update failed, re-downloading ${owner}/${repo}`);
140
+ await fs.remove(repoCacheDir);
141
+ }
142
+ }
143
+
144
+ if (!(await fs.pathExists(repoCacheDir))) {
145
+ const fetchSpinner = await createSpinner();
146
+ fetchSpinner.start(`Cloning ${owner}/${repo}...`);
147
+ try {
148
+ execSync(`git clone --depth 1 "${repoUrl}" "${repoCacheDir}"`, {
149
+ stdio: ['ignore', 'pipe', 'pipe'],
150
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
151
+ });
152
+ fetchSpinner.stop(`Cloned ${owner}/${repo}`);
153
+ } catch (error_) {
154
+ fetchSpinner.error(`Failed to clone ${owner}/${repo}`);
155
+ throw new Error(`Failed to clone ${repoUrl}: ${error_.message}`);
156
+ }
157
+ }
158
+
159
+ // Install dependencies if package.json exists
160
+ const packageJsonPath = path.join(repoCacheDir, 'package.json');
161
+ if (await fs.pathExists(packageJsonPath)) {
162
+ const installSpinner = await createSpinner();
163
+ installSpinner.start(`Installing dependencies for ${owner}/${repo}...`);
164
+ try {
165
+ execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
166
+ cwd: repoCacheDir,
167
+ stdio: ['ignore', 'pipe', 'pipe'],
168
+ timeout: 120_000,
169
+ });
170
+ installSpinner.stop(`Installed dependencies for ${owner}/${repo}`);
171
+ } catch (error_) {
172
+ installSpinner.error(`Failed to install dependencies for ${owner}/${repo}`);
173
+ if (!silent) await prompts.log.warn(` ${error_.message}`);
174
+ }
175
+ }
176
+
177
+ return repoCacheDir;
178
+ }
179
+
180
+ // ─── Source Finding ───────────────────────────────────────────────────────
181
+
182
+ /**
183
+ * Find the module source path within a cloned custom repo.
184
+ * @param {string} repoUrl - GitHub repository URL (for cache location)
185
+ * @param {string} [pluginSource] - Plugin source path from marketplace.json
186
+ * @returns {string|null} Path to directory containing module.yaml
187
+ */
188
+ async findModuleSource(repoUrl, pluginSource) {
189
+ const { owner, repo } = this.validateGitHubUrl(repoUrl);
190
+ const repoCacheDir = path.join(this.getCacheDir(), owner, repo);
191
+
192
+ if (!(await fs.pathExists(repoCacheDir))) return null;
193
+
194
+ // Try plugin source path first (e.g., "./src/pro-skills")
195
+ if (pluginSource) {
196
+ const sourcePath = path.join(repoCacheDir, pluginSource);
197
+ const moduleYaml = path.join(sourcePath, 'module.yaml');
198
+ if (await fs.pathExists(moduleYaml)) {
199
+ return sourcePath;
200
+ }
201
+ }
202
+
203
+ // Fallback: search skills/ and src/ directories
204
+ for (const dir of ['skills', 'src']) {
205
+ const rootCandidate = path.join(repoCacheDir, dir, 'module.yaml');
206
+ if (await fs.pathExists(rootCandidate)) {
207
+ return path.dirname(rootCandidate);
208
+ }
209
+ const dirPath = path.join(repoCacheDir, dir);
210
+ if (await fs.pathExists(dirPath)) {
211
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
212
+ for (const entry of entries) {
213
+ if (entry.isDirectory()) {
214
+ const subCandidate = path.join(dirPath, entry.name, 'module.yaml');
215
+ if (await fs.pathExists(subCandidate)) {
216
+ return path.dirname(subCandidate);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ // Check repo root
224
+ const rootCandidate = path.join(repoCacheDir, 'module.yaml');
225
+ if (await fs.pathExists(rootCandidate)) {
226
+ return repoCacheDir;
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ * Find module source by module code, searching the custom cache.
234
+ * @param {string} moduleCode - Module code to search for
235
+ * @param {Object} [options] - Options
236
+ * @returns {string|null} Path to the module source or null
237
+ */
238
+ async findModuleSourceByCode(moduleCode, options = {}) {
239
+ const cacheDir = this.getCacheDir();
240
+ if (!(await fs.pathExists(cacheDir))) return null;
241
+
242
+ // Search through all custom repo caches
243
+ try {
244
+ const owners = await fs.readdir(cacheDir, { withFileTypes: true });
245
+ for (const ownerEntry of owners) {
246
+ if (!ownerEntry.isDirectory()) continue;
247
+ const ownerPath = path.join(cacheDir, ownerEntry.name);
248
+ const repos = await fs.readdir(ownerPath, { withFileTypes: true });
249
+ for (const repoEntry of repos) {
250
+ if (!repoEntry.isDirectory()) continue;
251
+ const repoPath = path.join(ownerPath, repoEntry.name);
252
+
253
+ // Check marketplace.json for matching module code
254
+ const marketplacePath = path.join(repoPath, '.claude-plugin', 'marketplace.json');
255
+ if (await fs.pathExists(marketplacePath)) {
256
+ try {
257
+ const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
258
+ for (const plugin of data.plugins || []) {
259
+ if (plugin.name === moduleCode) {
260
+ // Found the module - find its source
261
+ const sourcePath = plugin.source ? path.join(repoPath, plugin.source) : repoPath;
262
+ const moduleYaml = path.join(sourcePath, 'module.yaml');
263
+ if (await fs.pathExists(moduleYaml)) {
264
+ return sourcePath;
265
+ }
266
+ }
267
+ }
268
+ } catch {
269
+ // Skip malformed marketplace.json
270
+ }
271
+ }
272
+ }
273
+ }
274
+ } catch {
275
+ // Cache doesn't exist or is inaccessible
276
+ }
277
+
278
+ return null;
279
+ }
280
+
281
+ // ─── Normalization ────────────────────────────────────────────────────────
282
+
283
+ /**
284
+ * Normalize a plugin from marketplace.json to a consistent shape.
285
+ * @param {Object} plugin - Plugin object from marketplace.json
286
+ * @param {string} repoUrl - Source repository URL
287
+ * @param {Object} data - Full marketplace.json data
288
+ * @returns {Object} Normalized module info
289
+ */
290
+ _normalizeCustomModule(plugin, repoUrl, data) {
291
+ return {
292
+ code: plugin.name,
293
+ name: plugin.name,
294
+ displayName: plugin.name,
295
+ description: plugin.description || '',
296
+ version: plugin.version || null,
297
+ author: plugin.author || data.owner || '',
298
+ url: repoUrl,
299
+ source: plugin.source || null,
300
+ type: 'custom',
301
+ trustTier: 'unverified',
302
+ builtIn: false,
303
+ isExternal: true,
304
+ };
305
+ }
306
+ }
307
+
308
+ module.exports = { CustomModuleManager };
@@ -4,64 +4,98 @@ const path = require('node:path');
4
4
  const { execSync } = require('node:child_process');
5
5
  const yaml = require('yaml');
6
6
  const prompts = require('../prompts');
7
+ const { RegistryClient } = require('./registry-client');
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');
7
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
20
  constructor() {
16
- this.externalModulesConfigPath = path.join(__dirname, '../external-official-modules.yaml');
17
- this.cachedModules = null;
21
+ this._client = new RegistryClient();
18
22
  }
19
23
 
20
24
  /**
21
- * Load and parse the external-official-modules.yaml file
22
- * @returns {Object} Parsed YAML content with modules object
25
+ * Load the official modules registry from GitHub, falling back to the
26
+ * bundled YAML file if the fetch fails.
27
+ * @returns {Object} Parsed YAML content with modules array
23
28
  */
24
29
  async loadExternalModulesConfig() {
25
30
  if (this.cachedModules) {
26
31
  return this.cachedModules;
27
32
  }
28
33
 
34
+ // Try remote registry first
35
+ try {
36
+ const content = await this._client.fetch(REGISTRY_RAW_URL);
37
+ const config = yaml.parse(content);
38
+ if (config?.modules?.length) {
39
+ this.cachedModules = config;
40
+ return config;
41
+ }
42
+ } catch {
43
+ // Fall through to local fallback
44
+ }
45
+
46
+ // Fallback to bundled file
29
47
  try {
30
- const content = await fs.readFile(this.externalModulesConfigPath, 'utf8');
48
+ const content = await fs.readFile(FALLBACK_CONFIG_PATH, 'utf8');
31
49
  const config = yaml.parse(content);
32
50
  this.cachedModules = config;
51
+ await prompts.log.warn('Could not reach BMad registry; using bundled module list.');
33
52
  return config;
34
53
  } catch (error) {
35
- await prompts.log.warn(`Failed to load external modules config: ${error.message}`);
36
- return { modules: {} };
54
+ await prompts.log.warn(`Failed to load modules config: ${error.message}`);
55
+ return { modules: [] };
37
56
  }
38
57
  }
39
58
 
40
59
  /**
41
- * Get list of available external modules
60
+ * Normalize a module entry from either the remote registry format
61
+ * (snake_case, array) or the legacy bundled format (kebab-case, object map).
62
+ * @param {Object} mod - Raw module config from YAML
63
+ * @param {string} [key] - Key name (only for legacy map format)
64
+ * @returns {Object} Normalized module info
65
+ */
66
+ _normalizeModule(mod, key) {
67
+ return {
68
+ key: key || mod.name,
69
+ url: mod.repository || mod.url,
70
+ moduleDefinition: mod.module_definition || mod['module-definition'],
71
+ code: mod.code,
72
+ name: mod.display_name || mod.name,
73
+ description: mod.description || '',
74
+ defaultSelected: mod.default_selected === true || mod.defaultSelected === true,
75
+ type: mod.type || 'bmad-org',
76
+ npmPackage: mod.npm_package || mod.npmPackage || null,
77
+ builtIn: mod.built_in === true,
78
+ isExternal: mod.built_in !== true,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Get list of available modules from the registry
42
84
  * @returns {Array<Object>} Array of module info objects
43
85
  */
44
86
  async listAvailable() {
45
87
  const config = await this.loadExternalModulesConfig();
46
- const modules = [];
47
88
 
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
- });
89
+ // Remote format: modules is an array
90
+ if (Array.isArray(config.modules)) {
91
+ return config.modules.map((mod) => this._normalizeModule(mod));
63
92
  }
64
93
 
94
+ // Legacy bundled format: modules is an object map
95
+ const modules = [];
96
+ for (const [key, mod] of Object.entries(config.modules || {})) {
97
+ modules.push(this._normalizeModule(mod, key));
98
+ }
65
99
  return modules;
66
100
  }
67
101
 
@@ -81,27 +115,8 @@ class ExternalModuleManager {
81
115
  * @returns {Object|null} Module info or null if not found
82
116
  */
83
117
  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
- };
118
+ const modules = await this.listAvailable();
119
+ return modules.find((m) => m.key === key) || null;
105
120
  }
106
121
 
107
122
  /**
@@ -154,7 +169,7 @@ class ExternalModuleManager {
154
169
  const moduleInfo = await this.getModuleByCode(moduleCode);
155
170
 
156
171
  if (!moduleInfo) {
157
- throw new Error(`External module '${moduleCode}' not found in external-official-modules.yaml`);
172
+ throw new Error(`External module '${moduleCode}' not found in the BMad registry`);
158
173
  }
159
174
 
160
175
  const cacheDir = this.getExternalCacheDir();
@@ -304,7 +319,7 @@ class ExternalModuleManager {
304
319
  async findExternalModuleSource(moduleCode, options = {}) {
305
320
  const moduleInfo = await this.getModuleByCode(moduleCode);
306
321
 
307
- if (!moduleInfo) {
322
+ if (!moduleInfo || moduleInfo.builtIn) {
308
323
  return null;
309
324
  }
310
325
 
@@ -349,6 +364,7 @@ class ExternalModuleManager {
349
364
  // Nothing found: return configured path (preserves old behavior for error messaging)
350
365
  return path.dirname(configuredPath);
351
366
  }
367
+ cachedModules = null;
352
368
  }
353
369
 
354
370
  module.exports = { ExternalModuleManager };
@@ -202,6 +202,22 @@ class OfficialModules {
202
202
  return externalSource;
203
203
  }
204
204
 
205
+ // Check community modules
206
+ const { CommunityModuleManager } = require('./community-manager');
207
+ const communityMgr = new CommunityModuleManager();
208
+ const communitySource = await communityMgr.findModuleSource(moduleCode, options);
209
+ if (communitySource) {
210
+ return communitySource;
211
+ }
212
+
213
+ // Check custom modules (from user-provided URLs, already cloned to cache)
214
+ const { CustomModuleManager } = require('./custom-module-manager');
215
+ const customMgr = new CustomModuleManager();
216
+ const customSource = await customMgr.findModuleSourceByCode(moduleCode, options);
217
+ if (customSource) {
218
+ return customSource;
219
+ }
220
+
205
221
  return null;
206
222
  }
207
223
 
@@ -1131,7 +1147,13 @@ class OfficialModules {
1131
1147
  // Collect all answers (static + prompted)
1132
1148
  let allAnswers = { ...staticAnswers };
1133
1149
 
1134
- if (questions.length > 0) {
1150
+ if (questions.length > 0 && silentMode) {
1151
+ // In silent mode (quick update), use defaults for new fields instead of prompting
1152
+ for (const q of questions) {
1153
+ allAnswers[q.name] = typeof q.default === 'function' ? q.default({}) : q.default;
1154
+ }
1155
+ await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured with defaults`);
1156
+ } else if (questions.length > 0) {
1135
1157
  // Only show header if we actually have questions
1136
1158
  await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
1137
1159
  await prompts.log.message('');
@@ -0,0 +1,66 @@
1
+ const https = require('node:https');
2
+ const yaml = require('yaml');
3
+
4
+ /**
5
+ * Shared HTTP client for fetching registry data from GitHub.
6
+ * Used by ExternalModuleManager, CommunityModuleManager, and CustomModuleManager.
7
+ */
8
+ class RegistryClient {
9
+ constructor(options = {}) {
10
+ this.timeout = options.timeout || 10_000;
11
+ }
12
+
13
+ /**
14
+ * Fetch a URL and return the response body as a string.
15
+ * Follows one redirect (GitHub sometimes 301s).
16
+ * @param {string} url - URL to fetch
17
+ * @param {number} [timeout] - Timeout in ms (overrides default)
18
+ * @returns {Promise<string>} Response body
19
+ */
20
+ fetch(url, timeout) {
21
+ const timeoutMs = timeout || this.timeout;
22
+ return new Promise((resolve, reject) => {
23
+ const req = https
24
+ .get(url, { timeout: timeoutMs }, (res) => {
25
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
26
+ return this.fetch(res.headers.location, timeoutMs).then(resolve, reject);
27
+ }
28
+ if (res.statusCode !== 200) {
29
+ return reject(new Error(`HTTP ${res.statusCode}`));
30
+ }
31
+ let data = '';
32
+ res.on('data', (chunk) => (data += chunk));
33
+ res.on('end', () => resolve(data));
34
+ })
35
+ .on('error', reject)
36
+ .on('timeout', () => {
37
+ req.destroy();
38
+ reject(new Error('Request timed out'));
39
+ });
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Fetch a URL and parse the response as YAML.
45
+ * @param {string} url - URL to fetch
46
+ * @param {number} [timeout] - Timeout in ms
47
+ * @returns {Promise<Object>} Parsed YAML content
48
+ */
49
+ async fetchYaml(url, timeout) {
50
+ const content = await this.fetch(url, timeout);
51
+ return yaml.parse(content);
52
+ }
53
+
54
+ /**
55
+ * Fetch a URL and parse the response as JSON.
56
+ * @param {string} url - URL to fetch
57
+ * @param {number} [timeout] - Timeout in ms
58
+ * @returns {Promise<Object>} Parsed JSON content
59
+ */
60
+ async fetchJson(url, timeout) {
61
+ const content = await this.fetch(url, timeout);
62
+ return JSON.parse(content);
63
+ }
64
+ }
65
+
66
+ module.exports = { RegistryClient };
@@ -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