bmad-method 6.3.1-next.8 → 6.4.0

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 (141) hide show
  1. package/package.json +3 -2
  2. package/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md +51 -36
  3. package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.toml +90 -0
  4. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md +50 -33
  5. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/customize.toml +81 -0
  6. package/src/bmm-skills/1-analysis/bmad-document-project/SKILL.md +57 -1
  7. package/src/bmm-skills/1-analysis/bmad-document-project/customize.toml +41 -0
  8. package/src/bmm-skills/1-analysis/bmad-document-project/workflows/deep-dive-instructions.md +1 -0
  9. package/src/bmm-skills/1-analysis/bmad-document-project/workflows/full-scan-instructions.md +1 -0
  10. package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +48 -9
  11. package/src/bmm-skills/1-analysis/bmad-prfaq/customize.toml +41 -0
  12. package/src/bmm-skills/1-analysis/bmad-prfaq/references/verdict.md +4 -0
  13. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +44 -9
  14. package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +47 -0
  15. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/contextual-discovery.md +8 -7
  16. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/draft-and-review.md +6 -5
  17. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/finalize.md +4 -1
  18. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/guided-elicitation.md +3 -2
  19. package/src/bmm-skills/1-analysis/research/bmad-domain-research/SKILL.md +91 -1
  20. package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +41 -0
  21. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +6 -0
  22. package/src/bmm-skills/1-analysis/research/bmad-market-research/SKILL.md +91 -1
  23. package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +41 -0
  24. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +6 -0
  25. package/src/bmm-skills/1-analysis/research/bmad-technical-research/SKILL.md +91 -1
  26. package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +41 -0
  27. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +6 -0
  28. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md +50 -35
  29. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/customize.toml +85 -0
  30. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md +50 -31
  31. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/customize.toml +60 -0
  32. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/SKILL.md +99 -1
  33. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/customize.toml +41 -0
  34. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md +6 -0
  35. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/SKILL.md +70 -1
  36. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/customize.toml +41 -0
  37. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md +6 -0
  38. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/SKILL.md +97 -1
  39. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/customize.toml +42 -0
  40. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +2 -0
  41. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/SKILL.md +99 -1
  42. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/customize.toml +42 -0
  43. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-13-report-complete.md +1 -0
  44. package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +50 -30
  45. package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +65 -0
  46. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/SKILL.md +86 -1
  47. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/customize.toml +41 -0
  48. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md +6 -0
  49. package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +69 -1
  50. package/src/bmm-skills/3-solutioning/bmad-create-architecture/customize.toml +41 -0
  51. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +6 -0
  52. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/SKILL.md +88 -1
  53. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/customize.toml +41 -0
  54. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md +6 -0
  55. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/SKILL.md +76 -1
  56. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/customize.toml +41 -0
  57. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md +6 -0
  58. package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +48 -43
  59. package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +90 -0
  60. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/SKILL.md +46 -7
  61. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/customize.toml +41 -0
  62. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md +6 -0
  63. package/src/bmm-skills/4-implementation/bmad-code-review/SKILL.md +85 -1
  64. package/src/bmm-skills/4-implementation/bmad-code-review/customize.toml +41 -0
  65. package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-04-present.md +6 -0
  66. package/src/bmm-skills/4-implementation/bmad-correct-course/SKILL.md +296 -1
  67. package/src/bmm-skills/4-implementation/bmad-correct-course/customize.toml +41 -0
  68. package/src/bmm-skills/4-implementation/bmad-create-story/SKILL.md +424 -1
  69. package/src/bmm-skills/4-implementation/bmad-create-story/customize.toml +41 -0
  70. package/src/bmm-skills/4-implementation/bmad-dev-story/SKILL.md +480 -1
  71. package/src/bmm-skills/4-implementation/bmad-dev-story/customize.toml +41 -0
  72. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md +171 -1
  73. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/customize.toml +41 -0
  74. package/src/bmm-skills/4-implementation/bmad-quick-dev/SKILL.md +106 -1
  75. package/src/bmm-skills/4-implementation/bmad-quick-dev/customize.toml +41 -0
  76. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-05-present.md +6 -0
  77. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +6 -0
  78. package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +1507 -1
  79. package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +41 -0
  80. package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +294 -1
  81. package/src/bmm-skills/4-implementation/bmad-sprint-planning/customize.toml +41 -0
  82. package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +292 -1
  83. package/src/bmm-skills/4-implementation/bmad-sprint-status/customize.toml +41 -0
  84. package/src/bmm-skills/module.yaml +49 -0
  85. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +7 -1
  86. package/src/core-skills/bmad-customize/SKILL.md +111 -0
  87. package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +231 -0
  88. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +249 -0
  89. package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
  90. package/src/core-skills/bmad-party-mode/SKILL.md +13 -10
  91. package/src/core-skills/module-help.csv +1 -0
  92. package/src/core-skills/module.yaml +2 -0
  93. package/src/scripts/resolve_config.py +176 -0
  94. package/src/scripts/resolve_customization.py +230 -0
  95. package/tools/installer/commands/install.js +13 -0
  96. package/tools/installer/core/config.js +4 -1
  97. package/tools/installer/core/install-paths.js +11 -5
  98. package/tools/installer/core/installer.js +181 -94
  99. package/tools/installer/core/manifest-generator.js +339 -184
  100. package/tools/installer/core/manifest.js +86 -86
  101. package/tools/installer/fs-native.js +5 -0
  102. package/tools/installer/ide/platform-codes.yaml +6 -0
  103. package/tools/installer/modules/channel-plan.js +203 -0
  104. package/tools/installer/modules/channel-resolver.js +241 -0
  105. package/tools/installer/modules/community-manager.js +130 -23
  106. package/tools/installer/modules/custom-module-manager.js +160 -19
  107. package/tools/installer/modules/external-manager.js +235 -32
  108. package/tools/installer/modules/official-modules.js +58 -12
  109. package/tools/installer/modules/registry-client.js +139 -7
  110. package/tools/installer/modules/registry-fallback.yaml +8 -0
  111. package/tools/installer/modules/version-resolver.js +336 -0
  112. package/tools/installer/project-root.js +54 -0
  113. package/tools/installer/ui.js +561 -50
  114. package/tools/platform-codes.yaml +6 -0
  115. package/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +0 -11
  116. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +0 -11
  117. package/src/bmm-skills/1-analysis/bmad-document-project/workflow.md +0 -25
  118. package/src/bmm-skills/1-analysis/research/bmad-domain-research/workflow.md +0 -51
  119. package/src/bmm-skills/1-analysis/research/bmad-market-research/workflow.md +0 -51
  120. package/src/bmm-skills/1-analysis/research/bmad-technical-research/workflow.md +0 -52
  121. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +0 -11
  122. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +0 -11
  123. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/workflow.md +0 -61
  124. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/workflow.md +0 -35
  125. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/workflow.md +0 -62
  126. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/workflow.md +0 -61
  127. package/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +0 -11
  128. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +0 -47
  129. package/src/bmm-skills/3-solutioning/bmad-create-architecture/workflow.md +0 -32
  130. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +0 -51
  131. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/workflow.md +0 -39
  132. package/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +0 -11
  133. package/src/bmm-skills/4-implementation/bmad-code-review/workflow.md +0 -55
  134. package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +0 -267
  135. package/src/bmm-skills/4-implementation/bmad-create-story/workflow.md +0 -380
  136. package/src/bmm-skills/4-implementation/bmad-dev-story/workflow.md +0 -450
  137. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/workflow.md +0 -136
  138. package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +0 -76
  139. package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +0 -1479
  140. package/src/bmm-skills/4-implementation/bmad-sprint-planning/workflow.md +0 -263
  141. package/src/bmm-skills/4-implementation/bmad-sprint-status/workflow.md +0 -261
