@git.zone/cli 1.15.5 → 1.16.1

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.
Files changed (81) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/gitzone.cli.js +26 -2
  3. package/dist_ts/gitzone.logging.d.ts +3 -0
  4. package/dist_ts/gitzone.logging.js +20 -2
  5. package/dist_ts/mod_format/classes.baseformatter.d.ts +21 -0
  6. package/dist_ts/mod_format/classes.baseformatter.js +73 -0
  7. package/dist_ts/mod_format/classes.changecache.d.ts +25 -0
  8. package/dist_ts/mod_format/classes.changecache.js +105 -0
  9. package/dist_ts/mod_format/classes.dependency-analyzer.d.ts +13 -0
  10. package/dist_ts/mod_format/classes.dependency-analyzer.js +87 -0
  11. package/dist_ts/mod_format/classes.diffreporter.d.ts +13 -0
  12. package/dist_ts/mod_format/classes.diffreporter.js +94 -0
  13. package/dist_ts/mod_format/classes.formatcontext.d.ts +19 -0
  14. package/dist_ts/mod_format/classes.formatcontext.js +51 -0
  15. package/dist_ts/mod_format/classes.formatplanner.d.ts +13 -0
  16. package/dist_ts/mod_format/classes.formatplanner.js +164 -0
  17. package/dist_ts/mod_format/classes.formatstats.d.ts +40 -0
  18. package/dist_ts/mod_format/classes.formatstats.js +161 -0
  19. package/dist_ts/mod_format/classes.rollbackmanager.d.ts +21 -0
  20. package/dist_ts/mod_format/classes.rollbackmanager.js +170 -0
  21. package/dist_ts/mod_format/format.copy.d.ts +22 -0
  22. package/dist_ts/mod_format/format.copy.js +66 -2
  23. package/dist_ts/mod_format/format.packagejson.js +48 -2
  24. package/dist_ts/mod_format/formatters/cleanup.formatter.d.ts +7 -0
  25. package/dist_ts/mod_format/formatters/cleanup.formatter.js +33 -0
  26. package/dist_ts/mod_format/formatters/copy.formatter.d.ts +4 -0
  27. package/dist_ts/mod_format/formatters/copy.formatter.js +8 -0
  28. package/dist_ts/mod_format/formatters/gitignore.formatter.d.ts +4 -0
  29. package/dist_ts/mod_format/formatters/gitignore.formatter.js +8 -0
  30. package/dist_ts/mod_format/formatters/legacy.formatter.d.ts +11 -0
  31. package/dist_ts/mod_format/formatters/legacy.formatter.js +29 -0
  32. package/dist_ts/mod_format/formatters/license.formatter.d.ts +4 -0
  33. package/dist_ts/mod_format/formatters/license.formatter.js +8 -0
  34. package/dist_ts/mod_format/formatters/npmextra.formatter.d.ts +4 -0
  35. package/dist_ts/mod_format/formatters/npmextra.formatter.js +8 -0
  36. package/dist_ts/mod_format/formatters/packagejson.formatter.d.ts +4 -0
  37. package/dist_ts/mod_format/formatters/packagejson.formatter.js +8 -0
  38. package/dist_ts/mod_format/formatters/prettier.formatter.d.ts +9 -0
  39. package/dist_ts/mod_format/formatters/prettier.formatter.js +111 -0
  40. package/dist_ts/mod_format/formatters/readme.formatter.d.ts +7 -0
  41. package/dist_ts/mod_format/formatters/readme.formatter.js +19 -0
  42. package/dist_ts/mod_format/formatters/templates.formatter.d.ts +4 -0
  43. package/dist_ts/mod_format/formatters/templates.formatter.js +8 -0
  44. package/dist_ts/mod_format/formatters/tsconfig.formatter.d.ts +4 -0
  45. package/dist_ts/mod_format/formatters/tsconfig.formatter.js +8 -0
  46. package/dist_ts/mod_format/index.d.ts +14 -1
  47. package/dist_ts/mod_format/index.js +202 -28
  48. package/dist_ts/mod_format/interfaces.format.d.ts +43 -0
  49. package/dist_ts/mod_format/interfaces.format.js +2 -0
  50. package/dist_ts/mod_format/mod.plugins.d.ts +5 -1
  51. package/dist_ts/mod_format/mod.plugins.js +6 -2
  52. package/package.json +2 -1
  53. package/readme.hints.md +187 -0
  54. package/readme.plan.md +170 -0
  55. package/ts/00_commitinfo_data.ts +1 -1
  56. package/ts/gitzone.cli.ts +29 -1
  57. package/ts/gitzone.logging.ts +23 -1
  58. package/ts/mod_format/classes.baseformatter.ts +93 -0
  59. package/ts/mod_format/classes.changecache.ts +144 -0
  60. package/ts/mod_format/classes.dependency-analyzer.ts +107 -0
  61. package/ts/mod_format/classes.diffreporter.ts +108 -0
  62. package/ts/mod_format/classes.formatcontext.ts +65 -0
  63. package/ts/mod_format/classes.formatplanner.ts +184 -0
  64. package/ts/mod_format/classes.formatstats.ts +209 -0
  65. package/ts/mod_format/classes.rollbackmanager.ts +218 -0
  66. package/ts/mod_format/format.copy.ts +77 -1
  67. package/ts/mod_format/format.packagejson.ts +50 -1
  68. package/ts/mod_format/formatters/cleanup.formatter.ts +39 -0
  69. package/ts/mod_format/formatters/copy.formatter.ts +8 -0
  70. package/ts/mod_format/formatters/gitignore.formatter.ts +8 -0
  71. package/ts/mod_format/formatters/legacy.formatter.ts +36 -0
  72. package/ts/mod_format/formatters/license.formatter.ts +8 -0
  73. package/ts/mod_format/formatters/npmextra.formatter.ts +8 -0
  74. package/ts/mod_format/formatters/packagejson.formatter.ts +8 -0
  75. package/ts/mod_format/formatters/prettier.formatter.ts +125 -0
  76. package/ts/mod_format/formatters/readme.formatter.ts +22 -0
  77. package/ts/mod_format/formatters/templates.formatter.ts +8 -0
  78. package/ts/mod_format/formatters/tsconfig.formatter.ts +8 -0
  79. package/ts/mod_format/index.ts +241 -33
  80. package/ts/mod_format/interfaces.format.ts +45 -0
  81. package/ts/mod_format/mod.plugins.ts +8 -0
