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.
- package/.claude-plugin/marketplace.json +78 -0
- package/package.json +8 -8
- package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
- package/src/core-skills/bmad-init/scripts/bmad_init.py +35 -4
- package/src/core-skills/bmad-init/scripts/tests/test_bmad_init.py +64 -0
- package/tools/{cli → installer}/bmad-cli.js +3 -1
- package/tools/{cli/lib → installer}/cli-utils.js +3 -4
- package/tools/{cli → installer}/commands/install.js +3 -3
- package/tools/{cli → installer}/commands/status.js +4 -4
- package/tools/{cli → installer}/commands/uninstall.js +5 -5
- package/tools/installer/core/config.js +52 -0
- package/tools/{cli/installers/lib → installer}/core/custom-module-cache.js +1 -1
- package/tools/installer/core/existing-install.js +127 -0
- package/tools/installer/core/install-paths.js +129 -0
- package/tools/installer/core/installer.js +1790 -0
- package/tools/{cli/installers/lib → installer}/core/manifest-generator.js +3 -3
- package/tools/{cli/installers/lib → installer}/core/manifest.js +2 -2
- package/tools/{cli/installers/lib/custom/handler.js → installer/custom-handler.js} +1 -1
- package/tools/{cli/installers/lib → installer}/ide/_config-driven.js +30 -397
- package/tools/{cli/installers/lib → installer}/ide/manager.js +1 -53
- package/tools/installer/ide/platform-codes.js +37 -0
- package/tools/installer/ide/platform-codes.yaml +190 -0
- package/tools/{cli/installers/lib → installer}/ide/shared/module-injections.js +1 -1
- package/tools/{cli/installers/lib → installer}/message-loader.js +2 -2
- package/tools/installer/modules/custom-modules.js +197 -0
- package/tools/installer/modules/external-manager.js +323 -0
- package/tools/{cli/installers/lib/core/config-collector.js → installer/modules/official-modules.js} +714 -43
- package/tools/{cli/lib → installer}/ui.js +65 -299
- package/tools/javascript-conventions.md +5 -0
- package/tools/bmad-npx-wrapper.js +0 -38
- package/tools/cli/installers/lib/core/dependency-resolver.js +0 -743
- package/tools/cli/installers/lib/core/detector.js +0 -223
- package/tools/cli/installers/lib/core/ide-config-manager.js +0 -157
- package/tools/cli/installers/lib/core/installer.js +0 -3002
- package/tools/cli/installers/lib/ide/_base-ide.js +0 -657
- package/tools/cli/installers/lib/ide/platform-codes.js +0 -100
- package/tools/cli/installers/lib/ide/platform-codes.yaml +0 -341
- package/tools/cli/installers/lib/modules/external-manager.js +0 -136
- package/tools/cli/installers/lib/modules/manager.js +0 -928
- package/tools/cli/lib/config.js +0 -213
- package/tools/cli/lib/platform-codes.js +0 -116
- package/tools/lib/xml-utils.js +0 -13
- /package/tools/{cli → installer}/README.md +0 -0
- /package/tools/{cli → installer}/external-official-modules.yaml +0 -0
- /package/tools/{cli/lib → installer}/file-ops.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/agent-command-generator.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/bmad-artifacts.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/path-utils.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/skill-manifest.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/agent-command-template.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/antigravity.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-agent.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-task.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-tool.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow-yaml.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow-yaml.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/rovodev.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/trae.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/windsurf-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/split/.gitkeep +0 -0
- /package/tools/{cli/installers → installer}/install-messages.yaml +0 -0
- /package/tools/{cli/lib → installer}/project-root.js +0 -0
- /package/tools/{cli/lib → installer}/prompts.js +0 -0
- /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('
|
|
7
|
-
const prompts = require('
|
|
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('
|
|
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('
|
|
5
|
-
const prompts = require('
|
|
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('
|
|
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
|
|
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
|
-
* -
|
|
19
|
-
* -
|
|
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
|
|
21
|
+
class ConfigDrivenIdeSetup {
|
|
23
22
|
constructor(platformCode, platformConfig) {
|
|
24
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
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.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
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
|
|
602
|
-
if (this.installerConfig?.
|
|
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('
|
|
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
|
+
};
|