mcp-code-context 2.2.0 → 2.3.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 (128) hide show
  1. package/.io/RULES.md +12 -0
  2. package/CHANGELOG.md +88 -1
  3. package/README.md +27 -0
  4. package/RELEASE_NOTES_v2.3.0.md +228 -0
  5. package/RELEASE_NOTES_v2.3.1.md +110 -0
  6. package/dist/src/ast/writers/safeRename.d.ts +14 -0
  7. package/dist/src/ast/writers/safeRename.js +258 -0
  8. package/dist/src/ast/writers/safeRename.js.map +1 -0
  9. package/dist/src/ast/writers/symbolWriter.d.ts +2 -0
  10. package/dist/src/ast/writers/symbolWriter.js +9 -14
  11. package/dist/src/ast/writers/symbolWriter.js.map +1 -1
  12. package/dist/src/cache/astCache.d.ts +33 -0
  13. package/dist/src/cache/astCache.js +113 -0
  14. package/dist/src/cache/astCache.js.map +1 -0
  15. package/dist/src/handlers/readHandlers.d.ts +70 -0
  16. package/dist/src/handlers/readHandlers.js +431 -0
  17. package/dist/src/handlers/readHandlers.js.map +1 -0
  18. package/dist/src/handlers/utilHandlers.d.ts +31 -0
  19. package/dist/src/handlers/utilHandlers.js +69 -0
  20. package/dist/src/handlers/utilHandlers.js.map +1 -0
  21. package/dist/src/handlers/writeHandlers.d.ts +57 -0
  22. package/dist/src/handlers/writeHandlers.js +475 -0
  23. package/dist/src/handlers/writeHandlers.js.map +1 -0
  24. package/dist/src/index.d.ts +6 -143
  25. package/dist/src/index.js +77 -1091
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/utils/arrayUtils.d.ts +12 -0
  28. package/dist/src/utils/arrayUtils.js +27 -0
  29. package/dist/src/utils/arrayUtils.js.map +1 -0
  30. package/dist/src/utils/constants.d.ts +46 -0
  31. package/dist/src/utils/constants.js +112 -0
  32. package/dist/src/utils/constants.js.map +1 -0
  33. package/dist/src/utils/ignoreManager.d.ts +8 -0
  34. package/dist/src/utils/ignoreManager.js +49 -0
  35. package/dist/src/utils/ignoreManager.js.map +1 -1
  36. package/dist/src/utils/normalization.d.ts +49 -0
  37. package/dist/src/utils/normalization.js +173 -0
  38. package/dist/src/utils/normalization.js.map +1 -0
  39. package/dist/src/utils/projectRoot.d.ts +12 -5
  40. package/dist/src/utils/projectRoot.js +22 -20
  41. package/dist/src/utils/projectRoot.js.map +1 -1
  42. package/dist/src/utils/secureValidation.d.ts +15 -0
  43. package/dist/src/utils/secureValidation.js +41 -0
  44. package/dist/src/utils/secureValidation.js.map +1 -0
  45. package/dist/src/utils/transactionManager.d.ts +45 -0
  46. package/dist/src/utils/transactionManager.js +102 -0
  47. package/dist/src/utils/transactionManager.js.map +1 -0
  48. package/dist/src/utils/validation.d.ts +87 -0
  49. package/dist/src/utils/validation.js +231 -0
  50. package/dist/src/utils/validation.js.map +1 -0
  51. package/docs/ARCHITECTURE.md +300 -0
  52. package/docs/architecture/ADR-001-modular-handlers.md +119 -0
  53. package/package.json +5 -5
  54. package/AGENT_INSTRUCTIONS.md +0 -135
  55. package/DEPLOYMENT_INSTRUCTIONS.md +0 -110
  56. package/MEJORAS_PROPUESTAS.md +0 -324
  57. package/dist/src/tests/test-confirmation-flow.d.ts +0 -1
  58. package/dist/src/tests/test-confirmation-flow.js +0 -43
  59. package/dist/src/tests/test-confirmation-flow.js.map +0 -1
  60. package/dist/tests/debug-replace.d.ts +0 -1
  61. package/dist/tests/debug-replace.js +0 -17
  62. package/dist/tests/debug-replace.js.map +0 -1
  63. package/dist/tests/test-backup-system.d.ts +0 -7
  64. package/dist/tests/test-backup-system.js +0 -152
  65. package/dist/tests/test-backup-system.js.map +0 -1
  66. package/dist/tests/test-confirmation-flow.d.ts +0 -1
  67. package/dist/tests/test-confirmation-flow.js +0 -43
  68. package/dist/tests/test-confirmation-flow.js.map +0 -1
  69. package/dist/tests/test-dart.d.ts +0 -1
  70. package/dist/tests/test-dart.js +0 -242
  71. package/dist/tests/test-dart.js.map +0 -1
  72. package/dist/tests/test-new-tools.d.ts +0 -4
  73. package/dist/tests/test-new-tools.js +0 -147
  74. package/dist/tests/test-new-tools.js.map +0 -1
  75. package/dist/tests/test-php-ast.d.ts +0 -1
  76. package/dist/tests/test-php-ast.js +0 -152
  77. package/dist/tests/test-php-ast.js.map +0 -1
  78. package/dist/tests/test-php.d.ts +0 -1
  79. package/dist/tests/test-php.js +0 -234
  80. package/dist/tests/test-php.js.map +0 -1
  81. package/dist/tests/test-smoke.d.ts +0 -5
  82. package/dist/tests/test-smoke.js +0 -83
  83. package/dist/tests/test-smoke.js.map +0 -1
  84. package/dist/tests/writers/test-crlf-handling.d.ts +0 -4
  85. package/dist/tests/writers/test-crlf-handling.js +0 -73
  86. package/dist/tests/writers/test-crlf-handling.js.map +0 -1
  87. package/dist/tests/writers/test-write-smoke.d.ts +0 -9
  88. package/dist/tests/writers/test-write-smoke.js +0 -346
  89. package/dist/tests/writers/test-write-smoke.js.map +0 -1
  90. package/src/ast/dartCompressor.ts +0 -1013
  91. package/src/ast/phpCompressor.ts +0 -814
  92. package/src/ast/semanticCompressor.ts +0 -808
  93. package/src/ast/writers/dartWriter.ts +0 -648
  94. package/src/ast/writers/phpWriter.ts +0 -508
  95. package/src/ast/writers/pyWriter.ts +0 -446
  96. package/src/ast/writers/symbolWriter.ts +0 -277
  97. package/src/ast/writers/tsWriter.ts +0 -418
  98. package/src/index.ts +0 -1784
  99. package/src/tools/readFileLines.ts +0 -104
  100. package/src/tools/searchCodePattern.ts +0 -129
  101. package/src/utils/backupManager.ts +0 -141
  102. package/src/utils/confirmationCache.ts +0 -61
  103. package/src/utils/diffEngine.ts +0 -223
  104. package/src/utils/fuzzyMatch.ts +0 -86
  105. package/src/utils/ignoreManager.ts +0 -144
  106. package/src/utils/projectRoot.ts +0 -44
  107. package/tests/test-backup-system.ts +0 -174
  108. package/tests/test-confirmation-flow.ts +0 -49
  109. package/tests/test-dart.d.ts +0 -1
  110. package/tests/test-dart.js +0 -209
  111. package/tests/test-dart.js.map +0 -1
  112. package/tests/test-dart.ts +0 -265
  113. package/tests/test-new-tools.ts +0 -165
  114. package/tests/test-php-ast.d.ts +0 -1
  115. package/tests/test-php-ast.js +0 -152
  116. package/tests/test-php-ast.js.map +0 -1
  117. package/tests/test-php-ast.ts +0 -155
  118. package/tests/test-php.d.ts +0 -1
  119. package/tests/test-php.js +0 -234
  120. package/tests/test-php.js.map +0 -1
  121. package/tests/test-php.ts +0 -261
  122. package/tests/test-smoke.d.ts +0 -5
  123. package/tests/test-smoke.js +0 -83
  124. package/tests/test-smoke.js.map +0 -1
  125. package/tests/test-smoke.ts +0 -93
  126. package/tests/writers/test-crlf-handling.ts +0 -100
  127. package/tests/writers/test-write-smoke.ts +0 -568
  128. package/tsconfig.json +0 -19