@@ -0,0 +1,65 @@
1
+ import * as plugins from './mod.plugins.js';
2
+ import { RollbackManager } from './classes.rollbackmanager.js';
3
+ import { ChangeCache } from './classes.changecache.js';
4
+ import { FormatStats } from './classes.formatstats.js';
5
+ import type { IFormatOperation, IFormatPlan } from './interfaces.format.js';
6
+
7
+ export class FormatContext {
8
+ private rollbackManager: RollbackManager;
9
+ private currentOperation: IFormatOperation | null = null;
10
+ private changeCache: ChangeCache;
11
+ private formatStats: FormatStats;
12
+
13
+ constructor() {
14
+ this.rollbackManager = new RollbackManager();
15
+ this.changeCache = new ChangeCache();
16
+ this.formatStats = new FormatStats();
17
+ }
18
+
19
+ async beginOperation(): Promise<void> {
20
+ this.currentOperation = await this.rollbackManager.createOperation();
21
+ }
22
+
23
+ async trackFileChange(filepath: string): Promise<void> {
24
+ if (!this.currentOperation) {
25
+ throw new Error('No operation in progress. Call beginOperation() first.');
26
+ }
27
+ await this.rollbackManager.backupFile(filepath, this.currentOperation.id);
28
+ }
29
+
30
+ async commitOperation(): Promise<void> {
31
+ if (!this.currentOperation) {
32
+ throw new Error('No operation in progress. Call beginOperation() first.');
33
+ }
34
+ await this.rollbackManager.markComplete(this.currentOperation.id);
35
+ this.currentOperation = null;
36
+ }
37
+
38
+ async rollbackOperation(): Promise<void> {
39
+ if (!this.currentOperation) {
40
+ throw new Error('No operation in progress. Call beginOperation() first.');
41
+ }
42
+ await this.rollbackManager.rollback(this.currentOperation.id);
43
+ this.currentOperation = null;
44
+ }
45
+
46
+ async rollbackTo(operationId: string): Promise<void> {
47
+ await this.rollbackManager.rollback(operationId);
48
+ }
49
+
50
+ getRollbackManager(): RollbackManager {
51
+ return this.rollbackManager;
52
+ }
53
+
54
+ getChangeCache(): ChangeCache {
55
+ return this.changeCache;
56
+ }
57
+
58
+ async initializeCache(): Promise<void> {
59
+ await this.changeCache.initialize();
60
+ }
61
+
62
+ getFormatStats(): FormatStats {
63
+ return this.formatStats;
64
+ }
65
+ }
@@ -0,0 +1,184 @@
1
+ import * as plugins from './mod.plugins.js';
2
+ import { FormatContext } from './classes.formatcontext.js';
3
+ import { BaseFormatter } from './classes.baseformatter.js';
4
+ import type { IFormatPlan, IPlannedChange } from './interfaces.format.js';
5
+ import { logger } from '../gitzone.logging.js';
6
+ import { DependencyAnalyzer } from './classes.dependency-analyzer.js';
7
+ import { DiffReporter } from './classes.diffreporter.js';
8
+
9
+ export class FormatPlanner {
10
+ private plannedChanges: Map<string, IPlannedChange[]> = new Map();
11
+ private dependencyAnalyzer = new DependencyAnalyzer();
12
+ private diffReporter = new DiffReporter();
13
+
14
+ async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
15
+ const plan: IFormatPlan = {
16
+ summary: {
17
+ totalFiles: 0,
18
+ filesAdded: 0,
19
+ filesModified: 0,
20
+ filesRemoved: 0,
21
+ estimatedTime: 0
22
+ },
23
+ changes: [],
24
+ warnings: []
25
+ };
26
+
27
+ for (const module of modules) {
28
+ try {
29
+ const changes = await module.analyze();
30
+ this.plannedChanges.set(module.name, changes);
31
+
32
+ for (const change of changes) {
33
+ plan.changes.push(change);
34
+
35
+ // Update summary
36
+ switch (change.type) {
37
+ case 'create':
38
+ plan.summary.filesAdded++;
39
+ break;
40
+ case 'modify':
41
+ plan.summary.filesModified++;
42
+ break;
43
+ case 'delete':
44
+ plan.summary.filesRemoved++;
45
+ break;
46
+ }
47
+ }
48
+ } catch (error) {
49
+ plan.warnings.push({
50
+ level: 'error',
51
+ message: `Failed to analyze module ${module.name}: ${error.message}`,
52
+ module: module.name
53
+ });
54
+ }
55
+ }
56
+
57
+ plan.summary.totalFiles = plan.summary.filesAdded + plan.summary.filesModified + plan.summary.filesRemoved;
58
+ plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate
59
+
60
+ return plan;
61
+ }
62
+
63
+ async executePlan(plan: IFormatPlan, modules: BaseFormatter[], context: FormatContext, parallel: boolean = true): Promise<void> {
64
+ await context.beginOperation();
65
+ const startTime = Date.now();
66
+
67
+ try {
68
+ if (parallel) {
69
+ // Get execution groups based on dependencies
70
+ const executionGroups = this.dependencyAnalyzer.getExecutionGroups(modules);
71
+
72
+ logger.log('info', `Executing formatters in ${executionGroups.length} groups...`);
73
+
74
+ for (let i = 0; i < executionGroups.length; i++) {
75
+ const group = executionGroups[i];
76
+ logger.log('info', `Executing group ${i + 1}: ${group.map(m => m.name).join(', ')}`);
77
+
78
+ // Execute modules in this group in parallel
79
+ const promises = group.map(async (module) => {
80
+ const changes = this.plannedChanges.get(module.name) || [];
81
+ if (changes.length > 0) {
82
+ logger.log('info', `Executing ${module.name} formatter...`);
83
+ await module.execute(changes);
84
+ }
85
+ });
86
+
87
+ await Promise.all(promises);
88
+ }
89
+ } else {
90
+ // Sequential execution (original implementation)
91
+ for (const module of modules) {
92
+ const changes = this.plannedChanges.get(module.name) || [];
93
+
94
+ if (changes.length > 0) {
95
+ logger.log('info', `Executing ${module.name} formatter...`);
96
+ await module.execute(changes);
97
+ }
98
+ }
99
+ }
100
+
101
+ const endTime = Date.now();
102
+ const duration = endTime - startTime;
103
+ logger.log('info', `Format operations completed in ${duration}ms`);
104
+
105
+ await context.commitOperation();
106
+ } catch (error) {
107
+ await context.rollbackOperation();
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ async displayPlan(plan: IFormatPlan, detailed: boolean = false): Promise<void> {
113
+ console.log('\nFormat Plan:');
114
+ console.log('━'.repeat(50));
115
+ console.log(`Summary: ${plan.summary.totalFiles} files will be changed`);
116
+ console.log(` • ${plan.summary.filesAdded} new files`);
117
+ console.log(` • ${plan.summary.filesModified} modified files`);
118
+ console.log(` • ${plan.summary.filesRemoved} deleted files`);
119
+ console.log('');
120
+ console.log('Changes by module:');
121
+
122
+ // Group changes by module
123
+ const changesByModule = new Map<string, IPlannedChange[]>();
124
+ for (const change of plan.changes) {
125
+ const moduleChanges = changesByModule.get(change.module) || [];
126
+ moduleChanges.push(change);
127
+ changesByModule.set(change.module, moduleChanges);
128
+ }
129
+
130
+ for (const [module, changes] of changesByModule) {
131
+ console.log(`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`);
132
+
133
+ for (const change of changes) {
134
+ const icon = this.getChangeIcon(change.type);
135
+ console.log(` ${icon} ${change.path} - ${change.description}`);
136
+
137
+ // Show diff for modified files if detailed view is requested
138
+ if (detailed && change.type === 'modify') {
139
+ const diff = await this.diffReporter.generateDiffForChange(change);
140
+ if (diff) {
141
+ this.diffReporter.displayDiff(change.path, diff);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ if (plan.warnings.length > 0) {
148
+ console.log('\nWarnings:');
149
+ for (const warning of plan.warnings) {
150
+ const icon = warning.level === 'error' ? '❌' : '⚠️';
151
+ console.log(` ${icon} ${warning.message}`);
152
+ }
153
+ }
154
+
155
+ console.log('\n' + '━'.repeat(50));
156
+ }
157
+
158
+ private getModuleIcon(module: string): string {
159
+ const icons: Record<string, string> = {
160
+ 'packagejson': '📦',
161
+ 'license': '📝',
162
+ 'tsconfig': '🔧',
163
+ 'cleanup': '🚮',
164
+ 'gitignore': '🔒',
165
+ 'prettier': '✨',
166
+ 'readme': '📖',
167
+ 'templates': '📄',
168
+ 'npmextra': '⚙️',
169
+ 'copy': '📋'
170
+ };
171
+ return icons[module] || '📁';
172
+ }
173
+
174
+ private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
175
+ switch (type) {
176
+ case 'create':
177
+ return '✅';
178
+ case 'modify':
179
+ return '✏️';
180
+ case 'delete':
181
+ return '❌';
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,209 @@
1
+ import * as plugins from './mod.plugins.js';
2
+ import { logger } from '../gitzone.logging.js';
3
+
4
+ export interface IModuleStats {
5
+ name: string;
6
+ filesProcessed: number;
7
+ executionTime: number;
8
+ errors: number;
9
+ successes: number;
10
+ filesCreated: number;
11
+ filesModified: number;
12
+ filesDeleted: number;
13
+ }
14
+
15
+ export interface IFormatStats {
16
+ totalExecutionTime: number;
17
+ startTime: number;
18
+ endTime: number;
19
+ moduleStats: Map<string, IModuleStats>;
20
+ overallStats: {
21
+ totalFiles: number;
22
+ totalCreated: number;
23
+ totalModified: number;
24
+ totalDeleted: number;
25
+ totalErrors: number;
26
+ cacheHits: number;
27
+ cacheMisses: number;
28
+ };
29
+ }
30
+
31
+ export class FormatStats {
32
+ private stats: IFormatStats;
33
+
34
+ constructor() {
35
+ this.stats = {
36
+ totalExecutionTime: 0,
37
+ startTime: Date.now(),
38
+ endTime: 0,
39
+ moduleStats: new Map(),
40
+ overallStats: {
41
+ totalFiles: 0,
42
+ totalCreated: 0,
43
+ totalModified: 0,
44
+ totalDeleted: 0,
45
+ totalErrors: 0,
46
+ cacheHits: 0,
47
+ cacheMisses: 0
48
+ }
49
+ };
50
+ }
51
+
52
+ startModule(moduleName: string): void {
53
+ this.stats.moduleStats.set(moduleName, {
54
+ name: moduleName,
55
+ filesProcessed: 0,
56
+ executionTime: 0,
57
+ errors: 0,
58
+ successes: 0,
59
+ filesCreated: 0,
60
+ filesModified: 0,
61
+ filesDeleted: 0
62
+ });
63
+ }
64
+
65
+ moduleStartTime(moduleName: string): number {
66
+ return Date.now();
67
+ }
68
+
69
+ endModule(moduleName: string, startTime: number): void {
70
+ const moduleStats = this.stats.moduleStats.get(moduleName);
71
+ if (moduleStats) {
72
+ moduleStats.executionTime = Date.now() - startTime;
73
+ }
74
+ }
75
+
76
+ recordFileOperation(moduleName: string, operation: 'create' | 'modify' | 'delete', success: boolean = true): void {
77
+ const moduleStats = this.stats.moduleStats.get(moduleName);
78
+ if (!moduleStats) return;
79
+
80
+ moduleStats.filesProcessed++;
81
+
82
+ if (success) {
83
+ moduleStats.successes++;
84
+ this.stats.overallStats.totalFiles++;
85
+
86
+ switch (operation) {
87
+ case 'create':
88
+ moduleStats.filesCreated++;
89
+ this.stats.overallStats.totalCreated++;
90
+ break;
91
+ case 'modify':
92
+ moduleStats.filesModified++;
93
+ this.stats.overallStats.totalModified++;
94
+ break;
95
+ case 'delete':
96
+ moduleStats.filesDeleted++;
97
+ this.stats.overallStats.totalDeleted++;
98
+ break;
99
+ }
100
+ } else {
101
+ moduleStats.errors++;
102
+ this.stats.overallStats.totalErrors++;
103
+ }
104
+ }
105
+
106
+ recordCacheHit(): void {
107
+ this.stats.overallStats.cacheHits++;
108
+ }
109
+
110
+ recordCacheMiss(): void {
111
+ this.stats.overallStats.cacheMisses++;
112
+ }
113
+
114
+ finish(): void {
115
+ this.stats.endTime = Date.now();
116
+ this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
117
+ }
118
+
119
+ displayStats(): void {
120
+ console.log('\n📊 Format Operation Statistics:');
121
+ console.log('═'.repeat(50));
122
+
123
+ // Overall stats
124
+ console.log('\nOverall Summary:');
125
+ console.log(` Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`);
126
+ console.log(` Files Processed: ${this.stats.overallStats.totalFiles}`);
127
+ console.log(` • Created: ${this.stats.overallStats.totalCreated}`);
128
+ console.log(` • Modified: ${this.stats.overallStats.totalModified}`);
129
+ console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
130
+ console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
131
+
132
+ if (this.stats.overallStats.cacheHits > 0 || this.stats.overallStats.cacheMisses > 0) {
133
+ const cacheHitRate = this.stats.overallStats.cacheHits /
134
+ (this.stats.overallStats.cacheHits + this.stats.overallStats.cacheMisses) * 100;
135
+ console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`);
136
+ console.log(` • Hits: ${this.stats.overallStats.cacheHits}`);
137
+ console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
138
+ }
139
+
140
+ // Module stats
141
+ console.log('\nModule Breakdown:');
142
+ console.log('─'.repeat(50));
143
+
144
+ const sortedModules = Array.from(this.stats.moduleStats.values())
145
+ .sort((a, b) => b.filesProcessed - a.filesProcessed);
146
+
147
+ for (const moduleStats of sortedModules) {
148
+ console.log(`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`);
149
+ console.log(` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`);
150
+ console.log(` Files Processed: ${moduleStats.filesProcessed}`);
151
+
152
+ if (moduleStats.filesCreated > 0) {
153
+ console.log(` • Created: ${moduleStats.filesCreated}`);
154
+ }
155
+ if (moduleStats.filesModified > 0) {
156
+ console.log(` • Modified: ${moduleStats.filesModified}`);
157
+ }
158
+ if (moduleStats.filesDeleted > 0) {
159
+ console.log(` • Deleted: ${moduleStats.filesDeleted}`);
160
+ }
161
+
162
+ if (moduleStats.errors > 0) {
163
+ console.log(` ❌ Errors: ${moduleStats.errors}`);
164
+ }
165
+ }
166
+
167
+ console.log('\n' + '═'.repeat(50));
168
+ }
169
+
170
+ async saveReport(outputPath: string): Promise<void> {
171
+ const report = {
172
+ timestamp: new Date().toISOString(),
173
+ executionTime: this.stats.totalExecutionTime,
174
+ overallStats: this.stats.overallStats,
175
+ moduleStats: Array.from(this.stats.moduleStats.values())
176
+ };
177
+
178
+ await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath);
179
+ logger.log('info', `Statistics report saved to ${outputPath}`);
180
+ }
181
+
182
+ private formatDuration(ms: number): string {
183
+ if (ms < 1000) {
184
+ return `${ms}ms`;
185
+ } else if (ms < 60000) {
186
+ return `${(ms / 1000).toFixed(1)}s`;
187
+ } else {
188
+ const minutes = Math.floor(ms / 60000);
189
+ const seconds = Math.floor((ms % 60000) / 1000);
190
+ return `${minutes}m ${seconds}s`;
191
+ }
192
+ }
193
+
194
+ private getModuleIcon(module: string): string {
195
+ const icons: Record<string, string> = {
196
+ 'packagejson': '📦',
197
+ 'license': '📝',
198
+ 'tsconfig': '🔧',
199
+ 'cleanup': '🚮',
200
+ 'gitignore': '🔒',
201
+ 'prettier': '✨',
202
+ 'readme': '📖',
203
+ 'templates': '📄',
204
+ 'npmextra': '⚙️',
205
+ 'copy': '📋'
206
+ };
207
+ return icons[module] || '📁';
208
+ }
209
+ }
@@ -0,0 +1,218 @@
1
+ import * as plugins from './mod.plugins.js';
2
+ import * as paths from '../paths.js';
3
+ import type { IFormatOperation } from './interfaces.format.js';
4
+
5
+ export class RollbackManager {
6
+ private backupDir: string;
7
+ private manifestPath: string;
8
+
9
+ constructor() {
10
+ this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups');
11
+ this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json');
12
+ }
13
+
14
+ async createOperation(): Promise<IFormatOperation> {
15
+ await this.ensureBackupDir();
16
+
17
+ const operation: IFormatOperation = {
18
+ id: this.generateOperationId(),
19
+ timestamp: Date.now(),
20
+ files: [],
21
+ status: 'pending'
22
+ };
23
+
24
+ await this.updateManifest(operation);
25
+ return operation;
26
+ }
27
+
28
+ async backupFile(filepath: string, operationId: string): Promise<void> {
29
+ const operation = await this.getOperation(operationId);
30
+ if (!operation) {
31
+ throw new Error(`Operation ${operationId} not found`);
32
+ }
33
+
34
+ const absolutePath = plugins.path.isAbsolute(filepath)
35
+ ? filepath
36
+ : plugins.path.join(paths.cwd, filepath);
37
+
38
+ // Check if file exists
39
+ const exists = await plugins.smartfile.fs.fileExists(absolutePath);
40
+ if (!exists) {
41
+ // File doesn't exist yet (will be created), so we skip backup
42
+ return;
43
+ }
44
+
45
+ // Read file content and metadata
46
+ const content = await plugins.smartfile.fs.toStringSync(absolutePath);
47
+ const stats = await plugins.smartfile.fs.stat(absolutePath);
48
+ const checksum = this.calculateChecksum(content);
49
+
50
+ // Create backup
51
+ const backupPath = this.getBackupPath(operationId, filepath);
52
+ await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath));
53
+ await plugins.smartfile.memory.toFs(content, backupPath);
54
+
55
+ // Update operation
56
+ operation.files.push({
57
+ path: filepath,
58
+ originalContent: content,
59
+ checksum,
60
+ permissions: stats.mode.toString(8)
61
+ });
62
+
63
+ await this.updateManifest(operation);
64
+ }
65
+
66
+ async rollback(operationId: string): Promise<void> {
67
+ const operation = await this.getOperation(operationId);
68
+ if (!operation) {
69
+ throw new Error(`Operation ${operationId} not found`);
70
+ }
71
+
72
+ if (operation.status === 'rolled-back') {
73
+ throw new Error(`Operation ${operationId} has already been rolled back`);
74
+ }
75
+
76
+ // Restore files in reverse order
77
+ for (let i = operation.files.length - 1; i >= 0; i--) {
78
+ const file = operation.files[i];
79
+ const absolutePath = plugins.path.isAbsolute(file.path)
80
+ ? file.path
81
+ : plugins.path.join(paths.cwd, file.path);
82
+
83
+ // Verify backup integrity
84
+ const backupPath = this.getBackupPath(operationId, file.path);
85
+ const backupContent = await plugins.smartfile.fs.toStringSync(backupPath);
86
+ const backupChecksum = this.calculateChecksum(backupContent);
87
+
88
+ if (backupChecksum !== file.checksum) {
89
+ throw new Error(`Backup integrity check failed for ${file.path}`);
90
+ }
91
+
92
+ // Restore file
93
+ await plugins.smartfile.memory.toFs(file.originalContent, absolutePath);
94
+
95
+ // Restore permissions
96
+ const mode = parseInt(file.permissions, 8);
97
+ // Note: Permissions restoration may not work on all platforms
98
+ }
99
+
100
+ // Update operation status
101
+ operation.status = 'rolled-back';
102
+ await this.updateManifest(operation);
103
+ }
104
+
105
+ async markComplete(operationId: string): Promise<void> {
106
+ const operation = await this.getOperation(operationId);
107
+ if (!operation) {
108
+ throw new Error(`Operation ${operationId} not found`);
109
+ }
110
+
111
+ operation.status = 'completed';
112
+ await this.updateManifest(operation);
113
+ }
114
+
115
+ async cleanOldBackups(retentionDays: number): Promise<void> {
116
+ const manifest = await this.getManifest();
117
+ const cutoffTime = Date.now() - (retentionDays * 24 * 60 * 60 * 1000);
118
+
119
+ const operationsToDelete = manifest.operations.filter(op =>
120
+ op.timestamp < cutoffTime && op.status === 'completed'
121
+ );
122
+
123
+ for (const operation of operationsToDelete) {
124
+ // Remove backup files
125
+ const operationDir = plugins.path.join(this.backupDir, 'operations', operation.id);
126
+ await plugins.smartfile.fs.remove(operationDir);
127
+
128
+ // Remove from manifest
129
+ manifest.operations = manifest.operations.filter(op => op.id !== operation.id);
130
+ }
131
+
132
+ await this.saveManifest(manifest);
133
+ }
134
+
135
+ async verifyBackup(operationId: string): Promise<boolean> {
136
+ const operation = await this.getOperation(operationId);
137
+ if (!operation) {
138
+ return false;
139
+ }
140
+
141
+ for (const file of operation.files) {
142
+ const backupPath = this.getBackupPath(operationId, file.path);
143
+ const exists = await plugins.smartfile.fs.fileExists(backupPath);
144
+
145
+ if (!exists) {
146
+ return false;
147
+ }
148
+
149
+ const content = await plugins.smartfile.fs.toStringSync(backupPath);
150
+ const checksum = this.calculateChecksum(content);
151
+
152
+ if (checksum !== file.checksum) {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ return true;
158
+ }
159
+
160
+ async listBackups(): Promise<IFormatOperation[]> {
161
+ const manifest = await this.getManifest();
162
+ return manifest.operations;
163
+ }
164
+
165
+ private async ensureBackupDir(): Promise<void> {
166
+ await plugins.smartfile.fs.ensureDir(this.backupDir);
167
+ await plugins.smartfile.fs.ensureDir(plugins.path.join(this.backupDir, 'operations'));
168
+ }
169
+
170
+ private generateOperationId(): string {
171
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
172
+ const random = Math.random().toString(36).substring(2, 8);
173
+ return `${timestamp}-${random}`;
174
+ }
175
+
176
+ private getBackupPath(operationId: string, filepath: string): string {
177
+ const filename = plugins.path.basename(filepath);
178
+ const dir = plugins.path.dirname(filepath);
179
+ const safeDir = dir.replace(/[/\\]/g, '__');
180
+ return plugins.path.join(this.backupDir, 'operations', operationId, 'files', safeDir, `${filename}.backup`);
181
+ }
182
+
183
+ private calculateChecksum(content: string | Buffer): string {
184
+ return plugins.crypto.createHash('sha256').update(content).digest('hex');
185
+ }
186
+
187
+ private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
188
+ const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
189
+ if (!exists) {
190
+ return { operations: [] };
191
+ }
192
+
193
+ const content = await plugins.smartfile.fs.toStringSync(this.manifestPath);
194
+ return JSON.parse(content);
195
+ }
196
+
197
+ private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> {
198
+ await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath);
199
+ }
200
+
201
+ private async getOperation(operationId: string): Promise<IFormatOperation | null> {
202
+ const manifest = await this.getManifest();
203
+ return manifest.operations.find(op => op.id === operationId) || null;
204
+ }
205
+
206
+ private async updateManifest(operation: IFormatOperation): Promise<void> {
207
+ const manifest = await this.getManifest();
208
+ const existingIndex = manifest.operations.findIndex(op => op.id === operation.id);
209
+
210
+ if (existingIndex !== -1) {
211
+ manifest.operations[existingIndex] = operation;
212
+ } else {
213
+ manifest.operations.push(operation);
214
+ }
215
+
216
+ await this.saveManifest(manifest);
217
+ }
218
+ }