@@ -15,6 +15,11 @@ class OfficialModules {
15
15
  // Tracked during interactive config collection so {directory_name}
16
16
  // placeholder defaults can be resolved in buildQuestion().
17
17
  this.currentProjectDir = null;
18
+ // Install-time channel flag state. Set by Config.build once, then used as
19
+ // the default for every findModuleSource/cloneExternalModule call so that
20
+ // pre-install config collection and the install step agree on which ref
21
+ // to clone.
22
+ this.channelOptions = options.channelOptions || null;
18
23
  }
19
24
 
20
25
  /**
@@ -38,7 +43,7 @@ class OfficialModules {
38
43
  * @returns {OfficialModules}
39
44
  */
40
45
  static async build(config, paths) {
41
- const instance = new OfficialModules();
46
+ const instance = new OfficialModules({ channelOptions: config.channelOptions });
42
47
 
43
48
  // Pre-collected by UI or quickUpdate — store and load existing for path-change detection
44
49
  if (config.moduleConfigs) {
@@ -196,6 +201,12 @@ class OfficialModules {
196
201
  * @returns {string|null} Path to the module source or null if not found
197
202
  */
198
203
  async findModuleSource(moduleCode, options = {}) {
204
+ // Inherit channelOptions from the install-scoped instance when the caller
205
+ // didn't pass one explicitly. Keeps pre-install config collection and the
206
+ // actual install step looking at the same git ref.
207
+ if (options.channelOptions === undefined && this.channelOptions) {
208
+ options = { ...options, channelOptions: this.channelOptions };
209
+ }
199
210
  const projectRoot = getProjectRoot();
200
211
 
201
212
  // Check for core module (directly under src/core-skills)
@@ -214,13 +225,13 @@ class OfficialModules {
214
225
  }
215
226
  }
216
227
 
217
- // Check external official modules
228
+ // Check external official modules (pass channelOptions so channel plan applies)
218
229
  const externalSource = await this.externalModuleManager.findExternalModuleSource(moduleCode, options);
219
230
  if (externalSource) {
220
231
  return externalSource;
221
232
  }
222
233
 
223
- // Check community modules
234
+ // Check community modules (pass channelOptions for --next/--pin overrides)
224
235
  const { CommunityModuleManager } = require('./community-manager');
225
236
  const communityMgr = new CommunityModuleManager();
226
237
  const communitySource = await communityMgr.findModuleSource(moduleCode, options);
@@ -258,7 +269,10 @@ class OfficialModules {
258
269
  return this.installFromResolution(resolved, bmadDir, fileTrackingCallback, options);
259
270
  }
260
271
 
261
- const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
272
+ const sourcePath = await this.findModuleSource(moduleName, {
273
+ silent: options.silent,
274
+ channelOptions: options.channelOptions,
275
+ });
262
276
  const targetPath = path.join(bmadDir, moduleName);
263
277
 
264
278
  if (!sourcePath) {
@@ -281,11 +295,24 @@ class OfficialModules {
281
295
  const manifestObj = new Manifest();
282
296
  const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
283
297
 
298
+ // Pick up channel resolution recorded by whichever manager did the clone.
299
+ const externalResolution = this.externalModuleManager.getResolution(moduleName);
300
+ let communityResolution = null;
301
+ if (!externalResolution) {
302
+ const { CommunityModuleManager } = require('./community-manager');
303
+ communityResolution = new CommunityModuleManager().getResolution(moduleName);
304
+ }
305
+ const resolution = externalResolution || communityResolution;
306
+
284
307
  await manifestObj.addModule(bmadDir, moduleName, {
285
- version: versionInfo.version,
308
+ version: resolution?.version || versionInfo.version,
286
309
  source: versionInfo.source,
287
310
  npmPackage: versionInfo.npmPackage,
288
311
  repoUrl: versionInfo.repoUrl,
312
+ channel: resolution?.channel,
313
+ sha: resolution?.sha,
314
+ registryApprovedTag: communityResolution?.registryApprovedTag,
315
+ registryApprovedSha: communityResolution?.registryApprovedSha,
289
316
  });
290
317
 
291
318
  return { success: true, module: moduleName, path: targetPath, versionInfo };
@@ -333,18 +360,37 @@ class OfficialModules {
333
360
  await this.createModuleDirectories(resolved.code, bmadDir, options);
334
361
  }
335
362
 
336
- // Update manifest
363
+ // Update manifest. For custom modules, derive channel from the git ref:
364
+ // cloneRef present → pinned at that ref
365
+ // cloneRef absent → next (main HEAD)
366
+ // local path → no channel concept
337
367
  const { Manifest } = require('../core/manifest');
338
368
  const manifestObj = new Manifest();
339
369
 
340
- await manifestObj.addModule(bmadDir, resolved.code, {
341
- version: resolved.version || null,
370
+ const hasGitClone = !!resolved.repoUrl;
371
+ const manifestEntry = {
372
+ version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || null),
342
373
  source: 'custom',
343
374
  npmPackage: null,
344
375
  repoUrl: resolved.repoUrl || null,
345
- });
376
+ };
377
+ if (hasGitClone) {
378
+ manifestEntry.channel = resolved.cloneRef ? 'pinned' : 'next';
379
+ if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
380
+ if (resolved.rawInput) manifestEntry.rawSource = resolved.rawInput;
381
+ }
382
+ if (resolved.localPath) manifestEntry.localPath = resolved.localPath;
383
+ await manifestObj.addModule(bmadDir, resolved.code, manifestEntry);
346
384
 
347
- return { success: true, module: resolved.code, path: targetPath, versionInfo: { version: resolved.version || '' } };
385
+ return {
386
+ success: true,
387
+ module: resolved.code,
388
+ path: targetPath,
389
+ // Match the manifestEntry.version expression above so downstream summary
390
+ // lines show the cloned ref (tag or 'main') instead of the on-disk
391
+ // package.json version for git-backed custom installs.
392
+ versionInfo: { version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || '') },
393
+ };
348
394
  }
349
395
 
350
396
  /**
@@ -820,10 +866,10 @@ class OfficialModules {
820
866
  let foundAny = false;
821
867
  const entries = await fs.readdir(bmadDir, { withFileTypes: true });
822
868
 
869
+ const nonModuleDirs = new Set(['_config', '_memory', 'memory', 'docs', 'scripts', 'custom']);
823
870
  for (const entry of entries) {
824
871
  if (entry.isDirectory()) {
825
- // Skip the _config directory - it's for system use
826
- if (entry.name === '_config' || entry.name === '_memory') {
872
+ if (nonModuleDirs.has(entry.name)) {
827
873
  continue;
828
874
  }
829
875
 
@@ -1,6 +1,37 @@
1
1
  const https = require('node:https');
2
2
  const yaml = require('yaml');
3
3
 
4
+ /**
5
+ * Build a rich Error from a non-2xx response. Includes the URL, the GitHub
6
+ * JSON error message (or a truncated body snippet), rate-limit reset time,
7
+ * and Retry-After — anything present that would help a user recover.
8
+ */
9
+ function buildHttpError(url, res, body) {
10
+ const parts = [`HTTP ${res.statusCode} ${url}`];
11
+
12
+ if (body) {
13
+ try {
14
+ const parsed = JSON.parse(body);
15
+ if (parsed.message) parts.push(parsed.message);
16
+ if (parsed.documentation_url) parts.push(`(see ${parsed.documentation_url})`);
17
+ } catch {
18
+ const snippet = body.slice(0, 200).trim();
19
+ if (snippet) parts.push(snippet);
20
+ }
21
+ }
22
+
23
+ const remaining = res.headers['x-ratelimit-remaining'];
24
+ const reset = res.headers['x-ratelimit-reset'];
25
+ if (remaining === '0' && reset) {
26
+ parts.push(`rate limit exhausted; resets at ${new Date(Number(reset) * 1000).toISOString()}`);
27
+ }
28
+
29
+ const retryAfter = res.headers['retry-after'];
30
+ if (retryAfter) parts.push(`retry after ${retryAfter}`);
31
+
32
+ return new Error(parts.join(' — '));
33
+ }
34
+
4
35
  /**
5
36
  * Shared HTTP client for fetching registry data from GitHub.
6
37
  * Used by ExternalModuleManager, CommunityModuleManager, and CustomModuleManager.
@@ -12,25 +43,31 @@ class RegistryClient {
12
43
 
13
44
  /**
14
45
  * Fetch a URL and return the response body as a string.
15
- * Follows one redirect (GitHub sometimes 301s).
46
+ * Follows up to 3 redirects (GitHub sometimes 301s).
16
47
  * @param {string} url - URL to fetch
17
48
  * @param {number} [timeout] - Timeout in ms (overrides default)
49
+ * @param {number} [maxRedirects=3] - Maximum redirects to follow
18
50
  * @returns {Promise<string>} Response body
19
51
  */
20
- fetch(url, timeout) {
52
+ fetch(url, timeout, maxRedirects = 3) {
21
53
  const timeoutMs = timeout || this.timeout;
22
54
  return new Promise((resolve, reject) => {
23
55
  const req = https
24
56
  .get(url, { timeout: timeoutMs }, (res) => {
25
57
  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}`));
58
+ if (maxRedirects <= 0) {
59
+ return reject(new Error('Too many redirects'));
60
+ }
61
+ return this.fetch(res.headers.location, timeoutMs, maxRedirects - 1).then(resolve, reject);
30
62
  }
31
63
  let data = '';
32
64
  res.on('data', (chunk) => (data += chunk));
33
- res.on('end', () => resolve(data));
65
+ res.on('end', () => {
66
+ if (res.statusCode !== 200) {
67
+ return reject(buildHttpError(url, res, data));
68
+ }
69
+ resolve(data);
70
+ });
34
71
  })
35
72
  .on('error', reject)
36
73
  .on('timeout', () => {
@@ -50,6 +87,101 @@ class RegistryClient {
50
87
  const content = await this.fetch(url, timeout);
51
88
  return yaml.parse(content);
52
89
  }
90
+
91
+ /**
92
+ * Fetch a file from a GitHub repo using the Contents API first,
93
+ * falling back to raw.githubusercontent.com if the API fails.
94
+ *
95
+ * The API endpoint (`api.github.com`) is tried first because corporate
96
+ * proxies commonly block `raw.githubusercontent.com` while allowing
97
+ * `api.github.com` under the "Software Development" category.
98
+ *
99
+ * @param {string} owner - Repository owner (e.g., 'bmad-code-org')
100
+ * @param {string} repo - Repository name (e.g., 'bmad-plugins-marketplace')
101
+ * @param {string} filePath - Path within the repo (e.g., 'registry/official.yaml')
102
+ * @param {string} ref - Git ref (branch, tag, or SHA; e.g., 'main')
103
+ * @param {number} [timeout] - Timeout in ms (overrides default)
104
+ * @returns {Promise<string>} Raw file content
105
+ */
106
+ async fetchGitHubFile(owner, repo, filePath, ref, timeout) {
107
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${ref}`;
108
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`;
109
+
110
+ // Try GitHub Contents API first (with raw content accept header)
111
+ try {
112
+ return await this._fetchWithHeaders(apiUrl, { Accept: 'application/vnd.github.raw+json' }, timeout);
113
+ } catch (apiError) {
114
+ // API failed — fall back to raw CDN
115
+ try {
116
+ return await this.fetch(rawUrl, timeout);
117
+ } catch (cdnError) {
118
+ throw new AggregateError([apiError, cdnError], `Both GitHub API and raw CDN failed for ${filePath}`);
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Fetch a file from GitHub and parse as YAML.
125
+ * @param {string} owner - Repository owner
126
+ * @param {string} repo - Repository name
127
+ * @param {string} filePath - Path within the repo
128
+ * @param {string} ref - Git ref
129
+ * @param {number} [timeout] - Timeout in ms
130
+ * @returns {Promise<Object>} Parsed YAML content
131
+ */
132
+ async fetchGitHubYaml(owner, repo, filePath, ref, timeout) {
133
+ const content = await this.fetchGitHubFile(owner, repo, filePath, ref, timeout);
134
+ return yaml.parse(content);
135
+ }
136
+
137
+ /**
138
+ * Fetch a URL with custom headers. Used for GitHub API requests.
139
+ * Follows up to 3 redirects.
140
+ * @param {string} url - URL to fetch
141
+ * @param {Object} headers - Request headers
142
+ * @param {number} [timeout] - Timeout in ms
143
+ * @param {number} [maxRedirects=3] - Maximum redirects to follow
144
+ * @returns {Promise<string>} Response body
145
+ * @private
146
+ */
147
+ _fetchWithHeaders(url, headers, timeout, maxRedirects = 3) {
148
+ const timeoutMs = timeout || this.timeout;
149
+ const parsed = new URL(url);
150
+ const options = {
151
+ hostname: parsed.hostname,
152
+ path: parsed.pathname + parsed.search,
153
+ timeout: timeoutMs,
154
+ headers: {
155
+ 'User-Agent': 'bmad-installer',
156
+ ...headers,
157
+ },
158
+ };
159
+
160
+ return new Promise((resolve, reject) => {
161
+ const req = https
162
+ .get(options, (res) => {
163
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
164
+ if (maxRedirects <= 0) {
165
+ return reject(new Error('Too many redirects'));
166
+ }
167
+ return this._fetchWithHeaders(res.headers.location, headers, timeoutMs, maxRedirects - 1).then(resolve, reject);
168
+ }
169
+ let data = '';
170
+ res.on('data', (chunk) => (data += chunk));
171
+ res.on('end', () => {
172
+ if (res.statusCode !== 200) {
173
+ return reject(buildHttpError(url, res, data));
174
+ }
175
+ resolve(data);
176
+ });
177
+ })
178
+ .on('error', reject)
179
+ .on('timeout', () => {
180
+ req.destroy();
181
+ reject(new Error('Request timed out'));
182
+ });
183
+ });
184
+ }
53
185
  }
54
186
 
55
187
  module.exports = { RegistryClient };
@@ -1,6 +1,10 @@
1
1
  # Fallback module registry — used only when the BMad Marketplace repo
2
2
  # (bmad-code-org/bmad-plugins-marketplace) is unreachable.
3
3
  # The remote registry/official.yaml is the source of truth.
4
+ #
5
+ # default_channel (optional) — the install channel when the user does not
6
+ # override with --channel/--pin/--next. Valid values: stable | next.
7
+ # Omit to inherit the installer's hardcoded default (stable).
4
8
 
5
9
  modules:
6
10
  bmad-builder:
@@ -12,6 +16,7 @@ modules:
12
16
  defaultSelected: false
13
17
  type: bmad-org
14
18
  npmPackage: bmad-builder
19
+ default_channel: stable
15
20
 
16
21
  bmad-creative-intelligence-suite:
17
22
  url: https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite
@@ -22,6 +27,7 @@ modules:
22
27
  defaultSelected: false
23
28
  type: bmad-org
24
29
  npmPackage: bmad-creative-intelligence-suite
30
+ default_channel: stable
25
31
 
26
32
  bmad-game-dev-studio:
27
33
  url: https://github.com/bmad-code-org/bmad-module-game-dev-studio.git
@@ -32,6 +38,7 @@ modules:
32
38
  defaultSelected: false
33
39
  type: bmad-org
34
40
  npmPackage: bmad-game-dev-studio
41
+ default_channel: stable
35
42
 
36
43
  bmad-method-test-architecture-enterprise:
37
44
  url: https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise
@@ -42,3 +49,4 @@ modules:
42
49
  defaultSelected: false
43
50
  type: bmad-org
44
51
  npmPackage: bmad-method-test-architecture-enterprise
52
+ default_channel: stable
@@ -0,0 +1,336 @@
1
+ const path = require('node:path');
2
+ const semver = require('semver');
3
+ const yaml = require('yaml');
4
+ const fs = require('../fs-native');
5
+ const { getExternalModuleCachePath, getModulePath, resolveInstalledModuleYaml } = require('../project-root');
6
+
7
+ const DEFAULT_PARENT_DEPTH = 8;
8
+
9
+ /**
10
+ * Resolve a module version from authoritative on-disk metadata.
11
+ * Preference order:
12
+ * 1. package.json nearest the module source/cache root
13
+ * 2. module.yaml in the module source directory
14
+ * 3. .claude-plugin/marketplace.json
15
+ * 4. caller-provided fallback version
16
+ *
17
+ * @param {string} moduleName - Module code/name
18
+ * @param {Object} [options]
19
+ * @param {string} [options.moduleSourcePath] - Directory containing module.yaml
20
+ * @param {string} [options.fallbackVersion] - Final fallback when no metadata is found
21
+ * @param {string[]} [options.marketplacePluginNames] - Preferred marketplace plugin names
22
+ * @returns {Promise<{version: string|null, source: string|null, path: string|null}>}
23
+ */
24
+ async function resolveModuleVersion(moduleName, options = {}) {
25
+ const moduleSourcePath = await normalizeDirectoryPath(options.moduleSourcePath);
26
+ const packageJsonPath = await findPackageJsonPath(moduleName, moduleSourcePath);
27
+
28
+ if (packageJsonPath) {
29
+ const packageVersion = await readPackageJsonVersion(packageJsonPath);
30
+ if (packageVersion) {
31
+ return {
32
+ version: packageVersion,
33
+ source: 'package.json',
34
+ path: packageJsonPath,
35
+ };
36
+ }
37
+ }
38
+
39
+ const moduleYamlPath = await findModuleYamlPath(moduleName, moduleSourcePath);
40
+ if (moduleYamlPath) {
41
+ const moduleVersion = await readModuleYamlVersion(moduleYamlPath);
42
+ if (moduleVersion) {
43
+ return {
44
+ version: moduleVersion,
45
+ source: 'module.yaml',
46
+ path: moduleYamlPath,
47
+ };
48
+ }
49
+ }
50
+
51
+ const marketplaceVersion = await findMarketplaceVersion(moduleName, moduleSourcePath, options.marketplacePluginNames || []);
52
+ if (marketplaceVersion) {
53
+ return marketplaceVersion;
54
+ }
55
+
56
+ const fallbackVersion = normalizeVersion(options.fallbackVersion);
57
+ if (fallbackVersion) {
58
+ return {
59
+ version: fallbackVersion,
60
+ source: 'fallback',
61
+ path: null,
62
+ };
63
+ }
64
+
65
+ return {
66
+ version: null,
67
+ source: null,
68
+ path: null,
69
+ };
70
+ }
71
+
72
+ async function findPackageJsonPath(moduleName, moduleSourcePath) {
73
+ const roots = await buildSearchRoots(moduleName, moduleSourcePath);
74
+
75
+ for (const root of roots) {
76
+ const packageJsonPath = await findNearestUpwardFile(root.searchDir, 'package.json', { boundaryDir: root.boundaryDir });
77
+ if (packageJsonPath) {
78
+ return packageJsonPath;
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ async function findModuleYamlPath(moduleName, moduleSourcePath) {
86
+ if (moduleSourcePath) {
87
+ const directModuleYamlPath = path.join(moduleSourcePath, 'module.yaml');
88
+ if (await fs.pathExists(directModuleYamlPath)) {
89
+ return directModuleYamlPath;
90
+ }
91
+ }
92
+
93
+ return resolveInstalledModuleYaml(moduleName);
94
+ }
95
+
96
+ async function findMarketplaceVersion(moduleName, moduleSourcePath, marketplacePluginNames) {
97
+ const roots = await buildSearchRoots(moduleName, moduleSourcePath);
98
+
99
+ for (const root of roots) {
100
+ const marketplacePath = await findNearestUpwardFile(root.searchDir, path.join('.claude-plugin', 'marketplace.json'), {
101
+ boundaryDir: root.boundaryDir,
102
+ });
103
+ if (!marketplacePath) {
104
+ continue;
105
+ }
106
+
107
+ const data = await readJsonFile(marketplacePath);
108
+ if (!data) {
109
+ continue;
110
+ }
111
+
112
+ const version = extractMarketplaceVersion(data, moduleName, marketplacePluginNames);
113
+ if (version) {
114
+ return {
115
+ version,
116
+ source: 'marketplace.json',
117
+ path: marketplacePath,
118
+ };
119
+ }
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ async function buildSearchRoots(moduleName, moduleSourcePath) {
126
+ const roots = [];
127
+ const seen = new Set();
128
+
129
+ const addRoot = async (candidate) => {
130
+ const normalized = await normalizeExistingDirectory(candidate);
131
+ if (!normalized || seen.has(normalized)) {
132
+ return;
133
+ }
134
+
135
+ seen.add(normalized);
136
+ roots.push({
137
+ searchDir: normalized,
138
+ boundaryDir: await findSearchBoundary(normalized),
139
+ });
140
+ };
141
+
142
+ await addRoot(moduleSourcePath);
143
+
144
+ if (moduleName === 'core' || moduleName === 'bmm') {
145
+ await addRoot(getModulePath(moduleName));
146
+ } else {
147
+ await addRoot(getExternalModuleCachePath(moduleName));
148
+ }
149
+
150
+ return roots;
151
+ }
152
+
153
+ async function findNearestUpwardFile(startDir, relativeFilePath, options = {}) {
154
+ const normalizedStartDir = await normalizeExistingDirectory(startDir);
155
+ if (!normalizedStartDir) {
156
+ return null;
157
+ }
158
+
159
+ const maxDepth = options.maxDepth ?? DEFAULT_PARENT_DEPTH;
160
+ const normalizedBoundaryDir = await normalizeDirectoryPath(options.boundaryDir);
161
+ let currentDir = normalizedStartDir;
162
+ for (let depth = 0; depth <= maxDepth; depth++) {
163
+ const candidate = path.join(currentDir, relativeFilePath);
164
+ if (await fs.pathExists(candidate)) {
165
+ return candidate;
166
+ }
167
+
168
+ if (normalizedBoundaryDir && currentDir === normalizedBoundaryDir) {
169
+ break;
170
+ }
171
+
172
+ const parentDir = path.dirname(currentDir);
173
+ if (parentDir === currentDir) {
174
+ break;
175
+ }
176
+ currentDir = parentDir;
177
+ }
178
+
179
+ return null;
180
+ }
181
+
182
+ async function findSearchBoundary(startDir) {
183
+ const normalizedStartDir = await normalizeExistingDirectory(startDir);
184
+ if (!normalizedStartDir) {
185
+ return null;
186
+ }
187
+
188
+ let currentDir = normalizedStartDir;
189
+ for (let depth = 0; depth <= DEFAULT_PARENT_DEPTH; depth++) {
190
+ if (
191
+ (await fs.pathExists(path.join(currentDir, 'package.json'))) ||
192
+ (await fs.pathExists(path.join(currentDir, '.claude-plugin', 'marketplace.json'))) ||
193
+ (await fs.pathExists(path.join(currentDir, '.git')))
194
+ ) {
195
+ return currentDir;
196
+ }
197
+
198
+ const parentDir = path.dirname(currentDir);
199
+ if (parentDir === currentDir) {
200
+ break;
201
+ }
202
+ currentDir = parentDir;
203
+ }
204
+
205
+ return normalizedStartDir;
206
+ }
207
+
208
+ async function normalizeDirectoryPath(candidate) {
209
+ if (!candidate) {
210
+ return null;
211
+ }
212
+
213
+ const resolvedPath = path.resolve(candidate);
214
+ try {
215
+ const stats = await fs.stat(resolvedPath);
216
+ return stats.isDirectory() ? resolvedPath : path.dirname(resolvedPath);
217
+ } catch {
218
+ return resolvedPath;
219
+ }
220
+ }
221
+
222
+ async function normalizeExistingDirectory(candidate) {
223
+ const normalized = await normalizeDirectoryPath(candidate);
224
+ if (!normalized) {
225
+ return null;
226
+ }
227
+
228
+ if (!(await fs.pathExists(normalized))) {
229
+ return null;
230
+ }
231
+
232
+ return normalized;
233
+ }
234
+
235
+ async function readPackageJsonVersion(packageJsonPath) {
236
+ const data = await readJsonFile(packageJsonPath);
237
+ return normalizeVersion(data?.version);
238
+ }
239
+
240
+ async function readModuleYamlVersion(moduleYamlPath) {
241
+ try {
242
+ const content = await fs.readFile(moduleYamlPath, 'utf8');
243
+ const data = yaml.parse(content);
244
+ return normalizeVersion(data?.version || data?.module_version || data?.moduleVersion);
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ async function readJsonFile(filePath) {
251
+ try {
252
+ const content = await fs.readFile(filePath, 'utf8');
253
+ return JSON.parse(content);
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+
259
+ function extractMarketplaceVersion(data, moduleName, marketplacePluginNames = []) {
260
+ const plugins = Array.isArray(data?.plugins) ? data.plugins : [];
261
+ if (plugins.length === 0) {
262
+ return null;
263
+ }
264
+
265
+ const preferredNames = new Set(
266
+ [moduleName, ...marketplacePluginNames]
267
+ .filter((value) => typeof value === 'string')
268
+ .map((value) => value.trim())
269
+ .filter(Boolean),
270
+ );
271
+
272
+ const exactMatches = [];
273
+ const fallbackVersions = [];
274
+
275
+ for (const plugin of plugins) {
276
+ const version = normalizeVersion(plugin?.version);
277
+ if (!version) {
278
+ continue;
279
+ }
280
+
281
+ fallbackVersions.push(version);
282
+
283
+ const pluginNames = [plugin?.name, plugin?.code].filter((value) => typeof value === 'string').map((value) => value.trim());
284
+ if (pluginNames.some((name) => preferredNames.has(name))) {
285
+ exactMatches.push(version);
286
+ }
287
+ }
288
+
289
+ return pickBestVersion(exactMatches.length > 0 ? exactMatches : fallbackVersions);
290
+ }
291
+
292
+ function pickBestVersion(versions) {
293
+ const candidates = versions.map(normalizeVersion).filter(Boolean);
294
+ if (candidates.length === 0) {
295
+ return null;
296
+ }
297
+
298
+ candidates.sort(compareVersionsDescending);
299
+ return candidates[0];
300
+ }
301
+
302
+ function compareVersionsDescending(left, right) {
303
+ const leftSemver = normalizeSemver(left);
304
+ const rightSemver = normalizeSemver(right);
305
+
306
+ if (leftSemver && rightSemver) {
307
+ return semver.rcompare(leftSemver, rightSemver);
308
+ }
309
+
310
+ if (leftSemver) {
311
+ return -1;
312
+ }
313
+
314
+ if (rightSemver) {
315
+ return 1;
316
+ }
317
+
318
+ return right.localeCompare(left, undefined, { numeric: true, sensitivity: 'base' });
319
+ }
320
+
321
+ function normalizeSemver(version) {
322
+ return semver.valid(version) || semver.valid(semver.coerce(version));
323
+ }
324
+
325
+ function normalizeVersion(version) {
326
+ if (typeof version !== 'string') {
327
+ return null;
328
+ }
329
+
330
+ const trimmed = version.trim();
331
+ return trimmed || null;
332
+ }
333
+
334
+ module.exports = {
335
+ resolveModuleVersion,
336
+ };