bmad-method 6.3.1-next.3 → 6.3.1-next.4

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.3.1-next.3",
4
+ "version": "6.3.1-next.4",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -1,20 +1,6 @@
1
- const path = require('node:path');
2
- const os = require('node:os');
3
1
  const prompts = require('./prompts');
4
2
 
5
3
  const CLIUtils = {
6
- /**
7
- * Get version from package.json
8
- */
9
- getVersion() {
10
- try {
11
- const packageJson = require(path.join(__dirname, '..', '..', 'package.json'));
12
- return packageJson.version || 'Unknown';
13
- } catch {
14
- return 'Unknown';
15
- }
16
- },
17
-
18
4
  /**
19
5
  * Display BMAD logo and version using @clack intro + box
20
6
  */
@@ -52,37 +38,6 @@ const CLIUtils = {
52
38
  });
53
39
  },
54
40
 
55
- /**
56
- * Display section header
57
- * @param {string} title - Section title
58
- * @param {string} subtitle - Optional subtitle
59
- */
60
- async displaySection(title, subtitle = null) {
61
- await prompts.note(subtitle || '', title);
62
- },
63
-
64
- /**
65
- * Display info box
66
- * @param {string|Array} content - Content to display
67
- * @param {Object} options - Box options
68
- */
69
- async displayBox(content, options = {}) {
70
- let text = content;
71
- if (Array.isArray(content)) {
72
- text = content.join('\n\n');
73
- }
74
-
75
- const color = await prompts.getColor();
76
- const borderColor = options.borderColor || 'cyan';
77
- const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
78
- const formatBorder = colorMap[borderColor] || color.cyan;
79
-
80
- await prompts.box(text, options.title, {
81
- rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
82
- formatBorder,
83
- });
84
- },
85
-
86
41
  /**
87
42
  * Display module configuration header
88
43
  * @param {string} moduleName - Module name (fallback if no custom header)
@@ -93,98 +48,6 @@ const CLIUtils = {
93
48
  const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
94
49
  await prompts.note(subheader || '', title);
95
50
  },
96
-
97
- /**
98
- * Display module with no custom configuration
99
- * @param {string} moduleName - Module name (fallback if no custom header)
100
- * @param {string} header - Custom header from module.yaml
101
- * @param {string} subheader - Custom subheader from module.yaml
102
- */
103
- async displayModuleNoConfig(moduleName, header = null, subheader = null) {
104
- const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`;
105
- await prompts.note(subheader || '', title);
106
- },
107
-
108
- /**
109
- * Display step indicator
110
- * @param {number} current - Current step
111
- * @param {number} total - Total steps
112
- * @param {string} description - Step description
113
- */
114
- async displayStep(current, total, description) {
115
- const progress = `[${current}/${total}]`;
116
- await prompts.log.step(`${progress} ${description}`);
117
- },
118
-
119
- /**
120
- * Display completion message
121
- * @param {string} message - Completion message
122
- */
123
- async displayComplete(message) {
124
- const color = await prompts.getColor();
125
- await prompts.box(`\u2728 ${message}`, 'Complete', {
126
- rounded: true,
127
- formatBorder: color.green,
128
- });
129
- },
130
-
131
- /**
132
- * Display error message
133
- * @param {string} message - Error message
134
- */
135
- async displayError(message) {
136
- const color = await prompts.getColor();
137
- await prompts.box(`\u2717 ${message}`, 'Error', {
138
- rounded: true,
139
- formatBorder: color.red,
140
- });
141
- },
142
-
143
- /**
144
- * Format list for display
145
- * @param {Array} items - Items to display
146
- * @param {string} prefix - Item prefix
147
- */
148
- formatList(items, prefix = '\u2022') {
149
- return items.map((item) => ` ${prefix} ${item}`).join('\n');
150
- },
151
-
152
- /**
153
- * Clear previous lines
154
- * @param {number} lines - Number of lines to clear
155
- */
156
- clearLines(lines) {
157
- for (let i = 0; i < lines; i++) {
158
- process.stdout.moveCursor(0, -1);
159
- process.stdout.clearLine(1);
160
- }
161
- },
162
-
163
- /**
164
- * Display module completion message
165
- * @param {string} moduleName - Name of the completed module
166
- * @param {boolean} clearScreen - Whether to clear the screen first (deprecated, always false now)
167
- */
168
- displayModuleComplete(moduleName, clearScreen = false) {
169
- // No longer clear screen or show boxes - just a simple completion message
170
- // This is deprecated but kept for backwards compatibility
171
- },
172
-
173
- /**
174
- * Expand path with ~ expansion
175
- * @param {string} inputPath - Path to expand
176
- * @returns {string} Expanded path
177
- */
178
- expandPath(inputPath) {
179
- if (!inputPath) return inputPath;
180
-
181
- // Expand ~ to home directory
182
- if (inputPath.startsWith('~')) {
183
- return path.join(os.homedir(), inputPath.slice(1));
184
- }
185
-
186
- return inputPath;
187
- },
188
51
  };
189
52
 
190
53
  module.exports = { CLIUtils };
@@ -107,117 +107,6 @@ class Manifest {
107
107
  return null;
108
108
  }
109
109
 
110
- /**
111
- * Update existing manifest
112
- * @param {string} bmadDir - Path to bmad directory
113
- * @param {Object} updates - Fields to update
114
- * @param {Array} installedFiles - Updated list of installed files
115
- */
116
- async update(bmadDir, updates, installedFiles = null) {
117
- const yaml = require('yaml');
118
- const manifest = (await this._readRaw(bmadDir)) || {
119
- installation: {},
120
- modules: [],
121
- ides: [],
122
- };
123
-
124
- // Handle module updates
125
- if (updates.modules) {
126
- // If modules is being updated, we need to preserve detailed module info
127
- const existingDetailed = manifest.modules || [];
128
- const incomingNames = updates.modules;
129
-
130
- // Build updated modules array
131
- const updatedModules = [];
132
- for (const name of incomingNames) {
133
- const existing = existingDetailed.find((m) => m.name === name);
134
- if (existing) {
135
- // Preserve existing details, update lastUpdated if this module is being updated
136
- updatedModules.push({
137
- ...existing,
138
- lastUpdated: new Date().toISOString(),
139
- });
140
- } else {
141
- // New module - add with minimal details
142
- updatedModules.push({
143
- name,
144
- version: null,
145
- installDate: new Date().toISOString(),
146
- lastUpdated: new Date().toISOString(),
147
- source: 'unknown',
148
- });
149
- }
150
- }
151
-
152
- manifest.modules = updatedModules;
153
- }
154
-
155
- // Merge other updates
156
- if (updates.version) {
157
- manifest.installation.version = updates.version;
158
- }
159
- if (updates.installDate) {
160
- manifest.installation.installDate = updates.installDate;
161
- }
162
- manifest.installation.lastUpdated = new Date().toISOString();
163
-
164
- if (updates.ides) {
165
- manifest.ides = updates.ides;
166
- }
167
-
168
- // Handle per-module version updates
169
- if (updates.moduleVersions) {
170
- for (const [moduleName, versionInfo] of Object.entries(updates.moduleVersions)) {
171
- const moduleIndex = manifest.modules.findIndex((m) => m.name === moduleName);
172
- if (moduleIndex !== -1) {
173
- manifest.modules[moduleIndex] = {
174
- ...manifest.modules[moduleIndex],
175
- ...versionInfo,
176
- lastUpdated: new Date().toISOString(),
177
- };
178
- }
179
- }
180
- }
181
-
182
- // Handle adding a new module with version info
183
- if (updates.addModule) {
184
- const { name, version, source, npmPackage, repoUrl, localPath } = updates.addModule;
185
- const existing = manifest.modules.find((m) => m.name === name);
186
- if (!existing) {
187
- const entry = {
188
- name,
189
- version: version || null,
190
- installDate: new Date().toISOString(),
191
- lastUpdated: new Date().toISOString(),
192
- source: source || 'external',
193
- npmPackage: npmPackage || null,
194
- repoUrl: repoUrl || null,
195
- };
196
- if (localPath) entry.localPath = localPath;
197
- manifest.modules.push(entry);
198
- }
199
- }
200
-
201
- const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
202
- await fs.ensureDir(path.dirname(manifestPath));
203
-
204
- // Clean the manifest data to remove any non-serializable values
205
- const cleanManifestData = structuredClone(manifest);
206
-
207
- const yamlContent = yaml.stringify(cleanManifestData, {
208
- indent: 2,
209
- lineWidth: 0,
210
- sortKeys: false,
211
- });
212
-
213
- // Ensure POSIX-compliant final newline
214
- const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n';
215
- await fs.writeFile(manifestPath, content, 'utf8');
216
-
217
- // Return the flattened format for compatibility
218
- return this._flattenManifest(manifest);
219
- }
220
-
221
110
  /**
222
111
  * Read raw manifest data without flattening
223
112
  * @param {string} bmadDir - Path to bmad directory
@@ -310,62 +199,6 @@ class Manifest {
310
199
  await this._writeRaw(bmadDir, manifest);
311
200
  }
312
201
 
313
- /**
314
- * Remove a module from the manifest
315
- * @param {string} bmadDir - Path to bmad directory
316
- * @param {string} moduleName - Module name to remove
317
- */
318
- async removeModule(bmadDir, moduleName) {
319
- const manifest = await this._readRaw(bmadDir);
320
- if (!manifest || !manifest.modules) {
321
- return;
322
- }
323
-
324
- const index = manifest.modules.findIndex((m) => m.name === moduleName);
325
- if (index !== -1) {
326
- manifest.modules.splice(index, 1);
327
- await this._writeRaw(bmadDir, manifest);
328
- }
329
- }
330
-
331
- /**
332
- * Update a single module's version info
333
- * @param {string} bmadDir - Path to bmad directory
334
- * @param {string} moduleName - Module name
335
- * @param {Object} versionInfo - Version info to update
336
- */
337
- async updateModuleVersion(bmadDir, moduleName, versionInfo) {
338
- const manifest = await this._readRaw(bmadDir);
339
- if (!manifest || !manifest.modules) {
340
- return;
341
- }
342
-
343
- const index = manifest.modules.findIndex((m) => m.name === moduleName);
344
- if (index !== -1) {
345
- manifest.modules[index] = {
346
- ...manifest.modules[index],
347
- ...versionInfo,
348
- lastUpdated: new Date().toISOString(),
349
- };
350
- await this._writeRaw(bmadDir, manifest);
351
- }
352
- }
353
-
354
- /**
355
- * Get version info for a specific module
356
- * @param {string} bmadDir - Path to bmad directory
357
- * @param {string} moduleName - Module name
358
- * @returns {Object|null} Module version info or null
359
- */
360
- async getModuleVersion(bmadDir, moduleName) {
361
- const manifest = await this._readRaw(bmadDir);
362
- if (!manifest || !manifest.modules) {
363
- return null;
364
- }
365
-
366
- return manifest.modules.find((m) => m.name === moduleName) || null;
367
- }
368
-
369
202
  /**
370
203
  * Get all modules with their version info
371
204
  * @param {string} bmadDir - Path to bmad directory
@@ -403,27 +236,6 @@ class Manifest {
403
236
  await fs.writeFile(manifestPath, content, 'utf8');
404
237
  }
405
238
 
406
- /**
407
- * Add an IDE configuration to the manifest
408
- * @param {string} bmadDir - Path to bmad directory
409
- * @param {string} ideName - IDE name to add
410
- */
411
- async addIde(bmadDir, ideName) {
412
- const manifest = await this.read(bmadDir);
413
- if (!manifest) {
414
- throw new Error('No manifest found');
415
- }
416
-
417
- if (!manifest.ides) {
418
- manifest.ides = [];
419
- }
420
-
421
- if (!manifest.ides.includes(ideName)) {
422
- manifest.ides.push(ideName);
423
- await this.update(bmadDir, { ides: manifest.ides });
424
- }
425
- }
426
-
427
239
  /**
428
240
  * Calculate SHA256 hash of a file
429
241
  * @param {string} filePath - Path to file
@@ -438,354 +250,6 @@ class Manifest {
438
250
  }
439
251
  }
440
252
 
441
- /**
442
- * Parse installed files to extract metadata
443
- * @param {Array} installedFiles - List of installed file paths
444
- * @param {string} bmadDir - Path to bmad directory for relative paths
445
- * @returns {Array} Array of file metadata objects
446
- */
447
- async parseInstalledFiles(installedFiles, bmadDir) {
448
- const fileMetadata = [];
449
-
450
- for (const filePath of installedFiles) {
451
- const fileExt = path.extname(filePath).toLowerCase();
452
- // Make path relative to parent of bmad directory, starting with 'bmad/'
453
- const relativePath = 'bmad' + filePath.replace(bmadDir, '').replaceAll('\\', '/');
454
-
455
- // Calculate file hash
456
- const hash = await this.calculateFileHash(filePath);
457
-
458
- // Handle markdown files - extract XML metadata if present
459
- if (fileExt === '.md') {
460
- try {
461
- if (await fs.pathExists(filePath)) {
462
- const content = await fs.readFile(filePath, 'utf8');
463
- const metadata = this.extractXmlNodeAttributes(content, filePath, relativePath);
464
-
465
- if (metadata) {
466
- // Has XML metadata
467
- metadata.hash = hash;
468
- fileMetadata.push(metadata);
469
- } else {
470
- // No XML metadata - still track the file
471
- fileMetadata.push({
472
- file: relativePath,
473
- type: 'md',
474
- name: path.basename(filePath, fileExt),
475
- title: null,
476
- hash: hash,
477
- });
478
- }
479
- }
480
- } catch (error) {
481
- await prompts.log.warn(`Could not parse ${filePath}: ${error.message}`);
482
- }
483
- }
484
- // Handle other file types (CSV, JSON, YAML, etc.)
485
- else {
486
- fileMetadata.push({
487
- file: relativePath,
488
- type: fileExt.slice(1), // Remove the dot
489
- name: path.basename(filePath, fileExt),
490
- title: null,
491
- hash: hash,
492
- });
493
- }
494
- }
495
-
496
- return fileMetadata;
497
- }
498
-
499
- /**
500
- * Extract XML node attributes from MD file content
501
- * @param {string} content - File content
502
- * @param {string} filePath - File path for context
503
- * @param {string} relativePath - Relative path starting with 'bmad/'
504
- * @returns {Object|null} Extracted metadata or null
505
- */
506
- extractXmlNodeAttributes(content, filePath, relativePath) {
507
- // Look for XML blocks in code fences
508
- const xmlBlockMatch = content.match(/```xml\s*([\s\S]*?)```/);
509
- if (!xmlBlockMatch) {
510
- return null;
511
- }
512
-
513
- const xmlContent = xmlBlockMatch[1];
514
-
515
- // Extract root XML node (agent, task, template, etc.)
516
- const rootNodeMatch = xmlContent.match(/<(\w+)([^>]*)>/);
517
- if (!rootNodeMatch) {
518
- return null;
519
- }
520
-
521
- const nodeType = rootNodeMatch[1];
522
- const attributes = rootNodeMatch[2];
523
-
524
- // Extract name and title attributes (id not needed since we have path)
525
- const nameMatch = attributes.match(/name="([^"]*)"/);
526
- const titleMatch = attributes.match(/title="([^"]*)"/);
527
-
528
- return {
529
- file: relativePath,
530
- type: nodeType,
531
- name: nameMatch ? nameMatch[1] : null,
532
- title: titleMatch ? titleMatch[1] : null,
533
- };
534
- }
535
-
536
- /**
537
- * Generate CSV manifest content
538
- * @param {Object} data - Manifest data
539
- * @param {Array} fileMetadata - File metadata array
540
- * @param {Object} moduleConfigs - Module configuration data
541
- * @returns {string} CSV content
542
- */
543
- generateManifestCsv(data, fileMetadata, moduleConfigs = {}) {
544
- const timestamp = new Date().toISOString();
545
- let csv = [];
546
-
547
- // Header section
548
- csv.push(
549
- '# BMAD Manifest',
550
- `# Generated: ${timestamp}`,
551
- '',
552
- '## Installation Info',
553
- 'Property,Value',
554
- `Version,${data.version}`,
555
- `InstallDate,${data.installDate || timestamp}`,
556
- `LastUpdated,${data.lastUpdated || timestamp}`,
557
- );
558
- if (data.language) {
559
- csv.push(`Language,${data.language}`);
560
- }
561
- csv.push('');
562
-
563
- // Modules section
564
- if (data.modules && data.modules.length > 0) {
565
- csv.push('## Modules', 'Name,Version,ShortTitle');
566
- for (const moduleName of data.modules) {
567
- const config = moduleConfigs[moduleName] || {};
568
- csv.push([moduleName, config.version || '', config['short-title'] || ''].map((v) => this.escapeCsv(v)).join(','));
569
- }
570
- csv.push('');
571
- }
572
-
573
- // IDEs section
574
- if (data.ides && data.ides.length > 0) {
575
- csv.push('## IDEs', 'IDE');
576
- for (const ide of data.ides) {
577
- csv.push(this.escapeCsv(ide));
578
- }
579
- csv.push('');
580
- }
581
-
582
- // Files section - NO LONGER USED
583
- // Files are now tracked in files-manifest.csv by ManifestGenerator
584
-
585
- return csv.join('\n');
586
- }
587
-
588
- /**
589
- * Parse CSV manifest content back to object
590
- * @param {string} csvContent - CSV content to parse
591
- * @returns {Object} Parsed manifest data
592
- */
593
- parseManifestCsv(csvContent) {
594
- const result = {
595
- modules: [],
596
- ides: [],
597
- files: [],
598
- };
599
-
600
- const lines = csvContent.split('\n');
601
- let section = '';
602
-
603
- for (const line_ of lines) {
604
- const line = line_.trim();
605
-
606
- // Skip empty lines and comments
607
- if (!line || line.startsWith('#')) {
608
- // Check for section headers
609
- if (line.startsWith('## ')) {
610
- section = line.slice(3).toLowerCase();
611
- }
612
- continue;
613
- }
614
-
615
- // Parse based on current section
616
- switch (section) {
617
- case 'installation info': {
618
- // Skip header row
619
- if (line === 'Property,Value') continue;
620
-
621
- const [property, ...valueParts] = line.split(',');
622
- const value = this.unescapeCsv(valueParts.join(','));
623
-
624
- switch (property) {
625
- // Path no longer stored in manifest
626
- case 'Version': {
627
- result.version = value;
628
- break;
629
- }
630
- case 'InstallDate': {
631
- result.installDate = value;
632
- break;
633
- }
634
- case 'LastUpdated': {
635
- result.lastUpdated = value;
636
- break;
637
- }
638
- case 'Language': {
639
- result.language = value;
640
- break;
641
- }
642
- }
643
-
644
- break;
645
- }
646
- case 'modules': {
647
- // Skip header row
648
- if (line === 'Name,Version,ShortTitle') continue;
649
-
650
- const parts = this.parseCsvLine(line);
651
- if (parts[0]) {
652
- result.modules.push(parts[0]);
653
- }
654
-
655
- break;
656
- }
657
- case 'ides': {
658
- // Skip header row
659
- if (line === 'IDE') continue;
660
-
661
- result.ides.push(this.unescapeCsv(line));
662
-
663
- break;
664
- }
665
- case 'files': {
666
- // Skip header rows (support both old and new format)
667
- if (line === 'Type,Path,Name,Title' || line === 'Type,Path,Name,Title,Hash') continue;
668
-
669
- const parts = this.parseCsvLine(line);
670
- if (parts.length >= 2) {
671
- result.files.push({
672
- type: parts[0] || '',
673
- file: parts[1] || '',
674
- name: parts[2] || null,
675
- title: parts[3] || null,
676
- hash: parts[4] || null, // Hash column (may not exist in old manifests)
677
- });
678
- }
679
-
680
- break;
681
- }
682
- // No default
683
- }
684
- }
685
-
686
- return result;
687
- }
688
-
689
- /**
690
- * Parse a CSV line handling quotes and commas
691
- * @param {string} line - CSV line to parse
692
- * @returns {Array} Array of values
693
- */
694
- parseCsvLine(line) {
695
- const result = [];
696
- let current = '';
697
- let inQuotes = false;
698
-
699
- for (let i = 0; i < line.length; i++) {
700
- const char = line[i];
701
-
702
- if (char === '"') {
703
- if (inQuotes && line[i + 1] === '"') {
704
- // Escaped quote
705
- current += '"';
706
- i++;
707
- } else {
708
- // Toggle quote state
709
- inQuotes = !inQuotes;
710
- }
711
- } else if (char === ',' && !inQuotes) {
712
- // Field separator
713
- result.push(this.unescapeCsv(current));
714
- current = '';
715
- } else {
716
- current += char;
717
- }
718
- }
719
-
720
- // Add the last field
721
- result.push(this.unescapeCsv(current));
722
-
723
- return result;
724
- }
725
-
726
- /**
727
- * Escape CSV special characters
728
- * @param {string} text - Text to escape
729
- * @returns {string} Escaped text
730
- */
731
- escapeCsv(text) {
732
- if (!text) return '';
733
- const str = String(text);
734
-
735
- // If contains comma, newline, or quote, wrap in quotes and escape quotes
736
- if (str.includes(',') || str.includes('\n') || str.includes('"')) {
737
- return '"' + str.replaceAll('"', '""') + '"';
738
- }
739
-
740
- return str;
741
- }
742
-
743
- /**
744
- * Unescape CSV field
745
- * @param {string} text - Text to unescape
746
- * @returns {string} Unescaped text
747
- */
748
- unescapeCsv(text) {
749
- if (!text) return '';
750
-
751
- // Remove surrounding quotes if present
752
- if (text.startsWith('"') && text.endsWith('"')) {
753
- text = text.slice(1, -1);
754
- // Unescape doubled quotes
755
- text = text.replaceAll('""', '"');
756
- }
757
-
758
- return text;
759
- }
760
-
761
- /**
762
- * Load module configuration files
763
- * @param {Array} modules - List of module names
764
- * @returns {Object} Module configurations indexed by name
765
- */
766
- async loadModuleConfigs(modules) {
767
- const configs = {};
768
-
769
- for (const moduleName of modules) {
770
- // Handle core module differently - it's in src/core-skills not src/modules/core
771
- const configPath =
772
- moduleName === 'core'
773
- ? path.join(process.cwd(), 'src', 'core-skills', 'config.yaml')
774
- : path.join(process.cwd(), 'src', 'modules', moduleName, 'config.yaml');
775
-
776
- try {
777
- if (await fs.pathExists(configPath)) {
778
- const yaml = require('yaml');
779
- const content = await fs.readFile(configPath, 'utf8');
780
- configs[moduleName] = yaml.parse(content);
781
- }
782
- } catch (error) {
783
- await prompts.log.warn(`Could not load config for module ${moduleName}: ${error.message}`);
784
- }
785
- }
786
-
787
- return configs;
788
- }
789
253
  /**
790
254
  * Get module version info from source
791
255
  * @param {string} moduleName - Module name/code
@@ -986,47 +450,6 @@ class Manifest {
986
450
 
987
451
  return updates;
988
452
  }
989
-
990
- /**
991
- * Compare two semantic versions
992
- * @param {string} v1 - First version
993
- * @param {string} v2 - Second version
994
- * @returns {number} -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
995
- */
996
- compareVersions(v1, v2) {
997
- if (!v1 || !v2) return 0;
998
-
999
- const normalize = (v) => {
1000
- // Remove leading 'v' if present
1001
- v = v.replace(/^v/, '');
1002
- // Handle prerelease tags
1003
- const parts = v.split('-');
1004
- const main = parts[0].split('.');
1005
- const prerelease = parts[1];
1006
- return { main, prerelease };
1007
- };
1008
-
1009
- const n1 = normalize(v1);
1010
- const n2 = normalize(v2);
1011
-
1012
- // Compare main version parts
1013
- for (let i = 0; i < 3; i++) {
1014
- const num1 = parseInt(n1.main[i] || '0', 10);
1015
- const num2 = parseInt(n2.main[i] || '0', 10);
1016
- if (num1 !== num2) {
1017
- return num1 < num2 ? -1 : 1;
1018
- }
1019
- }
1020
-
1021
- // If main versions are equal, compare prerelease
1022
- if (n1.prerelease && n2.prerelease) {
1023
- return n1.prerelease < n2.prerelease ? -1 : n1.prerelease > n2.prerelease ? 1 : 0;
1024
- }
1025
- if (n1.prerelease) return -1; // Prerelease is older than stable
1026
- if (n2.prerelease) return 1; // Stable is newer than prerelease
1027
-
1028
- return 0;
1029
- }
1030
453
  }
