bmad-method 6.3.1-next.2 → 6.3.1-next.21

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 (160) hide show
  1. package/package.json +3 -3
  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-08-scoping.md +70 -23
  35. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-11-polish.md +1 -1
  36. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md +6 -0
  37. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/SKILL.md +70 -1
  38. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/customize.toml +41 -0
  39. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md +6 -0
  40. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/SKILL.md +97 -1
  41. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/customize.toml +42 -0
  42. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +2 -0
  43. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/SKILL.md +99 -1
  44. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/customize.toml +42 -0
  45. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-13-report-complete.md +1 -0
  46. package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +50 -30
  47. package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +65 -0
  48. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/SKILL.md +86 -1
  49. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/customize.toml +41 -0
  50. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md +6 -0
  51. package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +69 -1
  52. package/src/bmm-skills/3-solutioning/bmad-create-architecture/customize.toml +41 -0
  53. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +6 -0
  54. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/SKILL.md +88 -1
  55. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/customize.toml +41 -0
  56. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md +6 -0
  57. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/SKILL.md +76 -1
  58. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/customize.toml +41 -0
  59. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md +6 -0
  60. package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +48 -43
  61. package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +90 -0
  62. package/src/bmm-skills/4-implementation/bmad-correct-course/SKILL.md +296 -1
  63. package/src/bmm-skills/4-implementation/bmad-correct-course/customize.toml +41 -0
  64. package/src/bmm-skills/4-implementation/bmad-create-story/SKILL.md +412 -1
  65. package/src/bmm-skills/4-implementation/bmad-create-story/customize.toml +41 -0
  66. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md +171 -1
  67. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/customize.toml +41 -0
  68. package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +1507 -1
  69. package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +41 -0
  70. package/src/bmm-skills/module.yaml +49 -0
  71. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +7 -1
  72. package/src/core-skills/bmad-customize/SKILL.md +111 -0
  73. package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +231 -0
  74. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +249 -0
  75. package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
  76. package/src/core-skills/bmad-party-mode/SKILL.md +13 -10
  77. package/src/core-skills/module-help.csv +1 -0
  78. package/src/core-skills/module.yaml +3 -0
  79. package/src/scripts/resolve_config.py +176 -0
  80. package/src/scripts/resolve_customization.py +230 -0
  81. package/tools/installer/cli-utils.js +0 -137
  82. package/tools/installer/commands/install.js +13 -0
  83. package/tools/installer/commands/status.js +1 -1
  84. package/tools/installer/commands/uninstall.js +1 -1
  85. package/tools/installer/core/config.js +4 -1
  86. package/tools/installer/core/existing-install.js +1 -1
  87. package/tools/installer/core/install-paths.js +12 -6
  88. package/tools/installer/core/installer.js +182 -95
  89. package/tools/installer/core/manifest-generator.js +347 -190
  90. package/tools/installer/core/manifest.js +49 -642
  91. package/tools/installer/file-ops.js +1 -1
  92. package/tools/installer/fs-native.js +116 -0
  93. package/tools/installer/ide/_config-driven.js +1 -1
  94. package/tools/installer/ide/platform-codes.js +1 -1
  95. package/tools/installer/ide/shared/path-utils.js +0 -145
  96. package/tools/installer/ide/shared/skill-manifest.js +1 -1
  97. package/tools/installer/message-loader.js +1 -1
  98. package/tools/installer/modules/channel-plan.js +203 -0
  99. package/tools/installer/modules/channel-resolver.js +241 -0
  100. package/tools/installer/modules/community-manager.js +131 -24
  101. package/tools/installer/modules/custom-module-manager.js +161 -47
  102. package/tools/installer/modules/external-manager.js +236 -73
  103. package/tools/installer/modules/official-modules.js +61 -63
  104. package/tools/installer/modules/plugin-resolver.js +1 -1
  105. package/tools/installer/modules/registry-client.js +133 -12
  106. package/tools/installer/modules/registry-fallback.yaml +8 -0
  107. package/tools/installer/modules/version-resolver.js +336 -0
  108. package/tools/installer/project-root.js +55 -1
  109. package/tools/installer/prompts.js +0 -106
  110. package/tools/installer/ui.js +457 -51
  111. package/tools/migrate-custom-module-paths.js +1 -1
  112. package/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +0 -11
  113. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +0 -11
  114. package/src/bmm-skills/1-analysis/bmad-document-project/workflow.md +0 -25
  115. package/src/bmm-skills/1-analysis/research/bmad-domain-research/workflow.md +0 -51
  116. package/src/bmm-skills/1-analysis/research/bmad-market-research/workflow.md +0 -51
  117. package/src/bmm-skills/1-analysis/research/bmad-technical-research/workflow.md +0 -52
  118. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +0 -11
  119. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +0 -11
  120. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/workflow.md +0 -61
  121. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/workflow.md +0 -35
  122. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/workflow.md +0 -62
  123. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/workflow.md +0 -61
  124. package/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +0 -11
  125. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +0 -47
  126. package/src/bmm-skills/3-solutioning/bmad-create-architecture/workflow.md +0 -32
  127. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +0 -51
  128. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/workflow.md +0 -39
  129. package/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +0 -11
  130. package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +0 -267
  131. package/src/bmm-skills/4-implementation/bmad-create-story/workflow.md +0 -380
  132. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/workflow.md +0 -136
  133. package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +0 -1479
  134. package/tools/installer/ide/shared/agent-command-generator.js +0 -180
  135. package/tools/installer/ide/shared/bmad-artifacts.js +0 -208
  136. package/tools/installer/ide/shared/module-injections.js +0 -136
  137. package/tools/installer/ide/templates/agent-command-template.md +0 -14
  138. package/tools/installer/ide/templates/combined/antigravity.md +0 -8
  139. package/tools/installer/ide/templates/combined/default-agent.md +0 -15
  140. package/tools/installer/ide/templates/combined/default-task.md +0 -10
  141. package/tools/installer/ide/templates/combined/default-tool.md +0 -10
  142. package/tools/installer/ide/templates/combined/default-workflow.md +0 -6
  143. package/tools/installer/ide/templates/combined/gemini-agent.toml +0 -14
  144. package/tools/installer/ide/templates/combined/gemini-task.toml +0 -11
  145. package/tools/installer/ide/templates/combined/gemini-tool.toml +0 -11
  146. package/tools/installer/ide/templates/combined/gemini-workflow-yaml.toml +0 -16
  147. package/tools/installer/ide/templates/combined/gemini-workflow.toml +0 -14
  148. package/tools/installer/ide/templates/combined/kiro-agent.md +0 -16
  149. package/tools/installer/ide/templates/combined/kiro-task.md +0 -9
  150. package/tools/installer/ide/templates/combined/kiro-tool.md +0 -9
  151. package/tools/installer/ide/templates/combined/kiro-workflow.md +0 -7
  152. package/tools/installer/ide/templates/combined/opencode-agent.md +0 -15
  153. package/tools/installer/ide/templates/combined/opencode-task.md +0 -13
  154. package/tools/installer/ide/templates/combined/opencode-tool.md +0 -13
  155. package/tools/installer/ide/templates/combined/opencode-workflow-yaml.md +0 -16
  156. package/tools/installer/ide/templates/combined/opencode-workflow.md +0 -16
  157. package/tools/installer/ide/templates/combined/rovodev.md +0 -9
  158. package/tools/installer/ide/templates/combined/trae.md +0 -9
  159. package/tools/installer/ide/templates/combined/windsurf-workflow.md +0 -10
  160. package/tools/installer/ide/templates/split/.gitkeep +0 -0
