bmad-method 6.0.0-Beta.1 → 6.0.0-Beta.2

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 (44) hide show
  1. package/CHANGELOG.md +8 -1
  2. package/package.json +1 -1
  3. package/src/bmm/module-help.csv +31 -31
  4. package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md +1 -1
  5. package/src/core/module-help.csv +8 -8
  6. package/tools/cli/installers/lib/core/installer.js +26 -40
  7. package/tools/cli/installers/lib/ide/_config-driven.js +423 -0
  8. package/tools/cli/installers/lib/ide/codex.js +40 -12
  9. package/tools/cli/installers/lib/ide/manager.js +65 -38
  10. package/tools/cli/installers/lib/ide/platform-codes.js +100 -0
  11. package/tools/cli/installers/lib/ide/platform-codes.yaml +241 -0
  12. package/tools/cli/installers/lib/ide/shared/agent-command-generator.js +19 -5
  13. package/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +5 -0
  14. package/tools/cli/installers/lib/ide/shared/path-utils.js +166 -50
  15. package/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +7 -5
  16. package/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +21 -3
  17. package/tools/cli/installers/lib/ide/templates/combined/antigravity.md +8 -0
  18. package/tools/cli/installers/lib/ide/templates/combined/default-agent.md +15 -0
  19. package/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md +14 -0
  20. package/tools/cli/installers/lib/ide/templates/combined/default-workflow.md +6 -0
  21. package/tools/cli/installers/lib/ide/templates/combined/rovodev.md +9 -0
  22. package/tools/cli/installers/lib/ide/templates/combined/trae.md +9 -0
  23. package/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md +10 -0
  24. package/tools/cli/installers/lib/ide/templates/split/gemini/body.md +10 -0
  25. package/tools/cli/installers/lib/ide/templates/split/gemini/header.toml +2 -0
  26. package/tools/cli/installers/lib/ide/templates/split/opencode/body.md +10 -0
  27. package/tools/cli/installers/lib/ide/templates/split/opencode/header.md +4 -0
  28. package/tools/cli/lib/ui.js +19 -75
  29. package/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md +0 -208
  30. package/tools/cli/installers/lib/ide/antigravity.js +0 -474
  31. package/tools/cli/installers/lib/ide/auggie.js +0 -244
  32. package/tools/cli/installers/lib/ide/claude-code.js +0 -506
  33. package/tools/cli/installers/lib/ide/cline.js +0 -272
  34. package/tools/cli/installers/lib/ide/crush.js +0 -149
  35. package/tools/cli/installers/lib/ide/cursor.js +0 -160
  36. package/tools/cli/installers/lib/ide/gemini.js +0 -301
  37. package/tools/cli/installers/lib/ide/github-copilot.js +0 -383
  38. package/tools/cli/installers/lib/ide/iflow.js +0 -191
  39. package/tools/cli/installers/lib/ide/opencode.js +0 -257
  40. package/tools/cli/installers/lib/ide/qwen.js +0 -372
  41. package/tools/cli/installers/lib/ide/roo.js +0 -273
  42. package/tools/cli/installers/lib/ide/rovo-dev.js +0 -290
  43. package/tools/cli/installers/lib/ide/trae.js +0 -313
  44. package/tools/cli/installers/lib/ide/windsurf.js +0 -258
