revert-ai 1.1.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.
@@ -0,0 +1,39 @@
1
+ import crypto from 'crypto';
2
+
3
+ export class Operation {
4
+ constructor(type, data) {
5
+ this.id = (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.floor(Math.random()*1e9)}`);
6
+ this.timestamp = new Date();
7
+ this.type = type;
8
+ this.data = data;
9
+ this.undone = false;
10
+ }
11
+
12
+ static fromJSON(json) {
13
+ const op = new Operation(json.type, json.data);
14
+ op.id = json.id;
15
+ op.timestamp = new Date(json.timestamp);
16
+ op.undone = json.undone || false;
17
+ return op;
18
+ }
19
+
20
+ toJSON() {
21
+ return {
22
+ id: this.id,
23
+ timestamp: this.timestamp.toISOString(),
24
+ type: this.type,
25
+ data: this.data,
26
+ undone: this.undone
27
+ };
28
+ }
29
+ }
30
+
31
+ export const OperationType = {
32
+ FILE_CREATE: 'file_create',
33
+ FILE_EDIT: 'file_edit',
34
+ FILE_DELETE: 'file_delete',
35
+ FILE_RENAME: 'file_rename',
36
+ DIRECTORY_CREATE: 'directory_create',
37
+ DIRECTORY_DELETE: 'directory_delete',
38
+ BASH_COMMAND: 'bash_command'
39
+ };
@@ -0,0 +1,176 @@
1
+ import fs from 'fs/promises';
2
+ import chalk from 'chalk';
3
+ import { OperationType } from './Operation.js';
4
+ import { i18n } from '../i18n/i18n.js';
5
+ import { renderSideBySide } from '../utils/diffRenderer.js';
6
+
7
+ export class OperationPreview {
8
+ static async generatePreview(operation) {
9
+ switch (operation.type) {
10
+ case OperationType.FILE_CREATE:
11
+ return await this.previewFileCreate(operation);
12
+ case OperationType.FILE_EDIT:
13
+ return await this.previewFileEdit(operation);
14
+ case OperationType.FILE_DELETE:
15
+ return await this.previewFileDelete(operation);
16
+ case OperationType.FILE_RENAME:
17
+ return await this.previewFileRename(operation);
18
+ case OperationType.DIRECTORY_CREATE:
19
+ return await this.previewDirectoryCreate(operation);
20
+ case OperationType.DIRECTORY_DELETE:
21
+ return await this.previewDirectoryDelete(operation);
22
+ case OperationType.BASH_COMMAND:
23
+ return await this.previewBashCommand(operation);
24
+ default:
25
+ return { preview: `Unknown operation: ${operation.type}`, hasContent: false };
26
+ }
27
+ }
28
+
29
+ static async previewFileCreate(operation) {
30
+ const { filePath } = operation.data;
31
+
32
+ try {
33
+ const exists = await fs.access(filePath).then(() => true).catch(() => false);
34
+ if (!exists) {
35
+ return {
36
+ preview: `${chalk.red('File does not exist:')} ${filePath}`,
37
+ hasContent: false
38
+ };
39
+ }
40
+
41
+ const content = await fs.readFile(filePath, 'utf8');
42
+ const lines = content.split('\n');
43
+ const preview = lines.slice(0, 5).join('\n');
44
+ const truncated = lines.length > 5;
45
+
46
+ return {
47
+ preview: `${chalk.red(i18n.t('action.will_delete_file'))} ${filePath}\n${chalk.gray(i18n.t('status.current_content'))}\n${preview}${truncated ? '\n...' : ''}`,
48
+ hasContent: true,
49
+ action: 'delete'
50
+ };
51
+ } catch (error) {
52
+ return {
53
+ preview: `${chalk.red('Error reading file:')} ${filePath} - ${error.message}`,
54
+ hasContent: false
55
+ };
56
+ }
57
+ }
58
+
59
+ static async previewFileEdit(operation) {
60
+ const { filePath, originalContent, oldString, newString, replaceAll, edits, isMultiEdit } = operation.data;
61
+
62
+ try {
63
+ const currentContent = await fs.readFile(filePath, 'utf8');
64
+ let preview = `${chalk.yellow(i18n.t('action.will_revert_file'))} ${filePath}\n\n`;
65
+
66
+ let revertedContent = currentContent;
67
+
68
+ if (originalContent) {
69
+ revertedContent = originalContent;
70
+ } else if (isMultiEdit && edits) {
71
+ for (let i = edits.length - 1; i >= 0; i--) {
72
+ const edit = edits[i];
73
+ if (edit.new_string && edit.old_string !== undefined) {
74
+ if (revertedContent.includes(edit.new_string)) {
75
+ revertedContent = revertedContent.replace(edit.new_string, edit.old_string);
76
+ }
77
+ }
78
+ }
79
+ } else if (oldString !== undefined && newString) {
80
+ if (replaceAll) {
81
+ revertedContent = revertedContent.split(newString).join(oldString);
82
+ } else {
83
+ if (revertedContent.includes(newString)) {
84
+ revertedContent = revertedContent.replace(newString, oldString);
85
+ }
86
+ }
87
+ }
88
+
89
+ preview += renderSideBySide(currentContent, revertedContent, { maxLines: 15 });
90
+
91
+ return {
92
+ preview,
93
+ hasContent: true,
94
+ action: 'revert'
95
+ };
96
+ } catch (error) {
97
+ return {
98
+ preview: `${chalk.yellow(i18n.t('action.will_revert_file'))} ${filePath}\n${chalk.red('Error:')} ${error.message}`,
99
+ hasContent: false,
100
+ action: 'revert'
101
+ };
102
+ }
103
+ }
104
+
105
+ static async previewFileDelete(operation) {
106
+ const { filePath, content } = operation.data;
107
+
108
+ if (!content) {
109
+ return {
110
+ preview: `${chalk.green(i18n.t('action.will_restore_file'))} ${filePath}\n${chalk.gray(i18n.t('status.content_not_available'))}`,
111
+ hasContent: false
112
+ };
113
+ }
114
+
115
+ const lines = content.split('\n');
116
+ const preview = lines.slice(0, 5).join('\n');
117
+ const truncated = lines.length > 5;
118
+
119
+ return {
120
+ preview: `${chalk.green(i18n.t('action.will_restore_file'))} ${filePath}\n${chalk.gray(i18n.t('status.content_to_restore'))}\n${preview}${truncated ? '\n...' : ''}`,
121
+ hasContent: true,
122
+ action: 'restore'
123
+ };
124
+ }
125
+
126
+ static async previewFileRename(operation) {
127
+ const { oldPath, newPath } = operation.data;
128
+
129
+ return {
130
+ preview: `${chalk.yellow(i18n.t('action.will_rename_back'))} ${newPath} → ${oldPath}`,
131
+ hasContent: false,
132
+ action: 'rename'
133
+ };
134
+ }
135
+
136
+ static async previewDirectoryCreate(operation) {
137
+ const { dirPath } = operation.data;
138
+
139
+ try {
140
+ const exists = await fs.access(dirPath).then(() => true).catch(() => false);
141
+ const status = exists ? chalk.red('Will remove directory:') : chalk.gray('Directory already removed:');
142
+
143
+ return {
144
+ preview: `${status} ${dirPath}`,
145
+ hasContent: false,
146
+ action: exists ? 'remove' : 'none'
147
+ };
148
+ } catch (error) {
149
+ return {
150
+ preview: `${chalk.yellow('Will remove directory:')} ${dirPath}`,
151
+ hasContent: false,
152
+ action: 'remove'
153
+ };
154
+ }
155
+ }
156
+
157
+ static async previewDirectoryDelete(operation) {
158
+ const { dirPath } = operation.data;
159
+
160
+ return {
161
+ preview: `${chalk.green('Will restore directory:')} ${dirPath}`,
162
+ hasContent: false,
163
+ action: 'restore'
164
+ };
165
+ }
166
+
167
+ static async previewBashCommand(operation) {
168
+ const { command } = operation.data;
169
+
170
+ return {
171
+ preview: `${chalk.red(i18n.t('action.cannot_undo_bash'))} ${command}\n${chalk.gray(i18n.t('action.manual_intervention'))}`,
172
+ hasContent: false,
173
+ action: 'manual'
174
+ };
175
+ }
176
+ }
@@ -0,0 +1,270 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { OperationType } from './Operation.js';
5
+
6
+ export class RedoManager {
7
+ constructor() {
8
+ this.backupDir = path.join(os.homedir(), '.revert-ai', 'backups');
9
+ }
10
+
11
+ async init() {
12
+ await fs.mkdir(this.backupDir, { recursive: true });
13
+ }
14
+
15
+ async redo(operation) {
16
+ switch (operation.type) {
17
+ case OperationType.FILE_CREATE:
18
+ return await this.redoFileCreate(operation);
19
+ case OperationType.FILE_EDIT:
20
+ return await this.redoFileEdit(operation);
21
+ case OperationType.FILE_DELETE:
22
+ return await this.redoFileDelete(operation);
23
+ case OperationType.FILE_RENAME:
24
+ return await this.redoFileRename(operation);
25
+ case OperationType.DIRECTORY_CREATE:
26
+ return await this.redoDirectoryCreate(operation);
27
+ case OperationType.DIRECTORY_DELETE:
28
+ return await this.redoDirectoryDelete(operation);
29
+ case OperationType.BASH_COMMAND:
30
+ return await this.redoBashCommand(operation);
31
+ default:
32
+ throw new Error(`Unknown operation type: ${operation.type}`);
33
+ }
34
+ }
35
+
36
+ async redoFileCreate(operation) {
37
+ const { filePath, content } = operation.data;
38
+
39
+ try {
40
+ // Check if file already exists
41
+ const exists = await fs.access(filePath).then(() => true).catch(() => false);
42
+ if (exists) {
43
+ return {
44
+ success: false,
45
+ message: `Cannot redo file creation: ${filePath} already exists`
46
+ };
47
+ }
48
+
49
+ // Try to restore from backup first
50
+ const backupPath = path.join(this.backupDir, `${operation.id}-deleted`);
51
+ let fileContent = content || '';
52
+
53
+ try {
54
+ fileContent = await fs.readFile(backupPath, 'utf8');
55
+ } catch (e) {
56
+ // If no backup, use original content if available
57
+ if (!content) {
58
+ return {
59
+ success: false,
60
+ message: `Cannot redo file creation: no content available for ${filePath}`
61
+ };
62
+ }
63
+ }
64
+
65
+ await fs.writeFile(filePath, fileContent);
66
+
67
+ return {
68
+ success: true,
69
+ message: `File recreated: ${filePath}`
70
+ };
71
+ } catch (error) {
72
+ return {
73
+ success: false,
74
+ message: `Failed to redo file creation: ${error.message}`
75
+ };
76
+ }
77
+ }
78
+
79
+ async redoFileEdit(operation) {
80
+ const { filePath, originalContent, oldString, newString, replaceAll, edits, isMultiEdit } = operation.data;
81
+
82
+ try {
83
+ const currentContent = await fs.readFile(filePath, 'utf8');
84
+ const backupPath = path.join(this.backupDir, `${operation.id}-redo`);
85
+ await fs.writeFile(backupPath, currentContent);
86
+
87
+ let redoneContent = currentContent;
88
+
89
+ if (originalContent) {
90
+ // This was a legacy full-content edit, but we can't safely redo it
91
+ // because we don't know what the "new" content should be
92
+ return {
93
+ success: false,
94
+ message: `Cannot redo legacy file edit: insufficient data for ${filePath}`
95
+ };
96
+ } else if (isMultiEdit && edits) {
97
+ // Redo MultiEdit by applying each edit in original order
98
+ for (const edit of edits) {
99
+ if (edit.old_string !== undefined && edit.new_string) {
100
+ if (redoneContent.includes(edit.old_string)) {
101
+ redoneContent = redoneContent.replace(edit.old_string, edit.new_string);
102
+ }
103
+ }
104
+ }
105
+ } else if (oldString !== undefined && newString) {
106
+ // Redo single Edit operation - apply the original string replacement
107
+ if (replaceAll) {
108
+ // Replace all occurrences
109
+ redoneContent = redoneContent.split(oldString).join(newString);
110
+ } else {
111
+ // Replace first occurrence only
112
+ if (redoneContent.includes(oldString)) {
113
+ redoneContent = redoneContent.replace(oldString, newString);
114
+ } else {
115
+ return {
116
+ success: false,
117
+ message: `Cannot redo edit: original string not found in ${filePath}`
118
+ };
119
+ }
120
+ }
121
+ } else {
122
+ return {
123
+ success: false,
124
+ message: `Cannot redo file edit: insufficient data for ${filePath}`
125
+ };
126
+ }
127
+
128
+ await fs.writeFile(filePath, redoneContent);
129
+
130
+ return {
131
+ success: true,
132
+ message: `File edit redone: ${filePath}`,
133
+ backupPath
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ success: false,
138
+ message: `Failed to redo file edit: ${error.message}`
139
+ };
140
+ }
141
+ }
142
+
143
+ async redoFileDelete(operation) {
144
+ const { filePath } = operation.data;
145
+
146
+ try {
147
+ const exists = await fs.access(filePath).then(() => true).catch(() => false);
148
+ if (!exists) {
149
+ return {
150
+ success: false,
151
+ message: `Cannot redo file deletion: ${filePath} does not exist`
152
+ };
153
+ }
154
+
155
+ // Backup the file before deleting
156
+ const backupPath = path.join(this.backupDir, `${operation.id}-redo-deleted`);
157
+ const content = await fs.readFile(filePath, 'utf8');
158
+ await fs.writeFile(backupPath, content);
159
+
160
+ await fs.unlink(filePath);
161
+
162
+ return {
163
+ success: true,
164
+ message: `File deleted again: ${filePath}`,
165
+ backupPath
166
+ };
167
+ } catch (error) {
168
+ return {
169
+ success: false,
170
+ message: `Failed to redo file deletion: ${error.message}`
171
+ };
172
+ }
173
+ }
174
+
175
+ async redoFileRename(operation) {
176
+ const { oldPath, newPath } = operation.data;
177
+
178
+ try {
179
+ const oldExists = await fs.access(oldPath).then(() => true).catch(() => false);
180
+ const newExists = await fs.access(newPath).then(() => true).catch(() => false);
181
+
182
+ if (!oldExists) {
183
+ return {
184
+ success: false,
185
+ message: `Cannot redo rename: ${oldPath} does not exist`
186
+ };
187
+ }
188
+
189
+ if (newExists) {
190
+ return {
191
+ success: false,
192
+ message: `Cannot redo rename: ${newPath} already exists`
193
+ };
194
+ }
195
+
196
+ await fs.rename(oldPath, newPath);
197
+
198
+ return {
199
+ success: true,
200
+ message: `File renamed again: ${oldPath} → ${newPath}`
201
+ };
202
+ } catch (error) {
203
+ return {
204
+ success: false,
205
+ message: `Failed to redo rename: ${error.message}`
206
+ };
207
+ }
208
+ }
209
+
210
+ async redoDirectoryCreate(operation) {
211
+ const { dirPath } = operation.data;
212
+
213
+ try {
214
+ const exists = await fs.access(dirPath).then(() => true).catch(() => false);
215
+ if (exists) {
216
+ return {
217
+ success: false,
218
+ message: `Cannot redo directory creation: ${dirPath} already exists`
219
+ };
220
+ }
221
+
222
+ await fs.mkdir(dirPath, { recursive: true });
223
+
224
+ return {
225
+ success: true,
226
+ message: `Directory created again: ${dirPath}`
227
+ };
228
+ } catch (error) {
229
+ return {
230
+ success: false,
231
+ message: `Failed to redo directory creation: ${error.message}`
232
+ };
233
+ }
234
+ }
235
+
236
+ async redoDirectoryDelete(operation) {
237
+ const { dirPath } = operation.data;
238
+
239
+ try {
240
+ const exists = await fs.access(dirPath).then(() => true).catch(() => false);
241
+ if (!exists) {
242
+ return {
243
+ success: false,
244
+ message: `Cannot redo directory deletion: ${dirPath} does not exist`
245
+ };
246
+ }
247
+
248
+ await fs.rmdir(dirPath);
249
+
250
+ return {
251
+ success: true,
252
+ message: `Directory deleted again: ${dirPath}`
253
+ };
254
+ } catch (error) {
255
+ return {
256
+ success: false,
257
+ message: `Failed to redo directory deletion: ${error.message}`
258
+ };
259
+ }
260
+ }
261
+
262
+ async redoBashCommand(operation) {
263
+ const { command } = operation.data;
264
+
265
+ return {
266
+ success: false,
267
+ message: `Cannot redo bash command: ${command}\nPlease manually re-run the command.`
268
+ };
269
+ }
270
+ }
@@ -0,0 +1,98 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { Operation, OperationType } from './Operation.js';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ export class SessionTracker {
12
+ constructor(sessionId = null) {
13
+ this.sessionId = sessionId || new Date().toISOString().replace(/[:.]/g, '-');
14
+ this.operations = [];
15
+ this.sessionDir = path.join(os.homedir(), '.revert-ai', 'sessions');
16
+ this.sessionFile = path.join(this.sessionDir, `${this.sessionId}.json`);
17
+ }
18
+
19
+ async init() {
20
+ await fs.mkdir(this.sessionDir, { recursive: true });
21
+ await this.load();
22
+ }
23
+
24
+ async load() {
25
+ try {
26
+ const data = await fs.readFile(this.sessionFile, 'utf8');
27
+ const session = JSON.parse(data);
28
+ this.operations = session.operations.map(op => Operation.fromJSON(op));
29
+ } catch (error) {
30
+ if (error.code !== 'ENOENT') {
31
+ throw error;
32
+ }
33
+ }
34
+ }
35
+
36
+ async save() {
37
+ const sessionData = {
38
+ sessionId: this.sessionId,
39
+ operations: this.operations.map(op => op.toJSON())
40
+ };
41
+ await fs.writeFile(this.sessionFile, JSON.stringify(sessionData, null, 2));
42
+ }
43
+
44
+ async addOperation(operation) {
45
+ this.operations.push(operation);
46
+ await this.save();
47
+ }
48
+
49
+ async getOperations(includeUndone = false) {
50
+ return includeUndone
51
+ ? this.operations
52
+ : this.operations.filter(op => !op.undone);
53
+ }
54
+
55
+ async getOperation(id) {
56
+ return this.operations.find(op => op.id === id);
57
+ }
58
+
59
+ async markUndone(operationId) {
60
+ const operation = await this.getOperation(operationId);
61
+ if (operation) {
62
+ operation.undone = true;
63
+ await this.save();
64
+ }
65
+ return operation;
66
+ }
67
+
68
+ static async listSessions() {
69
+ const sessionDir = path.join(os.homedir(), '.revert-ai', 'sessions');
70
+ try {
71
+ const files = await fs.readdir(sessionDir);
72
+ return files
73
+ .filter(f => f.endsWith('.json'))
74
+ .map(f => f.replace('.json', ''));
75
+ } catch (error) {
76
+ if (error.code === 'ENOENT') {
77
+ return [];
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ static async getCurrentSession() {
84
+ const currentFile = path.join(os.homedir(), '.revert-ai', 'current-session');
85
+ try {
86
+ const sessionId = await fs.readFile(currentFile, 'utf8');
87
+ return sessionId.trim();
88
+ } catch (error) {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ static async setCurrentSession(sessionId) {
94
+ const currentFile = path.join(os.homedir(), '.revert-ai', 'current-session');
95
+ await fs.mkdir(path.dirname(currentFile), { recursive: true });
96
+ await fs.writeFile(currentFile, sessionId);
97
+ }
98
+ }