1031
454
 
1032
455
  module.exports = { Manifest };
@@ -15,8 +15,6 @@
15
15
  * - standalone/agents/fred.md → bmad-agent-standalone-fred.md
16
16
  */
17
17
 
18
- // Type segments - agents are included in naming, others are filtered out
19
- const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
20
18
  const AGENT_SEGMENT = 'agents';
21
19
 
22
20
  // BMAD installation folder name - centralized constant for all installers
@@ -194,125 +192,6 @@ function parseDashName(filename) {
194
192
  };
195
193
  }
196
194
 
197
- // ============================================================================
198
- // LEGACY FUNCTIONS (underscore format) - kept for backward compatibility
199
- // ============================================================================
200
-
201
- /**
202
- * Convert hierarchical path to flat underscore-separated name (LEGACY)
203
- * @deprecated Use toDashName instead
204
- */
205
- function toUnderscoreName(module, type, name) {
206
- const isAgent = type === AGENT_SEGMENT;
207
- if (module === 'core') {
208
- return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
209
- }
210
- if (module === 'standalone') {
211
- return isAgent ? `bmad_agent_standalone_${name}.md` : `bmad_standalone_${name}.md`;
212
- }
213
- return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
214
- }
215
-
216
- /**
217
- * Convert relative path to flat underscore-separated name (LEGACY)
218
- * @deprecated Use toDashPath instead
219
- */
220
- function toUnderscorePath(relativePath) {
221
- // Strip common file extensions (same as toDashPath for consistency)
222
- const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
223
- const parts = withoutExt.split(/[/\\]/);
224
-
225
- const module = parts[0];
226
- const type = parts[1];
227
- const name = parts.slice(2).join('_');
228
-
229
- return toUnderscoreName(module, type, name);
230
- }
231
-
232
- /**
233
- * Create custom agent underscore name (LEGACY)
234
- * @deprecated Use customAgentDashName instead
235
- */
236
- function customAgentUnderscoreName(agentName) {
237
- return `bmad_custom_${agentName}.md`;
238
- }
239
-
240
- /**
241
- * Check if a filename uses underscore format (LEGACY)
242
- * @deprecated Use isDashFormat instead
243
- */
244
- function isUnderscoreFormat(filename) {
245
- return filename.startsWith('bmad_') && filename.includes('_');
246
- }
247
-
248
- /**
249
- * Extract parts from an underscore-formatted filename (LEGACY)
250
- * @deprecated Use parseDashName instead
251
- */
252
- function parseUnderscoreName(filename) {
253
- const withoutExt = filename.replace('.md', '');
254
- const parts = withoutExt.split('_');
255
-
256
- if (parts.length < 2 || parts[0] !== 'bmad') {
257
- return null;
258
- }
259
-
260
- const agentIndex = parts.indexOf('agent');
261
-
262
- if (agentIndex !== -1) {
263
- if (agentIndex === 1) {
264
- // bmad_agent_... - check for standalone
265
- if (parts.length >= 4 && parts[2] === 'standalone') {
266
- return {
267
- prefix: parts[0],
268
- module: 'standalone',
269
- type: 'agents',
270
- name: parts.slice(3).join('_'),
271
- };
272
- }
273
- return {
274
- prefix: parts[0],
275
- module: 'core',
276
- type: 'agents',
277
- name: parts.slice(agentIndex + 1).join('_'),
278
- };
279
- } else {
280
- return {
281
- prefix: parts[0],
282
- module: parts[1],
283
- type: 'agents',
284
- name: parts.slice(agentIndex + 1).join('_'),
285
- };
286
- }
287
- }
288
-
289
- if (parts.length === 2) {
290
- return {
291
- prefix: parts[0],
292
- module: 'core',
293
- type: 'workflows',
294
- name: parts[1],
295
- };
296
- }
297
-
298
- // Check for standalone non-agent: bmad_standalone_name
299
- if (parts[1] === 'standalone') {
300
- return {
301
- prefix: parts[0],
302
- module: 'standalone',
303
- type: 'workflows',
304
- name: parts.slice(2).join('_'),
305
- };
306
- }
307
-
308
- return {
309
- prefix: parts[0],
310
- module: parts[1],
311
- type: 'workflows',
312
- name: parts.slice(2).join('_'),
313
- };
314
- }
315
-
316
195
  /**
317
196
  * Resolve the skill name for an artifact.
318
197
  * Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
@@ -328,37 +207,13 @@ function resolveSkillName(artifact) {
328
207
  return toDashPath(artifact.relativePath);
329
208
  }
330
209
 
331
- // Backward compatibility aliases (colon format was same as underscore)
332
- const toColonName = toUnderscoreName;
333
- const toColonPath = toUnderscorePath;
334
- const customAgentColonName = customAgentUnderscoreName;
335
- const isColonFormat = isUnderscoreFormat;
336
- const parseColonName = parseUnderscoreName;
337
-
338
210
  module.exports = {
339
- // New standard (dash-based)
340
211
  toDashName,
341
212
  toDashPath,
342
213
  resolveSkillName,
343
214
  customAgentDashName,
344
215
  isDashFormat,
345
216
  parseDashName,
346
-
347
- // Legacy (underscore-based) - kept for backward compatibility
348
- toUnderscoreName,
349
- toUnderscorePath,
350
- customAgentUnderscoreName,
351
- isUnderscoreFormat,
352
- parseUnderscoreName,
353
-
354
- // Backward compatibility aliases
355
- toColonName,
356
- toColonPath,
357
- customAgentColonName,
358
- isColonFormat,
359
- parseColonName,
360
-
361
- TYPE_SEGMENTS,
362
217
  AGENT_SEGMENT,
363
218
  BMAD_FOLDER_NAME,
364
219
  };
@@ -155,33 +155,6 @@ class CustomModuleManager {
155
155
  };
156
156
  }
157
157
 
158
- /**
159
- * @deprecated Use parseSource() instead. Kept for backward compatibility.
160
- * Parse and validate a GitHub repository URL.
161
- * @param {string} url - GitHub URL to validate
162
- * @returns {Object} { owner, repo, isValid, error }
163
- */
164
- validateGitHubUrl(url) {
165
- if (!url || typeof url !== 'string') {
166
- return { owner: null, repo: null, isValid: false, error: 'URL is required' };
167
- }
168
- const trimmed = url.trim();
169
-
170
- // HTTPS format: https://github.com/owner/repo[.git] (strict, no trailing path)
171
- const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
172
- if (httpsMatch) {
173
- return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null };
174
- }
175
-
176
- // SSH format: git@github.com:owner/repo[.git]
177
- const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
178
- if (sshMatch) {
179
- return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null };
180
- }
181
-
182
- return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' };
183
- }
184
-
185
158
  // ─── Marketplace JSON ─────────────────────────────────────────────────────