@@ -0,0 +1,423 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const chalk = require('chalk');
4
+ const { BaseIdeSetup } = require('./_base-ide');
5
+ const { AgentCommandGenerator } = require('./shared/agent-command-generator');
6
+ const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
7
+ const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
8
+
9
+ /**
10
+ * Config-driven IDE setup handler
11
+ *
12
+ * This class provides a standardized way to install BMAD artifacts to IDEs
13
+ * based on configuration in platform-codes.yaml. It eliminates the need for
14
+ * individual installer files for each IDE.
15
+ *
16
+ * Features:
17
+ * - Config-driven from platform-codes.yaml
18
+ * - Template-based content generation
19
+ * - Multi-target installation support (e.g., GitHub Copilot)
20
+ * - Artifact type filtering (agents, workflows, tasks, tools)
21
+ */
22
+ class ConfigDrivenIdeSetup extends BaseIdeSetup {
23
+ constructor(platformCode, platformConfig) {
24
+ super(platformCode, platformConfig.name, platformConfig.preferred);
25
+ this.platformConfig = platformConfig;
26
+ this.installerConfig = platformConfig.installer || null;
27
+ }
28
+
29
+ /**
30
+ * Main setup method - called by IdeManager
31
+ * @param {string} projectDir - Project directory
32
+ * @param {string} bmadDir - BMAD installation directory
33
+ * @param {Object} options - Setup options
34
+ * @returns {Promise<Object>} Setup result
35
+ */
36
+ async setup(projectDir, bmadDir, options = {}) {
37
+ console.log(chalk.cyan(`Setting up ${this.name}...`));
38
+
39
+ // Clean up any old BMAD installation first
40
+ await this.cleanup(projectDir);
41
+
42
+ if (!this.installerConfig) {
43
+ return { success: false, reason: 'no-config' };
44
+ }
45
+
46
+ // Handle multi-target installations (e.g., GitHub Copilot)
47
+ if (this.installerConfig.targets) {
48
+ return this.installToMultipleTargets(projectDir, bmadDir, this.installerConfig.targets, options);
49
+ }
50
+
51
+ // Handle single-target installations
52
+ if (this.installerConfig.target_dir) {
53
+ return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
54
+ }
55
+
56
+ return { success: false, reason: 'invalid-config' };
57
+ }
58
+
59
+ /**
60
+ * Install to a single target directory
61
+ * @param {string} projectDir - Project directory
62
+ * @param {string} bmadDir - BMAD installation directory
63
+ * @param {Object} config - Installation configuration
64
+ * @param {Object} options - Setup options
65
+ * @returns {Promise<Object>} Installation result
66
+ */
67
+ async installToTarget(projectDir, bmadDir, config, options) {
68
+ const { target_dir, template_type, artifact_types } = config;
69
+ const targetPath = path.join(projectDir, target_dir);
70
+ await this.ensureDir(targetPath);
71
+
72
+ const selectedModules = options.selectedModules || [];
73
+ const results = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
74
+
75
+ // Install agents
76
+ if (!artifact_types || artifact_types.includes('agents')) {
77
+ const agentGen = new AgentCommandGenerator(this.bmadFolderName);
78
+ const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
79
+ results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config);
80
+ }
81
+
82
+ // Install workflows
83
+ if (!artifact_types || artifact_types.includes('workflows')) {
84
+ const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
85
+ const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
86
+ results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
87
+ }
88
+
89
+ // Install tasks and tools
90
+ if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
91
+ const taskToolGen = new TaskToolCommandGenerator();
92
+ const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath);
93
+ results.tasks = taskToolResult.tasks || 0;
94
+ results.tools = taskToolResult.tools || 0;
95
+ }
96
+
97
+ this.printSummary(results, target_dir);
98
+ return { success: true, results };
99
+ }
100
+
101
+ /**
102
+ * Install to multiple target directories
103
+ * @param {string} projectDir - Project directory
104
+ * @param {string} bmadDir - BMAD installation directory
105
+ * @param {Array} targets - Array of target configurations
106
+ * @param {Object} options - Setup options
107
+ * @returns {Promise<Object>} Installation result
108
+ */
109
+ async installToMultipleTargets(projectDir, bmadDir, targets, options) {
110
+ const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
111
+
112
+ for (const target of targets) {
113
+ const result = await this.installToTarget(projectDir, bmadDir, target, options);
114
+ if (result.success) {
115
+ allResults.agents += result.results.agents || 0;
116
+ allResults.workflows += result.results.workflows || 0;
117
+ allResults.tasks += result.results.tasks || 0;
118
+ allResults.tools += result.results.tools || 0;
119
+ }
120
+ }
121
+
122
+ return { success: true, results: allResults };
123
+ }
124
+
125
+ /**
126
+ * Write agent artifacts to target directory
127
+ * @param {string} targetPath - Target directory path
128
+ * @param {Array} artifacts - Agent artifacts
129
+ * @param {string} templateType - Template type to use
130
+ * @param {Object} config - Installation configuration
131
+ * @returns {Promise<number>} Count of artifacts written
132
+ */
133
+ async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) {
134
+ // Try to load platform-specific template, fall back to default-agent
135
+ const template = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
136
+ let count = 0;
137
+
138
+ for (const artifact of artifacts) {
139
+ const content = this.renderTemplate(template, artifact);
140
+ const filename = this.generateFilename(artifact, 'agent');
141
+ const filePath = path.join(targetPath, filename);
142
+ await this.writeFile(filePath, content);
143
+ count++;
144
+ }
145
+
146
+ return count;
147
+ }
148
+
149
+ /**
150
+ * Write workflow artifacts to target directory
151
+ * @param {string} targetPath - Target directory path
152
+ * @param {Array} artifacts - Workflow artifacts
153
+ * @param {string} templateType - Template type to use
154
+ * @param {Object} config - Installation configuration
155
+ * @returns {Promise<number>} Count of artifacts written
156
+ */
157
+ async writeWorkflowArtifacts(targetPath, artifacts, templateType, config = {}) {
158
+ let count = 0;
159
+
160
+ for (const artifact of artifacts) {
161
+ if (artifact.type === 'workflow-command') {
162
+ // Use different template based on workflow type (YAML vs MD)
163
+ // Default to 'default' template type, but allow override via config
164
+ const workflowTemplateType = artifact.isYamlWorkflow
165
+ ? config.yaml_workflow_template || `${templateType}-workflow-yaml`
166
+ : config.md_workflow_template || `${templateType}-workflow`;
167
+
168
+ // Fall back to default templates if specific ones don't exist
169
+ const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow';
170
+ const template = await this.loadTemplate(workflowTemplateType, 'workflow', config, finalTemplateType);
171
+ const content = this.renderTemplate(template, artifact);
172
+ const filename = this.generateFilename(artifact, 'workflow');
173
+ const filePath = path.join(targetPath, filename);
174
+ await this.writeFile(filePath, content);
175
+ count++;
176
+ }
177
+ }
178
+
179
+ return count;
180
+ }
181
+
182
+ /**
183
+ * Load template based on type and configuration
184
+ * @param {string} templateType - Template type (claude, windsurf, etc.)
185
+ * @param {string} artifactType - Artifact type (agent, workflow, task, tool)
186
+ * @param {Object} config - Installation configuration
187
+ * @param {string} fallbackTemplateType - Fallback template type if requested template not found
188
+ * @returns {Promise<string>} Template content
189
+ */
190
+ async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) {
191
+ const { header_template, body_template } = config;
192
+
193
+ // Check for separate header/body templates
194
+ if (header_template || body_template) {
195
+ return await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
196
+ }
197
+
198
+ // Load combined template
199
+ const templateName = `${templateType}-${artifactType}.md`;
200
+ const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
201
+
202
+ if (await fs.pathExists(templatePath)) {
203
+ return await fs.readFile(templatePath, 'utf8');
204
+ }
205
+
206
+ // Fall back to default template (if provided)
207
+ if (fallbackTemplateType) {
208
+ const fallbackPath = path.join(__dirname, 'templates', 'combined', `${fallbackTemplateType}.md`);
209
+ if (await fs.pathExists(fallbackPath)) {
210
+ return await fs.readFile(fallbackPath, 'utf8');
211
+ }
212
+ }
213
+
214
+ // Ultimate fallback - minimal template
215
+ return this.getDefaultTemplate(artifactType);
216
+ }
217
+
218
+ /**
219
+ * Load split templates (header + body)
220
+ * @param {string} templateType - Template type
221
+ * @param {string} artifactType - Artifact type
222
+ * @param {string} headerTpl - Header template name
223
+ * @param {string} bodyTpl - Body template name
224
+ * @returns {Promise<string>} Combined template content
225
+ */
226
+ async loadSplitTemplates(templateType, artifactType, headerTpl, bodyTpl) {
227
+ let header = '';
228
+ let body = '';
229
+
230
+ // Load header template
231
+ if (headerTpl) {
232
+ const headerPath = path.join(__dirname, 'templates', 'split', headerTpl);
233
+ if (await fs.pathExists(headerPath)) {
234
+ header = await fs.readFile(headerPath, 'utf8');
235
+ }
236
+ } else {
237
+ // Use default header for template type
238
+ const defaultHeaderPath = path.join(__dirname, 'templates', 'split', templateType, 'header.md');
239
+ if (await fs.pathExists(defaultHeaderPath)) {
240
+ header = await fs.readFile(defaultHeaderPath, 'utf8');
241
+ }
242
+ }
243
+
244
+ // Load body template
245
+ if (bodyTpl) {
246
+ const bodyPath = path.join(__dirname, 'templates', 'split', bodyTpl);
247
+ if (await fs.pathExists(bodyPath)) {
248
+ body = await fs.readFile(bodyPath, 'utf8');
249
+ }
250
+ } else {
251
+ // Use default body for template type
252
+ const defaultBodyPath = path.join(__dirname, 'templates', 'split', templateType, 'body.md');
253
+ if (await fs.pathExists(defaultBodyPath)) {
254
+ body = await fs.readFile(defaultBodyPath, 'utf8');
255
+ }
256
+ }
257
+
258
+ // Combine header and body
259
+ return `${header}\n${body}`;
260
+ }
261
+
262
+ /**
263
+ * Get default minimal template
264
+ * @param {string} artifactType - Artifact type
265
+ * @returns {string} Default template
266
+ */
267
+ getDefaultTemplate(artifactType) {
268
+ if (artifactType === 'agent') {
269
+ return `---
270
+ name: '{{name}}'
271
+ description: '{{description}}'
272
+ ---
273
+
274
+ You must fully embody this agent's persona and follow all activation instructions exactly as specified.
275
+
276
+ <agent-activation CRITICAL="TRUE">
277
+ 1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{path}}
278
+ 2. READ its entire contents - this contains the complete agent persona, menu, and instructions
279
+ 3. FOLLOW every step in the <activation> section precisely
280
+ </agent-activation>
281
+ `;
282
+ }
283
+ return `---
284
+ name: '{{name}}'
285
+ description: '{{description}}'
286
+ ---
287
+
288
+ # {{name}}
289
+
290
+ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
291
+ `;
292
+ }
293
+
294
+ /**
295
+ * Render template with artifact data
296
+ * @param {string} template - Template content
297
+ * @param {Object} artifact - Artifact data
298
+ * @returns {string} Rendered content
299
+ */
300
+ renderTemplate(template, artifact) {
301
+ // Use the appropriate path property based on artifact type
302
+ let pathToUse = artifact.relativePath || '';
303
+ if (artifact.type === 'agent-launcher') {
304
+ pathToUse = artifact.agentPath || artifact.relativePath || '';
305
+ } else if (artifact.type === 'workflow-command') {
306
+ pathToUse = artifact.workflowPath || artifact.relativePath || '';
307
+ }
308
+
309
+ let rendered = template
310
+ .replaceAll('{{name}}', artifact.name || '')
311
+ .replaceAll('{{module}}', artifact.module || 'core')
312
+ .replaceAll('{{path}}', pathToUse)
313
+ .replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
314
+ .replaceAll('{{workflow_path}}', pathToUse);
315
+
316
+ // Replace _bmad placeholder with actual folder name
317
+ rendered = rendered.replaceAll('_bmad', this.bmadFolderName);
318
+
319
+ // Replace {{bmadFolderName}} placeholder if present
320
+ rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
321
+
322
+ return rendered;
323
+ }
324
+
325
+ /**
326
+ * Generate filename for artifact
327
+ * @param {Object} artifact - Artifact data
328
+ * @param {string} artifactType - Artifact type (agent, workflow, task, tool)
329
+ * @returns {string} Generated filename
330
+ */
331
+ generateFilename(artifact, artifactType) {
332
+ const { toDashPath } = require('./shared/path-utils');
333
+ // toDashPath already handles the .agent.md suffix for agents correctly
334
+ // No need to add it again here
335
+ return toDashPath(artifact.relativePath);
336
+ }
337
+
338
+ /**
339
+ * Print installation summary
340
+ * @param {Object} results - Installation results
341
+ * @param {string} targetDir - Target directory (relative)
342
+ */
343
+ printSummary(results, targetDir) {
344
+ console.log(chalk.green(`\n✓ ${this.name} configured:`));
345
+ if (results.agents > 0) {
346
+ console.log(chalk.dim(` - ${results.agents} agents installed`));
347
+ }
348
+ if (results.workflows > 0) {
349
+ console.log(chalk.dim(` - ${results.workflows} workflow commands generated`));
350
+ }
351
+ if (results.tasks > 0 || results.tools > 0) {
352
+ console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`));
353
+ }
354
+ console.log(chalk.dim(` - Destination: ${targetDir}`));
355
+ }
356
+
357
+ /**
358
+ * Cleanup IDE configuration
359
+ * @param {string} projectDir - Project directory
360
+ */
361
+ async cleanup(projectDir) {
362
+ // Clean all target directories
363
+ if (this.installerConfig?.targets) {
364
+ for (const target of this.installerConfig.targets) {
365
+ await this.cleanupTarget(projectDir, target.target_dir);
366
+ }
367
+ } else if (this.installerConfig?.target_dir) {
368
+ await this.cleanupTarget(projectDir, this.installerConfig.target_dir);
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Cleanup a specific target directory
374
+ * @param {string} projectDir - Project directory
375
+ * @param {string} targetDir - Target directory to clean
376
+ */
377
+ async cleanupTarget(projectDir, targetDir) {
378
+ const targetPath = path.join(projectDir, targetDir);
379
+
380
+ if (!(await fs.pathExists(targetPath))) {
381
+ return;
382
+ }
383
+
384
+ // Remove all bmad* files
385
+ let entries;
386
+ try {
387
+ entries = await fs.readdir(targetPath);
388
+ } catch {
389
+ // Directory exists but can't be read - skip cleanup
390
+ return;
391
+ }
392
+
393
+ if (!entries || !Array.isArray(entries)) {
394
+ return;
395
+ }
396
+
397
+ let removedCount = 0;
398
+
399
+ for (const entry of entries) {
400
+ // Skip non-strings or undefined entries
401
+ if (!entry || typeof entry !== 'string') {
402
+ continue;
403
+ }
404
+ if (entry.startsWith('bmad')) {
405
+ const entryPath = path.join(targetPath, entry);
406
+ const stat = await fs.stat(entryPath);
407
+ if (stat.isFile()) {
408
+ await fs.remove(entryPath);
409
+ removedCount++;
410
+ } else if (stat.isDirectory()) {
411
+ await fs.remove(entryPath);
412
+ removedCount++;
413
+ }
414
+ }
415
+ }
416
+
417
+ if (removedCount > 0) {
418
+ console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`));
419
+ }
420
+ }
421
+ }
422
+
423
+ module.exports = { ConfigDrivenIdeSetup };
@@ -154,17 +154,25 @@ class CodexSetup extends BaseIdeSetup {
154
154
 
155
155
  // Check global location
156
156
  if (await fs.pathExists(globalDir)) {
157
- const entries = await fs.readdir(globalDir);
158
- if (entries.some((entry) => entry.startsWith('bmad'))) {
159
- return true;
157
+ try {
158
+ const entries = await fs.readdir(globalDir);
159
+ if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
160
+ return true;
161
+ }
162
+ } catch {
163
+ // Ignore errors
160
164
  }
161
165
  }
162
166
 
163
167
  // Check project-specific location
164
168
  if (await fs.pathExists(projectSpecificDir)) {
165
- const entries = await fs.readdir(projectSpecificDir);
166
- if (entries.some((entry) => entry.startsWith('bmad'))) {
167
- return true;
169
+ try {
170
+ const entries = await fs.readdir(projectSpecificDir);
171
+ if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
172
+ return true;
173
+ }
174
+ } catch {
175
+ // Ignore errors
168
176
  }
169
177
  }
170
178
 
@@ -253,19 +261,39 @@ class CodexSetup extends BaseIdeSetup {
253
261
  return;
254
262
  }
255
263
 
256
- const entries = await fs.readdir(destDir);
264
+ let entries;
265
+ try {
266
+ entries = await fs.readdir(destDir);
267
+ } catch (error) {
268
+ // Directory exists but can't be read - skip cleanup
269
+ console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`));
270
+ return;
271
+ }
272
+
273
+ if (!entries || !Array.isArray(entries)) {
274
+ return;
275
+ }
257
276
 
258
277
  for (const entry of entries) {
278
+ // Skip non-strings or undefined entries
279
+ if (!entry || typeof entry !== 'string') {
280
+ continue;
281
+ }
259
282
  if (!entry.startsWith('bmad')) {
260
283
  continue;
261
284
  }
262
285
 
263
286
  const entryPath = path.join(destDir, entry);
264
- const stat = await fs.stat(entryPath);
265
- if (stat.isFile()) {
266
- await fs.remove(entryPath);
267
- } else if (stat.isDirectory()) {
268
- await fs.remove(entryPath);
287
+ try {
288
+ const stat = await fs.stat(entryPath);
289
+ if (stat.isFile()) {
290
+ await fs.remove(entryPath);
291
+ } else if (stat.isDirectory()) {
292
+ await fs.remove(entryPath);
293
+ }
294
+ } catch (error) {
295
+ // Skip files that can't be processed
296
+ console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`));
269
297
  }
