bmad-method 6.3.1-next.1 → 6.3.1-next.11

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 (81) hide show
  1. package/package.json +1 -2
  2. package/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md +47 -36
  3. package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.yaml +44 -0
  4. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md +46 -33
  5. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/customize.yaml +38 -0
  6. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +22 -3
  7. package/src/bmm-skills/1-analysis/bmad-product-brief/customize.yaml +6 -0
  8. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/contextual-discovery.md +7 -7
  9. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/draft-and-review.md +5 -5
  10. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/finalize.md +3 -1
  11. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/guided-elicitation.md +2 -2
  12. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md +46 -35
  13. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/customize.yaml +41 -0
  14. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md +46 -31
  15. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/customize.yaml +26 -0
  16. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-08-scoping.md +70 -23
  17. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-11-polish.md +1 -1
  18. package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +46 -30
  19. package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.yaml +29 -0
  20. package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +57 -43
  21. package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.yaml +44 -0
  22. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +14 -5
  23. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-03-implement.md +2 -0
  24. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-05-present.md +17 -8
  25. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +4 -0
  26. package/src/bmm-skills/4-implementation/bmad-quick-dev/sync-sprint-status.md +19 -0
  27. package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +1 -0
  28. package/src/core-skills/module.yaml +1 -0
  29. package/src/scripts/resolve_customization.py +248 -0
  30. package/tools/installer/cli-utils.js +0 -137
  31. package/tools/installer/commands/status.js +1 -1
  32. package/tools/installer/commands/uninstall.js +1 -1
  33. package/tools/installer/core/existing-install.js +1 -1
  34. package/tools/installer/core/install-paths.js +7 -4
  35. package/tools/installer/core/installer.js +58 -10
  36. package/tools/installer/core/manifest-generator.js +9 -10
  37. package/tools/installer/core/manifest.js +1 -578
  38. package/tools/installer/file-ops.js +1 -1
  39. package/tools/installer/fs-native.js +116 -0
  40. package/tools/installer/ide/_config-driven.js +1 -1
  41. package/tools/installer/ide/platform-codes.js +1 -1
  42. package/tools/installer/ide/shared/path-utils.js +0 -145
  43. package/tools/installer/ide/shared/skill-manifest.js +1 -1
  44. package/tools/installer/message-loader.js +1 -1
  45. package/tools/installer/modules/community-manager.js +11 -6
  46. package/tools/installer/modules/custom-module-manager.js +1 -28
  47. package/tools/installer/modules/external-manager.js +5 -44
  48. package/tools/installer/modules/official-modules.js +5 -53
  49. package/tools/installer/modules/plugin-resolver.js +1 -1
  50. package/tools/installer/modules/registry-client.js +133 -12
  51. package/tools/installer/project-root.js +1 -1
  52. package/tools/installer/prompts.js +0 -106
  53. package/tools/installer/ui.js +45 -12
  54. package/tools/migrate-custom-module-paths.js +1 -1
  55. package/tools/installer/ide/shared/agent-command-generator.js +0 -180
  56. package/tools/installer/ide/shared/bmad-artifacts.js +0 -208
  57. package/tools/installer/ide/shared/module-injections.js +0 -136
  58. package/tools/installer/ide/templates/agent-command-template.md +0 -14
  59. package/tools/installer/ide/templates/combined/antigravity.md +0 -8
  60. package/tools/installer/ide/templates/combined/default-agent.md +0 -15
  61. package/tools/installer/ide/templates/combined/default-task.md +0 -10
  62. package/tools/installer/ide/templates/combined/default-tool.md +0 -10
  63. package/tools/installer/ide/templates/combined/default-workflow.md +0 -6
  64. package/tools/installer/ide/templates/combined/gemini-agent.toml +0 -14
  65. package/tools/installer/ide/templates/combined/gemini-task.toml +0 -11
  66. package/tools/installer/ide/templates/combined/gemini-tool.toml +0 -11
  67. package/tools/installer/ide/templates/combined/gemini-workflow-yaml.toml +0 -16
  68. package/tools/installer/ide/templates/combined/gemini-workflow.toml +0 -14
  69. package/tools/installer/ide/templates/combined/kiro-agent.md +0 -16
  70. package/tools/installer/ide/templates/combined/kiro-task.md +0 -9
  71. package/tools/installer/ide/templates/combined/kiro-tool.md +0 -9
  72. package/tools/installer/ide/templates/combined/kiro-workflow.md +0 -7
  73. package/tools/installer/ide/templates/combined/opencode-agent.md +0 -15
  74. package/tools/installer/ide/templates/combined/opencode-task.md +0 -13
  75. package/tools/installer/ide/templates/combined/opencode-tool.md +0 -13
  76. package/tools/installer/ide/templates/combined/opencode-workflow-yaml.md +0 -16
  77. package/tools/installer/ide/templates/combined/opencode-workflow.md +0 -16
  78. package/tools/installer/ide/templates/combined/rovodev.md +0 -9
  79. package/tools/installer/ide/templates/combined/trae.md +0 -9
  80. package/tools/installer/ide/templates/combined/windsurf-workflow.md +0 -10
  81. package/tools/installer/ide/templates/split/.gitkeep +0 -0
