aios-core 4.2.13 → 4.2.14
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/.aios-core/core/code-intel/helpers/dev-helper.js +206 -0
- package/.aios-core/core/registry/registry-schema.json +166 -166
- package/.aios-core/core/synapse/diagnostics/collectors/hook-collector.js +3 -3
- package/.aios-core/data/entity-registry.yaml +27 -0
- package/.aios-core/development/scripts/approval-workflow.js +642 -642
- package/.aios-core/development/scripts/backup-manager.js +606 -606
- package/.aios-core/development/scripts/branch-manager.js +389 -389
- package/.aios-core/development/scripts/code-quality-improver.js +1311 -1311
- package/.aios-core/development/scripts/commit-message-generator.js +849 -849
- package/.aios-core/development/scripts/conflict-resolver.js +674 -674
- package/.aios-core/development/scripts/dependency-analyzer.js +637 -637
- package/.aios-core/development/scripts/diff-generator.js +351 -351
- package/.aios-core/development/scripts/elicitation-engine.js +384 -384
- package/.aios-core/development/scripts/elicitation-session-manager.js +299 -299
- package/.aios-core/development/scripts/git-wrapper.js +461 -461
- package/.aios-core/development/scripts/manifest-preview.js +244 -244
- package/.aios-core/development/scripts/metrics-tracker.js +775 -775
- package/.aios-core/development/scripts/modification-validator.js +554 -554
- package/.aios-core/development/scripts/pattern-learner.js +1224 -1224
- package/.aios-core/development/scripts/performance-analyzer.js +757 -757
- package/.aios-core/development/scripts/refactoring-suggester.js +1138 -1138
- package/.aios-core/development/scripts/rollback-handler.js +530 -530
- package/.aios-core/development/scripts/security-checker.js +358 -358
- package/.aios-core/development/scripts/template-engine.js +239 -239
- package/.aios-core/development/scripts/template-validator.js +278 -278
- package/.aios-core/development/scripts/test-generator.js +843 -843
- package/.aios-core/development/scripts/transaction-manager.js +589 -589
- package/.aios-core/development/scripts/usage-tracker.js +673 -673
- package/.aios-core/development/scripts/validate-filenames.js +226 -226
- package/.aios-core/development/scripts/version-tracker.js +526 -526
- package/.aios-core/development/scripts/yaml-validator.js +396 -396
- package/.aios-core/development/tasks/build-autonomous.md +10 -4
- package/.aios-core/development/tasks/create-service.md +23 -0
- package/.aios-core/development/tasks/dev-develop-story.md +12 -6
- package/.aios-core/development/tasks/dev-suggest-refactoring.md +7 -1
- package/.aios-core/development/tasks/publish-npm.md +3 -3
- package/.aios-core/hooks/unified/README.md +1 -1
- package/.aios-core/install-manifest.yaml +65 -61
- package/.aios-core/manifests/schema/manifest-schema.json +190 -190
- package/.aios-core/product/templates/component-react-tmpl.tsx +98 -98
- package/.aios-core/product/templates/engine/schemas/adr.schema.json +102 -102
- package/.aios-core/product/templates/engine/schemas/dbdr.schema.json +205 -205
- package/.aios-core/product/templates/engine/schemas/epic.schema.json +175 -175
- package/.aios-core/product/templates/engine/schemas/pmdr.schema.json +175 -175
- package/.aios-core/product/templates/engine/schemas/prd-v2.schema.json +300 -300
- package/.aios-core/product/templates/engine/schemas/prd.schema.json +152 -152
- package/.aios-core/product/templates/engine/schemas/story.schema.json +222 -222
- package/.aios-core/product/templates/engine/schemas/task.schema.json +154 -154
- package/.aios-core/product/templates/eslintrc-security.json +32 -32
- package/.aios-core/product/templates/github-actions-cd.yml +212 -212
- package/.aios-core/product/templates/github-actions-ci.yml +172 -172
- package/.aios-core/product/templates/shock-report-tmpl.html +502 -502
- package/.aios-core/product/templates/token-exports-css-tmpl.css +240 -240
- package/.aios-core/quality/schemas/quality-metrics.schema.json +233 -233
- package/.aios-core/scripts/migrate-framework-docs.sh +300 -300
- package/README.en.md +747 -0
- package/README.md +4 -2
- package/bin/aios.js +7 -4
- package/package.json +1 -1
- package/packages/aios-pro-cli/src/recover.js +1 -1
- package/packages/installer/src/wizard/ide-config-generator.js +6 -6
- package/packages/installer/src/wizard/pro-setup.js +3 -3
- package/scripts/package-synapse.js +5 -5
- package/scripts/validate-package-completeness.js +3 -3
- package/.aios-core/.session/current-session.json +0 -14
- package/.aios-core/data/registry-update-log.jsonl +0 -191
- package/.aios-core/docs/SHARD-TRANSLATION-GUIDE.md +0 -335
- package/.aios-core/docs/component-creation-guide.md +0 -458
- package/.aios-core/docs/session-update-pattern.md +0 -307
- package/.aios-core/docs/standards/AIOS-FRAMEWORK-MASTER.md +0 -1963
- package/.aios-core/docs/standards/AIOS-LIVRO-DE-OURO-V2.1-SUMMARY.md +0 -1190
- package/.aios-core/docs/standards/AIOS-LIVRO-DE-OURO-V2.1.md +0 -439
- package/.aios-core/docs/standards/AIOS-LIVRO-DE-OURO.md +0 -5398
- package/.aios-core/docs/standards/V3-ARCHITECTURAL-DECISIONS.md +0 -523
- package/.aios-core/docs/template-syntax.md +0 -267
- package/.aios-core/docs/troubleshooting-guide.md +0 -625
- package/.aios-core/infrastructure/tests/utilities-audit-results.json +0 -501
- package/.aios-core/manifests/agents.csv +0 -29
- package/.aios-core/manifests/tasks.csv +0 -198
- package/.aios-core/manifests/workers.csv +0 -204
- package/.claude/rules/agent-authority.md +0 -105
- package/.claude/rules/coderabbit-integration.md +0 -93
- package/.claude/rules/ids-principles.md +0 -112
- package/.claude/rules/story-lifecycle.md +0 -139
- package/.claude/rules/workflow-execution.md +0 -150
- package/pro/README.md +0 -66
- package/pro/license/degradation.js +0 -220
- package/pro/license/errors.js +0 -450
- package/pro/license/feature-gate.js +0 -354
- package/pro/license/index.js +0 -181
- package/pro/license/license-api.js +0 -651
- package/pro/license/license-cache.js +0 -523
- package/pro/license/license-crypto.js +0 -303
- package/scripts/glue/README.md +0 -355
- package/scripts/glue/compose-agent-prompt.cjs +0 -362
- /package/.claude/hooks/{precompact-session-digest.js → precompact-session-digest.cjs} +0 -0
- /package/.claude/hooks/{synapse-engine.js → synapse-engine.cjs} +0 -0
|
@@ -1,531 +1,531 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rollback Handler for AIOS-FULLSTACK
|
|
3
|
-
* Handles undo operations for component transactions
|
|
4
|
-
* @module rollback-handler
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const TransactionManager = require('./transaction-manager');
|
|
8
|
-
const chalk = require('chalk');
|
|
9
|
-
const inquirer = require('inquirer');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const fs = require('fs').promises;
|
|
12
|
-
const ModificationValidator = require('./modification-validator');
|
|
13
|
-
|
|
14
|
-
class RollbackHandler {
|
|
15
|
-
constructor(options = {}) {
|
|
16
|
-
this.rootPath = options.rootPath || process.cwd();
|
|
17
|
-
this.transactionManager = new TransactionManager({ rootPath: this.rootPath });
|
|
18
|
-
this.modificationValidator = new ModificationValidator();
|
|
19
|
-
this.backupPath = path.join(this.rootPath, 'aios-core', '.backups');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Execute undo-last command
|
|
24
|
-
* @param {Object} options - Rollback options
|
|
25
|
-
* @returns {Promise<Object>} Rollback result
|
|
26
|
-
*/
|
|
27
|
-
async undoLast(options = {}) {
|
|
28
|
-
try {
|
|
29
|
-
let transaction;
|
|
30
|
-
|
|
31
|
-
if (options.transactionId) {
|
|
32
|
-
// Load specific transaction
|
|
33
|
-
transaction = await this.transactionManager.loadTransaction(options.transactionId);
|
|
34
|
-
if (!transaction) {
|
|
35
|
-
throw new Error(`Transaction not found: ${options.transactionId}`);
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
// Get last transaction
|
|
39
|
-
transaction = await this.transactionManager.getLastTransaction();
|
|
40
|
-
if (!transaction) {
|
|
41
|
-
console.log(chalk.yellow('No transactions found to rollback'));
|
|
42
|
-
return { success: false, error: 'No transactions found' };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Display transaction details
|
|
47
|
-
console.log(chalk.blue('\n📋 Transaction Details:'));
|
|
48
|
-
console.log(chalk.gray(`ID: ${transaction.id}`));
|
|
49
|
-
console.log(chalk.gray(`Type: ${transaction.type}`));
|
|
50
|
-
console.log(chalk.gray(`Date: ${new Date(transaction.startTime).toLocaleString()}`));
|
|
51
|
-
console.log(chalk.gray(`Status: ${transaction.status}`));
|
|
52
|
-
console.log(chalk.gray(`Operations: ${transaction.operations.length}`));
|
|
53
|
-
|
|
54
|
-
// Show operations
|
|
55
|
-
console.log(chalk.blue('\n📝 Operations to rollback:'));
|
|
56
|
-
for (const op of transaction.operations) {
|
|
57
|
-
const icon = this.getOperationIcon(op.type);
|
|
58
|
-
console.log(chalk.gray(` ${icon} ${op.type}: ${path.basename(op.path)}`));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Confirm rollback
|
|
62
|
-
if (!options.force) {
|
|
63
|
-
const { confirm } = await inquirer.prompt([{
|
|
64
|
-
type: 'confirm',
|
|
65
|
-
name: 'confirm',
|
|
66
|
-
message: 'Do you want to rollback this transaction?',
|
|
67
|
-
default: true
|
|
68
|
-
}]);
|
|
69
|
-
|
|
70
|
-
if (!confirm) {
|
|
71
|
-
console.log(chalk.yellow('Rollback cancelled'));
|
|
72
|
-
return { success: false, error: 'User cancelled' };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Execute rollback
|
|
77
|
-
console.log(chalk.blue('\n⚙️ Executing rollback...'));
|
|
78
|
-
|
|
79
|
-
const rollbackResult = await this.transactionManager.rollbackTransaction(
|
|
80
|
-
transaction.id,
|
|
81
|
-
{
|
|
82
|
-
continueOnError: options.continueOnError !== false
|
|
83
|
-
}
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// Display results
|
|
87
|
-
this.displayRollbackResults(rollbackResult);
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
success: rollbackResult.failed.length === 0,
|
|
91
|
-
result: rollbackResult
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.error(chalk.red(`\n❌ Rollback failed: ${error.message}`));
|
|
96
|
-
return {
|
|
97
|
-
success: false,
|
|
98
|
-
error: error.message
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* List recent transactions
|
|
105
|
-
* @param {number} limit - Number of transactions to show
|
|
106
|
-
* @returns {Promise<void>}
|
|
107
|
-
*/
|
|
108
|
-
async listTransactions(limit = 10) {
|
|
109
|
-
try {
|
|
110
|
-
const transactions = await this.transactionManager.listTransactions(limit);
|
|
111
|
-
|
|
112
|
-
if (transactions.length === 0) {
|
|
113
|
-
console.log(chalk.yellow('No transactions found'));
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
console.log(chalk.blue('\n📋 Recent Transactions:'));
|
|
118
|
-
console.log(chalk.gray('─'.repeat(80)));
|
|
119
|
-
|
|
120
|
-
for (const txn of transactions) {
|
|
121
|
-
const date = new Date(txn.startTime).toLocaleString();
|
|
122
|
-
const duration = txn.endTime
|
|
123
|
-
? `${new Date(txn.endTime) - new Date(txn.startTime)}ms`
|
|
124
|
-
: 'active';
|
|
125
|
-
|
|
126
|
-
console.log(chalk.white(`\nID: ${txn.id}`));
|
|
127
|
-
console.log(chalk.gray(`Type: ${txn.type}`));
|
|
128
|
-
console.log(chalk.gray(`Description: ${txn.description}`));
|
|
129
|
-
console.log(chalk.gray(`User: ${txn.user}`));
|
|
130
|
-
console.log(chalk.gray(`Date: ${date}`));
|
|
131
|
-
console.log(chalk.gray(`Status: ${this.getStatusColor(txn.status)}`));
|
|
132
|
-
console.log(chalk.gray(`Operations: ${txn.operations}`));
|
|
133
|
-
console.log(chalk.gray(`Duration: ${duration}`));
|
|
134
|
-
console.log(chalk.gray('─'.repeat(80)));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.error(chalk.red(`Failed to list transactions: ${error.message}`));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Execute selective rollback
|
|
144
|
-
* @param {string} transactionId - Transaction ID
|
|
145
|
-
* @param {Array<string>} operationIds - Specific operations to rollback
|
|
146
|
-
* @returns {Promise<Object>} Rollback result
|
|
147
|
-
*/
|
|
148
|
-
async selectiveRollback(transactionId, operationIds) {
|
|
149
|
-
try {
|
|
150
|
-
const transaction = await this.transactionManager.loadTransaction(transactionId);
|
|
151
|
-
if (!transaction) {
|
|
152
|
-
throw new Error(`Transaction not found: ${transactionId}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Filter operations
|
|
156
|
-
const selectedOps = transaction.operations.filter(op =>
|
|
157
|
-
operationIds.includes(op.id)
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
if (selectedOps.length === 0) {
|
|
161
|
-
throw new Error('No matching operations found');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
console.log(chalk.blue(`\n📝 Selective rollback: ${selectedOps.length} operations`));
|
|
165
|
-
|
|
166
|
-
// Create a new transaction for selective rollback
|
|
167
|
-
const rollbackTxnId = await this.transactionManager.beginTransaction({
|
|
168
|
-
type: 'selective_rollback',
|
|
169
|
-
description: `Selective rollback of ${transactionId}`,
|
|
170
|
-
metadata: {
|
|
171
|
-
originalTransaction: transactionId,
|
|
172
|
-
selectedOperations: operationIds
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const results = {
|
|
177
|
-
successful: [],
|
|
178
|
-
failed: [],
|
|
179
|
-
warnings: []
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// Rollback selected operations
|
|
183
|
-
for (const op of selectedOps) {
|
|
184
|
-
try {
|
|
185
|
-
await this.transactionManager.rollbackOperation(op, results);
|
|
186
|
-
} catch (error) {
|
|
187
|
-
results.failed.push({
|
|
188
|
-
operation: op.id,
|
|
189
|
-
error: error.message
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Commit rollback transaction
|
|
195
|
-
await this.transactionManager.commitTransaction(rollbackTxnId);
|
|
196
|
-
|
|
197
|
-
this.displayRollbackResults(results);
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
success: results.failed.length === 0,
|
|
201
|
-
result: results
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
} catch (error) {
|
|
205
|
-
console.error(chalk.red(`Selective rollback failed: ${error.message}`));
|
|
206
|
-
return {
|
|
207
|
-
success: false,
|
|
208
|
-
error: error.message
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Clean up old transactions
|
|
215
|
-
* @returns {Promise<number>} Number cleaned
|
|
216
|
-
*/
|
|
217
|
-
async cleanup() {
|
|
218
|
-
try {
|
|
219
|
-
console.log(chalk.blue('🧹 Cleaning up old transactions...'));
|
|
220
|
-
const cleaned = await this.transactionManager.cleanupOldTransactions();
|
|
221
|
-
console.log(chalk.green(`✅ Cleaned up ${cleaned} old transactions`));
|
|
222
|
-
return cleaned;
|
|
223
|
-
} catch (error) {
|
|
224
|
-
console.error(chalk.red(`Cleanup failed: ${error.message}`));
|
|
225
|
-
return 0;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Create backup before modification
|
|
231
|
-
* @param {string} componentType - Type of component
|
|
232
|
-
* @param {string} componentName - Name of component
|
|
233
|
-
* @param {string} content - Content to backup
|
|
234
|
-
* @returns {Promise<string>} Backup file path
|
|
235
|
-
*/
|
|
236
|
-
async createBackup(componentType, componentName, content) {
|
|
237
|
-
try {
|
|
238
|
-
// Ensure backup directory exists
|
|
239
|
-
await fs.mkdir(this.backupPath, { recursive: true });
|
|
240
|
-
|
|
241
|
-
// Create subdirectory for component type
|
|
242
|
-
const typeBackupPath = path.join(this.backupPath, componentType);
|
|
243
|
-
await fs.mkdir(typeBackupPath, { recursive: true });
|
|
244
|
-
|
|
245
|
-
// Generate backup filename with timestamp
|
|
246
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
247
|
-
const backupFilename = `${componentName}.${timestamp}.backup`;
|
|
248
|
-
const backupFilePath = path.join(typeBackupPath, backupFilename);
|
|
249
|
-
|
|
250
|
-
// Write backup
|
|
251
|
-
await fs.writeFile(backupFilePath, content, 'utf8');
|
|
252
|
-
|
|
253
|
-
// Record backup in transaction
|
|
254
|
-
if (this.currentTransactionId) {
|
|
255
|
-
await this.transactionManager.recordOperation({
|
|
256
|
-
type: 'backup_created',
|
|
257
|
-
path: backupFilePath,
|
|
258
|
-
componentType,
|
|
259
|
-
componentName,
|
|
260
|
-
timestamp
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return backupFilePath;
|
|
265
|
-
} catch (error) {
|
|
266
|
-
throw new Error(`Failed to create backup: ${error.message}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Restore from backup
|
|
272
|
-
* @param {string} backupPath - Path to backup file
|
|
273
|
-
* @param {string} targetPath - Path to restore to
|
|
274
|
-
* @returns {Promise<boolean>} Success status
|
|
275
|
-
*/
|
|
276
|
-
async restoreFromBackup(backupPath, targetPath) {
|
|
277
|
-
try {
|
|
278
|
-
// Verify backup exists
|
|
279
|
-
await fs.access(backupPath);
|
|
280
|
-
|
|
281
|
-
// Read backup content
|
|
282
|
-
const content = await fs.readFile(backupPath, 'utf8');
|
|
283
|
-
|
|
284
|
-
// Restore to target
|
|
285
|
-
await fs.writeFile(targetPath, content, 'utf8');
|
|
286
|
-
|
|
287
|
-
console.log(chalk.green(`✅ Restored from backup: ${path.basename(backupPath)}`));
|
|
288
|
-
return true;
|
|
289
|
-
} catch (error) {
|
|
290
|
-
console.error(chalk.red(`Failed to restore from backup: ${error.message}`));
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Validate modification before applying
|
|
297
|
-
* @param {string} componentType - Type of component
|
|
298
|
-
* @param {string} originalContent - Original content
|
|
299
|
-
* @param {string} modifiedContent - Modified content
|
|
300
|
-
* @returns {Promise<Object>} Validation result
|
|
301
|
-
*/
|
|
302
|
-
async validateModification(componentType, originalContent, modifiedContent) {
|
|
303
|
-
const validation = await this.modificationValidator.validateModification(
|
|
304
|
-
componentType,
|
|
305
|
-
originalContent,
|
|
306
|
-
modifiedContent
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
if (!validation.valid) {
|
|
310
|
-
console.log(chalk.red('\n❌ Modification validation failed:'));
|
|
311
|
-
validation.errors.forEach(error => {
|
|
312
|
-
console.log(chalk.red(` • ${error}`));
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (validation.warnings.length > 0) {
|
|
317
|
-
console.log(chalk.yellow('\n⚠️ Warnings:'));
|
|
318
|
-
validation.warnings.forEach(warning => {
|
|
319
|
-
console.log(chalk.yellow(` • ${warning}`));
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (validation.breakingChanges.length > 0) {
|
|
324
|
-
console.log(chalk.red('\n🚨 Breaking Changes Detected:'));
|
|
325
|
-
validation.breakingChanges.forEach(change => {
|
|
326
|
-
console.log(chalk.red(` • ${change.type}: ${change.impact}`));
|
|
327
|
-
if (change.items) {
|
|
328
|
-
console.log(chalk.red(` Items: ${change.items.join(', ')}`));
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return validation;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Rollback modification
|
|
338
|
-
* @param {Object} modificationData - Modification details
|
|
339
|
-
* @returns {Promise<Object>} Rollback result
|
|
340
|
-
*/
|
|
341
|
-
async rollbackModification(modificationData) {
|
|
342
|
-
const { componentType, componentName, backupPath, targetPath } = modificationData;
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
console.log(chalk.blue(`\n⏪ Rolling back ${componentType}: ${componentName}`));
|
|
346
|
-
|
|
347
|
-
// Restore from backup
|
|
348
|
-
const restored = await this.restoreFromBackup(backupPath, targetPath);
|
|
349
|
-
|
|
350
|
-
if (restored) {
|
|
351
|
-
// Record rollback
|
|
352
|
-
await this.transactionManager.recordOperation({
|
|
353
|
-
type: 'modification_rollback',
|
|
354
|
-
componentType,
|
|
355
|
-
componentName,
|
|
356
|
-
backupPath,
|
|
357
|
-
targetPath,
|
|
358
|
-
timestamp: new Date().toISOString()
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
success: true,
|
|
363
|
-
message: `Successfully rolled back ${componentType}: ${componentName}`
|
|
364
|
-
};
|
|
365
|
-
} else {
|
|
366
|
-
throw new Error('Restoration failed');
|
|
367
|
-
}
|
|
368
|
-
} catch (error) {
|
|
369
|
-
return {
|
|
370
|
-
success: false,
|
|
371
|
-
error: error.message
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* List available backups for a component
|
|
378
|
-
* @param {string} componentType - Type of component
|
|
379
|
-
* @param {string} componentName - Name of component
|
|
380
|
-
* @returns {Promise<Array>} List of backups
|
|
381
|
-
*/
|
|
382
|
-
async listBackups(componentType, componentName) {
|
|
383
|
-
try {
|
|
384
|
-
const typeBackupPath = path.join(this.backupPath, componentType);
|
|
385
|
-
const files = await fs.readdir(typeBackupPath);
|
|
386
|
-
|
|
387
|
-
const backups = files
|
|
388
|
-
.filter(file => file.startsWith(`${componentName}.`) && file.endsWith('.backup'))
|
|
389
|
-
.map(file => {
|
|
390
|
-
const match = file.match(/\.(\d{4}-\d{2}-\d{2}T[\d-]+Z)\.backup$/);
|
|
391
|
-
const timestamp = match ? match[1].replace(/-/g, ':') : 'unknown';
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
filename: file,
|
|
395
|
-
path: path.join(typeBackupPath, file),
|
|
396
|
-
timestamp: new Date(timestamp),
|
|
397
|
-
componentName,
|
|
398
|
-
componentType
|
|
399
|
-
};
|
|
400
|
-
})
|
|
401
|
-
.sort((a, b) => b.timestamp - a.timestamp);
|
|
402
|
-
|
|
403
|
-
return backups;
|
|
404
|
-
} catch (error) {
|
|
405
|
-
if (error.code === 'ENOENT') {
|
|
406
|
-
return [];
|
|
407
|
-
}
|
|
408
|
-
throw error;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Clean up old backups
|
|
414
|
-
* @param {number} daysToKeep - Number of days to keep backups
|
|
415
|
-
* @returns {Promise<number>} Number of backups deleted
|
|
416
|
-
*/
|
|
417
|
-
async cleanupBackups(daysToKeep = 30) {
|
|
418
|
-
try {
|
|
419
|
-
const cutoffDate = new Date();
|
|
420
|
-
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
421
|
-
|
|
422
|
-
let deletedCount = 0;
|
|
423
|
-
|
|
424
|
-
// Iterate through component types
|
|
425
|
-
const componentTypes = await fs.readdir(this.backupPath);
|
|
426
|
-
|
|
427
|
-
for (const componentType of componentTypes) {
|
|
428
|
-
const typePath = path.join(this.backupPath, componentType);
|
|
429
|
-
const stat = await fs.stat(typePath);
|
|
430
|
-
|
|
431
|
-
if (!stat.isDirectory()) continue;
|
|
432
|
-
|
|
433
|
-
const files = await fs.readdir(typePath);
|
|
434
|
-
|
|
435
|
-
for (const file of files) {
|
|
436
|
-
if (!file.endsWith('.backup')) continue;
|
|
437
|
-
|
|
438
|
-
const filePath = path.join(typePath, file);
|
|
439
|
-
const fileStat = await fs.stat(filePath);
|
|
440
|
-
|
|
441
|
-
if (fileStat.mtime < cutoffDate) {
|
|
442
|
-
await fs.unlink(filePath);
|
|
443
|
-
deletedCount++;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return deletedCount;
|
|
449
|
-
} catch (error) {
|
|
450
|
-
console.error(chalk.red(`Backup cleanup failed: ${error.message}`));
|
|
451
|
-
return 0;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Get operation icon
|
|
457
|
-
* @private
|
|
458
|
-
*/
|
|
459
|
-
getOperationIcon(type) {
|
|
460
|
-
const icons = {
|
|
461
|
-
create: '➕',
|
|
462
|
-
update: '✏️',
|
|
463
|
-
delete: '🗑️',
|
|
464
|
-
manifest_update: '📋',
|
|
465
|
-
metadata_update: '📊',
|
|
466
|
-
component_created: '📦',
|
|
467
|
-
backup_created: '💾',
|
|
468
|
-
modification_rollback: '⏪'
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
return icons[type] || '•';
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Get status color
|
|
476
|
-
* @private
|
|
477
|
-
*/
|
|
478
|
-
getStatusColor(status) {
|
|
479
|
-
switch (status) {
|
|
480
|
-
case 'active':
|
|
481
|
-
return chalk.yellow(status);
|
|
482
|
-
case 'committed':
|
|
483
|
-
return chalk.green(status);
|
|
484
|
-
case 'rolled_back':
|
|
485
|
-
return chalk.blue(status);
|
|
486
|
-
case 'failed':
|
|
487
|
-
return chalk.red(status);
|
|
488
|
-
default:
|
|
489
|
-
return status;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Display rollback results
|
|
495
|
-
* @private
|
|
496
|
-
*/
|
|
497
|
-
displayRollbackResults(results) {
|
|
498
|
-
console.log(chalk.blue('\n📊 Rollback Results:'));
|
|
499
|
-
|
|
500
|
-
if (results.successful.length > 0) {
|
|
501
|
-
console.log(chalk.green(`\n✅ Successful (${results.successful.length}):`));
|
|
502
|
-
results.successful.forEach(item => {
|
|
503
|
-
console.log(chalk.green(` ✓ ${item.action}: ${path.basename(item.path)}`));
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (results.warnings.length > 0) {
|
|
508
|
-
console.log(chalk.yellow(`\n⚠️ Warnings (${results.warnings.length}):`));
|
|
509
|
-
results.warnings.forEach(item => {
|
|
510
|
-
console.log(chalk.yellow(` ⚠ ${item.warning}: ${path.basename(item.path || 'N/A')}`));
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (results.failed.length > 0) {
|
|
515
|
-
console.log(chalk.red(`\n❌ Failed (${results.failed.length}):`));
|
|
516
|
-
results.failed.forEach(item => {
|
|
517
|
-
console.log(chalk.red(` ✗ ${item.operation}: ${item.error}`));
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Summary
|
|
522
|
-
const total = results.successful.length + results.failed.length;
|
|
523
|
-
const successRate = total > 0 ? (results.successful.length / total * 100).toFixed(0) : 0;
|
|
524
|
-
|
|
525
|
-
console.log(chalk.blue('\n📈 Summary:'));
|
|
526
|
-
console.log(chalk.gray(` Total operations: ${total}`));
|
|
527
|
-
console.log(chalk.gray(` Success rate: ${successRate}%`));
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Rollback Handler for AIOS-FULLSTACK
|
|
3
|
+
* Handles undo operations for component transactions
|
|
4
|
+
* @module rollback-handler
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const TransactionManager = require('./transaction-manager');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const inquirer = require('inquirer');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const ModificationValidator = require('./modification-validator');
|
|
13
|
+
|
|
14
|
+
class RollbackHandler {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.rootPath = options.rootPath || process.cwd();
|
|
17
|
+
this.transactionManager = new TransactionManager({ rootPath: this.rootPath });
|
|
18
|
+
this.modificationValidator = new ModificationValidator();
|
|
19
|
+
this.backupPath = path.join(this.rootPath, 'aios-core', '.backups');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute undo-last command
|
|
24
|
+
* @param {Object} options - Rollback options
|
|
25
|
+
* @returns {Promise<Object>} Rollback result
|
|
26
|
+
*/
|
|
27
|
+
async undoLast(options = {}) {
|
|
28
|
+
try {
|
|
29
|
+
let transaction;
|
|
30
|
+
|
|
31
|
+
if (options.transactionId) {
|
|
32
|
+
// Load specific transaction
|
|
33
|
+
transaction = await this.transactionManager.loadTransaction(options.transactionId);
|
|
34
|
+
if (!transaction) {
|
|
35
|
+
throw new Error(`Transaction not found: ${options.transactionId}`);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
// Get last transaction
|
|
39
|
+
transaction = await this.transactionManager.getLastTransaction();
|
|
40
|
+
if (!transaction) {
|
|
41
|
+
console.log(chalk.yellow('No transactions found to rollback'));
|
|
42
|
+
return { success: false, error: 'No transactions found' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Display transaction details
|
|
47
|
+
console.log(chalk.blue('\n📋 Transaction Details:'));
|
|
48
|
+
console.log(chalk.gray(`ID: ${transaction.id}`));
|
|
49
|
+
console.log(chalk.gray(`Type: ${transaction.type}`));
|
|
50
|
+
console.log(chalk.gray(`Date: ${new Date(transaction.startTime).toLocaleString()}`));
|
|
51
|
+
console.log(chalk.gray(`Status: ${transaction.status}`));
|
|
52
|
+
console.log(chalk.gray(`Operations: ${transaction.operations.length}`));
|
|
53
|
+
|
|
54
|
+
// Show operations
|
|
55
|
+
console.log(chalk.blue('\n📝 Operations to rollback:'));
|
|
56
|
+
for (const op of transaction.operations) {
|
|
57
|
+
const icon = this.getOperationIcon(op.type);
|
|
58
|
+
console.log(chalk.gray(` ${icon} ${op.type}: ${path.basename(op.path)}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Confirm rollback
|
|
62
|
+
if (!options.force) {
|
|
63
|
+
const { confirm } = await inquirer.prompt([{
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'confirm',
|
|
66
|
+
message: 'Do you want to rollback this transaction?',
|
|
67
|
+
default: true
|
|
68
|
+
}]);
|
|
69
|
+
|
|
70
|
+
if (!confirm) {
|
|
71
|
+
console.log(chalk.yellow('Rollback cancelled'));
|
|
72
|
+
return { success: false, error: 'User cancelled' };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Execute rollback
|
|
77
|
+
console.log(chalk.blue('\n⚙️ Executing rollback...'));
|
|
78
|
+
|
|
79
|
+
const rollbackResult = await this.transactionManager.rollbackTransaction(
|
|
80
|
+
transaction.id,
|
|
81
|
+
{
|
|
82
|
+
continueOnError: options.continueOnError !== false
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Display results
|
|
87
|
+
this.displayRollbackResults(rollbackResult);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: rollbackResult.failed.length === 0,
|
|
91
|
+
result: rollbackResult
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(chalk.red(`\n❌ Rollback failed: ${error.message}`));
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error: error.message
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* List recent transactions
|
|
105
|
+
* @param {number} limit - Number of transactions to show
|
|
106
|
+
* @returns {Promise<void>}
|
|
107
|
+
*/
|
|
108
|
+
async listTransactions(limit = 10) {
|
|
109
|
+
try {
|
|
110
|
+
const transactions = await this.transactionManager.listTransactions(limit);
|
|
111
|
+
|
|
112
|
+
if (transactions.length === 0) {
|
|
113
|
+
console.log(chalk.yellow('No transactions found'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(chalk.blue('\n📋 Recent Transactions:'));
|
|
118
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
119
|
+
|
|
120
|
+
for (const txn of transactions) {
|
|
121
|
+
const date = new Date(txn.startTime).toLocaleString();
|
|
122
|
+
const duration = txn.endTime
|
|
123
|
+
? `${new Date(txn.endTime) - new Date(txn.startTime)}ms`
|
|
124
|
+
: 'active';
|
|
125
|
+
|
|
126
|
+
console.log(chalk.white(`\nID: ${txn.id}`));
|
|
127
|
+
console.log(chalk.gray(`Type: ${txn.type}`));
|
|
128
|
+
console.log(chalk.gray(`Description: ${txn.description}`));
|
|
129
|
+
console.log(chalk.gray(`User: ${txn.user}`));
|
|
130
|
+
console.log(chalk.gray(`Date: ${date}`));
|
|
131
|
+
console.log(chalk.gray(`Status: ${this.getStatusColor(txn.status)}`));
|
|
132
|
+
console.log(chalk.gray(`Operations: ${txn.operations}`));
|
|
133
|
+
console.log(chalk.gray(`Duration: ${duration}`));
|
|
134
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(chalk.red(`Failed to list transactions: ${error.message}`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Execute selective rollback
|
|
144
|
+
* @param {string} transactionId - Transaction ID
|
|
145
|
+
* @param {Array<string>} operationIds - Specific operations to rollback
|
|
146
|
+
* @returns {Promise<Object>} Rollback result
|
|
147
|
+
*/
|
|
148
|
+
async selectiveRollback(transactionId, operationIds) {
|
|
149
|
+
try {
|
|
150
|
+
const transaction = await this.transactionManager.loadTransaction(transactionId);
|
|
151
|
+
if (!transaction) {
|
|
152
|
+
throw new Error(`Transaction not found: ${transactionId}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Filter operations
|
|
156
|
+
const selectedOps = transaction.operations.filter(op =>
|
|
157
|
+
operationIds.includes(op.id)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (selectedOps.length === 0) {
|
|
161
|
+
throw new Error('No matching operations found');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(chalk.blue(`\n📝 Selective rollback: ${selectedOps.length} operations`));
|
|
165
|
+
|
|
166
|
+
// Create a new transaction for selective rollback
|
|
167
|
+
const rollbackTxnId = await this.transactionManager.beginTransaction({
|
|
168
|
+
type: 'selective_rollback',
|
|
169
|
+
description: `Selective rollback of ${transactionId}`,
|
|
170
|
+
metadata: {
|
|
171
|
+
originalTransaction: transactionId,
|
|
172
|
+
selectedOperations: operationIds
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const results = {
|
|
177
|
+
successful: [],
|
|
178
|
+
failed: [],
|
|
179
|
+
warnings: []
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Rollback selected operations
|
|
183
|
+
for (const op of selectedOps) {
|
|
184
|
+
try {
|
|
185
|
+
await this.transactionManager.rollbackOperation(op, results);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
results.failed.push({
|
|
188
|
+
operation: op.id,
|
|
189
|
+
error: error.message
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Commit rollback transaction
|
|
195
|
+
await this.transactionManager.commitTransaction(rollbackTxnId);
|
|
196
|
+
|
|
197
|
+
this.displayRollbackResults(results);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
success: results.failed.length === 0,
|
|
201
|
+
result: results
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(chalk.red(`Selective rollback failed: ${error.message}`));
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
error: error.message
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Clean up old transactions
|
|
215
|
+
* @returns {Promise<number>} Number cleaned
|
|
216
|
+
*/
|
|
217
|
+
async cleanup() {
|
|
218
|
+
try {
|
|
219
|
+
console.log(chalk.blue('🧹 Cleaning up old transactions...'));
|
|
220
|
+
const cleaned = await this.transactionManager.cleanupOldTransactions();
|
|
221
|
+
console.log(chalk.green(`✅ Cleaned up ${cleaned} old transactions`));
|
|
222
|
+
return cleaned;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(chalk.red(`Cleanup failed: ${error.message}`));
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create backup before modification
|
|
231
|
+
* @param {string} componentType - Type of component
|
|
232
|
+
* @param {string} componentName - Name of component
|
|
233
|
+
* @param {string} content - Content to backup
|
|
234
|
+
* @returns {Promise<string>} Backup file path
|
|
235
|
+
*/
|
|
236
|
+
async createBackup(componentType, componentName, content) {
|
|
237
|
+
try {
|
|
238
|
+
// Ensure backup directory exists
|
|
239
|
+
await fs.mkdir(this.backupPath, { recursive: true });
|
|
240
|
+
|
|
241
|
+
// Create subdirectory for component type
|
|
242
|
+
const typeBackupPath = path.join(this.backupPath, componentType);
|
|
243
|
+
await fs.mkdir(typeBackupPath, { recursive: true });
|
|
244
|
+
|
|
245
|
+
// Generate backup filename with timestamp
|
|
246
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
247
|
+
const backupFilename = `${componentName}.${timestamp}.backup`;
|
|
248
|
+
const backupFilePath = path.join(typeBackupPath, backupFilename);
|
|
249
|
+
|
|
250
|
+
// Write backup
|
|
251
|
+
await fs.writeFile(backupFilePath, content, 'utf8');
|
|
252
|
+
|
|
253
|
+
// Record backup in transaction
|
|
254
|
+
if (this.currentTransactionId) {
|
|
255
|
+
await this.transactionManager.recordOperation({
|
|
256
|
+
type: 'backup_created',
|
|
257
|
+
path: backupFilePath,
|
|
258
|
+
componentType,
|
|
259
|
+
componentName,
|
|
260
|
+
timestamp
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return backupFilePath;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw new Error(`Failed to create backup: ${error.message}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Restore from backup
|
|
272
|
+
* @param {string} backupPath - Path to backup file
|
|
273
|
+
* @param {string} targetPath - Path to restore to
|
|
274
|
+
* @returns {Promise<boolean>} Success status
|
|
275
|
+
*/
|
|
276
|
+
async restoreFromBackup(backupPath, targetPath) {
|
|
277
|
+
try {
|
|
278
|
+
// Verify backup exists
|
|
279
|
+
await fs.access(backupPath);
|
|
280
|
+
|
|
281
|
+
// Read backup content
|
|
282
|
+
const content = await fs.readFile(backupPath, 'utf8');
|
|
283
|
+
|
|
284
|
+
// Restore to target
|
|
285
|
+
await fs.writeFile(targetPath, content, 'utf8');
|
|
286
|
+
|
|
287
|
+
console.log(chalk.green(`✅ Restored from backup: ${path.basename(backupPath)}`));
|
|
288
|
+
return true;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error(chalk.red(`Failed to restore from backup: ${error.message}`));
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Validate modification before applying
|
|
297
|
+
* @param {string} componentType - Type of component
|
|
298
|
+
* @param {string} originalContent - Original content
|
|
299
|
+
* @param {string} modifiedContent - Modified content
|
|
300
|
+
* @returns {Promise<Object>} Validation result
|
|
301
|
+
*/
|
|
302
|
+
async validateModification(componentType, originalContent, modifiedContent) {
|
|
303
|
+
const validation = await this.modificationValidator.validateModification(
|
|
304
|
+
componentType,
|
|
305
|
+
originalContent,
|
|
306
|
+
modifiedContent
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (!validation.valid) {
|
|
310
|
+
console.log(chalk.red('\n❌ Modification validation failed:'));
|
|
311
|
+
validation.errors.forEach(error => {
|
|
312
|
+
console.log(chalk.red(` • ${error}`));
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (validation.warnings.length > 0) {
|
|
317
|
+
console.log(chalk.yellow('\n⚠️ Warnings:'));
|
|
318
|
+
validation.warnings.forEach(warning => {
|
|
319
|
+
console.log(chalk.yellow(` • ${warning}`));
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (validation.breakingChanges.length > 0) {
|
|
324
|
+
console.log(chalk.red('\n🚨 Breaking Changes Detected:'));
|
|
325
|
+
validation.breakingChanges.forEach(change => {
|
|
326
|
+
console.log(chalk.red(` • ${change.type}: ${change.impact}`));
|
|
327
|
+
if (change.items) {
|
|
328
|
+
console.log(chalk.red(` Items: ${change.items.join(', ')}`));
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return validation;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Rollback modification
|
|
338
|
+
* @param {Object} modificationData - Modification details
|
|
339
|
+
* @returns {Promise<Object>} Rollback result
|
|
340
|
+
*/
|
|
341
|
+
async rollbackModification(modificationData) {
|
|
342
|
+
const { componentType, componentName, backupPath, targetPath } = modificationData;
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
console.log(chalk.blue(`\n⏪ Rolling back ${componentType}: ${componentName}`));
|
|
346
|
+
|
|
347
|
+
// Restore from backup
|
|
348
|
+
const restored = await this.restoreFromBackup(backupPath, targetPath);
|
|
349
|
+
|
|
350
|
+
if (restored) {
|
|
351
|
+
// Record rollback
|
|
352
|
+
await this.transactionManager.recordOperation({
|
|
353
|
+
type: 'modification_rollback',
|
|
354
|
+
componentType,
|
|
355
|
+
componentName,
|
|
356
|
+
backupPath,
|
|
357
|
+
targetPath,
|
|
358
|
+
timestamp: new Date().toISOString()
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
success: true,
|
|
363
|
+
message: `Successfully rolled back ${componentType}: ${componentName}`
|
|
364
|
+
};
|
|
365
|
+
} else {
|
|
366
|
+
throw new Error('Restoration failed');
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
error: error.message
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* List available backups for a component
|
|
378
|
+
* @param {string} componentType - Type of component
|
|
379
|
+
* @param {string} componentName - Name of component
|
|
380
|
+
* @returns {Promise<Array>} List of backups
|
|
381
|
+
*/
|
|
382
|
+
async listBackups(componentType, componentName) {
|
|
383
|
+
try {
|
|
384
|
+
const typeBackupPath = path.join(this.backupPath, componentType);
|
|
385
|
+
const files = await fs.readdir(typeBackupPath);
|
|
386
|
+
|
|
387
|
+
const backups = files
|
|
388
|
+
.filter(file => file.startsWith(`${componentName}.`) && file.endsWith('.backup'))
|
|
389
|
+
.map(file => {
|
|
390
|
+
const match = file.match(/\.(\d{4}-\d{2}-\d{2}T[\d-]+Z)\.backup$/);
|
|
391
|
+
const timestamp = match ? match[1].replace(/-/g, ':') : 'unknown';
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
filename: file,
|
|
395
|
+
path: path.join(typeBackupPath, file),
|
|
396
|
+
timestamp: new Date(timestamp),
|
|
397
|
+
componentName,
|
|
398
|
+
componentType
|
|
399
|
+
};
|
|
400
|
+
})
|
|
401
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
402
|
+
|
|
403
|
+
return backups;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (error.code === 'ENOENT') {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Clean up old backups
|
|
414
|
+
* @param {number} daysToKeep - Number of days to keep backups
|
|
415
|
+
* @returns {Promise<number>} Number of backups deleted
|
|
416
|
+
*/
|
|
417
|
+
async cleanupBackups(daysToKeep = 30) {
|
|
418
|
+
try {
|
|
419
|
+
const cutoffDate = new Date();
|
|
420
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
421
|
+
|
|
422
|
+
let deletedCount = 0;
|
|
423
|
+
|
|
424
|
+
// Iterate through component types
|
|
425
|
+
const componentTypes = await fs.readdir(this.backupPath);
|
|
426
|
+
|
|
427
|
+
for (const componentType of componentTypes) {
|
|
428
|
+
const typePath = path.join(this.backupPath, componentType);
|
|
429
|
+
const stat = await fs.stat(typePath);
|
|
430
|
+
|
|
431
|
+
if (!stat.isDirectory()) continue;
|
|
432
|
+
|
|
433
|
+
const files = await fs.readdir(typePath);
|
|
434
|
+
|
|
435
|
+
for (const file of files) {
|
|
436
|
+
if (!file.endsWith('.backup')) continue;
|
|
437
|
+
|
|
438
|
+
const filePath = path.join(typePath, file);
|
|
439
|
+
const fileStat = await fs.stat(filePath);
|
|
440
|
+
|
|
441
|
+
if (fileStat.mtime < cutoffDate) {
|
|
442
|
+
await fs.unlink(filePath);
|
|
443
|
+
deletedCount++;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return deletedCount;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error(chalk.red(`Backup cleanup failed: ${error.message}`));
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get operation icon
|
|
457
|
+
* @private
|
|
458
|
+
*/
|
|
459
|
+
getOperationIcon(type) {
|
|
460
|
+
const icons = {
|
|
461
|
+
create: '➕',
|
|
462
|
+
update: '✏️',
|
|
463
|
+
delete: '🗑️',
|
|
464
|
+
manifest_update: '📋',
|
|
465
|
+
metadata_update: '📊',
|
|
466
|
+
component_created: '📦',
|
|
467
|
+
backup_created: '💾',
|
|
468
|
+
modification_rollback: '⏪'
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
return icons[type] || '•';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get status color
|
|
476
|
+
* @private
|
|
477
|
+
*/
|
|
478
|
+
getStatusColor(status) {
|
|
479
|
+
switch (status) {
|
|
480
|
+
case 'active':
|
|
481
|
+
return chalk.yellow(status);
|
|
482
|
+
case 'committed':
|
|
483
|
+
return chalk.green(status);
|
|
484
|
+
case 'rolled_back':
|
|
485
|
+
return chalk.blue(status);
|
|
486
|
+
case 'failed':
|
|
487
|
+
return chalk.red(status);
|
|
488
|
+
default:
|
|
489
|
+
return status;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Display rollback results
|
|
495
|
+
* @private
|
|
496
|
+
*/
|
|
497
|
+
displayRollbackResults(results) {
|
|
498
|
+
console.log(chalk.blue('\n📊 Rollback Results:'));
|
|
499
|
+
|
|
500
|
+
if (results.successful.length > 0) {
|
|
501
|
+
console.log(chalk.green(`\n✅ Successful (${results.successful.length}):`));
|
|
502
|
+
results.successful.forEach(item => {
|
|
503
|
+
console.log(chalk.green(` ✓ ${item.action}: ${path.basename(item.path)}`));
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (results.warnings.length > 0) {
|
|
508
|
+
console.log(chalk.yellow(`\n⚠️ Warnings (${results.warnings.length}):`));
|
|
509
|
+
results.warnings.forEach(item => {
|
|
510
|
+
console.log(chalk.yellow(` ⚠ ${item.warning}: ${path.basename(item.path || 'N/A')}`));
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (results.failed.length > 0) {
|
|
515
|
+
console.log(chalk.red(`\n❌ Failed (${results.failed.length}):`));
|
|
516
|
+
results.failed.forEach(item => {
|
|
517
|
+
console.log(chalk.red(` ✗ ${item.operation}: ${item.error}`));
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Summary
|
|
522
|
+
const total = results.successful.length + results.failed.length;
|
|
523
|
+
const successRate = total > 0 ? (results.successful.length / total * 100).toFixed(0) : 0;
|
|
524
|
+
|
|
525
|
+
console.log(chalk.blue('\n📈 Summary:'));
|
|
526
|
+
console.log(chalk.gray(` Total operations: ${total}`));
|
|
527
|
+
console.log(chalk.gray(` Success rate: ${successRate}%`));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
531
|
module.exports = RollbackHandler;
|