270
298
  }
271
299
  }
@@ -5,11 +5,15 @@ const chalk = require('chalk');
5
5
  /**
6
6
  * IDE Manager - handles IDE-specific setup
7
7
  * Dynamically discovers and loads IDE handlers
8
+ *
9
+ * Loading strategy:
10
+ * 1. Custom installer files (codex.js, kilo.js, kiro-cli.js) - for platforms with unique installation logic
11
+ * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
8
12
  */
9
13
  class IdeManager {
10
14
  constructor() {
11
15
  this.handlers = new Map();
12
- this.loadHandlers();
16
+ this._initialized = false;
13
17
  this.bmadFolderName = 'bmad'; // Default, can be overridden
14
18
  }
15
19
 
@@ -28,53 +32,76 @@ class IdeManager {
28
32
  }
29
33
 
30
34
  /**
31
- * Dynamically load all IDE handlers from directory
35
+ * Ensure handlers are loaded (lazy loading)
32
36
  */
33
- loadHandlers() {
34
- const ideDir = __dirname;
37
+ async ensureInitialized() {
38
+ if (!this._initialized) {
39
+ await this.loadHandlers();
40
+ this._initialized = true;
41
+ }
42
+ }
35
43
 
36
- try {
37
- // Get all JS files in the IDE directory
38
- const files = fs.readdirSync(ideDir).filter((file) => {
39
- // Skip base class, manager, utility files (starting with _), and helper modules
40
- return (
41
- file.endsWith('.js') &&
42
- !file.startsWith('_') &&
43
- file !== 'manager.js' &&
44
- file !== 'workflow-command-generator.js' &&
45
- file !== 'task-tool-command-generator.js'
46
- );
47
- });
44
+ /**
45
+ * Dynamically load all IDE handlers
46
+ * 1. Load custom installer files first (codex.js, kilo.js, kiro-cli.js)
47
+ * 2. Load config-driven handlers from platform-codes.yaml
48
+ */
49
+ async loadHandlers() {
50
+ // Load custom installer files
51
+ this.loadCustomInstallerFiles();
48
52
 
49
- // Sort alphabetically for consistent ordering
50
- files.sort();
53
+ // Load config-driven handlers from platform-codes.yaml
54
+ await this.loadConfigDrivenHandlers();
55
+ }
51
56
 
52
- for (const file of files) {
53
- const moduleName = path.basename(file, '.js');
57
+ /**
58
+ * Load custom installer files (unique installation logic)
59
+ * These files have special installation patterns that don't fit the config-driven model
60
+ */
61
+ loadCustomInstallerFiles() {
62
+ const ideDir = __dirname;
63
+ const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js'];
54
64
 
55
- try {
56
- const modulePath = path.join(ideDir, file);
57
- const HandlerModule = require(modulePath);
65
+ for (const file of customFiles) {
66
+ const filePath = path.join(ideDir, file);
67
+ if (!fs.existsSync(filePath)) continue;
58
68
 
59
- // Get the first exported class (handles various export styles)
60
- const HandlerClass = HandlerModule.default || HandlerModule[Object.keys(HandlerModule)[0]];
69
+ try {
70
+ const HandlerModule = require(filePath);
71
+ const HandlerClass = HandlerModule.default || Object.values(HandlerModule)[0];
61
72
 
62
- if (HandlerClass) {
63
- const instance = new HandlerClass();
64
- // Use the name property from the instance (set in constructor)
65
- // Only add if the instance has a valid name
66
- if (instance.name && typeof instance.name === 'string') {
67
- this.handlers.set(instance.name, instance);
68
- } else {
69
- console.log(chalk.yellow(` Warning: ${moduleName} handler missing valid 'name' property`));
70
- }
73
+ if (HandlerClass) {
74
+ const instance = new HandlerClass();
75
+ if (instance.name && typeof instance.name === 'string') {
76
+ this.handlers.set(instance.name, instance);
71
77
  }
72
- } catch (error) {
73
- console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`));
74
78
  }
79
+ } catch (error) {
80
+ console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`));
75
81
  }
76
- } catch (error) {
77
- console.error(chalk.red('Failed to load IDE handlers:'), error.message);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Load config-driven handlers from platform-codes.yaml
87
+ * This creates ConfigDrivenIdeSetup instances for platforms with installer config
88
+ */
89
+ async loadConfigDrivenHandlers() {
90
+ const { loadPlatformCodes } = require('./platform-codes');
91
+ const platformConfig = await loadPlatformCodes();
92
+
93
+ const { ConfigDrivenIdeSetup } = require('./_config-driven');
94
+
95
+ for (const [platformCode, platformInfo] of Object.entries(platformConfig.platforms)) {
96
+ // Skip if already loaded by custom installer
97
+ if (this.handlers.has(platformCode)) continue;
98
+
99
+ // Skip if no installer config (platform may not need installation)
100
+ if (!platformInfo.installer) continue;
101
+
102
+ const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo);
103
+ handler.setBmadFolderName(this.bmadFolderName);
104
+ this.handlers.set(platformCode, handler);
78
105
  }
79
106
  }
80
107