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
@@ -3,8 +3,8 @@ const fs = require('fs-extra');
3
3
  const yaml = require('yaml');
4
4
  const crypto = require('node:crypto');
5
5
  const csv = require('csv-parse/sync');
6
- const { getSourcePath, getModulePath } = require('../../../lib/project-root');
7
- const prompts = require('../../../lib/prompts');
6
+ const { getSourcePath, getModulePath } = require('../project-root');
7
+ const prompts = require('../prompts');
8
8
  const {
9
9
  loadSkillManifest: loadSkillManifestShared,
10
10
  getCanonicalId: getCanonicalIdShared,
@@ -13,7 +13,7 @@ const {
13
13
  } = require('../ide/shared/skill-manifest');
14
14
 
15
15
  // Load package.json for version info
16
- const packageJson = require('../../../../../package.json');
16
+ const packageJson = require('../../../package.json');
17
17
 
18
18
  /**
19
19
  * Generates manifest files for installed skills and agents
@@ -1,8 +1,8 @@
1
1
  const path = require('node:path');
2
2
  const fs = require('fs-extra');
3
3
  const crypto = require('node:crypto');
4
- const { getProjectRoot } = require('../../../lib/project-root');
5
- const prompts = require('../../../lib/prompts');
4
+ const { getProjectRoot } = require('../project-root');
5
+ const prompts = require('../prompts');
6
6
 
7
7
  class Manifest {
8
8
  /**
@@ -1,7 +1,7 @@
1
1
  const path = require('node:path');
2
2
  const fs = require('fs-extra');
3
3
  const yaml = require('yaml');
4
- const prompts = require('../../../lib/prompts');
4
+ const prompts = require('./prompts');
5
5
  /**
6
6
  * Handler for custom content (custom.yaml)
7
7
  * Discovers custom agents and workflows in the project
@@ -2,9 +2,9 @@ const os = require('node:os');
2
2
  const path = require('node:path');
3
3
  const fs = require('fs-extra');
4
4
  const yaml = require('yaml');
5
- const { BaseIdeSetup } = require('./_base-ide');
6
- const prompts = require('../../../lib/prompts');
5
+ const prompts = require('../prompts');
7
6
  const csv = require('csv-parse/sync');
7
+ const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
8
8
 
9
9
  /**
10
10
  * Config-driven IDE setup handler
@@ -15,43 +15,45 @@ const csv = require('csv-parse/sync');
15
15
  *
16
16
  * Features:
17
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)
18
+ * - Verbatim skill installation from skill-manifest.csv
19
+ * - Legacy directory cleanup and IDE-specific marker removal
21
20
  */
22
- class ConfigDrivenIdeSetup extends BaseIdeSetup {
21
+ class ConfigDrivenIdeSetup {
23
22
  constructor(platformCode, platformConfig) {
24
- super(platformCode, platformConfig.name, platformConfig.preferred);
23
+ this.name = platformCode;
24
+ this.displayName = platformConfig.name || platformCode;
25
+ this.preferred = platformConfig.preferred || false;
25
26
  this.platformConfig = platformConfig;
26
27
  this.installerConfig = platformConfig.installer || null;
28
+ this.bmadFolderName = BMAD_FOLDER_NAME;
27
29
 
28
- // Set configDir from target_dir so base-class detect() works
29
- if (this.installerConfig?.target_dir) {
30
- this.configDir = this.installerConfig.target_dir;
31
- }
30
+ // Set configDir from target_dir so detect() works
31
+ this.configDir = this.installerConfig?.target_dir || null;
32
+ }
33
+
34
+ setBmadFolderName(bmadFolderName) {
35
+ this.bmadFolderName = bmadFolderName;
32
36
  }
33
37
 
34
38
  /**
35
39
  * Detect whether this IDE already has configuration in the project.
36
- * For skill_format platforms, checks for bmad-prefixed entries in target_dir
37
- * (matching old codex.js behavior) instead of just checking directory existence.
40
+ * Checks for bmad-prefixed entries in target_dir.
38
41
  * @param {string} projectDir - Project directory
39
42
  * @returns {Promise<boolean>}
40
43
  */
41
44
  async detect(projectDir) {
42
- if (this.installerConfig?.skill_format && this.configDir) {
43
- const dir = path.join(projectDir || process.cwd(), this.configDir);
44
- if (await fs.pathExists(dir)) {
45
- try {
46
- const entries = await fs.readdir(dir);
47
- return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
48
- } catch {
49
- return false;
50
- }
45
+ if (!this.configDir) return false;
46
+
47
+ const dir = path.join(projectDir || process.cwd(), this.configDir);
48
+ if (await fs.pathExists(dir)) {
49
+ try {
50
+ const entries = await fs.readdir(dir);
51
+ return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
52
+ } catch {
53
+ return false;
51
54
  }
52
- return false;
53
55
  }
54
- return super.detect(projectDir);
56
+ return false;
55
57
  }
56
58
 
57
59
  /**
@@ -90,12 +92,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
90
92
  return { success: false, reason: 'no-config' };
91
93
  }
92
94
 
93
- // Handle multi-target installations (e.g., GitHub Copilot)
94
- if (this.installerConfig.targets) {
95
- return this.installToMultipleTargets(projectDir, bmadDir, this.installerConfig.targets, options);
96
- }
97
-
98
- // Handle single-target installations
99
95
  if (this.installerConfig.target_dir) {
100
96
  return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
101
97
  }
@@ -113,13 +109,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
113
109
  */
114
110
  async installToTarget(projectDir, bmadDir, config, options) {
115
111
  const { target_dir } = config;
116
-
117
- if (!config.skill_format) {
118
- return { success: false, reason: 'missing-skill-format', error: 'Installer config missing skill_format — cannot install skills' };
119
- }
120
-
121
112
  const targetPath = path.join(projectDir, target_dir);
122
- await this.ensureDir(targetPath);
113
+ await fs.ensureDir(targetPath);
123
114
 
124
115
  this.skillWriteTracker = new Set();
125
116
  const results = { skills: 0 };
@@ -132,351 +123,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
132
123
  return { success: true, results };
133
124
  }
134
125
 
135
- /**
136
- * Install to multiple target directories
137
- * @param {string} projectDir - Project directory
138
- * @param {string} bmadDir - BMAD installation directory
139
- * @param {Array} targets - Array of target configurations
140
- * @param {Object} options - Setup options
141
- * @returns {Promise<Object>} Installation result
142
- */
143
- async installToMultipleTargets(projectDir, bmadDir, targets, options) {
144
- const allResults = { skills: 0 };
145
-
146
- for (const target of targets) {
147
- const result = await this.installToTarget(projectDir, bmadDir, target, options);
148
- if (result.success) {
149
- allResults.skills += result.results.skills || 0;
150
- }
151
- }
152
-
153
- return { success: true, results: allResults };
154
- }
155
-
156
- /**
157
- * Load template based on type and configuration
158
- * @param {string} templateType - Template type (claude, windsurf, etc.)
159
- * @param {string} artifactType - Artifact type (agent, workflow, task, tool)
160
- * @param {Object} config - Installation configuration
161
- * @param {string} fallbackTemplateType - Fallback template type if requested template not found
162
- * @returns {Promise<{content: string, extension: string}>} Template content and extension
163
- */
164
- async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) {
165
- const { header_template, body_template } = config;
166
-
167
- // Check for separate header/body templates
168
- if (header_template || body_template) {
169
- const content = await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
170
- // Allow config to override extension, default to .md
171
- const ext = config.extension || '.md';
172
- const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`;
173
- return { content, extension: normalizedExt };
174
- }
175
-
176
- // Load combined template - try multiple extensions
177
- // If artifactType is empty, templateType already contains full name (e.g., 'gemini-workflow-yaml')
178
- const templateBaseName = artifactType ? `${templateType}-${artifactType}` : templateType;
179
- const templateDir = path.join(__dirname, 'templates', 'combined');
180
- const extensions = ['.md', '.toml', '.yaml', '.yml'];
181
-
182
- for (const ext of extensions) {
183
- const templatePath = path.join(templateDir, templateBaseName + ext);
184
- if (await fs.pathExists(templatePath)) {
185
- const content = await fs.readFile(templatePath, 'utf8');
186
- return { content, extension: ext };
187
- }
188
- }
189
-
190
- // Fall back to default template (if provided)
191
- if (fallbackTemplateType) {
192
- for (const ext of extensions) {
193
- const fallbackPath = path.join(templateDir, `${fallbackTemplateType}${ext}`);
194
- if (await fs.pathExists(fallbackPath)) {
195
- const content = await fs.readFile(fallbackPath, 'utf8');
196
- return { content, extension: ext };
197
- }
198
- }
199
- }
200
-
201
- // Ultimate fallback - minimal template
202
- return { content: this.getDefaultTemplate(artifactType), extension: '.md' };
203
- }
204
-
205
- /**
206
- * Load split templates (header + body)
207
- * @param {string} templateType - Template type
208
- * @param {string} artifactType - Artifact type
209
- * @param {string} headerTpl - Header template name
210
- * @param {string} bodyTpl - Body template name
211
- * @returns {Promise<string>} Combined template content
212
- */
213
- async loadSplitTemplates(templateType, artifactType, headerTpl, bodyTpl) {
214
- let header = '';
215
- let body = '';
216
-
217
- // Load header template
218
- if (headerTpl) {
219
- const headerPath = path.join(__dirname, 'templates', 'split', headerTpl);
220
- if (await fs.pathExists(headerPath)) {
221
- header = await fs.readFile(headerPath, 'utf8');
222
- }
223
- } else {
224
- // Use default header for template type
225
- const defaultHeaderPath = path.join(__dirname, 'templates', 'split', templateType, 'header.md');
226
- if (await fs.pathExists(defaultHeaderPath)) {
227
- header = await fs.readFile(defaultHeaderPath, 'utf8');
228
- }
229
- }
230
-
231
- // Load body template
232
- if (bodyTpl) {
233
- const bodyPath = path.join(__dirname, 'templates', 'split', bodyTpl);
234
- if (await fs.pathExists(bodyPath)) {
235
- body = await fs.readFile(bodyPath, 'utf8');
236
- }
237
- } else {
238
- // Use default body for template type
239
- const defaultBodyPath = path.join(__dirname, 'templates', 'split', templateType, 'body.md');
240
- if (await fs.pathExists(defaultBodyPath)) {
241
- body = await fs.readFile(defaultBodyPath, 'utf8');
242
- }
243
- }
244
-
245
- // Combine header and body
246
- return `${header}\n${body}`;
247
- }
248
-
249
- /**
250
- * Get default minimal template
251
- * @param {string} artifactType - Artifact type
252
- * @returns {string} Default template
253
- */
254
- getDefaultTemplate(artifactType) {
255
- if (artifactType === 'agent') {
256
- return `---
257
- name: '{{name}}'
258
- description: '{{description}}'
259
- disable-model-invocation: true
260
- ---
261
-
262
- You must fully embody this agent's persona and follow all activation instructions exactly as specified.
263
-
264
- <agent-activation CRITICAL="TRUE">
265
- 1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{path}}
266
- 2. READ its entire contents - this contains the complete agent persona, menu, and instructions
267
- 3. FOLLOW every step in the <activation> section precisely
268
- </agent-activation>
269
- `;
270
- }
271
- return `---
272
- name: '{{name}}'
273
- description: '{{description}}'
274
- ---
275
-
276
- # {{name}}
277
-
278
- LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
279
- `;
280
- }
281
-
282
- /**
283
- * Render template with artifact data
284
- * @param {string} template - Template content
285
- * @param {Object} artifact - Artifact data
286
- * @returns {string} Rendered content
287
- */
288
- renderTemplate(template, artifact) {
289
- // Use the appropriate path property based on artifact type
290
- let pathToUse = artifact.relativePath || '';
291
- switch (artifact.type) {
292
- case 'agent-launcher': {
293
- pathToUse = artifact.agentPath || artifact.relativePath || '';
294
-
295
- break;
296
- }
297
- case 'workflow-command': {
298
- pathToUse = artifact.workflowPath || artifact.relativePath || '';
299
-
300
- break;
301
- }
302
- case 'task':
303
- case 'tool': {
304
- pathToUse = artifact.path || artifact.relativePath || '';
305
-
306
- break;
307
- }
308
- // No default
309
- }
310
-
311
- // Replace _bmad placeholder with actual folder name BEFORE inserting paths,
312
- // so that paths containing '_bmad' are not corrupted by the blanket replacement.
313
- let rendered = template.replaceAll('_bmad', this.bmadFolderName);
314
-
315
- // Replace {{bmadFolderName}} placeholder if present
316
- rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
317
-
318
- rendered = rendered
319
- .replaceAll('{{name}}', artifact.name || '')
320
- .replaceAll('{{module}}', artifact.module || 'core')
321
- .replaceAll('{{path}}', pathToUse)
322
- .replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
323
- .replaceAll('{{workflow_path}}', pathToUse);
324
-
325
- return rendered;
326
- }
327
-
328
- /**
329
- * Write artifact as a skill directory with SKILL.md inside.
330
- * Writes artifact as a skill directory with SKILL.md inside.
331
- * @param {string} targetPath - Base skills directory
332
- * @param {Object} artifact - Artifact data
333
- * @param {string} content - Rendered template content
334
- */
335
- async writeSkillFile(targetPath, artifact, content) {
336
- const { resolveSkillName } = require('./shared/path-utils');
337
-
338
- // Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md
339
- const flatName = resolveSkillName(artifact);
340
- const skillName = path.basename(flatName.replace(/\.md$/, ''));
341
-
342
- if (!skillName) {
343
- throw new Error(`Cannot derive skill name for artifact: ${artifact.relativePath || JSON.stringify(artifact)}`);
344
- }
345
-
346
- // Create skill directory
347
- const skillDir = path.join(targetPath, skillName);
348
- await this.ensureDir(skillDir);
349
- this.skillWriteTracker?.add(skillName);
350
-
351
- // Transform content: rewrite frontmatter for skills format
352
- const skillContent = this.transformToSkillFormat(content, skillName);
353
-
354
- await this.writeFile(path.join(skillDir, 'SKILL.md'), skillContent);
355
- }
356
-
357
- /**
358
- * Transform artifact content to Agent Skills format.
359
- * Rewrites frontmatter to contain only unquoted name and description.
360
- * @param {string} content - Original content with YAML frontmatter
361
- * @param {string} skillName - Skill name (must match directory name)
362
- * @returns {string} Transformed content
363
- */
364
- transformToSkillFormat(content, skillName) {
365
- // Normalize line endings
366
- content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
367
-
368
- // Parse frontmatter
369
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
370
- if (!fmMatch) {
371
- // No frontmatter -- wrap with minimal frontmatter
372
- const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
373
- return `---\n${fm}\n---\n\n${content}`;
374
- }
375
-
376
- const frontmatter = fmMatch[1];
377
- const body = fmMatch[2];
378
-
379
- // Parse frontmatter with yaml library to extract description
380
- let description;
381
- try {
382
- const parsed = yaml.parse(frontmatter);
383
- const rawDesc = parsed?.description;
384
- description = typeof rawDesc === 'string' && rawDesc ? rawDesc : `${skillName} skill`;
385
- } catch {
386
- description = `${skillName} skill`;
387
- }
388
-
389
- // Build new frontmatter with only name and description, unquoted
390
- const newFrontmatter = yaml.stringify({ name: skillName, description: String(description) }, { lineWidth: 0 }).trimEnd();
391
- return `---\n${newFrontmatter}\n---\n${body}`;
392
- }
393
-
394
- /**
395
- * Install a custom agent launcher.
396
- * For skill_format platforms, produces <skillDir>/SKILL.md.
397
- * For flat platforms, produces a single file in target_dir.
398
- * @param {string} projectDir - Project directory
399
- * @param {string} agentName - Agent name (e.g., "fred-commit-poet")
400
- * @param {string} agentPath - Path to compiled agent (relative to project root)
401
- * @param {Object} metadata - Agent metadata
402
- * @returns {Object|null} Info about created file/skill
403
- */
404
- async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
405
- if (!this.installerConfig?.target_dir) return null;
406
-
407
- const { customAgentDashName } = require('./shared/path-utils');
408
- const targetPath = path.join(projectDir, this.installerConfig.target_dir);
409
- await this.ensureDir(targetPath);
410
-
411
- // Build artifact to reuse existing template rendering.
412
- // The default-agent template already includes the _bmad/ prefix before {{path}},
413
- // but agentPath is relative to project root (e.g. "_bmad/custom/agents/fred.md").
414
- // Strip the bmadFolderName prefix so the template doesn't produce a double path.
415
- const bmadPrefix = this.bmadFolderName + '/';
416
- const normalizedPath = agentPath.startsWith(bmadPrefix) ? agentPath.slice(bmadPrefix.length) : agentPath;
417
-
418
- const artifact = {
419
- type: 'agent-launcher',
420
- name: agentName,
421
- description: metadata?.description || `${agentName} agent`,
422
- agentPath: normalizedPath,
423
- relativePath: normalizedPath,
424
- module: 'custom',
425
- };
426
-
427
- const { content: template } = await this.loadTemplate(
428
- this.installerConfig.template_type || 'default',
429
- 'agent',
430
- this.installerConfig,
431
- 'default-agent',
432
- );
433
- const content = this.renderTemplate(template, artifact);
434
-
435
- if (this.installerConfig.skill_format) {
436
- const skillName = customAgentDashName(agentName).replace(/\.md$/, '');
437
- const skillDir = path.join(targetPath, skillName);
438
- await this.ensureDir(skillDir);
439
- const skillContent = this.transformToSkillFormat(content, skillName);
440
- const skillPath = path.join(skillDir, 'SKILL.md');
441
- await this.writeFile(skillPath, skillContent);
442
- return { path: path.relative(projectDir, skillPath), command: `$${skillName}` };
443
- }
444
-
445
- // Flat file output
446
- const filename = customAgentDashName(agentName);
447
- const filePath = path.join(targetPath, filename);
448
- await this.writeFile(filePath, content);
449
- return { path: path.relative(projectDir, filePath), command: agentName };
450
- }
451
-
452
- /**
453
- * Generate filename for artifact
454
- * @param {Object} artifact - Artifact data
455
- * @param {string} artifactType - Artifact type (agent, workflow, task, tool)
456
- * @param {string} extension - File extension to use (e.g., '.md', '.toml')
457
- * @returns {string} Generated filename
458
- */
459
- generateFilename(artifact, artifactType, extension = '.md') {
460
- const { resolveSkillName } = require('./shared/path-utils');
461
-
462
- // Reuse central logic to ensure consistent naming conventions
463
- // Prefers canonicalId from manifest when available, falls back to path-derived name
464
- const standardName = resolveSkillName(artifact);
465
-
466
- // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
467
- // This handles any extensions that might slip through toDashPath()
468
- const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md');
469
-
470
- // If using default markdown, preserve the bmad-agent- prefix for agents
471
- if (extension === '.md') {
472
- return baseName;
473
- }
474
-
475
- // For other extensions (e.g., .toml), replace .md extension
476
- // Note: agent prefix is preserved even with non-markdown extensions
477
- return baseName.replace(/\.md$/, extension);
478
- }
479
-
480
126
  /**
481
127
  * Install verbatim native SKILL.md directories from skill-manifest.csv.
482
128
  * Copies the entire source directory as-is into the IDE skill directory.
@@ -598,22 +244,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
598
244
  await this.cleanupRovoDevPrompts(projectDir, options);
599
245
  }
600
246
 
601
- // Clean all target directories
602
- if (this.installerConfig?.targets) {
603
- const parentDirs = new Set();
604
- for (const target of this.installerConfig.targets) {
605
- await this.cleanupTarget(projectDir, target.target_dir, options);
606
- // Track parent directories for empty-dir cleanup
607
- const parentDir = path.dirname(target.target_dir);
608
- if (parentDir && parentDir !== '.') {
609
- parentDirs.add(parentDir);
610
- }
611
- }
612
- // After all targets cleaned, remove empty parent directories (recursive up to projectDir)
613
- for (const parentDir of parentDirs) {
614
- await this.removeEmptyParents(projectDir, parentDir);
615
- }
616
- } else if (this.installerConfig?.target_dir) {
247
+ // Clean target directory
248
+ if (this.installerConfig?.target_dir) {
617
249
  await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
618
250
  }
619
251
  }
@@ -711,6 +343,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
711
343
  }
712
344
  }
713
345
  }
346
+
714
347
  /**
715
348
  * Strip BMAD-owned content from .github/copilot-instructions.md.
716
349
  * The old custom installer injected content between <!-- BMAD:START --> and <!-- BMAD:END --> markers.
@@ -1,5 +1,5 @@
1
1
  const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
2
- const prompts = require('../../../lib/prompts');
2
+ const prompts = require('../prompts');
3
3
 
4
4
  /**
5
5
  * IDE Manager - handles IDE-specific setup
@@ -226,23 +226,6 @@ class IdeManager {
226
226
  return results;
227
227
  }
228
228
 
229
- /**
230
- * Get list of supported IDEs
231
- * @returns {Array} List of supported IDE names
232
- */
233
- getSupportedIdes() {
234
- return [...this.handlers.keys()];
235
- }
236
-
237
- /**
238
- * Check if an IDE is supported
239
- * @param {string} ideName - Name of the IDE
240
- * @returns {boolean} True if IDE is supported
241
- */
242
- isSupported(ideName) {
243
- return this.handlers.has(ideName.toLowerCase());
244
- }
245
-
246
229
  /**
247
230
  * Detect installed IDEs
248
231
  * @param {string} projectDir - Project directory
@@ -259,41 +242,6 @@ class IdeManager {
259
242
 
260
243
  return detected;
261
244
  }
262
-
263
- /**
264
- * Install custom agent launchers for specified IDEs
265
- * @param {Array} ides - List of IDE names to install for
266
- * @param {string} projectDir - Project directory
267
- * @param {string} agentName - Agent name (e.g., "fred-commit-poet")
268
- * @param {string} agentPath - Path to compiled agent (relative to project root)
269
- * @param {Object} metadata - Agent metadata
270
- * @returns {Object} Results for each IDE
271
- */
272
- async installCustomAgentLaunchers(ides, projectDir, agentName, agentPath, metadata) {
273
- const results = {};
274
-
275
- for (const ideName of ides) {
276
- const handler = this.handlers.get(ideName.toLowerCase());
277
-
278
- if (!handler) {
279
- await prompts.log.warn(`IDE '${ideName}' is not yet supported for custom agent installation`);
280
- continue;
281
- }
282
-
283
- try {
284
- if (typeof handler.installCustomAgentLauncher === 'function') {
285
- const result = await handler.installCustomAgentLauncher(projectDir, agentName, agentPath, metadata);
286
- if (result) {
287
- results[ideName] = result;
288
- }
289
- }
290
- } catch (error) {
291
- await prompts.log.warn(`Failed to install ${ideName} launcher: ${error.message}`);
292
- }
293
- }
294
-
295
- return results;
296
- }
297
245
  }
298
246
 
299
247
  module.exports = { IdeManager };
@@ -0,0 +1,37 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('node:path');
3
+ const yaml = require('yaml');
4
+
5
+ const PLATFORM_CODES_PATH = path.join(__dirname, 'platform-codes.yaml');
6
+
7
+ let _cachedPlatformCodes = null;
8
+
9
+ /**
10
+ * Load the platform codes configuration from YAML
11
+ * @returns {Object} Platform codes configuration
12
+ */
13
+ async function loadPlatformCodes() {
14
+ if (_cachedPlatformCodes) {
15
+ return _cachedPlatformCodes;
16
+ }
17
+
18
+ if (!(await fs.pathExists(PLATFORM_CODES_PATH))) {
19
+ throw new Error(`Platform codes configuration not found at: ${PLATFORM_CODES_PATH}`);
20
+ }
21
+
22
+ const content = await fs.readFile(PLATFORM_CODES_PATH, 'utf8');
23
+ _cachedPlatformCodes = yaml.parse(content);
24
+ return _cachedPlatformCodes;
25
+ }
26
+
27
+ /**
28
+ * Clear the cached platform codes (useful for testing)
29
+ */
30
+ function clearCache() {
31
+ _cachedPlatformCodes = null;
32
+ }
33
+
34
+ module.exports = {
35
+ loadPlatformCodes,
36
+ clearCache,
37
+ };