bmad-method 4.6.3 → 4.8.0

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 (50) hide show
  1. package/.prettierignore +0 -1
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +18 -1
  4. package/bmad-core/agents/analyst.md +11 -8
  5. package/bmad-core/agents/architect.md +10 -7
  6. package/bmad-core/agents/bmad-master.md +13 -11
  7. package/bmad-core/agents/bmad-orchestrator.md +3 -0
  8. package/bmad-core/agents/dev.md +19 -14
  9. package/bmad-core/agents/pm.md +8 -5
  10. package/bmad-core/agents/po.md +13 -10
  11. package/bmad-core/agents/qa.md +8 -5
  12. package/bmad-core/agents/sm.md +15 -25
  13. package/bmad-core/agents/ux-expert.md +11 -8
  14. package/bmad-core/core-config.yml +24 -0
  15. package/bmad-core/data/bmad-kb.md +391 -10
  16. package/bmad-core/tasks/create-next-story.md +63 -45
  17. package/bmad-core/utils/file-resolution-context.md +10 -0
  18. package/dist/agents/analyst.txt +404 -17
  19. package/dist/agents/architect.txt +14 -6
  20. package/dist/agents/bmad-master.txt +485 -109
  21. package/dist/agents/bmad-orchestrator.txt +402 -22
  22. package/dist/agents/dev.txt +26 -34
  23. package/dist/agents/pm.txt +9 -5
  24. package/dist/agents/po.txt +10 -10
  25. package/dist/agents/qa.txt +4 -4
  26. package/dist/agents/sm.txt +74 -63
  27. package/dist/agents/ux-expert.txt +9 -7
  28. package/dist/expansion-packs/bmad-2d-phaser-game-dev/teams/phaser-2d-nodejs-game-team.txt +49 -20
  29. package/dist/expansion-packs/expansion-creator/agents/bmad-the-creator.txt +6 -3
  30. package/dist/teams/team-all.txt +561 -158
  31. package/dist/teams/team-fullstack.txt +457 -57
  32. package/dist/teams/team-ide-minimal.txt +516 -133
  33. package/dist/teams/team-no-ui.txt +448 -50
  34. package/docs/bmad-workflow-guide.md +10 -8
  35. package/docs/claude-code-guide.md +1 -1
  36. package/docs/core-architecture.md +4 -4
  37. package/docs/cursor-guide.md +1 -1
  38. package/docs/roo-code-guide.md +1 -1
  39. package/docs/user-guide.md +30 -21
  40. package/docs/windsurf-guide.md +1 -1
  41. package/expansion-packs/bmad-infrastructure-devops/data/bmad-kb.md +3 -0
  42. package/expansion-packs/expansion-creator/templates/agent-tmpl.md +4 -1
  43. package/package.json +1 -1
  44. package/tools/builders/web-builder.js +158 -94
  45. package/tools/installer/bin/bmad.js +34 -2
  46. package/tools/installer/lib/file-manager.js +3 -2
  47. package/tools/installer/lib/ide-setup.js +39 -123
  48. package/tools/installer/lib/installer.js +13 -1
  49. package/tools/installer/package.json +1 -1
  50. /package/bmad-core/{templates/web-agent-startup-instructions-template.md → utils/web-agent-startup-instructions.md} +0 -0
@@ -1,19 +1,22 @@
1
- const fs = require('node:fs').promises;
2
- const path = require('node:path');
3
- const DependencyResolver = require('../lib/dependency-resolver');
1
+ const fs = require("node:fs").promises;
2
+ const path = require("node:path");
3
+ const DependencyResolver = require("../lib/dependency-resolver");
4
4
 
