bmad-method 4.20.0 → 4.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { program } = require('commander');
4
4
  const path = require('path');
5
+ const fs = require('fs').promises;
6
+ const yaml = require('js-yaml');
5
7
 
6
8
  // Dynamic imports for ES modules
7
9
  let chalk, inquirer;
@@ -45,17 +47,15 @@ program
45
47
  program
46
48
  .command('install')
47
49
  .description('Install BMAD Method agents and tools')
48
- .option('-f, --full', 'Install complete .bmad-core folder')
49
- .option('-a, --agent <agent>', 'Install specific agent with dependencies')
50
- .option('-t, --team <team>', 'Install specific team with required agents and dependencies')
50
+ .option('-f, --full', 'Install complete BMAD Method')
51
51
  .option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)')
52
- .option('-d, --directory <path>', 'Installation directory (default: .bmad-core)')
52
+ .option('-d, --directory <path>', 'Installation directory')
53
53
  .option('-i, --ide <ide...>', 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, roo, cline, gemini, other)')
54
54
  .option('-e, --expansion-packs <packs...>', 'Install specific expansion packs (can specify multiple)')
55
55
  .action(async (options) => {
56
56
  try {
57
57
  await initializeModules();
58
- if (!options.full && !options.agent && !options.team && !options.expansionOnly) {
58
+ if (!options.full && !options.expansionOnly) {
59
59
  // Interactive mode
60
60
  const answers = await promptInstallation();
61
61
  if (!answers._alreadyInstalled) {
@@ -64,15 +64,11 @@ program
64
64
  } else {
65
65
  // Direct mode
66
66
  let installType = 'full';
67
- if (options.agent) installType = 'single-agent';
68
- else if (options.team) installType = 'team';
69
- else if (options.expansionOnly) installType = 'expansion-only';
67
+ if (options.expansionOnly) installType = 'expansion-only';
70
68
 
71
69
  const config = {
72
70
  installType,
73
- agent: options.agent,
74
- team: options.team,
75
- directory: options.directory || '.bmad-core',
71
+ directory: options.directory || '.',
76
72
  ides: (options.ide || []).filter(ide => ide !== 'other'),
77
73
  expansionPacks: options.expansionPacks || []
78
74
  };
@@ -100,19 +96,6 @@ program
100
96
  }
101
97
  });
102
98
 
103
- program
104
- .command('list')
105
- .description('List available agents')
106
- .action(async () => {
107
- try {
108
- await installer.listAgents();
109
- } catch (error) {
110
- if (!chalk) await initializeModules();
111
- console.error(chalk.red('Error:'), error.message);
112
- process.exit(1);
113
- }
114
- });
115
-
116
99
  program
117
100
  .command('list:expansions')
118
101
  .description('List available expansion packs')
@@ -145,7 +128,7 @@ async function promptInstallation() {
145
128
 
146
129
  const answers = {};
147
130
 
148
- // Ask for installation directory
131
+ // Ask for installation directory first
149
132
  const { directory } = await inquirer.prompt([
150
133
  {
151
134
  type: 'input',
@@ -161,147 +144,85 @@ async function promptInstallation() {
161
144
  ]);
162
145
  answers.directory = directory;
163
146
 
164
- // Check if this is an existing v4 installation
165
- const installDir = path.resolve(answers.directory);
147
+ // Detect existing installations
148
+ const installDir = path.resolve(directory);
166
149
  const state = await installer.detectInstallationState(installDir);
167
-
150
+
151
+ // Check for existing expansion packs
152
+ const existingExpansionPacks = state.expansionPacks || {};
153
+
154
+ // Get available expansion packs
155
+ const availableExpansionPacks = await installer.getAvailableExpansionPacks();
156
+
157
+ // Build choices list
158
+ const choices = [];
159
+
160
+ // Load core config to get short-title
161
+ const coreConfigPath = path.join(__dirname, '..', '..', '..', 'bmad-core', 'core-config.yml');
162
+ const coreConfig = yaml.load(await fs.readFile(coreConfigPath, 'utf8'));
163
+ const coreShortTitle = coreConfig['short-title'] || 'BMad Agile Core System';
164
+
165
+ // Add BMAD core option
166
+ let bmadOptionText;
168
167
  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
168
+ const currentVersion = state.manifest?.version || 'unknown';
169
+ const newVersion = coreConfig.version || 'unknown'; // Use version from core-config.yml
170
+ const versionInfo = currentVersion === newVersion
171
+ ? `(v${currentVersion} - reinstall)`
172
+ : `(v${currentVersion} → v${newVersion})`;
173
+ bmadOptionText = `Update ${coreShortTitle} ${versionInfo} .bmad-core`;
174
+ } else {
175
+ bmadOptionText = `Install ${coreShortTitle} (v${coreConfig.version || version}) .bmad-core`;
191
176
  }
192
-
193
- // Ask for installation type
194
- const { installType } = await inquirer.prompt([
195
- {
196
- type: 'list',
197
- name: 'installType',
198
- message: 'How would you like to install BMAD?',
199
- choices: [
200
- {
201
- name: 'Complete installation (recommended) - All agents and tools',
202
- value: 'full'
203
- },
204
- {
205
- name: 'Team installation - Install a specific team with required agents',
206
- value: 'team'
207
- },
208
- {
209
- name: 'Single agent - Choose one agent to install',
210
- value: 'single-agent'
211
- },
212
- {
213
- name: 'Expansion packs only - Install expansion packs without bmad-core',
214
- value: 'expansion-only'
215
- }
216
- ]
177
+
178
+ choices.push({
179
+ name: bmadOptionText,
180
+ value: 'bmad-core',
181
+ checked: true
182
+ });
183
+
184
+ // Add expansion pack options
185
+ for (const pack of availableExpansionPacks) {
186
+ const existing = existingExpansionPacks[pack.id];
187
+ let packOptionText;
188
+
189
+ if (existing) {
190
+ const currentVersion = existing.manifest?.version || 'unknown';
191
+ const newVersion = pack.version;
192
+ const versionInfo = currentVersion === newVersion
193
+ ? `(v${currentVersion} - reinstall)`
194
+ : `(v${currentVersion} → v${newVersion})`;
195
+ packOptionText = `Update ${pack.description} ${versionInfo} .${pack.id}`;
196
+ } else {
197
+ packOptionText = `Install ${pack.description} (v${pack.version}) .${pack.id}`;
217
198
  }
218
- ]);
219
- answers.installType = installType;
220
-
221
- // If single agent, ask which one
222
- if (installType === 'single-agent') {
223
- const agents = await installer.getAvailableAgents();
224
- const { agent } = await inquirer.prompt([
225
- {
226
- type: 'list',
227
- name: 'agent',
228
- message: 'Select an agent to install:',
229
- choices: agents.map(a => ({
230
- name: `${a.id} - ${a.name} (${a.description})`,
231
- value: a.id
232
- }))
233
- }
234
- ]);
235
- answers.agent = agent;
236
- }
237
-
238
- // If team installation, ask which team
239
- if (installType === 'team') {
240
- const teams = await installer.getAvailableTeams();
241
- const { team } = await inquirer.prompt([
242
- {
243
- type: 'list',
244
- name: 'team',
245
- message: 'Select a team to install in your IDE project folder:',
246
- choices: teams.map(t => ({
247
- name: `${t.icon || 'šŸ“‹'} ${t.name}: ${t.description}`,
248
- value: t.id
249
- }))
250
- }
251
- ]);
252
- answers.team = team;
199
+
200
+ choices.push({
201
+ name: packOptionText,
202
+ value: pack.id,
203
+ checked: false
204
+ });
253
205
  }
254
-
255
- // Ask for expansion pack selection
256
- if (installType === 'full' || installType === 'team' || installType === 'expansion-only') {
257
- try {
258
- const availableExpansionPacks = await installer.getAvailableExpansionPacks();
259
-
260
- if (availableExpansionPacks.length > 0) {
261
- let choices;
262
- let message;
263
-
264
- if (installType === 'expansion-only') {
265
- message = 'Select expansion packs to install (required):'
266
- choices = availableExpansionPacks.map(pack => ({
267
- name: `${pack.name} - ${pack.description}`,
268
- value: pack.id
269
- }));
270
- } else {
271
- message = 'Select expansion packs to install (press Enter to skip, or check any to install):';
272
- choices = availableExpansionPacks.map(pack => ({
273
- name: `${pack.name} - ${pack.description}`,
274
- value: pack.id
275
- }));
206
+
207
+ // Ask what to install
208
+ const { selectedItems } = await inquirer.prompt([
209
+ {
210
+ type: 'checkbox',
211
+ name: 'selectedItems',
212
+ message: 'Select what to install/update (use space to select, enter to continue):',
213
+ choices: choices,
214
+ validate: (selected) => {
215
+ if (selected.length === 0) {
216
+ return 'Please select at least one item to install';
276
217
  }
277
-
278
- const { expansionPacks } = await inquirer.prompt([
279
- {
280
- type: 'checkbox',
281
- name: 'expansionPacks',
282
- message,
283
- choices,
284
- validate: installType === 'expansion-only' ? (answer) => {
285
- if (answer.length < 1) {
286
- return 'You must select at least one expansion pack for expansion-only installation.';
287
- }
288
- return true;
289
- } : undefined
290
- }
291
- ]);
292
-
293
- // Use selected expansion packs directly
294
- answers.expansionPacks = expansionPacks;
295
- } else {
296
- answers.expansionPacks = [];
218
+ return true;
297
219
  }
298
- } catch (error) {
299
- console.warn(chalk.yellow('Warning: Could not load expansion packs. Continuing without them.'));
300
- answers.expansionPacks = [];
301
220
  }
302
- } else {
303
- answers.expansionPacks = [];
304
- }
221
+ ]);
222
+
223
+ // Process selections
224
+ answers.installType = selectedItems.includes('bmad-core') ? 'full' : 'expansion-only';
225
+ answers.expansionPacks = selectedItems.filter(item => item !== 'bmad-core');
305
226
 
306
227
  // Ask for IDE configuration
307
228
  const { ides } = await inquirer.prompt([
@@ -83,12 +83,22 @@ class FileManager {
83
83
  this.manifestFile
84
84
  );
85
85
 
86
+ // Read version from core-config.yml
87
+ const coreConfigPath = path.join(__dirname, "../../../bmad-core/core-config.yml");
88
+ let coreVersion = "unknown";
89
+ try {
90
+ const coreConfigContent = await fs.readFile(coreConfigPath, "utf8");
91
+ const coreConfig = yaml.load(coreConfigContent);
92
+ coreVersion = coreConfig.version || "unknown";
93
+ } catch (error) {
94
+ console.warn("Could not read version from core-config.yml, using 'unknown'");
95
+ }
96
+
86
97
  const manifest = {
87
- version: require("../../../package.json").version,
98
+ version: coreVersion,
88
99
  installed_at: new Date().toISOString(),
89
100
  install_type: config.installType,
90
101
  agent: config.agent || null,
91
- ide_setup: config.ide || null,
92
102
  ides_setup: config.ides || [],
93
103
  expansion_packs: config.expansionPacks || [],
94
104
  files: [],
@@ -128,6 +138,21 @@ class FileManager {
128
138
  }
129
139
  }
130
140
 
141
+ async readExpansionPackManifest(installDir, packId) {
142
+ const manifestPath = path.join(
143
+ installDir,
144
+ `.${packId}`,
145
+ this.manifestFile
146
+ );
147
+
148
+ try {
149
+ const content = await fs.readFile(manifestPath, "utf8");
150
+ return yaml.load(content);
151
+ } catch (error) {
152
+ return null;
153
+ }
154
+ }
155
+
131
156
  async checkModifiedFiles(installDir, manifest) {
132
157
  const modified = [];
133
158
 
@@ -143,6 +168,33 @@ class FileManager {
143
168
  return modified;
144
169
  }
145
170
 
171
+ async checkFileIntegrity(installDir, manifest) {
172
+ const result = {
173
+ missing: [],
174
+ modified: []
175
+ };
176
+
177
+ for (const file of manifest.files) {
178
+ const filePath = path.join(installDir, file.path);
179
+
180
+ // Skip checking the manifest file itself - it will always be different due to timestamps
181
+ if (file.path.endsWith('install-manifest.yml')) {
182
+ continue;
183
+ }
184
+
185
+ if (!(await this.pathExists(filePath))) {
186
+ result.missing.push(file.path);
187
+ } else {
188
+ const currentHash = await this.calculateFileHash(filePath);
189
+ if (currentHash && currentHash !== file.hash) {
190
+ result.modified.push(file.path);
191
+ }
192
+ }
193
+ }
194
+
195
+ return result;
196
+ }
197
+
146
198
  async backupFile(filePath) {
147
199
  const backupPath = filePath + ".bak";
148
200
  let counter = 1;
@@ -183,6 +235,42 @@ class FileManager {
183
235
  async removeDirectory(dirPath) {
184
236
  await fs.remove(dirPath);
185
237
  }
238
+
239
+ async createExpansionPackManifest(installDir, packId, config, files) {
240
+ const manifestPath = path.join(
241
+ installDir,
242
+ `.${packId}`,
243
+ this.manifestFile
244
+ );
245
+
246
+ const manifest = {
247
+ version: config.expansionPackVersion || require("../../../package.json").version,
248
+ installed_at: new Date().toISOString(),
249
+ install_type: config.installType,
250
+ expansion_pack_id: config.expansionPackId,
251
+ expansion_pack_name: config.expansionPackName,
252
+ ides_setup: config.ides || [],
253
+ files: [],
254
+ };
255
+
256
+ // Add file information
257
+ for (const file of files) {
258
+ const filePath = path.join(installDir, file);
259
+ const hash = await this.calculateFileHash(filePath);
260
+
261
+ manifest.files.push({
262
+ path: file,
263
+ hash: hash,
264
+ modified: false,
265
+ });
266
+ }
267
+
268
+ // Write manifest
269
+ await fs.ensureDir(path.dirname(manifestPath));
270
+ await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));
271
+
272
+ return manifest;
273
+ }
186
274
  }
187
275
 
188
276
  module.exports = new FileManager();