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

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.
@@ -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
  };
@@ -598,7 +598,7 @@ class UI {
598
598
  const officialCodes = new Set(officialSelected);
599
599
  const externalManager = new ExternalModuleManager();
600
600
  const registryModules = await externalManager.listAvailable();
601
- const officialRegistryCodes = new Set(registryModules.map((m) => m.code));
601
+ const officialRegistryCodes = new Set(['core', 'bmm', ...registryModules.map((m) => m.code)]);
602
602
  const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
603
603
 
604
604
  // Phase 2: Community modules (category drill-down)
@@ -630,6 +630,11 @@ class UI {
630
630
  * @returns {Array} Selected official module codes
631
631
  */
632
632
  async _selectOfficialModules(installedModuleIds = new Set()) {
633
+ // Built-in modules (core, bmm) come from local source, not the registry
634
+ const { OfficialModules } = require('./modules/official-modules');
635
+ const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
636
+
637
+ // External modules come from the registry (with fallback)
633
638
  const externalManager = new ExternalModuleManager();
634
639
  const registryModules = await externalManager.listAvailable();
635
640
 
@@ -637,20 +642,34 @@ class UI {
637
642
  const initialValues = [];
638
643
  const lockedValues = ['core'];
639
644
 
640
- const buildModuleEntry = async (mod) => {
641
- const isInstalled = installedModuleIds.has(mod.code);
642
- const version = await getMarketplaceVersion(mod.code);
643
- const label = version ? `${mod.name} (v${version})` : mod.name;
645
+ const buildModuleEntry = async (code, name, description, isDefault) => {
646
+ const isInstalled = installedModuleIds.has(code);
647
+ const version = await getMarketplaceVersion(code);
648
+ const label = version ? `${name} (v${version})` : name;
644
649
  return {
645
650
  label,
646
- value: mod.code,
647
- hint: mod.description,
648
- selected: isInstalled,
651
+ value: code,
652
+ hint: description,
653
+ selected: isInstalled || isDefault,
649
654
  };
650
655
  };
651
656
 
657
+ // Add built-in modules first (always available regardless of network)
658
+ const builtInCodes = new Set();
659
+ for (const mod of builtInModules) {
660
+ const code = mod.id;
661
+ builtInCodes.add(code);
662
+ const entry = await buildModuleEntry(code, mod.name, mod.description, mod.defaultSelected);
663
+ allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
664
+ if (entry.selected) {
665
+ initialValues.push(code);
666
+ }
667
+ }
668
+
669
+ // Add external registry modules (skip built-in duplicates)
652
670
  for (const mod of registryModules) {
653
- const entry = await buildModuleEntry(mod);
671
+ if (mod.builtIn || builtInCodes.has(mod.code)) continue;
672
+ const entry = await buildModuleEntry(mod.code, mod.name, mod.description, mod.defaultSelected);
654
673
  allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
655
674
  if (entry.selected) {
656
675
  initialValues.push(mod.code);
@@ -1122,12 +1141,26 @@ class UI {
1122
1141
  * @returns {Array} Default module codes
1123
1142
  */
1124
1143
  async getDefaultModules(installedModuleIds = new Set()) {
1125
- const externalManager = new ExternalModuleManager();
1126
- const registryModules = await externalManager.listAvailable();
1144
+ // Built-in modules with default_selected come from local source
1145
+ const { OfficialModules } = require('./modules/official-modules');
1146
+ const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
1127
1147
 
1128
1148
  const defaultModules = [];
1149
+ const seen = new Set();
1150
+
1151
+ for (const mod of builtInModules) {
1152
+ if (mod.defaultSelected || installedModuleIds.has(mod.id)) {
1153
+ defaultModules.push(mod.id);
1154
+ seen.add(mod.id);
1155
+ }
1156
+ }
1157
+
1158
+ // Add external registry defaults
1159
+ const externalManager = new ExternalModuleManager();
1160
+ const registryModules = await externalManager.listAvailable();
1129
1161
 
1130
1162
  for (const mod of registryModules) {
1163
+ if (mod.builtIn || seen.has(mod.code)) continue;
1131
1164
  if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
1132
1165
  defaultModules.push(mod.code);
1133
1166
  }