bmad-enhanced 1.1.2 → 1.3.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.
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const yaml = require('js-yaml');
6
+
7
+ /**
8
+ * Config Merger for BMAD-Enhanced
9
+ * Smart YAML merging preserving user settings
10
+ */
11
+
12
+ /**
13
+ * Merge current config with new template while preserving user preferences
14
+ * @param {string} currentConfigPath - Path to current config.yaml
15
+ * @param {string} newVersion - New version to set
16
+ * @param {object} updates - Updates to apply (agents, workflows, etc.)
17
+ * @returns {Promise<object>} Merged config object
18
+ */
19
+ async function mergeConfig(currentConfigPath, newVersion, updates = {}) {
20
+ let current = {};
21
+
22
+ // Read current config if it exists
23
+ if (fs.existsSync(currentConfigPath)) {
24
+ try {
25
+ const currentContent = fs.readFileSync(currentConfigPath, 'utf8');
26
+ current = yaml.load(currentContent);
27
+ } catch (error) {
28
+ console.warn('Warning: Could not parse current config.yaml, using defaults');
29
+ current = {};
30
+ }
31
+ }
32
+
33
+ // Extract user preferences
34
+ const userPrefs = extractUserPreferences(current);
35
+
36
+ // Start with current config
37
+ const merged = { ...current };
38
+
39
+ // Update version (system field)
40
+ merged.version = newVersion;
41
+
42
+ // Apply updates
43
+ if (updates.agents) {
44
+ merged.agents = updates.agents;
45
+ }
46
+
47
+ if (updates.workflows) {
48
+ merged.workflows = updates.workflows;
49
+ }
50
+
51
+ // Preserve user preferences
52
+ Object.assign(merged, userPrefs);
53
+
54
+ // Ensure migration_history exists
55
+ if (!merged.migration_history) {
56
+ merged.migration_history = [];
57
+ }
58
+
59
+ return merged;
60
+ }
61
+
62
+ /**
63
+ * Extract user-specific preferences from config
64
+ * @param {object} config - Config object
65
+ * @returns {object} User preferences
66
+ */
67
+ function extractUserPreferences(config) {
68
+ const prefs = {};
69
+
70
+ // Preserve these fields if they exist and are not default placeholders
71
+ if (config.user_name && config.user_name !== '{user}') {
72
+ prefs.user_name = config.user_name;
73
+ }
74
+
75
+ if (config.communication_language && config.communication_language !== 'en') {
76
+ prefs.communication_language = config.communication_language;
77
+ }
78
+
79
+ if (config.output_folder && config.output_folder !== '{project-root}/_bmad-output/vortex-artifacts') {
80
+ prefs.output_folder = config.output_folder;
81
+ }
82
+
83
+ if (config.hasOwnProperty('party_mode_enabled')) {
84
+ prefs.party_mode_enabled = config.party_mode_enabled;
85
+ }
86
+
87
+ if (config.migration_history) {
88
+ prefs.migration_history = config.migration_history;
89
+ }
90
+
91
+ return prefs;
92
+ }
93
+
94
+ /**
95
+ * Validate merged config structure
96
+ * @param {object} config - Config to validate
97
+ * @returns {object} Validation result { valid: boolean, errors: [] }
98
+ */
99
+ function validateConfig(config) {
100
+ const errors = [];
101
+
102
+ // Required fields
103
+ const requiredFields = [
104
+ 'submodule_name',
105
+ 'description',
106
+ 'module',
107
+ 'version',
108
+ 'output_folder',
109
+ 'agents',
110
+ 'workflows'
111
+ ];
112
+
113
+ for (const field of requiredFields) {
114
+ if (!config.hasOwnProperty(field)) {
115
+ errors.push(`Missing required field: ${field}`);
116
+ }
117
+ }
118
+
119
+ // Validate version format (x.x.x)
120
+ if (config.version && !/^\d+\.\d+\.\d+$/.test(config.version)) {
121
+ errors.push(`Invalid version format: ${config.version} (expected x.x.x)`);
122
+ }
123
+
124
+ // Validate agents is array
125
+ if (config.agents && !Array.isArray(config.agents)) {
126
+ errors.push('agents must be an array');
127
+ }
128
+
129
+ // Validate workflows is array
130
+ if (config.workflows && !Array.isArray(config.workflows)) {
131
+ errors.push('workflows must be an array');
132
+ }
133
+
134
+ // Validate migration_history structure
135
+ if (config.migration_history) {
136
+ if (!Array.isArray(config.migration_history)) {
137
+ errors.push('migration_history must be an array');
138
+ } else {
139
+ config.migration_history.forEach((entry, index) => {
140
+ if (!entry.timestamp || !entry.from_version || !entry.to_version) {
141
+ errors.push(`migration_history[${index}] missing required fields`);
142
+ }
143
+ });
144
+ }
145
+ }
146
+
147
+ return {
148
+ valid: errors.length === 0,
149
+ errors
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Write config to file
155
+ * @param {string} configPath - Path to write config
156
+ * @param {object} config - Config object
157
+ * @returns {Promise<void>}
158
+ */
159
+ async function writeConfig(configPath, config) {
160
+ const yamlContent = yaml.dump(config, {
161
+ indent: 2,
162
+ lineWidth: -1, // Don't wrap long lines
163
+ noRefs: true
164
+ });
165
+
166
+ await fs.writeFile(configPath, yamlContent, 'utf8');
167
+ }
168
+
169
+ /**
170
+ * Add migration history entry
171
+ * @param {object} config - Config object
172
+ * @param {string} fromVersion - Version migrating from
173
+ * @param {string} toVersion - Version migrating to
174
+ * @param {Array<string>} migrationsApplied - List of migration names applied
175
+ * @returns {object} Updated config
176
+ */
177
+ function addMigrationHistory(config, fromVersion, toVersion, migrationsApplied) {
178
+ if (!config.migration_history) {
179
+ config.migration_history = [];
180
+ }
181
+
182
+ config.migration_history.push({
183
+ timestamp: new Date().toISOString(),
184
+ from_version: fromVersion,
185
+ to_version: toVersion,
186
+ migrations_applied: migrationsApplied
187
+ });
188
+
189
+ return config;
190
+ }
191
+
192
+ /**
193
+ * Get default config template for a version
194
+ * @param {string} version - Version to generate template for
195
+ * @returns {object} Default config template
196
+ */
197
+ function getDefaultConfig(version) {
198
+ return {
199
+ submodule_name: '_vortex',
200
+ description: 'Contextualize and Externalize streams - Strategic framing and validated learning',
201
+ module: 'bme',
202
+ version,
203
+ output_folder: '{project-root}/_bmad-output/vortex-artifacts',
204
+ user_name: '{user}',
205
+ communication_language: 'en',
206
+ agents: [
207
+ 'contextualization-expert',
208
+ 'lean-experiments-specialist'
209
+ ],
210
+ workflows: [
211
+ 'lean-persona',
212
+ 'product-vision',
213
+ 'contextualize-scope',
214
+ 'mvp',
215
+ 'lean-experiment',
216
+ 'proof-of-concept',
217
+ 'proof-of-value'
218
+ ],
219
+ party_mode_enabled: true,
220
+ core_module: 'bme',
221
+ migration_history: []
222
+ };
223
+ }
224
+
225
+ module.exports = {
226
+ mergeConfig,
227
+ extractUserPreferences,
228
+ validateConfig,
229
+ writeConfig,
230
+ addMigrationHistory,
231
+ getDefaultConfig
232
+ };
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const backupManager = require('./backup-manager');
7
+ const versionDetector = require('./version-detector');
8
+ const configMerger = require('./config-merger');
9
+ const validator = require('./validator');
10
+ const registry = require('../migrations/registry');
11
+
12
+ /**
13
+ * Migration Runner for BMAD-Enhanced
14
+ * Core orchestration: executes migrations, handles backups, manages rollback
15
+ */
16
+
17
+ /**
18
+ * Run migrations from current version to target version
19
+ * @param {string} fromVersion - Current version
20
+ * @param {string} toVersion - Target version
21
+ * @param {object} options - Options { dryRun, verbose }
22
+ * @returns {Promise<object>} Migration result
23
+ */
24
+ async function runMigrations(fromVersion, toVersion, options = {}) {
25
+ const { dryRun = false, verbose = false } = options;
26
+
27
+ console.log('');
28
+ if (dryRun) {
29
+ console.log(chalk.yellow.bold('═══ DRY RUN MODE ═══'));
30
+ console.log(chalk.yellow('No changes will be made to your installation'));
31
+ console.log('');
32
+ }
33
+
34
+ // 1. Get applicable migrations
35
+ const migrations = registry.getMigrationsFor(fromVersion, toVersion);
36
+
37
+ if (migrations.length === 0) {
38
+ console.log(chalk.yellow('No migrations needed'));
39
+ return { success: true, migrations: [], skipped: true };
40
+ }
41
+
42
+ console.log(chalk.cyan(`Found ${migrations.length} migration(s) to apply:`));
43
+ migrations.forEach((m, i) => {
44
+ const icon = m.breaking ? chalk.red('⚠') : chalk.green('✓');
45
+ console.log(` ${i + 1}. ${icon} ${m.name} - ${m.description}`);
46
+ });
47
+ console.log('');
48
+
49
+ // If dry run, just preview
50
+ if (dryRun) {
51
+ return await previewMigrations(migrations);
52
+ }
53
+
54
+ // 2. Acquire migration lock
55
+ await acquireMigrationLock();
56
+
57
+ let backupMetadata = null;
58
+ const results = [];
59
+
60
+ try {
61
+ // 3. Create backup
62
+ console.log(chalk.cyan('[1/5] Creating backup...'));
63
+ const userDataCount = await backupManager.countUserDataFiles();
64
+ backupMetadata = await backupManager.createBackup(fromVersion);
65
+ backupMetadata.userDataCount = userDataCount;
66
+ console.log(chalk.green(`✓ Backup created: ${path.basename(backupMetadata.backup_dir)}`));
67
+ console.log('');
68
+
69
+ // 4. Execute migrations sequentially
70
+ console.log(chalk.cyan('[2/5] Running migrations...'));
71
+ for (let i = 0; i < migrations.length; i++) {
72
+ const migration = migrations[i];
73
+ console.log(chalk.cyan(`\nMigration ${i + 1}/${migrations.length}: ${migration.name}`));
74
+
75
+ try {
76
+ const changes = await executeMigration(migration, { verbose });
77
+ results.push({
78
+ name: migration.name,
79
+ success: true,
80
+ changes
81
+ });
82
+
83
+ console.log(chalk.green(`✓ ${migration.name} completed`));
84
+ } catch (error) {
85
+ console.error(chalk.red(`✗ ${migration.name} failed: ${error.message}`));
86
+ throw new MigrationError(migration.name, error);
87
+ }
88
+ }
89
+ console.log('');
90
+ console.log(chalk.green('✓ All migrations completed'));
91
+ console.log('');
92
+
93
+ // 5. Update configuration and migration history
94
+ console.log(chalk.cyan('[3/5] Updating configuration...'));
95
+ await updateMigrationHistory(fromVersion, toVersion, results);
96
+ console.log(chalk.green('✓ Migration history updated'));
97
+ console.log('');
98
+
99
+ // 6. Validate installation
100
+ console.log(chalk.cyan('[4/5] Validating installation...'));
101
+ const validationResult = await validator.validateInstallation(backupMetadata);
102
+
103
+ // Display validation results
104
+ validationResult.checks.forEach(check => {
105
+ if (check.passed) {
106
+ console.log(chalk.green(` ✓ ${check.name}`));
107
+ if (check.info || check.warning) {
108
+ console.log(chalk.gray(` ${check.info || check.warning}`));
109
+ }
110
+ } else {
111
+ console.log(chalk.red(` ✗ ${check.name}`));
112
+ if (check.error) {
113
+ console.log(chalk.red(` Error: ${check.error}`));
114
+ }
115
+ }
116
+ });
117
+ console.log('');
118
+
119
+ if (!validationResult.valid) {
120
+ throw new Error('Installation validation failed');
121
+ }
122
+
123
+ console.log(chalk.green('✓ Installation validated'));
124
+ console.log('');
125
+
126
+ // 7. Cleanup old backups
127
+ console.log(chalk.cyan('[5/5] Cleanup...'));
128
+ const deletedCount = await backupManager.cleanupOldBackups(5);
129
+ if (deletedCount > 0) {
130
+ console.log(chalk.green(`✓ Cleaned up ${deletedCount} old backup(s)`));
131
+ } else {
132
+ console.log(chalk.green('✓ No old backups to clean up'));
133
+ }
134
+ console.log('');
135
+
136
+ // Release lock
137
+ await releaseMigrationLock();
138
+
139
+ // Create migration log
140
+ await createMigrationLog(fromVersion, toVersion, results, backupMetadata);
141
+
142
+ return {
143
+ success: true,
144
+ fromVersion,
145
+ toVersion,
146
+ results,
147
+ backupMetadata
148
+ };
149
+
150
+ } catch (error) {
151
+ console.error('');
152
+ console.error(chalk.red.bold('✗ Migration failed!'));
153
+ console.error('');
154
+ console.error(chalk.red(error.message));
155
+ console.error('');
156
+
157
+ // Rollback if we have a backup
158
+ if (backupMetadata) {
159
+ console.log(chalk.yellow('Restoring from backup...'));
160
+ try {
161
+ await backupManager.restoreBackup(backupMetadata);
162
+ console.log(chalk.green('✓ Installation restored from backup'));
163
+ console.log('');
164
+ } catch (restoreError) {
165
+ console.error(chalk.red('✗ Restore failed!'));
166
+ console.error(chalk.red(restoreError.message));
167
+ console.error('');
168
+ console.error(chalk.yellow(`Manual restore may be needed from: ${backupMetadata.backup_dir}`));
169
+ console.error('');
170
+ }
171
+ }
172
+
173
+ // Release lock
174
+ await releaseMigrationLock();
175
+
176
+ // Create error log
177
+ await createErrorLog(fromVersion, toVersion, error, backupMetadata);
178
+
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Preview migrations without applying
185
+ * @param {Array} migrations - Migrations to preview
186
+ * @returns {Promise<object>} Preview result
187
+ */
188
+ async function previewMigrations(migrations) {
189
+ const previews = [];
190
+
191
+ for (const migration of migrations) {
192
+ console.log(chalk.cyan(`\n${migration.name}:`));
193
+ console.log(chalk.gray(migration.description));
194
+
195
+ if (migration.module && migration.module.preview) {
196
+ const preview = await migration.module.preview();
197
+ console.log('');
198
+ console.log(chalk.white('Actions:'));
199
+ preview.actions.forEach(action => {
200
+ console.log(chalk.gray(` - ${action}`));
201
+ });
202
+
203
+ previews.push({
204
+ name: migration.name,
205
+ preview
206
+ });
207
+ }
208
+ }
209
+
210
+ console.log('');
211
+ console.log(chalk.green('To apply these changes, run:'));
212
+ console.log(chalk.cyan(' npx bmad-update'));
213
+ console.log('');
214
+
215
+ return {
216
+ success: true,
217
+ dryRun: true,
218
+ previews
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Execute a single migration
224
+ * @param {object} migration - Migration to execute
225
+ * @param {object} options - Options { verbose }
226
+ * @returns {Promise<Array<string>>} Changes made
227
+ */
228
+ async function executeMigration(migration, options = {}) {
229
+ const { verbose = false } = options;
230
+
231
+ if (!migration.module || !migration.module.apply) {
232
+ throw new Error(`Migration ${migration.name} has no apply function`);
233
+ }
234
+
235
+ const changes = await migration.module.apply();
236
+
237
+ if (verbose) {
238
+ changes.forEach(change => {
239
+ console.log(chalk.gray(` - ${change}`));
240
+ });
241
+ }
242
+
243
+ return changes;
244
+ }
245
+
246
+ /**
247
+ * Update migration history in config.yaml
248
+ * @param {string} fromVersion - Version migrated from
249
+ * @param {string} toVersion - Version migrated to
250
+ * @param {Array} results - Migration results
251
+ */
252
+ async function updateMigrationHistory(fromVersion, toVersion, results) {
253
+ const configPath = path.join(process.cwd(), '_bmad/bme/_vortex/config.yaml');
254
+
255
+ if (!fs.existsSync(configPath)) {
256
+ throw new Error('config.yaml not found');
257
+ }
258
+
259
+ const configContent = fs.readFileSync(configPath, 'utf8');
260
+ const yaml = require('js-yaml');
261
+ const config = yaml.load(configContent);
262
+
263
+ // Add migration history entry
264
+ const migrationsApplied = results.map(r => r.name);
265
+ const updatedConfig = configMerger.addMigrationHistory(
266
+ config,
267
+ fromVersion,
268
+ toVersion,
269
+ migrationsApplied
270
+ );
271
+
272
+ // Write config
273
+ await configMerger.writeConfig(configPath, updatedConfig);
274
+ }
275
+
276
+ /**
277
+ * Acquire migration lock to prevent concurrent migrations
278
+ */
279
+ async function acquireMigrationLock() {
280
+ const lockFile = path.join(process.cwd(), '_bmad-output/.migration-lock');
281
+
282
+ if (fs.existsSync(lockFile)) {
283
+ const lock = await fs.readJson(lockFile);
284
+ const age = Date.now() - lock.timestamp;
285
+
286
+ // Stale lock (older than 5 minutes)
287
+ if (age > 5 * 60 * 1000) {
288
+ console.log(chalk.yellow('Removing stale migration lock'));
289
+ await fs.remove(lockFile);
290
+ } else {
291
+ throw new Error('Migration already in progress. Please wait and try again.');
292
+ }
293
+ }
294
+
295
+ // Create lock
296
+ await fs.ensureDir(path.join(process.cwd(), '_bmad-output'));
297
+ await fs.writeJson(lockFile, {
298
+ timestamp: Date.now(),
299
+ pid: process.pid
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Release migration lock
305
+ */
306
+ async function releaseMigrationLock() {
307
+ const lockFile = path.join(process.cwd(), '_bmad-output/.migration-lock');
308
+
309
+ if (fs.existsSync(lockFile)) {
310
+ await fs.remove(lockFile);
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Create migration log
316
+ * @param {string} fromVersion - Version migrated from
317
+ * @param {string} toVersion - Version migrated to
318
+ * @param {Array} results - Migration results
319
+ * @param {object} backupMetadata - Backup metadata
320
+ */
321
+ async function createMigrationLog(fromVersion, toVersion, results, backupMetadata) {
322
+ const logsDir = path.join(process.cwd(), '_bmad-output/.logs');
323
+ await fs.ensureDir(logsDir);
324
+
325
+ const timestamp = Date.now();
326
+ const logFile = path.join(logsDir, `migration-${timestamp}.log`);
327
+
328
+ const logContent = [
329
+ `BMAD-Enhanced Migration Log`,
330
+ `Date: ${new Date().toISOString()}`,
331
+ `From Version: ${fromVersion}`,
332
+ `To Version: ${toVersion}`,
333
+ '',
334
+ 'Migrations Applied:',
335
+ ...results.map(r => ` - ${r.name}`),
336
+ '',
337
+ 'Changes:',
338
+ ...results.flatMap(r => r.changes.map(c => ` - ${c}`)),
339
+ '',
340
+ `Backup: ${backupMetadata.backup_dir}`,
341
+ '',
342
+ 'Status: SUCCESS'
343
+ ].join('\n');
344
+
345
+ await fs.writeFile(logFile, logContent, 'utf8');
346
+ }
347
+
348
+ /**
349
+ * Create error log
350
+ * @param {string} fromVersion - Version migrated from
351
+ * @param {string} toVersion - Version migrated to
352
+ * @param {Error} error - Error that occurred
353
+ * @param {object} backupMetadata - Backup metadata (if exists)
354
+ */
355
+ async function createErrorLog(fromVersion, toVersion, error, backupMetadata) {
356
+ const logsDir = path.join(process.cwd(), '_bmad-output/.logs');
357
+ await fs.ensureDir(logsDir);
358
+
359
+ const timestamp = Date.now();
360
+ const logFile = path.join(logsDir, `migration-error-${timestamp}.log`);
361
+
362
+ const logContent = [
363
+ `BMAD-Enhanced Migration Error Log`,
364
+ `Date: ${new Date().toISOString()}`,
365
+ `From Version: ${fromVersion}`,
366
+ `To Version: ${toVersion}`,
367
+ '',
368
+ `Error: ${error.message}`,
369
+ '',
370
+ 'Stack Trace:',
371
+ error.stack,
372
+ '',
373
+ backupMetadata ? `Backup: ${backupMetadata.backup_dir}` : 'No backup created',
374
+ backupMetadata ? 'Status: ROLLED BACK' : 'Status: FAILED (no backup)',
375
+ '',
376
+ 'Please report this issue at: https://github.com/amalik/BMAD-Enhanced/issues',
377
+ 'Include this log file when reporting.'
378
+ ].join('\n');
379
+
380
+ await fs.writeFile(logFile, logContent, 'utf8');
381
+
382
+ console.log(chalk.gray(`Migration log: ${logFile}`));
383
+ }
384
+
385
+ /**
386
+ * Custom error for migration failures
387
+ */
388
+ class MigrationError extends Error {
389
+ constructor(migrationName, originalError) {
390
+ super(`Migration ${migrationName} failed: ${originalError.message}`);
391
+ this.name = 'MigrationError';
392
+ this.migrationName = migrationName;
393
+ this.originalError = originalError;
394
+ }
395
+ }
396
+
397
+ module.exports = {
398
+ runMigrations,
399
+ previewMigrations,
400
+ executeMigration,
401
+ MigrationError
402
+ };