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.
- package/LICENSE +25 -0
- package/README.md +103 -0
- package/bin/revert-ai.js +589 -0
- package/index.js +4 -0
- package/package.json +74 -0
- package/src/core/AgentDetector.js +27 -0
- package/src/core/AgentParser.js +34 -0
- package/src/core/AiderSessionParser.js +143 -0
- package/src/core/ClaudeSessionParser.js +243 -0
- package/src/core/DependencyCascading.js +74 -0
- package/src/core/DependencyResolver.js +206 -0
- package/src/core/Operation.js +39 -0
- package/src/core/OperationPreview.js +176 -0
- package/src/core/RedoManager.js +270 -0
- package/src/core/SessionTracker.js +98 -0
- package/src/core/UndoManager.js +207 -0
- package/src/core/UndoTracker.js +72 -0
- package/src/hooks/claude-tracker.js +92 -0
- package/src/i18n/i18n.js +72 -0
- package/src/i18n/languages.js +471 -0
- package/src/utils/diffRenderer.js +112 -0
- package/src/utils/formatting.js +15 -0
- package/src/utils/gitHelper.js +66 -0
|
@@ -0,0 +1,207 @@
|
|
|
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 UndoManager {
|
|
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 undo(operation) {
|
|
16
|
+
switch (operation.type) {
|
|
17
|
+
case OperationType.FILE_CREATE:
|
|
18
|
+
return await this.undoFileCreate(operation);
|
|
19
|
+
case OperationType.FILE_EDIT:
|
|
20
|
+
return await this.undoFileEdit(operation);
|
|
21
|
+
case OperationType.FILE_DELETE:
|
|
22
|
+
return await this.undoFileDelete(operation);
|
|
23
|
+
case OperationType.FILE_RENAME:
|
|
24
|
+
return await this.undoFileRename(operation);
|
|
25
|
+
case OperationType.DIRECTORY_CREATE:
|
|
26
|
+
return await this.undoDirectoryCreate(operation);
|
|
27
|
+
case OperationType.DIRECTORY_DELETE:
|
|
28
|
+
return await this.undoDirectoryDelete(operation);
|
|
29
|
+
case OperationType.BASH_COMMAND:
|
|
30
|
+
return await this.undoBashCommand(operation);
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unknown operation type: ${operation.type}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async undoFileCreate(operation) {
|
|
37
|
+
const { filePath } = operation.data;
|
|
38
|
+
const backupPath = path.join(this.backupDir, `${operation.id}-deleted`);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
42
|
+
await fs.writeFile(backupPath, content);
|
|
43
|
+
await fs.unlink(filePath);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
message: `File deleted: ${filePath}`,
|
|
48
|
+
backupPath
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
message: `Failed to undo file creation: ${error.message}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async undoFileEdit(operation) {
|
|
59
|
+
const { filePath, originalContent, oldString, newString, replaceAll, edits, isMultiEdit } = operation.data;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const currentContent = await fs.readFile(filePath, 'utf8');
|
|
63
|
+
const backupPath = path.join(this.backupDir, `${operation.id}-current`);
|
|
64
|
+
await fs.writeFile(backupPath, currentContent);
|
|
65
|
+
|
|
66
|
+
let revertedContent = currentContent;
|
|
67
|
+
|
|
68
|
+
if (originalContent) {
|
|
69
|
+
// Legacy mode: we have the full original content (from local tracking)
|
|
70
|
+
revertedContent = originalContent;
|
|
71
|
+
} else if (isMultiEdit && edits) {
|
|
72
|
+
// Handle MultiEdit by reversing each edit in reverse order
|
|
73
|
+
for (let i = edits.length - 1; i >= 0; i--) {
|
|
74
|
+
const edit = edits[i];
|
|
75
|
+
if (edit.new_string && edit.old_string !== undefined) {
|
|
76
|
+
// Try to replace new_string back with old_string
|
|
77
|
+
if (revertedContent.includes(edit.new_string)) {
|
|
78
|
+
revertedContent = revertedContent.replace(edit.new_string, edit.old_string);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else if (oldString !== undefined && newString) {
|
|
83
|
+
// Handle single Edit operation - reverse the string replacement
|
|
84
|
+
if (replaceAll) {
|
|
85
|
+
// Replace all occurrences
|
|
86
|
+
revertedContent = revertedContent.split(newString).join(oldString);
|
|
87
|
+
} else {
|
|
88
|
+
// Replace first occurrence only
|
|
89
|
+
if (revertedContent.includes(newString)) {
|
|
90
|
+
revertedContent = revertedContent.replace(newString, oldString);
|
|
91
|
+
} else {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
message: `Cannot undo edit: expected string not found in ${filePath}`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
message: `Cannot undo file edit: insufficient data for ${filePath}`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await fs.writeFile(filePath, revertedContent);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
message: `File edit reverted: ${filePath}`,
|
|
110
|
+
backupPath
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
message: `Failed to undo file edit: ${error.message}`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async undoFileDelete(operation) {
|
|
121
|
+
const { filePath, content } = operation.data;
|
|
122
|
+
|
|
123
|
+
if (!content) {
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
message: `Cannot restore file: content not available for ${filePath}`
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await fs.writeFile(filePath, content);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
message: `File restored: ${filePath}`
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
message: `Failed to restore file: ${error.message}`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async undoFileRename(operation) {
|
|
146
|
+
const { oldPath, newPath } = operation.data;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await fs.rename(newPath, oldPath);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
success: true,
|
|
153
|
+
message: `File renamed back: ${newPath} → ${oldPath}`
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
message: `Failed to undo rename: ${error.message}`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async undoDirectoryCreate(operation) {
|
|
164
|
+
const { dirPath } = operation.data;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await fs.rmdir(dirPath);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
message: `Directory removed: ${dirPath}`
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
message: `Failed to remove directory: ${error.message}`
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async undoDirectoryDelete(operation) {
|
|
182
|
+
const { dirPath } = operation.data;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
success: true,
|
|
189
|
+
message: `Directory restored: ${dirPath}`
|
|
190
|
+
};
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
message: `Failed to restore directory: ${error.message}`
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async undoBashCommand(operation) {
|
|
200
|
+
const { command } = operation.data;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
message: `Cannot auto-undo bash command: ${command}\nPlease manually revert any changes.`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
export class UndoTracker {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.undoFile = path.join(os.homedir(), '.revert-ai', 'undone-operations.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async init() {
|
|
11
|
+
await fs.mkdir(path.dirname(this.undoFile), { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getUndoneOperations() {
|
|
15
|
+
try {
|
|
16
|
+
const data = await fs.readFile(this.undoFile, 'utf8');
|
|
17
|
+
return JSON.parse(data);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async markAsUndone(operationId, sessionFile) {
|
|
27
|
+
const undoneOps = await this.getUndoneOperations();
|
|
28
|
+
|
|
29
|
+
if (!undoneOps[sessionFile]) {
|
|
30
|
+
undoneOps[sessionFile] = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!undoneOps[sessionFile].includes(operationId)) {
|
|
34
|
+
undoneOps[sessionFile].push(operationId);
|
|
35
|
+
await fs.writeFile(this.undoFile, JSON.stringify(undoneOps, null, 2));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async markAsRedone(operationId, sessionFile) {
|
|
40
|
+
const undoneOps = await this.getUndoneOperations();
|
|
41
|
+
|
|
42
|
+
if (undoneOps[sessionFile]) {
|
|
43
|
+
const index = undoneOps[sessionFile].indexOf(operationId);
|
|
44
|
+
if (index > -1) {
|
|
45
|
+
undoneOps[sessionFile].splice(index, 1);
|
|
46
|
+
await fs.writeFile(this.undoFile, JSON.stringify(undoneOps, null, 2));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async isUndone(operationId, sessionFile) {
|
|
52
|
+
const undoneOps = await this.getUndoneOperations();
|
|
53
|
+
return undoneOps[sessionFile]?.includes(operationId) || false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async filterUndoneOperations(operations, sessionFile) {
|
|
57
|
+
const undoneOps = await this.getUndoneOperations();
|
|
58
|
+
const undoneIds = undoneOps[sessionFile] || [];
|
|
59
|
+
|
|
60
|
+
return operations.filter(op => !undoneIds.includes(op.id));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getUndoneOperationsList(operations, sessionFile) {
|
|
64
|
+
const undoneOps = await this.getUndoneOperations();
|
|
65
|
+
const undoneIds = undoneOps[sessionFile] || [];
|
|
66
|
+
|
|
67
|
+
// Return operations that have been undone, in reverse order (most recent first)
|
|
68
|
+
return operations
|
|
69
|
+
.filter(op => undoneIds.includes(op.id))
|
|
70
|
+
.reverse();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { SessionTracker } from '../core/SessionTracker.js';
|
|
5
|
+
import { Operation, OperationType } from '../core/Operation.js';
|
|
6
|
+
|
|
7
|
+
async function trackOperation() {
|
|
8
|
+
try {
|
|
9
|
+
const input = JSON.parse(process.argv[2] || '{}');
|
|
10
|
+
|
|
11
|
+
let sessionId = await SessionTracker.getCurrentSession();
|
|
12
|
+
if (!sessionId) {
|
|
13
|
+
sessionId = new Date().toISOString().replace(/[:.]/g, '-');
|
|
14
|
+
await SessionTracker.setCurrentSession(sessionId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const tracker = new SessionTracker(sessionId);
|
|
18
|
+
await tracker.init();
|
|
19
|
+
|
|
20
|
+
let operation = null;
|
|
21
|
+
|
|
22
|
+
if (input.tool === 'Write' && input.parameters?.file_path) {
|
|
23
|
+
operation = new Operation(OperationType.FILE_CREATE, {
|
|
24
|
+
filePath: input.parameters.file_path,
|
|
25
|
+
content: input.parameters.content || ''
|
|
26
|
+
});
|
|
27
|
+
} else if (input.tool === 'Edit' && input.parameters?.file_path) {
|
|
28
|
+
let originalContent = '';
|
|
29
|
+
try {
|
|
30
|
+
originalContent = await fs.readFile(input.parameters.file_path, 'utf8');
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
|
|
33
|
+
operation = new Operation(OperationType.FILE_EDIT, {
|
|
34
|
+
filePath: input.parameters.file_path,
|
|
35
|
+
originalContent,
|
|
36
|
+
newContent: input.parameters.new_string || ''
|
|
37
|
+
});
|
|
38
|
+
} else if (input.tool === 'Bash' && input.parameters?.command) {
|
|
39
|
+
const command = input.parameters.command;
|
|
40
|
+
|
|
41
|
+
if (command.includes('rm ') && command.includes('-f')) {
|
|
42
|
+
const match = command.match(/rm\s+.*?\s+([^\s]+)$/);
|
|
43
|
+
if (match) {
|
|
44
|
+
const filePath = match[1];
|
|
45
|
+
let content = '';
|
|
46
|
+
try {
|
|
47
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
48
|
+
} catch (e) {}
|
|
49
|
+
|
|
50
|
+
operation = new Operation(OperationType.FILE_DELETE, {
|
|
51
|
+
filePath,
|
|
52
|
+
content
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
} else if (command.includes('mv ')) {
|
|
56
|
+
const match = command.match(/mv\s+([^\s]+)\s+([^\s]+)$/);
|
|
57
|
+
if (match) {
|
|
58
|
+
operation = new Operation(OperationType.FILE_RENAME, {
|
|
59
|
+
oldPath: match[1],
|
|
60
|
+
newPath: match[2]
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
} else if (command.includes('mkdir')) {
|
|
64
|
+
const match = command.match(/mkdir\s+.*?\s+([^\s]+)$/);
|
|
65
|
+
if (match) {
|
|
66
|
+
operation = new Operation(OperationType.DIRECTORY_CREATE, {
|
|
67
|
+
dirPath: match[1]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} else if (command.includes('rmdir') || (command.includes('rm') && command.includes('-r'))) {
|
|
71
|
+
const match = command.match(/rm(?:dir)?\s+.*?\s+([^\s]+)$/);
|
|
72
|
+
if (match) {
|
|
73
|
+
operation = new Operation(OperationType.DIRECTORY_DELETE, {
|
|
74
|
+
dirPath: match[1]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
operation = new Operation(OperationType.BASH_COMMAND, {
|
|
79
|
+
command
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (operation) {
|
|
85
|
+
await tracker.addOperation(operation);
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Failed to track operation:', error.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
trackOperation();
|
package/src/i18n/i18n.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { languages } from './languages.js';
|
|
5
|
+
|
|
6
|
+
class I18n {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.currentLanguage = 'en';
|
|
9
|
+
this.configFile = path.join(os.homedir(), '.revert-ai', 'config.json');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async init() {
|
|
13
|
+
await this.loadConfig();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async loadConfig() {
|
|
17
|
+
try {
|
|
18
|
+
const config = JSON.parse(await fs.readFile(this.configFile, 'utf8'));
|
|
19
|
+
this.currentLanguage = config.language || 'en';
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// Use default language if config doesn't exist
|
|
22
|
+
this.currentLanguage = 'en';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async saveConfig() {
|
|
27
|
+
try {
|
|
28
|
+
await fs.mkdir(path.dirname(this.configFile), { recursive: true });
|
|
29
|
+
const config = { language: this.currentLanguage };
|
|
30
|
+
await fs.writeFile(this.configFile, JSON.stringify(config, null, 2));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Failed to save language config:', error.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async setLanguage(lang) {
|
|
37
|
+
if (!languages[lang]) {
|
|
38
|
+
throw new Error(`Unsupported language: ${lang}`);
|
|
39
|
+
}
|
|
40
|
+
this.currentLanguage = lang;
|
|
41
|
+
await this.saveConfig();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
t(key, params = {}) {
|
|
45
|
+
const lang = languages[this.currentLanguage] || languages['en'];
|
|
46
|
+
let message = lang.messages[key] || key;
|
|
47
|
+
|
|
48
|
+
// Replace parameters in the message
|
|
49
|
+
Object.keys(params).forEach(param => {
|
|
50
|
+
message = message.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return message;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getAvailableLanguages() {
|
|
57
|
+
return Object.keys(languages).map(code => ({
|
|
58
|
+
code,
|
|
59
|
+
name: languages[code].name
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getCurrentLanguage() {
|
|
64
|
+
return {
|
|
65
|
+
code: this.currentLanguage,
|
|
66
|
+
name: languages[this.currentLanguage]?.name || 'Unknown'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create a singleton instance
|
|
72
|
+
export const i18n = new I18n();
|