bmad-method 6.2.2 → 6.2.3-next.1

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 (77) hide show
  1. package/.claude-plugin/marketplace.json +78 -0
  2. package/package.json +8 -8
  3. package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
  4. package/src/core-skills/bmad-init/scripts/bmad_init.py +35 -4
  5. package/src/core-skills/bmad-init/scripts/tests/test_bmad_init.py +64 -0
  6. package/tools/{cli → installer}/bmad-cli.js +3 -1
  7. package/tools/{cli/lib → installer}/cli-utils.js +3 -4
  8. package/tools/{cli → installer}/commands/install.js +3 -3
  9. package/tools/{cli → installer}/commands/status.js +4 -4
  10. package/tools/{cli → installer}/commands/uninstall.js +5 -5
  11. package/tools/installer/core/config.js +52 -0
  12. package/tools/{cli/installers/lib → installer}/core/custom-module-cache.js +1 -1
  13. package/tools/installer/core/existing-install.js +127 -0
  14. package/tools/installer/core/install-paths.js +129 -0
  15. package/tools/installer/core/installer.js +1790 -0
  16. package/tools/{cli/installers/lib → installer}/core/manifest-generator.js +3 -3
  17. package/tools/{cli/installers/lib → installer}/core/manifest.js +2 -2
  18. package/tools/{cli/installers/lib/custom/handler.js → installer/custom-handler.js} +1 -1
  19. package/tools/{cli/installers/lib → installer}/ide/_config-driven.js +30 -397
  20. package/tools/{cli/installers/lib → installer}/ide/manager.js +1 -53
  21. package/tools/installer/ide/platform-codes.js +37 -0
  22. package/tools/installer/ide/platform-codes.yaml +190 -0
  23. package/tools/{cli/installers/lib → installer}/ide/shared/module-injections.js +1 -1
  24. package/tools/{cli/installers/lib → installer}/message-loader.js +2 -2
  25. package/tools/installer/modules/custom-modules.js +197 -0
  26. package/tools/installer/modules/external-manager.js +323 -0
  27. package/tools/{cli/installers/lib/core/config-collector.js → installer/modules/official-modules.js} +714 -43
  28. package/tools/{cli/lib → installer}/ui.js +65 -299
  29. package/tools/javascript-conventions.md +5 -0
  30. package/tools/bmad-npx-wrapper.js +0 -38
  31. package/tools/cli/installers/lib/core/dependency-resolver.js +0 -743
  32. package/tools/cli/installers/lib/core/detector.js +0 -223
  33. package/tools/cli/installers/lib/core/ide-config-manager.js +0 -157
  34. package/tools/cli/installers/lib/core/installer.js +0 -3002
  35. package/tools/cli/installers/lib/ide/_base-ide.js +0 -657
  36. package/tools/cli/installers/lib/ide/platform-codes.js +0 -100
  37. package/tools/cli/installers/lib/ide/platform-codes.yaml +0 -341
  38. package/tools/cli/installers/lib/modules/external-manager.js +0 -136
  39. package/tools/cli/installers/lib/modules/manager.js +0 -928
  40. package/tools/cli/lib/config.js +0 -213
  41. package/tools/cli/lib/platform-codes.js +0 -116
  42. package/tools/lib/xml-utils.js +0 -13
  43. /package/tools/{cli → installer}/README.md +0 -0
  44. /package/tools/{cli → installer}/external-official-modules.yaml +0 -0
  45. /package/tools/{cli/lib → installer}/file-ops.js +0 -0
  46. /package/tools/{cli/installers/lib → installer}/ide/shared/agent-command-generator.js +0 -0
  47. /package/tools/{cli/installers/lib → installer}/ide/shared/bmad-artifacts.js +0 -0
  48. /package/tools/{cli/installers/lib → installer}/ide/shared/path-utils.js +0 -0
  49. /package/tools/{cli/installers/lib → installer}/ide/shared/skill-manifest.js +0 -0
  50. /package/tools/{cli/installers/lib → installer}/ide/templates/agent-command-template.md +0 -0
  51. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/antigravity.md +0 -0
  52. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-agent.md +0 -0
  53. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-task.md +0 -0
  54. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-tool.md +0 -0
  55. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-workflow.md +0 -0
  56. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-agent.toml +0 -0
  57. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-task.toml +0 -0
  58. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-tool.toml +0 -0
  59. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow-yaml.toml +0 -0
  60. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow.toml +0 -0
  61. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-agent.md +0 -0
  62. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-task.md +0 -0
  63. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-tool.md +0 -0
  64. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-workflow.md +0 -0
  65. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-agent.md +0 -0
  66. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-task.md +0 -0
  67. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-tool.md +0 -0
  68. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow-yaml.md +0 -0
  69. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow.md +0 -0
  70. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/rovodev.md +0 -0
  71. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/trae.md +0 -0
  72. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/windsurf-workflow.md +0 -0
  73. /package/tools/{cli/installers/lib → installer}/ide/templates/split/.gitkeep +0 -0
  74. /package/tools/{cli/installers → installer}/install-messages.yaml +0 -0
  75. /package/tools/{cli/lib → installer}/project-root.js +0 -0
  76. /package/tools/{cli/lib → installer}/prompts.js +0 -0
  77. /package/tools/{cli/lib → installer}/yaml-format.js +0 -0
