aios-core 4.2.13 → 4.2.15
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/pro/license/degradation.js +220 -220
- package/pro/license/errors.js +450 -450
- package/pro/license/feature-gate.js +354 -354
- package/pro/license/index.js +181 -181
- package/pro/license/license-cache.js +523 -523
- package/pro/license/license-crypto.js +303 -303
- 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/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,590 +1,590 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transaction Manager for AIOS-FULLSTACK
|
|
3
|
-
* Manages component operations with rollback support
|
|
4
|
-
* @module transaction-manager
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('fs-extra');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const crypto = require('crypto');
|
|
10
|
-
const chalk = require('chalk');
|
|
11
|
-
const ComponentMetadata = require('./component-metadata');
|
|
12
|
-
|
|
13
|
-
class TransactionManager {
|
|
14
|
-
constructor(options = {}) {
|
|
15
|
-
this.rootPath = options.rootPath || process.cwd();
|
|
16
|
-
this.transactionPath = path.join(this.rootPath, 'aios-core', 'transactions');
|
|
17
|
-
this.backupPath = path.join(this.rootPath, 'aios-core', 'backups');
|
|
18
|
-
this.componentMetadata = new ComponentMetadata({ rootPath: this.rootPath });
|
|
19
|
-
|
|
20
|
-
// Active transactions
|
|
21
|
-
this.activeTransactions = new Map();
|
|
22
|
-
|
|
23
|
-
// Transaction retention (30 days)
|
|
24
|
-
this.retentionDays = options.retentionDays || 30;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Begin a new transaction
|
|
29
|
-
* @param {Object} options - Transaction options
|
|
30
|
-
* @returns {Promise<string>} Transaction ID
|
|
31
|
-
*/
|
|
32
|
-
async beginTransaction(options = {}) {
|
|
33
|
-
try {
|
|
34
|
-
const transactionId = this.generateTransactionId();
|
|
35
|
-
|
|
36
|
-
const transaction = {
|
|
37
|
-
id: transactionId,
|
|
38
|
-
type: options.type || 'component_operation',
|
|
39
|
-
description: options.description || 'Component operation',
|
|
40
|
-
user: options.user || process.env.USER || 'system',
|
|
41
|
-
startTime: new Date().toISOString(),
|
|
42
|
-
status: 'active',
|
|
43
|
-
operations: [],
|
|
44
|
-
backups: [],
|
|
45
|
-
metadata: options.metadata || {},
|
|
46
|
-
rollbackOnError: options.rollbackOnError !== false
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// Save initial transaction state
|
|
50
|
-
await this.saveTransaction(transaction);
|
|
51
|
-
|
|
52
|
-
// Store in active transactions
|
|
53
|
-
this.activeTransactions.set(transactionId, transaction);
|
|
54
|
-
|
|
55
|
-
console.log(chalk.blue(`📋 Transaction started: ${transactionId}`));
|
|
56
|
-
|
|
57
|
-
return transactionId;
|
|
58
|
-
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.error(chalk.red(`Failed to begin transaction: ${error.message}`));
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Record a file operation in the transaction
|
|
67
|
-
* @param {string} transactionId - Transaction ID
|
|
68
|
-
* @param {Object} operation - Operation details
|
|
69
|
-
* @returns {Promise<void>}
|
|
70
|
-
*/
|
|
71
|
-
async recordOperation(transactionId, operation) {
|
|
72
|
-
try {
|
|
73
|
-
const transaction = this.activeTransactions.get(transactionId);
|
|
74
|
-
if (!transaction) {
|
|
75
|
-
throw new Error(`Transaction not found: ${transactionId}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const operationRecord = {
|
|
79
|
-
id: `op-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
80
|
-
timestamp: new Date().toISOString(),
|
|
81
|
-
type: operation.type, // create, update, delete
|
|
82
|
-
target: operation.target, // file, manifest, metadata
|
|
83
|
-
path: operation.path,
|
|
84
|
-
previousState: null,
|
|
85
|
-
newState: null,
|
|
86
|
-
metadata: operation.metadata || {}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Backup current state if needed
|
|
90
|
-
if (operation.type === 'update' || operation.type === 'delete') {
|
|
91
|
-
operationRecord.previousState = await this.backupCurrentState(operation.path);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Record new state for create/update
|
|
95
|
-
if (operation.type === 'create' || operation.type === 'update') {
|
|
96
|
-
operationRecord.newState = operation.content || operation.data;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Add to transaction
|
|
100
|
-
transaction.operations.push(operationRecord);
|
|
101
|
-
|
|
102
|
-
// Save updated transaction
|
|
103
|
-
await this.saveTransaction(transaction);
|
|
104
|
-
|
|
105
|
-
} catch (error) {
|
|
106
|
-
console.error(chalk.red(`Failed to record operation: ${error.message}`));
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Commit a transaction
|
|
113
|
-
* @param {string} transactionId - Transaction ID
|
|
114
|
-
* @returns {Promise<Object>} Commit result
|
|
115
|
-
*/
|
|
116
|
-
async commitTransaction(transactionId) {
|
|
117
|
-
try {
|
|
118
|
-
const transaction = this.activeTransactions.get(transactionId);
|
|
119
|
-
if (!transaction) {
|
|
120
|
-
throw new Error(`Transaction not found: ${transactionId}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
transaction.endTime = new Date().toISOString();
|
|
124
|
-
transaction.status = 'committed';
|
|
125
|
-
transaction.duration = new Date(transaction.endTime) - new Date(transaction.startTime);
|
|
126
|
-
|
|
127
|
-
// Save final transaction state
|
|
128
|
-
await this.saveTransaction(transaction);
|
|
129
|
-
|
|
130
|
-
// Clean up backups after successful commit (keep for history)
|
|
131
|
-
await this.archiveBackups(transaction);
|
|
132
|
-
|
|
133
|
-
// Remove from active transactions
|
|
134
|
-
this.activeTransactions.delete(transactionId);
|
|
135
|
-
|
|
136
|
-
console.log(chalk.green(`✅ Transaction committed: ${transactionId}`));
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
transactionId,
|
|
140
|
-
operations: transaction.operations.length,
|
|
141
|
-
duration: transaction.duration
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.error(chalk.red(`Failed to commit transaction: ${error.message}`));
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Rollback a transaction
|
|
152
|
-
* @param {string} transactionId - Transaction ID
|
|
153
|
-
* @param {Object} options - Rollback options
|
|
154
|
-
* @returns {Promise<Object>} Rollback result
|
|
155
|
-
*/
|
|
156
|
-
async rollbackTransaction(transactionId, options = {}) {
|
|
157
|
-
try {
|
|
158
|
-
const transaction = this.activeTransactions.get(transactionId) ||
|
|
159
|
-
await this.loadTransaction(transactionId);
|
|
160
|
-
|
|
161
|
-
if (!transaction) {
|
|
162
|
-
throw new Error(`Transaction not found: ${transactionId}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
console.log(chalk.yellow(`⚙️ Rolling back transaction: ${transactionId}`));
|
|
166
|
-
|
|
167
|
-
const rollbackResults = {
|
|
168
|
-
transactionId,
|
|
169
|
-
successful: [],
|
|
170
|
-
failed: [],
|
|
171
|
-
warnings: []
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// Process operations in reverse order
|
|
175
|
-
const operations = [...transaction.operations].reverse();
|
|
176
|
-
|
|
177
|
-
for (const operation of operations) {
|
|
178
|
-
try {
|
|
179
|
-
await this.rollbackOperation(operation, rollbackResults);
|
|
180
|
-
} catch (error) {
|
|
181
|
-
rollbackResults.failed.push({
|
|
182
|
-
operation: operation.id,
|
|
183
|
-
error: error.message
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
if (!options.continueOnError) {
|
|
187
|
-
throw error;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Update transaction status
|
|
193
|
-
transaction.status = 'rolled_back';
|
|
194
|
-
transaction.rollbackTime = new Date().toISOString();
|
|
195
|
-
transaction.rollbackResults = rollbackResults;
|
|
196
|
-
|
|
197
|
-
await this.saveTransaction(transaction);
|
|
198
|
-
|
|
199
|
-
// Remove from active transactions
|
|
200
|
-
this.activeTransactions.delete(transactionId);
|
|
201
|
-
|
|
202
|
-
console.log(chalk.green(`✅ Rollback completed`));
|
|
203
|
-
console.log(chalk.gray(` Successful: ${rollbackResults.successful.length}`));
|
|
204
|
-
console.log(chalk.gray(` Failed: ${rollbackResults.failed.length}`));
|
|
205
|
-
console.log(chalk.gray(` Warnings: ${rollbackResults.warnings.length}`));
|
|
206
|
-
|
|
207
|
-
return rollbackResults;
|
|
208
|
-
|
|
209
|
-
} catch (error) {
|
|
210
|
-
console.error(chalk.red(`Rollback failed: ${error.message}`));
|
|
211
|
-
throw error;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Rollback a single operation
|
|
217
|
-
* @private
|
|
218
|
-
*/
|
|
219
|
-
async rollbackOperation(operation, results) {
|
|
220
|
-
console.log(chalk.gray(` Rolling back: ${operation.type} ${operation.path}`));
|
|
221
|
-
|
|
222
|
-
switch (operation.type) {
|
|
223
|
-
case 'create':
|
|
224
|
-
// Delete created file
|
|
225
|
-
if (await fs.pathExists(operation.path)) {
|
|
226
|
-
await fs.remove(operation.path);
|
|
227
|
-
results.successful.push({
|
|
228
|
-
operation: operation.id,
|
|
229
|
-
action: 'deleted',
|
|
230
|
-
path: operation.path
|
|
231
|
-
});
|
|
232
|
-
} else {
|
|
233
|
-
results.warnings.push({
|
|
234
|
-
operation: operation.id,
|
|
235
|
-
warning: 'File already removed',
|
|
236
|
-
path: operation.path
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
break;
|
|
240
|
-
|
|
241
|
-
case 'update':
|
|
242
|
-
// Restore previous state
|
|
243
|
-
if (operation.previousState) {
|
|
244
|
-
await this.restoreFromBackup(operation.path, operation.previousState);
|
|
245
|
-
results.successful.push({
|
|
246
|
-
operation: operation.id,
|
|
247
|
-
action: 'restored',
|
|
248
|
-
path: operation.path
|
|
249
|
-
});
|
|
250
|
-
} else {
|
|
251
|
-
results.warnings.push({
|
|
252
|
-
operation: operation.id,
|
|
253
|
-
warning: 'No backup available',
|
|
254
|
-
path: operation.path
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
break;
|
|
258
|
-
|
|
259
|
-
case 'delete':
|
|
260
|
-
// Restore deleted file
|
|
261
|
-
if (operation.previousState) {
|
|
262
|
-
await this.restoreFromBackup(operation.path, operation.previousState);
|
|
263
|
-
results.successful.push({
|
|
264
|
-
operation: operation.id,
|
|
265
|
-
action: 'restored',
|
|
266
|
-
path: operation.path
|
|
267
|
-
});
|
|
268
|
-
} else {
|
|
269
|
-
results.warnings.push({
|
|
270
|
-
operation: operation.id,
|
|
271
|
-
warning: 'No backup available',
|
|
272
|
-
path: operation.path
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
break;
|
|
276
|
-
|
|
277
|
-
case 'manifest_update':
|
|
278
|
-
// Special handling for manifest updates
|
|
279
|
-
await this.rollbackManifestUpdate(operation, results);
|
|
280
|
-
break;
|
|
281
|
-
|
|
282
|
-
case 'metadata_update':
|
|
283
|
-
// Special handling for metadata updates
|
|
284
|
-
await this.rollbackMetadataUpdate(operation, results);
|
|
285
|
-
break;
|
|
286
|
-
|
|
287
|
-
default:
|
|
288
|
-
results.warnings.push({
|
|
289
|
-
operation: operation.id,
|
|
290
|
-
warning: `Unknown operation type: ${operation.type}`
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Get the last transaction for selective rollback
|
|
297
|
-
* @returns {Promise<Object|null>} Last transaction
|
|
298
|
-
*/
|
|
299
|
-
async getLastTransaction() {
|
|
300
|
-
try {
|
|
301
|
-
const transactionsDir = this.transactionPath;
|
|
302
|
-
if (!await fs.pathExists(transactionsDir)) {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Get all transaction files
|
|
307
|
-
const files = await fs.readdir(transactionsDir);
|
|
308
|
-
const transactionFiles = files.filter(f => f.endsWith('.json'));
|
|
309
|
-
|
|
310
|
-
if (transactionFiles.length === 0) {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Sort by timestamp (newest first)
|
|
315
|
-
const transactions = [];
|
|
316
|
-
for (const file of transactionFiles) {
|
|
317
|
-
const transaction = await fs.readJson(path.join(transactionsDir, file));
|
|
318
|
-
transactions.push(transaction);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
transactions.sort((a, b) =>
|
|
322
|
-
new Date(b.startTime) - new Date(a.startTime)
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
return transactions[0];
|
|
326
|
-
|
|
327
|
-
} catch (error) {
|
|
328
|
-
console.error(chalk.red(`Failed to get last transaction: ${error.message}`));
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* List recent transactions
|
|
335
|
-
* @param {number} limit - Number of transactions to return
|
|
336
|
-
* @returns {Promise<Array>} Recent transactions
|
|
337
|
-
*/
|
|
338
|
-
async listTransactions(limit = 10) {
|
|
339
|
-
try {
|
|
340
|
-
const transactionsDir = this.transactionPath;
|
|
341
|
-
if (!await fs.pathExists(transactionsDir)) {
|
|
342
|
-
return [];
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const files = await fs.readdir(transactionsDir);
|
|
346
|
-
const transactionFiles = files.filter(f => f.endsWith('.json'));
|
|
347
|
-
|
|
348
|
-
const transactions = [];
|
|
349
|
-
for (const file of transactionFiles) {
|
|
350
|
-
const transaction = await fs.readJson(path.join(transactionsDir, file));
|
|
351
|
-
transactions.push({
|
|
352
|
-
id: transaction.id,
|
|
353
|
-
type: transaction.type,
|
|
354
|
-
description: transaction.description,
|
|
355
|
-
user: transaction.user,
|
|
356
|
-
startTime: transaction.startTime,
|
|
357
|
-
endTime: transaction.endTime,
|
|
358
|
-
status: transaction.status,
|
|
359
|
-
operations: transaction.operations.length
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Sort by start time (newest first)
|
|
364
|
-
transactions.sort((a, b) =>
|
|
365
|
-
new Date(b.startTime) - new Date(a.startTime)
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
return transactions.slice(0, limit);
|
|
369
|
-
|
|
370
|
-
} catch (error) {
|
|
371
|
-
console.error(chalk.red(`Failed to list transactions: ${error.message}`));
|
|
372
|
-
return [];
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Clean up old transactions
|
|
378
|
-
* @returns {Promise<number>} Number of transactions cleaned
|
|
379
|
-
*/
|
|
380
|
-
async cleanupOldTransactions() {
|
|
381
|
-
try {
|
|
382
|
-
const transactionsDir = this.transactionPath;
|
|
383
|
-
if (!await fs.pathExists(transactionsDir)) {
|
|
384
|
-
return 0;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const cutoffDate = new Date();
|
|
388
|
-
cutoffDate.setDate(cutoffDate.getDate() - this.retentionDays);
|
|
389
|
-
|
|
390
|
-
const files = await fs.readdir(transactionsDir);
|
|
391
|
-
let cleaned = 0;
|
|
392
|
-
|
|
393
|
-
for (const file of files) {
|
|
394
|
-
if (file.endsWith('.json')) {
|
|
395
|
-
const filePath = path.join(transactionsDir, file);
|
|
396
|
-
const transaction = await fs.readJson(filePath);
|
|
397
|
-
|
|
398
|
-
if (new Date(transaction.startTime) < cutoffDate) {
|
|
399
|
-
// Clean up transaction and its backups
|
|
400
|
-
await fs.remove(filePath);
|
|
401
|
-
|
|
402
|
-
// Remove associated backups
|
|
403
|
-
if (transaction.backups) {
|
|
404
|
-
for (const backup of transaction.backups) {
|
|
405
|
-
if (await fs.pathExists(backup.path)) {
|
|
406
|
-
await fs.remove(backup.path);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
cleaned++;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
console.log(chalk.gray(`🧹 Cleaned up ${cleaned} old transactions`));
|
|
417
|
-
return cleaned;
|
|
418
|
-
|
|
419
|
-
} catch (error) {
|
|
420
|
-
console.error(chalk.red(`Cleanup failed: ${error.message}`));
|
|
421
|
-
return 0;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Backup current state of a file
|
|
427
|
-
* @private
|
|
428
|
-
*/
|
|
429
|
-
async backupCurrentState(filePath) {
|
|
430
|
-
try {
|
|
431
|
-
if (!await fs.pathExists(filePath)) {
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
436
|
-
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
437
|
-
|
|
438
|
-
const backup = {
|
|
439
|
-
path: filePath,
|
|
440
|
-
content: content,
|
|
441
|
-
hash: hash,
|
|
442
|
-
timestamp: new Date().toISOString()
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
// Save backup
|
|
446
|
-
const backupId = `backup-${Date.now()}-${hash.substr(0, 8)}`;
|
|
447
|
-
const backupPath = path.join(this.backupPath, backupId);
|
|
448
|
-
|
|
449
|
-
await fs.ensureDir(this.backupPath);
|
|
450
|
-
await fs.writeJson(backupPath, backup, { spaces: 2 });
|
|
451
|
-
|
|
452
|
-
return backupId;
|
|
453
|
-
|
|
454
|
-
} catch (error) {
|
|
455
|
-
console.error(chalk.red(`Backup failed: ${error.message}`));
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Restore from backup
|
|
462
|
-
* @private
|
|
463
|
-
*/
|
|
464
|
-
async restoreFromBackup(targetPath, backupId) {
|
|
465
|
-
try {
|
|
466
|
-
const backupPath = path.join(this.backupPath, backupId);
|
|
467
|
-
const backup = await fs.readJson(backupPath);
|
|
468
|
-
|
|
469
|
-
// Ensure directory exists
|
|
470
|
-
await fs.ensureDir(path.dirname(targetPath));
|
|
471
|
-
|
|
472
|
-
// Restore content
|
|
473
|
-
await fs.writeFile(targetPath, backup.content, 'utf8');
|
|
474
|
-
|
|
475
|
-
console.log(chalk.green(` ✓ Restored: ${path.basename(targetPath)}`));
|
|
476
|
-
|
|
477
|
-
} catch (error) {
|
|
478
|
-
console.error(chalk.red(`Restore failed: ${error.message}`));
|
|
479
|
-
throw error;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Rollback manifest update
|
|
485
|
-
* @private
|
|
486
|
-
*/
|
|
487
|
-
async rollbackManifestUpdate(operation, results) {
|
|
488
|
-
try {
|
|
489
|
-
const manifestPath = path.join(this.rootPath, 'aios-core', 'team-manifest.yaml');
|
|
490
|
-
|
|
491
|
-
if (operation.previousState) {
|
|
492
|
-
// Restore previous manifest state
|
|
493
|
-
const backupPath = path.join(this.backupPath, operation.previousState);
|
|
494
|
-
const backup = await fs.readJson(backupPath);
|
|
495
|
-
await fs.writeFile(manifestPath, backup.content, 'utf8');
|
|
496
|
-
|
|
497
|
-
results.successful.push({
|
|
498
|
-
operation: operation.id,
|
|
499
|
-
action: 'manifest_restored',
|
|
500
|
-
path: manifestPath
|
|
501
|
-
});
|
|
502
|
-
} else {
|
|
503
|
-
results.warnings.push({
|
|
504
|
-
operation: operation.id,
|
|
505
|
-
warning: 'No manifest backup available'
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
} catch (error) {
|
|
510
|
-
results.failed.push({
|
|
511
|
-
operation: operation.id,
|
|
512
|
-
error: `Manifest rollback failed: ${error.message}`
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Rollback metadata update
|
|
519
|
-
* @private
|
|
520
|
-
*/
|
|
521
|
-
async rollbackMetadataUpdate(operation, results) {
|
|
522
|
-
try {
|
|
523
|
-
if (operation.metadata?.componentType && operation.metadata?.componentId) {
|
|
524
|
-
// Revert metadata changes
|
|
525
|
-
// This would need integration with ComponentMetadata
|
|
526
|
-
results.warnings.push({
|
|
527
|
-
operation: operation.id,
|
|
528
|
-
warning: 'Metadata rollback not fully implemented'
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
} catch (error) {
|
|
533
|
-
results.failed.push({
|
|
534
|
-
operation: operation.id,
|
|
535
|
-
error: `Metadata rollback failed: ${error.message}`
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Archive backups after successful commit
|
|
542
|
-
* @private
|
|
543
|
-
*/
|
|
544
|
-
async archiveBackups(transaction) {
|
|
545
|
-
// Move backups to archive directory with transaction reference
|
|
546
|
-
const archivePath = path.join(this.backupPath, 'archive', transaction.id);
|
|
547
|
-
await fs.ensureDir(archivePath);
|
|
548
|
-
|
|
549
|
-
// Archive transaction file
|
|
550
|
-
const transactionArchive = path.join(archivePath, 'transaction.json');
|
|
551
|
-
await fs.writeJson(transactionArchive, transaction, { spaces: 2 });
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Generate transaction ID
|
|
556
|
-
* @private
|
|
557
|
-
*/
|
|
558
|
-
generateTransactionId() {
|
|
559
|
-
return `txn-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Save transaction to disk
|
|
564
|
-
* @private
|
|
565
|
-
*/
|
|
566
|
-
async saveTransaction(transaction) {
|
|
567
|
-
await fs.ensureDir(this.transactionPath);
|
|
568
|
-
const filePath = path.join(this.transactionPath, `${transaction.id}.json`);
|
|
569
|
-
await fs.writeJson(filePath, transaction, { spaces: 2 });
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Load transaction from disk
|
|
574
|
-
* @private
|
|
575
|
-
*/
|
|
576
|
-
async loadTransaction(transactionId) {
|
|
577
|
-
try {
|
|
578
|
-
const filePath = path.join(this.transactionPath, `${transactionId}.json`);
|
|
579
|
-
if (await fs.pathExists(filePath)) {
|
|
580
|
-
return await fs.readJson(filePath);
|
|
581
|
-
}
|
|
582
|
-
return null;
|
|
583
|
-
} catch (error) {
|
|
584
|
-
console.error(chalk.red(`Failed to load transaction: ${error.message}`));
|
|
585
|
-
return null;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Transaction Manager for AIOS-FULLSTACK
|
|
3
|
+
* Manages component operations with rollback support
|
|
4
|
+
* @module transaction-manager
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const ComponentMetadata = require('./component-metadata');
|
|
12
|
+
|
|
13
|
+
class TransactionManager {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.rootPath = options.rootPath || process.cwd();
|
|
16
|
+
this.transactionPath = path.join(this.rootPath, 'aios-core', 'transactions');
|
|
17
|
+
this.backupPath = path.join(this.rootPath, 'aios-core', 'backups');
|
|
18
|
+
this.componentMetadata = new ComponentMetadata({ rootPath: this.rootPath });
|
|
19
|
+
|
|
20
|
+
// Active transactions
|
|
21
|
+
this.activeTransactions = new Map();
|
|
22
|
+
|
|
23
|
+
// Transaction retention (30 days)
|
|
24
|
+
this.retentionDays = options.retentionDays || 30;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Begin a new transaction
|
|
29
|
+
* @param {Object} options - Transaction options
|
|
30
|
+
* @returns {Promise<string>} Transaction ID
|
|
31
|
+
*/
|
|
32
|
+
async beginTransaction(options = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const transactionId = this.generateTransactionId();
|
|
35
|
+
|
|
36
|
+
const transaction = {
|
|
37
|
+
id: transactionId,
|
|
38
|
+
type: options.type || 'component_operation',
|
|
39
|
+
description: options.description || 'Component operation',
|
|
40
|
+
user: options.user || process.env.USER || 'system',
|
|
41
|
+
startTime: new Date().toISOString(),
|
|
42
|
+
status: 'active',
|
|
43
|
+
operations: [],
|
|
44
|
+
backups: [],
|
|
45
|
+
metadata: options.metadata || {},
|
|
46
|
+
rollbackOnError: options.rollbackOnError !== false
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Save initial transaction state
|
|
50
|
+
await this.saveTransaction(transaction);
|
|
51
|
+
|
|
52
|
+
// Store in active transactions
|
|
53
|
+
this.activeTransactions.set(transactionId, transaction);
|
|
54
|
+
|
|
55
|
+
console.log(chalk.blue(`📋 Transaction started: ${transactionId}`));
|
|
56
|
+
|
|
57
|
+
return transactionId;
|
|
58
|
+
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(chalk.red(`Failed to begin transaction: ${error.message}`));
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Record a file operation in the transaction
|
|
67
|
+
* @param {string} transactionId - Transaction ID
|
|
68
|
+
* @param {Object} operation - Operation details
|
|
69
|
+
* @returns {Promise<void>}
|
|
70
|
+
*/
|
|
71
|
+
async recordOperation(transactionId, operation) {
|
|
72
|
+
try {
|
|
73
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
74
|
+
if (!transaction) {
|
|
75
|
+
throw new Error(`Transaction not found: ${transactionId}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const operationRecord = {
|
|
79
|
+
id: `op-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
type: operation.type, // create, update, delete
|
|
82
|
+
target: operation.target, // file, manifest, metadata
|
|
83
|
+
path: operation.path,
|
|
84
|
+
previousState: null,
|
|
85
|
+
newState: null,
|
|
86
|
+
metadata: operation.metadata || {}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Backup current state if needed
|
|
90
|
+
if (operation.type === 'update' || operation.type === 'delete') {
|
|
91
|
+
operationRecord.previousState = await this.backupCurrentState(operation.path);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Record new state for create/update
|
|
95
|
+
if (operation.type === 'create' || operation.type === 'update') {
|
|
96
|
+
operationRecord.newState = operation.content || operation.data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add to transaction
|
|
100
|
+
transaction.operations.push(operationRecord);
|
|
101
|
+
|
|
102
|
+
// Save updated transaction
|
|
103
|
+
await this.saveTransaction(transaction);
|
|
104
|
+
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(chalk.red(`Failed to record operation: ${error.message}`));
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Commit a transaction
|
|
113
|
+
* @param {string} transactionId - Transaction ID
|
|
114
|
+
* @returns {Promise<Object>} Commit result
|
|
115
|
+
*/
|
|
116
|
+
async commitTransaction(transactionId) {
|
|
117
|
+
try {
|
|
118
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
119
|
+
if (!transaction) {
|
|
120
|
+
throw new Error(`Transaction not found: ${transactionId}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
transaction.endTime = new Date().toISOString();
|
|
124
|
+
transaction.status = 'committed';
|
|
125
|
+
transaction.duration = new Date(transaction.endTime) - new Date(transaction.startTime);
|
|
126
|
+
|
|
127
|
+
// Save final transaction state
|
|
128
|
+
await this.saveTransaction(transaction);
|
|
129
|
+
|
|
130
|
+
// Clean up backups after successful commit (keep for history)
|
|
131
|
+
await this.archiveBackups(transaction);
|
|
132
|
+
|
|
133
|
+
// Remove from active transactions
|
|
134
|
+
this.activeTransactions.delete(transactionId);
|
|
135
|
+
|
|
136
|
+
console.log(chalk.green(`✅ Transaction committed: ${transactionId}`));
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
transactionId,
|
|
140
|
+
operations: transaction.operations.length,
|
|
141
|
+
duration: transaction.duration
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(chalk.red(`Failed to commit transaction: ${error.message}`));
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Rollback a transaction
|
|
152
|
+
* @param {string} transactionId - Transaction ID
|
|
153
|
+
* @param {Object} options - Rollback options
|
|
154
|
+
* @returns {Promise<Object>} Rollback result
|
|
155
|
+
*/
|
|
156
|
+
async rollbackTransaction(transactionId, options = {}) {
|
|
157
|
+
try {
|
|
158
|
+
const transaction = this.activeTransactions.get(transactionId) ||
|
|
159
|
+
await this.loadTransaction(transactionId);
|
|
160
|
+
|
|
161
|
+
if (!transaction) {
|
|
162
|
+
throw new Error(`Transaction not found: ${transactionId}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(chalk.yellow(`⚙️ Rolling back transaction: ${transactionId}`));
|
|
166
|
+
|
|
167
|
+
const rollbackResults = {
|
|
168
|
+
transactionId,
|
|
169
|
+
successful: [],
|
|
170
|
+
failed: [],
|
|
171
|
+
warnings: []
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Process operations in reverse order
|
|
175
|
+
const operations = [...transaction.operations].reverse();
|
|
176
|
+
|
|
177
|
+
for (const operation of operations) {
|
|
178
|
+
try {
|
|
179
|
+
await this.rollbackOperation(operation, rollbackResults);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
rollbackResults.failed.push({
|
|
182
|
+
operation: operation.id,
|
|
183
|
+
error: error.message
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!options.continueOnError) {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Update transaction status
|
|
193
|
+
transaction.status = 'rolled_back';
|
|
194
|
+
transaction.rollbackTime = new Date().toISOString();
|
|
195
|
+
transaction.rollbackResults = rollbackResults;
|
|
196
|
+
|
|
197
|
+
await this.saveTransaction(transaction);
|
|
198
|
+
|
|
199
|
+
// Remove from active transactions
|
|
200
|
+
this.activeTransactions.delete(transactionId);
|
|
201
|
+
|
|
202
|
+
console.log(chalk.green(`✅ Rollback completed`));
|
|
203
|
+
console.log(chalk.gray(` Successful: ${rollbackResults.successful.length}`));
|
|
204
|
+
console.log(chalk.gray(` Failed: ${rollbackResults.failed.length}`));
|
|
205
|
+
console.log(chalk.gray(` Warnings: ${rollbackResults.warnings.length}`));
|
|
206
|
+
|
|
207
|
+
return rollbackResults;
|
|
208
|
+
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(chalk.red(`Rollback failed: ${error.message}`));
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Rollback a single operation
|
|
217
|
+
* @private
|
|
218
|
+
*/
|
|
219
|
+
async rollbackOperation(operation, results) {
|
|
220
|
+
console.log(chalk.gray(` Rolling back: ${operation.type} ${operation.path}`));
|
|
221
|
+
|
|
222
|
+
switch (operation.type) {
|
|
223
|
+
case 'create':
|
|
224
|
+
// Delete created file
|
|
225
|
+
if (await fs.pathExists(operation.path)) {
|
|
226
|
+
await fs.remove(operation.path);
|
|
227
|
+
results.successful.push({
|
|
228
|
+
operation: operation.id,
|
|
229
|
+
action: 'deleted',
|
|
230
|
+
path: operation.path
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
results.warnings.push({
|
|
234
|
+
operation: operation.id,
|
|
235
|
+
warning: 'File already removed',
|
|
236
|
+
path: operation.path
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case 'update':
|
|
242
|
+
// Restore previous state
|
|
243
|
+
if (operation.previousState) {
|
|
244
|
+
await this.restoreFromBackup(operation.path, operation.previousState);
|
|
245
|
+
results.successful.push({
|
|
246
|
+
operation: operation.id,
|
|
247
|
+
action: 'restored',
|
|
248
|
+
path: operation.path
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
results.warnings.push({
|
|
252
|
+
operation: operation.id,
|
|
253
|
+
warning: 'No backup available',
|
|
254
|
+
path: operation.path
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case 'delete':
|
|
260
|
+
// Restore deleted file
|
|
261
|
+
if (operation.previousState) {
|
|
262
|
+
await this.restoreFromBackup(operation.path, operation.previousState);
|
|
263
|
+
results.successful.push({
|
|
264
|
+
operation: operation.id,
|
|
265
|
+
action: 'restored',
|
|
266
|
+
path: operation.path
|
|
267
|
+
});
|
|
268
|
+
} else {
|
|
269
|
+
results.warnings.push({
|
|
270
|
+
operation: operation.id,
|
|
271
|
+
warning: 'No backup available',
|
|
272
|
+
path: operation.path
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
case 'manifest_update':
|
|
278
|
+
// Special handling for manifest updates
|
|
279
|
+
await this.rollbackManifestUpdate(operation, results);
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
case 'metadata_update':
|
|
283
|
+
// Special handling for metadata updates
|
|
284
|
+
await this.rollbackMetadataUpdate(operation, results);
|
|
285
|
+
break;
|
|
286
|
+
|
|
287
|
+
default:
|
|
288
|
+
results.warnings.push({
|
|
289
|
+
operation: operation.id,
|
|
290
|
+
warning: `Unknown operation type: ${operation.type}`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get the last transaction for selective rollback
|
|
297
|
+
* @returns {Promise<Object|null>} Last transaction
|
|
298
|
+
*/
|
|
299
|
+
async getLastTransaction() {
|
|
300
|
+
try {
|
|
301
|
+
const transactionsDir = this.transactionPath;
|
|
302
|
+
if (!await fs.pathExists(transactionsDir)) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Get all transaction files
|
|
307
|
+
const files = await fs.readdir(transactionsDir);
|
|
308
|
+
const transactionFiles = files.filter(f => f.endsWith('.json'));
|
|
309
|
+
|
|
310
|
+
if (transactionFiles.length === 0) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Sort by timestamp (newest first)
|
|
315
|
+
const transactions = [];
|
|
316
|
+
for (const file of transactionFiles) {
|
|
317
|
+
const transaction = await fs.readJson(path.join(transactionsDir, file));
|
|
318
|
+
transactions.push(transaction);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
transactions.sort((a, b) =>
|
|
322
|
+
new Date(b.startTime) - new Date(a.startTime)
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
return transactions[0];
|
|
326
|
+
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error(chalk.red(`Failed to get last transaction: ${error.message}`));
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* List recent transactions
|
|
335
|
+
* @param {number} limit - Number of transactions to return
|
|
336
|
+
* @returns {Promise<Array>} Recent transactions
|
|
337
|
+
*/
|
|
338
|
+
async listTransactions(limit = 10) {
|
|
339
|
+
try {
|
|
340
|
+
const transactionsDir = this.transactionPath;
|
|
341
|
+
if (!await fs.pathExists(transactionsDir)) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const files = await fs.readdir(transactionsDir);
|
|
346
|
+
const transactionFiles = files.filter(f => f.endsWith('.json'));
|
|
347
|
+
|
|
348
|
+
const transactions = [];
|
|
349
|
+
for (const file of transactionFiles) {
|
|
350
|
+
const transaction = await fs.readJson(path.join(transactionsDir, file));
|
|
351
|
+
transactions.push({
|
|
352
|
+
id: transaction.id,
|
|
353
|
+
type: transaction.type,
|
|
354
|
+
description: transaction.description,
|
|
355
|
+
user: transaction.user,
|
|
356
|
+
startTime: transaction.startTime,
|
|
357
|
+
endTime: transaction.endTime,
|
|
358
|
+
status: transaction.status,
|
|
359
|
+
operations: transaction.operations.length
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Sort by start time (newest first)
|
|
364
|
+
transactions.sort((a, b) =>
|
|
365
|
+
new Date(b.startTime) - new Date(a.startTime)
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
return transactions.slice(0, limit);
|
|
369
|
+
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error(chalk.red(`Failed to list transactions: ${error.message}`));
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Clean up old transactions
|
|
378
|
+
* @returns {Promise<number>} Number of transactions cleaned
|
|
379
|
+
*/
|
|
380
|
+
async cleanupOldTransactions() {
|
|
381
|
+
try {
|
|
382
|
+
const transactionsDir = this.transactionPath;
|
|
383
|
+
if (!await fs.pathExists(transactionsDir)) {
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const cutoffDate = new Date();
|
|
388
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.retentionDays);
|
|
389
|
+
|
|
390
|
+
const files = await fs.readdir(transactionsDir);
|
|
391
|
+
let cleaned = 0;
|
|
392
|
+
|
|
393
|
+
for (const file of files) {
|
|
394
|
+
if (file.endsWith('.json')) {
|
|
395
|
+
const filePath = path.join(transactionsDir, file);
|
|
396
|
+
const transaction = await fs.readJson(filePath);
|
|
397
|
+
|
|
398
|
+
if (new Date(transaction.startTime) < cutoffDate) {
|
|
399
|
+
// Clean up transaction and its backups
|
|
400
|
+
await fs.remove(filePath);
|
|
401
|
+
|
|
402
|
+
// Remove associated backups
|
|
403
|
+
if (transaction.backups) {
|
|
404
|
+
for (const backup of transaction.backups) {
|
|
405
|
+
if (await fs.pathExists(backup.path)) {
|
|
406
|
+
await fs.remove(backup.path);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
cleaned++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
console.log(chalk.gray(`🧹 Cleaned up ${cleaned} old transactions`));
|
|
417
|
+
return cleaned;
|
|
418
|
+
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error(chalk.red(`Cleanup failed: ${error.message}`));
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Backup current state of a file
|
|
427
|
+
* @private
|
|
428
|
+
*/
|
|
429
|
+
async backupCurrentState(filePath) {
|
|
430
|
+
try {
|
|
431
|
+
if (!await fs.pathExists(filePath)) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
436
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
437
|
+
|
|
438
|
+
const backup = {
|
|
439
|
+
path: filePath,
|
|
440
|
+
content: content,
|
|
441
|
+
hash: hash,
|
|
442
|
+
timestamp: new Date().toISOString()
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Save backup
|
|
446
|
+
const backupId = `backup-${Date.now()}-${hash.substr(0, 8)}`;
|
|
447
|
+
const backupPath = path.join(this.backupPath, backupId);
|
|
448
|
+
|
|
449
|
+
await fs.ensureDir(this.backupPath);
|
|
450
|
+
await fs.writeJson(backupPath, backup, { spaces: 2 });
|
|
451
|
+
|
|
452
|
+
return backupId;
|
|
453
|
+
|
|
454
|
+
} catch (error) {
|
|
455
|
+
console.error(chalk.red(`Backup failed: ${error.message}`));
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Restore from backup
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
async restoreFromBackup(targetPath, backupId) {
|
|
465
|
+
try {
|
|
466
|
+
const backupPath = path.join(this.backupPath, backupId);
|
|
467
|
+
const backup = await fs.readJson(backupPath);
|
|
468
|
+
|
|
469
|
+
// Ensure directory exists
|
|
470
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
471
|
+
|
|
472
|
+
// Restore content
|
|
473
|
+
await fs.writeFile(targetPath, backup.content, 'utf8');
|
|
474
|
+
|
|
475
|
+
console.log(chalk.green(` ✓ Restored: ${path.basename(targetPath)}`));
|
|
476
|
+
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error(chalk.red(`Restore failed: ${error.message}`));
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Rollback manifest update
|
|
485
|
+
* @private
|
|
486
|
+
*/
|
|
487
|
+
async rollbackManifestUpdate(operation, results) {
|
|
488
|
+
try {
|
|
489
|
+
const manifestPath = path.join(this.rootPath, 'aios-core', 'team-manifest.yaml');
|
|
490
|
+
|
|
491
|
+
if (operation.previousState) {
|
|
492
|
+
// Restore previous manifest state
|
|
493
|
+
const backupPath = path.join(this.backupPath, operation.previousState);
|
|
494
|
+
const backup = await fs.readJson(backupPath);
|
|
495
|
+
await fs.writeFile(manifestPath, backup.content, 'utf8');
|
|
496
|
+
|
|
497
|
+
results.successful.push({
|
|
498
|
+
operation: operation.id,
|
|
499
|
+
action: 'manifest_restored',
|
|
500
|
+
path: manifestPath
|
|
501
|
+
});
|
|
502
|
+
} else {
|
|
503
|
+
results.warnings.push({
|
|
504
|
+
operation: operation.id,
|
|
505
|
+
warning: 'No manifest backup available'
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
} catch (error) {
|
|
510
|
+
results.failed.push({
|
|
511
|
+
operation: operation.id,
|
|
512
|
+
error: `Manifest rollback failed: ${error.message}`
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Rollback metadata update
|
|
519
|
+
* @private
|
|
520
|
+
*/
|
|
521
|
+
async rollbackMetadataUpdate(operation, results) {
|
|
522
|
+
try {
|
|
523
|
+
if (operation.metadata?.componentType && operation.metadata?.componentId) {
|
|
524
|
+
// Revert metadata changes
|
|
525
|
+
// This would need integration with ComponentMetadata
|
|
526
|
+
results.warnings.push({
|
|
527
|
+
operation: operation.id,
|
|
528
|
+
warning: 'Metadata rollback not fully implemented'
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
} catch (error) {
|
|
533
|
+
results.failed.push({
|
|
534
|
+
operation: operation.id,
|
|
535
|
+
error: `Metadata rollback failed: ${error.message}`
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Archive backups after successful commit
|
|
542
|
+
* @private
|
|
543
|
+
*/
|
|
544
|
+
async archiveBackups(transaction) {
|
|
545
|
+
// Move backups to archive directory with transaction reference
|
|
546
|
+
const archivePath = path.join(this.backupPath, 'archive', transaction.id);
|
|
547
|
+
await fs.ensureDir(archivePath);
|
|
548
|
+
|
|
549
|
+
// Archive transaction file
|
|
550
|
+
const transactionArchive = path.join(archivePath, 'transaction.json');
|
|
551
|
+
await fs.writeJson(transactionArchive, transaction, { spaces: 2 });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Generate transaction ID
|
|
556
|
+
* @private
|
|
557
|
+
*/
|
|
558
|
+
generateTransactionId() {
|
|
559
|
+
return `txn-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Save transaction to disk
|
|
564
|
+
* @private
|
|
565
|
+
*/
|
|
566
|
+
async saveTransaction(transaction) {
|
|
567
|
+
await fs.ensureDir(this.transactionPath);
|
|
568
|
+
const filePath = path.join(this.transactionPath, `${transaction.id}.json`);
|
|
569
|
+
await fs.writeJson(filePath, transaction, { spaces: 2 });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Load transaction from disk
|
|
574
|
+
* @private
|
|
575
|
+
*/
|
|
576
|
+
async loadTransaction(transactionId) {
|
|
577
|
+
try {
|
|
578
|
+
const filePath = path.join(this.transactionPath, `${transactionId}.json`);
|
|
579
|
+
if (await fs.pathExists(filePath)) {
|
|
580
|
+
return await fs.readJson(filePath);
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error(chalk.red(`Failed to load transaction: ${error.message}`));
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
590
|
module.exports = TransactionManager;
|