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,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
+ };