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.
- package/README.md +178 -58
- package/package.json +7 -3
- package/scripts/install-all-agents.js +10 -12
- package/scripts/install-emma.js +5 -5
- package/scripts/install-wade.js +5 -5
- package/scripts/postinstall.js +100 -11
- package/scripts/update/bmad-migrate.js +116 -0
- package/scripts/update/bmad-update.js +206 -0
- package/scripts/update/bmad-version.js +94 -0
- package/scripts/update/lib/backup-manager.js +279 -0
- package/scripts/update/lib/config-merger.js +232 -0
- package/scripts/update/lib/migration-runner.js +402 -0
- package/scripts/update/lib/validator.js +430 -0
- package/scripts/update/lib/version-detector.js +246 -0
- package/scripts/update/migrations/1.0.x-to-1.3.0.js +265 -0
- package/scripts/update/migrations/1.1.x-to-1.3.0.js +166 -0
- package/scripts/update/migrations/1.2.x-to-1.3.0.js +166 -0
- package/scripts/update/migrations/registry.js +185 -0
|
@@ -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
|
+
};
|