codeguard-testgen 1.0.14 → 1.0.16

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 (50) hide show
  1. package/README.md +157 -1034
  2. package/dist/ai.d.ts +8 -0
  3. package/dist/ai.d.ts.map +1 -0
  4. package/dist/ai.js +332 -0
  5. package/dist/ai.js.map +1 -0
  6. package/dist/ast.d.ts +8 -0
  7. package/dist/ast.d.ts.map +1 -0
  8. package/dist/ast.js +988 -0
  9. package/dist/ast.js.map +1 -0
  10. package/dist/config.d.ts +4 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +4 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/git.d.ts +18 -0
  15. package/dist/git.d.ts.map +1 -0
  16. package/dist/git.js +208 -0
  17. package/dist/git.js.map +1 -0
  18. package/dist/globals.d.ts +24 -0
  19. package/dist/globals.d.ts.map +1 -0
  20. package/dist/globals.js +40 -0
  21. package/dist/globals.js.map +1 -0
  22. package/dist/index.d.ts +9 -54
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +85 -5434
  25. package/dist/index.js.map +1 -1
  26. package/dist/pathResolver.d.ts +12 -0
  27. package/dist/pathResolver.d.ts.map +1 -0
  28. package/dist/pathResolver.js +44 -0
  29. package/dist/pathResolver.js.map +1 -0
  30. package/dist/reviewer.d.ts +13 -0
  31. package/dist/reviewer.d.ts.map +1 -0
  32. package/dist/reviewer.js +402 -0
  33. package/dist/reviewer.js.map +1 -0
  34. package/dist/testGenerator.d.ts +24 -0
  35. package/dist/testGenerator.d.ts.map +1 -0
  36. package/dist/testGenerator.js +1107 -0
  37. package/dist/testGenerator.js.map +1 -0
  38. package/dist/toolDefinitions.d.ts +6 -0
  39. package/dist/toolDefinitions.d.ts.map +1 -0
  40. package/dist/toolDefinitions.js +370 -0
  41. package/dist/toolDefinitions.js.map +1 -0
  42. package/dist/toolHandlers.d.ts +76 -0
  43. package/dist/toolHandlers.d.ts.map +1 -0
  44. package/dist/toolHandlers.js +1430 -0
  45. package/dist/toolHandlers.js.map +1 -0
  46. package/dist/types.d.ts +74 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +3 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +1 -2