186
159
 
187
160
  /**
@@ -109,46 +109,6 @@ class ExternalModuleManager {
109
109
  return modules.find((m) => m.code === code) || null;
110
110
  }
111
111
 
112
- /**
113
- * Get module info by key
114
- * @param {string} key - The module key (e.g., 'bmad-creative-intelligence-suite')
115
- * @returns {Object|null} Module info or null if not found
116
- */
117
- async getModuleByKey(key) {
118
- const modules = await this.listAvailable();
119
- return modules.find((m) => m.key === key) || null;
120
- }
121
-
122
- /**
123
- * Check if a module code exists in external modules
124
- * @param {string} code - The module code to check
125
- * @returns {boolean} True if the module exists
126
- */
127
- async hasModule(code) {
128
- const module = await this.getModuleByCode(code);
129
- return module !== null;
130
- }
131
-
132
- /**
133
- * Get the URL for a module by code
134
- * @param {string} code - The module code
135
- * @returns {string|null} The URL or null if not found
136
- */
137
- async getModuleUrl(code) {
138
- const module = await this.getModuleByCode(code);
139
- return module ? module.url : null;
140
- }
141
-
142
- /**
143
- * Get the module definition path for a module by code
144
- * @param {string} code - The module code
145
- * @returns {string|null} The module definition path or null if not found
146
- */
147
- async getModuleDefinition(code) {
148
- const module = await this.getModuleByCode(code);
149
- return module ? module.moduleDefinition : null;
150
- }
151
-
152
112
  /**
153
113
  * Get the cache directory for external modules
154
114
  * @returns {string} Path to the external modules cache directory
@@ -12,6 +12,8 @@ class OfficialModules {
12
12
  // Config collection state (merged from ConfigCollector)
13
13
  this.collectedConfig = {};
14
14
  this._existingConfig = null;
15
+ // Tracked during interactive config collection so {directory_name}
16
+ // placeholder defaults can be resolved in buildQuestion().
15
17
  this.currentProjectDir = null;
16
18
  }
17
19
 
@@ -500,32 +502,6 @@ class OfficialModules {
500
502
  }
501
503
  }
502
504
 
503
- /**
504
- * Find all .md agent files recursively in a directory
505
- * @param {string} dir - Directory to search
506
- * @returns {Array} List of .md agent file paths
507
- */
508
- async findAgentMdFiles(dir) {
509
- const agentFiles = [];
510
-
511
- async function searchDirectory(searchDir) {
512
- const entries = await fs.readdir(searchDir, { withFileTypes: true });
513
-
514
- for (const entry of entries) {
515
- const fullPath = path.join(searchDir, entry.name);
516
-
517
- if (entry.isFile() && entry.name.endsWith('.md')) {
518
- agentFiles.push(fullPath);
519
- } else if (entry.isDirectory()) {
520
- await searchDirectory(fullPath);
521
- }
522
- }
523
- }
524
-
525
- await searchDirectory(dir);
526
- return agentFiles;
527
- }
528
-
529
505
  /**
530
506
  * Create directories declared in module.yaml's `directories` key
531
507
  * This replaces the security-risky module installer pattern with declarative config
@@ -699,29 +675,6 @@ class OfficialModules {
699
675
  return { createdDirs, movedDirs, createdWdsFolders };
700
676
  }
701
677
 
702
- /**
703
- * Private: Process module configuration
704
- * @param {string} modulePath - Path to installed module
705
- * @param {string} moduleName - Module name
706
- */
707
- async processModuleConfig(modulePath, moduleName) {
708
- const configPath = path.join(modulePath, 'config.yaml');
709
-
710
- if (await fs.pathExists(configPath)) {
711
- try {
712
- let configContent = await fs.readFile(configPath, 'utf8');
713
-
714
- // Replace path placeholders
715
- configContent = configContent.replaceAll('{project-root}', `bmad/${moduleName}`);
716
- configContent = configContent.replaceAll('{module}', moduleName);
717
-
718
- await fs.writeFile(configPath, configContent, 'utf8');
719
- } catch (error) {
720
- await prompts.log.warn(`Failed to process module config: ${error.message}`);
721
- }
722
- }
723
- }
724
-
725
678
  /**
726
679
  * Private: Sync module files (preserving user modifications)
727
680
  * @param {string} sourcePath - Source module path
@@ -1091,7 +1044,6 @@ class OfficialModules {
1091
1044
  */