@@ -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', () => {
@@ -52,14 +89,98 @@ class RegistryClient {
52
89
  }
53
90
 
54
91
  /**
55
- * Fetch a URL and parse the response as JSON.
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.
56
140
  * @param {string} url - URL to fetch
141
+ * @param {Object} headers - Request headers
57
142
  * @param {number} [timeout] - Timeout in ms
58
- * @returns {Promise<Object>} Parsed JSON content
143
+ * @param {number} [maxRedirects=3] - Maximum redirects to follow
144
+ * @returns {Promise<string>} Response body
145
+ * @private
59
146
  */
60
- async fetchJson(url, timeout) {
61
- const content = await this.fetch(url, timeout);
62
- return JSON.parse(content);
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
+ });
63
184
  }
64
185
  }
65
186
 
@@ -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
+ };
@@ -1,5 +1,6 @@
1
1
  const path = require('node:path');
2
- const fs = require('fs-extra');
2
+ const os = require('node:os');
3
+ const fs = require('./fs-native');
3
4
 
4
5
  /**
5
6
  * Find the BMAD project root directory by looking for package.json
@@ -69,9 +70,62 @@ function getModulePath(moduleName, ...segments) {
69
70
  return getSourcePath('modules', moduleName, ...segments);
70
71
  }
71
72
 
73
+ /**
74
+ * Path to the local external-module clone cache.
75
+ * External official modules (bmb, cis, gds, tea, wds, etc.) are cloned here
76
+ * by ExternalModuleManager during install and are not copied into <src>/modules/.
77
+ */
78
+ function getExternalModuleCachePath(moduleName, ...segments) {
79
+ const base = process.env.BMAD_EXTERNAL_MODULES_CACHE || path.join(os.homedir(), '.bmad', 'cache', 'external-modules');
80
+ return path.join(base, moduleName, ...segments);
81
+ }
82
+
83
+ /**
84
+ * Locate an installed module's `module.yaml` by filesystem lookup only.
85
+ *
86
+ * Built-in modules (core, bmm) live under <src>. External official modules are
87
+ * cloned into ~/.bmad/cache/external-modules/<name>/ with varying internal
88
+ * layouts (some at src/module.yaml, some at skills/module.yaml, some nested).
89
+ * This mirrors the candidate-path search in
90
+ * ExternalModuleManager.findExternalModuleSource but performs no git/network
91
+ * work, which keeps it safe to call during manifest writing.
92
+ *
93
+ * @param {string} moduleName
94
+ * @returns {Promise<string|null>} Absolute path to module.yaml, or null if not found.
95
+ */
96
+ async function resolveInstalledModuleYaml(moduleName) {
97
+ const builtIn = path.join(getModulePath(moduleName), 'module.yaml');
98
+ if (await fs.pathExists(builtIn)) return builtIn;
99
+
100
+ const cacheRoot = getExternalModuleCachePath(moduleName);
101
+ if (!(await fs.pathExists(cacheRoot))) return null;
102
+
103
+ for (const dir of ['skills', 'src']) {
104
+ const direct = path.join(cacheRoot, dir, 'module.yaml');
105
+ if (await fs.pathExists(direct)) return direct;
106
+
107
+ const dirPath = path.join(cacheRoot, dir);
108
+ if (await fs.pathExists(dirPath)) {
109
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
110
+ for (const entry of entries) {
111
+ if (!entry.isDirectory()) continue;
112
+ const nested = path.join(dirPath, entry.name, 'module.yaml');
113
+ if (await fs.pathExists(nested)) return nested;
114
+ }
115
+ }
116
+ }
117
+
118
+ const atRoot = path.join(cacheRoot, 'module.yaml');
119
+ if (await fs.pathExists(atRoot)) return atRoot;
120
+
121
+ return null;
122
+ }
123
+
72
124
  module.exports = {
73
125
  getProjectRoot,
74
126
  getSourcePath,
75
127
  getModulePath,
128
+ getExternalModuleCachePath,
129
+ resolveInstalledModuleYaml,
76
130
  findProjectRoot,
77
131
  };