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

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 (155) hide show
  1. package/package.json +1 -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-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/status.js +1 -1
  83. package/tools/installer/commands/uninstall.js +1 -1
  84. package/tools/installer/core/existing-install.js +1 -1
  85. package/tools/installer/core/install-paths.js +12 -6
  86. package/tools/installer/core/installer.js +88 -88
  87. package/tools/installer/core/manifest-generator.js +331 -189
  88. package/tools/installer/core/manifest.js +24 -642
  89. package/tools/installer/file-ops.js +1 -1
  90. package/tools/installer/fs-native.js +116 -0
  91. package/tools/installer/ide/_config-driven.js +1 -1
  92. package/tools/installer/ide/platform-codes.js +1 -1
  93. package/tools/installer/ide/shared/path-utils.js +0 -145
  94. package/tools/installer/ide/shared/skill-manifest.js +1 -1
  95. package/tools/installer/message-loader.js +1 -1
  96. package/tools/installer/modules/community-manager.js +11 -6
  97. package/tools/installer/modules/custom-module-manager.js +1 -28
  98. package/tools/installer/modules/external-manager.js +5 -44
  99. package/tools/installer/modules/official-modules.js +5 -53
  100. package/tools/installer/modules/plugin-resolver.js +1 -1
  101. package/tools/installer/modules/registry-client.js +133 -12
  102. package/tools/installer/modules/version-resolver.js +336 -0
  103. package/tools/installer/project-root.js +55 -1
  104. package/tools/installer/prompts.js +0 -106
  105. package/tools/installer/ui.js +50 -48
  106. package/tools/migrate-custom-module-paths.js +1 -1
  107. package/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +0 -11
  108. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +0 -11
  109. package/src/bmm-skills/1-analysis/bmad-document-project/workflow.md +0 -25
  110. package/src/bmm-skills/1-analysis/research/bmad-domain-research/workflow.md +0 -51
  111. package/src/bmm-skills/1-analysis/research/bmad-market-research/workflow.md +0 -51
  112. package/src/bmm-skills/1-analysis/research/bmad-technical-research/workflow.md +0 -52
  113. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +0 -11
  114. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +0 -11
  115. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/workflow.md +0 -61
  116. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/workflow.md +0 -35
  117. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/workflow.md +0 -62
  118. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/workflow.md +0 -61
  119. package/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +0 -11
  120. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +0 -47
  121. package/src/bmm-skills/3-solutioning/bmad-create-architecture/workflow.md +0 -32
  122. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +0 -51
  123. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/workflow.md +0 -39
  124. package/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +0 -11
  125. package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +0 -267
  126. package/src/bmm-skills/4-implementation/bmad-create-story/workflow.md +0 -380
  127. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/workflow.md +0 -136
  128. package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +0 -1479
  129. package/tools/installer/ide/shared/agent-command-generator.js +0 -180
  130. package/tools/installer/ide/shared/bmad-artifacts.js +0 -208
  131. package/tools/installer/ide/shared/module-injections.js +0 -136
  132. package/tools/installer/ide/templates/agent-command-template.md +0 -14
  133. package/tools/installer/ide/templates/combined/antigravity.md +0 -8
  134. package/tools/installer/ide/templates/combined/default-agent.md +0 -15
  135. package/tools/installer/ide/templates/combined/default-task.md +0 -10
  136. package/tools/installer/ide/templates/combined/default-tool.md +0 -10
  137. package/tools/installer/ide/templates/combined/default-workflow.md +0 -6
  138. package/tools/installer/ide/templates/combined/gemini-agent.toml +0 -14
  139. package/tools/installer/ide/templates/combined/gemini-task.toml +0 -11
  140. package/tools/installer/ide/templates/combined/gemini-tool.toml +0 -11
  141. package/tools/installer/ide/templates/combined/gemini-workflow-yaml.toml +0 -16
  142. package/tools/installer/ide/templates/combined/gemini-workflow.toml +0 -14
  143. package/tools/installer/ide/templates/combined/kiro-agent.md +0 -16
  144. package/tools/installer/ide/templates/combined/kiro-task.md +0 -9
  145. package/tools/installer/ide/templates/combined/kiro-tool.md +0 -9
  146. package/tools/installer/ide/templates/combined/kiro-workflow.md +0 -7
  147. package/tools/installer/ide/templates/combined/opencode-agent.md +0 -15
  148. package/tools/installer/ide/templates/combined/opencode-task.md +0 -13
  149. package/tools/installer/ide/templates/combined/opencode-tool.md +0 -13
  150. package/tools/installer/ide/templates/combined/opencode-workflow-yaml.md +0 -16
  151. package/tools/installer/ide/templates/combined/opencode-workflow.md +0 -16
  152. package/tools/installer/ide/templates/combined/rovodev.md +0 -9
  153. package/tools/installer/ide/templates/combined/trae.md +0 -9
  154. package/tools/installer/ide/templates/combined/windsurf-workflow.md +0 -10
  155. 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
 
@@ -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
  };