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.
- package/CHANGELOG.md +8 -1
- package/package.json +1 -1
- package/src/bmm/module-help.csv +31 -31
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md +1 -1
- package/src/core/module-help.csv +8 -8
- package/tools/cli/installers/lib/core/installer.js +26 -40
- package/tools/cli/installers/lib/ide/_config-driven.js +423 -0
- package/tools/cli/installers/lib/ide/codex.js +40 -12
- package/tools/cli/installers/lib/ide/manager.js +65 -38
- package/tools/cli/installers/lib/ide/platform-codes.js +100 -0
- package/tools/cli/installers/lib/ide/platform-codes.yaml +241 -0
- package/tools/cli/installers/lib/ide/shared/agent-command-generator.js +19 -5
- package/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +5 -0
- package/tools/cli/installers/lib/ide/shared/path-utils.js +166 -50
- package/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +7 -5
- package/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +21 -3
- package/tools/cli/installers/lib/ide/templates/combined/antigravity.md +8 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-agent.md +15 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md +14 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-workflow.md +6 -0
- package/tools/cli/installers/lib/ide/templates/combined/rovodev.md +9 -0
- package/tools/cli/installers/lib/ide/templates/combined/trae.md +9 -0
- package/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md +10 -0
- package/tools/cli/installers/lib/ide/templates/split/gemini/body.md +10 -0
- package/tools/cli/installers/lib/ide/templates/split/gemini/header.toml +2 -0
- package/tools/cli/installers/lib/ide/templates/split/opencode/body.md +10 -0
- package/tools/cli/installers/lib/ide/templates/split/opencode/header.md +4 -0
- package/tools/cli/lib/ui.js +19 -75
- package/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md +0 -208
- package/tools/cli/installers/lib/ide/antigravity.js +0 -474
- package/tools/cli/installers/lib/ide/auggie.js +0 -244
- package/tools/cli/installers/lib/ide/claude-code.js +0 -506
- package/tools/cli/installers/lib/ide/cline.js +0 -272
- package/tools/cli/installers/lib/ide/crush.js +0 -149
- package/tools/cli/installers/lib/ide/cursor.js +0 -160
- package/tools/cli/installers/lib/ide/gemini.js +0 -301
- package/tools/cli/installers/lib/ide/github-copilot.js +0 -383
- package/tools/cli/installers/lib/ide/iflow.js +0 -191
- package/tools/cli/installers/lib/ide/opencode.js +0 -257
- package/tools/cli/installers/lib/ide/qwen.js +0 -372
- package/tools/cli/installers/lib/ide/roo.js +0 -273
- package/tools/cli/installers/lib/ide/rovo-dev.js +0 -290
- package/tools/cli/installers/lib/ide/trae.js +0 -313
- 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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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.
|
|
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
|
-
*
|
|
35
|
+
* Ensure handlers are loaded (lazy loading)
|
|
32
36
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
async ensureInitialized() {
|
|
38
|
+
if (!this._initialized) {
|
|
39
|
+
await this.loadHandlers();
|
|
40
|
+
this._initialized = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
53
|
+
// Load config-driven handlers from platform-codes.yaml
|
|
54
|
+
await this.loadConfigDrivenHandlers();
|
|
55
|
+
}
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
for (const file of customFiles) {
|
|
66
|
+
const filePath = path.join(ideDir, file);
|
|
67
|
+
if (!fs.existsSync(filePath)) continue;
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
try {
|
|
70
|
+
const HandlerModule = require(filePath);
|
|
71
|
+
const HandlerClass = HandlerModule.default || Object.values(HandlerModule)[0];
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
}
|
|
77
|
-
|
|
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
|
|