@wundr.io/cli 1.0.11 → 1.0.13

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 (180) hide show
  1. package/bin/wundr.js +8 -4
  2. package/dist/ai/ai-service.d.ts.map +1 -1
  3. package/dist/ai/ai-service.js.map +1 -1
  4. package/dist/ai/claude-client.js.map +1 -1
  5. package/dist/ai/conversation-manager.js.map +1 -1
  6. package/dist/commands/ai.d.ts.map +1 -1
  7. package/dist/commands/ai.js +179 -24
  8. package/dist/commands/ai.js.map +1 -1
  9. package/dist/commands/analyze-optimized.d.ts.map +1 -1
  10. package/dist/commands/analyze-optimized.js +15 -6
  11. package/dist/commands/analyze-optimized.js.map +1 -1
  12. package/dist/commands/batch.d.ts +22 -0
  13. package/dist/commands/batch.d.ts.map +1 -1
  14. package/dist/commands/batch.js +130 -14
  15. package/dist/commands/batch.js.map +1 -1
  16. package/dist/commands/chat.d.ts +1 -0
  17. package/dist/commands/chat.d.ts.map +1 -1
  18. package/dist/commands/chat.js +7 -3
  19. package/dist/commands/chat.js.map +1 -1
  20. package/dist/commands/claude-init.d.ts +1 -1
  21. package/dist/commands/claude-init.d.ts.map +1 -1
  22. package/dist/commands/claude-init.js +16 -16
  23. package/dist/commands/claude-init.js.map +1 -1
  24. package/dist/commands/claude-setup.d.ts +5 -5
  25. package/dist/commands/claude-setup.d.ts.map +1 -1
  26. package/dist/commands/claude-setup.js +65 -59
  27. package/dist/commands/claude-setup.js.map +1 -1
  28. package/dist/commands/computer-setup.d.ts +1 -0
  29. package/dist/commands/computer-setup.d.ts.map +1 -1
  30. package/dist/commands/computer-setup.js +35 -7
  31. package/dist/commands/computer-setup.js.map +1 -1
  32. package/dist/commands/dashboard.js.map +1 -1
  33. package/dist/commands/govern.js.map +1 -1
  34. package/dist/commands/init.d.ts.map +1 -1
  35. package/dist/commands/init.js +3 -3
  36. package/dist/commands/init.js.map +1 -1
  37. package/dist/commands/orchestrator.d.ts.map +1 -1
  38. package/dist/commands/orchestrator.js +11 -4
  39. package/dist/commands/orchestrator.js.map +1 -1
  40. package/dist/commands/performance-optimizer.d.ts.map +1 -1
  41. package/dist/commands/performance-optimizer.js.map +1 -1
  42. package/dist/commands/rag.d.ts.map +1 -1
  43. package/dist/commands/rag.js +9 -6
  44. package/dist/commands/rag.js.map +1 -1
  45. package/dist/commands/setup.d.ts +5 -10
  46. package/dist/commands/setup.d.ts.map +1 -1
  47. package/dist/commands/setup.js +35 -260
  48. package/dist/commands/setup.js.map +1 -1
  49. package/dist/commands/watch.d.ts.map +1 -1
  50. package/dist/commands/watch.js.map +1 -1
  51. package/dist/context/session-manager.js.map +1 -1
  52. package/dist/framework/command-interface.d.ts +349 -0
  53. package/dist/framework/command-interface.d.ts.map +1 -0
  54. package/dist/framework/command-interface.js +101 -0
  55. package/dist/framework/command-interface.js.map +1 -0
  56. package/dist/framework/command-registry.d.ts +173 -0
  57. package/dist/framework/command-registry.d.ts.map +1 -0
  58. package/dist/framework/command-registry.js +734 -0
  59. package/dist/framework/command-registry.js.map +1 -0
  60. package/dist/framework/completion-exporter.d.ts +79 -0
  61. package/dist/framework/completion-exporter.d.ts.map +1 -0
  62. package/dist/framework/completion-exporter.js +259 -0
  63. package/dist/framework/completion-exporter.js.map +1 -0
  64. package/dist/framework/debug-logger.d.ts +163 -0
  65. package/dist/framework/debug-logger.d.ts.map +1 -0
  66. package/dist/framework/debug-logger.js +373 -0
  67. package/dist/framework/debug-logger.js.map +1 -0
  68. package/dist/framework/error-handler.d.ts +196 -0
  69. package/dist/framework/error-handler.d.ts.map +1 -0
  70. package/dist/framework/error-handler.js +613 -0
  71. package/dist/framework/error-handler.js.map +1 -0
  72. package/dist/framework/help-generator.d.ts +78 -0
  73. package/dist/framework/help-generator.d.ts.map +1 -0
  74. package/dist/framework/help-generator.js +414 -0
  75. package/dist/framework/help-generator.js.map +1 -0
  76. package/dist/framework/index.d.ts +62 -0
  77. package/dist/framework/index.d.ts.map +1 -0
  78. package/dist/framework/index.js +95 -0
  79. package/dist/framework/index.js.map +1 -0
  80. package/dist/framework/interactive-repl.d.ts +138 -0
  81. package/dist/framework/interactive-repl.d.ts.map +1 -0
  82. package/dist/framework/interactive-repl.js +567 -0
  83. package/dist/framework/interactive-repl.js.map +1 -0
  84. package/dist/framework/output-formatter.d.ts +274 -0
  85. package/dist/framework/output-formatter.d.ts.map +1 -0
  86. package/dist/framework/output-formatter.js +545 -0
  87. package/dist/framework/output-formatter.js.map +1 -0
  88. package/dist/framework/progress-manager.d.ts +192 -0
  89. package/dist/framework/progress-manager.d.ts.map +1 -0
  90. package/dist/framework/progress-manager.js +408 -0
  91. package/dist/framework/progress-manager.js.map +1 -0
  92. package/dist/interactive/interactive-mode.js.map +1 -1
  93. package/dist/nlp/command-mapper.js.map +1 -1
  94. package/dist/nlp/command-parser.js.map +1 -1
  95. package/dist/nlp/intent-parser.d.ts.map +1 -1
  96. package/dist/nlp/intent-parser.js +4 -2
  97. package/dist/nlp/intent-parser.js.map +1 -1
  98. package/dist/plugins/plugin-manager.d.ts +2 -1
  99. package/dist/plugins/plugin-manager.d.ts.map +1 -1
  100. package/dist/plugins/plugin-manager.js +30 -19
  101. package/dist/plugins/plugin-manager.js.map +1 -1
  102. package/dist/utils/backup-rollback-manager.d.ts.map +1 -1
  103. package/dist/utils/backup-rollback-manager.js +1 -2
  104. package/dist/utils/backup-rollback-manager.js.map +1 -1
  105. package/dist/utils/logger.js.map +1 -1
  106. package/package.json +6 -6
  107. package/src/ai/ai-service.ts +16 -17
  108. package/src/ai/claude-client.ts +16 -16
  109. package/src/ai/conversation-manager.ts +29 -29
  110. package/src/cli.ts +4 -4
  111. package/src/commands/ai.ts +246 -78
  112. package/src/commands/alignment.ts +74 -74
  113. package/src/commands/analyze-optimized.ts +111 -78
  114. package/src/commands/analyze.ts +14 -14
  115. package/src/commands/batch.ts +179 -42
  116. package/src/commands/chat.ts +37 -30
  117. package/src/commands/claude-init.ts +41 -45
  118. package/src/commands/claude-setup.ts +204 -119
  119. package/src/commands/computer-setup.ts +85 -43
  120. package/src/commands/create-command.ts +4 -4
  121. package/src/commands/create.ts +27 -27
  122. package/src/commands/dashboard.ts +24 -24
  123. package/src/commands/govern.ts +25 -25
  124. package/src/commands/governance.ts +34 -34
  125. package/src/commands/guardian.ts +56 -56
  126. package/src/commands/init.ts +25 -22
  127. package/src/commands/orchestrator.ts +68 -41
  128. package/src/commands/performance-optimizer.ts +34 -35
  129. package/src/commands/plugins.ts +27 -27
  130. package/src/commands/project-update.ts +175 -72
  131. package/src/commands/rag.ts +185 -78
  132. package/src/commands/session.ts +35 -35
  133. package/src/commands/setup.ts +40 -344
  134. package/src/commands/test-init.ts +3 -3
  135. package/src/commands/test.ts +4 -4
  136. package/src/commands/watch.ts +28 -29
  137. package/src/commands/worktree.ts +49 -49
  138. package/src/context/context-manager.ts +10 -10
  139. package/src/context/session-manager.ts +41 -41
  140. package/src/framework/command-interface.ts +520 -0
  141. package/src/framework/command-registry.ts +942 -0
  142. package/src/framework/completion-exporter.ts +383 -0
  143. package/src/framework/debug-logger.ts +519 -0
  144. package/src/framework/error-handler.ts +867 -0
  145. package/src/framework/help-generator.ts +540 -0
  146. package/src/framework/index.ts +169 -0
  147. package/src/framework/interactive-repl.ts +703 -0
  148. package/src/framework/output-formatter.ts +834 -0
  149. package/src/framework/progress-manager.ts +539 -0
  150. package/src/index.ts +4 -4
  151. package/src/interactive/interactive-mode.ts +16 -16
  152. package/src/lib/conflict-resolution.ts +799 -9
  153. package/src/lib/merge-strategy.ts +529 -7
  154. package/src/lib/safety-mechanisms.ts +422 -18
  155. package/src/lib/state-detection.ts +1015 -13
  156. package/src/nlp/command-mapper.ts +29 -29
  157. package/src/nlp/command-parser.ts +17 -17
  158. package/src/nlp/intent-classifier.ts +7 -7
  159. package/src/nlp/intent-parser.ts +54 -52
  160. package/src/plugins/plugin-manager.ts +61 -39
  161. package/src/tests/computer-setup-integration.test.ts +46 -15
  162. package/src/types/modules.d.ts +424 -1
  163. package/src/utils/backup-rollback-manager.ts +11 -8
  164. package/src/utils/config-manager.ts +3 -3
  165. package/src/utils/error-handler.ts +2 -2
  166. package/src/utils/logger.ts +22 -22
  167. package/templates/batch/ci-cd.yaml +7 -7
  168. package/test-suites/api/health.spec.ts +20 -23
  169. package/test-suites/helpers/test-config.ts +14 -13
  170. package/test-suites/ui/accessibility.spec.ts +27 -22
  171. package/test-suites/ui/smoke.spec.ts +26 -21
  172. package/dist/commands/computer-setup-commands.d.ts +0 -53
  173. package/dist/commands/computer-setup-commands.d.ts.map +0 -1
  174. package/dist/commands/computer-setup-commands.js +0 -705
  175. package/dist/commands/computer-setup-commands.js.map +0 -1
  176. package/dist/commands/vp.d.ts +0 -7
  177. package/dist/commands/vp.d.ts.map +0 -1
  178. package/dist/commands/vp.js +0 -571
  179. package/dist/commands/vp.js.map +0 -1
  180. package/src/commands/computer-setup-commands.ts +0 -872