@@ -0,0 +1,323 @@
1
+ const fs = require('fs-extra');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { execSync } = require('node:child_process');
5
+ const yaml = require('yaml');
6
+ const prompts = require('../prompts');
7
+
8
+ /**
9
+ * Manages external official modules defined in external-official-modules.yaml
10
+ * These are modules hosted in external repositories that can be installed
11
+ *
12
+ * @class ExternalModuleManager
13
+ */
14
+ class ExternalModuleManager {
15
+ constructor() {
16
+ this.externalModulesConfigPath = path.join(__dirname, '../external-official-modules.yaml');
17
+ this.cachedModules = null;
18
+ }
19
+
20
+ /**
21
+ * Load and parse the external-official-modules.yaml file
22
+ * @returns {Object} Parsed YAML content with modules object
23
+ */
24
+ async loadExternalModulesConfig() {
25
+ if (this.cachedModules) {
26
+ return this.cachedModules;
27
+ }
28
+
29
+ try {
30
+ const content = await fs.readFile(this.externalModulesConfigPath, 'utf8');
31
+ const config = yaml.parse(content);
32
+ this.cachedModules = config;
33
+ return config;
34
+ } catch (error) {
35
+ await prompts.log.warn(`Failed to load external modules config: ${error.message}`);
36
+ return { modules: {} };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get list of available external modules
42
+ * @returns {Array<Object>} Array of module info objects
43
+ */
44
+ async listAvailable() {
45
+ const config = await this.loadExternalModulesConfig();
46
+ const modules = [];
47
+
48
+ for (const [key, moduleConfig] of Object.entries(config.modules || {})) {
49
+ modules.push({
50
+ key,
51
+ url: moduleConfig.url,
52
+ moduleDefinition: moduleConfig['module-definition'],
53
+ code: moduleConfig.code,
54
+ name: moduleConfig.name,
55
+ header: moduleConfig.header,
56
+ subheader: moduleConfig.subheader,
57
+ description: moduleConfig.description || '',
58
+ defaultSelected: moduleConfig.defaultSelected === true,
59
+ type: moduleConfig.type || 'community', // bmad-org or community
60
+ npmPackage: moduleConfig.npmPackage || null, // Include npm package name
61
+ isExternal: true,
62
+ });
63
+ }
64
+
65
+ return modules;
66
+ }
67
+
68
+ /**
69
+ * Get module info by code
70
+ * @param {string} code - The module code (e.g., 'cis')
71
+ * @returns {Object|null} Module info or null if not found
72
+ */
73
+ async getModuleByCode(code) {
74
+ const modules = await this.listAvailable();
75
+ return modules.find((m) => m.code === code) || null;
76
+ }
77
+
78
+ /**
79
+ * Get module info by key
80
+ * @param {string} key - The module key (e.g., 'bmad-creative-intelligence-suite')
81
+ * @returns {Object|null} Module info or null if not found
82
+ */
83
+ async getModuleByKey(key) {
84
+ const config = await this.loadExternalModulesConfig();
85
+ const moduleConfig = config.modules?.[key];
86
+
87
+ if (!moduleConfig) {
88
+ return null;
89
+ }
90
+
91
+ return {
92
+ key,
93
+ url: moduleConfig.url,
94
+ moduleDefinition: moduleConfig['module-definition'],
95
+ code: moduleConfig.code,
96
+ name: moduleConfig.name,
97
+ header: moduleConfig.header,
98
+ subheader: moduleConfig.subheader,
99
+ description: moduleConfig.description || '',
100
+ defaultSelected: moduleConfig.defaultSelected === true,
101
+ type: moduleConfig.type || 'community', // bmad-org or community
102
+ npmPackage: moduleConfig.npmPackage || null, // Include npm package name
103
+ isExternal: true,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Check if a module code exists in external modules
109
+ * @param {string} code - The module code to check
110
+ * @returns {boolean} True if the module exists
111
+ */
112
+ async hasModule(code) {
113
+ const module = await this.getModuleByCode(code);
114
+ return module !== null;
115
+ }
116
+
117
+ /**
118
+ * Get the URL for a module by code
119
+ * @param {string} code - The module code
120
+ * @returns {string|null} The URL or null if not found
121
+ */
122
+ async getModuleUrl(code) {
123
+ const module = await this.getModuleByCode(code);
124
+ return module ? module.url : null;
125
+ }
126
+
127
+ /**
128
+ * Get the module definition path for a module by code
129
+ * @param {string} code - The module code
130
+ * @returns {string|null} The module definition path or null if not found
131
+ */
132
+ async getModuleDefinition(code) {
133
+ const module = await this.getModuleByCode(code);
134
+ return module ? module.moduleDefinition : null;
135
+ }
136
+
137
+ /**
138
+ * Get the cache directory for external modules
139
+ * @returns {string} Path to the external modules cache directory
140
+ */
141
+ getExternalCacheDir() {
142
+ const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules');
143
+ return cacheDir;
144
+ }
145
+
146
+ /**
147
+ * Clone an external module repository to cache
148
+ * @param {string} moduleCode - Code of the external module
149
+ * @param {Object} options - Clone options
150
+ * @param {boolean} options.silent - Suppress spinner output
151
+ * @returns {string} Path to the cloned repository
152
+ */
153
+ async cloneExternalModule(moduleCode, options = {}) {
154
+ const moduleInfo = await this.getModuleByCode(moduleCode);
155
+
156
+ if (!moduleInfo) {
157
+ throw new Error(`External module '${moduleCode}' not found in external-official-modules.yaml`);
158
+ }
159
+
160
+ const cacheDir = this.getExternalCacheDir();
161
+ const moduleCacheDir = path.join(cacheDir, moduleCode);
162
+ const silent = options.silent || false;
163
+
164
+ // Create cache directory if it doesn't exist
165
+ await fs.ensureDir(cacheDir);
166
+
167
+ // Helper to create a spinner or a no-op when silent
168
+ const createSpinner = async () => {
169
+ if (silent) {
170
+ return {
171
+ start() {},
172
+ stop() {},
173
+ error() {},
174
+ message() {},
175
+ cancel() {},
176
+ clear() {},
177
+ get isSpinning() {
178
+ return false;
179
+ },
180
+ get isCancelled() {
181
+ return false;
182
+ },
183
+ };
184
+ }
185
+ return await prompts.spinner();
186
+ };
187
+
188
+ // Track if we need to install dependencies
189
+ let needsDependencyInstall = false;
190
+ let wasNewClone = false;
191
+
192
+ // Check if already cloned
193
+ if (await fs.pathExists(moduleCacheDir)) {
194
+ // Try to update if it's a git repo
195
+ const fetchSpinner = await createSpinner();
196
+ fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
197
+ try {
198
+ const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
199
+ // Fetch and reset to remote - works better with shallow clones than pull
200
+ execSync('git fetch origin --depth 1', {
201
+ cwd: moduleCacheDir,
202
+ stdio: ['ignore', 'pipe', 'pipe'],
203
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
204
+ });
205
+ execSync('git reset --hard origin/HEAD', {
206
+ cwd: moduleCacheDir,
207
+ stdio: ['ignore', 'pipe', 'pipe'],
208
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
209
+ });
210
+ const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
211
+
212
+ fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
213
+ // Force dependency install if we got new code
214
+ if (currentRef !== newRef) {
215
+ needsDependencyInstall = true;
216
+ }
217
+ } catch {
218
+ fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`);
219
+ // If update fails, remove and re-clone
220
+ await fs.remove(moduleCacheDir);
221
+ wasNewClone = true;
222
+ }
223
+ } else {
224
+ wasNewClone = true;
225
+ }
226
+
227
+ // Clone if not exists or was removed
228
+ if (wasNewClone) {
229
+ const fetchSpinner = await createSpinner();
230
+ fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
231
+ try {
232
+ execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
233
+ stdio: ['ignore', 'pipe', 'pipe'],
234
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
235
+ });
236
+ fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
237
+ } catch (error) {
238
+ fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`);
239
+ throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`);
240
+ }
241
+ }
242
+
243
+ // Install dependencies if package.json exists
244
+ const packageJsonPath = path.join(moduleCacheDir, 'package.json');
245
+ const nodeModulesPath = path.join(moduleCacheDir, 'node_modules');
246
+ if (await fs.pathExists(packageJsonPath)) {
247
+ // Install if node_modules doesn't exist, or if package.json is newer (dependencies changed)
248
+ const nodeModulesMissing = !(await fs.pathExists(nodeModulesPath));
249
+
250
+ // Force install if we updated or cloned new
251
+ if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
252
+ const installSpinner = await createSpinner();
253
+ installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
254
+ try {
255
+ execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
256
+ cwd: moduleCacheDir,
257
+ stdio: ['ignore', 'pipe', 'pipe'],
258
+ timeout: 120_000, // 2 minute timeout
259
+ });
260
+ installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
261
+ } catch (error) {
262
+ installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
263
+ if (!silent) await prompts.log.warn(` ${error.message}`);
264
+ }
265
+ } else {
266
+ // Check if package.json is newer than node_modules
267
+ let packageJsonNewer = false;
268
+ try {
269
+ const packageStats = await fs.stat(packageJsonPath);
270
+ const nodeModulesStats = await fs.stat(nodeModulesPath);
271
+ packageJsonNewer = packageStats.mtime > nodeModulesStats.mtime;
272
+ } catch {
273
+ // If stat fails, assume we need to install
274
+ packageJsonNewer = true;
275
+ }
276
+
277
+ if (packageJsonNewer) {
278
+ const installSpinner = await createSpinner();
279
+ installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
280
+ try {
281
+ execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
282
+ cwd: moduleCacheDir,
283
+ stdio: ['ignore', 'pipe', 'pipe'],
284
+ timeout: 120_000, // 2 minute timeout
285
+ });
286
+ installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
287
+ } catch (error) {
288
+ installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
289
+ if (!silent) await prompts.log.warn(` ${error.message}`);
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ return moduleCacheDir;
296
+ }
297
+
298
+ /**
299
+ * Find the source path for an external module
300
+ * @param {string} moduleCode - Code of the external module
301
+ * @param {Object} options - Options passed to cloneExternalModule
302
+ * @returns {string|null} Path to the module source or null if not found
303
+ */
304
+ async findExternalModuleSource(moduleCode, options = {}) {
305
+ const moduleInfo = await this.getModuleByCode(moduleCode);
306
+
307
+ if (!moduleInfo) {
308
+ return null;
309
+ }
310
+
311
+ // Clone the external module repo
312
+ const cloneDir = await this.cloneExternalModule(moduleCode, options);
313
+
314
+ // The module-definition specifies the path to module.yaml relative to repo root
315
+ // We need to return the directory containing module.yaml
316
+ const moduleDefinitionPath = moduleInfo.moduleDefinition; // e.g., 'src/module.yaml'
317
+ const moduleDir = path.dirname(path.join(cloneDir, moduleDefinitionPath));
318
+
319
+ return moduleDir;
320
+ }
321
+ }
322
+
323
+ module.exports = { ExternalModuleManager };