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
@@ -1,657 +0,0 @@
1
- const path = require('node:path');
2
- const fs = require('fs-extra');
3
- const prompts = require('../../../lib/prompts');
4
- const { getSourcePath } = require('../../../lib/project-root');
5
- const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
6
-
7
- /**
8
- * Base class for IDE-specific setup
9
- * All IDE handlers should extend this class
10
- */
11
- class BaseIdeSetup {
12
- constructor(name, displayName = null, preferred = false) {
13
- this.name = name;
14
- this.displayName = displayName || name; // Human-readable name for UI
15
- this.preferred = preferred; // Whether this IDE should be shown in preferred list
16
- this.configDir = null; // Override in subclasses
17
- this.rulesDir = null; // Override in subclasses
18
- this.configFile = null; // Override in subclasses when detection is file-based
19
- this.detectionPaths = []; // Additional paths that indicate the IDE is configured
20
- this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
21
- }
22
-
23
- /**
24
- * Set the bmad folder name for placeholder replacement
25
- * @param {string} bmadFolderName - The bmad folder name
26
- */
27
- setBmadFolderName(bmadFolderName) {
28
- this.bmadFolderName = bmadFolderName;
29
- }
30
-
31
- /**
32
- * Main setup method - must be implemented by subclasses
33
- * @param {string} projectDir - Project directory
34
- * @param {string} bmadDir - BMAD installation directory
35
- * @param {Object} options - Setup options
36
- */
37
- async setup(projectDir, bmadDir, options = {}) {
38
- throw new Error(`setup() must be implemented by ${this.name} handler`);
39
- }
40
-
41
- /**
42
- * Cleanup IDE configuration
43
- * @param {string} projectDir - Project directory
44
- */
45
- async cleanup(projectDir, options = {}) {
46
- // Default implementation - can be overridden
47
- if (this.configDir) {
48
- const configPath = path.join(projectDir, this.configDir);
49
- if (await fs.pathExists(configPath)) {
50
- const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME);
51
- if (await fs.pathExists(bmadRulesPath)) {
52
- await fs.remove(bmadRulesPath);
53
- if (!options.silent) await prompts.log.message(`Removed ${this.name} BMAD configuration`);
54
- }
55
- }
56
- }
57
- }
58
-
59
- /**
60
- * Install a custom agent launcher - subclasses should override
61
- * @param {string} projectDir - Project directory
62
- * @param {string} agentName - Agent name (e.g., "fred-commit-poet")
63
- * @param {string} agentPath - Path to compiled agent (relative to project root)
64
- * @param {Object} metadata - Agent metadata
65
- * @returns {Object|null} Info about created command, or null if not supported
66
- */
67
- async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
68
- // Default implementation - subclasses can override
69
- return null;
70
- }
71
-
72
- /**
73
- * Detect whether this IDE already has configuration in the project
74
- * Subclasses can override for custom logic
75
- * @param {string} projectDir - Project directory
76
- * @returns {boolean}
77
- */
78
- async detect(projectDir) {
79
- const pathsToCheck = [];
80
-
81
- if (this.configDir) {
82
- pathsToCheck.push(path.join(projectDir, this.configDir));
83
- }
84
-
85
- if (this.configFile) {
86
- pathsToCheck.push(path.join(projectDir, this.configFile));
87
- }
88
-
89
- if (Array.isArray(this.detectionPaths)) {
90
- for (const candidate of this.detectionPaths) {
91
- if (!candidate) continue;
92
- const resolved = path.isAbsolute(candidate) ? candidate : path.join(projectDir, candidate);
93
- pathsToCheck.push(resolved);
94
- }
95
- }
96
-
97
- for (const candidate of pathsToCheck) {
98
- if (await fs.pathExists(candidate)) {
99
- return true;
100
- }
101
- }
102
-
103
- return false;
104
- }
105
-
106
- /**
107
- * Get list of agents from BMAD installation
108
- * @param {string} bmadDir - BMAD installation directory
109
- * @returns {Array} List of agent files
110
- */
111
- async getAgents(bmadDir) {
112
- const agents = [];
113
-
114
- // Get core agents
115
- const coreAgentsPath = path.join(bmadDir, 'core', 'agents');
116
- if (await fs.pathExists(coreAgentsPath)) {
117
- const coreAgents = await this.scanDirectory(coreAgentsPath, '.md');
118
- agents.push(
119
- ...coreAgents.map((a) => ({
120
- ...a,
121
- module: 'core',
122
- })),
123
- );
124
- }
125
-
126
- // Get module agents
127
- const entries = await fs.readdir(bmadDir, { withFileTypes: true });
128
- for (const entry of entries) {
129
- if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config' && entry.name !== 'agents') {
130
- const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
131
- if (await fs.pathExists(moduleAgentsPath)) {
132
- const moduleAgents = await this.scanDirectory(moduleAgentsPath, '.md');
133
- agents.push(
134
- ...moduleAgents.map((a) => ({
135
- ...a,
136
- module: entry.name,
137
- })),
138
- );
139
- }
140
- }
141
- }
142
-
143
- // Get standalone agents from bmad/agents/ directory
144
- const standaloneAgentsDir = path.join(bmadDir, 'agents');
145
- if (await fs.pathExists(standaloneAgentsDir)) {
146
- const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true });
147
-
148
- for (const agentDir of agentDirs) {
149
- if (!agentDir.isDirectory()) continue;
150
-
151
- const agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
152
- const agentFiles = await fs.readdir(agentDirPath);
153
-
154
- for (const file of agentFiles) {
155
- if (!file.endsWith('.md')) continue;
156
- if (file.includes('.customize.')) continue;
157
-
158
- const filePath = path.join(agentDirPath, file);
159
- const content = await fs.readFile(filePath, 'utf8');
160
-
161
- if (content.includes('localskip="true"')) continue;
162
-
163
- agents.push({
164
- name: file.replace('.md', ''),
165
- path: filePath,
166
- relativePath: path.relative(standaloneAgentsDir, filePath),
167
- filename: file,
168
- module: 'standalone', // Mark as standalone agent
169
- });
170
- }
171
- }
172
- }
173
-
174
- return agents;
175
- }
176
-
177
- /**
178
- * Get list of tasks from BMAD installation
179
- * @param {string} bmadDir - BMAD installation directory
180
- * @param {boolean} standaloneOnly - If true, only return standalone tasks
181
- * @returns {Array} List of task files
182
- */
183
- async getTasks(bmadDir, standaloneOnly = false) {
184
- const tasks = [];
185
-
186
- // Get core tasks (scan for both .md and .xml)
187
- const coreTasksPath = path.join(bmadDir, 'core', 'tasks');
188
- if (await fs.pathExists(coreTasksPath)) {
189
- const coreTasks = await this.scanDirectoryWithStandalone(coreTasksPath, ['.md', '.xml']);
190
- tasks.push(
191
- ...coreTasks.map((t) => ({
192
- ...t,
193
- module: 'core',
194
- })),
195
- );
196
- }
197
-
198
- // Get module tasks
199
- const entries = await fs.readdir(bmadDir, { withFileTypes: true });
200
- for (const entry of entries) {
201
- if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config' && entry.name !== 'agents') {
202
- const moduleTasksPath = path.join(bmadDir, entry.name, 'tasks');
203
- if (await fs.pathExists(moduleTasksPath)) {
204
- const moduleTasks = await this.scanDirectoryWithStandalone(moduleTasksPath, ['.md', '.xml']);
205
- tasks.push(
206
- ...moduleTasks.map((t) => ({
207
- ...t,
208
- module: entry.name,
209
- })),
210
- );
211
- }
212
- }
213
- }
214
-
215
- // Filter by standalone if requested
216
- if (standaloneOnly) {
217
- return tasks.filter((t) => t.standalone === true);
218
- }
219
-
220
- return tasks;
221
- }
222
-
223
- /**
224
- * Get list of tools from BMAD installation
225
- * @param {string} bmadDir - BMAD installation directory
226
- * @param {boolean} standaloneOnly - If true, only return standalone tools
227
- * @returns {Array} List of tool files
228
- */
229
- async getTools(bmadDir, standaloneOnly = false) {
230
- const tools = [];
231
-
232
- // Get core tools (scan for both .md and .xml)
233
- const coreToolsPath = path.join(bmadDir, 'core', 'tools');
234
- if (await fs.pathExists(coreToolsPath)) {
235
- const coreTools = await this.scanDirectoryWithStandalone(coreToolsPath, ['.md', '.xml']);
236
- tools.push(
237
- ...coreTools.map((t) => ({
238
- ...t,
239
- module: 'core',
240
- })),
241
- );
242
- }
243
-
244
- // Get module tools
245
- const entries = await fs.readdir(bmadDir, { withFileTypes: true });
246
- for (const entry of entries) {
247
- if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config' && entry.name !== 'agents') {
248
- const moduleToolsPath = path.join(bmadDir, entry.name, 'tools');
249
- if (await fs.pathExists(moduleToolsPath)) {
250
- const moduleTools = await this.scanDirectoryWithStandalone(moduleToolsPath, ['.md', '.xml']);
251
- tools.push(
252
- ...moduleTools.map((t) => ({
253
- ...t,
254
- module: entry.name,
255
- })),
256
- );
257
- }
258
- }
259
- }
260
-
261
- // Filter by standalone if requested
262
- if (standaloneOnly) {
263
- return tools.filter((t) => t.standalone === true);
264
- }
265
-
266
- return tools;
267
- }
268
-
269
- /**
270
- * Get list of workflows from BMAD installation
271
- * @param {string} bmadDir - BMAD installation directory
272
- * @param {boolean} standaloneOnly - If true, only return standalone workflows
273
- * @returns {Array} List of workflow files
274
- */
275
- async getWorkflows(bmadDir, standaloneOnly = false) {
276
- const workflows = [];
277
-
278
- // Get core workflows
279
- const coreWorkflowsPath = path.join(bmadDir, 'core', 'workflows');
280
- if (await fs.pathExists(coreWorkflowsPath)) {
281
- const coreWorkflows = await this.findWorkflowFiles(coreWorkflowsPath);
282
- workflows.push(
283
- ...coreWorkflows.map((w) => ({
284
- ...w,
285
- module: 'core',
286
- })),
287
- );
288
- }
289
-
290
- // Get module workflows
291
- const entries = await fs.readdir(bmadDir, { withFileTypes: true });
292
- for (const entry of entries) {
293
- if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config' && entry.name !== 'agents') {
294
- const moduleWorkflowsPath = path.join(bmadDir, entry.name, 'workflows');
295
- if (await fs.pathExists(moduleWorkflowsPath)) {
296
- const moduleWorkflows = await this.findWorkflowFiles(moduleWorkflowsPath);
297
- workflows.push(
298
- ...moduleWorkflows.map((w) => ({
299
- ...w,
300
- module: entry.name,
301
- })),
302
- );
303
- }
304
- }
305
- }
306
-
307
- // Filter by standalone if requested
308
- if (standaloneOnly) {
309
- return workflows.filter((w) => w.standalone === true);
310
- }
311
-
312
- return workflows;
313
- }
314
-
315
- /**
316
- * Recursively find workflow.md files
317
- * @param {string} dir - Directory to search
318
- * @param {string} [rootDir] - Original root directory (used internally for recursion)
319
- * @returns {Array} List of workflow file info objects
320
- */
321
- async findWorkflowFiles(dir, rootDir = null) {
322
- rootDir = rootDir || dir;
323
- const workflows = [];
324
-
325
- if (!(await fs.pathExists(dir))) {
326
- return workflows;
327
- }
328
-
329
- const entries = await fs.readdir(dir, { withFileTypes: true });
330
-
331
- for (const entry of entries) {
332
- const fullPath = path.join(dir, entry.name);
333
-
334
- if (entry.isDirectory()) {
335
- // Recursively search subdirectories
336
- const subWorkflows = await this.findWorkflowFiles(fullPath, rootDir);
337
- workflows.push(...subWorkflows);
338
- } else if (entry.isFile() && entry.name === 'workflow.md') {
339
- // Read workflow.md frontmatter to get name and standalone property
340
- try {
341
- const content = await fs.readFile(fullPath, 'utf8');
342
- const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
343
- if (!frontmatterMatch) continue;
344
-
345
- const workflowData = yaml.parse(frontmatterMatch[1]);
346
-
347
- if (workflowData && workflowData.name) {
348
- // Workflows are standalone by default unless explicitly false
349
- const standalone = workflowData.standalone !== false && workflowData.standalone !== 'false';
350
- workflows.push({
351
- name: workflowData.name,
352
- path: fullPath,
353
- relativePath: path.relative(rootDir, fullPath),
354
- filename: entry.name,
355
- description: workflowData.description || '',
356
- standalone: standalone,
357
- });
358
- }
359
- } catch {
360
- // Skip invalid workflow files
361
- }
362
- }
363
- }
364
-
365
- return workflows;
366
- }
367
-
368
- /**
369
- * Scan a directory for files with specific extension(s)
370
- * @param {string} dir - Directory to scan
371
- * @param {string|Array<string>} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml'])
372
- * @param {string} [rootDir] - Original root directory (used internally for recursion)
373
- * @returns {Array} List of file info objects
374
- */
375
- async scanDirectory(dir, ext, rootDir = null) {
376
- rootDir = rootDir || dir;
377
- const files = [];
378
-
379
- if (!(await fs.pathExists(dir))) {
380
- return files;
381
- }
382
-
383
- // Normalize ext to array
384
- const extensions = Array.isArray(ext) ? ext : [ext];
385
-
386
- const entries = await fs.readdir(dir, { withFileTypes: true });
387
-
388
- for (const entry of entries) {
389
- const fullPath = path.join(dir, entry.name);
390
-
391
- if (entry.isDirectory()) {
392
- // Recursively scan subdirectories
393
- const subFiles = await this.scanDirectory(fullPath, ext, rootDir);
394
- files.push(...subFiles);
395
- } else if (entry.isFile()) {
396
- // Check if file matches any of the extensions
397
- const matchedExt = extensions.find((e) => entry.name.endsWith(e));
398
- if (matchedExt) {
399
- files.push({
400
- name: path.basename(entry.name, matchedExt),
401
- path: fullPath,
402
- relativePath: path.relative(rootDir, fullPath),
403
- filename: entry.name,
404
- });
405
- }
406
- }
407
- }
408
-
409
- return files;
410
- }
411
-
412
- /**
413
- * Scan a directory for files with specific extension(s) and check standalone attribute
414
- * @param {string} dir - Directory to scan
415
- * @param {string|Array<string>} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml'])
416
- * @param {string} [rootDir] - Original root directory (used internally for recursion)
417
- * @returns {Array} List of file info objects with standalone property
418
- */
419
- async scanDirectoryWithStandalone(dir, ext, rootDir = null) {
420
- rootDir = rootDir || dir;
421
- const files = [];
422
-
423
- if (!(await fs.pathExists(dir))) {
424
- return files;
425
- }
426
-
427
- // Normalize ext to array
428
- const extensions = Array.isArray(ext) ? ext : [ext];
429
-
430
- const entries = await fs.readdir(dir, { withFileTypes: true });
431
-
432
- for (const entry of entries) {
433
- const fullPath = path.join(dir, entry.name);
434
-
435
- if (entry.isDirectory()) {
436
- // Recursively scan subdirectories
437
- const subFiles = await this.scanDirectoryWithStandalone(fullPath, ext, rootDir);
438
- files.push(...subFiles);
439
- } else if (entry.isFile()) {
440
- // Check if file matches any of the extensions
441
- const matchedExt = extensions.find((e) => entry.name.endsWith(e));
442
- if (matchedExt) {
443
- // Read file content to check for standalone attribute
444
- // All non-internal files are considered standalone by default
445
- let standalone = true;
446
- try {
447
- const content = await fs.readFile(fullPath, 'utf8');
448
-
449
- // Skip internal/engine files (not user-facing)
450
- if (content.includes('internal="true"')) {
451
- continue;
452
- }
453
-
454
- // Check for explicit standalone: false
455
- if (entry.name.endsWith('.xml')) {
456
- // For XML files, check for standalone="false" attribute
457
- const tagMatch = content.match(/<(task|tool)[^>]*standalone="false"/);
458
- standalone = !tagMatch;
459
- } else if (entry.name.endsWith('.md')) {
460
- // For MD files, parse YAML frontmatter
461
- const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
462
- if (frontmatterMatch) {
463
- try {
464
- const yaml = require('yaml');
465
- const frontmatter = yaml.parse(frontmatterMatch[1]);
466
- standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false';
467
- } catch {
468
- // If YAML parsing fails, default to standalone
469
- }
470
- }
471
- // No frontmatter means standalone (default)
472
- }
473
- } catch {
474
- // If we can't read the file, default to standalone
475
- standalone = true;
476
- }
477
-
478
- files.push({
479
- name: path.basename(entry.name, matchedExt),
480
- path: fullPath,
481
- relativePath: path.relative(rootDir, fullPath),
482
- filename: entry.name,
483
- standalone: standalone,
484
- });
485
- }
486
- }
487
- }
488
-
489
- return files;
490
- }
491
-
492
- /**
493
- * Create IDE command/rule file from agent or task
494
- * @param {string} content - File content
495
- * @param {Object} metadata - File metadata
496
- * @param {string} projectDir - The actual project directory path
497
- * @returns {string} Processed content
498
- */
499
- processContent(content, metadata = {}, projectDir = null) {
500
- // Replace placeholders
501
- let processed = content;
502
-
503
- // Only replace {project-root} if a specific projectDir is provided
504
- // Otherwise leave the placeholder intact
505
- // Note: Don't add trailing slash - paths in source include leading slash
506
- if (projectDir) {
507
- processed = processed.replaceAll('{project-root}', projectDir);
508
- }
509
- processed = processed.replaceAll('{module}', metadata.module || 'core');
510
- processed = processed.replaceAll('{agent}', metadata.name || '');
511
- processed = processed.replaceAll('{task}', metadata.name || '');
512
-
513
- return processed;
514
- }
515
-
516
- /**
517
- * Ensure directory exists
518
- * @param {string} dirPath - Directory path
519
- */
520
- async ensureDir(dirPath) {
521
- await fs.ensureDir(dirPath);
522
- }
523
-
524
- /**
525
- * Write file with content (replaces _bmad placeholder)
526
- * @param {string} filePath - File path
527
- * @param {string} content - File content
528
- */
529
- async writeFile(filePath, content) {
530
- // Replace _bmad placeholder if present
531
- if (typeof content === 'string' && content.includes('_bmad')) {
532
- content = content.replaceAll('_bmad', this.bmadFolderName);
533
- }
534
-
535
- // Replace escape sequence _bmad with literal _bmad
536
- if (typeof content === 'string' && content.includes('_bmad')) {
537
- content = content.replaceAll('_bmad', '_bmad');
538
- }
539
- await this.ensureDir(path.dirname(filePath));
540
- await fs.writeFile(filePath, content, 'utf8');
541
- }
542
-
543
- /**
544
- * Copy file from source to destination (replaces _bmad placeholder in text files)
545
- * @param {string} source - Source file path
546
- * @param {string} dest - Destination file path
547
- */
548
- async copyFile(source, dest) {
549
- // List of text file extensions that should have placeholder replacement
550
- const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv'];
551
- const ext = path.extname(source).toLowerCase();
552
-
553
- await this.ensureDir(path.dirname(dest));
554
-
555
- // Check if this is a text file that might contain placeholders
556
- if (textExtensions.includes(ext)) {
557
- try {
558
- // Read the file content
559
- let content = await fs.readFile(source, 'utf8');
560
-
561
- // Replace _bmad placeholder with actual folder name
562
- if (content.includes('_bmad')) {
563
- content = content.replaceAll('_bmad', this.bmadFolderName);
564
- }
565
-
566
- // Replace escape sequence _bmad with literal _bmad
567
- if (content.includes('_bmad')) {
568
- content = content.replaceAll('_bmad', '_bmad');
569
- }
570
-
571
- // Write to dest with replaced content
572
- await fs.writeFile(dest, content, 'utf8');
573
- } catch {
574
- // If reading as text fails, fall back to regular copy
575
- await fs.copy(source, dest, { overwrite: true });
576
- }
577
- } else {
578
- // Binary file or other file type - just copy directly
579
- await fs.copy(source, dest, { overwrite: true });
580
- }
581
- }
582
-
583
- /**
584
- * Check if path exists
585
- * @param {string} pathToCheck - Path to check
586
- * @returns {boolean} True if path exists
587
- */
588
- async exists(pathToCheck) {
589
- return await fs.pathExists(pathToCheck);
590
- }
591
-
592
- /**
593
- * Alias for exists method
594
- * @param {string} pathToCheck - Path to check
595
- * @returns {boolean} True if path exists
596
- */
597
- async pathExists(pathToCheck) {
598
- return await fs.pathExists(pathToCheck);
599
- }
600
-
601
- /**
602
- * Read file content
603
- * @param {string} filePath - File path
604
- * @returns {string} File content
605
- */
606
- async readFile(filePath) {
607
- return await fs.readFile(filePath, 'utf8');
608
- }
609
-
610
- /**
611
- * Format name as title
612
- * @param {string} name - Name to format
613
- * @returns {string} Formatted title
614
- */
615
- formatTitle(name) {
616
- return name
617
- .split('-')
618
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
619
- .join(' ');
620
- }
621
-
622
- /**
623
- * Flatten a relative path to a single filename for flat slash command naming
624
- * @deprecated Use toColonPath() or toDashPath() from shared/path-utils.js instead
625
- * Example: 'module/agents/name.md' -> 'bmad-module-agents-name.md'
626
- * Used by IDEs that ignore directory structure for slash commands (e.g., Antigravity, Codex)
627
- * @param {string} relativePath - Relative path to flatten
628
- * @returns {string} Flattened filename with 'bmad-' prefix
629
- */
630
- flattenFilename(relativePath) {
631
- const sanitized = relativePath.replaceAll(/[/\\]/g, '-');
632
- return `bmad-${sanitized}`;
633
- }
634
-
635
- /**
636
- * Create agent configuration file
637
- * @param {string} bmadDir - BMAD installation directory
638
- * @param {Object} agent - Agent information
639
- */
640
- async createAgentConfig(bmadDir, agent) {
641
- const agentConfigDir = path.join(bmadDir, '_config', 'agents');
642
- await this.ensureDir(agentConfigDir);
643
-
644
- // Load agent config template
645
- const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
646
- const templateContent = await this.readFile(templatePath);
647
-
648
- const configContent = `# Agent Config: ${agent.name}
649
-
650
- ${templateContent}`;
651
-
652
- const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
653
- await this.writeFile(configPath, configContent);
654
- }
655
- }
656
-
657
- module.exports = { BaseIdeSetup };