@@ -1,28 +1,550 @@
1
1
  /**
2
- * Merge Strategy - Stub implementation
3
- * TODO: Implement full merge strategy system
2
+ * Merge Strategy Module
3
+ *
4
+ * Implements two-way and three-way text merge with line-based diff (LCS).
5
+ * Conflict markers follow the standard git format:
6
+ * <<<<<<< OURS
7
+ * ...ours lines...
8
+ * =======
9
+ * ...theirs lines...
10
+ * >>>>>>> THEIRS
4
11
  */
5
12
 
13
+ import * as path from 'path';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Public interfaces
17
+ // ---------------------------------------------------------------------------
18
+
6
19
  export interface MergeResult {
7
20
  success: boolean;
8
21
  content: string;
9
22
  conflicts: Array<{ line: number; description: string }>;
10
23
  }
11
24
 
25
+ export interface ThreeWayMergeOptions {
26
+ /** Original ancestor content */
27
+ base: string;
28
+ /** Local / user-modified content */
29
+ user: string;
30
+ /** Incoming / target content */
31
+ target: string;
32
+ /** Optional file path for type-specific strategies */
33
+ filePath?: string;
34
+ /** Explicit file type override */
35
+ fileType?: string;
36
+ }
37
+
38
+ export interface MergeStrategyOptions {
39
+ /** Automatically resolve conflicts by favouring the user side */
40
+ autoResolve?: boolean;
41
+ /** Preserve comment-only lines during merge */
42
+ preserveComments?: boolean;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // LCS (Longest Common Subsequence) helpers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /**
50
+ * Compute the LCS table for two arrays of strings using classic DP.
51
+ * Returns the table so callers can reconstruct diffs.
52
+ */
53
+ function buildLcsTable(a: string[], b: string[]): number[][] {
54
+ const m = a.length;
55
+ const n = b.length;
56
+ // Allocate (m+1) x (n+1) table
57
+ const dp: number[][] = Array.from({ length: m + 1 }, () =>
58
+ new Array(n + 1).fill(0)
59
+ );
60
+
61
+ for (let i = 1; i <= m; i++) {
62
+ for (let j = 1; j <= n; j++) {
63
+ if (a[i - 1] === b[j - 1]) {
64
+ dp[i][j] = dp[i - 1][j - 1] + 1;
65
+ } else {
66
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
67
+ }
68
+ }
69
+ }
70
+ return dp;
71
+ }
72
+
73
+ type DiffOp =
74
+ | { op: 'equal'; line: string }
75
+ | { op: 'insert'; line: string }
76
+ | { op: 'delete'; line: string };
77
+
78
+ /**
79
+ * Produce a sequence of diff operations between arrays `a` (old) and `b` (new)
80
+ * using the LCS table.
81
+ */
82
+ function diffLines(a: string[], b: string[]): DiffOp[] {
83
+ const dp = buildLcsTable(a, b);
84
+ const ops: DiffOp[] = [];
85
+
86
+ let i = a.length;
87
+ let j = b.length;
88
+
89
+ while (i > 0 || j > 0) {
90
+ if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
91
+ ops.push({ op: 'equal', line: a[i - 1] });
92
+ i--;
93
+ j--;
94
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
95
+ ops.push({ op: 'insert', line: b[j - 1] });
96
+ j--;
97
+ } else {
98
+ ops.push({ op: 'delete', line: a[i - 1] });
99
+ i--;
100
+ }
101
+ }
102
+
103
+ ops.reverse();
104
+ return ops;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Two-way merge
109
+ // ---------------------------------------------------------------------------
110
+
111
+ /**
112
+ * Apply changes from `modified` onto `base` using a two-way line diff.
113
+ * Lines deleted in `modified` are removed from the result; lines added in
114
+ * `modified` are appended at the appropriate positions.
115
+ *
116
+ * Parameters follow the convention in the original stub:
117
+ * merge(base, modified) - local alias: _local = base, _remote = modified
118
+ */
119
+ function applyTwoWayMerge(base: string, modified: string): MergeResult {
120
+ const baseLines = base.split('\n');
121
+ const modLines = modified.split('\n');
122
+
123
+ const ops = diffLines(baseLines, modLines);
124
+ const result: string[] = [];
125
+
126
+ for (const op of ops) {
127
+ if (op.op === 'equal' || op.op === 'insert') {
128
+ result.push(op.line);
129
+ }
130
+ // Deleted lines are simply dropped
131
+ }
132
+
133
+ return {
134
+ success: true,
135
+ content: result.join('\n'),
136
+ conflicts: [],
137
+ };
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Three-way merge
142
+ // ---------------------------------------------------------------------------
143
+
144
+ /**
145
+ * Represents a chunk of lines from one side that differs from the base.
146
+ */
147
+ interface Chunk {
148
+ /** Index in the base where this chunk starts (0-based) */
149
+ baseStart: number;
150
+ /** Index in the base where this chunk ends (exclusive) */
151
+ baseEnd: number;
152
+ /** Lines from this side (empty for pure deletions) */
153
+ lines: string[];
154
+ /** Which side produced this chunk */
155
+ side: 'ours' | 'theirs';
156
+ }
157
+
158
+ /**
159
+ * Extract changed chunks from a diff between base lines and side lines.
160
+ * Each contiguous run of non-equal operations becomes one chunk.
161
+ */
162
+ function extractChunks(
163
+ baseLines: string[],
164
+ sideLines: string[],
165
+ side: 'ours' | 'theirs'
166
+ ): Chunk[] {
167
+ const ops = diffLines(baseLines, sideLines);
168
+ const chunks: Chunk[] = [];
169
+
170
+ // Map operations back to base indices
171
+ let baseIdx = 0;
172
+ let opIdx = 0;
173
+
174
+ while (opIdx < ops.length) {
175
+ const op = ops[opIdx];
176
+
177
+ if (op.op === 'equal') {
178
+ baseIdx++;
179
+ opIdx++;
180
+ continue;
181
+ }
182
+
183
+ // Start of a non-equal run
184
+ const chunkBaseStart = baseIdx;
185
+ const chunkLines: string[] = [];
186
+
187
+ while (opIdx < ops.length && ops[opIdx].op !== 'equal') {
188
+ const cur = ops[opIdx];
189
+ if (cur.op === 'delete') {
190
+ baseIdx++;
191
+ } else {
192
+ // insert
193
+ chunkLines.push(cur.line);
194
+ }
195
+ opIdx++;
196
+ }
197
+
198
+ chunks.push({
199
+ baseStart: chunkBaseStart,
200
+ baseEnd: baseIdx,
201
+ lines: chunkLines,
202
+ side,
203
+ });
204
+ }
205
+
206
+ return chunks;
207
+ }
208
+
209
+ /**
210
+ * Core three-way merge implementation.
211
+ *
212
+ * Algorithm:
213
+ * 1. Compute diffs of `ours` vs `base` and `theirs` vs `base`.
214
+ * 2. Walk the base line-by-line, applying non-overlapping chunks from each side.
215
+ * 3. When both sides modify the same base region differently, emit conflict markers.
216
+ */
217
+ function performThreeWayMerge(
218
+ base: string,
219
+ ours: string,
220
+ theirs: string,
221
+ autoResolve = false
222
+ ): MergeResult {
223
+ // Fast-path: all identical
224
+ if (base === ours && base === theirs) {
225
+ return { success: true, content: base, conflicts: [] };
226
+ }
227
+ // Fast-path: only one side changed
228
+ if (base === ours) {
229
+ return { success: true, content: theirs, conflicts: [] };
230
+ }
231
+ if (base === theirs) {
232
+ return { success: true, content: ours, conflicts: [] };
233
+ }
234
+ // Fast-path: both sides are identical to each other (regardless of base)
235
+ if (ours === theirs) {
236
+ return { success: true, content: ours, conflicts: [] };
237
+ }
238
+
239
+ const baseLines = base.split('\n');
240
+ const oursLines = ours.split('\n');
241
+ const theirsLines = theirs.split('\n');
242
+
243
+ const ourChunks = extractChunks(baseLines, oursLines, 'ours');
244
+ const theirChunks = extractChunks(baseLines, theirsLines, 'theirs');
245
+
246
+ // Build a map: baseLineIndex -> list of chunks that START at that index
247
+ const ourChunkMap = new Map<number, Chunk>();
248
+ const theirChunkMap = new Map<number, Chunk>();
249
+
250
+ for (const c of ourChunks) {
251
+ ourChunkMap.set(c.baseStart, c);
252
+ }
253
+ for (const c of theirChunks) {
254
+ theirChunkMap.set(c.baseStart, c);
255
+ }
256
+
257
+ const result: string[] = [];
258
+ const conflicts: Array<{ line: number; description: string }> = [];
259
+
260
+ let i = 0; // current base line index
261
+
262
+ while (i <= baseLines.length) {
263
+ const ourChunk = ourChunkMap.get(i);
264
+ const theirChunk = theirChunkMap.get(i);
265
+
266
+ if (!ourChunk && !theirChunk) {
267
+ // No changes at this position - emit base line if it exists
268
+ if (i < baseLines.length) {
269
+ result.push(baseLines[i]);
270
+ }
271
+ i++;
272
+ continue;
273
+ }
274
+
275
+ if (ourChunk && !theirChunk) {
276
+ // Only ours changed this region
277
+ for (const line of ourChunk.lines) {
278
+ result.push(line);
279
+ }
280
+ i = ourChunk.baseEnd;
281
+ continue;
282
+ }
283
+
284
+ if (!ourChunk && theirChunk) {
285
+ // Only theirs changed this region
286
+ for (const line of theirChunk.lines) {
287
+ result.push(line);
288
+ }
289
+ i = theirChunk.baseEnd;
290
+ continue;
291
+ }
292
+
293
+ // Both sides have a chunk starting here - check for real conflict
294
+ // They conflict if they modify overlapping base regions differently
295
+ const ourEnd = ourChunk!.baseEnd;
296
+ const theirEnd = theirChunk!.baseEnd;
297
+
298
+ // If the resulting content is the same, no conflict
299
+ if (ourChunk!.lines.join('\n') === theirChunk!.lines.join('\n')) {
300
+ for (const line of ourChunk!.lines) {
301
+ result.push(line);
302
+ }
303
+ i = Math.max(ourEnd, theirEnd);
304
+ continue;
305
+ }
306
+
307
+ if (autoResolve) {
308
+ // Prefer ours when auto-resolving
309
+ for (const line of ourChunk!.lines) {
310
+ result.push(line);
311
+ }
312
+ i = Math.max(ourEnd, theirEnd);
313
+ continue;
314
+ }
315
+
316
+ // Emit conflict markers
317
+ const conflictLineNumber = result.length + 1;
318
+ result.push('<<<<<<< OURS');
319
+ for (const line of ourChunk!.lines) {
320
+ result.push(line);
321
+ }
322
+ result.push('=======');
323
+ for (const line of theirChunk!.lines) {
324
+ result.push(line);
325
+ }
326
+ result.push('>>>>>>> THEIRS');
327
+
328
+ conflicts.push({
329
+ line: conflictLineNumber,
330
+ description: `Conflict at base line ${i + 1}: both sides modified this region differently`,
331
+ });
332
+
333
+ i = Math.max(ourEnd, theirEnd);
334
+ }
335
+
336
+ return {
337
+ success: conflicts.length === 0,
338
+ content: result.join('\n'),
339
+ conflicts,
340
+ };
341
+ }
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // JSON deep merge helper
345
+ // ---------------------------------------------------------------------------
346
+
347
+ function deepMergeJson(
348
+ base: Record<string, unknown>,
349
+ user: Record<string, unknown>,
350
+ target: Record<string, unknown>
351
+ ): Record<string, unknown> {
352
+ const merged: Record<string, unknown> = { ...target };
353
+
354
+ // Preserve user-added keys and user-modified values
355
+ for (const key of Object.keys(user)) {
356
+ if (!(key in target)) {
357
+ // User added this key; target doesn't have it - keep user's value
358
+ merged[key] = user[key];
359
+ } else if (
360
+ typeof user[key] === 'object' &&
361
+ user[key] !== null &&
362
+ !Array.isArray(user[key]) &&
363
+ typeof target[key] === 'object' &&
364
+ target[key] !== null &&
365
+ !Array.isArray(target[key])
366
+ ) {
367
+ // Both are objects - recurse
368
+ const baseVal =
369
+ typeof base[key] === 'object' &&
370
+ base[key] !== null &&
371
+ !Array.isArray(base[key])
372
+ ? (base[key] as Record<string, unknown>)
373
+ : {};
374
+ merged[key] = deepMergeJson(
375
+ baseVal,
376
+ user[key] as Record<string, unknown>,
377
+ target[key] as Record<string, unknown>
378
+ );
379
+ } else if (
380
+ key in base &&
381
+ base[key] !== user[key] &&
382
+ base[key] === target[key]
383
+ ) {
384
+ // User changed the value; target kept the base value - take user's change
385
+ merged[key] = user[key];
386
+ }
387
+ // If target also changed the value from base, target wins (already in merged)
388
+ }
389
+
390
+ return merged;
391
+ }
392
+
393
+ function mergeJson(base: string, user: string, target: string): MergeResult {
394
+ try {
395
+ const baseObj = JSON.parse(base);
396
+ const userObj = JSON.parse(user);
397
+ const targetObj = JSON.parse(target);
398
+
399
+ if (
400
+ typeof baseObj !== 'object' ||
401
+ baseObj === null ||
402
+ typeof userObj !== 'object' ||
403
+ userObj === null ||
404
+ typeof targetObj !== 'object' ||
405
+ targetObj === null
406
+ ) {
407
+ // Primitive JSON - fall back to text merge
408
+ return performThreeWayMerge(base, user, target);
409
+ }
410
+
411
+ const merged = deepMergeJson(baseObj, userObj, targetObj);
412
+ return {
413
+ success: true,
414
+ content: JSON.stringify(merged, null, 2),
415
+ conflicts: [],
416
+ };
417
+ } catch {
418
+ // Invalid JSON - fall back to text merge
419
+ return performThreeWayMerge(base, user, target);
420
+ }
421
+ }
422
+
423
+ // ---------------------------------------------------------------------------
424
+ // detectFileType
425
+ // ---------------------------------------------------------------------------
426
+
427
+ const EXT_MAP: Record<string, string> = {
428
+ '.json': 'json',
429
+ '.yaml': 'yaml',
430
+ '.yml': 'yaml',
431
+ '.md': 'markdown',
432
+ '.markdown': 'markdown',
433
+ '.ts': 'typescript',
434
+ '.tsx': 'typescript',
435
+ '.js': 'javascript',
436
+ '.jsx': 'javascript',
437
+ '.sh': 'shell',
438
+ '.bash': 'shell',
439
+ '.zsh': 'shell',
440
+ '.toml': 'toml',
441
+ '.ini': 'ini',
442
+ '.env': 'env',
443
+ '.txt': 'text',
444
+ '.xml': 'xml',
445
+ '.html': 'html',
446
+ '.css': 'css',
447
+ '.scss': 'css',
448
+ };
449
+
450
+ export function detectFileType(filePath: string): string {
451
+ const ext = path.extname(filePath).toLowerCase();
452
+ return EXT_MAP[ext] ?? 'text';
453
+ }
454
+
455
+ // ---------------------------------------------------------------------------
456
+ // MergeStrategyManager class
457
+ // ---------------------------------------------------------------------------
458
+
12
459
  export class MergeStrategyManager {
13
- merge(_local: string, _remote: string, _base: string): MergeResult {
14
- throw new Error('Merge strategy not yet implemented');
460
+ private autoResolve: boolean;
461
+ private preserveComments: boolean;
462
+
463
+ constructor(options: MergeStrategyOptions = {}) {
464
+ this.autoResolve = options.autoResolve ?? false;
465
+ this.preserveComments = options.preserveComments ?? false;
466
+ }
467
+
468
+ /**
469
+ * Two-way merge: apply changes from `_remote` (modified) onto `_local` (base).
470
+ * The third `_base` parameter is accepted for backwards-compatibility but is
471
+ * unused in the two-way variant - use `threeWayMerge` when the ancestor is
472
+ * meaningful.
473
+ */
474
+ merge(_local: string, _remote: string, _base?: string): MergeResult {
475
+ return applyTwoWayMerge(_local, _remote);
476
+ }
477
+
478
+ /**
479
+ * Three-way merge using the ancestor `base`, local `user` changes and
480
+ * incoming `target` changes.
481
+ *
482
+ * Accepts either an options object (used by project-update.ts) or three
483
+ * positional strings for simpler call-sites.
484
+ */
485
+ threeWayMerge(
486
+ optionsOrBase: ThreeWayMergeOptions | string,
487
+ user?: string,
488
+ target?: string
489
+ ): MergeResult {
490
+ let base: string;
491
+ let userContent: string;
492
+ let targetContent: string;
493
+ let fileType: string | undefined;
494
+
495
+ if (typeof optionsOrBase === 'object') {
496
+ base = optionsOrBase.base;
497
+ userContent = optionsOrBase.user;
498
+ targetContent = optionsOrBase.target;
499
+ fileType =
500
+ optionsOrBase.fileType ??
501
+ (optionsOrBase.filePath
502
+ ? detectFileType(optionsOrBase.filePath)
503
+ : undefined);
504
+ } else {
505
+ base = optionsOrBase;
506
+ userContent = user ?? '';
507
+ targetContent = target ?? '';
508
+ }
509
+
510
+ if (fileType === 'json') {
511
+ return mergeJson(base, userContent, targetContent);
512
+ }
513
+
514
+ return performThreeWayMerge(
515
+ base,
516
+ userContent,
517
+ targetContent,
518
+ this.autoResolve
519
+ );
15
520
  }
16
521
  }
17
522
 
523
+ // ---------------------------------------------------------------------------
524
+ // Standalone exported functions (original stubs)
525
+ // ---------------------------------------------------------------------------
526
+
527
+ /**
528
+ * Standalone three-way merge function.
529
+ *
530
+ * Parameter order matches the original stub signature:
531
+ * threeWayMerge(local, remote, base)
532
+ * where `local` is ours, `remote` is theirs, and `base` is the common ancestor.
533
+ */
18
534
  export function threeWayMerge(
19
535
  _local: string,
20
536
  _remote: string,
21
537
  _base: string
22
538
  ): MergeResult {
23
- throw new Error('Three-way merge not yet implemented');
539
+ return performThreeWayMerge(_base, _local, _remote);
24
540
  }
25
541
 
26
- export function detectFileType(_filePath: string): string {
27
- return 'unknown';
542
+ // ---------------------------------------------------------------------------
543
+ // Factory helper (used by tests)
544
+ // ---------------------------------------------------------------------------
545
+
546
+ export function createMergeManager(
547
+ options: MergeStrategyOptions = {}
548
+ ): MergeStrategyManager {
549
+ return new MergeStrategyManager(options);
28
550
  }