mcp-code-context 2.2.0 → 2.4.0

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 (132) hide show
  1. package/.io/RULES.md +12 -0
  2. package/CHANGELOG.md +114 -1
  3. package/README.md +28 -1
  4. package/dist/src/ast/writers/safeRename.d.ts +14 -0
  5. package/dist/src/ast/writers/safeRename.js +258 -0
  6. package/dist/src/ast/writers/safeRename.js.map +1 -0
  7. package/dist/src/ast/writers/symbolWriter.d.ts +2 -0
  8. package/dist/src/ast/writers/symbolWriter.js +9 -14
  9. package/dist/src/ast/writers/symbolWriter.js.map +1 -1
  10. package/dist/src/cache/astCache.d.ts +33 -0
  11. package/dist/src/cache/astCache.js +113 -0
  12. package/dist/src/cache/astCache.js.map +1 -0
  13. package/dist/src/handlers/readHandlers.d.ts +70 -0
  14. package/dist/src/handlers/readHandlers.js +431 -0
  15. package/dist/src/handlers/readHandlers.js.map +1 -0
  16. package/dist/src/handlers/utilHandlers.d.ts +31 -0
  17. package/dist/src/handlers/utilHandlers.js +69 -0
  18. package/dist/src/handlers/utilHandlers.js.map +1 -0
  19. package/dist/src/handlers/writeHandlers.d.ts +57 -0
  20. package/dist/src/handlers/writeHandlers.js +475 -0
  21. package/dist/src/handlers/writeHandlers.js.map +1 -0
  22. package/dist/src/index.d.ts +6 -143
  23. package/dist/src/index.js +77 -1091
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/tools/searchCodePattern.d.ts +7 -2
  26. package/dist/src/tools/searchCodePattern.js +68 -4
  27. package/dist/src/tools/searchCodePattern.js.map +1 -1
  28. package/dist/src/utils/arrayUtils.d.ts +12 -0
  29. package/dist/src/utils/arrayUtils.js +27 -0
  30. package/dist/src/utils/arrayUtils.js.map +1 -0
  31. package/dist/src/utils/constants.d.ts +46 -0
  32. package/dist/src/utils/constants.js +112 -0
  33. package/dist/src/utils/constants.js.map +1 -0
  34. package/dist/src/utils/ignoreManager.d.ts +8 -0
  35. package/dist/src/utils/ignoreManager.js +49 -0
  36. package/dist/src/utils/ignoreManager.js.map +1 -1
  37. package/dist/src/utils/normalization.d.ts +49 -0
  38. package/dist/src/utils/normalization.js +173 -0
  39. package/dist/src/utils/normalization.js.map +1 -0
  40. package/dist/src/utils/projectRoot.d.ts +12 -5
  41. package/dist/src/utils/projectRoot.js +22 -20
  42. package/dist/src/utils/projectRoot.js.map +1 -1
  43. package/dist/src/utils/secureValidation.d.ts +15 -0
  44. package/dist/src/utils/secureValidation.js +41 -0
  45. package/dist/src/utils/secureValidation.js.map +1 -0
  46. package/dist/src/utils/transactionManager.d.ts +45 -0
  47. package/dist/src/utils/transactionManager.js +102 -0
  48. package/dist/src/utils/transactionManager.js.map +1 -0
  49. package/dist/src/utils/validation.d.ts +87 -0
  50. package/dist/src/utils/validation.js +231 -0
  51. package/dist/src/utils/validation.js.map +1 -0
  52. package/docs/ARCHITECTURE.md +300 -0
  53. package/docs/architecture/ADR-001-modular-handlers.md +119 -0
  54. package/llms.txt +10 -1
  55. package/package.json +5 -5
  56. package/AGENT_INSTRUCTIONS.md +0 -135
  57. package/DEPLOYMENT_INSTRUCTIONS.md +0 -110
  58. package/MEJORAS_PROPUESTAS.md +0 -324
  59. package/RELEASE_NOTES_v2.1.3.md +0 -78
  60. package/RELEASE_NOTES_v2.2.0.md +0 -286
  61. package/dist/src/tests/test-confirmation-flow.d.ts +0 -1
  62. package/dist/src/tests/test-confirmation-flow.js +0 -43
  63. package/dist/src/tests/test-confirmation-flow.js.map +0 -1
  64. package/dist/tests/debug-replace.d.ts +0 -1
  65. package/dist/tests/debug-replace.js +0 -17
  66. package/dist/tests/debug-replace.js.map +0 -1
  67. package/dist/tests/test-backup-system.d.ts +0 -7
  68. package/dist/tests/test-backup-system.js +0 -152
  69. package/dist/tests/test-backup-system.js.map +0 -1
  70. package/dist/tests/test-confirmation-flow.d.ts +0 -1
  71. package/dist/tests/test-confirmation-flow.js +0 -43
  72. package/dist/tests/test-confirmation-flow.js.map +0 -1
  73. package/dist/tests/test-dart.d.ts +0 -1
  74. package/dist/tests/test-dart.js +0 -242
  75. package/dist/tests/test-dart.js.map +0 -1
  76. package/dist/tests/test-new-tools.d.ts +0 -4
  77. package/dist/tests/test-new-tools.js +0 -147
  78. package/dist/tests/test-new-tools.js.map +0 -1
  79. package/dist/tests/test-php-ast.d.ts +0 -1
  80. package/dist/tests/test-php-ast.js +0 -152
  81. package/dist/tests/test-php-ast.js.map +0 -1
  82. package/dist/tests/test-php.d.ts +0 -1
  83. package/dist/tests/test-php.js +0 -234
  84. package/dist/tests/test-php.js.map +0 -1
  85. package/dist/tests/test-smoke.d.ts +0 -5
  86. package/dist/tests/test-smoke.js +0 -83
  87. package/dist/tests/test-smoke.js.map +0 -1
  88. package/dist/tests/writers/test-crlf-handling.d.ts +0 -4
  89. package/dist/tests/writers/test-crlf-handling.js +0 -73
  90. package/dist/tests/writers/test-crlf-handling.js.map +0 -1
  91. package/dist/tests/writers/test-write-smoke.d.ts +0 -9
  92. package/dist/tests/writers/test-write-smoke.js +0 -346
  93. package/dist/tests/writers/test-write-smoke.js.map +0 -1
  94. package/src/ast/dartCompressor.ts +0 -1013
  95. package/src/ast/phpCompressor.ts +0 -814
  96. package/src/ast/semanticCompressor.ts +0 -808
  97. package/src/ast/writers/dartWriter.ts +0 -648
  98. package/src/ast/writers/phpWriter.ts +0 -508
  99. package/src/ast/writers/pyWriter.ts +0 -446
  100. package/src/ast/writers/symbolWriter.ts +0 -277
  101. package/src/ast/writers/tsWriter.ts +0 -418
  102. package/src/index.ts +0 -1784
  103. package/src/tools/readFileLines.ts +0 -104
  104. package/src/tools/searchCodePattern.ts +0 -129
  105. package/src/utils/backupManager.ts +0 -141
  106. package/src/utils/confirmationCache.ts +0 -61
  107. package/src/utils/diffEngine.ts +0 -223
  108. package/src/utils/fuzzyMatch.ts +0 -86
  109. package/src/utils/ignoreManager.ts +0 -144
  110. package/src/utils/projectRoot.ts +0 -44
  111. package/tests/test-backup-system.ts +0 -174
  112. package/tests/test-confirmation-flow.ts +0 -49
  113. package/tests/test-dart.d.ts +0 -1
  114. package/tests/test-dart.js +0 -209
  115. package/tests/test-dart.js.map +0 -1
  116. package/tests/test-dart.ts +0 -265
  117. package/tests/test-new-tools.ts +0 -165
  118. package/tests/test-php-ast.d.ts +0 -1
  119. package/tests/test-php-ast.js +0 -152
  120. package/tests/test-php-ast.js.map +0 -1
  121. package/tests/test-php-ast.ts +0 -155
  122. package/tests/test-php.d.ts +0 -1
  123. package/tests/test-php.js +0 -234
  124. package/tests/test-php.js.map +0 -1
  125. package/tests/test-php.ts +0 -261
  126. package/tests/test-smoke.d.ts +0 -5
  127. package/tests/test-smoke.js +0 -83
  128. package/tests/test-smoke.js.map +0 -1
  129. package/tests/test-smoke.ts +0 -93
  130. package/tests/writers/test-crlf-handling.ts +0 -100
  131. package/tests/writers/test-write-smoke.ts +0 -568
  132. package/tsconfig.json +0 -19