@@ -0,0 +1,1430 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readFile = readFile;
4
+ exports.readFileLines = readFileLines;
5
+ exports.resolveImportPath = resolveImportPath;
6
+ exports.validateTestFileSyntax = validateTestFileSyntax;
7
+ exports.writeTestFile = writeTestFile;
8
+ exports.editTestFile = editTestFile;
9
+ exports.replaceFunctionTests = replaceFunctionTests;
10
+ exports.stripAnsi = stripAnsi;
11
+ exports.runTestsVitest = runTestsVitest;
12
+ exports.runTestsJest = runTestsJest;
13
+ exports.runTestsIsolated = runTestsIsolated;
14
+ exports.parseFailingTestNames = parseFailingTestNames;
15
+ exports.listDirectory = listDirectory;
16
+ exports.findFile = findFile;
17
+ exports.calculateRelativePath = calculateRelativePath;
18
+ exports.parseMultiplePaths = parseMultiplePaths;
19
+ exports.normalizePath = normalizePath;
20
+ exports.calculateSingleRelativePath = calculateSingleRelativePath;
21
+ exports.deleteLines = deleteLines;
22
+ exports.insertLines = insertLines;
23
+ exports.replaceLines = replaceLines;
24
+ exports.searchReplaceBlock = searchReplaceBlock;
25
+ exports.insertAtPosition = insertAtPosition;
26
+ exports.writeReview = writeReview;
27
+ exports.searchCodebase = searchCodebase;
28
+ exports.executeTool = executeTool;
29
+ const fs = require("fs/promises");
30
+ const fsSync = require("fs");
31
+ const path = require("path");
32
+ const child_process_1 = require("child_process");
33
+ const babelParser = require("@babel/parser");
34
+ const globals_1 = require("./globals");
35
+ const pathResolver_1 = require("./pathResolver");
36
+ const ast_1 = require("./ast");
37
+ const fuzzyMatcher_1 = require("./fuzzyMatcher");
38
+ const toolDefinitions_1 = require("./toolDefinitions");
39
+ // ---------------------------------------------------------------------------
40
+ // File reading
41
+ // ---------------------------------------------------------------------------
42
+ async function readFile(filePath) {
43
+ const MAX_LINES = 1000;
44
+ const tryReadFile = async (targetPath) => {
45
+ try {
46
+ const content = await fs.readFile(targetPath, 'utf-8');
47
+ const lines = content.split('\n');
48
+ if (lines.length > MAX_LINES) {
49
+ const isTestFile = /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(targetPath);
50
+ if (globals_1.g.globalIndexer) {
51
+ const cached = globals_1.g.globalIndexer.getFileAnalysis(targetPath);
52
+ if (cached) {
53
+ let preamble = undefined;
54
+ if (isTestFile) {
55
+ const preambleResult = (0, ast_1.getFilePreamble)(targetPath);
56
+ if (preambleResult.success) {
57
+ preamble = preambleResult;
58
+ }
59
+ }
60
+ return {
61
+ success: true,
62
+ usedCache: true,
63
+ fileSize: lines.length,
64
+ filePath: targetPath,
65
+ analysis: cached,
66
+ preamble: preamble,
67
+ message: isTestFile
68
+ ? `Test file has ${lines.length} lines (max ${MAX_LINES}). Returned cached analysis + preamble (imports/mocks/setup). Use get_function_ast with proper file path for specific test functions.`
69
+ : `File has ${lines.length} lines (max ${MAX_LINES}). Returned cached analysis + preamble (imports/setup). Use read_file_lines to read specific section of file or get_function_ast with proper file path for specific functions.`
70
+ };
71
+ }
72
+ }
73
+ return {
74
+ success: false,
75
+ error: `File too large (${lines.length} lines, max ${MAX_LINES}). Use get_file_preamble for imports/mocks, analyze_file_ast for structure, or get_function_ast for specific functions or read_file_lines to read specific section of file.`,
76
+ fileSize: lines.length,
77
+ filePath: targetPath,
78
+ suggestion: 'For large files: 1) get_file_preamble for imports/mocks/setup, 2) analyze_file_ast for all functions, 3) get_function_ast for specific functions'
79
+ };
80
+ }
81
+ return { success: true, content, filePath: targetPath, fileSize: lines.length };
82
+ }
83
+ catch (error) {
84
+ return null;
85
+ }
86
+ };
87
+ const result = await tryReadFile(filePath);
88
+ if (result) {
89
+ return result;
90
+ }
91
+ if (globals_1.g.globalIndexer) {
92
+ const indexData = globals_1.g.globalIndexer.index;
93
+ if (indexData && indexData.files) {
94
+ const match = (0, pathResolver_1.findBestMatchInIndex)(filePath, indexData.files);
95
+ if (match) {
96
+ const retryResult = await tryReadFile(match.suggestion);
97
+ if (retryResult) {
98
+ return retryResult;
99
+ }
100
+ return {
101
+ success: false,
102
+ error: `Found file at ${match.suggestion} but failed to read it`,
103
+ suggestion: match.suggestion,
104
+ allMatches: match.allMatches.length > 1 ? match.allMatches : undefined
105
+ };
106
+ }
107
+ }
108
+ return {
109
+ success: false,
110
+ error: `File not found: ${filePath}. No matching files found in index.`
111
+ };
112
+ }
113
+ return {
114
+ success: false,
115
+ error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
116
+ };
117
+ }
118
+ /**
119
+ * Read specific lines from a file
120
+ * Useful for examining syntax errors, bracket mismatches, or specific sections
121
+ */
122
+ async function readFileLines(filePath, startLine, endLine) {
123
+ if (startLine < 1) {
124
+ return {
125
+ success: false,
126
+ error: 'Start line must be >= 1'
127
+ };
128
+ }
129
+ if (endLine < startLine) {
130
+ return {
131
+ success: false,
132
+ error: 'End line must be >= start line'
133
+ };
134
+ }
135
+ if (endLine - startLine > 2000) {
136
+ return {
137
+ success: false,
138
+ error: 'You are not allowed to read more than 2000 lines at once. End line - start line must be less than 2000'
139
+ };
140
+ }
141
+ const tryReadFileLines = async (targetPath) => {
142
+ try {
143
+ const content = await fs.readFile(targetPath, 'utf-8');
144
+ const allLines = content.split('\n');
145
+ if (startLine > allLines.length) {
146
+ return {
147
+ success: false,
148
+ error: `Start line ${startLine} exceeds file length (${allLines.length} lines)`,
149
+ fileLength: allLines.length,
150
+ filePath: targetPath
151
+ };
152
+ }
153
+ const actualEndLine = Math.min(endLine, allLines.length);
154
+ const requestedLines = allLines.slice(startLine - 1, actualEndLine);
155
+ const formattedLines = requestedLines.map((line, index) => {
156
+ const lineNumber = startLine + index;
157
+ return `${lineNumber.toString().padStart(6, ' ')}|${line}`;
158
+ }).join('\n');
159
+ return {
160
+ success: true,
161
+ filePath: targetPath,
162
+ startLine,
163
+ endLine: actualEndLine,
164
+ lineCount: requestedLines.length,
165
+ content: formattedLines,
166
+ rawLines: requestedLines
167
+ };
168
+ }
169
+ catch (error) {
170
+ return null;
171
+ }
172
+ };
173
+ const result = await tryReadFileLines(filePath);
174
+ if (result) {
175
+ return result;
176
+ }
177
+ if (globals_1.g.globalIndexer) {
178
+ const indexData = globals_1.g.globalIndexer.index;
179
+ if (indexData && indexData.files) {
180
+ const match = (0, pathResolver_1.findBestMatchInIndex)(filePath, indexData.files);
181
+ if (match) {
182
+ const retryResult = await tryReadFileLines(match.suggestion);
183
+ if (retryResult) {
184
+ return retryResult;
185
+ }
186
+ return {
187
+ success: false,
188
+ error: `Found file at ${match.suggestion} but failed to read it`,
189
+ suggestion: match.suggestion,
190
+ allMatches: match.allMatches.length > 1 ? match.allMatches : undefined
191
+ };
192
+ }
193
+ }
194
+ return {
195
+ success: false,
196
+ error: `File not found: ${filePath}. No matching files found in index.`
197
+ };
198
+ }
199
+ return {
200
+ success: false,
201
+ error: `File not found: ${filePath}. Tip: Enable indexing for better file lookup.`
202
+ };
203
+ }
204
+ function resolveImportPath(fromFile, importPath) {
205
+ try {
206
+ if (importPath.startsWith('.')) {
207
+ const dir = path.dirname(fromFile);
208
+ const resolved = path.resolve(dir, importPath);
209
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js'];
210
+ for (const ext of extensions) {
211
+ const withExt = resolved + ext;
212
+ if (fsSync.existsSync(withExt)) {
213
+ return { success: true, resolvedPath: withExt };
214
+ }
215
+ }
216
+ if (fsSync.existsSync(resolved)) {
217
+ return { success: true, resolvedPath: resolved };
218
+ }
219
+ }
220
+ return {
221
+ success: true,
222
+ resolvedPath: importPath,
223
+ isExternal: true
224
+ };
225
+ }
226
+ catch (error) {
227
+ return { success: false, error: error.message };
228
+ }
229
+ }
230
+ /**
231
+ * Validate test file syntax using AST parsing
232
+ * Provides detailed error information for AI to retry
233
+ */
234
+ async function validateTestFileSyntax(filePath) {
235
+ try {
236
+ const content = await fs.readFile(filePath, 'utf-8');
237
+ babelParser.parse(content, {
238
+ sourceType: 'module',
239
+ plugins: [
240
+ 'typescript',
241
+ 'jsx',
242
+ 'decorators-legacy'
243
+ ]
244
+ });
245
+ return { valid: true };
246
+ }
247
+ catch (error) {
248
+ const line = error.loc?.line || error.lineNumber || 0;
249
+ const column = error.loc?.column || error.column || 0;
250
+ let errorMessage = error.message || 'Unknown syntax error';
251
+ errorMessage = errorMessage.replace(/^.*?: /, '');
252
+ let suggestion = '';
253
+ if (errorMessage.includes('Unexpected token') || errorMessage.includes('unexpected')) {
254
+ suggestion = 'Check for missing/extra brackets, parentheses, or commas near this location.';
255
+ }
256
+ else if (errorMessage.includes('Unterminated')) {
257
+ suggestion = 'You have an unclosed string, comment, or template literal.';
258
+ }
259
+ else if (errorMessage.includes('Expected')) {
260
+ suggestion = 'Missing required syntax element. Check if you closed all blocks properly.';
261
+ }
262
+ else if (errorMessage.includes('duplicate') || errorMessage.includes('already')) {
263
+ suggestion = 'Duplicate declaration detected. Check for repeated variable/function names.';
264
+ }
265
+ else {
266
+ suggestion = 'Review the code structure around this line for syntax issues.';
267
+ }
268
+ return {
269
+ valid: false,
270
+ error: errorMessage,
271
+ location: { line, column },
272
+ suggestion
273
+ };
274
+ }
275
+ }
276
+ async function writeTestFile(filePath, content, sourceFilePath, functionMode = false) {
277
+ try {
278
+ const invalidPatterns = [
279
+ /\/\/\s*(Mock setup|Assertions|Call function|Add test|Further test|Additional test)/i,
280
+ /\/\/\s*(Add more|write more|Similarly|write tests for)/i,
281
+ /\/\/\s*TODO/i,
282
+ /\/\/\s*\.\.\./,
283
+ /\/\/.*etc\./i,
284
+ /expect\(\).*\/\//,
285
+ ];
286
+ const hasPlaceholders = invalidPatterns.some(pattern => pattern.test(content));
287
+ if (hasPlaceholders) {
288
+ const foundPlaceholder = content.match(invalidPatterns.find(p => p.test(content)) || /\/\/.*/);
289
+ return {
290
+ success: false,
291
+ error: `REJECTED: Test file contains placeholder comment: "${foundPlaceholder?.[0]}"\n\nYou must write COMPLETE tests with actual code for ALL functions, not comments like "// Add more tests", "// Similarly", "// Further tests". Write the FULL implementation for EVERY function!`
292
+ };
293
+ }
294
+ const expectCount = (content.match(/expect\(/g) || []).length;
295
+ const testCount = (content.match(/test\(|it\(/g) || []).length;
296
+ const describeCount = (content.match(/describe\(/g) || []).length;
297
+ if (testCount > 0 && expectCount === 0) {
298
+ return {
299
+ success: false,
300
+ error: 'REJECTED: Test file has test cases but NO expect() assertions! Every test MUST have at least one expect() statement. Write actual assertions!'
301
+ };
302
+ }
303
+ let expectedFunctionCount = 3;
304
+ if (!functionMode) {
305
+ if (sourceFilePath && fsSync.existsSync(sourceFilePath)) {
306
+ const analysis = (0, ast_1.analyzeFileAST)(sourceFilePath);
307
+ if (analysis.success) {
308
+ const exportedFunctions = analysis.analysis.functions.filter((f) => f.exported);
309
+ expectedFunctionCount = exportedFunctions.length;
310
+ if (describeCount < expectedFunctionCount) {
311
+ return {
312
+ success: false,
313
+ error: `REJECTED: Source file has ${expectedFunctionCount} exported functions but test file only has ${describeCount} describe blocks!\n\nMissing tests for: ${exportedFunctions.slice(describeCount).map((f) => f.name).join(', ')}\n\nWrite a describe block with tests for EVERY function!`
314
+ };
315
+ }
316
+ }
317
+ }
318
+ }
319
+ const dir = path.dirname(filePath);
320
+ await fs.mkdir(dir, { recursive: true });
321
+ await fs.writeFile(filePath, content, 'utf-8');
322
+ return {
323
+ success: true,
324
+ path: filePath,
325
+ stats: {
326
+ tests: testCount,
327
+ expectations: expectCount
328
+ }
329
+ };
330
+ }
331
+ catch (error) {
332
+ return { success: false, error: error.message };
333
+ }
334
+ }
335
+ async function editTestFile(filePath, oldContent, newContent) {
336
+ try {
337
+ const dir = path.dirname(filePath);
338
+ await fs.mkdir(dir, { recursive: true });
339
+ let content = await fs.readFile(filePath, 'utf-8');
340
+ const normalizeWhitespace = (str) => str.replace(/\s+/g, ' ').trim();
341
+ const normalizedContent = normalizeWhitespace(content);
342
+ const normalizedOld = normalizeWhitespace(oldContent);
343
+ if (content.includes(oldContent)) {
344
+ content = content.replace(oldContent, newContent);
345
+ await fs.writeFile(filePath, content, 'utf-8');
346
+ return { success: true, message: 'File edited successfully (exact match)' };
347
+ }
348
+ if (normalizedContent.includes(normalizedOld)) {
349
+ const lines = content.split('\n');
350
+ const oldLines = oldContent.split('\n');
351
+ for (let i = 0; i <= lines.length - oldLines.length; i++) {
352
+ const section = lines.slice(i, i + oldLines.length).join('\n');
353
+ if (normalizeWhitespace(section) === normalizedOld) {
354
+ lines.splice(i, oldLines.length, newContent);
355
+ content = lines.join('\n');
356
+ await fs.writeFile(filePath, content, 'utf-8');
357
+ return { success: true, message: 'File edited successfully (normalized match)' };
358
+ }
359
+ }
360
+ }
361
+ if (!oldContent || oldContent.trim().length < 10) {
362
+ await fs.writeFile(filePath, newContent, 'utf-8');
363
+ return { success: true, message: 'File overwritten (old content was empty/short)' };
364
+ }
365
+ const preview = content.substring(0, 500);
366
+ return {
367
+ success: false,
368
+ error: `Old content not found. Current file preview:\n${preview}\n...\n\nHint: Use search_replace_block with more context to find the correct location.`,
369
+ currentContent: preview
370
+ };
371
+ }
372
+ catch (error) {
373
+ return { success: false, error: error.message };
374
+ }
375
+ }
376
+ async function replaceFunctionTests(testFilePath, functionName, newTestContent) {
377
+ try {
378
+ const dir = path.dirname(testFilePath);
379
+ await fs.mkdir(dir, { recursive: true });
380
+ if (!fsSync.existsSync(testFilePath)) {
381
+ return await writeTestFile(testFilePath, newTestContent);
382
+ }
383
+ const content = await fs.readFile(testFilePath, 'utf-8');
384
+ const lines = content.split('\n');
385
+ let describeStartLine = -1;
386
+ let describeEndLine = -1;
387
+ for (let i = 0; i < lines.length; i++) {
388
+ const line = lines[i];
389
+ const describeMatch = line.match(/describe\s*\(\s*['"]([^'"]+)['"]\s*,/);
390
+ if (describeMatch) {
391
+ const describeName = describeMatch[1];
392
+ if (describeName === functionName ||
393
+ describeName.endsWith(` - ${functionName}`) ||
394
+ describeName.endsWith(`-${functionName}`) ||
395
+ describeName.startsWith(`${functionName} -`) ||
396
+ describeName.startsWith(`${functionName}-`)) {
397
+ describeStartLine = i;
398
+ let bracketDepth = 0;
399
+ let foundOpening = false;
400
+ for (let j = i; j < lines.length; j++) {
401
+ const currentLine = lines[j];
402
+ // Strip string literals and comments before counting braces
403
+ const stripped = currentLine
404
+ .replace(/\/\/.*$/, '') // remove line comments
405
+ .replace(/\/\*.*?\*\//g, '') // remove inline block comments
406
+ .replace(/'(?:[^'\\]|\\.)*'/g, '') // remove single-quoted strings
407
+ .replace(/"(?:[^"\\]|\\.)*"/g, '') // remove double-quoted strings
408
+ .replace(/`(?:[^`\\]|\\.)*`/g, ''); // remove template literals (single-line)
409
+ for (const char of stripped) {
410
+ if (char === '{') {
411
+ bracketDepth++;
412
+ foundOpening = true;
413
+ }
414
+ else if (char === '}') {
415
+ bracketDepth--;
416
+ if (foundOpening && bracketDepth === 0) {
417
+ const afterBrace = currentLine.substring(currentLine.lastIndexOf('}') + 1).trim();
418
+ if (afterBrace === ');' || afterBrace === ')' || afterBrace === '') {
419
+ describeEndLine = j;
420
+ break;
421
+ }
422
+ }
423
+ }
424
+ }
425
+ if (describeEndLine !== -1)
426
+ break;
427
+ }
428
+ break;
429
+ }
430
+ }
431
+ }
432
+ if (describeStartLine !== -1 && describeEndLine !== -1) {
433
+ const beforeBlock = lines.slice(0, describeStartLine);
434
+ const afterBlock = lines.slice(describeEndLine + 1);
435
+ const updatedLines = [...beforeBlock];
436
+ if (beforeBlock.length > 0 && beforeBlock[beforeBlock.length - 1].trim() !== '') {
437
+ updatedLines.push('');
438
+ }
439
+ updatedLines.push(newTestContent);
440
+ if (afterBlock.length > 0 && afterBlock[0].trim() !== '') {
441
+ updatedLines.push('');
442
+ }
443
+ updatedLines.push(...afterBlock);
444
+ const updatedContent = updatedLines.join('\n');
445
+ await fs.writeFile(testFilePath, updatedContent, 'utf-8');
446
+ const validation = await validateTestFileSyntax(testFilePath);
447
+ if (!validation.valid) {
448
+ await fs.writeFile(testFilePath, content, 'utf-8');
449
+ return {
450
+ success: false,
451
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
452
+ syntaxError: true,
453
+ location: validation.location,
454
+ suggestion: validation.suggestion
455
+ };
456
+ }
457
+ return {
458
+ success: true,
459
+ message: `Replaced existing tests for function '${functionName}' (lines ${describeStartLine + 1}-${describeEndLine + 1})`,
460
+ replaced: true,
461
+ replacedLines: { start: describeStartLine + 1, end: describeEndLine + 1 }
462
+ };
463
+ }
464
+ else {
465
+ // Strip any leading imports/mocks from new_test_content so only the describe block is appended
466
+ const contentLines = newTestContent.split('\n');
467
+ let describeLineIdx = contentLines.findIndex(l => /^\s*describe\s*\(/.test(l));
468
+ if (describeLineIdx > 0) {
469
+ // Extract non-describe preamble (imports/mocks) and add them at the top of the file
470
+ const preambleLines = contentLines.slice(0, describeLineIdx).filter(l => l.trim() !== '');
471
+ const describeContent = contentLines.slice(describeLineIdx).join('\n');
472
+ if (preambleLines.length > 0) {
473
+ // Find where imports end in the existing file to insert new imports/mocks there
474
+ let lastImportLine = -1;
475
+ for (let i = 0; i < lines.length; i++) {
476
+ const trimmed = lines[i].trim();
477
+ if (trimmed.startsWith('import ') || trimmed.startsWith('vi.mock(') ||
478
+ trimmed.startsWith('jest.mock(') || trimmed.startsWith('const mock') ||
479
+ trimmed.startsWith('vi.mocked(')) {
480
+ lastImportLine = i;
481
+ }
482
+ }
483
+ const insertPreambleAt = lastImportLine !== -1 ? lastImportLine + 1 : 0;
484
+ // Only add preamble lines that don't already exist in the file
485
+ const existingContent = content;
486
+ const newPreamble = preambleLines.filter(l => !existingContent.includes(l.trim()));
487
+ if (newPreamble.length > 0) {
488
+ lines.splice(insertPreambleAt, 0, ...newPreamble);
489
+ }
490
+ }
491
+ newTestContent = describeContent;
492
+ }
493
+ // Append the describe block at the end of the file
494
+ // Remove trailing empty lines first, then add proper spacing
495
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
496
+ lines.pop();
497
+ }
498
+ lines.push('', newTestContent, '');
499
+ const updatedContent = lines.join('\n');
500
+ await fs.writeFile(testFilePath, updatedContent, 'utf-8');
501
+ const validation = await validateTestFileSyntax(testFilePath);
502
+ if (!validation.valid) {
503
+ await fs.writeFile(testFilePath, content, 'utf-8');
504
+ return {
505
+ success: false,
506
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
507
+ syntaxError: true,
508
+ location: validation.location,
509
+ suggestion: validation.suggestion
510
+ };
511
+ }
512
+ return {
513
+ success: true,
514
+ message: `Added tests for function '${functionName}' at end of file`,
515
+ replaced: false,
516
+ appended: true
517
+ };
518
+ }
519
+ }
520
+ catch (error) {
521
+ return { success: false, error: error.message };
522
+ }
523
+ }
524
+ // Strip ANSI color codes from output
525
+ function stripAnsi(str) {
526
+ return str.replace(/\u001b\[[0-9;]*m/g, '').replace(/\\u001b\[[0-9;]*m/g, '');
527
+ }
528
+ function runTestsVitest(testFilePath, functionNames) {
529
+ try {
530
+ const packageManager = globals_1.g.CONFIG.packageManager || 'npm';
531
+ let command = packageManager === 'yarn'
532
+ ? `yarn test ${testFilePath} -- --reporter=dot --no-color`
533
+ : `npm run test ${testFilePath} -- --reporter=dot --no-color`;
534
+ if (functionNames && functionNames.length > 0) {
535
+ const escapedNames = functionNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
536
+ const pattern = escapedNames.join('|');
537
+ command += ` -t "${pattern}"`;
538
+ }
539
+ const output = (0, child_process_1.execSync)(command, {
540
+ encoding: 'utf-8',
541
+ stdio: 'pipe',
542
+ timeout: 60000,
543
+ maxBuffer: 10 * 1024 * 1024
544
+ });
545
+ return {
546
+ success: true,
547
+ output: stripAnsi(output),
548
+ passed: true,
549
+ command
550
+ };
551
+ }
552
+ catch (error) {
553
+ let output = '';
554
+ if (error.stdout)
555
+ output += error.stdout;
556
+ if (error.stderr)
557
+ output += error.stderr;
558
+ if (!output && error.message) {
559
+ output = error.message;
560
+ }
561
+ return {
562
+ success: false,
563
+ output: stripAnsi(output) || 'Test failed with no output captured',
564
+ passed: false,
565
+ error: error.message
566
+ };
567
+ }
568
+ }
569
+ function runTestsJest(testFilePath, functionNames) {
570
+ try {
571
+ let command = `npx jest ${testFilePath} --no-coverage --verbose=false`;
572
+ if (functionNames && functionNames.length > 0) {
573
+ const escapedNames = functionNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
574
+ const pattern = escapedNames.join('|');
575
+ command += ` -t "${pattern}"`;
576
+ }
577
+ const output = (0, child_process_1.execSync)(command, {
578
+ encoding: 'utf-8',
579
+ stdio: 'pipe',
580
+ timeout: 10000
581
+ });
582
+ return {
583
+ success: true,
584
+ output,
585
+ passed: true,
586
+ command
587
+ };
588
+ }
589
+ catch (error) {
590
+ return {
591
+ success: false,
592
+ output: error.stdout + error.stderr,
593
+ passed: false,
594
+ error: error.message
595
+ };
596
+ }
597
+ }
598
+ /**
599
+ * Run specific tests in isolation to detect if failure is due to pollution or regression
600
+ * Used by smartValidateTestSuite to differentiate between test infrastructure issues and source code bugs
601
+ */
602
+ function runTestsIsolated(testFilePath, specificTestNames) {
603
+ try {
604
+ const packageManager = globals_1.g.CONFIG.packageManager || 'npm';
605
+ let command = packageManager === 'yarn'
606
+ ? `yarn test ${testFilePath} -- --reporter=dot --no-color`
607
+ : `npm run test ${testFilePath} -- --reporter=dot --no-color`;
608
+ if (specificTestNames && specificTestNames.length > 0) {
609
+ const escapedNames = specificTestNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
610
+ const pattern = escapedNames.join('|');
611
+ command += ` -t "${pattern}"`;
612
+ }
613
+ const output = (0, child_process_1.execSync)(command, {
614
+ encoding: 'utf-8',
615
+ stdio: 'pipe',
616
+ timeout: 60000,
617
+ maxBuffer: 10 * 1024 * 1024
618
+ });
619
+ return {
620
+ success: true,
621
+ output: stripAnsi(output),
622
+ passed: true,
623
+ command
624
+ };
625
+ }
626
+ catch (error) {
627
+ let output = '';
628
+ if (error.stdout)
629
+ output += error.stdout;
630
+ if (error.stderr)
631
+ output += error.stderr;
632
+ if (!output && error.message) {
633
+ output = error.message;
634
+ }
635
+ return {
636
+ success: false,
637
+ output: stripAnsi(output) || 'Test failed with no output captured',
638
+ passed: false,
639
+ error: error.message
640
+ };
641
+ }
642
+ }
643
+ /**
644
+ * Parse test output to extract names of failing tests
645
+ * Works with both Vitest and Jest output formats
646
+ * Returns array of test names that failed
647
+ */
648
+ function parseFailingTestNames(testOutput) {
649
+ const failingTests = [];
650
+ const lines = testOutput.split('\n');
651
+ for (let i = 0; i < lines.length; i++) {
652
+ const line = lines[i];
653
+ const bulletMatch = line.match(/^\s*ā—\s+(.+?)\s+›\s+(.+?)$/);
654
+ if (bulletMatch) {
655
+ const testName = bulletMatch[2].trim();
656
+ failingTests.push(testName);
657
+ continue;
658
+ }
659
+ const xMatch = line.match(/^\s*āœ•\s+(.+?)(?:\s+\(\d+m?s\))?$/);
660
+ if (xMatch) {
661
+ const testName = xMatch[1].trim();
662
+ failingTests.push(testName);
663
+ continue;
664
+ }
665
+ const failMatch = line.match(/^\s*āœ“?\s*(.+?)\s+\(\d+m?s\)$/);
666
+ if (failMatch && line.includes('āœ•')) {
667
+ const testName = failMatch[1].trim();
668
+ failingTests.push(testName);
669
+ }
670
+ }
671
+ return [...new Set(failingTests)];
672
+ }
673
+ function listDirectory(directoryPath) {
674
+ try {
675
+ if (!fsSync.existsSync(directoryPath)) {
676
+ return { success: false, error: `Directory not found: ${directoryPath}` };
677
+ }
678
+ const items = fsSync.readdirSync(directoryPath);
679
+ const details = items.map(item => {
680
+ const fullPath = path.join(directoryPath, item);
681
+ const stats = fsSync.statSync(fullPath);
682
+ return {
683
+ name: item,
684
+ path: fullPath,
685
+ type: stats.isDirectory() ? 'directory' : 'file',
686
+ isDirectory: stats.isDirectory()
687
+ };
688
+ });
689
+ return {
690
+ success: true,
691
+ path: directoryPath,
692
+ items: details,
693
+ files: details.filter(d => !d.isDirectory).map(d => d.name),
694
+ directories: details.filter(d => d.isDirectory).map(d => d.name)
695
+ };
696
+ }
697
+ catch (error) {
698
+ return { success: false, error: error.message };
699
+ }
700
+ }
701
+ function findFile(filename) {
702
+ try {
703
+ const searchDirs = ['src', 'tests', '.'];
704
+ const found = [];
705
+ function searchRecursive(dir) {
706
+ if (!fsSync.existsSync(dir))
707
+ return;
708
+ const items = fsSync.readdirSync(dir);
709
+ for (const item of items) {
710
+ const fullPath = path.join(dir, item);
711
+ const stats = fsSync.statSync(fullPath);
712
+ if (stats.isDirectory()) {
713
+ if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(item)) {
714
+ searchRecursive(fullPath);
715
+ }
716
+ }
717
+ else {
718
+ const normalizedFullPath = fullPath.replace(/\\/g, '/');
719
+ const normalizedSearch = filename.replace(/\\/g, '/');
720
+ if (normalizedFullPath === normalizedSearch ||
721
+ normalizedFullPath.includes(normalizedSearch) ||
722
+ normalizedFullPath.endsWith(normalizedSearch) ||
723
+ item === filename ||
724
+ item.includes(filename)) {
725
+ found.push(fullPath);
726
+ }
727
+ }
728
+ }
729
+ }
730
+ for (const dir of searchDirs) {
731
+ searchRecursive(dir);
732
+ }
733
+ if (found.length === 0) {
734
+ return {
735
+ success: false,
736
+ error: `File "${filename}" not found in repository`
737
+ };
738
+ }
739
+ return {
740
+ success: true,
741
+ filename,
742
+ found: found,
743
+ count: found.length,
744
+ primaryMatch: found[0]
745
+ };
746
+ }
747
+ catch (error) {
748
+ return { success: false, error: error.message };
749
+ }
750
+ }
751
+ /**
752
+ * Calculate relative import path from one file to one or more target files.
753
+ * Handles multiple paths for efficiency - AI can get all relative paths in a single call.
754
+ */
755
+ function calculateRelativePath(fromFile, toFile) {
756
+ try {
757
+ const normalizedFromFile = normalizePath(fromFile);
758
+ if (!normalizedFromFile) {
759
+ return { success: false, error: `Invalid from_file path: "${fromFile}"` };
760
+ }
761
+ const toFiles = parseMultiplePaths(toFile);
762
+ if (toFiles.length === 0) {
763
+ return { success: false, error: `No valid to_file paths provided: "${toFile}"` };
764
+ }
765
+ const results = [];
766
+ const errors = [];
767
+ for (const targetFile of toFiles) {
768
+ const result = calculateSingleRelativePath(normalizedFromFile, targetFile);
769
+ if (result.success) {
770
+ results.push(result);
771
+ }
772
+ else {
773
+ errors.push({ file: targetFile, error: result.error });
774
+ }
775
+ }
776
+ if (toFiles.length === 1) {
777
+ return results.length > 0 ? results[0] : errors[0];
778
+ }
779
+ else {
780
+ return {
781
+ success: errors.length === 0,
782
+ from: normalizedFromFile,
783
+ totalFiles: toFiles.length,
784
+ successCount: results.length,
785
+ errorCount: errors.length,
786
+ results,
787
+ errors: errors.length > 0 ? errors : undefined
788
+ };
789
+ }
790
+ }
791
+ catch (error) {
792
+ return { success: false, error: error.message };
793
+ }
794
+ }
795
+ /**
796
+ * Parse comma-separated or space-separated file paths into an array.
797
+ */
798
+ function parseMultiplePaths(input) {
799
+ if (!input || typeof input !== 'string') {
800
+ return [];
801
+ }
802
+ const paths = input
803
+ .split(',')
804
+ .map(p => p.trim())
805
+ .filter(p => p.length > 0)
806
+ .map(p => normalizePath(p))
807
+ .filter((p) => p !== null);
808
+ return paths;
809
+ }
810
+ /**
811
+ * Normalize a file path - handle various edge cases.
812
+ */
813
+ function normalizePath(filePath) {
814
+ if (!filePath || typeof filePath !== 'string') {
815
+ return null;
816
+ }
817
+ let normalized = filePath.trim();
818
+ if ((normalized.startsWith('"') && normalized.endsWith('"')) ||
819
+ (normalized.startsWith("'") && normalized.endsWith("'"))) {
820
+ normalized = normalized.slice(1, -1);
821
+ }
822
+ normalized = normalized.replace(/\\/g, '/');
823
+ if (normalized.startsWith('./')) {
824
+ normalized = normalized.slice(2);
825
+ }
826
+ normalized = normalized.replace(/\/+/g, '/');
827
+ if (normalized.endsWith('/')) {
828
+ normalized = normalized.slice(0, -1);
829
+ }
830
+ if (normalized.length === 0 || /^[\s@#$%^&*]/.test(normalized)) {
831
+ return null;
832
+ }
833
+ return normalized;
834
+ }
835
+ /**
836
+ * Calculate relative path from one file to a single target file.
837
+ */
838
+ function calculateSingleRelativePath(fromFile, toFile) {
839
+ try {
840
+ const fromDir = path.dirname(fromFile);
841
+ let relativePath = path.relative(fromDir, toFile);
842
+ if (!relativePath) {
843
+ const basename = path.basename(toFile);
844
+ relativePath = './' + basename;
845
+ }
846
+ relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, '');
847
+ relativePath = relativePath.replace(/\/index$/, '');
848
+ if (!relativePath.startsWith('.') && !relativePath.startsWith('..')) {
849
+ relativePath = './' + relativePath;
850
+ }
851
+ relativePath = relativePath.replace(/\\/g, '/');
852
+ if (relativePath === '.') {
853
+ const basename = path.basename(toFile).replace(/\.(ts|tsx|js|jsx)$/, '');
854
+ relativePath = './' + basename;
855
+ }
856
+ return {
857
+ success: true,
858
+ from: fromFile,
859
+ to: toFile,
860
+ relativePath,
861
+ importStatement: `import { ... } from '${relativePath}';`
862
+ };
863
+ }
864
+ catch (error) {
865
+ return { success: false, file: toFile, error: error.message };
866
+ }
867
+ }
868
+ async function deleteLines(filePath, startLine, endLine) {
869
+ try {
870
+ const content = await fs.readFile(filePath, 'utf-8');
871
+ const lines = content.split('\n');
872
+ if (startLine < 1 || endLine < 1 || startLine > lines.length || endLine > lines.length) {
873
+ return {
874
+ success: false,
875
+ error: `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`
876
+ };
877
+ }
878
+ if (startLine > endLine) {
879
+ return {
880
+ success: false,
881
+ error: `Start line (${startLine}) must be <= end line (${endLine})`
882
+ };
883
+ }
884
+ const deletedLines = lines.splice(startLine - 1, endLine - startLine + 1);
885
+ await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
886
+ return {
887
+ success: true,
888
+ message: `Deleted lines ${startLine}-${endLine}`,
889
+ deletedCount: deletedLines.length,
890
+ deletedContent: deletedLines.join('\n')
891
+ };
892
+ }
893
+ catch (error) {
894
+ return { success: false, error: error.message };
895
+ }
896
+ }
897
+ async function insertLines(filePath, lineNumber, content) {
898
+ try {
899
+ const fileContent = await fs.readFile(filePath, 'utf-8');
900
+ const lines = fileContent.split('\n');
901
+ if (lineNumber < 1 || lineNumber > lines.length + 1) {
902
+ return {
903
+ success: false,
904
+ error: `Invalid line number: ${lineNumber}. File has ${lines.length} lines (use 1-${lines.length + 1}).`
905
+ };
906
+ }
907
+ const newLines = content.split('\n');
908
+ lines.splice(lineNumber - 1, 0, ...newLines);
909
+ await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
910
+ return {
911
+ success: true,
912
+ message: `Inserted ${newLines.length} line(s) at line ${lineNumber}`,
913
+ insertedCount: newLines.length
914
+ };
915
+ }
916
+ catch (error) {
917
+ return { success: false, error: error.message };
918
+ }
919
+ }
920
+ async function replaceLines(filePath, startLine, endLine, newContent) {
921
+ try {
922
+ const content = await fs.readFile(filePath, 'utf-8');
923
+ const lines = content.split('\n');
924
+ if (startLine < 1 || endLine < 1 || startLine > lines.length || endLine > lines.length) {
925
+ return {
926
+ success: false,
927
+ error: `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`
928
+ };
929
+ }
930
+ if (startLine > endLine) {
931
+ return {
932
+ success: false,
933
+ error: `Start line (${startLine}) must be <= end line (${endLine})`
934
+ };
935
+ }
936
+ const oldLines = lines.splice(startLine - 1, endLine - startLine + 1, ...newContent.split('\n'));
937
+ await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
938
+ return {
939
+ success: true,
940
+ message: `Replaced lines ${startLine}-${endLine}`,
941
+ oldContent: oldLines.join('\n'),
942
+ newLineCount: newContent.split('\n').length
943
+ };
944
+ }
945
+ catch (error) {
946
+ return { success: false, error: error.message };
947
+ }
948
+ }
949
+ /**
950
+ * Search and replace a code block using fuzzy matching
951
+ * Much more reliable than line-based editing!
952
+ */
953
+ async function searchReplaceBlock(filePath, search, replace, matchMode = 'normalized') {
954
+ try {
955
+ const content = await fs.readFile(filePath, 'utf-8');
956
+ if (matchMode !== 'exact' && content.includes(search)) {
957
+ const newContent = content.replace(search, replace);
958
+ await fs.writeFile(filePath, newContent, 'utf-8');
959
+ if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
960
+ const validation = await validateTestFileSyntax(filePath);
961
+ if (!validation.valid) {
962
+ await fs.writeFile(filePath, content, 'utf-8');
963
+ return {
964
+ success: false,
965
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
966
+ syntaxError: true,
967
+ location: validation.location,
968
+ suggestion: validation.suggestion
969
+ };
970
+ }
971
+ }
972
+ return {
973
+ success: true,
974
+ message: 'Replaced block (exact match)',
975
+ matchMode: 'exact',
976
+ confidence: 1.0,
977
+ linesChanged: newContent.split('\n').length - content.split('\n').length
978
+ };
979
+ }
980
+ const contentSize = content.length;
981
+ const searchSize = search.length;
982
+ const isLargeOperation = contentSize > 200000 || searchSize > 2000;
983
+ const searchMode = isLargeOperation ? 'normalized' : matchMode;
984
+ const result = (0, fuzzyMatcher_1.smartSearch)(content, search, searchMode);
985
+ if (!result || !result.found) {
986
+ const suggestions = isLargeOperation ? [] : (0, fuzzyMatcher_1.findSimilarBlocks)(content, search, 3);
987
+ const errorMessage = isLargeOperation
988
+ ? 'Search block not found. File is large - try including more unique context or use exact match mode.'
989
+ : (0, fuzzyMatcher_1.getSearchFailureMessage)(content, search);
990
+ return {
991
+ success: false,
992
+ error: errorMessage,
993
+ searchPreview: search.substring(0, 200) + (search.length > 200 ? '...' : ''),
994
+ suggestions: suggestions.map(s => ({
995
+ text: s.text.substring(0, 150) + (s.text.length > 150 ? '...' : ''),
996
+ similarity: Math.round(s.similarity * 100) + '%',
997
+ lines: `${s.startLine}-${s.endLine}`
998
+ })),
999
+ hint: suggestions.length > 0
1000
+ ? `Found ${suggestions.length} similar block(s). Check if your search text has typos or try including more context lines.`
1001
+ : 'No similar blocks found. Make sure your search text matches the file content and includes enough context (3-5 lines around the target).'
1002
+ };
1003
+ }
1004
+ const newContent = content.substring(0, result.startIndex) +
1005
+ replace +
1006
+ content.substring(result.endIndex);
1007
+ await fs.writeFile(filePath, newContent, 'utf-8');
1008
+ if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
1009
+ const validation = await validateTestFileSyntax(filePath);
1010
+ if (!validation.valid) {
1011
+ await fs.writeFile(filePath, content, 'utf-8');
1012
+ return {
1013
+ success: false,
1014
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
1015
+ syntaxError: true,
1016
+ location: validation.location,
1017
+ suggestion: validation.suggestion
1018
+ };
1019
+ }
1020
+ }
1021
+ const getLineNumber = (charIndex) => {
1022
+ return content.substring(0, charIndex).split('\n').length;
1023
+ };
1024
+ const startLine = getLineNumber(result.startIndex);
1025
+ const endLine = getLineNumber(result.endIndex);
1026
+ return {
1027
+ success: true,
1028
+ message: `Replaced block successfully using ${result.matchType} matching`,
1029
+ matchMode: result.matchType,
1030
+ confidence: result.confidence,
1031
+ linesChanged: newContent.split('\n').length - content.split('\n').length,
1032
+ linesReplaced: `${startLine}-${endLine}`,
1033
+ replacedText: result.originalText.substring(0, 200) + (result.originalText.length > 200 ? '...' : '')
1034
+ };
1035
+ }
1036
+ catch (error) {
1037
+ return { success: false, error: error.message };
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Insert content at a specific position in the file
1042
+ * Simpler than search_replace_block for adding new content
1043
+ */
1044
+ async function insertAtPosition(filePath, content, position, afterMarker) {
1045
+ try {
1046
+ const fileContent = await fs.readFile(filePath, 'utf-8');
1047
+ const lines = fileContent.split('\n');
1048
+ let insertIndex = 0;
1049
+ let insertionPoint = '';
1050
+ if (afterMarker) {
1051
+ const result = (0, fuzzyMatcher_1.smartSearch)(fileContent, afterMarker, 'normalized');
1052
+ if (!result || !result.found) {
1053
+ return {
1054
+ success: false,
1055
+ error: `Marker not found: "${afterMarker.substring(0, 100)}..."`,
1056
+ hint: 'Use search_replace_block or specify a position instead'
1057
+ };
1058
+ }
1059
+ let charCount = 0;
1060
+ for (let i = 0; i < lines.length; i++) {
1061
+ charCount += lines[i].length + 1;
1062
+ if (charCount >= result.endIndex) {
1063
+ insertIndex = i + 1;
1064
+ insertionPoint = `after marker at line ${i + 1}`;
1065
+ break;
1066
+ }
1067
+ }
1068
+ }
1069
+ else {
1070
+ switch (position) {
1071
+ case 'beginning':
1072
+ insertIndex = 0;
1073
+ insertionPoint = 'beginning of file';
1074
+ break;
1075
+ case 'end':
1076
+ insertIndex = lines.length;
1077
+ insertionPoint = 'end of file';
1078
+ break;
1079
+ case 'after_imports': {
1080
+ let lastImportLine = -1;
1081
+ let insideMultiLineImport = false;
1082
+ for (let i = 0; i < lines.length; i++) {
1083
+ const trimmed = lines[i].trim();
1084
+ // Track multi-line import/require statements
1085
+ if (insideMultiLineImport) {
1086
+ lastImportLine = i;
1087
+ // Check if this line closes the multi-line import
1088
+ if (trimmed.includes('from ') || trimmed.endsWith(');') ||
1089
+ (trimmed.startsWith('}') && !trimmed.includes('{'))) {
1090
+ insideMultiLineImport = false;
1091
+ }
1092
+ continue;
1093
+ }
1094
+ if (trimmed.startsWith('import ') || trimmed.startsWith('require(') ||
1095
+ (trimmed.startsWith('const ') && trimmed.includes('require('))) {
1096
+ lastImportLine = i;
1097
+ // Detect if this is a multi-line import (has opening { but no closing } or no from)
1098
+ const hasOpenBrace = trimmed.includes('{');
1099
+ const hasCloseBrace = trimmed.includes('}');
1100
+ const hasFrom = trimmed.includes('from ');
1101
+ const endsWithSemicolon = trimmed.endsWith(';');
1102
+ if (hasOpenBrace && !hasCloseBrace) {
1103
+ insideMultiLineImport = true;
1104
+ }
1105
+ else if (trimmed.startsWith('import ') && !hasFrom && !endsWithSemicolon) {
1106
+ insideMultiLineImport = true;
1107
+ }
1108
+ }
1109
+ else if (trimmed && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*') && lastImportLine >= 0) {
1110
+ break;
1111
+ }
1112
+ }
1113
+ if (lastImportLine === -1) {
1114
+ return {
1115
+ success: false,
1116
+ error: 'No import statements found in file',
1117
+ hint: 'Use position: "beginning" instead or specify after_marker'
1118
+ };
1119
+ }
1120
+ insertIndex = lastImportLine + 1;
1121
+ insertionPoint = `after imports at line ${lastImportLine + 1}`;
1122
+ break;
1123
+ }
1124
+ case 'before_first_describe': {
1125
+ let firstDescribe = -1;
1126
+ for (let i = 0; i < lines.length; i++) {
1127
+ if (lines[i].trim().startsWith('describe(')) {
1128
+ firstDescribe = i;
1129
+ break;
1130
+ }
1131
+ }
1132
+ if (firstDescribe === -1) {
1133
+ return {
1134
+ success: false,
1135
+ error: 'No describe blocks found in file',
1136
+ hint: 'Use position: "end" instead or specify after_marker'
1137
+ };
1138
+ }
1139
+ insertIndex = firstDescribe;
1140
+ insertionPoint = `before first describe at line ${firstDescribe + 1}`;
1141
+ break;
1142
+ }
1143
+ default:
1144
+ insertIndex = lines.length;
1145
+ insertionPoint = 'end of file';
1146
+ }
1147
+ }
1148
+ const contentLines = content.split('\n');
1149
+ lines.splice(insertIndex, 0, ...contentLines);
1150
+ const newContent = lines.join('\n');
1151
+ await fs.writeFile(filePath, newContent, 'utf-8');
1152
+ if (filePath.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
1153
+ const validation = await validateTestFileSyntax(filePath);
1154
+ if (!validation.valid) {
1155
+ await fs.writeFile(filePath, fileContent, 'utf-8');
1156
+ return {
1157
+ success: false,
1158
+ error: `Syntax error after modification at line ${validation.location?.line}:${validation.location?.column}\n${validation.error}`,
1159
+ syntaxError: true,
1160
+ location: validation.location,
1161
+ suggestion: validation.suggestion
1162
+ };
1163
+ }
1164
+ }
1165
+ return {
1166
+ success: true,
1167
+ message: `Inserted ${contentLines.length} line(s) at ${insertionPoint}`,
1168
+ insertedAt: insertionPoint,
1169
+ lineCount: contentLines.length
1170
+ };
1171
+ }
1172
+ catch (error) {
1173
+ return { success: false, error: error.message };
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Write code review findings to a markdown file in the reviews/ directory
1178
+ */
1179
+ async function writeReview(filePath, reviewContent) {
1180
+ try {
1181
+ const reviewsDir = path.dirname(filePath);
1182
+ if (!fsSync.existsSync(reviewsDir)) {
1183
+ await fs.mkdir(reviewsDir, { recursive: true });
1184
+ }
1185
+ await fs.writeFile(filePath, reviewContent, 'utf-8');
1186
+ return {
1187
+ success: true,
1188
+ message: `Review written to ${filePath}`,
1189
+ filePath: filePath
1190
+ };
1191
+ }
1192
+ catch (error) {
1193
+ return {
1194
+ success: false,
1195
+ error: `Failed to write review: ${error.message}`
1196
+ };
1197
+ }
1198
+ }
1199
+ // Search codebase utility
1200
+ function searchCodebase(pattern, fileExtension, maxResults = 20, filesOnly = false) {
1201
+ try {
1202
+ const limit = Math.min(maxResults || 20, 50);
1203
+ let cmd = `grep -rn`;
1204
+ if (filesOnly) {
1205
+ cmd = `grep -rl`;
1206
+ }
1207
+ else {
1208
+ cmd += ` -C 1`;
1209
+ }
1210
+ if (fileExtension) {
1211
+ const ext = fileExtension.startsWith('.') ? fileExtension : `.${fileExtension}`;
1212
+ cmd += ` --include="*${ext}"`;
1213
+ }
1214
+ const excludeDirs = ['node_modules', 'dist', 'build', '.git', 'coverage', '.codeguard-cache', '.jest-cache', '.testgen-cache', '.vitest', 'reviews'];
1215
+ for (const dir of excludeDirs) {
1216
+ cmd += ` --exclude-dir=${dir}`;
1217
+ }
1218
+ cmd += ` "${pattern}" .`;
1219
+ cmd += ` | head -n ${limit * 3}`;
1220
+ const output = (0, child_process_1.execSync)(cmd, {
1221
+ encoding: 'utf-8',
1222
+ cwd: process.cwd(),
1223
+ maxBuffer: 5 * 1024 * 1024,
1224
+ stdio: ['pipe', 'pipe', 'ignore']
1225
+ });
1226
+ if (filesOnly) {
1227
+ const files = output.trim().split('\n').filter(f => f.length > 0).slice(0, limit);
1228
+ return {
1229
+ success: true,
1230
+ pattern,
1231
+ filesOnly: true,
1232
+ totalFiles: files.length,
1233
+ files: files.map(f => ({ file: f })),
1234
+ message: `Found ${files.length} file(s) matching pattern`,
1235
+ hint: 'Use search_codebase with files_only=false on specific files to see content'
1236
+ };
1237
+ }
1238
+ const lines = output.trim().split('\n').slice(0, limit);
1239
+ const results = [];
1240
+ const fileSet = new Set();
1241
+ let currentFile = '';
1242
+ let currentMatches = [];
1243
+ for (const line of lines) {
1244
+ if (line.includes(':')) {
1245
+ const match = line.match(/^([^:]+):(\d+):(.*)/);
1246
+ if (match) {
1247
+ const [, file, lineNum, content] = match;
1248
+ fileSet.add(file);
1249
+ if (file !== currentFile) {
1250
+ if (currentFile && currentMatches.length > 0) {
1251
+ results.push({
1252
+ file: currentFile,
1253
+ matchCount: currentMatches.length,
1254
+ matches: currentMatches.slice(0, 5)
1255
+ });
1256
+ }
1257
+ currentFile = file;
1258
+ currentMatches = [];
1259
+ }
1260
+ const truncatedContent = content.length > 150 ? content.substring(0, 150) + '...' : content;
1261
+ currentMatches.push(`Line ${lineNum}: ${truncatedContent}`);
1262
+ }
1263
+ }
1264
+ }
1265
+ if (currentFile && currentMatches.length > 0) {
1266
+ results.push({
1267
+ file: currentFile,
1268
+ matchCount: currentMatches.length,
1269
+ matches: currentMatches.slice(0, 5)
1270
+ });
1271
+ }
1272
+ const totalMatches = results.reduce((sum, r) => sum + r.matchCount, 0);
1273
+ return {
1274
+ success: true,
1275
+ pattern,
1276
+ totalFiles: fileSet.size,
1277
+ totalMatches,
1278
+ files: results.slice(0, 10),
1279
+ truncated: results.length > 10 || lines.length >= limit,
1280
+ message: `Found ${fileSet.size} file(s) with ${totalMatches} match(es)${results.length > 10 ? ' (showing first 10 files)' : ''}`,
1281
+ hint: results.length > 10 ? 'Too many results. Try adding file_extension filter or more specific pattern.' : undefined
1282
+ };
1283
+ }
1284
+ catch (error) {
1285
+ if (error.status === 1 || error.message?.includes('No such file')) {
1286
+ return {
1287
+ success: true,
1288
+ pattern,
1289
+ totalFiles: 0,
1290
+ totalMatches: 0,
1291
+ files: [],
1292
+ message: 'No matches found'
1293
+ };
1294
+ }
1295
+ return {
1296
+ success: false,
1297
+ error: `Search failed: ${error.message}`
1298
+ };
1299
+ }
1300
+ }
1301
+ // ---------------------------------------------------------------------------
1302
+ // Tool execution router
1303
+ // ---------------------------------------------------------------------------
1304
+ async function executeTool(toolName, args) {
1305
+ let friendlyMessage = toolDefinitions_1.TOOL_MESSAGES[toolName] || `šŸ”§ ${toolName}`;
1306
+ if (toolName === 'upsert_function_tests' && args.function_name) {
1307
+ friendlyMessage = `āœļø Writing/updating test cases for function: ${args.function_name}`;
1308
+ }
1309
+ else if (toolName === 'search_replace_block') {
1310
+ const preview = args.search ? args.search.substring(0, 50) + (args.search.length > 50 ? '...' : '') : '';
1311
+ friendlyMessage = `šŸ”šŸ”„ Searching for: "${preview}"`;
1312
+ }
1313
+ else if (toolName === 'insert_at_position' && args.position) {
1314
+ friendlyMessage = `āž• Inserting at: ${args.position}`;
1315
+ }
1316
+ else if (toolName === 'read_file_lines' && args.start_line && args.end_line) {
1317
+ friendlyMessage = `šŸ“– Reading lines ${args.start_line}-${args.end_line}`;
1318
+ }
1319
+ (0, globals_1.updateSpinner)(friendlyMessage);
1320
+ let result;
1321
+ try {
1322
+ switch (toolName) {
1323
+ case 'read_file':
1324
+ result = await readFile(args.file_path);
1325
+ break;
1326
+ case 'read_file_lines':
1327
+ result = await readFileLines(args.file_path, args.start_line, args.end_line);
1328
+ break;
1329
+ case 'analyze_file_ast':
1330
+ if (globals_1.g.globalIndexer && !args.function_name) {
1331
+ if (globals_1.g.globalIndexer.isFileStale(args.file_path)) {
1332
+ globals_1.g.globalSpinner.text = 'šŸ”„ File modified, re-analyzing...';
1333
+ result = (0, ast_1.analyzeFileAST)(args.file_path, args.function_name);
1334
+ if (result.success && !args.function_name) {
1335
+ await globals_1.g.globalIndexer.updateIndex([args.file_path], ast_1.analyzeFileAST);
1336
+ }
1337
+ break;
1338
+ }
1339
+ const cached = globals_1.g.globalIndexer.getFileAnalysis(args.file_path);
1340
+ if (cached) {
1341
+ globals_1.g.globalSpinner.text = 'šŸ“¦ Using cached analysis';
1342
+ result = {
1343
+ success: true,
1344
+ analysis: cached,
1345
+ summary: {
1346
+ functionCount: cached.functions.length,
1347
+ classCount: cached.classes.length,
1348
+ exportCount: cached.exports.length,
1349
+ typeCount: cached.types.length,
1350
+ }
1351
+ };
1352
+ break;
1353
+ }
1354
+ }
1355
+ result = (0, ast_1.analyzeFileAST)(args.file_path, args.function_name);
1356
+ break;
1357
+ case 'get_function_ast':
1358
+ result = (0, ast_1.getFunctionAST)(args.file_path, args.function_name);
1359
+ break;
1360
+ case 'get_imports_ast':
1361
+ result = (0, ast_1.getImportsAST)(args.file_path);
1362
+ break;
1363
+ case 'get_type_definitions':
1364
+ result = (0, ast_1.getTypeDefinitions)(args.file_path);
1365
+ break;
1366
+ case 'get_class_methods':
1367
+ result = (0, ast_1.getClassMethods)(args.file_path, args.class_name);
1368
+ break;
1369
+ case 'get_file_preamble':
1370
+ result = (0, ast_1.getFilePreamble)(args.file_path);
1371
+ break;
1372
+ case 'resolve_import_path':
1373
+ result = resolveImportPath(args.from_file, args.import_path);
1374
+ break;
1375
+ case 'upsert_function_tests':
1376
+ if (globals_1.g.EXPECTED_TEST_FILE_PATH && args.test_file_path !== globals_1.g.EXPECTED_TEST_FILE_PATH) {
1377
+ const normalizedExpected = path.normalize(globals_1.g.EXPECTED_TEST_FILE_PATH).replace(/\\/g, '/');
1378
+ const normalizedProvided = path.normalize(args.test_file_path).replace(/\\/g, '/');
1379
+ if (normalizedExpected !== normalizedProvided) {
1380
+ if (globals_1.g.globalSpinner)
1381
+ globals_1.g.globalSpinner.stop();
1382
+ console.log(`\nāš ļø BLOCKED: AI attempted separate test file, enforcing single file policy`);
1383
+ (0, globals_1.updateSpinner)(friendlyMessage);
1384
+ args.test_file_path = globals_1.g.EXPECTED_TEST_FILE_PATH;
1385
+ }
1386
+ }
1387
+ result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
1388
+ break;
1389
+ case 'run_tests':
1390
+ if (globals_1.g.CONFIG.testEnv == 'vitest') {
1391
+ result = runTestsVitest(args.test_file_path, args.function_names);
1392
+ }
1393
+ else {
1394
+ result = runTestsJest(args.test_file_path, args.function_names);
1395
+ }
1396
+ break;
1397
+ case 'list_directory':
1398
+ result = listDirectory(args.directory_path);
1399
+ break;
1400
+ case 'find_file':
1401
+ result = findFile(args.filename);
1402
+ break;
1403
+ case 'calculate_relative_path':
1404
+ result = calculateRelativePath(args.from_file, args.to_file);
1405
+ break;
1406
+ case 'search_replace_block':
1407
+ result = await searchReplaceBlock(args.file_path, args.search, args.replace, args.match_mode);
1408
+ break;
1409
+ case 'insert_at_position':
1410
+ result = await insertAtPosition(args.file_path, args.content, args.position, args.after_marker);
1411
+ break;
1412
+ case 'write_review':
1413
+ result = await writeReview(args.file_path, args.review_content);
1414
+ break;
1415
+ case 'search_codebase':
1416
+ result = searchCodebase(args.pattern, args.file_extension, args.max_results, args.files_only);
1417
+ break;
1418
+ default:
1419
+ result = { success: false, error: `Unknown tool: ${toolName}` };
1420
+ }
1421
+ }
1422
+ catch (error) {
1423
+ result = { success: false, error: error.message, stack: error.stack };
1424
+ }
1425
+ if (result.success) {
1426
+ await new Promise(resolve => setTimeout(resolve, 150));
1427
+ }
1428
+ return result;
1429
+ }
1430
+ //# sourceMappingURL=toolHandlers.js.map