@@ -1,6 +1,37 @@
1
1
  const https = require('node:https');
2
2
  const yaml = require('yaml');
3
3
 
4
+ /**
5
+ * Build a rich Error from a non-2xx response. Includes the URL, the GitHub
6
+ * JSON error message (or a truncated body snippet), rate-limit reset time,
7
+ * and Retry-After — anything present that would help a user recover.
8
+ */
9
+ function buildHttpError(url, res, body) {
10
+ const parts = [`HTTP ${res.statusCode} ${url}`];
11
+
12
+ if (body) {
13
+ try {
14
+ const parsed = JSON.parse(body);
15
+ if (parsed.message) parts.push(parsed.message);
16
+ if (parsed.documentation_url) parts.push(`(see ${parsed.documentation_url})`);
17
+ } catch {
18
+ const snippet = body.slice(0, 200).trim();
19
+ if (snippet) parts.push(snippet);
20
+ }
21
+ }
22
+
23
+ const remaining = res.headers['x-ratelimit-remaining'];
24
+ const reset = res.headers['x-ratelimit-reset'];
25
+ if (remaining === '0' && reset) {
26
+ parts.push(`rate limit exhausted; resets at ${new Date(Number(reset) * 1000).toISOString()}`);
27
+ }
28
+
29
+ const retryAfter = res.headers['retry-after'];
30
+ if (retryAfter) parts.push(`retry after ${retryAfter}`);
31
+
32
+ return new Error(parts.join(' — '));
33
+ }
34
+
4
35
  /**
5
36
  * Shared HTTP client for fetching registry data from GitHub.
6
37
  * Used by ExternalModuleManager, CommunityModuleManager, and CustomModuleManager.
@@ -12,25 +43,31 @@ class RegistryClient {
12
43
 
13
44
  /**
14
45
  * Fetch a URL and return the response body as a string.
15
- * Follows one redirect (GitHub sometimes 301s).
46
+ * Follows up to 3 redirects (GitHub sometimes 301s).
16
47
  * @param {string} url - URL to fetch
17
48
  * @param {number} [timeout] - Timeout in ms (overrides default)
49
+ * @param {number} [maxRedirects=3] - Maximum redirects to follow
18
50
  * @returns {Promise<string>} Response body
19
51
  */