1092
1045
  async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
1093
1046
  this.currentProjectDir = projectDir;
1094
-
1095
1047
  // Load existing config if not already loaded
1096
1048
  if (!this._existingConfig) {
1097
1049
  await this.loadExistingConfig(projectDir);
@@ -50,17 +50,6 @@ class RegistryClient {
50
50
  const content = await this.fetch(url, timeout);
51
51
  return yaml.parse(content);
52
52
  }
53
-
54
- /**
55
- * Fetch a URL and parse the response as JSON.
56
- * @param {string} url - URL to fetch
57
- * @param {number} [timeout] - Timeout in ms
58
- * @returns {Promise<Object>} Parsed JSON content
59
- */
60
- async fetchJson(url, timeout) {
61
- const content = await this.fetch(url, timeout);
62
- return JSON.parse(content);
63
- }
64
53
  }
65
54
 
66
55
  module.exports = { RegistryClient };
@@ -498,26 +498,6 @@ async function password(options) {
498
498
  return result;
499
499
  }
500
500
 
501
- /**
502
- * Group multiple prompts together
503
- * @param {Object} prompts - Object of prompt functions
504
- * @param {Object} [options] - Group options
505
- * @returns {Promise<Object>} Object with all answers
506
- */
507
- async function group(prompts, options = {}) {
508
- const clack = await getClack();
509
-
510
- const result = await clack.group(prompts, {
511
- onCancel: () => {
512
- clack.cancel('Operation cancelled');
513
- process.exit(0);
514
- },
515
- ...options,
516
- });
517
-
518
- return result;
519
- }
520
-
521
501
  /**
522
502
  * Run tasks with spinner feedback
523
503
  * @param {Array} tasks - Array of task objects [{title, task, enabled?}]
@@ -578,42 +558,6 @@ async function box(content, title, options) {
578
558
  clack.box(content, title, options);
579
559
  }
580
560
 
581
- /**
582
- * Create a progress bar for visualizing task completion
583
- * @param {Object} [options] - Progress options (max, style, etc.)
584
- * @returns {Promise<Object>} Progress controller with start, advance, stop methods
585
- */
586
- async function progress(options) {
587
- const clack = await getClack();
588
- return clack.progress(options);
589
- }
590
-
591
- /**
592
- * Create a task log for displaying scrolling subprocess output
593
- * @param {Object} options - TaskLog options (title, limit, retainLog)
594
- * @returns {Promise<Object>} TaskLog controller with message, success, error methods
595
- */
596
- async function taskLog(options) {
597
- const clack = await getClack();
598
- return clack.taskLog(options);
599
- }
600
-
601
- /**
602
- * File system path prompt with autocomplete
603
- * @param {Object} options - Path options
604
- * @param {string} options.message - The prompt message
605
- * @param {string} [options.initialValue] - Initial path value
606
- * @param {boolean} [options.directory=false] - Only allow directories
607
- * @param {Function} [options.validate] - Validation function
608
- * @returns {Promise<string>} Selected path
609
- */
610
- async function pathPrompt(options) {
611
- const clack = await getClack();
612
- const result = await clack.path(options);
613
- await handleCancel(result);
614
- return result;
615
- }
616
-
617
561
  /**
618
562
  * Autocomplete single-select prompt with type-ahead filtering
619
563
  * @param {Object} options - Autocomplete options
@@ -631,50 +575,6 @@ async function autocomplete(options) {
631
575
  return result;
632
576
  }
633
577
 
634
- /**
635
- * Key-based instant selection prompt
636
- * @param {Object} options - SelectKey options
637
- * @param {string} options.message - The prompt message
638
- * @param {Array} options.options - Array of choices [{value, label, hint?}]
639
- * @returns {Promise<any>} Selected value
640
- */
641
- async function selectKey(options) {
642
- const clack = await getClack();
643
- const result = await clack.selectKey(options);
644
- await handleCancel(result);
645
- return result;
646
- }
647
-
648
- /**
649
- * Stream messages with dynamic content (for LLMs, generators, etc.)
650
- */
651
- const stream = {
652
- async info(generator) {
653
- const clack = await getClack();
654
- return clack.stream.info(generator);
655
- },
656
- async success(generator) {
657
- const clack = await getClack();
658
- return clack.stream.success(generator);
659
- },
660
- async step(generator) {
661
- const clack = await getClack();
662
- return clack.stream.step(generator);
663
- },
664
- async warn(generator) {
665
- const clack = await getClack();
666
- return clack.stream.warn(generator);
667
- },
668
- async error(generator) {
669
- const clack = await getClack();
670
- return clack.stream.error(generator);
671
- },
672
- async message(generator, options) {
673
- const clack = await getClack();
674
- return clack.stream.message(generator, options);
675
- },
676
- };
677
-
678
578
  /**
679
579
  * Get the color utility (picocolors instance from @clack/prompts)
680
580
  * @returns {Promise<Object>} The color utility (picocolors)
@@ -790,20 +690,14 @@ module.exports = {
790
690
  note,
791
691
  box,
792
692
  spinner,
793
- progress,
794
- taskLog,
795
693
  select,
796
694
  multiselect,
797
695
  autocompleteMultiselect,
798
696
  autocomplete,
799
- selectKey,
800
697
  confirm,
801
698
  text,
802
- path: pathPrompt,
803
699
  password,
804
- group,
805
700
  tasks,
806
701
  log,
807
- stream,
808
702
  prompt,
809
703
  };
@@ -1,136 +0,0 @@
1
- const path = require('node:path');
2
- const fs = require('fs-extra');
3
- const yaml = require('yaml');
4
- const { glob } = require('glob');
5
- const { getSourcePath } = require('../../project-root');
6
-
7
- async function loadModuleInjectionConfig(handler, moduleName) {
8
- const sourceModulesPath = getSourcePath('modules');
9
- const handlerBaseDir = path.join(sourceModulesPath, moduleName, 'sub-modules', handler);
10
- const configPath = path.join(handlerBaseDir, 'injections.yaml');
11
-
12
- if (!(await fs.pathExists(configPath))) {
13
- return null;
14
- }
15
-
16
- const configContent = await fs.readFile(configPath, 'utf8');
17
- const config = yaml.parse(configContent) || {};
18
-
19
- return {
20
- config,
21
- handlerBaseDir,
22
- configPath,
23
- };
24
- }
25
-
26
- function shouldApplyInjection(injection, subagentChoices) {
27
- if (!subagentChoices || subagentChoices.install === 'none') {
28
- return false;
29
- }
30
-
31
- if (subagentChoices.install === 'all') {
32
- return true;
33
- }
34
-
35
- if (subagentChoices.install === 'selective') {
36
- const selected = subagentChoices.selected || [];
37
-
38
- if (injection.requires === 'any' && selected.length > 0) {
39
- return true;
40
- }
41
-
42
- if (injection.requires) {
43
- const required = `${injection.requires}.md`;
44
- return selected.includes(required);
45
- }
46
-
47
- if (injection.point) {
48
- const selectedNames = selected.map((file) => file.replace('.md', ''));
49
- return selectedNames.some((name) => injection.point.includes(name));
50
- }
51
- }
52
-
53
- return false;
54
- }
55
-
56
- function filterAgentInstructions(content, selectedFiles) {
57
- if (!selectedFiles || selectedFiles.length === 0) {
58
- return '';
59
- }
60
-
61
- const selectedAgents = selectedFiles.map((file) => file.replace('.md', ''));
62
- const lines = content.split('\n');
63
- const filteredLines = [];
64
-
65
- for (const line of lines) {
66
- if (line.includes('<llm') || line.includes('</llm>')) {
67
- filteredLines.push(line);
68
- } else if (line.includes('subagent')) {
69
- let shouldInclude = false;
70
- for (const agent of selectedAgents) {
71
- if (line.includes(agent)) {
72
- shouldInclude = true;
73
- break;
74
- }
75
- }
76
-
77
- if (shouldInclude) {
78
- filteredLines.push(line);
79
- }
80
- } else if (line.includes('When creating PRDs') || line.includes('ACTIVELY delegate')) {
81
- filteredLines.push(line);
82
- }
83
- }
84
-
85
- if (filteredLines.length > 2) {
86
- return filteredLines.join('\n');
87
- }
88
-
89
- return '';
90
- }
91
-
92
- async function resolveSubagentFiles(handlerBaseDir, subagentConfig, subagentChoices) {
93
- if (!subagentConfig || !subagentConfig.files) {
94
- return [];
95
- }
96
-
97
- if (!subagentChoices || subagentChoices.install === 'none') {
98
- return [];
99
- }
100
-
101
- let filesToCopy = subagentConfig.files;
102
-
103
- if (subagentChoices.install === 'selective') {
104
- filesToCopy = subagentChoices.selected || [];
105
- }
106
-
107
- const sourceDir = path.join(handlerBaseDir, subagentConfig.source || '');
108
- const resolved = [];
109
-
110
- for (const file of filesToCopy) {
111
- // Use forward slashes for glob pattern (works on both Windows and Unix)
112
- // Convert backslashes to forward slashes for glob compatibility
113
- const normalizedSourceDir = sourceDir.replaceAll('\\', '/');
114
- const pattern = `${normalizedSourceDir}/**/${file}`;
115
- const matches = await glob(pattern);
116
-
117
- if (matches.length > 0) {
118
- const absolutePath = matches[0];
119
- resolved.push({
120
- file,
121
- absolutePath,
122
- relativePath: path.relative(sourceDir, absolutePath),
123
- sourceDir,
124
- });
125
- }
126
- }
127
-
128
- return resolved;
129
- }
130
-
131
- module.exports = {
132
- loadModuleInjectionConfig,
133
- shouldApplyInjection,
134
- filterAgentInstructions,
135
- resolveSubagentFiles,
136
- };