@wundr.io/cli 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/wundr.js +8 -4
- package/package.json +23 -23
- package/src/ai/ai-service.ts +16 -17
- package/src/ai/claude-client.ts +16 -16
- package/src/ai/conversation-manager.ts +29 -29
- package/src/cli.ts +4 -4
- package/src/commands/ai.ts +246 -78
- package/src/commands/alignment.ts +74 -74
- package/src/commands/analyze-optimized.ts +111 -78
- package/src/commands/analyze.ts +14 -14
- package/src/commands/batch.ts +179 -42
- package/src/commands/chat.ts +37 -30
- package/src/commands/claude-init.ts +41 -45
- package/src/commands/claude-setup.ts +204 -119
- package/src/commands/computer-setup.ts +85 -43
- package/src/commands/create-command.ts +4 -4
- package/src/commands/create.ts +27 -27
- package/src/commands/dashboard.ts +24 -24
- package/src/commands/govern.ts +25 -25
- package/src/commands/governance.ts +34 -34
- package/src/commands/guardian.ts +56 -56
- package/src/commands/init.ts +25 -22
- package/src/commands/orchestrator.ts +68 -41
- package/src/commands/performance-optimizer.ts +34 -35
- package/src/commands/plugins.ts +27 -27
- package/src/commands/project-update.ts +175 -72
- package/src/commands/rag.ts +185 -78
- package/src/commands/session.ts +35 -35
- package/src/commands/setup.ts +40 -344
- package/src/commands/test-init.ts +3 -3
- package/src/commands/test.ts +4 -4
- package/src/commands/watch.ts +28 -29
- package/src/commands/worktree.ts +49 -49
- package/src/context/context-manager.ts +10 -10
- package/src/context/session-manager.ts +41 -41
- package/src/framework/command-interface.ts +520 -0
- package/src/framework/command-registry.ts +942 -0
- package/src/framework/completion-exporter.ts +383 -0
- package/src/framework/debug-logger.ts +519 -0
- package/src/framework/error-handler.ts +867 -0
- package/src/framework/help-generator.ts +540 -0
- package/src/framework/index.ts +169 -0
- package/src/framework/interactive-repl.ts +703 -0
- package/src/framework/output-formatter.ts +834 -0
- package/src/framework/progress-manager.ts +539 -0
- package/src/index.ts +4 -4
- package/src/interactive/interactive-mode.ts +16 -16
- package/src/lib/conflict-resolution.ts +799 -9
- package/src/lib/merge-strategy.ts +529 -7
- package/src/lib/safety-mechanisms.ts +422 -18
- package/src/lib/state-detection.ts +1015 -13
- package/src/nlp/command-mapper.ts +29 -29
- package/src/nlp/command-parser.ts +17 -17
- package/src/nlp/intent-classifier.ts +7 -7
- package/src/nlp/intent-parser.ts +54 -52
- package/src/plugins/plugin-manager.ts +61 -39
- package/src/tests/computer-setup-integration.test.ts +46 -15
- package/src/types/modules.d.ts +424 -1
- package/src/utils/backup-rollback-manager.ts +11 -8
- package/src/utils/config-manager.ts +3 -3
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/logger.ts +22 -22
- package/templates/batch/ci-cd.yaml +7 -7
- package/test-suites/api/health.spec.ts +20 -23
- package/test-suites/helpers/test-config.ts +14 -13
- package/test-suites/ui/accessibility.spec.ts +27 -22
- package/test-suites/ui/smoke.spec.ts +26 -21
- package/LICENSE +0 -21
- package/dist/ai/ai-service.d.ts +0 -152
- package/dist/ai/ai-service.d.ts.map +0 -1
- package/dist/ai/ai-service.js +0 -430
- package/dist/ai/ai-service.js.map +0 -1
- package/dist/ai/claude-client.d.ts +0 -130
- package/dist/ai/claude-client.d.ts.map +0 -1
- package/dist/ai/claude-client.js +0 -340
- package/dist/ai/claude-client.js.map +0 -1
- package/dist/ai/conversation-manager.d.ts +0 -164
- package/dist/ai/conversation-manager.d.ts.map +0 -1
- package/dist/ai/conversation-manager.js +0 -614
- package/dist/ai/conversation-manager.js.map +0 -1
- package/dist/ai/index.d.ts +0 -5
- package/dist/ai/index.d.ts.map +0 -1
- package/dist/ai/index.js +0 -8
- package/dist/ai/index.js.map +0 -1
- package/dist/cli.d.ts +0 -36
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -192
- package/dist/cli.js.map +0 -1
- package/dist/commands/ai.d.ts +0 -89
- package/dist/commands/ai.d.ts.map +0 -1
- package/dist/commands/ai.js +0 -799
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/alignment.d.ts +0 -78
- package/dist/commands/alignment.d.ts.map +0 -1
- package/dist/commands/alignment.js +0 -817
- package/dist/commands/alignment.js.map +0 -1
- package/dist/commands/analyze-optimized.d.ts +0 -14
- package/dist/commands/analyze-optimized.d.ts.map +0 -1
- package/dist/commands/analyze-optimized.js +0 -600
- package/dist/commands/analyze-optimized.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -65
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -435
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -71
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -738
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -71
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -674
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/claude-init.d.ts +0 -28
- package/dist/commands/claude-init.d.ts.map +0 -1
- package/dist/commands/claude-init.js +0 -591
- package/dist/commands/claude-init.js.map +0 -1
- package/dist/commands/claude-setup.d.ts +0 -119
- package/dist/commands/claude-setup.d.ts.map +0 -1
- package/dist/commands/claude-setup.js +0 -1073
- package/dist/commands/claude-setup.js.map +0 -1
- package/dist/commands/computer-setup-commands.d.ts +0 -53
- package/dist/commands/computer-setup-commands.d.ts.map +0 -1
- package/dist/commands/computer-setup-commands.js +0 -705
- package/dist/commands/computer-setup-commands.js.map +0 -1
- package/dist/commands/computer-setup.d.ts +0 -7
- package/dist/commands/computer-setup.d.ts.map +0 -1
- package/dist/commands/computer-setup.js +0 -849
- package/dist/commands/computer-setup.js.map +0 -1
- package/dist/commands/create-command.d.ts +0 -7
- package/dist/commands/create-command.d.ts.map +0 -1
- package/dist/commands/create-command.js +0 -158
- package/dist/commands/create-command.js.map +0 -1
- package/dist/commands/create.d.ts +0 -74
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -556
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -91
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/commands/dashboard.js +0 -538
- package/dist/commands/dashboard.js.map +0 -1
- package/dist/commands/govern.d.ts +0 -70
- package/dist/commands/govern.d.ts.map +0 -1
- package/dist/commands/govern.js +0 -481
- package/dist/commands/govern.js.map +0 -1
- package/dist/commands/governance.d.ts +0 -17
- package/dist/commands/governance.d.ts.map +0 -1
- package/dist/commands/governance.js +0 -703
- package/dist/commands/governance.js.map +0 -1
- package/dist/commands/guardian.d.ts +0 -20
- package/dist/commands/guardian.d.ts.map +0 -1
- package/dist/commands/guardian.js +0 -597
- package/dist/commands/guardian.js.map +0 -1
- package/dist/commands/init.d.ts +0 -59
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -650
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/orchestrator.d.ts +0 -7
- package/dist/commands/orchestrator.d.ts.map +0 -1
- package/dist/commands/orchestrator.js +0 -571
- package/dist/commands/orchestrator.js.map +0 -1
- package/dist/commands/performance-optimizer.d.ts +0 -30
- package/dist/commands/performance-optimizer.d.ts.map +0 -1
- package/dist/commands/performance-optimizer.js +0 -650
- package/dist/commands/performance-optimizer.js.map +0 -1
- package/dist/commands/plugins.d.ts +0 -87
- package/dist/commands/plugins.d.ts.map +0 -1
- package/dist/commands/plugins.js +0 -685
- package/dist/commands/plugins.js.map +0 -1
- package/dist/commands/rag.d.ts +0 -7
- package/dist/commands/rag.d.ts.map +0 -1
- package/dist/commands/rag.js +0 -748
- package/dist/commands/rag.js.map +0 -1
- package/dist/commands/session.d.ts +0 -41
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/session.js +0 -441
- package/dist/commands/session.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -397
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/test-init.d.ts +0 -9
- package/dist/commands/test-init.d.ts.map +0 -1
- package/dist/commands/test-init.js +0 -222
- package/dist/commands/test-init.js.map +0 -1
- package/dist/commands/test.d.ts +0 -25
- package/dist/commands/test.d.ts.map +0 -1
- package/dist/commands/test.js +0 -217
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/vp.d.ts +0 -7
- package/dist/commands/vp.d.ts.map +0 -1
- package/dist/commands/vp.js +0 -571
- package/dist/commands/vp.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -76
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -613
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/worktree.d.ts +0 -63
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/commands/worktree.js +0 -774
- package/dist/commands/worktree.js.map +0 -1
- package/dist/context/context-manager.d.ts +0 -155
- package/dist/context/context-manager.d.ts.map +0 -1
- package/dist/context/context-manager.js +0 -383
- package/dist/context/context-manager.js.map +0 -1
- package/dist/context/index.d.ts +0 -3
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -6
- package/dist/context/index.js.map +0 -1
- package/dist/context/session-manager.d.ts +0 -207
- package/dist/context/session-manager.d.ts.map +0 -1
- package/dist/context/session-manager.js +0 -686
- package/dist/context/session-manager.js.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/interactive/interactive-mode.d.ts +0 -76
- package/dist/interactive/interactive-mode.d.ts.map +0 -1
- package/dist/interactive/interactive-mode.js +0 -732
- package/dist/interactive/interactive-mode.js.map +0 -1
- package/dist/nlp/command-mapper.d.ts +0 -174
- package/dist/nlp/command-mapper.d.ts.map +0 -1
- package/dist/nlp/command-mapper.js +0 -624
- package/dist/nlp/command-mapper.js.map +0 -1
- package/dist/nlp/command-parser.d.ts +0 -106
- package/dist/nlp/command-parser.d.ts.map +0 -1
- package/dist/nlp/command-parser.js +0 -417
- package/dist/nlp/command-parser.js.map +0 -1
- package/dist/nlp/index.d.ts +0 -5
- package/dist/nlp/index.d.ts.map +0 -1
- package/dist/nlp/index.js +0 -8
- package/dist/nlp/index.js.map +0 -1
- package/dist/nlp/intent-classifier.d.ts +0 -59
- package/dist/nlp/intent-classifier.d.ts.map +0 -1
- package/dist/nlp/intent-classifier.js +0 -384
- package/dist/nlp/intent-classifier.js.map +0 -1
- package/dist/nlp/intent-parser.d.ts +0 -152
- package/dist/nlp/intent-parser.d.ts.map +0 -1
- package/dist/nlp/intent-parser.js +0 -744
- package/dist/nlp/intent-parser.js.map +0 -1
- package/dist/plugins/plugin-manager.d.ts +0 -120
- package/dist/plugins/plugin-manager.d.ts.map +0 -1
- package/dist/plugins/plugin-manager.js +0 -595
- package/dist/plugins/plugin-manager.js.map +0 -1
- package/dist/types/index.d.ts +0 -224
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/backup-rollback-manager.d.ts +0 -72
- package/dist/utils/backup-rollback-manager.d.ts.map +0 -1
- package/dist/utils/backup-rollback-manager.js +0 -289
- package/dist/utils/backup-rollback-manager.js.map +0 -1
- package/dist/utils/claude-config-installer.d.ts +0 -98
- package/dist/utils/claude-config-installer.d.ts.map +0 -1
- package/dist/utils/claude-config-installer.js +0 -678
- package/dist/utils/claude-config-installer.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -73
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -339
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/error-handler.d.ts +0 -46
- package/dist/utils/error-handler.d.ts.map +0 -1
- package/dist/utils/error-handler.js +0 -169
- package/dist/utils/error-handler.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -25
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -105
- package/dist/utils/logger.js.map +0 -1
- package/src/commands/computer-setup-commands.ts +0 -872
|
@@ -1,28 +1,550 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Merge Strategy
|
|
3
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
539
|
+
return performThreeWayMerge(_base, _local, _remote);
|
|
24
540
|
}
|
|
25
541
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
}
|