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
package/bin/revert-ai.js
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { SessionTracker } from '../src/core/SessionTracker.js';
|
|
7
|
+
import { UndoManager } from '../src/core/UndoManager.js';
|
|
8
|
+
import { formatDistance } from '../src/utils/formatting.js';
|
|
9
|
+
import { Operation, OperationType } from '../src/core/Operation.js';
|
|
10
|
+
import { AgentDetector } from '../src/core/AgentDetector.js';
|
|
11
|
+
import { OperationPreview } from '../src/core/OperationPreview.js';
|
|
12
|
+
import { i18n } from '../src/i18n/i18n.js';
|
|
13
|
+
import { UndoTracker } from '../src/core/UndoTracker.js';
|
|
14
|
+
import { RedoManager } from '../src/core/RedoManager.js';
|
|
15
|
+
import { GitHelper } from '../src/utils/gitHelper.js';
|
|
16
|
+
import { getDependentOperations } from '../src/core/DependencyCascading.js';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
|
|
19
|
+
// Initialize i18n
|
|
20
|
+
await i18n.init();
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('revert-ai')
|
|
26
|
+
.description('Undo individual steps performed by Claude Code within a session')
|
|
27
|
+
.version('1.1.1');
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('list')
|
|
31
|
+
.description(i18n.t('cmd.list.description'))
|
|
32
|
+
.option('-a, --all', i18n.t('opt.all'))
|
|
33
|
+
.option('-s, --session <id>', i18n.t('opt.session'))
|
|
34
|
+
.option('--claude', i18n.t('opt.claude'), true)
|
|
35
|
+
.option('--local', i18n.t('opt.local'))
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
try {
|
|
38
|
+
let operations = [];
|
|
39
|
+
|
|
40
|
+
if (!options.local) {
|
|
41
|
+
const detection = await AgentDetector.detect();
|
|
42
|
+
if (!detection) {
|
|
43
|
+
console.log(chalk.yellow('No active AI agent session (Claude Code or Aider) found in this directory.'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { agent, parser } = detection;
|
|
48
|
+
const sessionFile = await parser.getCurrentSessionFile();
|
|
49
|
+
operations = await parser.parseSessionFile(sessionFile, options.all);
|
|
50
|
+
console.log(chalk.bold(`\nOperations from ${agent === 'claude' ? 'Claude Code' : 'Aider'} session:\n`));
|
|
51
|
+
} else {
|
|
52
|
+
// Use local revert-ai tracking
|
|
53
|
+
const sessionId = options.session || await SessionTracker.getCurrentSession();
|
|
54
|
+
if (!sessionId) {
|
|
55
|
+
console.log(chalk.yellow('No local revert-ai session found.'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tracker = new SessionTracker(sessionId);
|
|
60
|
+
await tracker.init();
|
|
61
|
+
operations = await tracker.getOperations(options.all);
|
|
62
|
+
console.log(chalk.bold(`\nOperations in local session ${sessionId}:\n`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (operations.length === 0) {
|
|
66
|
+
console.log(chalk.yellow('No operations found.'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
operations.forEach((op, index) => {
|
|
71
|
+
const status = op.undone ? chalk.red('[UNDONE]') : chalk.green('[ACTIVE]');
|
|
72
|
+
const time = formatDistance(op.timestamp);
|
|
73
|
+
|
|
74
|
+
console.log(`${index + 1}. ${status} ${chalk.cyan(op.type)} - ${time}`);
|
|
75
|
+
console.log(` ID: ${op.id}`);
|
|
76
|
+
|
|
77
|
+
switch (op.type) {
|
|
78
|
+
case 'file_create':
|
|
79
|
+
case 'file_edit':
|
|
80
|
+
case 'file_delete':
|
|
81
|
+
console.log(` File: ${op.data.filePath}`);
|
|
82
|
+
break;
|
|
83
|
+
case 'file_rename':
|
|
84
|
+
console.log(` From: ${op.data.oldPath}`);
|
|
85
|
+
console.log(` To: ${op.data.newPath}`);
|
|
86
|
+
break;
|
|
87
|
+
case 'directory_create':
|
|
88
|
+
case 'directory_delete':
|
|
89
|
+
console.log(` Directory: ${op.data.dirPath}`);
|
|
90
|
+
break;
|
|
91
|
+
case 'bash_command':
|
|
92
|
+
console.log(` Command: ${op.data.command}`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
console.log('');
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command('undo [operation-id]')
|
|
104
|
+
.description('Undo operations from the current Claude Code session')
|
|
105
|
+
.option('-s, --session <id>', 'Specify session ID')
|
|
106
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
107
|
+
.option('--local', 'Use local revert-ai tracking instead of Claude sessions')
|
|
108
|
+
.action(async (operationId, options) => {
|
|
109
|
+
try {
|
|
110
|
+
let operations = [];
|
|
111
|
+
let sessionFile = null;
|
|
112
|
+
|
|
113
|
+
if (options.local) {
|
|
114
|
+
// Use local revert-ai tracking
|
|
115
|
+
const sessionId = options.session || await SessionTracker.getCurrentSession();
|
|
116
|
+
if (!sessionId) {
|
|
117
|
+
console.log(chalk.yellow('No local revert-ai session found.'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tracker = new SessionTracker(sessionId);
|
|
122
|
+
await tracker.init();
|
|
123
|
+
operations = await tracker.getOperations();
|
|
124
|
+
} else {
|
|
125
|
+
const detection = await AgentDetector.detect();
|
|
126
|
+
if (!detection) {
|
|
127
|
+
console.log(chalk.yellow('No active AI agent session found in this directory.'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const { parser } = detection;
|
|
131
|
+
sessionFile = await parser.getCurrentSessionFile();
|
|
132
|
+
operations = await parser.parseSessionFile(sessionFile);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (operations.length === 0) {
|
|
136
|
+
console.log(chalk.yellow('No operations to undo.'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Reverse operations so most recent is first
|
|
141
|
+
operations.reverse();
|
|
142
|
+
|
|
143
|
+
let selectedIndex = 0;
|
|
144
|
+
|
|
145
|
+
if (!operationId) {
|
|
146
|
+
const choices = operations.map((op, index) => {
|
|
147
|
+
const dependentOps = getDependentOperations(index, operations, process.cwd());
|
|
148
|
+
let name = `${op.type} - ${formatDistance(op.timestamp)}`;
|
|
149
|
+
|
|
150
|
+
if (dependentOps.length > 1) {
|
|
151
|
+
name += chalk.red(` (+ ${dependentOps.length - 1} dependent ops will be undone)`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
name: name,
|
|
156
|
+
value: index,
|
|
157
|
+
short: `${op.type} (${dependentOps.length} ops)`
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log(chalk.yellow('\n⚠️ AST Cascading undo: Selecting an operation will undo it and only other subsequent operations that depend on it.\n'));
|
|
162
|
+
|
|
163
|
+
const answer = await inquirer.prompt([{
|
|
164
|
+
type: 'list',
|
|
165
|
+
name: 'selectedIndex',
|
|
166
|
+
message: 'Select operation to undo:',
|
|
167
|
+
choices: choices,
|
|
168
|
+
pageSize: 15
|
|
169
|
+
}]);
|
|
170
|
+
|
|
171
|
+
selectedIndex = answer.selectedIndex;
|
|
172
|
+
} else {
|
|
173
|
+
selectedIndex = operations.findIndex(op => op.id === operationId);
|
|
174
|
+
if (selectedIndex === -1) {
|
|
175
|
+
console.log(chalk.red(`Operation ${operationId} not found.`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const operationsToUndo = getDependentOperations(selectedIndex, operations, process.cwd());
|
|
181
|
+
|
|
182
|
+
if (!options.yes) {
|
|
183
|
+
console.log(chalk.yellow(`\\nThis will undo ${operationsToUndo.length} operation(s):\\n`));
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < operationsToUndo.length; i++) {
|
|
186
|
+
const op = operationsToUndo[i];
|
|
187
|
+
console.log(`${chalk.bold(`${i + 1}.`)} ${chalk.cyan(op.type)} - ${formatDistance(op.timestamp)}`);
|
|
188
|
+
|
|
189
|
+
const preview = await OperationPreview.generatePreview(op);
|
|
190
|
+
console.log(` ${preview.preview.replace(/\\n/g, '\\n ')}`);
|
|
191
|
+
console.log('');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const confirm = await inquirer.prompt([{
|
|
195
|
+
type: 'confirm',
|
|
196
|
+
name: 'proceed',
|
|
197
|
+
message: `Are you sure you want to undo these ${operationsToUndo.length} operations?`,
|
|
198
|
+
default: false
|
|
199
|
+
}]);
|
|
200
|
+
|
|
201
|
+
if (!confirm.proceed) {
|
|
202
|
+
console.log(chalk.yellow('Undo cancelled.'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (GitHelper.isWorkspaceDirty()) {
|
|
206
|
+
const gitAnswer = await inquirer.prompt([{
|
|
207
|
+
type: 'confirm',
|
|
208
|
+
name: 'stash',
|
|
209
|
+
message: '⚠️ You have uncommitted changes in your repository. Would you like to stash them before proceeding?',
|
|
210
|
+
default: true
|
|
211
|
+
}]);
|
|
212
|
+
|
|
213
|
+
if (gitAnswer.stash) {
|
|
214
|
+
console.log(chalk.cyan('\nStashing uncommitted changes...'));
|
|
215
|
+
GitHelper.saveStash(`revert-ai: Pre-undo stash for ${operationsToUndo.length} operations`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const undoManager = new UndoManager();
|
|
220
|
+
await undoManager.init();
|
|
221
|
+
|
|
222
|
+
const undoTracker = new UndoTracker();
|
|
223
|
+
await undoTracker.init();
|
|
224
|
+
|
|
225
|
+
console.log(chalk.cyan(`\\nUndoing ${operationsToUndo.length} operations...\\n`));
|
|
226
|
+
|
|
227
|
+
let successCount = 0;
|
|
228
|
+
let failCount = 0;
|
|
229
|
+
|
|
230
|
+
for (const operation of operationsToUndo) {
|
|
231
|
+
const result = await undoManager.undo(operation);
|
|
232
|
+
|
|
233
|
+
if (result.success) {
|
|
234
|
+
successCount++;
|
|
235
|
+
console.log(chalk.green(`✓ ${result.message}`));
|
|
236
|
+
if (result.backupPath) {
|
|
237
|
+
console.log(chalk.gray(` Backup saved to: ${result.backupPath}`));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Mark operation as undone if using Claude Code sessions
|
|
241
|
+
if (sessionFile) {
|
|
242
|
+
await undoTracker.markAsUndone(operation.id, sessionFile);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
failCount++;
|
|
246
|
+
console.log(chalk.red(`✗ ${result.message}`));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(chalk.bold(`\\nCompleted: ${chalk.green(successCount)} successful, ${chalk.red(failCount)} failed`));
|
|
251
|
+
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
program
|
|
258
|
+
.command('redo [operation-id]')
|
|
259
|
+
.description(i18n.t('cmd.redo.description'))
|
|
260
|
+
.option('-s, --session <id>', i18n.t('opt.session'))
|
|
261
|
+
.option('-y, --yes', i18n.t('opt.yes'))
|
|
262
|
+
.option('--local', 'Use local revert-ai tracking instead of Claude sessions')
|
|
263
|
+
.action(async (operationId, options) => {
|
|
264
|
+
try {
|
|
265
|
+
let operations = [];
|
|
266
|
+
let sessionFile = null;
|
|
267
|
+
|
|
268
|
+
if (options.local) {
|
|
269
|
+
// Use local revert-ai tracking
|
|
270
|
+
const sessionId = options.session || await SessionTracker.getCurrentSession();
|
|
271
|
+
if (!sessionId) {
|
|
272
|
+
console.log(chalk.yellow('No local revert-ai session found.'));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const tracker = new SessionTracker(sessionId);
|
|
277
|
+
await tracker.init();
|
|
278
|
+
// For local tracking, we'd need to implement redo tracking
|
|
279
|
+
console.log(chalk.yellow('Redo for local tracking is not yet implemented.'));
|
|
280
|
+
return;
|
|
281
|
+
} else {
|
|
282
|
+
const detection = await AgentDetector.detect();
|
|
283
|
+
if (!detection) {
|
|
284
|
+
console.log(chalk.yellow('No active AI agent session found in this directory.'));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const { parser } = detection;
|
|
288
|
+
sessionFile = await parser.getCurrentSessionFile();
|
|
289
|
+
|
|
290
|
+
// Get all operations including undone ones
|
|
291
|
+
const originalOperations = await parser.parseSessionFile(sessionFile, true);
|
|
292
|
+
const undoTracker = new UndoTracker();
|
|
293
|
+
await undoTracker.init();
|
|
294
|
+
|
|
295
|
+
operations = await undoTracker.getUndoneOperationsList(originalOperations, sessionFile);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (operations.length === 0) {
|
|
299
|
+
console.log(chalk.yellow(i18n.t('msg.no_operations_to_redo')));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Operations are already in reverse order (most recent undo first)
|
|
304
|
+
let selectedIndex = 0;
|
|
305
|
+
|
|
306
|
+
if (!operationId) {
|
|
307
|
+
const choices = operations.map((op, index) => {
|
|
308
|
+
const operationsToRedo = index + 1;
|
|
309
|
+
let name = `${op.type} - ${formatDistance(op.timestamp)}`;
|
|
310
|
+
|
|
311
|
+
if (operationsToRedo > 1) {
|
|
312
|
+
name += chalk.green(` (+ ${operationsToRedo - 1} more will be redone)`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
name: name,
|
|
317
|
+
value: index,
|
|
318
|
+
short: `${op.type} (${operationsToRedo} ops)`
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
console.log(chalk.yellow('\\n⚠️ Cascading redo: Selecting an operation will redo it and ALL undone operations that came before it.\\n'));
|
|
323
|
+
|
|
324
|
+
const answer = await inquirer.prompt([{
|
|
325
|
+
type: 'list',
|
|
326
|
+
name: 'selectedIndex',
|
|
327
|
+
message: i18n.t('prompt.select_operation_redo'),
|
|
328
|
+
choices: choices,
|
|
329
|
+
pageSize: 15
|
|
330
|
+
}]);
|
|
331
|
+
|
|
332
|
+
selectedIndex = answer.selectedIndex;
|
|
333
|
+
} else {
|
|
334
|
+
selectedIndex = operations.findIndex(op => op.id === operationId);
|
|
335
|
+
if (selectedIndex === -1) {
|
|
336
|
+
console.log(chalk.red(`Operation ${operationId} not found.`));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const operationsToRedo = operations.slice(0, selectedIndex + 1);
|
|
342
|
+
|
|
343
|
+
if (!options.yes) {
|
|
344
|
+
console.log(chalk.yellow(`\\n${i18n.t('header.this_will_redo', { count: operationsToRedo.length })}\\n`));
|
|
345
|
+
|
|
346
|
+
for (let i = 0; i < operationsToRedo.length; i++) {
|
|
347
|
+
const op = operationsToRedo[i];
|
|
348
|
+
console.log(`${chalk.bold(`${i + 1}.`)} ${chalk.cyan(op.type)} - ${formatDistance(op.timestamp)}`);
|
|
349
|
+
console.log('');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const confirm = await inquirer.prompt([{
|
|
353
|
+
type: 'confirm',
|
|
354
|
+
name: 'proceed',
|
|
355
|
+
message: i18n.t('prompt.confirm_redo', { count: operationsToRedo.length }),
|
|
356
|
+
default: false
|
|
357
|
+
}]);
|
|
358
|
+
|
|
359
|
+
if (!confirm.proceed) {
|
|
360
|
+
console.log(chalk.yellow('Redo cancelled.'));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const redoManager = new RedoManager();
|
|
366
|
+
await redoManager.init();
|
|
367
|
+
|
|
368
|
+
const undoTracker = new UndoTracker();
|
|
369
|
+
await undoTracker.init();
|
|
370
|
+
|
|
371
|
+
console.log(chalk.cyan(`\\n${i18n.t('header.redoing', { count: operationsToRedo.length })}\\n`));
|
|
372
|
+
|
|
373
|
+
let successCount = 0;
|
|
374
|
+
let failCount = 0;
|
|
375
|
+
|
|
376
|
+
// Redo operations in reverse order (oldest undone operation first)
|
|
377
|
+
for (const operation of operationsToRedo.reverse()) {
|
|
378
|
+
const result = await redoManager.redo(operation);
|
|
379
|
+
|
|
380
|
+
if (result.success) {
|
|
381
|
+
successCount++;
|
|
382
|
+
console.log(chalk.green(`✓ ${result.message}`));
|
|
383
|
+
if (result.backupPath) {
|
|
384
|
+
console.log(chalk.gray(` Backup saved to: ${result.backupPath}`));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Mark operation as redone (remove from undone list)
|
|
388
|
+
if (sessionFile) {
|
|
389
|
+
await undoTracker.markAsRedone(operation.id, sessionFile);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
failCount++;
|
|
393
|
+
console.log(chalk.red(`✗ ${result.message}`));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.log(chalk.bold(`\\nCompleted: ${chalk.green(successCount)} successful, ${chalk.red(failCount)} failed`));
|
|
398
|
+
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
program
|
|
405
|
+
.command('preview [operation-id]')
|
|
406
|
+
.description('Preview what would be undone without making changes')
|
|
407
|
+
.option('-s, --session <id>', 'Specify session ID')
|
|
408
|
+
.option('--local', 'Use local revert-ai tracking instead of Claude sessions')
|
|
409
|
+
.action(async (operationId, options) => {
|
|
410
|
+
try {
|
|
411
|
+
let operations = [];
|
|
412
|
+
|
|
413
|
+
if (options.local) {
|
|
414
|
+
const sessionId = options.session || await SessionTracker.getCurrentSession();
|
|
415
|
+
if (!sessionId) {
|
|
416
|
+
console.log(chalk.yellow('No local revert-ai session found.'));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const tracker = new SessionTracker(sessionId);
|
|
421
|
+
await tracker.init();
|
|
422
|
+
operations = await tracker.getOperations();
|
|
423
|
+
} else {
|
|
424
|
+
const detection = await AgentDetector.detect();
|
|
425
|
+
if (!detection) {
|
|
426
|
+
console.log(chalk.yellow('No active AI agent session found in this directory.'));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const { parser } = detection;
|
|
430
|
+
const sessionFile = await parser.getCurrentSessionFile();
|
|
431
|
+
operations = await parser.parseSessionFile(sessionFile);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (operations.length === 0) {
|
|
435
|
+
console.log(chalk.yellow('No operations found.'));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
operations.reverse();
|
|
440
|
+
|
|
441
|
+
let selectedIndex = 0;
|
|
442
|
+
|
|
443
|
+
if (!operationId) {
|
|
444
|
+
const choices = operations.map((op, index) => {
|
|
445
|
+
const dependentOps = getDependentOperations(index, operations, process.cwd());
|
|
446
|
+
let name = `${op.type} - ${formatDistance(op.timestamp)}`;
|
|
447
|
+
|
|
448
|
+
if (dependentOps.length > 1) {
|
|
449
|
+
name += chalk.gray(` (+ ${dependentOps.length - 1} dependent ops would be undone)`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
name: name,
|
|
454
|
+
value: index
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const answer = await inquirer.prompt([{
|
|
459
|
+
type: 'list',
|
|
460
|
+
name: 'selectedIndex',
|
|
461
|
+
message: 'Select operation to preview:',
|
|
462
|
+
choices: choices,
|
|
463
|
+
pageSize: 15
|
|
464
|
+
}]);
|
|
465
|
+
|
|
466
|
+
selectedIndex = answer.selectedIndex;
|
|
467
|
+
} else {
|
|
468
|
+
selectedIndex = operations.findIndex(op => op.id === operationId);
|
|
469
|
+
if (selectedIndex === -1) {
|
|
470
|
+
console.log(chalk.red(`Operation ${operationId} not found.`));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const operationsToUndo = getDependentOperations(selectedIndex, operations, process.cwd());
|
|
476
|
+
|
|
477
|
+
console.log(chalk.blue(`\\n📋 Preview: Would undo ${operationsToUndo.length} operation(s):\\n`));
|
|
478
|
+
|
|
479
|
+
for (let i = 0; i < operationsToUndo.length; i++) {
|
|
480
|
+
const op = operationsToUndo[i];
|
|
481
|
+
console.log(`${chalk.bold(`${i + 1}.`)} ${chalk.cyan(op.type)} - ${formatDistance(op.timestamp)}`);
|
|
482
|
+
|
|
483
|
+
const preview = await OperationPreview.generatePreview(op);
|
|
484
|
+
console.log(` ${preview.preview.replace(/\\n/g, '\\n ')}`);
|
|
485
|
+
console.log('');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log(chalk.gray('💡 To actually perform these undos, run: revert-ai undo'));
|
|
489
|
+
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
program
|
|
496
|
+
.command('sessions')
|
|
497
|
+
.description('List all available Claude Code sessions')
|
|
498
|
+
.option('--local', 'Show local revert-ai sessions instead of Claude sessions')
|
|
499
|
+
.action(async (options) => {
|
|
500
|
+
try {
|
|
501
|
+
if (options.local) {
|
|
502
|
+
const sessions = await SessionTracker.listSessions();
|
|
503
|
+
const currentSession = await SessionTracker.getCurrentSession();
|
|
504
|
+
|
|
505
|
+
if (sessions.length === 0) {
|
|
506
|
+
console.log(chalk.yellow('No local sessions found.'));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
console.log(chalk.bold('\nAvailable local sessions:\n'));
|
|
511
|
+
|
|
512
|
+
sessions.forEach(session => {
|
|
513
|
+
const isCurrent = session === currentSession;
|
|
514
|
+
const marker = isCurrent ? chalk.green('→ ') : ' ';
|
|
515
|
+
console.log(`${marker}${session}`);
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
const detection = await AgentDetector.detect();
|
|
519
|
+
if (!detection) {
|
|
520
|
+
console.log(chalk.yellow('No active AI agent session found in this directory.'));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const { parser } = detection;
|
|
524
|
+
const sessions = await parser.getAllSessions();
|
|
525
|
+
|
|
526
|
+
if (sessions.length === 0) {
|
|
527
|
+
console.log(chalk.yellow('No active sessions found.'));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
console.log(chalk.bold('\nAvailable sessions:\n'));
|
|
532
|
+
|
|
533
|
+
const currentProjectDir = await parser.getCurrentProjectDir();
|
|
534
|
+
const currentProjectDirName = path.basename(currentProjectDir);
|
|
535
|
+
|
|
536
|
+
sessions.forEach(session => {
|
|
537
|
+
const isCurrent = session.rawProjectDir === currentProjectDirName;
|
|
538
|
+
const marker = isCurrent ? chalk.green('→ ') : ' ';
|
|
539
|
+
console.log(`${marker}${chalk.cyan(session.id)}`);
|
|
540
|
+
console.log(` Project: ${session.project}`);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
program
|
|
549
|
+
.command('session <id>')
|
|
550
|
+
.description(i18n.t('cmd.session.description'))
|
|
551
|
+
.action(async (sessionId) => {
|
|
552
|
+
try {
|
|
553
|
+
await SessionTracker.setCurrentSession(sessionId);
|
|
554
|
+
console.log(chalk.green(`Switched to session: ${sessionId}`));
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
program
|
|
561
|
+
.command('language [lang]')
|
|
562
|
+
.description(i18n.t('cmd.language.description'))
|
|
563
|
+
.action(async (lang) => {
|
|
564
|
+
try {
|
|
565
|
+
if (!lang) {
|
|
566
|
+
// Show current language and available options
|
|
567
|
+
const current = i18n.getCurrentLanguage();
|
|
568
|
+
const available = i18n.getAvailableLanguages();
|
|
569
|
+
|
|
570
|
+
console.log(chalk.bold(`\\nCurrent language: ${chalk.cyan(current.name)} (${current.code})\\n`));
|
|
571
|
+
console.log(chalk.bold('Available languages:'));
|
|
572
|
+
available.forEach(({ code, name }) => {
|
|
573
|
+
const marker = code === current.code ? chalk.green('→ ') : ' ';
|
|
574
|
+
console.log(`${marker}${code} - ${name}`);
|
|
575
|
+
});
|
|
576
|
+
console.log(chalk.gray('\\nUsage: revert-ai language <code>'));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
await i18n.setLanguage(lang);
|
|
581
|
+
const newLang = i18n.getCurrentLanguage();
|
|
582
|
+
console.log(chalk.green(i18n.t('msg.language_set', { language: newLang.name })));
|
|
583
|
+
} catch (error) {
|
|
584
|
+
const available = i18n.getAvailableLanguages().map(l => l.code).join(', ');
|
|
585
|
+
console.error(chalk.red(i18n.t('msg.language_invalid', { languages: available })));
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
program.parse(process.argv);
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "revert-ai",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Intelligent undo/redo for AI Coding Assistants (Claude Code, Aider, etc.) with cascading safety, visual diffs, and Git checkpoints",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"revert-ai": "./bin/revert-ai.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
|
|
11
|
+
"test:watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
|
12
|
+
"demo": "./demo-language.sh",
|
|
13
|
+
"build:binaries": "npx @yao-pkg/pkg bin/revert-ai.js --targets node18-macos-x64,node18-macos-arm64,node18-linux-x64,node18-win-x64 --out-path dist/",
|
|
14
|
+
"prepublishOnly": "npm test"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"undo",
|
|
20
|
+
"redo",
|
|
21
|
+
"revert",
|
|
22
|
+
"cli",
|
|
23
|
+
"ai-development",
|
|
24
|
+
"session-management",
|
|
25
|
+
"file-operations",
|
|
26
|
+
"backup",
|
|
27
|
+
"preview",
|
|
28
|
+
"internationalization",
|
|
29
|
+
"japanese"
|
|
30
|
+
],
|
|
31
|
+
"author": "Harsha Rajkumar",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"type": "module",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16.0.0"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/harsharajkumar-273/revert-ai.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/harsharajkumar-273/revert-ai/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/harsharajkumar-273/revert-ai#readme",
|
|
45
|
+
"files": [
|
|
46
|
+
"bin/",
|
|
47
|
+
"src/",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@babel/parser": "^8.0.4",
|
|
53
|
+
"chalk": "^5.3.0",
|
|
54
|
+
"commander": "^11.1.0",
|
|
55
|
+
"inquirer": "^9.2.12"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"jest": "^29.7.0"
|
|
59
|
+
},
|
|
60
|
+
"pkg": {
|
|
61
|
+
"scripts": "src/**/*.js",
|
|
62
|
+
"assets": [
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE"
|
|
65
|
+
],
|
|
66
|
+
"targets": [
|
|
67
|
+
"node18-macos-x64",
|
|
68
|
+
"node18-macos-arm64",
|
|
69
|
+
"node18-linux-x64",
|
|
70
|
+
"node18-win-x64"
|
|
71
|
+
],
|
|
72
|
+
"outputPath": "dist"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ClaudeSessionParser } from './ClaudeSessionParser.js';
|
|
2
|
+
import { AiderSessionParser } from './AiderSessionParser.js';
|
|
3
|
+
|
|
4
|
+
export class AgentDetector {
|
|
5
|
+
/**
|
|
6
|
+
* Automatically detect which AI agent session is active in the current directory.
|
|
7
|
+
* Checks for Aider first, then Claude Code.
|
|
8
|
+
* @returns {Promise<{ agent: string, parser: import('./AgentParser.js').AgentParser } | null>}
|
|
9
|
+
*/
|
|
10
|
+
static async detect() {
|
|
11
|
+
// 1. Check Aider first
|
|
12
|
+
const aider = new AiderSessionParser();
|
|
13
|
+
const hasAider = await aider.detect();
|
|
14
|
+
if (hasAider) {
|
|
15
|
+
return { agent: 'aider', parser: aider };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 2. Check Claude Code
|
|
19
|
+
const claude = new ClaudeSessionParser();
|
|
20
|
+
const hasClaude = await claude.getCurrentSessionFile();
|
|
21
|
+
if (hasClaude) {
|
|
22
|
+
return { agent: 'claude', parser: claude };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|