@@ -1,104 +0,0 @@
1
- /**
2
- * read_file_lines — Read specific line ranges from a file
3
- *
4
- * Efficiently reads only the requested lines without loading the entire file into memory.
5
- * Supports reading by exact line range or around a pattern match.
6
- */
7
-
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
10
-
11
- export interface ReadFileLinesArgs {
12
- filePath: string;
13
- startLine?: number;
14
- endLine?: number;
15
- aroundPattern?: string;
16
- contextLines?: number;
17
- }
18
-
19
- export interface ReadFileLinesResult {
20
- success: boolean;
21
- content?: string;
22
- error?: string;
23
- lineRange?: { start: number; end: number };
24
- }
25
-
26
- /**
27
- * Read specific lines from a file
28
- */
29
- export function readFileLines(args: ReadFileLinesArgs): ReadFileLinesResult {
30
- const { filePath, startLine, endLine, aroundPattern, contextLines = 5 } = args;
31
-
32
- const resolvedPath = path.resolve(filePath);
33
-
34
- if (!fs.existsSync(resolvedPath)) {
35
- return {
36
- success: false,
37
- error: `File not found: ${resolvedPath}`,
38
- };
39
- }
40
-
41
- let content: string;
42
- try {
43
- content = fs.readFileSync(resolvedPath, "utf-8");
44
- } catch (error: unknown) {
45
- return {
46
- success: false,
47
- error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
48
- };
49
- }
50
-
51
- const lines = content.split("\n");
52
- const totalLines = lines.length;
53
-
54
- // Mode 1: Exact line range
55
- if (startLine !== undefined && endLine !== undefined) {
56
- if (startLine < 1 || endLine < 1 || startLine > totalLines || endLine > totalLines) {
57
- return {
58
- success: false,
59
- error: `Invalid line range: ${startLine}-${endLine}. File has ${totalLines} lines.`,
60
- };
61
- }
62
-
63
- if (startLine > endLine) {
64
- return {
65
- success: false,
66
- error: `startLine (${startLine}) must be <= endLine (${endLine})`,
67
- };
68
- }
69
-
70
- const selectedLines = lines.slice(startLine - 1, endLine);
71
- return {
72
- success: true,
73
- content: selectedLines.join("\n"),
74
- lineRange: { start: startLine, end: endLine },
75
- };
76
- }
77
-
78
- // Mode 2: Around pattern
79
- if (aroundPattern) {
80
- const matchIndex = lines.findIndex((line) => line.includes(aroundPattern));
81
-
82
- if (matchIndex === -1) {
83
- return {
84
- success: false,
85
- error: `Pattern "${aroundPattern}" not found in file`,
86
- };
87
- }
88
-
89
- const start = Math.max(0, matchIndex - contextLines);
90
- const end = Math.min(totalLines - 1, matchIndex + contextLines);
91
-
92
- const selectedLines = lines.slice(start, end + 1);
93
- return {
94
- success: true,
95
- content: selectedLines.join("\n"),
96
- lineRange: { start: start + 1, end: end + 1 },
97
- };
98
- }
99
-
100
- return {
101
- success: false,
102
- error: "Must provide either (startLine + endLine) or aroundPattern",
103
- };
104
- }
@@ -1,129 +0,0 @@
1
- /**
2
- * search_code_pattern — Search for code patterns across files
3
- *
4
- * AST-aware search that finds patterns in code while optionally excluding
5
- * matches in strings, comments, and other non-code contexts.
6
- */
7
-
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
10
- import { IgnoreManager } from "../utils/ignoreManager.js";
11
-
12
- export interface SearchCodePatternArgs {
13
- rootDir: string;
14
- pattern: string;
15
- fileExtensions?: string[];
16
- excludeDirs?: string[];
17
- showContext?: boolean;
18
- contextLines?: number;
19
- maxResults?: number;
20
- astAware?: boolean;
21
- }
22
-
23
- export interface SearchMatch {
24
- file: string;
25
- lineNumber: number;
26
- line: string;
27
- context?: string[];
28
- }
29
-
30
- export interface SearchCodePatternResult {
31
- success: boolean;
32
- matches?: SearchMatch[];
33
- error?: string;
34
- totalMatches?: number;
35
- }
36
-
37
- const DEFAULT_EXTENSIONS = [
38
- ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs",
39
- ".py", ".php", ".dart", ".java", ".go", ".rs"
40
- ];
41
-
42
- /**
43
- * Search for a pattern across code files
44
- */
45
- export function searchCodePattern(args: SearchCodePatternArgs): SearchCodePatternResult {
46
- const {
47
- rootDir,
48
- pattern,
49
- fileExtensions = DEFAULT_EXTENSIONS,
50
- excludeDirs = ["node_modules", "dist", "build", ".git"],
51
- showContext = true,
52
- contextLines = 3,
53
- maxResults = 50,
54
- astAware = false,
55
- } = args;
56
-
57
- const resolvedRoot = path.resolve(rootDir);
58
-
59
- if (!fs.existsSync(resolvedRoot)) {
60
- return {
61
- success: false,
62
- error: `Directory not found: ${resolvedRoot}`,
63
- };
64
- }
65
-
66
- const ignoreManager = new IgnoreManager(resolvedRoot);
67
- const allFiles = ignoreManager.walkDirectory();
68
-
69
- const matches: SearchMatch[] = [];
70
- let totalMatches = 0;
71
-
72
- const regex = new RegExp(pattern, "gi");
73
-
74
- for (const file of allFiles) {
75
- // Check extension
76
- const ext = path.extname(file).toLowerCase();
77
- if (!fileExtensions.includes(ext)) continue;
78
-
79
- // Check excluded directories
80
- const relativePath = path.relative(resolvedRoot, file);
81
- if (excludeDirs.some(dir => relativePath.includes(dir))) continue;
82
-
83
- let content: string;
84
- try {
85
- content = fs.readFileSync(file, "utf-8");
86
- } catch {
87
- continue;
88
- }
89
-
90
- const lines = content.split("\n");
91
-
92
- for (let i = 0; i < lines.length; i++) {
93
- const line = lines[i];
94
-
95
- if (regex.test(line)) {
96
- totalMatches++;
97
-
98
- if (matches.length < maxResults) {
99
- const match: SearchMatch = {
100
- file: path.relative(resolvedRoot, file).replace(/\\/g, "/"),
101
- lineNumber: i + 1,
102
- line: line.trim(),
103
- };
104
-
105
- if (showContext) {
106
- const start = Math.max(0, i - contextLines);
107
- const end = Math.min(lines.length - 1, i + contextLines);
108
- match.context = lines.slice(start, end + 1).map((l, idx) => {
109
- const lineNum = start + idx + 1;
110
- const marker = lineNum === i + 1 ? ">" : " ";
111
- return `${marker} ${lineNum}: ${l}`;
112
- });
113
- }
114
-
115
- matches.push(match);
116
- }
117
- }
118
-
119
- // Reset regex lastIndex for global flag
120
- regex.lastIndex = 0;
121
- }
122
- }
123
-
124
- return {
125
- success: true,
126
- matches,
127
- totalMatches,
128
- };
129
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * backupManager.ts — File Backup & Restore Utility
3
- *
4
- * Provides opt-in backup functionality for surgical write operations.
5
- * Creates centralized `.mcp-backups/` directory at project root.
6
- *
7
- * Usage:
8
- * - Before modifying: createBackup(filePath) → returns backup path
9
- * - On failure: restoreBackup(filePath) → restores from backup
10
- * - Cleanup: cleanAllBackups(projectRoot) → removes all backups
11
- */
12
-
13
- import * as fs from "node:fs";
14
- import * as path from "node:path";
15
- import { findProjectRoot } from "./projectRoot.js";
16
-
17
- /**
18
- * Create a backup copy of a file before modifying it.
19
- * Keeps a rolling window of the last 5 backups per file in a centralized directory.
20
- *
21
- * Backups are stored at: [project-root]/.mcp-backups/[relative-path]/[filename].N.backup
22
- */
23
- export function createBackup(filePath: string): string {
24
- if (!fs.existsSync(filePath)) return "";
25
-
26
- // Find project root
27
- const projectRoot = findProjectRoot(filePath);
28
- if (!projectRoot) {
29
- // Fallback to old behavior if no project root found
30
- return createBackupLegacy(filePath);
31
- }
32
-
33
- // Calculate relative path from project root
34
- const relativePath = path.relative(projectRoot, filePath);
35
- const parsed = path.parse(relativePath);
36
-
37
- // Create centralized backup directory
38
- const backupDir = path.join(projectRoot, ".mcp-backups", parsed.dir);
39
-
40
- if (!fs.existsSync(backupDir)) {
41
- fs.mkdirSync(backupDir, { recursive: true });
42
- }
43
-
44
- // Push existing backups down the stack (max 5)
45
- for (let i = 4; i >= 1; i--) {
46
- const oldB = path.join(backupDir, `${parsed.base}.${i}.backup`);
47
- const newB = path.join(backupDir, `${parsed.base}.${i + 1}.backup`);
48
- if (fs.existsSync(oldB)) {
49
- if (i === 4 && fs.existsSync(newB)) {
50
- fs.unlinkSync(newB); // Drop the 5th one to make room
51
- }
52
- fs.renameSync(oldB, newB);
53
- }
54
- }
55
-
56
- const backupPath = path.join(backupDir, `${parsed.base}.1.backup`);
57
- fs.copyFileSync(filePath, backupPath);
58
- return backupPath;
59
- }
60
-
61
- /**
62
- * Legacy backup function (creates .mcp-backups in same directory)
63
- * Used as fallback when project root cannot be determined
64
- */
65
- function createBackupLegacy(filePath: string): string {
66
- const parsed = path.parse(filePath);
67
- const backupDir = path.join(parsed.dir, ".mcp-backups");
68
-
69
- if (!fs.existsSync(backupDir)) {
70
- fs.mkdirSync(backupDir, { recursive: true });
71
- }
72
-
73
- for (let i = 4; i >= 1; i--) {
74
- const oldB = path.join(backupDir, `${parsed.base}.${i}.backup`);
75
- const newB = path.join(backupDir, `${parsed.base}.${i + 1}.backup`);
76
- if (fs.existsSync(oldB)) {
77
- if (i === 4 && fs.existsSync(newB)) {
78
- fs.unlinkSync(newB);
79
- }
80
- fs.renameSync(oldB, newB);
81
- }
82
- }
83
-
84
- const backupPath = path.join(backupDir, `${parsed.base}.1.backup`);
85
- fs.copyFileSync(filePath, backupPath);
86
- return backupPath;
87
- }
88
-
89
- /**
90
- * Restore a file from its backup.
91
- */
92
- export function restoreBackup(filePath: string, steps = 1): boolean {
93
- // Try centralized backup first
94
- const projectRoot = findProjectRoot(filePath);
95
-
96
- if (projectRoot) {
97
- const relativePath = path.relative(projectRoot, filePath);
98
- const parsed = path.parse(relativePath);
99
- const backupDir = path.join(projectRoot, ".mcp-backups", parsed.dir);
100
- const targetBackup = path.join(backupDir, `${parsed.base}.${steps}.backup`);
101
-
102
- if (fs.existsSync(targetBackup)) {
103
- fs.copyFileSync(targetBackup, filePath);
104
- return true;
105
- }
106
- }
107
-
108
- // Fallback to legacy location
109
- const parsed = path.parse(filePath);
110
- const backupDir = path.join(parsed.dir, ".mcp-backups");
111
- const targetBackup = path.join(backupDir, `${parsed.base}.${steps}.backup`);
112
-
113
- if (!fs.existsSync(targetBackup)) return false;
114
-
115
- fs.copyFileSync(targetBackup, filePath);
116
- return true;
117
- }
118
-
119
- /**
120
- * Clean all backups for a project.
121
- * Removes the entire .mcp-backups directory at project root.
122
- */
123
- export function cleanAllBackups(projectRoot: string): boolean {
124
- const backupDir = path.join(projectRoot, ".mcp-backups");
125
-
126
- if (!fs.existsSync(backupDir)) return false;
127
-
128
- try {
129
- fs.rmSync(backupDir, { recursive: true, force: true });
130
- return true;
131
- } catch {
132
- return false;
133
- }
134
- }
135
-
136
- /**
137
- * Clean legacy backups (no-op, kept for compatibility).
138
- */
139
- export function cleanBackup(filePath: string): void {
140
- // No-op. We keep the rolling 5 versions instead of deleting.
141
- }
@@ -1,61 +0,0 @@
1
- import crypto from "node:crypto";
2
-
3
- export interface PendingOperation {
4
- token: string;
5
- operation: "write_file_surgical" | "insert_symbol" | "rename_symbol" | "remove_symbol";
6
- filePath: string;
7
- payload: any; // Contains specific args needed to re-execute or apply
8
- expiresAt: number;
9
- }
10
-
11
- const CACHE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
12
-
13
- class ConfirmationCacheManager {
14
- private cache = new Map<string, PendingOperation>();
15
-
16
- public add(
17
- operation: PendingOperation["operation"],
18
- filePath: string,
19
- payload: any
20
- ): string {
21
- const token = crypto.randomUUID();
22
-
23
- this.cache.set(token, {
24
- token,
25
- operation,
26
- filePath,
27
- payload,
28
- expiresAt: Date.now() + CACHE_TIMEOUT_MS,
29
- });
30
-
31
- // Clean up expired items lazily
32
- this.cleanup();
33
-
34
- return token;
35
- }
36
-
37
- public get(token: string): PendingOperation | undefined {
38
- this.cleanup();
39
- return this.cache.get(token);
40
- }
41
-
42
- public consume(token: string): PendingOperation | undefined {
43
- this.cleanup();
44
- const op = this.cache.get(token);
45
- if (op) {
46
- this.cache.delete(token);
47
- }
48
- return op;
49
- }
50
-
51
- private cleanup() {
52
- const now = Date.now();
53
- for (const [key, value] of this.cache.entries()) {
54
- if (now > value.expiresAt) {
55
- this.cache.delete(key);
56
- }
57
- }
58
- }
59
- }
60
-
61
- export const confirmationCache = new ConfirmationCacheManager();
@@ -1,223 +0,0 @@
1
- /**
2
- * diffEngine.ts — Unified Diff Generator
3
- *
4
- * Generates human-readable unified diffs (like `git diff`) for dry-run reports
5
- * and post-write summaries. Uses a simple Myers-style diff algorithm.
6
- * No external dependencies.
7
- */
8
-
9
- // ─── Types ──────────────────────────────────────────────────────────
10
-
11
- interface DiffHunk {
12
- oldStart: number;
13
- oldCount: number;
14
- newStart: number;
15
- newCount: number;
16
- lines: string[];
17
- }
18
-
19
- // ─── Public API ─────────────────────────────────────────────────────
20
-
21
- /**
22
- * Generate a unified diff between two versions of a single file.
23
- * Returns a string formatted like `git diff --unified=3`.
24
- */
25
- export function generateDiff(
26
- oldContent: string,
27
- newContent: string,
28
- filePath: string,
29
- contextLines = 3,
30
- ): string {
31
- const oldLines = oldContent.split("\n");
32
- const newLines = newContent.split("\n");
33
-
34
- const hunks = computeHunks(oldLines, newLines, contextLines);
35
-
36
- if (hunks.length === 0) {
37
- return `--- ${filePath}\n+++ ${filePath}\n(no changes)\n`;
38
- }
39
-
40
- const header = [
41
- `--- a/${normalizedPath(filePath)}`,
42
- `+++ b/${normalizedPath(filePath)}`,
43
- ];
44
-
45
- const hunkTexts = hunks.map((hunk) => {
46
- const hunkHeader = `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`;
47
- return [hunkHeader, ...hunk.lines].join("\n");
48
- });
49
-
50
- return [...header, ...hunkTexts].join("\n") + "\n";
51
- }
52
-
53
- /**
54
- * Generate a combined diff across multiple files.
55
- * Returns a single string with all file diffs separated by blank lines.
56
- */
57
- export function generateMultiFileDiff(
58
- changes: Array<{
59
- filePath: string;
60
- oldContent: string;
61
- newContent: string;
62
- }>,
63
- ): string {
64
- const diffs: string[] = [];
65
-
66
- for (const change of changes) {
67
- const diff = generateDiff(
68
- change.oldContent,
69
- change.newContent,
70
- change.filePath,
71
- );
72
- diffs.push(diff);
73
- }
74
-
75
- return diffs.join("\n");
76
- }
77
-
78
- // ─── Diff Algorithm ─────────────────────────────────────────────────
79
-
80
- /**
81
- * Compute the Longest Common Subsequence table for two arrays of strings.
82
- */
83
- function computeLCS(a: string[], b: string[]): number[][] {
84
- const m = a.length;
85
- const n = b.length;
86
-
87
- // Use a 2D array for the LCS table
88
- const dp: number[][] = Array.from({ length: m + 1 }, () =>
89
- new Array(n + 1).fill(0),
90
- );
91
-
92
- for (let i = 1; i <= m; i++) {
93
- for (let j = 1; j <= n; j++) {
94
- if (a[i - 1] === b[j - 1]) {
95
- dp[i][j] = dp[i - 1][j - 1] + 1;
96
- } else {
97
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
98
- }
99
- }
100
- }
101
-
102
- return dp;
103
- }
104
-
105
- /**
106
- * Backtrack through the LCS table to produce a list of diff operations.
107
- */
108
- function backtrackDiff(
109
- dp: number[][],
110
- a: string[],
111
- b: string[],
112
- ): Array<{ type: "keep" | "remove" | "add"; line: string }> {
113
- const result: Array<{ type: "keep" | "remove" | "add"; line: string }> = [];
114
- let i = a.length;
115
- let j = b.length;
116
-
117
- while (i > 0 || j > 0) {
118
- if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
119
- result.unshift({ type: "keep", line: a[i - 1] });
120
- i--;
121
- j--;
122
- } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
123
- result.unshift({ type: "add", line: b[j - 1] });
124
- j--;
125
- } else if (i > 0) {
126
- result.unshift({ type: "remove", line: a[i - 1] });
127
- i--;
128
- }
129
- }
130
-
131
- return result;
132
- }
133
-
134
- /**
135
- * Group diff operations into hunks with surrounding context.
136
- */
137
- function computeHunks(
138
- oldLines: string[],
139
- newLines: string[],
140
- contextLines: number,
141
- ): DiffHunk[] {
142
- const dp = computeLCS(oldLines, newLines);
143
- const ops = backtrackDiff(dp, oldLines, newLines);
144
-
145
- // Find ranges of changes (non-"keep" operations)
146
- const changeIndices: number[] = [];
147
- for (let i = 0; i < ops.length; i++) {
148
- if (ops[i].type !== "keep") {
149
- changeIndices.push(i);
150
- }
151
- }
152
-
153
- if (changeIndices.length === 0) return [];
154
-
155
- // Group nearby changes into hunks
156
- const groups: Array<{ start: number; end: number }> = [];
157
- let groupStart = changeIndices[0];
158
- let groupEnd = changeIndices[0];
159
-
160
- for (let k = 1; k < changeIndices.length; k++) {
161
- if (changeIndices[k] - groupEnd <= contextLines * 2 + 1) {
162
- // Merge into current group
163
- groupEnd = changeIndices[k];
164
- } else {
165
- groups.push({ start: groupStart, end: groupEnd });
166
- groupStart = changeIndices[k];
167
- groupEnd = changeIndices[k];
168
- }
169
- }
170
- groups.push({ start: groupStart, end: groupEnd });
171
-
172
- // Build hunks with context
173
- const hunks: DiffHunk[] = [];
174
-
175
- for (const group of groups) {
176
- const hunkStart = Math.max(0, group.start - contextLines);
177
- const hunkEnd = Math.min(ops.length - 1, group.end + contextLines);
178
-
179
- const lines: string[] = [];
180
- let oldLineNum = 0;
181
- let newLineNum = 0;
182
-
183
- // Count lines before hunk start to get correct line numbers
184
- for (let i = 0; i < hunkStart; i++) {
185
- if (ops[i].type === "keep" || ops[i].type === "remove") oldLineNum++;
186
- if (ops[i].type === "keep" || ops[i].type === "add") newLineNum++;
187
- }
188
-
189
- const oldStart = oldLineNum + 1;
190
- const newStart = newLineNum + 1;
191
- let oldCount = 0;
192
- let newCount = 0;
193
-
194
- for (let i = hunkStart; i <= hunkEnd; i++) {
195
- const op = ops[i];
196
- switch (op.type) {
197
- case "keep":
198
- lines.push(` ${op.line}`);
199
- oldCount++;
200
- newCount++;
201
- break;
202
- case "remove":
203
- lines.push(`-${op.line}`);
204
- oldCount++;
205
- break;
206
- case "add":
207
- lines.push(`+${op.line}`);
208
- newCount++;
209
- break;
210
- }
211
- }
212
-
213
- hunks.push({ oldStart, oldCount, newStart, newCount, lines });
214
- }
215
-
216
- return hunks;
217
- }
218
-
219
- // ─── Utilities ──────────────────────────────────────────────────────
220
-
221
- function normalizedPath(filePath: string): string {
222
- return filePath.replace(/\\/g, "/");
223
- }