@mrxkun/mcfast-mcp 2.1.2 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrxkun/mcfast-mcp",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "Ultra-fast code editing with fuzzy patching, auto-rollback, and 5 unified tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,10 @@
27
27
  "url": "git+https://github.com/ndpmmo/mcfast.git"
28
28
  },
29
29
  "dependencies": {
30
+ "@babel/generator": "^7.29.1",
31
+ "@babel/parser": "^7.29.0",
32
+ "@babel/traverse": "^7.29.0",
33
+ "@babel/types": "^7.29.0",
30
34
  "@modelcontextprotocol/sdk": "^0.6.0",
31
35
  "fast-glob": "^3.3.3",
32
36
  "ignore": "^7.0.5"
package/src/index.js CHANGED
@@ -16,6 +16,18 @@ import fg from "fast-glob";
16
16
  import { detectEditStrategy, extractSearchReplace } from './strategies/edit-strategy.js';
17
17
  import { detectSearchStrategy } from './strategies/search-strategy.js';
18
18
  import { applyFuzzyPatch } from './strategies/fuzzy-patch.js';
19
+ import {
20
+ detectRefactoringPattern,
21
+ parseCode,
22
+ renameIdentifier,
23
+ inlineVariable,
24
+ applyASTTransformation
25
+ } from './strategies/ast-detector.js';
26
+ import {
27
+ detectCrossFileEdit,
28
+ batchRenameSymbol,
29
+ updateImportPaths
30
+ } from './strategies/multi-file-coordinator.js';
19
31
  import { safeEdit } from './utils/backup.js';
20
32
  import { formatError } from './utils/error-formatter.js';
21
33
 
@@ -493,6 +505,61 @@ async function handleReapply({ instruction, files, errorContext = "", attempt =
493
505
  * Auto-detects best strategy based on input
494
506
  */
495
507
  async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
508
+ // Check for multi-file edits first
509
+ const multiFileEdit = detectCrossFileEdit(instruction, files);
510
+
511
+ if (multiFileEdit && Object.keys(files).length > 1) {
512
+ console.error(`${colors.cyan}[MULTI-FILE EDIT]${colors.reset} ${multiFileEdit.type} - ${multiFileEdit.reason}`);
513
+
514
+ try {
515
+ let result;
516
+
517
+ if (multiFileEdit.type === 'symbol_rename') {
518
+ result = await batchRenameSymbol(files, multiFileEdit.symbol, multiFileEdit.newSymbol);
519
+ } else if (multiFileEdit.type === 'dependency') {
520
+ // Extract old and new paths from instruction
521
+ const pathMatch = instruction.match(/from\s+['"]([^'"]+)['"]\s+to\s+['"]([^'"]+)['"]/i);
522
+ if (pathMatch) {
523
+ result = await updateImportPaths(files, pathMatch[1], pathMatch[2]);
524
+ } else {
525
+ throw new Error('Could not extract import paths from instruction');
526
+ }
527
+ } else {
528
+ throw new Error(`Unsupported multi-file edit type: ${multiFileEdit.type}`);
529
+ }
530
+
531
+ if (!result.success) {
532
+ return {
533
+ content: [{
534
+ type: "text",
535
+ text: formatError('generic', {
536
+ error: result.message || result.error
537
+ })
538
+ }],
539
+ isError: true
540
+ };
541
+ }
542
+
543
+ return {
544
+ content: [{
545
+ type: "text",
546
+ text: `✅ Multi-File Edit Successful\n\nType: ${multiFileEdit.type}\nFiles Modified: ${result.files.length}\n\nBackups created for all files.`
547
+ }]
548
+ };
549
+
550
+ } catch (error) {
551
+ return {
552
+ content: [{
553
+ type: "text",
554
+ text: formatError('generic', {
555
+ error: `Multi-file edit failed: ${error.message}`
556
+ })
557
+ }],
558
+ isError: true
559
+ };
560
+ }
561
+ }
562
+
496
563
  const strategy = detectEditStrategy({ instruction, code_edit, files });
497
564
 
498
565
  console.error(`${colors.cyan}[EDIT STRATEGY]${colors.reset} ${strategy}`);
@@ -565,7 +632,88 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
565
632
  }
566
633
  }
567
634
 
568
- // Strategy 2: Search/Replace (deterministic, fastest)
635
+ // Strategy 2: AST Refactor (symbol-aware operations)
636
+ if (strategy === 'ast_refactor') {
637
+ const filePath = Object.keys(files)[0];
638
+ const fileContent = files[filePath];
639
+ const pattern = detectRefactoringPattern(instruction);
640
+
641
+ if (!pattern) {
642
+ return {
643
+ content: [{
644
+ type: "text",
645
+ text: formatError('generic', {
646
+ error: 'Could not detect refactoring pattern from instruction'
647
+ })
648
+ }],
649
+ isError: true
650
+ };
651
+ }
652
+
653
+ try {
654
+ let transformResult;
655
+ const editResult = await safeEdit(filePath, async () => {
656
+ // Apply AST transformation based on pattern type
657
+ if (pattern.type.startsWith('rename')) {
658
+ transformResult = applyASTTransformation(fileContent, filePath, (ast) => {
659
+ return renameIdentifier(ast, pattern.oldName, pattern.newName);
660
+ });
661
+ } else if (pattern.type.startsWith('inline')) {
662
+ transformResult = applyASTTransformation(fileContent, filePath, (ast) => {
663
+ return inlineVariable(ast, pattern.oldName);
664
+ });
665
+ } else {
666
+ throw new Error(`Unsupported refactoring type: ${pattern.type}`);
667
+ }
668
+
669
+ if (!transformResult.success) {
670
+ throw new Error(transformResult.message);
671
+ }
672
+
673
+ // Write transformed code
674
+ await fs.writeFile(filePath, transformResult.code, 'utf8');
675
+ });
676
+
677
+ if (!editResult.success) {
678
+ let errorType = 'generic';
679
+ if (editResult.error.includes('ENOENT')) errorType = 'file_not_found';
680
+ else if (editResult.error.includes('EACCES')) errorType = 'permission_denied';
681
+ else if (editResult.error.includes('Validation failed')) errorType = 'syntax_error';
682
+
683
+ return {
684
+ content: [{
685
+ type: "text",
686
+ text: formatError(errorType, {
687
+ filePath,
688
+ error: editResult.error,
689
+ backupPath: editResult.backupPath
690
+ })
691
+ }],
692
+ isError: true
693
+ };
694
+ }
695
+
696
+ return {
697
+ content: [{
698
+ type: "text",
699
+ text: `✅ AST Refactor Applied Successfully\n\nOperation: ${pattern.type}\nChanges: ${transformResult.count || transformResult.replacements || 0} locations\n\nBackup: ${editResult.backupPath}`
700
+ }]
701
+ };
702
+
703
+ } catch (error) {
704
+ return {
705
+ content: [{
706
+ type: "text",
707
+ text: formatError('generic', {
708
+ error: `AST refactor failed: ${error.message}`
709
+ })
710
+ }],
711
+ isError: true
712
+ };
713
+ }
714
+ }
715
+
716
+ // Strategy 3: Search/Replace (deterministic, fastest)
569
717
  if (strategy === 'search_replace') {
570
718
  const extracted = extractSearchReplace(instruction);
571
719
  if (extracted) {
@@ -0,0 +1,288 @@
1
+ /**
2
+ * AST-Aware Detection for mcfast v2.2
3
+ * Enables symbol-aware refactoring using Babel parser
4
+ */
5
+
6
+ import parser from '@babel/parser';
7
+ import _traverse from '@babel/traverse';
8
+ import _generate from '@babel/generator';
9
+ import * as t from '@babel/types';
10
+ import path from 'path';
11
+
12
+ // Handle default exports from Babel (CommonJS compatibility)
13
+ const traverse = _traverse.default || _traverse;
14
+ const generate = _generate.default || _generate;
15
+
16
+ /**
17
+ * Parse code into AST based on file extension
18
+ */
19
+ export function parseCode(code, filePath) {
20
+ const ext = path.extname(filePath).toLowerCase();
21
+ const plugins = [];
22
+
23
+ // TypeScript support
24
+ if (['.ts', '.tsx', '.mts', '.cts'].includes(ext)) {
25
+ plugins.push('typescript');
26
+ }
27
+
28
+ // JSX support
29
+ if (['.jsx', '.tsx'].includes(ext)) {
30
+ plugins.push('jsx');
31
+ }
32
+
33
+ // Common plugins
34
+ plugins.push('decorators-legacy', 'classProperties', 'objectRestSpread');
35
+
36
+ try {
37
+ return parser.parse(code, {
38
+ sourceType: 'module',
39
+ plugins,
40
+ errorRecovery: true
41
+ });
42
+ } catch (error) {
43
+ // Fallback: try as script
44
+ try {
45
+ return parser.parse(code, {
46
+ sourceType: 'script',
47
+ plugins,
48
+ errorRecovery: true
49
+ });
50
+ } catch (fallbackError) {
51
+ throw new Error(`Failed to parse ${filePath}: ${error.message}`);
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Detect refactoring patterns from instruction
58
+ */
59
+ export function detectRefactoringPattern(instruction) {
60
+ const patterns = {
61
+ rename_variable: /rename\s+(?:variable|var|const|let)\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
62
+ rename_function: /rename\s+(?:function|func)\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
63
+ rename_class: /rename\s+class\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
64
+ rename_any: /rename\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
65
+ extract_function: /extract\s+(?:function|method)/i,
66
+ inline_variable: /inline\s+(?:variable|var|const|let)\s+["`']?(\w+)["`']?/i,
67
+ inline_function: /inline\s+(?:function|func)\s+["`']?(\w+)["`']?/i
68
+ };
69
+
70
+ for (const [type, pattern] of Object.entries(patterns)) {
71
+ const match = instruction.match(pattern);
72
+ if (match) {
73
+ return {
74
+ type,
75
+ oldName: match[1],
76
+ newName: match[2] || null
77
+ };
78
+ }
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Rename identifier in AST
86
+ */
87
+ export function renameIdentifier(ast, oldName, newName, options = {}) {
88
+ const { scope = 'all' } = options;
89
+ const renamedLocations = [];
90
+
91
+ traverse(ast, {
92
+ Identifier(path) {
93
+ if (path.node.name === oldName) {
94
+ // Check if this is a binding (declaration) or reference
95
+ const binding = path.scope.getBinding(oldName);
96
+
97
+ if (scope === 'all' || (binding && path.isReferencedIdentifier())) {
98
+ path.node.name = newName;
99
+ renamedLocations.push({
100
+ line: path.node.loc?.start.line,
101
+ column: path.node.loc?.start.column,
102
+ type: path.parent.type
103
+ });
104
+ }
105
+ }
106
+ }
107
+ });
108
+
109
+ return {
110
+ success: true,
111
+ ast,
112
+ count: renamedLocations.length,
113
+ locations: renamedLocations
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Find all usages of an identifier
119
+ */
120
+ export function findIdentifierUsages(ast, name) {
121
+ const usages = [];
122
+
123
+ traverse(ast, {
124
+ Identifier(path) {
125
+ if (path.node.name === name) {
126
+ const binding = path.scope.getBinding(name);
127
+ usages.push({
128
+ line: path.node.loc?.start.line,
129
+ column: path.node.loc?.start.column,
130
+ type: path.parent.type,
131
+ isDeclaration: binding && path.isBindingIdentifier(),
132
+ isReference: path.isReferencedIdentifier(),
133
+ scope: path.scope.uid
134
+ });
135
+ }
136
+ }
137
+ });
138
+
139
+ return usages;
140
+ }
141
+
142
+ /**
143
+ * Extract function from selected code
144
+ */
145
+ export function extractFunction(ast, startLine, endLine, functionName) {
146
+ let extractedStatements = [];
147
+ let insertionPoint = null;
148
+
149
+ traverse(ast, {
150
+ enter(path) {
151
+ const loc = path.node.loc;
152
+ if (!loc) return;
153
+
154
+ // Find statements within range
155
+ if (loc.start.line >= startLine && loc.end.line <= endLine) {
156
+ if (t.isStatement(path.node) && !path.findParent(p => extractedStatements.includes(p.node))) {
157
+ extractedStatements.push(path.node);
158
+ if (!insertionPoint) {
159
+ insertionPoint = path.getStatementParent();
160
+ }
161
+ }
162
+ }
163
+ }
164
+ });
165
+
166
+ if (extractedStatements.length === 0) {
167
+ return { success: false, message: 'No statements found in range' };
168
+ }
169
+
170
+ // Create new function
171
+ const newFunction = t.functionDeclaration(
172
+ t.identifier(functionName),
173
+ [],
174
+ t.blockStatement(extractedStatements)
175
+ );
176
+
177
+ // Replace extracted code with function call
178
+ const functionCall = t.expressionStatement(
179
+ t.callExpression(t.identifier(functionName), [])
180
+ );
181
+
182
+ return {
183
+ success: true,
184
+ newFunction,
185
+ functionCall,
186
+ insertionPoint
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Inline variable (replace all usages with its value)
192
+ */
193
+ export function inlineVariable(ast, variableName) {
194
+ let variableValue = null;
195
+ let declarationPath = null;
196
+ const replacements = [];
197
+
198
+ // Find variable declaration
199
+ traverse(ast, {
200
+ VariableDeclarator(path) {
201
+ if (path.node.id.name === variableName) {
202
+ variableValue = path.node.init;
203
+ declarationPath = path;
204
+ }
205
+ }
206
+ });
207
+
208
+ if (!variableValue) {
209
+ return { success: false, message: `Variable ${variableName} not found` };
210
+ }
211
+
212
+ // Replace all references
213
+ traverse(ast, {
214
+ Identifier(path) {
215
+ if (path.node.name === variableName && path.isReferencedIdentifier()) {
216
+ // Clone the value to avoid reference issues
217
+ const clonedValue = t.cloneNode(variableValue, true);
218
+ path.replaceWith(clonedValue);
219
+ replacements.push({
220
+ line: path.node.loc?.start.line,
221
+ column: path.node.loc?.start.column
222
+ });
223
+ }
224
+ }
225
+ });
226
+
227
+ // Remove declaration
228
+ if (declarationPath) {
229
+ const parent = declarationPath.parentPath;
230
+ if (parent.node.declarations.length === 1) {
231
+ parent.remove();
232
+ } else {
233
+ declarationPath.remove();
234
+ }
235
+ }
236
+
237
+ return {
238
+ success: true,
239
+ ast,
240
+ replacements: replacements.length,
241
+ locations: replacements
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Apply AST transformation and generate code
247
+ */
248
+ export function applyASTTransformation(code, filePath, transformation) {
249
+ try {
250
+ const ast = parseCode(code, filePath);
251
+ const result = transformation(ast);
252
+
253
+ if (!result.success) {
254
+ return result;
255
+ }
256
+
257
+ const output = generate(result.ast || ast, {
258
+ retainLines: true,
259
+ comments: true
260
+ });
261
+
262
+ return {
263
+ success: true,
264
+ code: output.code,
265
+ ...result
266
+ };
267
+ } catch (error) {
268
+ return {
269
+ success: false,
270
+ message: `AST transformation failed: ${error.message}`
271
+ };
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Check if instruction requires AST-based refactoring
277
+ */
278
+ export function requiresAST(instruction) {
279
+ const astPatterns = [
280
+ /rename\s+(?:variable|function|class|method)/i,
281
+ /extract\s+(?:function|method)/i,
282
+ /inline\s+(?:variable|function)/i,
283
+ /move\s+(?:function|class)/i,
284
+ /split\s+(?:function|class)/i
285
+ ];
286
+
287
+ return astPatterns.some(pattern => pattern.test(instruction));
288
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { isDiffBasedEdit } from './fuzzy-patch.js';
7
+ import { requiresAST, detectRefactoringPattern } from './ast-detector.js';
7
8
 
8
9
  /**
9
10
  * Detect if instruction is a simple search/replace
@@ -64,7 +65,7 @@ export function hasPlaceholders(codeEdit) {
64
65
 
65
66
  /**
66
67
  * Determine the best edit strategy
67
- * @returns {'fuzzy_patch' | 'search_replace' | 'placeholder_merge' | 'mercury_intelligent'}
68
+ * @returns {'fuzzy_patch' | 'ast_refactor' | 'search_replace' | 'placeholder_merge' | 'mercury_intelligent'}
68
69
  */
69
70
  export function detectEditStrategy({ instruction, code_edit, files }) {
70
71
  // Priority 1: Fuzzy Patch (unified diff format)
@@ -72,16 +73,21 @@ export function detectEditStrategy({ instruction, code_edit, files }) {
72
73
  return 'fuzzy_patch';
73
74
  }
74
75
 
75
- // Priority 2: Search/Replace (fastest, deterministic)
76
+ // Priority 2: AST Refactor (symbol-aware operations)
77
+ if (requiresAST(instruction)) {
78
+ return 'ast_refactor';
79
+ }
80
+
81
+ // Priority 3: Search/Replace (fastest, deterministic)
76
82
  if (isSearchReplace(instruction)) {
77
83
  return 'search_replace';
78
84
  }
79
85
 
80
- // Priority 3: Placeholder Merge (token-efficient)
86
+ // Priority 4: Placeholder Merge (token-efficient)
81
87
  if (code_edit && hasPlaceholders(code_edit)) {
82
88
  return 'placeholder_merge';
83
89
  }
84
90
 
85
- // Priority 4: Mercury Intelligent (most flexible)
91
+ // Priority 5: Mercury Intelligent (most flexible)
86
92
  return 'mercury_intelligent';
87
93
  }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Multi-File Coordination for mcfast v2.2
3
+ * Enables cross-file edits with atomic rollback
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { parseCode, renameIdentifier, findIdentifierUsages } from './ast-detector.js';
9
+ import { createBackup, restoreFromBackup, validateFile } from '../utils/backup.js';
10
+ import _generate from '@babel/generator';
11
+
12
+ // Handle default export from Babel (CommonJS compatibility)
13
+ const generate = _generate.default || _generate;
14
+
15
+ /**
16
+ * Detect if edit requires multiple files
17
+ */
18
+ export function detectCrossFileEdit(instruction, files) {
19
+ const fileKeys = Object.keys(files);
20
+
21
+ // Pattern 1: Explicit multi-file mention
22
+ if (/in\s+all\s+files|across\s+files|in\s+every|everywhere/i.test(instruction)) {
23
+ return {
24
+ type: 'broadcast',
25
+ files: fileKeys,
26
+ reason: 'Explicit multi-file instruction'
27
+ };
28
+ }
29
+
30
+ // Pattern 2: Import/Export changes
31
+ if (/change\s+import|update\s+import|fix\s+import|update\s+export/i.test(instruction)) {
32
+ return {
33
+ type: 'dependency',
34
+ files: fileKeys,
35
+ reason: 'Import/export modification'
36
+ };
37
+ }
38
+
39
+ // Pattern 3: Symbol rename (check if symbol exists in multiple files)
40
+ const renameMatch = instruction.match(/rename\s+(?:variable|function|class|const|let)?\s*["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i);
41
+ if (renameMatch && fileKeys.length > 1) {
42
+ const [, oldName, newName] = renameMatch;
43
+ const filesWithSymbol = findFilesUsingSymbol(files, oldName);
44
+
45
+ if (filesWithSymbol.length > 1) {
46
+ return {
47
+ type: 'symbol_rename',
48
+ symbol: oldName,
49
+ newSymbol: newName,
50
+ files: filesWithSymbol,
51
+ reason: `Symbol "${oldName}" found in ${filesWithSymbol.length} files`
52
+ };
53
+ }
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Find files that use a specific symbol
61
+ */
62
+ export function findFilesUsingSymbol(files, symbolName) {
63
+ const filesWithSymbol = [];
64
+
65
+ for (const [filePath, content] of Object.entries(files)) {
66
+ // Quick text search first (performance optimization)
67
+ if (!content.includes(symbolName)) continue;
68
+
69
+ try {
70
+ const ast = parseCode(content, filePath);
71
+ const usages = findIdentifierUsages(ast, symbolName);
72
+
73
+ if (usages.length > 0) {
74
+ filesWithSymbol.push({
75
+ path: filePath,
76
+ usageCount: usages.length,
77
+ usages
78
+ });
79
+ }
80
+ } catch (error) {
81
+ // If AST parsing fails, fall back to text search
82
+ const regex = new RegExp(`\\b${symbolName}\\b`, 'g');
83
+ const matches = content.match(regex);
84
+ if (matches && matches.length > 0) {
85
+ filesWithSymbol.push({
86
+ path: filePath,
87
+ usageCount: matches.length,
88
+ usages: [],
89
+ parseError: error.message
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ return filesWithSymbol.map(f => f.path);
96
+ }
97
+
98
+ /**
99
+ * Find files that import from a specific file
100
+ */
101
+ export function findDependentFiles(files, targetFile) {
102
+ const dependents = [];
103
+ const targetBasename = path.basename(targetFile, path.extname(targetFile));
104
+
105
+ for (const [filePath, content] of Object.entries(files)) {
106
+ if (filePath === targetFile) continue;
107
+
108
+ // Check for imports from target file
109
+ const importPatterns = [
110
+ new RegExp(`from\\s+['"](\\.\\/|\\.\\.\\/)*${targetBasename}['"]`, 'g'),
111
+ new RegExp(`require\\(['"](\\.\\/|\\.\\.\\/)*${targetBasename}['"]\\)`, 'g'),
112
+ new RegExp(`import\\(['"](\\.\\/|\\.\\.\\/)*${targetBasename}['"]\\)`, 'g')
113
+ ];
114
+
115
+ const hasImport = importPatterns.some(pattern => pattern.test(content));
116
+ if (hasImport) {
117
+ dependents.push(filePath);
118
+ }
119
+ }
120
+
121
+ return dependents;
122
+ }
123
+
124
+ /**
125
+ * Atomic multi-file edit with rollback
126
+ */
127
+ export async function safeMultiFileEdit(fileEdits) {
128
+ const backups = new Map();
129
+ const completed = [];
130
+ const results = [];
131
+
132
+ try {
133
+ // Phase 1: Backup all files
134
+ console.error(`[MULTI-FILE] Backing up ${fileEdits.length} files...`);
135
+ for (const { filePath } of fileEdits) {
136
+ const backupPath = await createBackup(filePath);
137
+ backups.set(filePath, backupPath);
138
+ }
139
+
140
+ // Phase 2: Apply all edits
141
+ console.error(`[MULTI-FILE] Applying edits...`);
142
+ for (const { filePath, editFn } of fileEdits) {
143
+ try {
144
+ const result = await editFn();
145
+ completed.push(filePath);
146
+ results.push({ filePath, success: true, result });
147
+ } catch (error) {
148
+ throw new Error(`Edit failed for ${filePath}: ${error.message}`);
149
+ }
150
+ }
151
+
152
+ // Phase 3: Validate all files
153
+ console.error(`[MULTI-FILE] Validating ${completed.length} files...`);
154
+ for (const filePath of completed) {
155
+ const validation = await validateFile(filePath);
156
+ if (!validation.valid) {
157
+ throw new Error(`Validation failed for ${filePath}: ${validation.error}`);
158
+ }
159
+ }
160
+
161
+ console.error(`[MULTI-FILE] ✅ All ${completed.length} files edited successfully`);
162
+
163
+ return {
164
+ success: true,
165
+ files: completed,
166
+ results,
167
+ backups: Array.from(backups.entries())
168
+ };
169
+
170
+ } catch (error) {
171
+ // Rollback ALL files
172
+ console.error(`[MULTI-FILE] ❌ Error: ${error.message}`);
173
+ console.error(`[MULTI-FILE] Rolling back ${backups.size} files...`);
174
+
175
+ for (const [filePath, backupPath] of backups) {
176
+ try {
177
+ await restoreFromBackup(filePath, backupPath);
178
+ } catch (restoreError) {
179
+ console.error(`[MULTI-FILE] Failed to restore ${filePath}: ${restoreError.message}`);
180
+ }
181
+ }
182
+
183
+ return {
184
+ success: false,
185
+ error: error.message,
186
+ rolledBack: Array.from(backups.keys())
187
+ };
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Batch rename symbol across multiple files
193
+ */
194
+ export async function batchRenameSymbol(files, oldName, newName) {
195
+ const fileEdits = [];
196
+
197
+ for (const [filePath, content] of Object.entries(files)) {
198
+ // Skip if symbol not in file
199
+ if (!content.includes(oldName)) continue;
200
+
201
+ fileEdits.push({
202
+ filePath,
203
+ editFn: async () => {
204
+ const ast = parseCode(content, filePath);
205
+ const result = renameIdentifier(ast, oldName, newName);
206
+
207
+ if (!result.success) {
208
+ throw new Error(result.message);
209
+ }
210
+
211
+ const output = generate(result.ast, {
212
+ retainLines: true,
213
+ comments: true
214
+ });
215
+
216
+ await fs.writeFile(filePath, output.code, 'utf8');
217
+
218
+ return {
219
+ count: result.count,
220
+ locations: result.locations
221
+ };
222
+ }
223
+ });
224
+ }
225
+
226
+ if (fileEdits.length === 0) {
227
+ return {
228
+ success: false,
229
+ message: `Symbol "${oldName}" not found in any files`
230
+ };
231
+ }
232
+
233
+ return await safeMultiFileEdit(fileEdits);
234
+ }
235
+
236
+ /**
237
+ * Update import paths across multiple files
238
+ */
239
+ export async function updateImportPaths(files, oldPath, newPath) {
240
+ const fileEdits = [];
241
+ const oldBasename = path.basename(oldPath, path.extname(oldPath));
242
+ const newBasename = path.basename(newPath, path.extname(newPath));
243
+
244
+ for (const [filePath, content] of Object.entries(files)) {
245
+ const hasImport = content.includes(oldBasename);
246
+ if (!hasImport) continue;
247
+
248
+ fileEdits.push({
249
+ filePath,
250
+ editFn: async () => {
251
+ // Replace import paths
252
+ let newContent = content;
253
+
254
+ // Handle various import formats
255
+ const patterns = [
256
+ {
257
+ regex: new RegExp(`from\\s+(['"])(\\.\\/|\\.\\.\\/)*${oldBasename}\\1`, 'g'),
258
+ replace: (match, quote, prefix) => `from ${quote}${prefix || './'}${newBasename}${quote}`
259
+ },
260
+ {
261
+ regex: new RegExp(`require\\((['"])(\\.\\/|\\.\\.\\/)*${oldBasename}\\1\\)`, 'g'),
262
+ replace: (match, quote, prefix) => `require(${quote}${prefix || './'}${newBasename}${quote})`
263
+ },
264
+ {
265
+ regex: new RegExp(`import\\((['"])(\\.\\/|\\.\\.\\/)*${oldBasename}\\1\\)`, 'g'),
266
+ replace: (match, quote, prefix) => `import(${quote}${prefix || './'}${newBasename}${quote})`
267
+ }
268
+ ];
269
+
270
+ let changeCount = 0;
271
+ for (const { regex, replace } of patterns) {
272
+ const matches = newContent.match(regex);
273
+ if (matches) {
274
+ changeCount += matches.length;
275
+ newContent = newContent.replace(regex, replace);
276
+ }
277
+ }
278
+
279
+ if (changeCount === 0) {
280
+ throw new Error('No import statements found to update');
281
+ }
282
+
283
+ await fs.writeFile(filePath, newContent, 'utf8');
284
+
285
+ return { changeCount };
286
+ }
287
+ });
288
+ }
289
+
290
+ if (fileEdits.length === 0) {
291
+ return {
292
+ success: false,
293
+ message: `No files import from "${oldPath}"`
294
+ };
295
+ }
296
+
297
+ return await safeMultiFileEdit(fileEdits);
298
+ }