@@ -1,446 +0,0 @@
1
- /**
2
- * pyWriter.ts — Python Surgical Writer
3
- *
4
- * Uses indentation-based block detection to locate symbol boundaries
5
- * in Python files. Python's significant whitespace makes this different
6
- * from brace-delimited languages — we detect block ends by watching
7
- * for dedentation back to the original level.
8
- *
9
- * Supports: .py, .pyi
10
- */
11
-
12
- // ─── Types ──────────────────────────────────────────────────────────
13
-
14
- export interface WriteResult {
15
- success: boolean;
16
- newContent: string;
17
- symbolsAffected: string[];
18
- error?: string;
19
- }
20
-
21
- export type InsertPosition = "before" | "after" | "inside_start" | "inside_end";
22
-
23
- // ─── Replace Symbol ─────────────────────────────────────────────────
24
-
25
- /**
26
- * Replace the full text of a named symbol in a Python file.
27
- * The newContent should include the def/class signature and body.
28
- * When className is provided, scopes the search to within that class only.
29
- */
30
- export function replacePySymbol(
31
- content: string,
32
- symbolName: string,
33
- newContent: string,
34
- className?: string,
35
- ): WriteResult {
36
- const lines = content.split("\n");
37
- const range = findSymbolRange(lines, symbolName, className);
38
-
39
- if (!range) {
40
- return {
41
- success: false,
42
- newContent: content,
43
- symbolsAffected: [],
44
- error: className
45
- ? `Symbol "${symbolName}" not found in class "${className}" in Python file`
46
- : `Symbol "${symbolName}" not found in Python file`,
47
- };
48
- }
49
-
50
- const { start, end } = range;
51
-
52
- // Detect indentation of original
53
- const originalIndent = getIndent(lines[start]);
54
- const reindented = reindentPython(newContent.trim(), originalIndent);
55
-
56
- // Splice
57
- const before = lines.slice(0, start);
58
- const after = lines.slice(end + 1);
59
- const result = [...before, reindented, ...after].join("\n");
60
-
61
- return {
62
- success: true,
63
- newContent: result,
64
- symbolsAffected: [symbolName],
65
- };
66
- }
67
-
68
- // ─── Insert Code ────────────────────────────────────────────────────
69
-
70
- /**
71
- * Insert new code at a precise location relative to an anchor symbol.
72
- * When className is provided, scopes the anchor search to within that class only.
73
- */
74
- export function insertPyCode(
75
- content: string,
76
- code: string,
77
- anchorSymbol: string | null,
78
- position: InsertPosition,
79
- className?: string,
80
- ): WriteResult {
81
- const lines = content.split("\n");
82
-
83
- if (!anchorSymbol) {
84
- // No anchor — append at end of file
85
- lines.push("", "", code.trim(), "");
86
- return {
87
- success: true,
88
- newContent: lines.join("\n"),
89
- symbolsAffected: [],
90
- };
91
- }
92
-
93
- const range = findSymbolRange(lines, anchorSymbol, className);
94
- if (!range) {
95
- return {
96
- success: false,
97
- newContent: content,
98
- symbolsAffected: [],
99
- error: className
100
- ? `Anchor symbol "${anchorSymbol}" not found in class "${className}"`
101
- : `Anchor symbol "${anchorSymbol}" not found`,
102
- };
103
- }
104
-
105
- const { start, end } = range;
106
- const indent = getIndent(lines[start]);
107
-
108
- switch (position) {
109
- case "before": {
110
- const reindented = reindentPython(code.trim(), indent);
111
- // Add proper spacing (PEP 8: two blank lines before top-level, one inside class)
112
- const spacing = indent === "" ? "\n\n" : "\n";
113
- lines.splice(start, 0, reindented + spacing);
114
- break;
115
- }
116
- case "after": {
117
- const reindented = reindentPython(code.trim(), indent);
118
- const spacing = indent === "" ? "\n\n" : "\n";
119
- lines.splice(end + 1, 0, spacing + reindented);
120
- break;
121
- }
122
- case "inside_start": {
123
- // Find the colon line (class Foo:) and insert after it
124
- const colonLine = findColonLine(lines, start);
125
- if (colonLine === -1) {
126
- return {
127
- success: false,
128
- newContent: content,
129
- symbolsAffected: [],
130
- error: `Cannot find class body start for "${anchorSymbol}"`,
131
- };
132
- }
133
- // Check if there's a docstring, insert after it
134
- const insertAt = skipDocstring(lines, colonLine + 1);
135
- const innerIndent = indent + " "; // Python convention: 4-space indent
136
- const innerCode = reindentPython(code.trim(), innerIndent);
137
- lines.splice(insertAt, 0, innerCode, "");
138
- break;
139
- }
140
- case "inside_end": {
141
- const innerIndent = indent + " ";
142
- const innerCode = reindentPython(code.trim(), innerIndent);
143
- lines.splice(end + 1, 0, "", innerCode);
144
- break;
145
- }
146
- }
147
-
148
- return {
149
- success: true,
150
- newContent: lines.join("\n"),
151
- symbolsAffected: [anchorSymbol],
152
- };
153
- }
154
-
155
- // ─── Rename Symbol ──────────────────────────────────────────────────
156
-
157
- /**
158
- * Rename all occurrences of a symbol within a Python file.
159
- */
160
- export function renamePySymbol(
161
- content: string,
162
- oldName: string,
163
- newName: string,
164
- ): WriteResult {
165
- const lines = content.split("\n");
166
- const range = findSymbolRange(lines, oldName);
167
-
168
- if (!range) {
169
- return {
170
- success: false,
171
- newContent: content,
172
- symbolsAffected: [],
173
- error: `Symbol "${oldName}" not found for renaming`,
174
- };
175
- }
176
-
177
- // Whole-word replacement
178
- const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
179
- const regex = new RegExp(`\\b${escaped}\\b`, "g");
180
- const result = content.replace(regex, newName);
181
-
182
- return {
183
- success: true,
184
- newContent: result,
185
- symbolsAffected: [oldName],
186
- };
187
- }
188
-
189
- // ─── Remove Symbol ──────────────────────────────────────────────────
190
-
191
- /**
192
- * Remove a named symbol from a Python file.
193
- * When className is provided, scopes the search to within that class only.
194
- */
195
- export function removePySymbol(
196
- content: string,
197
- symbolName: string,
198
- className?: string,
199
- ): WriteResult {
200
- const lines = content.split("\n");
201
- const range = findSymbolRange(lines, symbolName, className);
202
-
203
- if (!range) {
204
- return {
205
- success: false,
206
- newContent: content,
207
- symbolsAffected: [],
208
- error: className
209
- ? `Symbol "${symbolName}" not found in class "${className}" for removal`
210
- : `Symbol "${symbolName}" not found for removal`,
211
- };
212
- }
213
-
214
- // Also remove leading decorators
215
- let actualStart = range.start;
216
- while (actualStart > 0 && lines[actualStart - 1].trim().startsWith("@")) {
217
- actualStart--;
218
- }
219
-
220
- lines.splice(actualStart, range.end - actualStart + 1);
221
-
222
- let result = lines.join("\n");
223
- result = result.replace(/\n{3,}/g, "\n\n");
224
-
225
- return {
226
- success: true,
227
- newContent: result,
228
- symbolsAffected: [symbolName],
229
- };
230
- }
231
-
232
- // ─── Symbol Finder ──────────────────────────────────────────────────
233
-
234
- interface SymbolRange {
235
- start: number; // 0-indexed line
236
- end: number; // 0-indexed line (inclusive)
237
- }
238
-
239
- /**
240
- * Find the start and end line indices of a named symbol in a Python file.
241
- * Uses indentation to detect block boundaries (Python's significant whitespace).
242
- * When className is provided, scopes the search to within that class only.
243
- */
244
- function findSymbolRange(
245
- lines: string[],
246
- symbolName: string,
247
- className?: string,
248
- ): SymbolRange | null {
249
- const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
250
-
251
- // If className is provided, find the class first, then search within it
252
- if (className) {
253
- const escapedClass = className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
254
-
255
- for (let i = 0; i < lines.length; i++) {
256
- const trimmed = lines[i].trim();
257
- if (!new RegExp(`^class\\s+${escapedClass}[\\s(:]`).test(trimmed)) continue;
258
-
259
- // Found the class — search for the method within it
260
- const classIndent = measureIndent(lines[i]);
261
- let j = i + 1;
262
-
263
- while (j < lines.length) {
264
- const bodyLine = lines[j];
265
- if (bodyLine.trim() === "") { j++; continue; }
266
- if (measureIndent(bodyLine) <= classIndent && bodyLine.trim() !== "") break;
267
-
268
- const bodyTrimmed = bodyLine.trim();
269
- const isFunc = new RegExp(
270
- `^(?:async\\s+)?def\\s+${escaped}\\s*\\(`,
271
- ).test(bodyTrimmed);
272
-
273
- if (isFunc) {
274
- const startLine = j;
275
- const baseIndent = measureIndent(bodyLine);
276
-
277
- // Check for decorators above
278
- let decoratorStart = startLine;
279
- while (
280
- decoratorStart > 0 &&
281
- lines[decoratorStart - 1].trim().startsWith("@")
282
- ) {
283
- decoratorStart--;
284
- }
285
-
286
- // Find end of block
287
- let endLine = startLine;
288
- let k = startLine + 1;
289
- while (k < lines.length) {
290
- const innerLine = lines[k];
291
- if (innerLine.trim() === "") { k++; continue; }
292
- if (measureIndent(innerLine) > baseIndent) { endLine = k; k++; }
293
- else break;
294
- }
295
-
296
- while (endLine > startLine && lines[endLine].trim() === "") {
297
- endLine--;
298
- }
299
-
300
- return { start: decoratorStart, end: endLine };
301
- }
302
- j++;
303
- }
304
- return null; // Method not found within class
305
- }
306
- return null; // Class not found
307
- }
308
-
309
- // Original behavior when no className is provided
310
- for (let i = 0; i < lines.length; i++) {
311
- const line = lines[i];
312
- const trimmed = line.trim();
313
-
314
- const isClass = new RegExp(`^class\\s+${escaped}[\\s(:]`).test(trimmed);
315
- const isFunc = new RegExp(
316
- `^(?:async\\s+)?def\\s+${escaped}\\s*\\(`,
317
- ).test(trimmed);
318
-
319
- if (!isClass && !isFunc) continue;
320
-
321
- const startLine = i;
322
- const baseIndent = measureIndent(line);
323
-
324
- // Also check for decorators above
325
- let decoratorStart = startLine;
326
- while (
327
- decoratorStart > 0 &&
328
- lines[decoratorStart - 1].trim().startsWith("@")
329
- ) {
330
- decoratorStart--;
331
- }
332
-
333
- // Find the end of the block
334
- let endLine = startLine;
335
- let j = startLine + 1;
336
-
337
- while (j < lines.length) {
338
- const bodyLine = lines[j];
339
- if (bodyLine.trim() === "") {
340
- j++;
341
- continue;
342
- }
343
- if (measureIndent(bodyLine) > baseIndent) {
344
- endLine = j;
345
- j++;
346
- } else {
347
- break;
348
- }
349
- }
350
-
351
- // Trim trailing blank lines from the block
352
- while (endLine > startLine && lines[endLine].trim() === "") {
353
- endLine--;
354
- }
355
-
356
- return { start: decoratorStart, end: endLine };
357
- }
358
-
359
- return null;
360
- }
361
-
362
- // ─── Utility Functions ──────────────────────────────────────────────
363
-
364
- function measureIndent(line: string): number {
365
- const match = line.match(/^(\s*)/);
366
- return match ? match[1].length : 0;
367
- }
368
-
369
- function getIndent(line: string): string {
370
- const match = line.match(/^(\s*)/);
371
- return match ? match[1] : "";
372
- }
373
-
374
- /**
375
- * Re-indent a block of Python code to match a target indentation level.
376
- * Preserves relative indentation within the block.
377
- */
378
- function reindentPython(code: string, targetIndent: string): string {
379
- const lines = code.split("\n");
380
- if (lines.length === 0) return code;
381
-
382
- // Find minimum non-empty indentation
383
- let minIndent = Infinity;
384
- for (const line of lines) {
385
- if (line.trim() === "") continue;
386
- const indent = measureIndent(line);
387
- if (indent < minIndent) minIndent = indent;
388
- }
389
-
390
- if (minIndent === Infinity) minIndent = 0;
391
-
392
- return lines
393
- .map((line) => {
394
- if (line.trim() === "") return "";
395
- return targetIndent + line.slice(minIndent);
396
- })
397
- .join("\n");
398
- }
399
-
400
- /**
401
- * Find the line containing the colon that starts a class/function body.
402
- */
403
- function findColonLine(lines: string[], startIdx: number): number {
404
- for (let i = startIdx; i < lines.length; i++) {
405
- if (lines[i].includes(":")) return i;
406
- }
407
- return -1;
408
- }
409
-
410
- /**
411
- * Skip past a docstring (if present) to find the first line of actual code.
412
- */
413
- function skipDocstring(lines: string[], startIdx: number): number {
414
- let i = startIdx;
415
-
416
- // Skip blank lines
417
- while (i < lines.length && lines[i].trim() === "") i++;
418
-
419
- if (i >= lines.length) return startIdx;
420
-
421
- const trimmed = lines[i].trim();
422
-
423
- // Look for triple-quoted docstring
424
- if (
425
- trimmed.startsWith('"""') ||
426
- trimmed.startsWith("'''")
427
- ) {
428
- const quote = trimmed.slice(0, 3);
429
-
430
- // Single-line docstring
431
- if (trimmed.endsWith(quote) && trimmed.length > 3) {
432
- return i + 1;
433
- }
434
-
435
- // Multi-line docstring
436
- i++;
437
- while (i < lines.length) {
438
- if (lines[i].trim().endsWith(quote)) {
439
- return i + 1;
440
- }
441
- i++;
442
- }
443
- }
444
-
445
- return startIdx;
446
- }
@@ -1,277 +0,0 @@
1
- /**
2
- * symbolWriter.ts — Central Dispatcher for Surgical Write Operations
3
- *
4
- * Routes write operations to the correct language-specific writer based
5
- * on file extension. This is the "write" counterpart to semanticCompressor.ts.
6
- *
7
- * Supports:
8
- * - TypeScript/JavaScript (.ts, .tsx, .js, .jsx, .mts, .mjs, .cts, .cjs)
9
- * - PHP (.php, .phtml)
10
- * - Dart (.dart)
11
- * - Python (.py, .pyi)
12
- */
13
-
14
- import * as path from "node:path";
15
-
16
- import {
17
- replaceTSSymbol,
18
- insertTSCode,
19
- renameTSSymbol,
20
- removeTSSymbol,
21
- type WriteResult,
22
- type InsertPosition,
23
- } from "./tsWriter.js";
24
-
25
- import {
26
- replacePHPSymbol,
27
- insertPHPCode,
28
- renamePHPSymbol,
29
- removePHPSymbol,
30
- } from "./phpWriter.js";
31
-
32
- import {
33
- replaceDartSymbol,
34
- insertDartCode,
35
- renameDartSymbol,
36
- removeDartSymbol,
37
- } from "./dartWriter.js";
38
-
39
- import {
40
- replacePySymbol,
41
- insertPyCode,
42
- renamePySymbol,
43
- removePySymbol,
44
- } from "./pyWriter.js";
45
-
46
- // Re-export types for consumers
47
- export type { WriteResult, InsertPosition };
48
-
49
- // ─── Extension Sets ─────────────────────────────────────────────────
50
-
51
- const TS_EXTENSIONS = new Set([
52
- ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs",
53
- ]);
54
-
55
- const PHP_EXTENSIONS = new Set([".php", ".phtml"]);
56
-
57
- const DART_EXTENSIONS = new Set([".dart"]);
58
-
59
- const PY_EXTENSIONS = new Set([".py", ".pyi"]);
60
-
61
- /** All extensions supported for surgical writing */
62
- export const WRITABLE_EXTENSIONS = new Set([
63
- ...TS_EXTENSIONS,
64
- ...PHP_EXTENSIONS,
65
- ...DART_EXTENSIONS,
66
- ...PY_EXTENSIONS,
67
- ]);
68
-
69
- // ─── Replace Symbol ─────────────────────────────────────────────────
70
-
71
- /**
72
- * Replace the full source code of a named symbol in a file.
73
- * Dispatches to the correct language-specific writer.
74
- *
75
- * @param filePath - Used to detect language via extension
76
- * @param content - Current file content
77
- * @param symbolName - Name of the symbol to replace
78
- * @param newContent - Complete replacement code (signature + body)
79
- * @param className - Optional. Scopes search to within a specific class
80
- */
81
- export function replaceSymbol(
82
- filePath: string,
83
- content: string,
84
- symbolName: string,
85
- newContent: string,
86
- className?: string,
87
- ): WriteResult {
88
- const ext = path.extname(filePath).toLowerCase();
89
-
90
- if (TS_EXTENSIONS.has(ext)) {
91
- return replaceTSSymbol(content, symbolName, newContent, filePath, className);
92
- }
93
-
94
- if (PHP_EXTENSIONS.has(ext)) {
95
- return replacePHPSymbol(content, symbolName, newContent, className);
96
- }
97
-
98
- if (DART_EXTENSIONS.has(ext)) {
99
- return replaceDartSymbol(content, symbolName, newContent, className);
100
- }
101
-
102
- if (PY_EXTENSIONS.has(ext)) {
103
- return replacePySymbol(content, symbolName, newContent, className);
104
- }
105
-
106
- return {
107
- success: false,
108
- newContent: content,
109
- symbolsAffected: [],
110
- error: `Unsupported file type "${ext}" for surgical writing. Supported: TypeScript, JavaScript, PHP, Dart, Python.`,
111
- };
112
- }
113
-
114
- // ─── Insert Code ────────────────────────────────────────────────────
115
-
116
- /**
117
- * Insert new code at a precise location relative to an anchor symbol.
118
- *
119
- * @param filePath - Used to detect language via extension
120
- * @param content - Current file content
121
- * @param code - The code to insert
122
- * @param anchorSymbol - Symbol to position relative to (null = end of file)
123
- * @param position - Where to insert: before, after, inside_start, inside_end
124
- * @param className - Optional. Scopes anchor search to within a specific class
125
- */
126
- export function insertCode(
127
- filePath: string,
128
- content: string,
129
- code: string,
130
- anchorSymbol: string | null,
131
- position: InsertPosition,
132
- className?: string,
133
- ): WriteResult {
134
- const ext = path.extname(filePath).toLowerCase();
135
-
136
- if (TS_EXTENSIONS.has(ext)) {
137
- return insertTSCode(content, code, anchorSymbol, position, filePath, className);
138
- }
139
-
140
- if (PHP_EXTENSIONS.has(ext)) {
141
- return insertPHPCode(content, code, anchorSymbol, position, className);
142
- }
143
-
144
- if (DART_EXTENSIONS.has(ext)) {
145
- return insertDartCode(content, code, anchorSymbol, position, className);
146
- }
147
-
148
- if (PY_EXTENSIONS.has(ext)) {
149
- return insertPyCode(content, code, anchorSymbol, position, className);
150
- }
151
-
152
- return {
153
- success: false,
154
- newContent: content,
155
- symbolsAffected: [],
156
- error: `Unsupported file type "${ext}" for code insertion.`,
157
- };
158
- }
159
-
160
- // ─── Rename Symbol ──────────────────────────────────────────────────
161
-
162
- /**
163
- * Rename all occurrences of a symbol within a single file.
164
- *
165
- * @param filePath - Used to detect language via extension
166
- * @param content - Current file content
167
- * @param oldName - Current symbol name
168
- * @param newName - New symbol name
169
- */
170
- export function renameInFile(
171
- filePath: string,
172
- content: string,
173
- oldName: string,
174
- newName: string,
175
- ): WriteResult {
176
- const ext = path.extname(filePath).toLowerCase();
177
-
178
- if (TS_EXTENSIONS.has(ext)) {
179
- return renameTSSymbol(content, oldName, newName, filePath);
180
- }
181
-
182
- if (PHP_EXTENSIONS.has(ext)) {
183
- return renamePHPSymbol(content, oldName, newName);
184
- }
185
-
186
- if (DART_EXTENSIONS.has(ext)) {
187
- return renameDartSymbol(content, oldName, newName);
188
- }
189
-
190
- if (PY_EXTENSIONS.has(ext)) {
191
- return renamePySymbol(content, oldName, newName);
192
- }
193
-
194
- return {
195
- success: false,
196
- newContent: content,
197
- symbolsAffected: [],
198
- error: `Unsupported file type "${ext}" for symbol renaming.`,
199
- };
200
- }
201
-
202
- /**
203
- * Renames all whole-word occurrences of a symbol in a file.
204
- * UNLIKE renameInFile, this does NOT check for the symbol definition.
205
- * Used for updating references in dependent files.
206
- */
207
- export function renameReferencesInFile(
208
- filePath: string,
209
- content: string,
210
- oldName: string,
211
- newName: string,
212
- ): WriteResult {
213
- const ext = path.extname(filePath).toLowerCase();
214
-
215
- // Simple whole-word replacement is generally safe for cross-file reference updates
216
- // where we ALREADY know via import analysis that the file depends on this symbol.
217
-
218
- // Scape regex special chars
219
- const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
220
-
221
- // Use a regex that respects common identifier boundaries across JS/PHP/Dart/Python
222
- const regex = new RegExp(`(?<=^|[^a-zA-Z0-9_$])(${escaped})(?=[^a-zA-Z0-9_$]|$)`, "g");
223
-
224
- if (!regex.test(content)) {
225
- return { success: true, newContent: content, symbolsAffected: [] };
226
- }
227
-
228
- const result = content.replace(regex, newName);
229
-
230
- return {
231
- success: true,
232
- newContent: result,
233
- symbolsAffected: [oldName],
234
- };
235
- }
236
-
237
- // ─── Remove Symbol ──────────────────────────────────────────────────
238
-
239
- /**
240
- * Remove a named symbol from a file.
241
- *
242
- * @param filePath - Used to detect language via extension
243
- * @param content - Current file content
244
- * @param symbolName - Name of the symbol to remove
245
- * @param className - Optional. Scopes search to within a specific class
246
- */
247
- export function removeSymbolFromFile(
248
- filePath: string,
249
- content: string,
250
- symbolName: string,
251
- className?: string,
252
- ): WriteResult {
253
- const ext = path.extname(filePath).toLowerCase();
254
-
255
- if (TS_EXTENSIONS.has(ext)) {
256
- return removeTSSymbol(content, symbolName, filePath, className);
257
- }
258
-
259
- if (PHP_EXTENSIONS.has(ext)) {
260
- return removePHPSymbol(content, symbolName, className);
261
- }
262
-
263
- if (DART_EXTENSIONS.has(ext)) {
264
- return removeDartSymbol(content, symbolName, className);
265
- }
266
-
267
- if (PY_EXTENSIONS.has(ext)) {
268
- return removePySymbol(content, symbolName, className);
269
- }
270
-
271
- return {
272
- success: false,
273
- newContent: content,
274
- symbolsAffected: [],
275
- error: `Unsupported file type "${ext}" for symbol removal.`,
276
- };
277
- }