5
5
  class WebBuilder {
6
6
  constructor(options = {}) {
7
7
  this.rootDir = options.rootDir || process.cwd();
8
- this.outputDirs = options.outputDirs || [
9
- path.join(this.rootDir, 'dist')
10
- ];
8
+ this.outputDirs = options.outputDirs || [path.join(this.rootDir, "dist")];
11
9
  this.resolver = new DependencyResolver(this.rootDir);
12
- this.templatePath = path.join(this.rootDir, 'bmad-core', 'templates', 'web-agent-startup-instructions-template.md');
10
+ this.templatePath = path.join(
11
+ this.rootDir,
12
+ "bmad-core",
13
+ "utils",
14
+ "web-agent-startup-instructions.md"
15
+ );
13
16
  }
14
17
 
15
18
  parseYaml(content) {
16
- const yaml = require('js-yaml');
19
+ const yaml = require("js-yaml");
17
20
  return yaml.load(content);
18
21
  }
19
22
 
@@ -38,10 +41,10 @@ class WebBuilder {
38
41
 
39
42
  // Write to all output directories
40
43
  for (const outputDir of this.outputDirs) {
41
- const outputPath = path.join(outputDir, 'agents');
44
+ const outputPath = path.join(outputDir, "agents");
42
45
  await fs.mkdir(outputPath, { recursive: true });
43
46
  const outputFile = path.join(outputPath, `${agentId}.txt`);
44
- await fs.writeFile(outputFile, bundle, 'utf8');
47
+ await fs.writeFile(outputFile, bundle, "utf8");
45
48
  }
46
49
  }
47
50
 
@@ -57,10 +60,10 @@ class WebBuilder {
57
60
 
58
61
  // Write to all output directories
59
62
  for (const outputDir of this.outputDirs) {
60
- const outputPath = path.join(outputDir, 'teams');
63
+ const outputPath = path.join(outputDir, "teams");
61
64
  await fs.mkdir(outputPath, { recursive: true });
62
65
  const outputFile = path.join(outputPath, `${teamId}.txt`);
63
- await fs.writeFile(outputFile, bundle, 'utf8');
66
+ await fs.writeFile(outputFile, bundle, "utf8");
64
67
  }
65
68
  }
66
69
 
@@ -69,7 +72,7 @@ class WebBuilder {
69
72
 
70
73
  async buildAgentBundle(agentId) {
71
74
  const dependencies = await this.resolver.resolveAgentDependencies(agentId);
72
- const template = await fs.readFile(this.templatePath, 'utf8');
75
+ const template = await fs.readFile(this.templatePath, "utf8");
73
76
 
74
77
  const sections = [template];
75
78
 
@@ -81,12 +84,12 @@ class WebBuilder {
81
84
  sections.push(this.formatSection(resource.path, resource.content));
82
85
  }
83
86
 
84
- return sections.join('\n');
87
+ return sections.join("\n");
85
88
  }
86
89
 
87
90
  async buildTeamBundle(teamId) {
88
91
  const dependencies = await this.resolver.resolveTeamDependencies(teamId);
89
- const template = await fs.readFile(this.templatePath, 'utf8');
92
+ const template = await fs.readFile(this.templatePath, "utf8");
90
93
 
91
94
  const sections = [template];
92
95
 
@@ -103,21 +106,72 @@ class WebBuilder {
103
106
  sections.push(this.formatSection(resource.path, resource.content));
104
107
  }
105
108
 
106
- return sections.join('\n');
109
+ return sections.join("\n");
110
+ }
111
+
112
+ processAgentContent(content) {
113
+ // First, replace content before YAML with the template
114
+ const yamlMatch = content.match(/```ya?ml\n([\s\S]*?)\n```/);
115
+ if (!yamlMatch) return content;
116
+
117
+ const yamlContent = yamlMatch[1];
118
+ const yamlStartIndex = content.indexOf(yamlMatch[0]);
119
+ const yamlEndIndex = yamlStartIndex + yamlMatch[0].length;
120
+
121
+ // Parse YAML and remove root and IDE-FILE-RESOLUTION properties
122
+ try {
123
+ const yaml = require("js-yaml");
124
+ const parsed = yaml.load(yamlContent);
125
+
126
+ // Remove the properties if they exist at root level
127
+ delete parsed.root;
128
+ delete parsed['IDE-FILE-RESOLUTION'];
129
+ delete parsed['REQUEST-RESOLUTION'];
130
+
131
+ // Also remove from activation-instructions if they exist
132
+ if (parsed['activation-instructions'] && Array.isArray(parsed['activation-instructions'])) {
133
+ parsed['activation-instructions'] = parsed['activation-instructions'].filter(instruction => {
134
+ return !instruction.startsWith('IDE-FILE-RESOLUTION:') &&
135
+ !instruction.startsWith('REQUEST-RESOLUTION:');
136
+ });
137
+ }
138
+
139
+ // Reconstruct the YAML
140
+ const cleanedYaml = yaml.dump(parsed, { lineWidth: -1 });
141
+
142
+ // Get the agent name from the YAML for the header
143
+ const agentName = parsed.agent?.id || 'agent';
144
+
145
+ // Build the new content with just the agent header and YAML
146
+ const newHeader = `# ${agentName}\n\nCRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n`;
147
+ const afterYaml = content.substring(yamlEndIndex);
148
+
149
+ return newHeader + "```yaml\n" + cleanedYaml.trim() + "\n```" + afterYaml;
150
+ } catch (error) {
151
+ console.warn("Failed to process agent YAML:", error.message);
152
+ // If parsing fails, return original content
153
+ return content;
154
+ }
107
155
  }
108
156
 
109
157
  formatSection(path, content) {
110
- const separator = '====================';
158
+ const separator = "====================";
159
+
160
+ // Process agent content if this is an agent file
161
+ if (path.startsWith("agents#")) {
162
+ content = this.processAgentContent(content);
163
+ }
164
+
111
165
  return [
112
166
  `${separator} START: ${path} ${separator}`,
113
167
  content.trim(),
114
168
  `${separator} END: ${path} ${separator}`,
115
- ''
116
- ].join('\n');
169
+ "",
170
+ ].join("\n");
117
171
  }
118
172
 
119
173
  async validate() {
120
- console.log('Validating agent configurations...');
174
+ console.log("Validating agent configurations...");
121
175
  const agents = await this.resolver.listAgents();
122
176
  for (const agentId of agents) {
123
177
  try {
@@ -129,7 +183,7 @@ class WebBuilder {
129
183
  }
130
184
  }
131
185
 
132
- console.log('\nValidating team configurations...');
186
+ console.log("\nValidating team configurations...");
133
187
  const teams = await this.resolver.listTeams();
134
188
  for (const teamId of teams) {
135
189
  try {
@@ -154,10 +208,8 @@ class WebBuilder {
154
208
  }
155
209
 
156
210
  async buildExpansionPack(packName, options = {}) {
157
- const packDir = path.join(this.rootDir, 'expansion-packs', packName);
158
- const outputDirs = [
159
- path.join(this.rootDir, 'dist', 'expansion-packs', packName)
160
- ];
211
+ const packDir = path.join(this.rootDir, "expansion-packs", packName);
212
+ const outputDirs = [path.join(this.rootDir, "dist", "expansion-packs", packName)];
161
213
 
162
214
  // Clean output directories if requested
163
215
  if (options.clean !== false) {
@@ -171,27 +223,27 @@ class WebBuilder {
171
223
  }
172
224
 
173
225
  // Build individual agents first
174
- const agentsDir = path.join(packDir, 'agents');
226
+ const agentsDir = path.join(packDir, "agents");
175
227
  try {
176
228
  const agentFiles = await fs.readdir(agentsDir);
177
- const agentMarkdownFiles = agentFiles.filter(f => f.endsWith('.md'));
178
-
229
+ const agentMarkdownFiles = agentFiles.filter((f) => f.endsWith(".md"));
230
+
179
231
  if (agentMarkdownFiles.length > 0) {
180
232
  console.log(` Building individual agents for ${packName}:`);
181
-
233
+
182
234
  for (const agentFile of agentMarkdownFiles) {
183
- const agentName = agentFile.replace('.md', '');
235
+ const agentName = agentFile.replace(".md", "");
184
236
  console.log(` - ${agentName}`);
185
-
237
+
186
238
  // Build individual agent bundle
187
239
  const bundle = await this.buildExpansionAgentBundle(packName, packDir, agentName);
188
-
240
+
189
241
  // Write to all output directories
190
242
  for (const outputDir of outputDirs) {
191
- const agentsOutputDir = path.join(outputDir, 'agents');
243
+ const agentsOutputDir = path.join(outputDir, "agents");
192
244
  await fs.mkdir(agentsOutputDir, { recursive: true });
193
245
  const outputFile = path.join(agentsOutputDir, `${agentName}.txt`);
194
- await fs.writeFile(outputFile, bundle, 'utf8');
246
+ await fs.writeFile(outputFile, bundle, "utf8");
195
247
  }
196
248
  }
197
249
  }
@@ -200,24 +252,24 @@ class WebBuilder {
200
252
  }
201
253
 
202
254
  // Build team bundle
203
- const agentTeamsDir = path.join(packDir, 'agent-teams');
255
+ const agentTeamsDir = path.join(packDir, "agent-teams");
204
256
  try {
205
257
  const teamFiles = await fs.readdir(agentTeamsDir);
206
- const teamFile = teamFiles.find(f => f.endsWith('.yml'));
207
-
258
+ const teamFile = teamFiles.find((f) => f.endsWith(".yml"));
259
+
208
260
  if (teamFile) {
209
261
  console.log(` Building team bundle for ${packName}`);
210
262
  const teamConfigPath = path.join(agentTeamsDir, teamFile);
211
-
263
+
212
264
  // Build expansion pack as a team bundle
213
265
  const bundle = await this.buildExpansionTeamBundle(packName, packDir, teamConfigPath);
214
-
266
+
215
267
  // Write to all output directories
216
268
  for (const outputDir of outputDirs) {
217
- const teamsOutputDir = path.join(outputDir, 'teams');
269
+ const teamsOutputDir = path.join(outputDir, "teams");
218
270
  await fs.mkdir(teamsOutputDir, { recursive: true });
219
- const outputFile = path.join(teamsOutputDir, teamFile.replace('.yml', '.txt'));
220
- await fs.writeFile(outputFile, bundle, 'utf8');
271
+ const outputFile = path.join(teamsOutputDir, teamFile.replace(".yml", ".txt"));
272
+ await fs.writeFile(outputFile, bundle, "utf8");
221
273
  console.log(` ✓ Created bundle: ${path.relative(this.rootDir, outputFile)}`);
222
274
  }
223
275
  } else {
@@ -229,49 +281,58 @@ class WebBuilder {
229
281
  }
230
282
 
231
283
  async buildExpansionAgentBundle(packName, packDir, agentName) {
232
- const template = await fs.readFile(this.templatePath, 'utf8');
284
+ const template = await fs.readFile(this.templatePath, "utf8");
233
285
  const sections = [template];
234
286
 
235
287
  // Add agent configuration
236
- const agentPath = path.join(packDir, 'agents', `${agentName}.md`);
237
- const agentContent = await fs.readFile(agentPath, 'utf8');
288
+ const agentPath = path.join(packDir, "agents", `${agentName}.md`);
289
+ const agentContent = await fs.readFile(agentPath, "utf8");
238
290
  sections.push(this.formatSection(`agents#${agentName}`, agentContent));
239
291
 
240
292
  // Resolve and add agent dependencies
241
293
  const agentYaml = agentContent.match(/```yaml\n([\s\S]*?)\n```/);
242
294
  if (agentYaml) {
243
295
  try {
244
- const yaml = require('js-yaml');
296
+ const yaml = require("js-yaml");
245
297
  const agentConfig = yaml.load(agentYaml[1]);
246
-
298
+
247
299
  if (agentConfig.dependencies) {
248
300
  // Add resources, first try expansion pack, then core
249
301
  for (const [resourceType, resources] of Object.entries(agentConfig.dependencies)) {
250
302
  if (Array.isArray(resources)) {
251
303
  for (const resourceName of resources) {
252
304
  let found = false;
253
- const extensions = ['.md', '.yml', '.yaml'];
254
-
305
+ const extensions = [".md", ".yml", ".yaml"];
306
+
255
307
  // Try expansion pack first
256
308
  for (const ext of extensions) {
257
309
  const resourcePath = path.join(packDir, resourceType, `${resourceName}${ext}`);
258
310
  try {
259
- const resourceContent = await fs.readFile(resourcePath, 'utf8');
260
- sections.push(this.formatSection(`${resourceType}#${resourceName}`, resourceContent));
311
+ const resourceContent = await fs.readFile(resourcePath, "utf8");
312
+ sections.push(
313
+ this.formatSection(`${resourceType}#${resourceName}`, resourceContent)
314
+ );
261
315
  found = true;
262
316
  break;
263
317
  } catch (error) {
264
318
  // Not in expansion pack, continue
265
319
  }
266
320
  }
267
-
321
+
268
322
  // If not found in expansion pack, try core
269
323
  if (!found) {
270
324
  for (const ext of extensions) {
271
- const corePath = path.join(this.rootDir, 'bmad-core', resourceType, `${resourceName}${ext}`);
325
+ const corePath = path.join(
326
+ this.rootDir,
327
+ "bmad-core",
328
+ resourceType,
329
+ `${resourceName}${ext}`
330
+ );
272
331
  try {
273
- const coreContent = await fs.readFile(corePath, 'utf8');
274
- sections.push(this.formatSection(`${resourceType}#${resourceName}`, coreContent));
332
+ const coreContent = await fs.readFile(corePath, "utf8");
333
+ sections.push(
334
+ this.formatSection(`${resourceType}#${resourceName}`, coreContent)
335
+ );
275
336
  found = true;
276
337
  break;
277
338
  } catch (error) {
@@ -279,9 +340,11 @@ class WebBuilder {
279
340
  }
280
341
  }
281
342
  }
282
-
343
+
283
344
  if (!found) {
284
- console.warn(` ⚠ Dependency ${resourceType}#${resourceName} not found in expansion pack or core`);
345
+ console.warn(
346
+ ` ⚠ Dependency ${resourceType}#${resourceName} not found in expansion pack or core`
347
+ );
285
348
  }
286
349
  }
287
350
  }
@@ -292,27 +355,27 @@ class WebBuilder {
292
355
  }
293
356
  }
294
357
 
295
- return sections.join('\n');
358
+ return sections.join("\n");
296
359
  }
297
360
 
298
361
  async buildExpansionTeamBundle(packName, packDir, teamConfigPath) {
299
- const template = await fs.readFile(this.templatePath, 'utf8');
362
+ const template = await fs.readFile(this.templatePath, "utf8");
300
363
 
301
364
  const sections = [template];
302
365
 
303
366
  // Add team configuration and parse to get agent list
304
- const teamContent = await fs.readFile(teamConfigPath, 'utf8');
305
- const teamFileName = path.basename(teamConfigPath, '.yml');
367
+ const teamContent = await fs.readFile(teamConfigPath, "utf8");
368
+ const teamFileName = path.basename(teamConfigPath, ".yml");
306
369
  const teamConfig = this.parseYaml(teamContent);
307
370
  sections.push(this.formatSection(`agent-teams#${teamFileName}`, teamContent));
308
371
 
309
372
  // Get list of expansion pack agents
310
373
  const expansionAgents = new Set();
311
- const agentsDir = path.join(packDir, 'agents');
374
+ const agentsDir = path.join(packDir, "agents");
312
375
  try {
313
376
  const agentFiles = await fs.readdir(agentsDir);
314
- for (const agentFile of agentFiles.filter(f => f.endsWith('.md'))) {
315
- const agentName = agentFile.replace('.md', '');
377
+ for (const agentFile of agentFiles.filter((f) => f.endsWith(".md"))) {
378
+ const agentName = agentFile.replace(".md", "");
316
379
  expansionAgents.add(agentName);
317
380
  }
318
381
  } catch (error) {
@@ -321,13 +384,15 @@ class WebBuilder {
321
384
 
322
385
  // Build a map of all available expansion pack resources for override checking
323
386
  const expansionResources = new Map();
324
- const resourceDirs = ['templates', 'tasks', 'checklists', 'workflows', 'data'];
387
+ const resourceDirs = ["templates", "tasks", "checklists", "workflows", "data"];
325
388
  for (const resourceDir of resourceDirs) {
326
389
  const resourcePath = path.join(packDir, resourceDir);
327
390
  try {
328
391
  const resourceFiles = await fs.readdir(resourcePath);
329
- for (const resourceFile of resourceFiles.filter(f => f.endsWith('.md') || f.endsWith('.yml'))) {
330
- const fileName = resourceFile.replace(/\.(md|yml)$/, '');
392
+ for (const resourceFile of resourceFiles.filter(
393
+ (f) => f.endsWith(".md") || f.endsWith(".yml")
394
+ )) {
395
+ const fileName = resourceFile.replace(/\.(md|yml)$/, "");
331
396
  expansionResources.set(`${resourceDir}#${fileName}`, true);
332
397
  }
333
398
  } catch (error) {
@@ -337,22 +402,21 @@ class WebBuilder {
337
402
 
338
403
  // Process all agents listed in team configuration
339
404
  const agentsToProcess = teamConfig.agents || [];
340
-
405
+
341
406
  // Ensure bmad-orchestrator is always included for teams
342
- if (!agentsToProcess.includes('bmad-orchestrator')) {
407
+ if (!agentsToProcess.includes("bmad-orchestrator")) {
343
408
  console.warn(` ⚠ Team ${teamFileName} missing bmad-orchestrator, adding automatically`);
344
- agentsToProcess.unshift('bmad-orchestrator');
409
+ agentsToProcess.unshift("bmad-orchestrator");
345
410
  }
346
411
 
347
412
  // Track all dependencies from all agents (deduplicated)
348
413
  const allDependencies = new Map();
349
414
 
350
415
  for (const agentId of agentsToProcess) {
351
-
352
416
  if (expansionAgents.has(agentId)) {
353
417
  // Use expansion pack version (override)
354
418
  const agentPath = path.join(agentsDir, `${agentId}.md`);
355
- const agentContent = await fs.readFile(agentPath, 'utf8');
419
+ const agentContent = await fs.readFile(agentPath, "utf8");
356
420
  sections.push(this.formatSection(`agents#${agentId}`, agentContent));
357
421
 
358
422
  // Parse and collect dependencies from expansion agent
@@ -379,8 +443,8 @@ class WebBuilder {
379
443
  } else {
380
444
  // Use core BMAD version
381
445
  try {
382
- const coreAgentPath = path.join(this.rootDir, 'bmad-core', 'agents', `${agentId}.md`);
383
- const coreAgentContent = await fs.readFile(coreAgentPath, 'utf8');
446
+ const coreAgentPath = path.join(this.rootDir, "bmad-core", "agents", `${agentId}.md`);
447
+ const coreAgentContent = await fs.readFile(coreAgentPath, "utf8");
384
448
  sections.push(this.formatSection(`agents#${agentId}`, coreAgentContent));
385
449
 
386
450
  // Parse and collect dependencies from core agent
@@ -389,8 +453,8 @@ class WebBuilder {
389
453
  try {
390
454
  // Clean up the YAML to handle command descriptions after dashes
391
455
  let yamlContent = agentYaml[1];
392
- yamlContent = yamlContent.replace(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, '$1$2');
393
-
456
+ yamlContent = yamlContent.replace(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, "$1$2");
457
+
394
458
  const agentConfig = this.parseYaml(yamlContent);
395
459
  if (agentConfig.dependencies) {
396
460
  for (const [resourceType, resources] of Object.entries(agentConfig.dependencies)) {
@@ -418,15 +482,15 @@ class WebBuilder {
418
482
  // Always prefer expansion pack versions if they exist
419
483
  for (const [key, dep] of allDependencies) {
420
484
  let found = false;
421
- const extensions = ['.md', '.yml', '.yaml'];
422
-
485
+ const extensions = [".md", ".yml", ".yaml"];
486
+
423
487
  // Always check expansion pack first, even if the dependency came from a core agent
424
488
  if (expansionResources.has(key)) {
425
489
  // We know it exists in expansion pack, find and load it
426
490
  for (const ext of extensions) {
427
491
  const expansionPath = path.join(packDir, dep.type, `${dep.name}${ext}`);
428
492
  try {
429
- const content = await fs.readFile(expansionPath, 'utf8');
493
+ const content = await fs.readFile(expansionPath, "utf8");
430
494
  sections.push(this.formatSection(key, content));
431
495
  console.log(` ✓ Using expansion override for ${key}`);
432
496
  found = true;
@@ -436,13 +500,13 @@ class WebBuilder {
436
500
  }
437
501
  }
438
502
  }
439
-
503
+
440
504
  // If not found in expansion pack (or doesn't exist there), try core
441
505
  if (!found) {
442
506
  for (const ext of extensions) {
443
- const corePath = path.join(this.rootDir, 'bmad-core', dep.type, `${dep.name}${ext}`);
507
+ const corePath = path.join(this.rootDir, "bmad-core", dep.type, `${dep.name}${ext}`);
444
508
  try {
445
- const content = await fs.readFile(corePath, 'utf8');
509
+ const content = await fs.readFile(corePath, "utf8");
446
510
  sections.push(this.formatSection(key, content));
447
511
  found = true;
448
512
  break;
@@ -451,7 +515,7 @@ class WebBuilder {
451
515
  }
452
516
  }
453
517
  }
454
-
518
+
455
519
  if (!found) {
456
520
  console.warn(` ⚠ Dependency ${key} not found in expansion pack or core`);
457
521
  }
@@ -462,11 +526,13 @@ class WebBuilder {
462
526
  const resourcePath = path.join(packDir, resourceDir);
463
527
  try {
464
528
  const resourceFiles = await fs.readdir(resourcePath);
465
- for (const resourceFile of resourceFiles.filter(f => f.endsWith('.md') || f.endsWith('.yml'))) {
529
+ for (const resourceFile of resourceFiles.filter(
530
+ (f) => f.endsWith(".md") || f.endsWith(".yml")
531
+ )) {
466
532
  const filePath = path.join(resourcePath, resourceFile);
467
- const fileContent = await fs.readFile(filePath, 'utf8');
468
- const fileName = resourceFile.replace(/\.(md|yml)$/, '');
469
-
533
+ const fileContent = await fs.readFile(filePath, "utf8");
534
+ const fileName = resourceFile.replace(/\.(md|yml)$/, "");
535
+
470
536
  // Only add if not already included as a dependency
471
537
  const resourceKey = `${resourceDir}#${fileName}`;
472
538
  if (!allDependencies.has(resourceKey)) {
@@ -478,18 +544,16 @@ class WebBuilder {
478
544
  }
479
545
  }
480
546
 
481
- return sections.join('\n');
547
+ return sections.join("\n");
482
548
  }
483
549
 
484
550
  async listExpansionPacks() {
485
- const expansionPacksDir = path.join(this.rootDir, 'expansion-packs');
551
+ const expansionPacksDir = path.join(this.rootDir, "expansion-packs");
486
552
  try {
487
553
  const entries = await fs.readdir(expansionPacksDir, { withFileTypes: true });
488
- return entries
489
- .filter(entry => entry.isDirectory())
490
- .map(entry => entry.name);
554
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
491
555
  } catch (error) {
492
- console.warn('No expansion-packs directory found');
556
+ console.warn("No expansion-packs directory found");
493
557
  return [];
494
558
  }
495
559
  }
@@ -499,4 +563,4 @@ class WebBuilder {
499
563
  }
500
564
  }
501
565
 
502
- module.exports = WebBuilder;
566
+ module.exports = WebBuilder;
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { program } = require('commander');
4
+ const path = require('path');
4
5
 
5
6
  // Dynamic imports for ES modules
6
7
  let chalk, inquirer;
@@ -57,7 +58,9 @@ program
57
58
  if (!options.full && !options.agent && !options.team && !options.expansionOnly) {
58
59
  // Interactive mode
59
60
  const answers = await promptInstallation();
60
- await installer.install(answers);
61
+ if (!answers._alreadyInstalled) {
62
+ await installer.install(answers);
63
+ }
61
64
  } else {
62
65
  // Direct mode
63
66
  let installType = 'full';
@@ -158,6 +161,35 @@ async function promptInstallation() {
158
161
  ]);
159
162
  answers.directory = directory;
160
163
 
164
+ // Check if this is an existing v4 installation
165
+ const installDir = path.resolve(answers.directory);
166
+ const state = await installer.detectInstallationState(installDir);
167
+
168
+ if (state.type === 'v4_existing') {
169
+ console.log(chalk.yellow('\n🔍 Found existing BMAD v4 installation'));
170
+ console.log(` Directory: ${installDir}`);
171
+ console.log(` Version: ${state.manifest?.version || 'Unknown'}`);
172
+ console.log(` Installed: ${state.manifest?.installed_at ? new Date(state.manifest.installed_at).toLocaleDateString() : 'Unknown'}`);
173
+
174
+ const { shouldUpdate } = await inquirer.prompt([
175
+ {
176
+ type: 'confirm',
177
+ name: 'shouldUpdate',
178
+ message: 'Would you like to update your existing BMAD v4 installation?',
179
+ default: true
180
+ }
181
+ ]);
182
+
183
+ if (shouldUpdate) {
184
+ // Skip other prompts and go directly to update
185
+ answers.installType = 'update';
186
+ answers._alreadyInstalled = true; // Flag to prevent double installation
187
+ await installer.install(answers);
188
+ return answers; // Return the answers object
189
+ }
190
+ // If user doesn't want to update, continue with normal flow
191
+ }
192
+
161
193
  // Ask for installation type
162
194
  const { installType } = await inquirer.prompt([
163
195
  {
@@ -373,7 +405,7 @@ async function promptInstallation() {
373
405
  type: 'input',
374
406
  name: 'webBundlesDirectory',
375
407
  message: 'Enter directory for web bundles:',
376
- default: `${directory}/web-bundles`,
408
+ default: `${answers.directory}/web-bundles`,
377
409
  validate: (input) => {
378
410
  if (!input.trim()) {
379
411
  return 'Please enter a valid directory path';
@@ -2,6 +2,7 @@ const fs = require("fs-extra");
2
2
  const path = require("path");
3
3
  const crypto = require("crypto");
4
4
  const glob = require("glob");
5
+ const yaml = require("js-yaml");
5
6
 
6
7
  // Dynamic import for ES module
7
8
  let chalk;
@@ -106,7 +107,7 @@ class FileManager {
106
107
 
107
108
  // Write manifest
108
109
  await fs.ensureDir(path.dirname(manifestPath));
109
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
110
+ await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));
110
111
 
111
112
  return manifest;
112
113
  }
@@ -120,7 +121,7 @@ class FileManager {
120
121
 
121
122
  try {
122
123
  const content = await fs.readFile(manifestPath, "utf8");
123
- return JSON.parse(content);
124
+ return yaml.load(content);
124
125
  } catch (error) {
125
126
  return null;
126
127
  }