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,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const registry = require('./migrations/registry');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* BMAD-Enhanced Migrate CLI
|
|
8
|
+
* Manual migration control for advanced users
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
// No args - show available migrations
|
|
15
|
+
if (args.length === 0) {
|
|
16
|
+
showAvailableMigrations();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const migrationName = args[0];
|
|
21
|
+
|
|
22
|
+
// Find migration
|
|
23
|
+
const migrations = registry.getAllMigrations();
|
|
24
|
+
const migration = migrations.find(m => m.name === migrationName);
|
|
25
|
+
|
|
26
|
+
if (!migration) {
|
|
27
|
+
console.error('');
|
|
28
|
+
console.error(chalk.red(`Migration '${migrationName}' not found.`));
|
|
29
|
+
console.error('');
|
|
30
|
+
console.error('Run ' + chalk.cyan('npx bmad-migrate') + ' to see available migrations.');
|
|
31
|
+
console.error('');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Load migration module
|
|
36
|
+
if (!migration.module) {
|
|
37
|
+
try {
|
|
38
|
+
migration.module = require(`./migrations/${migration.name}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('');
|
|
41
|
+
console.error(chalk.red(`Failed to load migration: ${error.message}`));
|
|
42
|
+
console.error('');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Run migration
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.cyan.bold(`Running migration: ${migration.name}`));
|
|
50
|
+
console.log(chalk.gray(migration.description));
|
|
51
|
+
console.log('');
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const changes = await migration.module.apply();
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(chalk.green.bold('✓ Migration completed'));
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.cyan('Changes:'));
|
|
60
|
+
changes.forEach(change => {
|
|
61
|
+
console.log(chalk.gray(` - ${change}`));
|
|
62
|
+
});
|
|
63
|
+
console.log('');
|
|
64
|
+
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('');
|
|
67
|
+
console.error(chalk.red.bold('✗ Migration failed'));
|
|
68
|
+
console.error(chalk.red(error.message));
|
|
69
|
+
console.error('');
|
|
70
|
+
if (error.stack) {
|
|
71
|
+
console.error(chalk.gray(error.stack));
|
|
72
|
+
console.error('');
|
|
73
|
+
}
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Show available migrations
|
|
80
|
+
*/
|
|
81
|
+
function showAvailableMigrations() {
|
|
82
|
+
const migrations = registry.getAllMigrations();
|
|
83
|
+
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.bold('Available Migrations'));
|
|
86
|
+
console.log('');
|
|
87
|
+
|
|
88
|
+
if (migrations.length === 0) {
|
|
89
|
+
console.log(chalk.yellow('No migrations available'));
|
|
90
|
+
console.log('');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
migrations.forEach((m, index) => {
|
|
95
|
+
const breaking = m.breaking ? chalk.red('[BREAKING]') : chalk.green('[SAFE]');
|
|
96
|
+
console.log(` ${index + 1}. ${chalk.cyan(m.name)} ${breaking}`);
|
|
97
|
+
console.log(` ${chalk.gray(m.description)}`);
|
|
98
|
+
console.log(` ${chalk.gray(`${m.fromVersion} → ${m.toVersion}`)}`);
|
|
99
|
+
console.log('');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log('Usage: ' + chalk.cyan('npx bmad-migrate <migration-name>'));
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(chalk.yellow('Warning: Manual migrations bypass safety checks.'));
|
|
105
|
+
console.log(chalk.yellow(' Use ' + chalk.cyan('npx bmad-update') + ' for normal updates.'));
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Run main
|
|
110
|
+
main().catch(error => {
|
|
111
|
+
console.error('');
|
|
112
|
+
console.error(chalk.red('Unexpected error:'));
|
|
113
|
+
console.error(chalk.red(error.message));
|
|
114
|
+
console.error('');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const versionDetector = require('./lib/version-detector');
|
|
6
|
+
const migrationRunner = require('./lib/migration-runner');
|
|
7
|
+
const registry = require('./migrations/registry');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* BMAD-Enhanced Update CLI
|
|
11
|
+
* Main update command for users
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const dryRun = args.includes('--dry-run');
|
|
17
|
+
const yes = args.includes('--yes') || args.includes('-y');
|
|
18
|
+
const force = args.includes('--force');
|
|
19
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
20
|
+
|
|
21
|
+
// Header
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log(chalk.bold.magenta('╔════════════════════════════════════════╗'));
|
|
24
|
+
console.log(chalk.bold.magenta('║ BMAD-Enhanced Update Manager ║'));
|
|
25
|
+
console.log(chalk.bold.magenta('╚════════════════════════════════════════╝'));
|
|
26
|
+
console.log('');
|
|
27
|
+
|
|
28
|
+
// 1. Detect current state
|
|
29
|
+
const currentVersion = versionDetector.getCurrentVersion();
|
|
30
|
+
const targetVersion = versionDetector.getTargetVersion();
|
|
31
|
+
const scenario = versionDetector.detectInstallationScenario();
|
|
32
|
+
|
|
33
|
+
// Handle different scenarios
|
|
34
|
+
if (scenario === 'fresh') {
|
|
35
|
+
console.log(chalk.yellow('No previous installation detected.'));
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log('Run: ' + chalk.cyan('npx bmad-install-agents'));
|
|
38
|
+
console.log('');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (scenario === 'partial' || scenario === 'corrupted') {
|
|
43
|
+
console.log(chalk.red('Installation appears incomplete or corrupted.'));
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log('Recommend running: ' + chalk.cyan('npx bmad-install-agents'));
|
|
46
|
+
console.log('');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!currentVersion) {
|
|
51
|
+
console.log(chalk.yellow('Could not detect current version.'));
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log('Run: ' + chalk.cyan('npx bmad-install-agents'));
|
|
54
|
+
console.log('');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get migration path
|
|
59
|
+
const migrationPath = versionDetector.getMigrationPath(currentVersion, targetVersion);
|
|
60
|
+
|
|
61
|
+
// Already up to date
|
|
62
|
+
if (migrationPath.type === 'up-to-date') {
|
|
63
|
+
console.log(chalk.green(`✓ Already up to date! (v${currentVersion})`));
|
|
64
|
+
console.log('');
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Downgrade attempt
|
|
69
|
+
if (migrationPath.type === 'downgrade') {
|
|
70
|
+
console.log(chalk.red.bold('⚠ DOWNGRADE DETECTED'));
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(` Current version: ${currentVersion}`);
|
|
73
|
+
console.log(` Package version: ${targetVersion}`);
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(chalk.yellow('Downgrades are not officially supported.'));
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log('If you want to downgrade, please:');
|
|
78
|
+
console.log(' 1. Backup your installation');
|
|
79
|
+
console.log(' 2. Uninstall current version');
|
|
80
|
+
console.log(' 3. Install desired version');
|
|
81
|
+
console.log('');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Show migration plan
|
|
86
|
+
console.log(chalk.cyan('Migration Plan:'));
|
|
87
|
+
console.log(` From: ${chalk.red(currentVersion)}`);
|
|
88
|
+
console.log(` To: ${chalk.green(targetVersion)}`);
|
|
89
|
+
console.log('');
|
|
90
|
+
|
|
91
|
+
const migrations = registry.getMigrationsFor(currentVersion, targetVersion);
|
|
92
|
+
|
|
93
|
+
if (migrations.length === 0) {
|
|
94
|
+
console.log(chalk.yellow('No migrations needed (versions compatible)'));
|
|
95
|
+
console.log('');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.cyan('Migrations to apply:'));
|
|
100
|
+
migrations.forEach((m, i) => {
|
|
101
|
+
const icon = m.breaking ? chalk.red('⚠') : chalk.green('✓');
|
|
102
|
+
console.log(` ${i + 1}. ${icon} ${m.description}`);
|
|
103
|
+
});
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
// 3. Show breaking changes warning
|
|
107
|
+
const breakingChanges = registry.getBreakingChanges(currentVersion, targetVersion);
|
|
108
|
+
if (breakingChanges.length > 0) {
|
|
109
|
+
console.log(chalk.red.bold('⚠ BREAKING CHANGES:'));
|
|
110
|
+
breakingChanges.forEach(change => {
|
|
111
|
+
console.log(chalk.yellow(` - ${change}`));
|
|
112
|
+
});
|
|
113
|
+
console.log('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. Dry run - preview only
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
console.log(chalk.yellow.bold('DRY RUN - Previewing changes'));
|
|
119
|
+
console.log('');
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await migrationRunner.runMigrations(currentVersion, targetVersion, { dryRun: true, verbose });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(chalk.red('Error during preview:'), error.message);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 5. Confirm with user (unless --yes)
|
|
132
|
+
if (!yes) {
|
|
133
|
+
console.log(chalk.cyan('Your data will be backed up automatically before migration.'));
|
|
134
|
+
console.log('');
|
|
135
|
+
|
|
136
|
+
const confirmed = await confirm('Proceed with migration?');
|
|
137
|
+
|
|
138
|
+
if (!confirmed) {
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log(chalk.yellow('Migration cancelled.'));
|
|
141
|
+
console.log('');
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 6. Run migrations
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(chalk.cyan.bold('Starting migration...'));
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const result = await migrationRunner.runMigrations(currentVersion, targetVersion, { verbose });
|
|
152
|
+
|
|
153
|
+
// 7. Show success report
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(chalk.green.bold('✓ Migration completed successfully!'));
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(chalk.cyan('Changes applied:'));
|
|
158
|
+
result.results.forEach(r => {
|
|
159
|
+
console.log(chalk.green(` ✓ ${r.name}`));
|
|
160
|
+
if (verbose) {
|
|
161
|
+
r.changes.forEach(change => {
|
|
162
|
+
console.log(chalk.gray(` - ${change}`));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(chalk.gray(`Backup location: ${result.backupMetadata.backup_dir}`));
|
|
168
|
+
console.log('');
|
|
169
|
+
|
|
170
|
+
} catch (error) {
|
|
171
|
+
// Error already logged by migration-runner
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Confirm action with user
|
|
178
|
+
* @param {string} message - Confirmation message
|
|
179
|
+
* @returns {Promise<boolean>} True if user confirms
|
|
180
|
+
*/
|
|
181
|
+
async function confirm(message) {
|
|
182
|
+
const rl = readline.createInterface({
|
|
183
|
+
input: process.stdin,
|
|
184
|
+
output: process.stdout
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return new Promise(resolve => {
|
|
188
|
+
rl.question(chalk.yellow(`${message} [y/N]: `), answer => {
|
|
189
|
+
rl.close();
|
|
190
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Run main
|
|
196
|
+
main().catch(error => {
|
|
197
|
+
console.error('');
|
|
198
|
+
console.error(chalk.red.bold('Unexpected error:'));
|
|
199
|
+
console.error(chalk.red(error.message));
|
|
200
|
+
console.error('');
|
|
201
|
+
if (error.stack) {
|
|
202
|
+
console.error(chalk.gray(error.stack));
|
|
203
|
+
console.error('');
|
|
204
|
+
}
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
const versionDetector = require('./lib/version-detector');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* BMAD-Enhanced Version CLI
|
|
11
|
+
* Show version information and migration history
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const currentVersion = versionDetector.getCurrentVersion();
|
|
16
|
+
const targetVersion = versionDetector.getTargetVersion();
|
|
17
|
+
const scenario = versionDetector.detectInstallationScenario();
|
|
18
|
+
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log(chalk.bold('BMAD-Enhanced Version Information'));
|
|
21
|
+
console.log('');
|
|
22
|
+
|
|
23
|
+
// Fresh install - not installed yet
|
|
24
|
+
if (scenario === 'fresh' || !currentVersion) {
|
|
25
|
+
console.log(chalk.yellow('Status: Not installed'));
|
|
26
|
+
console.log(`Package version: ${chalk.cyan(targetVersion)}`);
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log('Run: ' + chalk.cyan('npx bmad-install-agents'));
|
|
29
|
+
console.log('');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Installed
|
|
34
|
+
console.log(`Installed version: ${chalk.cyan(currentVersion)}`);
|
|
35
|
+
console.log(`Package version: ${chalk.cyan(targetVersion)}`);
|
|
36
|
+
console.log('');
|
|
37
|
+
|
|
38
|
+
// Status
|
|
39
|
+
if (currentVersion === targetVersion) {
|
|
40
|
+
console.log(chalk.green('Status: ✓ Up to date'));
|
|
41
|
+
} else if (versionDetector.compareVersions(currentVersion, targetVersion) < 0) {
|
|
42
|
+
console.log(chalk.yellow('Status: ⚠ Update available'));
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log('Run: ' + chalk.cyan('npx bmad-update --dry-run') + ' (to preview)');
|
|
45
|
+
console.log(' ' + chalk.cyan('npx bmad-update') + ' (to apply)');
|
|
46
|
+
} else {
|
|
47
|
+
console.log(chalk.yellow(`Status: Package version (${targetVersion}) is older than installed (${currentVersion})`));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Show migration history
|
|
51
|
+
const migrationHistory = await getMigrationHistory();
|
|
52
|
+
if (migrationHistory && migrationHistory.length > 0) {
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(chalk.cyan('Migration History:'));
|
|
55
|
+
migrationHistory.forEach((entry, index) => {
|
|
56
|
+
const timestamp = new Date(entry.timestamp).toLocaleDateString();
|
|
57
|
+
console.log(chalk.gray(` ${index + 1}. ${entry.from_version} → ${entry.to_version} (${timestamp})`));
|
|
58
|
+
if (entry.migrations_applied && entry.migrations_applied.length > 0) {
|
|
59
|
+
entry.migrations_applied.forEach(m => {
|
|
60
|
+
console.log(chalk.gray(` - ${m}`));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get migration history from config.yaml
|
|
71
|
+
* @returns {Promise<Array|null>} Migration history or null
|
|
72
|
+
*/
|
|
73
|
+
async function getMigrationHistory() {
|
|
74
|
+
const configPath = path.join(process.cwd(), '_bmad/bme/_vortex/config.yaml');
|
|
75
|
+
|
|
76
|
+
if (!fs.existsSync(configPath)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
82
|
+
const config = yaml.load(configContent);
|
|
83
|
+
|
|
84
|
+
return config.migration_history || null;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Run main
|
|
91
|
+
main().catch(error => {
|
|
92
|
+
console.error('Error:', error.message);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Backup Manager for BMAD-Enhanced
|
|
8
|
+
* Creates backups before migrations and restores on failure
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a backup of critical installation files
|
|
13
|
+
* @param {string} version - Current version being backed up
|
|
14
|
+
* @returns {Promise<object>} Backup metadata
|
|
15
|
+
*/
|
|
16
|
+
async function createBackup(version) {
|
|
17
|
+
const timestamp = Date.now();
|
|
18
|
+
const backupDir = path.join(
|
|
19
|
+
process.cwd(),
|
|
20
|
+
'_bmad-output/.backups',
|
|
21
|
+
`backup-${version}-${timestamp}`
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
console.log(`Creating backup in: ${backupDir}`);
|
|
25
|
+
|
|
26
|
+
// Ensure backup directory exists
|
|
27
|
+
await ensureBackupDirectory();
|
|
28
|
+
|
|
29
|
+
// Create backup directory
|
|
30
|
+
await fs.ensureDir(backupDir);
|
|
31
|
+
|
|
32
|
+
const filesToBackup = getFilesToBackup();
|
|
33
|
+
const backedUpFiles = [];
|
|
34
|
+
|
|
35
|
+
// Copy each file/directory to backup
|
|
36
|
+
for (const file of filesToBackup) {
|
|
37
|
+
const sourcePath = path.join(process.cwd(), file.path);
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(sourcePath)) {
|
|
40
|
+
console.log(` Skipping ${file.path} (does not exist)`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const destPath = path.join(backupDir, file.name);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (file.type === 'file') {
|
|
48
|
+
await fs.copy(sourcePath, destPath);
|
|
49
|
+
} else if (file.type === 'directory') {
|
|
50
|
+
await fs.copy(sourcePath, destPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
backedUpFiles.push(file.path);
|
|
54
|
+
console.log(` ✓ Backed up: ${file.path}`);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(` ✗ Failed to backup ${file.path}:`, error.message);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Count user data files (for integrity check)
|
|
62
|
+
const userDataCount = await countUserDataFiles();
|
|
63
|
+
|
|
64
|
+
// Create backup manifest
|
|
65
|
+
const manifest = {
|
|
66
|
+
version,
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
timestampMs: timestamp,
|
|
69
|
+
files_backed_up: backedUpFiles,
|
|
70
|
+
user_data_count: userDataCount,
|
|
71
|
+
backup_dir: backupDir
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await fs.writeJson(path.join(backupDir, 'backup-manifest.json'), manifest, { spaces: 2 });
|
|
75
|
+
|
|
76
|
+
console.log(` ✓ Backup manifest created`);
|
|
77
|
+
console.log(` ✓ Backup complete: ${backedUpFiles.length} items backed up`);
|
|
78
|
+
|
|
79
|
+
return manifest;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Restore from backup after migration failure
|
|
84
|
+
* @param {object} backupMetadata - Metadata from createBackup
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
*/
|
|
87
|
+
async function restoreBackup(backupMetadata) {
|
|
88
|
+
const backupDir = backupMetadata.backup_dir;
|
|
89
|
+
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(`Restoring from backup: ${backupDir}`);
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(backupDir)) {
|
|
94
|
+
throw new Error(`Backup directory not found: ${backupDir}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const filesToRestore = getFilesToBackup();
|
|
98
|
+
|
|
99
|
+
for (const file of filesToRestore) {
|
|
100
|
+
const sourcePath = path.join(backupDir, file.name);
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(sourcePath)) {
|
|
103
|
+
console.log(` Skipping ${file.name} (not in backup)`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const destPath = path.join(process.cwd(), file.path);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Remove existing file/directory first
|
|
111
|
+
if (fs.existsSync(destPath)) {
|
|
112
|
+
await fs.remove(destPath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Restore from backup
|
|
116
|
+
await fs.copy(sourcePath, destPath);
|
|
117
|
+
console.log(` ✓ Restored: ${file.path}`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(` ✗ Failed to restore ${file.path}:`, error.message);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(` ✓ Restoration complete`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* List available backups
|
|
129
|
+
* @returns {Promise<Array>} List of backup metadata
|
|
130
|
+
*/
|
|
131
|
+
async function listBackups() {
|
|
132
|
+
const backupsDir = path.join(process.cwd(), '_bmad-output/.backups');
|
|
133
|
+
|
|
134
|
+
if (!fs.existsSync(backupsDir)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const entries = await fs.readdir(backupsDir);
|
|
139
|
+
const backups = [];
|
|
140
|
+
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const backupPath = path.join(backupsDir, entry);
|
|
143
|
+
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
144
|
+
|
|
145
|
+
if (fs.existsSync(manifestPath)) {
|
|
146
|
+
try {
|
|
147
|
+
const manifest = await fs.readJson(manifestPath);
|
|
148
|
+
backups.push(manifest);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.warn(`Could not read manifest for ${entry}:`, error.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Sort by timestamp (newest first)
|
|
156
|
+
backups.sort((a, b) => b.timestampMs - a.timestampMs);
|
|
157
|
+
|
|
158
|
+
return backups;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Clean up old backups, keeping only the most recent N
|
|
163
|
+
* @param {number} keepCount - Number of backups to keep
|
|
164
|
+
* @returns {Promise<number>} Number of backups deleted
|
|
165
|
+
*/
|
|
166
|
+
async function cleanupOldBackups(keepCount = 5) {
|
|
167
|
+
const backups = await listBackups();
|
|
168
|
+
|
|
169
|
+
if (backups.length <= keepCount) {
|
|
170
|
+
return 0; // Nothing to clean up
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const toDelete = backups.slice(keepCount);
|
|
174
|
+
let deletedCount = 0;
|
|
175
|
+
|
|
176
|
+
for (const backup of toDelete) {
|
|
177
|
+
try {
|
|
178
|
+
await fs.remove(backup.backup_dir);
|
|
179
|
+
console.log(` Deleted old backup: ${path.basename(backup.backup_dir)}`);
|
|
180
|
+
deletedCount++;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.warn(` Could not delete backup ${backup.backup_dir}:`, error.message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return deletedCount;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Ensure backup directory exists
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
async function ensureBackupDirectory() {
|
|
194
|
+
const backupDir = path.join(process.cwd(), '_bmad-output/.backups');
|
|
195
|
+
|
|
196
|
+
if (!fs.existsSync(backupDir)) {
|
|
197
|
+
const outputDir = path.join(process.cwd(), '_bmad-output');
|
|
198
|
+
|
|
199
|
+
if (!fs.existsSync(outputDir)) {
|
|
200
|
+
throw new Error('_bmad-output directory not found. Is BMAD Method installed?');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await fs.ensureDir(backupDir);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get list of files/directories to backup
|
|
209
|
+
* @returns {Array} List of file/directory definitions
|
|
210
|
+
*/
|
|
211
|
+
function getFilesToBackup() {
|
|
212
|
+
return [
|
|
213
|
+
{
|
|
214
|
+
name: 'config.yaml',
|
|
215
|
+
path: '_bmad/bme/_vortex/config.yaml',
|
|
216
|
+
type: 'file'
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'agents',
|
|
220
|
+
path: '_bmad/bme/_vortex/agents',
|
|
221
|
+
type: 'directory'
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'workflows',
|
|
225
|
+
path: '_bmad/bme/_vortex/workflows',
|
|
226
|
+
type: 'directory'
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'agent-manifest.csv',
|
|
230
|
+
path: '_bmad/_config/agent-manifest.csv',
|
|
231
|
+
type: 'file'
|
|
232
|
+
}
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Count user data files in _bmad-output (for integrity check)
|
|
238
|
+
* @returns {Promise<number>} Number of user files
|
|
239
|
+
*/
|
|
240
|
+
async function countUserDataFiles() {
|
|
241
|
+
const outputDir = path.join(process.cwd(), '_bmad-output');
|
|
242
|
+
|
|
243
|
+
if (!fs.existsSync(outputDir)) {
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let count = 0;
|
|
248
|
+
|
|
249
|
+
async function countRecursive(dir) {
|
|
250
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
251
|
+
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
const fullPath = path.join(dir, entry.name);
|
|
254
|
+
|
|
255
|
+
// Skip .backups and .logs directories
|
|
256
|
+
if (entry.name === '.backups' || entry.name === '.logs') {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (entry.isDirectory()) {
|
|
261
|
+
await countRecursive(fullPath);
|
|
262
|
+
} else if (entry.isFile()) {
|
|
263
|
+
count++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await countRecursive(outputDir);
|
|
269
|
+
return count;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
createBackup,
|
|
274
|
+
restoreBackup,
|
|
275
|
+
listBackups,
|
|
276
|
+
cleanupOldBackups,
|
|
277
|
+
ensureBackupDirectory,
|
|
278
|
+
countUserDataFiles
|
|
279
|
+
};
|