agileflow 2.95.2 → 2.96.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.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/api-routes.js +605 -0
- package/lib/api-server.js +260 -0
- package/lib/claude-cli-bridge.js +221 -0
- package/lib/dashboard-protocol.js +541 -0
- package/lib/dashboard-server.js +1601 -0
- package/lib/drivers/claude-driver.ts +310 -0
- package/lib/drivers/codex-driver.ts +454 -0
- package/lib/drivers/driver-manager.ts +158 -0
- package/lib/drivers/gemini-driver.ts +485 -0
- package/lib/drivers/index.ts +17 -0
- package/lib/flag-detection.js +350 -0
- package/lib/git-operations.js +267 -0
- package/lib/lock-file.js +144 -0
- package/lib/merge-operations.js +959 -0
- package/lib/protocol/driver.ts +360 -0
- package/lib/protocol/index.ts +12 -0
- package/lib/protocol/ir.ts +271 -0
- package/lib/session-display.js +330 -0
- package/lib/worktree-operations.js +221 -0
- package/package.json +2 -2
- package/scripts/agileflow-welcome.js +272 -24
- package/scripts/api-server-runner.js +177 -0
- package/scripts/archive-completed-stories.sh +22 -0
- package/scripts/automation-run-due.js +126 -0
- package/scripts/backfill-ideation-status.js +124 -0
- package/scripts/claude-tmux.sh +62 -1
- package/scripts/context-loader.js +292 -0
- package/scripts/dashboard-serve.js +323 -0
- package/scripts/lib/automation-registry.js +544 -0
- package/scripts/lib/automation-runner.js +476 -0
- package/scripts/lib/concurrency-limiter.js +513 -0
- package/scripts/lib/configure-features.js +46 -0
- package/scripts/lib/context-formatter.js +61 -0
- package/scripts/lib/damage-control-utils.js +29 -4
- package/scripts/lib/hook-metrics.js +324 -0
- package/scripts/lib/ideation-index.js +1196 -0
- package/scripts/lib/process-cleanup.js +359 -0
- package/scripts/lib/quality-gates.js +574 -0
- package/scripts/lib/status-task-bridge.js +522 -0
- package/scripts/lib/sync-ideation-status.js +292 -0
- package/scripts/lib/task-registry-cache.js +490 -0
- package/scripts/lib/task-registry.js +1181 -0
- package/scripts/migrate-ideation-index.js +515 -0
- package/scripts/precompact-context.sh +104 -0
- package/scripts/ralph-loop.js +2 -2
- package/scripts/session-manager.js +363 -2770
- package/scripts/spawn-parallel.js +45 -9
- package/src/core/agents/api-validator.md +180 -0
- package/src/core/agents/api.md +2 -0
- package/src/core/agents/code-reviewer.md +289 -0
- package/src/core/agents/configuration/damage-control.md +17 -0
- package/src/core/agents/database.md +2 -0
- package/src/core/agents/error-analyzer.md +203 -0
- package/src/core/agents/logic-analyzer-edge.md +171 -0
- package/src/core/agents/logic-analyzer-flow.md +254 -0
- package/src/core/agents/logic-analyzer-invariant.md +207 -0
- package/src/core/agents/logic-analyzer-race.md +267 -0
- package/src/core/agents/logic-analyzer-type.md +218 -0
- package/src/core/agents/logic-consensus.md +256 -0
- package/src/core/agents/orchestrator.md +89 -1
- package/src/core/agents/schema-validator.md +451 -0
- package/src/core/agents/team-coordinator.md +328 -0
- package/src/core/agents/ui-validator.md +328 -0
- package/src/core/agents/ui.md +2 -0
- package/src/core/commands/api.md +267 -0
- package/src/core/commands/automate.md +415 -0
- package/src/core/commands/babysit.md +290 -9
- package/src/core/commands/ideate/history.md +403 -0
- package/src/core/commands/{ideate.md → ideate/new.md} +244 -34
- package/src/core/commands/logic/audit.md +368 -0
- package/src/core/commands/roadmap/analyze.md +1 -1
- package/src/core/experts/documentation/expertise.yaml +29 -2
- package/src/core/templates/CONTEXT.md.example +49 -0
- package/src/core/templates/claude-settings.advanced.example.json +4 -0
- package/tools/cli/commands/serve.js +456 -0
- package/tools/cli/installers/core/installer.js +7 -2
- package/tools/cli/installers/ide/claude-code.js +85 -0
- package/tools/cli/lib/content-injector.js +27 -1
- package/tools/cli/lib/ui.js +26 -57
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge-operations.js - Session merge and conflict resolution
|
|
3
|
+
*
|
|
4
|
+
* Provides merge checking, smart conflict resolution, and change management operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execSync, spawnSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const { getProjectRoot, getAgileflowDir } = require('./paths');
|
|
12
|
+
const { getMainBranch, getCurrentBranch, gitCache } = require('./git-operations');
|
|
13
|
+
|
|
14
|
+
const ROOT = getProjectRoot();
|
|
15
|
+
const SESSIONS_DIR = path.join(getAgileflowDir(ROOT), 'sessions');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if session branch is mergeable to main
|
|
19
|
+
* @param {string} sessionId - Session ID
|
|
20
|
+
* @param {Function} loadRegistry - Registry loader function
|
|
21
|
+
* @returns {Object} Mergeability result
|
|
22
|
+
*/
|
|
23
|
+
function checkMergeability(sessionId, loadRegistry) {
|
|
24
|
+
const registry = loadRegistry();
|
|
25
|
+
const session = registry.sessions[sessionId];
|
|
26
|
+
|
|
27
|
+
if (!session) {
|
|
28
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (session.is_main) {
|
|
32
|
+
return { success: false, error: 'Cannot merge main session' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const branchName = session.branch;
|
|
36
|
+
const mainBranch = getMainBranch();
|
|
37
|
+
|
|
38
|
+
// Check for uncommitted changes in the session worktree
|
|
39
|
+
const statusResult = spawnSync('git', ['status', '--porcelain'], {
|
|
40
|
+
cwd: session.path,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (statusResult.stdout && statusResult.stdout.trim()) {
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
mergeable: false,
|
|
48
|
+
reason: 'uncommitted_changes',
|
|
49
|
+
details: statusResult.stdout.trim(),
|
|
50
|
+
branchName,
|
|
51
|
+
mainBranch,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if branch has commits ahead of main
|
|
56
|
+
const aheadBehind = spawnSync(
|
|
57
|
+
'git',
|
|
58
|
+
['rev-list', '--left-right', '--count', `${mainBranch}...${branchName}`],
|
|
59
|
+
{
|
|
60
|
+
cwd: ROOT,
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const [behind, ahead] = (aheadBehind.stdout || '0\t0').trim().split('\t').map(Number);
|
|
66
|
+
|
|
67
|
+
if (ahead === 0) {
|
|
68
|
+
return {
|
|
69
|
+
success: true,
|
|
70
|
+
mergeable: false,
|
|
71
|
+
reason: 'no_changes',
|
|
72
|
+
details: 'Branch has no commits ahead of main',
|
|
73
|
+
branchName,
|
|
74
|
+
mainBranch,
|
|
75
|
+
commitsAhead: 0,
|
|
76
|
+
commitsBehind: behind,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Try merge --no-commit --no-ff to check for conflicts (dry run)
|
|
81
|
+
const currentBranch = getCurrentBranch();
|
|
82
|
+
|
|
83
|
+
// Checkout main in ROOT for the test merge
|
|
84
|
+
const checkoutMain = spawnSync('git', ['checkout', mainBranch], {
|
|
85
|
+
cwd: ROOT,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (checkoutMain.status !== 0) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: `Failed to checkout ${mainBranch}: ${checkoutMain.stderr}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Try the merge
|
|
97
|
+
const testMerge = spawnSync('git', ['merge', '--no-commit', '--no-ff', branchName], {
|
|
98
|
+
cwd: ROOT,
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const hasConflicts = testMerge.status !== 0;
|
|
103
|
+
|
|
104
|
+
// Abort the test merge
|
|
105
|
+
spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
|
|
106
|
+
|
|
107
|
+
// Go back to original branch if different
|
|
108
|
+
if (currentBranch && currentBranch !== mainBranch) {
|
|
109
|
+
spawnSync('git', ['checkout', currentBranch], { cwd: ROOT, encoding: 'utf8' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
mergeable: !hasConflicts,
|
|
115
|
+
branchName,
|
|
116
|
+
mainBranch,
|
|
117
|
+
commitsAhead: ahead,
|
|
118
|
+
commitsBehind: behind,
|
|
119
|
+
hasConflicts,
|
|
120
|
+
conflictDetails: hasConflicts ? testMerge.stderr : null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get merge preview (commits and files to be merged)
|
|
126
|
+
* @param {string} sessionId - Session ID
|
|
127
|
+
* @param {Function} loadRegistry - Registry loader function
|
|
128
|
+
* @returns {Object} Preview result
|
|
129
|
+
*/
|
|
130
|
+
function getMergePreview(sessionId, loadRegistry) {
|
|
131
|
+
const registry = loadRegistry();
|
|
132
|
+
const session = registry.sessions[sessionId];
|
|
133
|
+
|
|
134
|
+
if (!session) {
|
|
135
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (session.is_main) {
|
|
139
|
+
return { success: false, error: 'Cannot preview merge for main session' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const branchName = session.branch;
|
|
143
|
+
const mainBranch = getMainBranch();
|
|
144
|
+
|
|
145
|
+
// Get commits that would be merged
|
|
146
|
+
const logResult = spawnSync('git', ['log', '--oneline', `${mainBranch}..${branchName}`], {
|
|
147
|
+
cwd: ROOT,
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const commits = (logResult.stdout || '').trim().split('\n').filter(Boolean);
|
|
152
|
+
|
|
153
|
+
// Get files changed
|
|
154
|
+
const diffResult = spawnSync('git', ['diff', '--name-status', `${mainBranch}...${branchName}`], {
|
|
155
|
+
cwd: ROOT,
|
|
156
|
+
encoding: 'utf8',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const filesChanged = (diffResult.stdout || '').trim().split('\n').filter(Boolean);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
branchName,
|
|
164
|
+
mainBranch,
|
|
165
|
+
nickname: session.nickname,
|
|
166
|
+
commits,
|
|
167
|
+
commitCount: commits.length,
|
|
168
|
+
filesChanged,
|
|
169
|
+
fileCount: filesChanged.length,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Execute merge operation
|
|
175
|
+
* @param {string} sessionId - Session ID
|
|
176
|
+
* @param {Object} options - Merge options
|
|
177
|
+
* @param {Function} loadRegistry - Registry loader function
|
|
178
|
+
* @param {Function} saveRegistry - Registry saver function
|
|
179
|
+
* @param {Function} removeLock - Lock remover function
|
|
180
|
+
* @returns {Object} Merge result
|
|
181
|
+
*/
|
|
182
|
+
function integrateSession(sessionId, options = {}, loadRegistry, saveRegistry, removeLock) {
|
|
183
|
+
const {
|
|
184
|
+
strategy = 'squash',
|
|
185
|
+
deleteBranch = true,
|
|
186
|
+
deleteWorktree = true,
|
|
187
|
+
message = null,
|
|
188
|
+
} = options;
|
|
189
|
+
|
|
190
|
+
const registry = loadRegistry();
|
|
191
|
+
const session = registry.sessions[sessionId];
|
|
192
|
+
|
|
193
|
+
if (!session) {
|
|
194
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (session.is_main) {
|
|
198
|
+
return { success: false, error: 'Cannot merge main session' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const branchName = session.branch;
|
|
202
|
+
const mainBranch = getMainBranch();
|
|
203
|
+
|
|
204
|
+
// Ensure we're on main branch in ROOT
|
|
205
|
+
const checkoutMain = spawnSync('git', ['checkout', mainBranch], {
|
|
206
|
+
cwd: ROOT,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (checkoutMain.status !== 0) {
|
|
211
|
+
return { success: false, error: `Failed to checkout ${mainBranch}: ${checkoutMain.stderr}` };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Pull latest main (optional, for safety) - ignore errors for local-only repos
|
|
215
|
+
spawnSync('git', ['pull', '--ff-only'], { cwd: ROOT, encoding: 'utf8' });
|
|
216
|
+
|
|
217
|
+
// Build commit message
|
|
218
|
+
const commitMessage =
|
|
219
|
+
message ||
|
|
220
|
+
`Merge session ${sessionId}${session.nickname ? ` "${session.nickname}"` : ''}: ${branchName}`;
|
|
221
|
+
|
|
222
|
+
// Execute merge based on strategy
|
|
223
|
+
let mergeResult;
|
|
224
|
+
|
|
225
|
+
if (strategy === 'squash') {
|
|
226
|
+
mergeResult = spawnSync('git', ['merge', '--squash', branchName], {
|
|
227
|
+
cwd: ROOT,
|
|
228
|
+
encoding: 'utf8',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (mergeResult.status === 0) {
|
|
232
|
+
// Create the squash commit
|
|
233
|
+
const commitResult = spawnSync('git', ['commit', '-m', commitMessage], {
|
|
234
|
+
cwd: ROOT,
|
|
235
|
+
encoding: 'utf8',
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (commitResult.status !== 0) {
|
|
239
|
+
return { success: false, error: `Failed to create squash commit: ${commitResult.stderr}` };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
// Regular merge commit
|
|
244
|
+
mergeResult = spawnSync('git', ['merge', '--no-ff', '-m', commitMessage, branchName], {
|
|
245
|
+
cwd: ROOT,
|
|
246
|
+
encoding: 'utf8',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (mergeResult.status !== 0) {
|
|
251
|
+
// Abort if merge failed
|
|
252
|
+
spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
|
|
253
|
+
return { success: false, error: `Merge failed: ${mergeResult.stderr}`, hasConflicts: true };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const result = {
|
|
257
|
+
success: true,
|
|
258
|
+
merged: true,
|
|
259
|
+
strategy,
|
|
260
|
+
branchName,
|
|
261
|
+
mainBranch,
|
|
262
|
+
commitMessage,
|
|
263
|
+
mainPath: ROOT,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Delete worktree first (before branch, as worktree holds ref)
|
|
267
|
+
if (deleteWorktree && session.path !== ROOT && fs.existsSync(session.path)) {
|
|
268
|
+
try {
|
|
269
|
+
execSync(`git worktree remove "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
270
|
+
result.worktreeDeleted = true;
|
|
271
|
+
} catch (e) {
|
|
272
|
+
try {
|
|
273
|
+
execSync(`git worktree remove --force "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
274
|
+
result.worktreeDeleted = true;
|
|
275
|
+
} catch (e2) {
|
|
276
|
+
result.worktreeDeleted = false;
|
|
277
|
+
result.worktreeError = e2.message;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Delete branch if requested
|
|
283
|
+
if (deleteBranch) {
|
|
284
|
+
const deleteBranchResult = spawnSync('git', ['branch', '-d', branchName], {
|
|
285
|
+
cwd: ROOT,
|
|
286
|
+
encoding: 'utf8',
|
|
287
|
+
});
|
|
288
|
+
result.branchDeleted = deleteBranchResult.status === 0;
|
|
289
|
+
if (!result.branchDeleted) {
|
|
290
|
+
// Try force delete if normal delete fails
|
|
291
|
+
const forceDelete = spawnSync('git', ['branch', '-D', branchName], {
|
|
292
|
+
cwd: ROOT,
|
|
293
|
+
encoding: 'utf8',
|
|
294
|
+
});
|
|
295
|
+
result.branchDeleted = forceDelete.status === 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Remove from registry
|
|
300
|
+
removeLock(sessionId);
|
|
301
|
+
delete registry.sessions[sessionId];
|
|
302
|
+
saveRegistry(registry);
|
|
303
|
+
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Generate auto commit message for session
|
|
309
|
+
* @param {Object} session - Session object
|
|
310
|
+
* @returns {string} Generated commit message
|
|
311
|
+
*/
|
|
312
|
+
function generateCommitMessage(session) {
|
|
313
|
+
const nickname = session.nickname || `session-${session.id || 'unknown'}`;
|
|
314
|
+
const branch = session.branch || 'unknown';
|
|
315
|
+
return `chore: commit uncommitted changes from ${nickname}\n\nBranch: ${branch}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Commit all changes in session worktree
|
|
320
|
+
* @param {string} sessionId - Session ID
|
|
321
|
+
* @param {Object} options - Options including message
|
|
322
|
+
* @param {Function} loadRegistry - Registry loader function
|
|
323
|
+
* @returns {Object} Commit result
|
|
324
|
+
*/
|
|
325
|
+
function commitChanges(sessionId, options = {}, loadRegistry) {
|
|
326
|
+
const registry = loadRegistry();
|
|
327
|
+
const session = registry.sessions[sessionId];
|
|
328
|
+
|
|
329
|
+
if (!session) {
|
|
330
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!fs.existsSync(session.path)) {
|
|
334
|
+
return { success: false, error: `Session directory not found: ${session.path}` };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Stage all changes
|
|
338
|
+
const addResult = spawnSync('git', ['add', '-A'], {
|
|
339
|
+
cwd: session.path,
|
|
340
|
+
encoding: 'utf8',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (addResult.status !== 0) {
|
|
344
|
+
return { success: false, error: `Failed to stage changes: ${addResult.stderr}` };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Generate commit message if not provided
|
|
348
|
+
const message = options.message || generateCommitMessage({ ...session, id: sessionId });
|
|
349
|
+
|
|
350
|
+
// Create commit
|
|
351
|
+
const commitResult = spawnSync('git', ['commit', '-m', message], {
|
|
352
|
+
cwd: session.path,
|
|
353
|
+
encoding: 'utf8',
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (commitResult.status !== 0) {
|
|
357
|
+
// Check if nothing to commit (all changes already staged/committed)
|
|
358
|
+
if (commitResult.stdout && commitResult.stdout.includes('nothing to commit')) {
|
|
359
|
+
return { success: true, message: 'No changes to commit', commitHash: null };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
success: false,
|
|
363
|
+
error: `Failed to commit: ${commitResult.stderr || commitResult.stdout}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Get commit hash
|
|
368
|
+
const hashResult = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
369
|
+
cwd: session.path,
|
|
370
|
+
encoding: 'utf8',
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
commitHash: hashResult.stdout?.trim(),
|
|
376
|
+
message,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Stash changes in session worktree
|
|
382
|
+
* @param {string} sessionId - Session ID
|
|
383
|
+
* @param {Function} loadRegistry - Registry loader function
|
|
384
|
+
* @returns {Object} Stash result
|
|
385
|
+
*/
|
|
386
|
+
function stashChanges(sessionId, loadRegistry) {
|
|
387
|
+
const registry = loadRegistry();
|
|
388
|
+
const session = registry.sessions[sessionId];
|
|
389
|
+
|
|
390
|
+
if (!session) {
|
|
391
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!fs.existsSync(session.path)) {
|
|
395
|
+
return { success: false, error: `Session directory not found: ${session.path}` };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const stashMsg = `AgileFlow: session ${sessionId} merge prep`;
|
|
399
|
+
const result = spawnSync('git', ['stash', 'push', '-m', stashMsg], {
|
|
400
|
+
cwd: session.path,
|
|
401
|
+
encoding: 'utf8',
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (result.status !== 0) {
|
|
405
|
+
return { success: false, error: `Failed to stash: ${result.stderr}` };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Check if stash was actually created (might be "No local changes to save")
|
|
409
|
+
if (result.stdout && result.stdout.includes('No local changes to save')) {
|
|
410
|
+
return { success: true, message: 'No changes to stash', stashCreated: false };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { success: true, message: stashMsg, stashCreated: true };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Unstash changes (pop stash)
|
|
418
|
+
* @param {string} sessionId - Session ID (for error messages)
|
|
419
|
+
* @returns {Object} Unstash result
|
|
420
|
+
*/
|
|
421
|
+
function unstashChanges(sessionId) {
|
|
422
|
+
// Note: After merge, the session worktree is deleted. Stash is popped on main.
|
|
423
|
+
const result = spawnSync('git', ['stash', 'pop'], {
|
|
424
|
+
cwd: ROOT,
|
|
425
|
+
encoding: 'utf8',
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
if (result.status !== 0) {
|
|
429
|
+
// Check if no stash exists
|
|
430
|
+
if (result.stderr && result.stderr.includes('No stash entries found')) {
|
|
431
|
+
return { success: true, message: 'No stash to pop' };
|
|
432
|
+
}
|
|
433
|
+
return { success: false, error: `Failed to unstash: ${result.stderr}` };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { success: true };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Discard all uncommitted changes in session worktree
|
|
441
|
+
* @param {string} sessionId - Session ID
|
|
442
|
+
* @param {Function} loadRegistry - Registry loader function
|
|
443
|
+
* @returns {Object} Discard result
|
|
444
|
+
*/
|
|
445
|
+
function discardChanges(sessionId, loadRegistry) {
|
|
446
|
+
const registry = loadRegistry();
|
|
447
|
+
const session = registry.sessions[sessionId];
|
|
448
|
+
|
|
449
|
+
if (!session) {
|
|
450
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!fs.existsSync(session.path)) {
|
|
454
|
+
return { success: false, error: `Session directory not found: ${session.path}` };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Reset staged changes
|
|
458
|
+
spawnSync('git', ['reset', 'HEAD'], {
|
|
459
|
+
cwd: session.path,
|
|
460
|
+
encoding: 'utf8',
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Discard working directory changes
|
|
464
|
+
const checkoutResult = spawnSync('git', ['checkout', '--', '.'], {
|
|
465
|
+
cwd: session.path,
|
|
466
|
+
encoding: 'utf8',
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (checkoutResult.status !== 0) {
|
|
470
|
+
return { success: false, error: `Failed to discard changes: ${checkoutResult.stderr}` };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { success: true };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Categorize a file by type for merge strategy selection.
|
|
478
|
+
* @param {string} filePath - File path
|
|
479
|
+
* @returns {string} Category: 'docs', 'test', 'schema', 'config', 'source'
|
|
480
|
+
*/
|
|
481
|
+
function categorizeFile(filePath) {
|
|
482
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
483
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
484
|
+
const dirname = path.dirname(filePath).toLowerCase();
|
|
485
|
+
|
|
486
|
+
// Documentation files
|
|
487
|
+
if (ext === '.md' || basename === 'readme' || basename.startsWith('readme.')) {
|
|
488
|
+
return 'docs';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Test files
|
|
492
|
+
if (
|
|
493
|
+
filePath.includes('.test.') ||
|
|
494
|
+
filePath.includes('.spec.') ||
|
|
495
|
+
filePath.includes('__tests__') ||
|
|
496
|
+
dirname.includes('test') ||
|
|
497
|
+
dirname.includes('tests')
|
|
498
|
+
) {
|
|
499
|
+
return 'test';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Schema/migration files
|
|
503
|
+
if (
|
|
504
|
+
ext === '.sql' ||
|
|
505
|
+
filePath.includes('schema') ||
|
|
506
|
+
filePath.includes('migration') ||
|
|
507
|
+
filePath.includes('prisma')
|
|
508
|
+
) {
|
|
509
|
+
return 'schema';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Config files
|
|
513
|
+
if (
|
|
514
|
+
ext === '.json' ||
|
|
515
|
+
ext === '.yaml' ||
|
|
516
|
+
ext === '.yml' ||
|
|
517
|
+
ext === '.toml' ||
|
|
518
|
+
basename.includes('config') ||
|
|
519
|
+
basename.startsWith('.') // dotfiles
|
|
520
|
+
) {
|
|
521
|
+
return 'config';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Default: source code
|
|
525
|
+
return 'source';
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get merge strategy for a file category.
|
|
530
|
+
* @param {string} category - File category
|
|
531
|
+
* @returns {Object} Strategy info with strategy, gitStrategy, description
|
|
532
|
+
*/
|
|
533
|
+
function getMergeStrategy(category) {
|
|
534
|
+
const strategies = {
|
|
535
|
+
docs: {
|
|
536
|
+
strategy: 'accept_both',
|
|
537
|
+
gitStrategy: 'union',
|
|
538
|
+
description: 'Documentation is additive - both changes kept',
|
|
539
|
+
},
|
|
540
|
+
test: {
|
|
541
|
+
strategy: 'accept_both',
|
|
542
|
+
gitStrategy: 'union',
|
|
543
|
+
description: 'Tests are additive - both test files kept',
|
|
544
|
+
},
|
|
545
|
+
schema: {
|
|
546
|
+
strategy: 'take_theirs',
|
|
547
|
+
gitStrategy: 'theirs',
|
|
548
|
+
description: 'Schemas evolve forward - session version used',
|
|
549
|
+
},
|
|
550
|
+
config: {
|
|
551
|
+
strategy: 'merge_keys',
|
|
552
|
+
gitStrategy: 'ours',
|
|
553
|
+
description: 'Config changes need review - main version kept',
|
|
554
|
+
},
|
|
555
|
+
source: {
|
|
556
|
+
strategy: 'intelligent_merge',
|
|
557
|
+
gitStrategy: 'recursive',
|
|
558
|
+
description: 'Source code merged by git recursive strategy',
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
return strategies[category] || strategies.source;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Get list of files that would conflict during merge.
|
|
567
|
+
* @param {string} sessionId - Session ID
|
|
568
|
+
* @param {Function} loadRegistry - Registry loader function
|
|
569
|
+
* @returns {Object} Conflicting files result
|
|
570
|
+
*/
|
|
571
|
+
function getConflictingFiles(sessionId, loadRegistry) {
|
|
572
|
+
const registry = loadRegistry();
|
|
573
|
+
const session = registry.sessions[sessionId];
|
|
574
|
+
|
|
575
|
+
if (!session) {
|
|
576
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const branchName = session.branch;
|
|
580
|
+
const mainBranch = getMainBranch();
|
|
581
|
+
|
|
582
|
+
// Get files changed in both branches since divergence
|
|
583
|
+
const mergeBase = spawnSync('git', ['merge-base', mainBranch, branchName], {
|
|
584
|
+
cwd: ROOT,
|
|
585
|
+
encoding: 'utf8',
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (mergeBase.status !== 0) {
|
|
589
|
+
return { success: false, error: 'Could not find merge base' };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const base = mergeBase.stdout.trim();
|
|
593
|
+
|
|
594
|
+
// Files changed in main since base
|
|
595
|
+
const mainFiles = spawnSync('git', ['diff', '--name-only', base, mainBranch], {
|
|
596
|
+
cwd: ROOT,
|
|
597
|
+
encoding: 'utf8',
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Files changed in session branch since base
|
|
601
|
+
const branchFiles = spawnSync('git', ['diff', '--name-only', base, branchName], {
|
|
602
|
+
cwd: ROOT,
|
|
603
|
+
encoding: 'utf8',
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const mainSet = new Set((mainFiles.stdout || '').trim().split('\n').filter(Boolean));
|
|
607
|
+
const branchSet = new Set((branchFiles.stdout || '').trim().split('\n').filter(Boolean));
|
|
608
|
+
|
|
609
|
+
// Find intersection (files changed in both)
|
|
610
|
+
const conflicting = [...mainSet].filter(f => branchSet.has(f));
|
|
611
|
+
|
|
612
|
+
return { success: true, files: conflicting };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Resolve a single file conflict using the designated strategy.
|
|
617
|
+
* @param {Object} resolution - Resolution info from categorization
|
|
618
|
+
* @returns {Object} Resolution result
|
|
619
|
+
*/
|
|
620
|
+
function resolveConflict(resolution) {
|
|
621
|
+
const { file, gitStrategy } = resolution;
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
switch (gitStrategy) {
|
|
625
|
+
case 'union':
|
|
626
|
+
// Union merge - concatenate both versions
|
|
627
|
+
try {
|
|
628
|
+
const base = spawnSync('git', ['show', `:1:${file}`], { cwd: ROOT, encoding: 'utf8' });
|
|
629
|
+
const ours = spawnSync('git', ['show', `:2:${file}`], { cwd: ROOT, encoding: 'utf8' });
|
|
630
|
+
const theirs = spawnSync('git', ['show', `:3:${file}`], { cwd: ROOT, encoding: 'utf8' });
|
|
631
|
+
|
|
632
|
+
if (base.status === 0 && ours.status === 0 && theirs.status === 0) {
|
|
633
|
+
const tmpBase = path.join(ROOT, '.git', 'MERGE_BASE_TMP');
|
|
634
|
+
const tmpOurs = path.join(ROOT, '.git', 'MERGE_OURS_TMP');
|
|
635
|
+
const tmpTheirs = path.join(ROOT, '.git', 'MERGE_THEIRS_TMP');
|
|
636
|
+
|
|
637
|
+
fs.writeFileSync(tmpBase, base.stdout);
|
|
638
|
+
fs.writeFileSync(tmpOurs, ours.stdout);
|
|
639
|
+
fs.writeFileSync(tmpTheirs, theirs.stdout);
|
|
640
|
+
|
|
641
|
+
spawnSync('git', ['merge-file', '--union', tmpOurs, tmpBase, tmpTheirs], {
|
|
642
|
+
cwd: ROOT,
|
|
643
|
+
encoding: 'utf8',
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
fs.copyFileSync(tmpOurs, path.join(ROOT, file));
|
|
647
|
+
|
|
648
|
+
fs.unlinkSync(tmpBase);
|
|
649
|
+
fs.unlinkSync(tmpOurs);
|
|
650
|
+
fs.unlinkSync(tmpTheirs);
|
|
651
|
+
} else {
|
|
652
|
+
execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
653
|
+
}
|
|
654
|
+
} catch (unionError) {
|
|
655
|
+
execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
658
|
+
|
|
659
|
+
case 'theirs':
|
|
660
|
+
execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
661
|
+
break;
|
|
662
|
+
|
|
663
|
+
case 'ours':
|
|
664
|
+
execSync(`git checkout --ours "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
665
|
+
break;
|
|
666
|
+
|
|
667
|
+
case 'recursive':
|
|
668
|
+
default:
|
|
669
|
+
execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
execSync(`git add "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
674
|
+
return { success: true };
|
|
675
|
+
} catch (e) {
|
|
676
|
+
return { success: false, error: e.message };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Save merge log for audit trail.
|
|
682
|
+
* @param {Object} log - Merge log entry
|
|
683
|
+
*/
|
|
684
|
+
function saveMergeLog(log) {
|
|
685
|
+
const logPath = path.join(SESSIONS_DIR, 'merge-log.json');
|
|
686
|
+
|
|
687
|
+
let logs = { merges: [] };
|
|
688
|
+
if (fs.existsSync(logPath)) {
|
|
689
|
+
try {
|
|
690
|
+
logs = JSON.parse(fs.readFileSync(logPath, 'utf8'));
|
|
691
|
+
} catch (e) {
|
|
692
|
+
// Start fresh
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
logs.merges.push(log);
|
|
697
|
+
|
|
698
|
+
// Keep only last 50 merges
|
|
699
|
+
if (logs.merges.length > 50) {
|
|
700
|
+
logs.merges = logs.merges.slice(-50);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
fs.writeFileSync(logPath, JSON.stringify(logs, null, 2));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Get merge history from audit log.
|
|
708
|
+
* @returns {Object} Merge history result
|
|
709
|
+
*/
|
|
710
|
+
function getMergeHistory() {
|
|
711
|
+
const logPath = path.join(SESSIONS_DIR, 'merge-log.json');
|
|
712
|
+
|
|
713
|
+
if (!fs.existsSync(logPath)) {
|
|
714
|
+
return { success: true, merges: [] };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const logs = JSON.parse(fs.readFileSync(logPath, 'utf8'));
|
|
719
|
+
return { success: true, merges: logs.merges || [] };
|
|
720
|
+
} catch (e) {
|
|
721
|
+
return { success: false, error: e.message };
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Smart merge with automatic conflict resolution.
|
|
727
|
+
* @param {string} sessionId - Session ID
|
|
728
|
+
* @param {Object} options - Merge options
|
|
729
|
+
* @param {Function} loadRegistry - Registry loader
|
|
730
|
+
* @param {Function} saveRegistry - Registry saver
|
|
731
|
+
* @param {Function} removeLock - Lock remover
|
|
732
|
+
* @param {Function} unregisterSession - Session unregisterer
|
|
733
|
+
* @returns {Object} Smart merge result
|
|
734
|
+
*/
|
|
735
|
+
function smartMerge(sessionId, options = {}, loadRegistry, saveRegistry, removeLock, unregisterSession) {
|
|
736
|
+
const { c } = require('./colors');
|
|
737
|
+
const {
|
|
738
|
+
strategy = 'squash',
|
|
739
|
+
deleteBranch = true,
|
|
740
|
+
deleteWorktree = true,
|
|
741
|
+
message = null,
|
|
742
|
+
} = options;
|
|
743
|
+
|
|
744
|
+
const registry = loadRegistry();
|
|
745
|
+
const session = registry.sessions[sessionId];
|
|
746
|
+
|
|
747
|
+
if (!session) {
|
|
748
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (session.is_main) {
|
|
752
|
+
return { success: false, error: 'Cannot merge main session' };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const branchName = session.branch;
|
|
756
|
+
const mainBranch = getMainBranch();
|
|
757
|
+
|
|
758
|
+
// First, try normal merge
|
|
759
|
+
const checkResult = checkMergeability(sessionId, loadRegistry);
|
|
760
|
+
if (!checkResult.success) {
|
|
761
|
+
return checkResult;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// If no conflicts, use regular merge
|
|
765
|
+
if (!checkResult.hasConflicts) {
|
|
766
|
+
return integrateSession(sessionId, options, loadRegistry, saveRegistry, removeLock);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// We have conflicts - try smart resolution
|
|
770
|
+
console.log(`${c.amber}Conflicts detected - attempting auto-resolution...${c.reset}`);
|
|
771
|
+
|
|
772
|
+
// Get list of conflicting files
|
|
773
|
+
const conflictFiles = getConflictingFiles(sessionId, loadRegistry);
|
|
774
|
+
if (!conflictFiles.success) {
|
|
775
|
+
return conflictFiles;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Categorize and plan resolutions
|
|
779
|
+
const resolutions = conflictFiles.files.map(file => {
|
|
780
|
+
const category = categorizeFile(file);
|
|
781
|
+
const strategyInfo = getMergeStrategy(category);
|
|
782
|
+
return {
|
|
783
|
+
file,
|
|
784
|
+
category,
|
|
785
|
+
...strategyInfo,
|
|
786
|
+
};
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Log merge audit
|
|
790
|
+
const mergeLog = {
|
|
791
|
+
session: sessionId,
|
|
792
|
+
started_at: new Date().toISOString(),
|
|
793
|
+
files_to_resolve: resolutions,
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// Ensure we're on main branch
|
|
797
|
+
const checkoutMain = spawnSync('git', ['checkout', mainBranch], {
|
|
798
|
+
cwd: ROOT,
|
|
799
|
+
encoding: 'utf8',
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
if (checkoutMain.status !== 0) {
|
|
803
|
+
return { success: false, error: `Failed to checkout ${mainBranch}: ${checkoutMain.stderr}` };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Start the merge
|
|
807
|
+
const startMerge = spawnSync('git', ['merge', '--no-commit', '--no-ff', branchName], {
|
|
808
|
+
cwd: ROOT,
|
|
809
|
+
encoding: 'utf8',
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// If merge started but has conflicts, resolve them
|
|
813
|
+
if (startMerge.status !== 0) {
|
|
814
|
+
const resolvedFiles = [];
|
|
815
|
+
const unresolvedFiles = [];
|
|
816
|
+
|
|
817
|
+
for (const resolution of resolutions) {
|
|
818
|
+
const resolveResult = resolveConflict(resolution);
|
|
819
|
+
if (resolveResult.success) {
|
|
820
|
+
resolvedFiles.push({
|
|
821
|
+
file: resolution.file,
|
|
822
|
+
strategy: resolution.strategy,
|
|
823
|
+
description: resolution.description,
|
|
824
|
+
});
|
|
825
|
+
} else {
|
|
826
|
+
unresolvedFiles.push({
|
|
827
|
+
file: resolution.file,
|
|
828
|
+
error: resolveResult.error,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// If any files couldn't be resolved, abort
|
|
834
|
+
if (unresolvedFiles.length > 0) {
|
|
835
|
+
spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
|
|
836
|
+
return {
|
|
837
|
+
success: false,
|
|
838
|
+
error: 'Some conflicts could not be auto-resolved',
|
|
839
|
+
autoResolved: resolvedFiles,
|
|
840
|
+
unresolved: unresolvedFiles,
|
|
841
|
+
hasConflicts: true,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// All conflicts resolved - commit the merge
|
|
846
|
+
const commitMessage =
|
|
847
|
+
message ||
|
|
848
|
+
`Merge session ${sessionId}${session.nickname ? ` "${session.nickname}"` : ''}: ${branchName} (auto-resolved)`;
|
|
849
|
+
|
|
850
|
+
// Stage all resolved files
|
|
851
|
+
spawnSync('git', ['add', '-A'], { cwd: ROOT, encoding: 'utf8' });
|
|
852
|
+
|
|
853
|
+
// Create commit
|
|
854
|
+
const commitResult = spawnSync('git', ['commit', '-m', commitMessage], {
|
|
855
|
+
cwd: ROOT,
|
|
856
|
+
encoding: 'utf8',
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
if (commitResult.status !== 0) {
|
|
860
|
+
spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
|
|
861
|
+
return { success: false, error: `Failed to commit merge: ${commitResult.stderr}` };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Log successful merge
|
|
865
|
+
mergeLog.merged_at = new Date().toISOString();
|
|
866
|
+
mergeLog.files_auto_resolved = resolvedFiles;
|
|
867
|
+
mergeLog.commits_merged = checkResult.commitsAhead;
|
|
868
|
+
saveMergeLog(mergeLog);
|
|
869
|
+
|
|
870
|
+
const result = {
|
|
871
|
+
success: true,
|
|
872
|
+
merged: true,
|
|
873
|
+
autoResolved: resolvedFiles,
|
|
874
|
+
strategy,
|
|
875
|
+
branchName,
|
|
876
|
+
mainBranch,
|
|
877
|
+
commitMessage,
|
|
878
|
+
mainPath: ROOT,
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
// Cleanup worktree and branch
|
|
882
|
+
if (deleteWorktree && session.path !== ROOT && fs.existsSync(session.path)) {
|
|
883
|
+
try {
|
|
884
|
+
execSync(`git worktree remove "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
885
|
+
result.worktreeDeleted = true;
|
|
886
|
+
} catch (e) {
|
|
887
|
+
try {
|
|
888
|
+
execSync(`git worktree remove --force "${session.path}"`, {
|
|
889
|
+
cwd: ROOT,
|
|
890
|
+
encoding: 'utf8',
|
|
891
|
+
});
|
|
892
|
+
result.worktreeDeleted = true;
|
|
893
|
+
} catch (e2) {
|
|
894
|
+
result.worktreeDeleted = false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (deleteBranch) {
|
|
900
|
+
try {
|
|
901
|
+
execSync(`git branch -D "${branchName}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
902
|
+
result.branchDeleted = true;
|
|
903
|
+
} catch (e) {
|
|
904
|
+
result.branchDeleted = false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Unregister the session
|
|
909
|
+
unregisterSession(sessionId);
|
|
910
|
+
|
|
911
|
+
return result;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Merge succeeded without conflicts
|
|
915
|
+
const commitMessage =
|
|
916
|
+
message ||
|
|
917
|
+
`Merge session ${sessionId}${session.nickname ? ` "${session.nickname}"` : ''}: ${branchName}`;
|
|
918
|
+
|
|
919
|
+
const commitResult = spawnSync('git', ['commit', '-m', commitMessage], {
|
|
920
|
+
cwd: ROOT,
|
|
921
|
+
encoding: 'utf8',
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
if (commitResult.status !== 0) {
|
|
925
|
+
return { success: false, error: `Failed to commit: ${commitResult.stderr}` };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
success: true,
|
|
930
|
+
merged: true,
|
|
931
|
+
strategy,
|
|
932
|
+
branchName,
|
|
933
|
+
mainBranch,
|
|
934
|
+
commitMessage,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
module.exports = {
|
|
939
|
+
// Merge checks
|
|
940
|
+
checkMergeability,
|
|
941
|
+
getMergePreview,
|
|
942
|
+
// Merge execution
|
|
943
|
+
integrateSession,
|
|
944
|
+
generateCommitMessage,
|
|
945
|
+
// Changes handling
|
|
946
|
+
commitChanges,
|
|
947
|
+
stashChanges,
|
|
948
|
+
unstashChanges,
|
|
949
|
+
discardChanges,
|
|
950
|
+
// Smart merge
|
|
951
|
+
categorizeFile,
|
|
952
|
+
getMergeStrategy,
|
|
953
|
+
smartMerge,
|
|
954
|
+
getConflictingFiles,
|
|
955
|
+
resolveConflict,
|
|
956
|
+
// Merge history
|
|
957
|
+
saveMergeLog,
|
|
958
|
+
getMergeHistory,
|
|
959
|
+
};
|