20
- fetch(url, timeout) {
52
+ fetch(url, timeout, maxRedirects = 3) {
21
53
  const timeoutMs = timeout || this.timeout;
22
54
  return new Promise((resolve, reject) => {
23
55
  const req = https
24
56
  .get(url, { timeout: timeoutMs }, (res) => {
25
57
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
26
- return this.fetch(res.headers.location, timeoutMs).then(resolve, reject);
27
- }
28
- if (res.statusCode !== 200) {
29
- return reject(new Error(`HTTP ${res.statusCode}`));
58
+ if (maxRedirects <= 0) {
59
+ return reject(new Error('Too many redirects'));
60
+ }
61
+ return this.fetch(res.headers.location, timeoutMs, maxRedirects - 1).then(resolve, reject);
30
62
  }
31
63
  let data = '';
32
64
  res.on('data', (chunk) => (data += chunk));
33
- res.on('end', () => resolve(data));
65
+ res.on('end', () => {
66
+ if (res.statusCode !== 200) {
67
+ return reject(buildHttpError(url, res, data));
68
+ }
69
+ resolve(data);
70
+ });
34
71
  })
35
72
  .on('error', reject)
36
73
  .on('timeout', () => {
@@ -52,14 +89,98 @@ class RegistryClient {
52
89
  }
53
90
 
54
91
  /**
55
- * Fetch a URL and parse the response as JSON.
92
+ * Fetch a file from a GitHub repo using the Contents API first,
93
+ * falling back to raw.githubusercontent.com if the API fails.
94
+ *
95
+ * The API endpoint (`api.github.com`) is tried first because corporate
96
+ * proxies commonly block `raw.githubusercontent.com` while allowing
97
+ * `api.github.com` under the "Software Development" category.
98
+ *
99
+ * @param {string} owner - Repository owner (e.g., 'bmad-code-org')
100
+ * @param {string} repo - Repository name (e.g., 'bmad-plugins-marketplace')
101
+ * @param {string} filePath - Path within the repo (e.g., 'registry/official.yaml')
102
+ * @param {string} ref - Git ref (branch, tag, or SHA; e.g., 'main')
103
+ * @param {number} [timeout] - Timeout in ms (overrides default)
104
+ * @returns {Promise<string>} Raw file content
105
+ */
106
+ async fetchGitHubFile(owner, repo, filePath, ref, timeout) {
107
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${ref}`;
108
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`;
109
+
110
+ // Try GitHub Contents API first (with raw content accept header)
111
+ try {
112
+ return await this._fetchWithHeaders(apiUrl, { Accept: 'application/vnd.github.raw+json' }, timeout);
113
+ } catch (apiError) {
114
+ // API failed — fall back to raw CDN
115
+ try {
116
+ return await this.fetch(rawUrl, timeout);
117
+ } catch (cdnError) {
118
+ throw new AggregateError([apiError, cdnError], `Both GitHub API and raw CDN failed for ${filePath}`);
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Fetch a file from GitHub and parse as YAML.
125
+ * @param {string} owner - Repository owner
126
+ * @param {string} repo - Repository name
127
+ * @param {string} filePath - Path within the repo
128
+ * @param {string} ref - Git ref
129
+ * @param {number} [timeout] - Timeout in ms
130
+ * @returns {Promise<Object>} Parsed YAML content
131
+ */
132
+ async fetchGitHubYaml(owner, repo, filePath, ref, timeout) {
133
+ const content = await this.fetchGitHubFile(owner, repo, filePath, ref, timeout);
134
+ return yaml.parse(content);
135
+ }
136
+
137
+ /**
138
+ * Fetch a URL with custom headers. Used for GitHub API requests.
139
+ * Follows up to 3 redirects.
56
140
  * @param {string} url - URL to fetch
141
+ * @param {Object} headers - Request headers
57
142
  * @param {number} [timeout] - Timeout in ms
58
- * @returns {Promise<Object>} Parsed JSON content
143
+ * @param {number} [maxRedirects=3] - Maximum redirects to follow
144
+ * @returns {Promise<string>} Response body
145
+ * @private
59
146
  */
60
- async fetchJson(url, timeout) {
61
- const content = await this.fetch(url, timeout);
62
- return JSON.parse(content);
147
+ _fetchWithHeaders(url, headers, timeout, maxRedirects = 3) {
148
+ const timeoutMs = timeout || this.timeout;
149
+ const parsed = new URL(url);
150
+ const options = {
151
+ hostname: parsed.hostname,
152
+ path: parsed.pathname + parsed.search,
153
+ timeout: timeoutMs,
154
+ headers: {
155
+ 'User-Agent': 'bmad-installer',
156
+ ...headers,
157
+ },
158
+ };
159
+
160
+ return new Promise((resolve, reject) => {
161
+ const req = https
162
+ .get(options, (res) => {
163
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
164
+ if (maxRedirects <= 0) {
165
+ return reject(new Error('Too many redirects'));
166
+ }
167
+ return this._fetchWithHeaders(res.headers.location, headers, timeoutMs, maxRedirects - 1).then(resolve, reject);
168
+ }
169
+ let data = '';
170
+ res.on('data', (chunk) => (data += chunk));
171
+ res.on('end', () => {
172
+ if (res.statusCode !== 200) {
173
+ return reject(buildHttpError(url, res, data));
174
+ }
175
+ resolve(data);
176
+ });
177
+ })
178
+ .on('error', reject)
179
+ .on('timeout', () => {
180
+ req.destroy();
181
+ reject(new Error('Request timed out'));
182
+ });
183
+ });
63
184
  }
64
185
  }
65
186
 
@@ -1,5 +1,5 @@
1
1
  const path = require('node:path');
2
- const fs = require('fs-extra');
2
+ const fs = require('./fs-native');
3
3
 
4
4
  /**
5
5
  * Find the BMAD project root directory by looking for package.json
@@ -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,6 +1,6 @@
1
1
  const path = require('node:path');
2
2
  const os = require('node:os');
3
- const fs = require('fs-extra');
3
+ const fs = require('./fs-native');
4
4
  const { CLIUtils } = require('./cli-utils');
5
5
  const { ExternalModuleManager } = require('./modules/external-manager');
6
6
  const { getProjectRoot } = require('./project-root');
@@ -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
  }
@@ -3,7 +3,7 @@
3
3
  * This should be run once to update existing installations
4
4
  */
5
5
 
6
- const fs = require('fs-extra');
6
+ const fs = require('./installer/fs-native');
7
7
  const path = require('node:path');
8
8
  const yaml = require('yaml');
9
9
  const chalk = require('chalk');
@@ -1,180 +0,0 @@
1
- const path = require('node:path');
2
- const fs = require('fs-extra');
3
- const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
4
-
5
- /**
6
- * Generates launcher command files for each agent
7
- */
8
- class AgentCommandGenerator {
9
- constructor(bmadFolderName = BMAD_FOLDER_NAME) {
10
- this.templatePath = path.join(__dirname, '../templates/agent-command-template.md');
11
- this.bmadFolderName = bmadFolderName;
12
- }
13
-
14
- /**
15
- * Collect agent artifacts for IDE installation
16
- * @param {string} bmadDir - BMAD installation directory
17
- * @param {Array} selectedModules - Modules to include
18
- * @returns {Object} Artifacts array with metadata
19
- */
20
- async collectAgentArtifacts(bmadDir, selectedModules = []) {
21
- const { getAgentsFromBmad } = require('./bmad-artifacts');
22
-
23
- // Get agents from INSTALLED bmad/ directory
24
- const agents = await getAgentsFromBmad(bmadDir, selectedModules);
25
-
26
- const artifacts = [];
27
-
28
- for (const agent of agents) {
29
- const launcherContent = await this.generateLauncherContent(agent);
30
- // Use relativePath if available (for nested agents), otherwise just name with .md
31
- const agentPathInModule = agent.relativePath || `${agent.name}.md`;
32
- // Calculate the relative agent path (e.g., bmm/agents/pm.md)
33
- let agentRelPath = agent.path || '';
34
- // Normalize path separators for cross-platform compatibility
35
- agentRelPath = agentRelPath.replaceAll('\\', '/');
36
- // Remove _bmad/ prefix if present to get relative path from project root
37
- // Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
38
- if (agentRelPath.includes('_bmad/')) {
39
- const parts = agentRelPath.split(/_bmad\//);
40
- if (parts.length > 1) {
41
- agentRelPath = parts.slice(1).join('/');
42
- }
43
- }
44
- artifacts.push({
45
- type: 'agent-launcher',
46
- name: agent.name,
47
- description: agent.description || `${agent.name} agent`,
48
- module: agent.module,
49
- canonicalId: agent.canonicalId || '',
50
- relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
51
- agentPath: agentRelPath, // Relative path to actual agent file
52
- content: launcherContent,
53
- sourcePath: agent.path,
54
- });
55
- }
56
-
57
- return {
58
- artifacts,
59
- counts: {
60
- agents: agents.length,
61
- },
62
- };
63
- }
64
-
65
- /**
66
- * Generate launcher content for an agent
67
- * @param {Object} agent - Agent metadata
68
- * @returns {string} Launcher file content
69
- */
70
- async generateLauncherContent(agent) {
71
- // Load the template
72
- const template = await fs.readFile(this.templatePath, 'utf8');
73
-
74
- // Replace template variables
75
- // Use relativePath if available (for nested agents), otherwise just name with .md
76
- const agentPathInModule = agent.relativePath || `${agent.name}.md`;
77
- return template
78
- .replaceAll('{{name}}', agent.name)
79
- .replaceAll('{{module}}', agent.module)
80
- .replaceAll('{{path}}', agentPathInModule)
81
- .replaceAll('{{description}}', agent.description || `${agent.name} agent`)
82
- .replaceAll('_bmad', this.bmadFolderName)
83
- .replaceAll('_bmad', '_bmad');
84
- }
85
-
86
- /**
87
- * Write agent launcher artifacts to IDE commands directory
88
- * @param {string} baseCommandsDir - Base commands directory for the IDE
89
- * @param {Array} artifacts - Agent launcher artifacts
90
- * @returns {number} Count of launchers written
91
- */
92
- async writeAgentLaunchers(baseCommandsDir, artifacts) {
93
- let writtenCount = 0;
94
-
95
- for (const artifact of artifacts) {
96
- if (artifact.type === 'agent-launcher') {
97
- const moduleAgentsDir = path.join(baseCommandsDir, artifact.module, 'agents');
98
- await fs.ensureDir(moduleAgentsDir);
99
-
100
- const launcherPath = path.join(moduleAgentsDir, `${artifact.name}.md`);
101
- await fs.writeFile(launcherPath, artifact.content);
102
- writtenCount++;
103
- }
104
- }
105
-
106
- return writtenCount;
107
- }
108
-
109
- /**
110
- * Write agent launcher artifacts using underscore format (Windows-compatible)
111
- * Creates flat files like: bmad_bmm_pm.md
112
- *
113
- * @param {string} baseCommandsDir - Base commands directory for the IDE
114
- * @param {Array} artifacts - Agent launcher artifacts
115
- * @returns {number} Count of launchers written
116
- */
117
- async writeColonArtifacts(baseCommandsDir, artifacts) {
118
- let writtenCount = 0;
119
-
120
- for (const artifact of artifacts) {
121
- if (artifact.type === 'agent-launcher') {
122
- // Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md
123
- const flatName = toColonPath(artifact.relativePath);
124
- const launcherPath = path.join(baseCommandsDir, flatName);
125
- await fs.ensureDir(path.dirname(launcherPath));
126
- await fs.writeFile(launcherPath, artifact.content);
127
- writtenCount++;
128
- }
129
- }
130
-
131
- return writtenCount;
132
- }
133
-
134
- /**
135
- * Write agent launcher artifacts using dash format (NEW STANDARD)
136
- * Creates flat files like: bmad-agent-bmm-pm.md
137
- *
138
- * The bmad-agent- prefix distinguishes agents from workflows/tasks/tools.
139
- *
140
- * @param {string} baseCommandsDir - Base commands directory for the IDE
141
- * @param {Array} artifacts - Agent launcher artifacts
142
- * @returns {number} Count of launchers written
143
- */
144
- async writeDashArtifacts(baseCommandsDir, artifacts) {
145
- let writtenCount = 0;
146
-
147
- for (const artifact of artifacts) {
148
- if (artifact.type === 'agent-launcher') {
149
- // Convert relativePath to dash format: bmm/agents/pm.md → bmad-agent-bmm-pm.md
150
- const flatName = toDashPath(artifact.relativePath);
151
- const launcherPath = path.join(baseCommandsDir, flatName);
152
- await fs.ensureDir(path.dirname(launcherPath));
153
- await fs.writeFile(launcherPath, artifact.content);
154
- writtenCount++;
155
- }
156
- }
157
-
158
- return writtenCount;
159
- }
160
-
161
- /**
162
- * Get the custom agent name in underscore format (Windows-compatible)
163
- * @param {string} agentName - Custom agent name
164
- * @returns {string} Underscore-formatted filename
165
- */
166
- getCustomAgentColonName(agentName) {
167
- return customAgentColonName(agentName);
168
- }
169
-
170
- /**
171
- * Get the custom agent name in underscore format (Windows-compatible)
172
- * @param {string} agentName - Custom agent name
173
- * @returns {string} Underscore-formatted filename
174
- */
175
- getCustomAgentDashName(agentName) {
176
- return customAgentDashName(agentName);
177
- }
178
- }
179
-
180
- module.exports = { AgentCommandGenerator };