@litmers/cursorflow-orchestrator 0.1.20 → 0.1.28

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 (224) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/commands/cursorflow-clean.md +19 -0
  3. package/commands/cursorflow-runs.md +59 -0
  4. package/commands/cursorflow-stop.md +55 -0
  5. package/dist/cli/clean.js +171 -0
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +7 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +1 -1
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +83 -42
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.d.ts +7 -0
  14. package/dist/cli/monitor.js +1007 -189
  15. package/dist/cli/monitor.js.map +1 -1
  16. package/dist/cli/prepare.js +87 -3
  17. package/dist/cli/prepare.js.map +1 -1
  18. package/dist/cli/resume.js +188 -236
  19. package/dist/cli/resume.js.map +1 -1
  20. package/dist/cli/run.js +125 -3
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/runs.d.ts +5 -0
  23. package/dist/cli/runs.js +214 -0
  24. package/dist/cli/runs.js.map +1 -0
  25. package/dist/cli/setup-commands.js +0 -0
  26. package/dist/cli/signal.js +1 -1
  27. package/dist/cli/signal.js.map +1 -1
  28. package/dist/cli/stop.d.ts +5 -0
  29. package/dist/cli/stop.js +215 -0
  30. package/dist/cli/stop.js.map +1 -0
  31. package/dist/cli/tasks.d.ts +10 -0
  32. package/dist/cli/tasks.js +165 -0
  33. package/dist/cli/tasks.js.map +1 -0
  34. package/dist/core/auto-recovery.d.ts +212 -0
  35. package/dist/core/auto-recovery.js +737 -0
  36. package/dist/core/auto-recovery.js.map +1 -0
  37. package/dist/core/failure-policy.d.ts +156 -0
  38. package/dist/core/failure-policy.js +488 -0
  39. package/dist/core/failure-policy.js.map +1 -0
  40. package/dist/core/orchestrator.d.ts +15 -2
  41. package/dist/core/orchestrator.js +397 -15
  42. package/dist/core/orchestrator.js.map +1 -1
  43. package/dist/core/reviewer.d.ts +2 -0
  44. package/dist/core/reviewer.js +2 -0
  45. package/dist/core/reviewer.js.map +1 -1
  46. package/dist/core/runner.d.ts +33 -10
  47. package/dist/core/runner.js +321 -146
  48. package/dist/core/runner.js.map +1 -1
  49. package/dist/services/logging/buffer.d.ts +67 -0
  50. package/dist/services/logging/buffer.js +309 -0
  51. package/dist/services/logging/buffer.js.map +1 -0
  52. package/dist/services/logging/console.d.ts +89 -0
  53. package/dist/services/logging/console.js +169 -0
  54. package/dist/services/logging/console.js.map +1 -0
  55. package/dist/services/logging/file-writer.d.ts +71 -0
  56. package/dist/services/logging/file-writer.js +516 -0
  57. package/dist/services/logging/file-writer.js.map +1 -0
  58. package/dist/services/logging/formatter.d.ts +39 -0
  59. package/dist/services/logging/formatter.js +227 -0
  60. package/dist/services/logging/formatter.js.map +1 -0
  61. package/dist/services/logging/index.d.ts +11 -0
  62. package/dist/services/logging/index.js +30 -0
  63. package/dist/services/logging/index.js.map +1 -0
  64. package/dist/services/logging/parser.d.ts +31 -0
  65. package/dist/services/logging/parser.js +222 -0
  66. package/dist/services/logging/parser.js.map +1 -0
  67. package/dist/services/process/index.d.ts +59 -0
  68. package/dist/services/process/index.js +257 -0
  69. package/dist/services/process/index.js.map +1 -0
  70. package/dist/types/agent.d.ts +20 -0
  71. package/dist/types/agent.js +6 -0
  72. package/dist/types/agent.js.map +1 -0
  73. package/dist/types/config.d.ts +65 -0
  74. package/dist/types/config.js +6 -0
  75. package/dist/types/config.js.map +1 -0
  76. package/dist/types/events.d.ts +125 -0
  77. package/dist/types/events.js +6 -0
  78. package/dist/types/events.js.map +1 -0
  79. package/dist/types/index.d.ts +12 -0
  80. package/dist/types/index.js +37 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/types/lane.d.ts +43 -0
  83. package/dist/types/lane.js +6 -0
  84. package/dist/types/lane.js.map +1 -0
  85. package/dist/types/logging.d.ts +71 -0
  86. package/dist/types/logging.js +16 -0
  87. package/dist/types/logging.js.map +1 -0
  88. package/dist/types/review.d.ts +17 -0
  89. package/dist/types/review.js +6 -0
  90. package/dist/types/review.js.map +1 -0
  91. package/dist/types/run.d.ts +32 -0
  92. package/dist/types/run.js +6 -0
  93. package/dist/types/run.js.map +1 -0
  94. package/dist/types/task.d.ts +71 -0
  95. package/dist/types/task.js +6 -0
  96. package/dist/types/task.js.map +1 -0
  97. package/dist/ui/components.d.ts +134 -0
  98. package/dist/ui/components.js +389 -0
  99. package/dist/ui/components.js.map +1 -0
  100. package/dist/ui/log-viewer.d.ts +49 -0
  101. package/dist/ui/log-viewer.js +449 -0
  102. package/dist/ui/log-viewer.js.map +1 -0
  103. package/dist/utils/checkpoint.d.ts +87 -0
  104. package/dist/utils/checkpoint.js +317 -0
  105. package/dist/utils/checkpoint.js.map +1 -0
  106. package/dist/utils/config.d.ts +4 -0
  107. package/dist/utils/config.js +11 -2
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/cursor-agent.js.map +1 -1
  110. package/dist/utils/dependency.d.ts +74 -0
  111. package/dist/utils/dependency.js +420 -0
  112. package/dist/utils/dependency.js.map +1 -0
  113. package/dist/utils/doctor.js +10 -5
  114. package/dist/utils/doctor.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +10 -33
  116. package/dist/utils/enhanced-logger.js +94 -9
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/git.d.ts +121 -0
  119. package/dist/utils/git.js +322 -2
  120. package/dist/utils/git.js.map +1 -1
  121. package/dist/utils/health.d.ts +91 -0
  122. package/dist/utils/health.js +556 -0
  123. package/dist/utils/health.js.map +1 -0
  124. package/dist/utils/lock.d.ts +95 -0
  125. package/dist/utils/lock.js +332 -0
  126. package/dist/utils/lock.js.map +1 -0
  127. package/dist/utils/log-buffer.d.ts +17 -0
  128. package/dist/utils/log-buffer.js +14 -0
  129. package/dist/utils/log-buffer.js.map +1 -0
  130. package/dist/utils/log-constants.d.ts +23 -0
  131. package/dist/utils/log-constants.js +28 -0
  132. package/dist/utils/log-constants.js.map +1 -0
  133. package/dist/utils/log-formatter.d.ts +9 -0
  134. package/dist/utils/log-formatter.js +113 -70
  135. package/dist/utils/log-formatter.js.map +1 -1
  136. package/dist/utils/log-service.d.ts +19 -0
  137. package/dist/utils/log-service.js +47 -0
  138. package/dist/utils/log-service.js.map +1 -0
  139. package/dist/utils/logger.d.ts +46 -27
  140. package/dist/utils/logger.js +82 -60
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/process-manager.d.ts +21 -0
  143. package/dist/utils/process-manager.js +138 -0
  144. package/dist/utils/process-manager.js.map +1 -0
  145. package/dist/utils/retry.d.ts +121 -0
  146. package/dist/utils/retry.js +374 -0
  147. package/dist/utils/retry.js.map +1 -0
  148. package/dist/utils/run-service.d.ts +88 -0
  149. package/dist/utils/run-service.js +412 -0
  150. package/dist/utils/run-service.js.map +1 -0
  151. package/dist/utils/state.d.ts +58 -2
  152. package/dist/utils/state.js +306 -3
  153. package/dist/utils/state.js.map +1 -1
  154. package/dist/utils/task-service.d.ts +82 -0
  155. package/dist/utils/task-service.js +348 -0
  156. package/dist/utils/task-service.js.map +1 -0
  157. package/dist/utils/types.d.ts +2 -272
  158. package/dist/utils/types.js +16 -0
  159. package/dist/utils/types.js.map +1 -1
  160. package/package.json +38 -23
  161. package/scripts/ai-security-check.js +0 -1
  162. package/scripts/local-security-gate.sh +0 -0
  163. package/scripts/monitor-lanes.sh +94 -0
  164. package/scripts/patches/test-cursor-agent.js +0 -1
  165. package/scripts/release.sh +0 -0
  166. package/scripts/setup-security.sh +0 -0
  167. package/scripts/stream-logs.sh +72 -0
  168. package/scripts/verify-and-fix.sh +0 -0
  169. package/src/cli/clean.ts +180 -0
  170. package/src/cli/index.ts +7 -0
  171. package/src/cli/init.ts +1 -1
  172. package/src/cli/logs.ts +79 -42
  173. package/src/cli/monitor.ts +1815 -899
  174. package/src/cli/prepare.ts +97 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +154 -3
  177. package/src/cli/runs.ts +212 -0
  178. package/src/cli/setup-commands.ts +0 -0
  179. package/src/cli/signal.ts +1 -1
  180. package/src/cli/stop.ts +209 -0
  181. package/src/cli/tasks.ts +154 -0
  182. package/src/core/auto-recovery.ts +909 -0
  183. package/src/core/failure-policy.ts +592 -0
  184. package/src/core/orchestrator.ts +1136 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +1443 -1217
  187. package/src/services/logging/buffer.ts +326 -0
  188. package/src/services/logging/console.ts +193 -0
  189. package/src/services/logging/file-writer.ts +526 -0
  190. package/src/services/logging/formatter.ts +268 -0
  191. package/src/services/logging/index.ts +16 -0
  192. package/src/services/logging/parser.ts +232 -0
  193. package/src/services/process/index.ts +261 -0
  194. package/src/types/agent.ts +24 -0
  195. package/src/types/config.ts +79 -0
  196. package/src/types/events.ts +156 -0
  197. package/src/types/index.ts +29 -0
  198. package/src/types/lane.ts +56 -0
  199. package/src/types/logging.ts +96 -0
  200. package/src/types/review.ts +20 -0
  201. package/src/types/run.ts +37 -0
  202. package/src/types/task.ts +79 -0
  203. package/src/ui/components.ts +430 -0
  204. package/src/ui/log-viewer.ts +485 -0
  205. package/src/utils/checkpoint.ts +374 -0
  206. package/src/utils/config.ts +11 -2
  207. package/src/utils/cursor-agent.ts +1 -1
  208. package/src/utils/dependency.ts +482 -0
  209. package/src/utils/doctor.ts +11 -5
  210. package/src/utils/enhanced-logger.ts +108 -49
  211. package/src/utils/git.ts +871 -499
  212. package/src/utils/health.ts +596 -0
  213. package/src/utils/lock.ts +346 -0
  214. package/src/utils/log-buffer.ts +28 -0
  215. package/src/utils/log-constants.ts +26 -0
  216. package/src/utils/log-formatter.ts +120 -37
  217. package/src/utils/log-service.ts +49 -0
  218. package/src/utils/logger.ts +100 -51
  219. package/src/utils/process-manager.ts +100 -0
  220. package/src/utils/retry.ts +413 -0
  221. package/src/utils/run-service.ts +433 -0
  222. package/src/utils/state.ts +369 -3
  223. package/src/utils/task-service.ts +370 -0
  224. package/src/utils/types.ts +2 -315
package/src/utils/git.ts CHANGED
@@ -1,499 +1,871 @@
1
- /**
2
- * Git utilities for CursorFlow
3
- */
4
-
5
- import { execSync, spawnSync } from 'child_process';
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import { safeJoin } from './path';
9
-
10
- /**
11
- * Acquire a file-based lock for Git operations
12
- */
13
- function acquireLock(lockName: string, cwd?: string): string | null {
14
- const repoRoot = cwd || getRepoRoot();
15
- const lockDir = safeJoin(repoRoot, '_cursorflow', 'locks');
16
- if (!fs.existsSync(lockDir)) {
17
- fs.mkdirSync(lockDir, { recursive: true });
18
- }
19
-
20
- const lockFile = safeJoin(lockDir, `${lockName}.lock`);
21
-
22
- try {
23
- // wx flag ensures atomic creation
24
- fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' });
25
- return lockFile;
26
- } catch {
27
- return null;
28
- }
29
- }
30
-
31
- /**
32
- * Release a file-based lock
33
- */
34
- function releaseLock(lockFile: string | null): void {
35
- if (lockFile && fs.existsSync(lockFile)) {
36
- try {
37
- fs.unlinkSync(lockFile);
38
- } catch {
39
- // Ignore
40
- }
41
- }
42
- }
43
-
44
- /**
45
- * Run Git command with locking
46
- */
47
- async function runGitWithLock<T>(
48
- lockName: string,
49
- fn: () => T,
50
- options: { cwd?: string; maxRetries?: number; retryDelay?: number } = {}
51
- ): Promise<T> {
52
- const maxRetries = options.maxRetries ?? 10;
53
- const retryDelay = options.retryDelay ?? 500;
54
-
55
- let retries = 0;
56
- let lockFile = null;
57
-
58
- while (retries < maxRetries) {
59
- lockFile = acquireLock(lockName, options.cwd);
60
- if (lockFile) break;
61
-
62
- retries++;
63
- const delay = Math.floor(Math.random() * retryDelay) + retryDelay / 2;
64
- await new Promise(resolve => setTimeout(resolve, delay));
65
- }
66
-
67
- if (!lockFile) {
68
- throw new Error(`Failed to acquire lock: ${lockName}`);
69
- }
70
-
71
- try {
72
- return fn();
73
- } finally {
74
- releaseLock(lockFile);
75
- }
76
- }
77
-
78
- export interface GitRunOptions {
79
- cwd?: string;
80
- silent?: boolean;
81
- }
82
-
83
- export interface GitResult {
84
- exitCode: number;
85
- stdout: string;
86
- stderr: string;
87
- success: boolean;
88
- }
89
-
90
- export interface WorktreeInfo {
91
- path: string;
92
- branch?: string;
93
- head?: string;
94
- }
95
-
96
- export interface ChangedFile {
97
- status: string;
98
- file: string;
99
- }
100
-
101
- export interface CommitInfo {
102
- hash: string;
103
- shortHash: string;
104
- author: string;
105
- authorEmail: string;
106
- timestamp: number;
107
- subject: string;
108
- }
109
-
110
- /**
111
- * Filter out noisy git stderr messages
112
- */
113
- function filterGitStderr(stderr: string): string {
114
- if (!stderr) return '';
115
-
116
- const lines = stderr.split('\n');
117
- const filtered = lines.filter(line => {
118
- // GitHub noise
119
- if (line.includes('remote: Create a pull request')) return false;
120
- if (line.trim().startsWith('remote:') && line.includes('pull/new')) return false;
121
- if (line.trim() === 'remote:') return false; // Empty remote lines
122
-
123
- return true;
124
- });
125
-
126
- return filtered.join('\n');
127
- }
128
-
129
- /**
130
- * Run git command and return output
131
- */
132
- export function runGit(args: string[], options: GitRunOptions = {}): string {
133
- const { cwd, silent = false } = options;
134
-
135
- try {
136
- const stdioMode = silent ? 'pipe' : ['inherit', 'inherit', 'pipe'];
137
-
138
- const result = spawnSync('git', args, {
139
- cwd: cwd || process.cwd(),
140
- encoding: 'utf8',
141
- stdio: stdioMode as any,
142
- });
143
-
144
- if (!silent && result.stderr) {
145
- const filteredStderr = filterGitStderr(result.stderr);
146
- if (filteredStderr) {
147
- process.stderr.write(filteredStderr);
148
- }
149
- }
150
-
151
- if (result.status !== 0 && !silent) {
152
- throw new Error(`Git command failed: git ${args.join(' ')}\n${result.stderr || ''}`);
153
- }
154
-
155
- return result.stdout ? result.stdout.trim() : '';
156
- } catch (error) {
157
- if (silent) {
158
- return '';
159
- }
160
- throw error;
161
- }
162
- }
163
-
164
- /**
165
- * Run git command and return result object
166
- */
167
- export function runGitResult(args: string[], options: GitRunOptions = {}): GitResult {
168
- const { cwd } = options;
169
-
170
- const result = spawnSync('git', args, {
171
- cwd: cwd || process.cwd(),
172
- encoding: 'utf8',
173
- stdio: 'pipe',
174
- });
175
-
176
- return {
177
- exitCode: result.status ?? 1,
178
- stdout: (result.stdout || '').toString().trim(),
179
- stderr: (result.stderr || '').toString().trim(),
180
- success: result.status === 0,
181
- };
182
- }
183
-
184
- /**
185
- * Get current branch name
186
- */
187
- export function getCurrentBranch(cwd?: string): string {
188
- return runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, silent: true });
189
- }
190
-
191
- /**
192
- * Get repository root directory
193
- */
194
- export function getRepoRoot(cwd?: string): string {
195
- return runGit(['rev-parse', '--show-toplevel'], { cwd, silent: true });
196
- }
197
-
198
- /**
199
- * Check if directory is a git repository
200
- */
201
- export function isGitRepo(cwd?: string): boolean {
202
- const result = runGitResult(['rev-parse', '--git-dir'], { cwd });
203
- return result.success;
204
- }
205
-
206
- /**
207
- * Check if worktree exists
208
- */
209
- export function worktreeExists(worktreePath: string, cwd?: string): boolean {
210
- const result = runGitResult(['worktree', 'list'], { cwd });
211
- if (!result.success) return false;
212
-
213
- return result.stdout.includes(worktreePath);
214
- }
215
-
216
- /**
217
- * Create worktree
218
- */
219
- export function createWorktree(worktreePath: string, branchName: string, options: { cwd?: string; baseBranch?: string } = {}): string {
220
- const { cwd, baseBranch = 'main' } = options;
221
-
222
- // Use a file-based lock to prevent race conditions during worktree creation
223
- const lockDir = safeJoin(cwd || getRepoRoot(), '_cursorflow', 'locks');
224
- if (!fs.existsSync(lockDir)) {
225
- fs.mkdirSync(lockDir, { recursive: true });
226
- }
227
- const lockFile = safeJoin(lockDir, 'worktree.lock');
228
-
229
- let retries = 20;
230
- let acquired = false;
231
-
232
- while (retries > 0 && !acquired) {
233
- try {
234
- fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' });
235
- acquired = true;
236
- } catch {
237
- retries--;
238
- const delay = Math.floor(Math.random() * 500) + 200;
239
- // Use synchronous sleep to keep the function signature synchronous
240
- const end = Date.now() + delay;
241
- while (Date.now() < end) { /* wait */ }
242
- }
243
- }
244
-
245
- if (!acquired) {
246
- throw new Error('Failed to acquire worktree lock after multiple retries');
247
- }
248
-
249
- try {
250
- // Check if branch already exists
251
- const branchExists = runGitResult(['rev-parse', '--verify', branchName], { cwd }).success;
252
-
253
- if (branchExists) {
254
- // Branch exists, checkout to worktree
255
- runGit(['worktree', 'add', worktreePath, branchName], { cwd });
256
- } else {
257
- // Create new branch from base
258
- runGit(['worktree', 'add', '-b', branchName, worktreePath, baseBranch], { cwd });
259
- }
260
-
261
- return worktreePath;
262
- } finally {
263
- try {
264
- fs.unlinkSync(lockFile);
265
- } catch {
266
- // Ignore
267
- }
268
- }
269
- }
270
-
271
- /**
272
- * Remove worktree
273
- */
274
- export function removeWorktree(worktreePath: string, options: { cwd?: string; force?: boolean } = {}): void {
275
- const { cwd, force = false } = options;
276
-
277
- const args = ['worktree', 'remove', worktreePath];
278
- if (force) {
279
- args.push('--force');
280
- }
281
-
282
- runGit(args, { cwd });
283
- }
284
-
285
- /**
286
- * List all worktrees
287
- */
288
- export function listWorktrees(cwd?: string): WorktreeInfo[] {
289
- const result = runGitResult(['worktree', 'list', '--porcelain'], { cwd });
290
- if (!result.success) return [];
291
-
292
- const worktrees: WorktreeInfo[] = [];
293
- const lines = result.stdout.split('\n');
294
- let current: Partial<WorktreeInfo> = {};
295
-
296
- for (const line of lines) {
297
- if (line.startsWith('worktree ')) {
298
- if (current.path) {
299
- worktrees.push(current as WorktreeInfo);
300
- }
301
- current = { path: line.slice(9) };
302
- } else if (line.startsWith('branch ')) {
303
- current.branch = line.slice(7);
304
- } else if (line.startsWith('HEAD ')) {
305
- current.head = line.slice(5);
306
- }
307
- }
308
-
309
- if (current.path) {
310
- worktrees.push(current as WorktreeInfo);
311
- }
312
-
313
- return worktrees;
314
- }
315
-
316
- /**
317
- * Check if there are uncommitted changes
318
- */
319
- export function hasUncommittedChanges(cwd?: string): boolean {
320
- const result = runGitResult(['status', '--porcelain'], { cwd });
321
- return result.success && result.stdout.length > 0;
322
- }
323
-
324
- /**
325
- * Get list of changed files
326
- */
327
- export function getChangedFiles(cwd?: string): ChangedFile[] {
328
- const result = runGitResult(['status', '--porcelain'], { cwd });
329
- if (!result.success) return [];
330
-
331
- return result.stdout
332
- .split('\n')
333
- .filter(line => line.trim())
334
- .map(line => {
335
- const status = line.slice(0, 2);
336
- const file = line.slice(3);
337
- return { status, file };
338
- });
339
- }
340
-
341
- /**
342
- * Create commit
343
- */
344
- export function commit(message: string, options: { cwd?: string; addAll?: boolean } = {}): void {
345
- const { cwd, addAll = true } = options;
346
-
347
- if (addAll) {
348
- runGit(['add', '-A'], { cwd });
349
- }
350
-
351
- runGit(['commit', '-m', message], { cwd });
352
- }
353
-
354
- /**
355
- * Check if a remote exists
356
- */
357
- export function remoteExists(remoteName = 'origin', options: { cwd?: string } = {}): boolean {
358
- const result = runGitResult(['remote'], { cwd: options.cwd });
359
- if (!result.success) return false;
360
- return result.stdout.split('\n').map(r => r.trim()).includes(remoteName);
361
- }
362
-
363
- /**
364
- * Push to remote
365
- */
366
- export function push(branchName: string, options: { cwd?: string; force?: boolean; setUpstream?: boolean } = {}): void {
367
- const { cwd, force = false, setUpstream = false } = options;
368
-
369
- // Check if origin exists before pushing
370
- if (!remoteExists('origin', { cwd })) {
371
- // If no origin, just skip pushing (useful for local tests)
372
- return;
373
- }
374
-
375
- const args = ['push'];
376
-
377
- if (force) {
378
- args.push('--force');
379
- }
380
-
381
- if (setUpstream) {
382
- args.push('-u', 'origin', branchName);
383
- } else {
384
- args.push('origin', branchName);
385
- }
386
-
387
- runGit(args, { cwd });
388
- }
389
-
390
- /**
391
- * Fetch from remote
392
- */
393
- export function fetch(options: { cwd?: string; prune?: boolean } = {}): void {
394
- const { cwd, prune = true } = options;
395
-
396
- const args = ['fetch', 'origin'];
397
- if (prune) {
398
- args.push('--prune');
399
- }
400
-
401
- runGit(args, { cwd });
402
- }
403
-
404
- /**
405
- * Check if branch exists (local or remote)
406
- */
407
- export function branchExists(branchName: string, options: { cwd?: string; remote?: boolean } = {}): boolean {
408
- const { cwd, remote = false } = options;
409
-
410
- if (remote) {
411
- const result = runGitResult(['ls-remote', '--heads', 'origin', branchName], { cwd });
412
- return result.success && result.stdout.length > 0;
413
- } else {
414
- const result = runGitResult(['rev-parse', '--verify', branchName], { cwd });
415
- return result.success;
416
- }
417
- }
418
-
419
- /**
420
- * Delete branch
421
- */
422
- export function deleteBranch(branchName: string, options: { cwd?: string; force?: boolean; remote?: boolean } = {}): void {
423
- const { cwd, force = false, remote = false } = options;
424
-
425
- if (remote) {
426
- runGit(['push', 'origin', '--delete', branchName], { cwd });
427
- } else {
428
- const args = ['branch', force ? '-D' : '-d', branchName];
429
- runGit(args, { cwd });
430
- }
431
- }
432
-
433
- /**
434
- * Merge branch
435
- */
436
- export function merge(branchName: string, options: { cwd?: string; noFf?: boolean; message?: string | null } = {}): void {
437
- const { cwd, noFf = false, message = null } = options;
438
-
439
- const args = ['merge'];
440
-
441
- if (noFf) {
442
- args.push('--no-ff');
443
- }
444
-
445
- if (message) {
446
- args.push('-m', message);
447
- }
448
-
449
- args.push(branchName);
450
-
451
- runGit(args, { cwd });
452
- }
453
-
454
- /**
455
- * Get commit info
456
- */
457
- export function getCommitInfo(commitHash: string, options: { cwd?: string } = {}): CommitInfo | null {
458
- const { cwd } = options;
459
-
460
- const format = '--format=%H%n%h%n%an%n%ae%n%at%n%s';
461
- const result = runGitResult(['show', '-s', format, commitHash], { cwd });
462
-
463
- if (!result.success) return null;
464
-
465
- const lines = result.stdout.split('\n');
466
- return {
467
- hash: lines[0] || '',
468
- shortHash: lines[1] || '',
469
- author: lines[2] || '',
470
- authorEmail: lines[3] || '',
471
- timestamp: parseInt(lines[4] || '0'),
472
- subject: lines[5] || '',
473
- };
474
- }
475
-
476
- /**
477
- * Get diff statistics for the last operation (commit or merge)
478
- * Comparing HEAD with its first parent
479
- */
480
- export function getLastOperationStats(cwd?: string): string {
481
- try {
482
- // Check if there are any commits
483
- const hasCommits = runGitResult(['rev-parse', 'HEAD'], { cwd }).success;
484
- if (!hasCommits) return '';
485
-
486
- // Check if HEAD has a parent
487
- const hasParent = runGitResult(['rev-parse', 'HEAD^1'], { cwd }).success;
488
- if (!hasParent) {
489
- // If no parent, show stats for the first commit
490
- // Using an empty tree hash as the base
491
- const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
492
- return runGit(['diff', '--stat', emptyTree, 'HEAD'], { cwd, silent: true });
493
- }
494
-
495
- return runGit(['diff', '--stat', 'HEAD^1', 'HEAD'], { cwd, silent: true });
496
- } catch (e) {
497
- return '';
498
- }
499
- }
1
+ /**
2
+ * Git utilities for CursorFlow
3
+ */
4
+
5
+ import { execSync, spawnSync } from 'child_process';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { safeJoin } from './path';
9
+
10
+ /**
11
+ * Acquire a file-based lock for Git operations
12
+ */
13
+ function acquireLock(lockName: string, cwd?: string): string | null {
14
+ const repoRoot = cwd || getRepoRoot();
15
+ const lockDir = safeJoin(repoRoot, '_cursorflow', 'locks');
16
+ if (!fs.existsSync(lockDir)) {
17
+ fs.mkdirSync(lockDir, { recursive: true });
18
+ }
19
+
20
+ const lockFile = safeJoin(lockDir, `${lockName}.lock`);
21
+
22
+ try {
23
+ // wx flag ensures atomic creation
24
+ fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' });
25
+ return lockFile;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Release a file-based lock
33
+ */
34
+ function releaseLock(lockFile: string | null): void {
35
+ if (lockFile && fs.existsSync(lockFile)) {
36
+ try {
37
+ fs.unlinkSync(lockFile);
38
+ } catch {
39
+ // Ignore
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Run Git command with locking
46
+ */
47
+ async function runGitWithLock<T>(
48
+ lockName: string,
49
+ fn: () => T,
50
+ options: { cwd?: string; maxRetries?: number; retryDelay?: number } = {}
51
+ ): Promise<T> {
52
+ const maxRetries = options.maxRetries ?? 10;
53
+ const retryDelay = options.retryDelay ?? 500;
54
+
55
+ let retries = 0;
56
+ let lockFile = null;
57
+
58
+ while (retries < maxRetries) {
59
+ lockFile = acquireLock(lockName, options.cwd);
60
+ if (lockFile) break;
61
+
62
+ retries++;
63
+ const delay = Math.floor(Math.random() * retryDelay) + retryDelay / 2;
64
+ await new Promise(resolve => setTimeout(resolve, delay));
65
+ }
66
+
67
+ if (!lockFile) {
68
+ throw new Error(`Failed to acquire lock: ${lockName}`);
69
+ }
70
+
71
+ try {
72
+ return fn();
73
+ } finally {
74
+ releaseLock(lockFile);
75
+ }
76
+ }
77
+
78
+ export interface GitRunOptions {
79
+ cwd?: string;
80
+ silent?: boolean;
81
+ }
82
+
83
+ export interface GitResult {
84
+ exitCode: number;
85
+ stdout: string;
86
+ stderr: string;
87
+ success: boolean;
88
+ }
89
+
90
+ export interface WorktreeInfo {
91
+ path: string;
92
+ branch?: string;
93
+ head?: string;
94
+ }
95
+
96
+ export interface ChangedFile {
97
+ status: string;
98
+ file: string;
99
+ }
100
+
101
+ export interface CommitInfo {
102
+ hash: string;
103
+ shortHash: string;
104
+ author: string;
105
+ authorEmail: string;
106
+ timestamp: number;
107
+ subject: string;
108
+ }
109
+
110
+ /**
111
+ * Filter out noisy git stderr messages
112
+ */
113
+ function filterGitStderr(stderr: string): string {
114
+ if (!stderr) return '';
115
+
116
+ const lines = stderr.split('\n');
117
+ const filtered = lines.filter(line => {
118
+ // GitHub noise
119
+ if (line.includes('remote: Create a pull request')) return false;
120
+ if (line.trim().startsWith('remote:') && line.includes('pull/new')) return false;
121
+ if (line.trim() === 'remote:') return false; // Empty remote lines
122
+
123
+ return true;
124
+ });
125
+
126
+ return filtered.join('\n');
127
+ }
128
+
129
+ /**
130
+ * Run git command and return output
131
+ */
132
+ export function runGit(args: string[], options: GitRunOptions = {}): string {
133
+ const { cwd, silent = false } = options;
134
+
135
+ try {
136
+ const stdioMode = silent ? 'pipe' : ['inherit', 'inherit', 'pipe'];
137
+
138
+ const result = spawnSync('git', args, {
139
+ cwd: cwd || process.cwd(),
140
+ encoding: 'utf8',
141
+ stdio: stdioMode as any,
142
+ });
143
+
144
+ if (!silent && result.stderr) {
145
+ const filteredStderr = filterGitStderr(result.stderr);
146
+ if (filteredStderr) {
147
+ process.stderr.write(filteredStderr);
148
+ }
149
+ }
150
+
151
+ if (result.status !== 0 && !silent) {
152
+ throw new Error(`Git command failed: git ${args.join(' ')}\n${result.stderr || ''}`);
153
+ }
154
+
155
+ return result.stdout ? result.stdout.trim() : '';
156
+ } catch (error) {
157
+ if (silent) {
158
+ return '';
159
+ }
160
+ throw error;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Run git command and return result object
166
+ */
167
+ export function runGitResult(args: string[], options: GitRunOptions = {}): GitResult {
168
+ const { cwd } = options;
169
+
170
+ const result = spawnSync('git', args, {
171
+ cwd: cwd || process.cwd(),
172
+ encoding: 'utf8',
173
+ stdio: 'pipe',
174
+ });
175
+
176
+ return {
177
+ exitCode: result.status ?? 1,
178
+ stdout: (result.stdout || '').toString().trim(),
179
+ stderr: (result.stderr || '').toString().trim(),
180
+ success: result.status === 0,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Get current branch name
186
+ */
187
+ export function getCurrentBranch(cwd?: string): string {
188
+ return runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, silent: true });
189
+ }
190
+
191
+ /**
192
+ * Get repository root directory
193
+ */
194
+ export function getRepoRoot(cwd?: string): string {
195
+ return runGit(['rev-parse', '--show-toplevel'], { cwd, silent: true });
196
+ }
197
+
198
+ /**
199
+ * Get main repository root directory (for worktrees)
200
+ */
201
+ export function getMainRepoRoot(cwd?: string): string {
202
+ try {
203
+ const result = runGitResult(['worktree', 'list', '--porcelain'], { cwd });
204
+ if (result.success && result.stdout) {
205
+ const firstLine = result.stdout.split('\n')[0];
206
+ if (firstLine && firstLine.startsWith('worktree ')) {
207
+ return firstLine.slice(9).trim();
208
+ }
209
+ }
210
+ } catch {
211
+ // Fallback to normal repo root
212
+ }
213
+ return getRepoRoot(cwd);
214
+ }
215
+
216
+ /**
217
+ * Check if directory is a git repository
218
+ */
219
+ export function isGitRepo(cwd?: string): boolean {
220
+ const result = runGitResult(['rev-parse', '--git-dir'], { cwd });
221
+ return result.success;
222
+ }
223
+
224
+ /**
225
+ * Check if worktree exists
226
+ */
227
+ export function worktreeExists(worktreePath: string, cwd?: string): boolean {
228
+ const result = runGitResult(['worktree', 'list'], { cwd });
229
+ if (!result.success) return false;
230
+
231
+ return result.stdout.includes(worktreePath);
232
+ }
233
+
234
+ /**
235
+ * Create worktree
236
+ */
237
+ export function createWorktree(worktreePath: string, branchName: string, options: { cwd?: string; baseBranch?: string } = {}): string {
238
+ let { cwd, baseBranch } = options;
239
+
240
+ if (!baseBranch) {
241
+ baseBranch = getCurrentBranch(cwd) || 'refs/heads/main';
242
+ }
243
+
244
+ // Ensure baseBranch is unambiguous (branch name rather than tag)
245
+ const unambiguousBase = (baseBranch.startsWith('refs/') || baseBranch.includes('/'))
246
+ ? baseBranch
247
+ : `refs/heads/${baseBranch}`;
248
+
249
+ // Use a file-based lock to prevent race conditions during worktree creation
250
+ const lockDir = safeJoin(cwd || getRepoRoot(), '_cursorflow', 'locks');
251
+ if (!fs.existsSync(lockDir)) {
252
+ fs.mkdirSync(lockDir, { recursive: true });
253
+ }
254
+ const lockFile = safeJoin(lockDir, 'worktree.lock');
255
+
256
+ let retries = 20;
257
+ let acquired = false;
258
+
259
+ while (retries > 0 && !acquired) {
260
+ try {
261
+ fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' });
262
+ acquired = true;
263
+ } catch {
264
+ retries--;
265
+ const delay = Math.floor(Math.random() * 500) + 200;
266
+ // Use synchronous sleep to keep the function signature synchronous
267
+ const end = Date.now() + delay;
268
+ while (Date.now() < end) { /* wait */ }
269
+ }
270
+ }
271
+
272
+ if (!acquired) {
273
+ throw new Error('Failed to acquire worktree lock after multiple retries');
274
+ }
275
+
276
+ try {
277
+ // Check if branch already exists
278
+ const branchExists = runGitResult(['rev-parse', '--verify', branchName], { cwd }).success;
279
+
280
+ if (branchExists) {
281
+ // Branch exists, checkout to worktree
282
+ runGit(['worktree', 'add', worktreePath, branchName], { cwd });
283
+ } else {
284
+ // Create new branch from base
285
+ runGit(['worktree', 'add', '-b', branchName, worktreePath, unambiguousBase], { cwd });
286
+ }
287
+
288
+ return worktreePath;
289
+ } finally {
290
+ try {
291
+ fs.unlinkSync(lockFile);
292
+ } catch {
293
+ // Ignore
294
+ }
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Remove worktree
300
+ */
301
+ export function removeWorktree(worktreePath: string, options: { cwd?: string; force?: boolean } = {}): void {
302
+ const { cwd, force = false } = options;
303
+
304
+ const args = ['worktree', 'remove', worktreePath];
305
+ if (force) {
306
+ args.push('--force');
307
+ }
308
+
309
+ runGit(args, { cwd });
310
+ }
311
+
312
+ /**
313
+ * List all worktrees
314
+ */
315
+ export function listWorktrees(cwd?: string): WorktreeInfo[] {
316
+ const result = runGitResult(['worktree', 'list', '--porcelain'], { cwd });
317
+ if (!result.success) return [];
318
+
319
+ const worktrees: WorktreeInfo[] = [];
320
+ const lines = result.stdout.split('\n');
321
+ let current: Partial<WorktreeInfo> = {};
322
+
323
+ for (const line of lines) {
324
+ if (line.startsWith('worktree ')) {
325
+ if (current.path) {
326
+ worktrees.push(current as WorktreeInfo);
327
+ }
328
+ current = { path: line.slice(9) };
329
+ } else if (line.startsWith('branch ')) {
330
+ current.branch = line.slice(7);
331
+ } else if (line.startsWith('HEAD ')) {
332
+ current.head = line.slice(5);
333
+ }
334
+ }
335
+
336
+ if (current.path) {
337
+ worktrees.push(current as WorktreeInfo);
338
+ }
339
+
340
+ return worktrees;
341
+ }
342
+
343
+ /**
344
+ * Check if there are uncommitted changes
345
+ */
346
+ export function hasUncommittedChanges(cwd?: string): boolean {
347
+ const result = runGitResult(['status', '--porcelain'], { cwd });
348
+ return result.success && result.stdout.length > 0;
349
+ }
350
+
351
+ /**
352
+ * Get list of changed files
353
+ */
354
+ export function getChangedFiles(cwd?: string): ChangedFile[] {
355
+ const result = runGitResult(['status', '--porcelain'], { cwd });
356
+ if (!result.success) return [];
357
+
358
+ return result.stdout
359
+ .split('\n')
360
+ .filter(line => line.trim())
361
+ .map(line => {
362
+ const status = line.slice(0, 2);
363
+ const file = line.slice(3);
364
+ return { status, file };
365
+ });
366
+ }
367
+
368
+ /**
369
+ * Create commit
370
+ */
371
+ export function commit(message: string, options: { cwd?: string; addAll?: boolean } = {}): void {
372
+ const { cwd, addAll = true } = options;
373
+
374
+ if (addAll) {
375
+ runGit(['add', '-A'], { cwd });
376
+ }
377
+
378
+ runGit(['commit', '-m', message], { cwd });
379
+ }
380
+
381
+ /**
382
+ * Check if a remote exists
383
+ */
384
+ export function remoteExists(remoteName = 'origin', options: { cwd?: string } = {}): boolean {
385
+ const result = runGitResult(['remote'], { cwd: options.cwd });
386
+ if (!result.success) return false;
387
+ return result.stdout.split('\n').map(r => r.trim()).includes(remoteName);
388
+ }
389
+
390
+ /**
391
+ * Push to remote
392
+ */
393
+ export function push(branchName: string, options: { cwd?: string; force?: boolean; setUpstream?: boolean } = {}): void {
394
+ const { cwd, force = false, setUpstream = false } = options;
395
+
396
+ // Check if origin exists before pushing
397
+ if (!remoteExists('origin', { cwd })) {
398
+ // If no origin, just skip pushing (useful for local tests)
399
+ return;
400
+ }
401
+
402
+ const args = ['push'];
403
+
404
+ if (force) {
405
+ args.push('--force');
406
+ }
407
+
408
+ if (setUpstream) {
409
+ args.push('-u', 'origin', branchName);
410
+ } else {
411
+ args.push('origin', branchName);
412
+ }
413
+
414
+ runGit(args, { cwd });
415
+ }
416
+
417
+ /**
418
+ * Fetch from remote
419
+ */
420
+ export function fetch(options: { cwd?: string; prune?: boolean } = {}): void {
421
+ const { cwd, prune = true } = options;
422
+
423
+ const args = ['fetch', 'origin'];
424
+ if (prune) {
425
+ args.push('--prune');
426
+ }
427
+
428
+ runGit(args, { cwd });
429
+ }
430
+
431
+ /**
432
+ * Check if branch exists (local or remote)
433
+ */
434
+ export function branchExists(branchName: string, options: { cwd?: string; remote?: boolean } = {}): boolean {
435
+ const { cwd, remote = false } = options;
436
+
437
+ if (remote) {
438
+ const result = runGitResult(['ls-remote', '--heads', 'origin', branchName], { cwd });
439
+ return result.success && result.stdout.length > 0;
440
+ } else {
441
+ const result = runGitResult(['rev-parse', '--verify', branchName], { cwd });
442
+ return result.success;
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Delete branch
448
+ */
449
+ export function deleteBranch(branchName: string, options: { cwd?: string; force?: boolean; remote?: boolean } = {}): void {
450
+ const { cwd, force = false, remote = false } = options;
451
+
452
+ if (remote) {
453
+ runGit(['push', 'origin', '--delete', branchName], { cwd });
454
+ } else {
455
+ const args = ['branch', force ? '-D' : '-d', branchName];
456
+ runGit(args, { cwd });
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Merge branch
462
+ */
463
+ export function merge(branchName: string, options: { cwd?: string; noFf?: boolean; message?: string | null } = {}): void {
464
+ const { cwd, noFf = false, message = null } = options;
465
+
466
+ const args = ['merge'];
467
+
468
+ if (noFf) {
469
+ args.push('--no-ff');
470
+ }
471
+
472
+ if (message) {
473
+ args.push('-m', message);
474
+ }
475
+
476
+ args.push(branchName);
477
+
478
+ runGit(args, { cwd });
479
+ }
480
+
481
+ /**
482
+ * Get commit info
483
+ */
484
+ export function getCommitInfo(commitHash: string, options: { cwd?: string } = {}): CommitInfo | null {
485
+ const { cwd } = options;
486
+
487
+ const format = '--format=%H%n%h%n%an%n%ae%n%at%n%s';
488
+ const result = runGitResult(['show', '-s', format, commitHash], { cwd });
489
+
490
+ if (!result.success) return null;
491
+
492
+ const lines = result.stdout.split('\n');
493
+ return {
494
+ hash: lines[0] || '',
495
+ shortHash: lines[1] || '',
496
+ author: lines[2] || '',
497
+ authorEmail: lines[3] || '',
498
+ timestamp: parseInt(lines[4] || '0'),
499
+ subject: lines[5] || '',
500
+ };
501
+ }
502
+
503
+ /**
504
+ * Get diff statistics for the last operation (commit or merge)
505
+ * Comparing HEAD with its first parent
506
+ */
507
+ export function getLastOperationStats(cwd?: string): string {
508
+ try {
509
+ // Check if there are any commits
510
+ const hasCommits = runGitResult(['rev-parse', 'HEAD'], { cwd }).success;
511
+ if (!hasCommits) return '';
512
+
513
+ // Check if HEAD has a parent
514
+ const hasParent = runGitResult(['rev-parse', 'HEAD^1'], { cwd }).success;
515
+ if (!hasParent) {
516
+ // If no parent, show stats for the first commit
517
+ // Using an empty tree hash as the base
518
+ const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
519
+ return runGit(['diff', '--stat', emptyTree, 'HEAD'], { cwd, silent: true });
520
+ }
521
+
522
+ return runGit(['diff', '--stat', 'HEAD^1', 'HEAD'], { cwd, silent: true });
523
+ } catch (e) {
524
+ return '';
525
+ }
526
+ }
527
+
528
+ // ============================================================================
529
+ // Enhanced Git Functions for Robustness
530
+ // ============================================================================
531
+
532
+ /**
533
+ * Generate a unique branch name that doesn't conflict with existing branches
534
+ */
535
+ export function generateUniqueBranchName(baseName: string, options: { cwd?: string; maxAttempts?: number } = {}): string {
536
+ const { cwd, maxAttempts = 10 } = options;
537
+ const timestamp = Date.now().toString(36);
538
+ const random = () => Math.random().toString(36).substring(2, 5);
539
+
540
+ // First attempt: base name with timestamp
541
+ let candidate = `${baseName}-${timestamp}-${random()}`;
542
+
543
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
544
+ if (!branchExists(candidate, { cwd })) {
545
+ return candidate;
546
+ }
547
+ // Try with new random suffix
548
+ candidate = `${baseName}-${timestamp}-${random()}`;
549
+ }
550
+
551
+ // Last resort: use full timestamp with random
552
+ return `${baseName}-${Date.now()}-${random()}`;
553
+ }
554
+
555
+ /**
556
+ * Safe merge result
557
+ */
558
+ export interface SafeMergeResult {
559
+ success: boolean;
560
+ conflict: boolean;
561
+ conflictingFiles: string[];
562
+ error?: string;
563
+ aborted: boolean;
564
+ }
565
+
566
+ /**
567
+ * Safely merge a branch with conflict detection and auto-abort
568
+ */
569
+ export function safeMerge(branchName: string, options: {
570
+ cwd?: string;
571
+ noFf?: boolean;
572
+ message?: string | null;
573
+ abortOnConflict?: boolean;
574
+ strategy?: 'ours' | 'theirs' | null;
575
+ } = {}): SafeMergeResult {
576
+ const { cwd, noFf = false, message = null, abortOnConflict = true, strategy = null } = options;
577
+
578
+ const args = ['merge'];
579
+
580
+ if (noFf) {
581
+ args.push('--no-ff');
582
+ }
583
+
584
+ if (strategy) {
585
+ args.push('-X', strategy);
586
+ }
587
+
588
+ if (message) {
589
+ args.push('-m', message);
590
+ }
591
+
592
+ args.push(branchName);
593
+
594
+ const result = runGitResult(args, { cwd });
595
+
596
+ if (result.success) {
597
+ return {
598
+ success: true,
599
+ conflict: false,
600
+ conflictingFiles: [],
601
+ aborted: false,
602
+ };
603
+ }
604
+
605
+ // Check for conflicts
606
+ const output = result.stdout + result.stderr;
607
+ const isConflict = output.includes('CONFLICT') || output.includes('Automatic merge failed');
608
+
609
+ if (isConflict) {
610
+ // Get conflicting files
611
+ const conflictingFiles = getConflictingFiles(cwd);
612
+
613
+ if (abortOnConflict) {
614
+ // Abort the merge
615
+ runGitResult(['merge', '--abort'], { cwd });
616
+
617
+ return {
618
+ success: false,
619
+ conflict: true,
620
+ conflictingFiles,
621
+ error: 'Merge conflict detected and aborted',
622
+ aborted: true,
623
+ };
624
+ }
625
+
626
+ return {
627
+ success: false,
628
+ conflict: true,
629
+ conflictingFiles,
630
+ error: 'Merge conflict - manual resolution required',
631
+ aborted: false,
632
+ };
633
+ }
634
+
635
+ return {
636
+ success: false,
637
+ conflict: false,
638
+ conflictingFiles: [],
639
+ error: result.stderr || 'Merge failed',
640
+ aborted: false,
641
+ };
642
+ }
643
+
644
+ /**
645
+ * Get list of conflicting files
646
+ */
647
+ export function getConflictingFiles(cwd?: string): string[] {
648
+ const result = runGitResult(['diff', '--name-only', '--diff-filter=U'], { cwd });
649
+ if (!result.success) return [];
650
+
651
+ return result.stdout.split('\n').filter(f => f.trim());
652
+ }
653
+
654
+ /**
655
+ * Check if merge is in progress
656
+ */
657
+ export function isMergeInProgress(cwd?: string): boolean {
658
+ const repoRoot = getRepoRoot(cwd);
659
+ return fs.existsSync(path.join(repoRoot, '.git', 'MERGE_HEAD'));
660
+ }
661
+
662
+ /**
663
+ * Abort ongoing merge
664
+ */
665
+ export function abortMerge(cwd?: string): boolean {
666
+ const result = runGitResult(['merge', '--abort'], { cwd });
667
+ return result.success;
668
+ }
669
+
670
+ /**
671
+ * Get HEAD commit hash
672
+ */
673
+ export function getHead(cwd?: string): string {
674
+ return runGit(['rev-parse', 'HEAD'], { cwd, silent: true });
675
+ }
676
+
677
+ /**
678
+ * Get short HEAD commit hash
679
+ */
680
+ export function getHeadShort(cwd?: string): string {
681
+ return runGit(['rev-parse', '--short', 'HEAD'], { cwd, silent: true });
682
+ }
683
+
684
+ /**
685
+ * Stash changes with optional message
686
+ */
687
+ export function stash(message?: string, options: { cwd?: string } = {}): boolean {
688
+ const args = ['stash', 'push'];
689
+ if (message) {
690
+ args.push('-m', message);
691
+ }
692
+
693
+ const result = runGitResult(args, { cwd: options.cwd });
694
+ return result.success;
695
+ }
696
+
697
+ /**
698
+ * Pop stashed changes
699
+ */
700
+ export function stashPop(options: { cwd?: string } = {}): boolean {
701
+ const result = runGitResult(['stash', 'pop'], { cwd: options.cwd });
702
+ return result.success;
703
+ }
704
+
705
+ /**
706
+ * Clean worktree (remove untracked files)
707
+ */
708
+ export function cleanWorktree(options: { cwd?: string; force?: boolean; directories?: boolean } = {}): void {
709
+ const args = ['clean'];
710
+ if (options.force) args.push('-f');
711
+ if (options.directories) args.push('-d');
712
+
713
+ runGit(args, { cwd: options.cwd });
714
+ }
715
+
716
+ /**
717
+ * Reset worktree to specific commit/branch
718
+ */
719
+ export function reset(target: string, options: { cwd?: string; mode?: 'soft' | 'mixed' | 'hard' } = {}): void {
720
+ const args = ['reset'];
721
+ if (options.mode) args.push(`--${options.mode}`);
722
+ args.push(target);
723
+
724
+ runGit(args, { cwd: options.cwd });
725
+ }
726
+
727
+ /**
728
+ * Checkout specific commit or branch
729
+ */
730
+ export function checkout(target: string, options: { cwd?: string; force?: boolean; createBranch?: boolean } = {}): void {
731
+ const args = ['checkout'];
732
+ if (options.force) args.push('-f');
733
+ if (options.createBranch) args.push('-b');
734
+ args.push(target);
735
+
736
+ runGit(args, { cwd: options.cwd });
737
+ }
738
+
739
+ /**
740
+ * Get commits between two refs
741
+ */
742
+ export function getCommitsBetween(fromRef: string, toRef: string, options: { cwd?: string } = {}): CommitInfo[] {
743
+ const format = '%H|%h|%an|%ae|%at|%s';
744
+ const result = runGitResult(['log', '--format=' + format, `${fromRef}..${toRef}`], { cwd: options.cwd });
745
+
746
+ if (!result.success) return [];
747
+
748
+ return result.stdout.split('\n')
749
+ .filter(line => line.trim())
750
+ .map(line => {
751
+ const parts = line.split('|');
752
+ return {
753
+ hash: parts[0] || '',
754
+ shortHash: parts[1] || '',
755
+ author: parts[2] || '',
756
+ authorEmail: parts[3] || '',
757
+ timestamp: parseInt(parts[4] || '0'),
758
+ subject: parts[5] || '',
759
+ };
760
+ });
761
+ }
762
+
763
+ /**
764
+ * Enhanced worktree creation with async lock
765
+ */
766
+ export async function createWorktreeAsync(
767
+ worktreePath: string,
768
+ branchName: string,
769
+ options: { cwd?: string; baseBranch?: string; timeout?: number } = {}
770
+ ): Promise<string> {
771
+ let { cwd, baseBranch, timeout = 30000 } = options;
772
+
773
+ if (!baseBranch) {
774
+ baseBranch = getCurrentBranch(cwd) || 'refs/heads/main';
775
+ }
776
+
777
+ // Ensure baseBranch is unambiguous
778
+ const unambiguousBase = (baseBranch.startsWith('refs/') || baseBranch.includes('/'))
779
+ ? baseBranch
780
+ : `refs/heads/${baseBranch}`;
781
+
782
+ const { acquireLock, releaseLock } = await import('./lock');
783
+ const lockDir = safeJoin(cwd || getRepoRoot(), '_cursorflow', 'locks');
784
+ if (!fs.existsSync(lockDir)) {
785
+ fs.mkdirSync(lockDir, { recursive: true });
786
+ }
787
+ const lockFile = safeJoin(lockDir, 'worktree.lock');
788
+
789
+ const acquired = await acquireLock(lockFile, {
790
+ timeoutMs: timeout,
791
+ operation: `createWorktree:${branchName}`,
792
+ });
793
+
794
+ if (!acquired) {
795
+ throw new Error('Failed to acquire worktree lock after timeout');
796
+ }
797
+
798
+ try {
799
+ // Check if branch already exists
800
+ const branchExistsLocal = runGitResult(['rev-parse', '--verify', branchName], { cwd }).success;
801
+
802
+ if (branchExistsLocal) {
803
+ runGit(['worktree', 'add', worktreePath, branchName], { cwd });
804
+ } else {
805
+ runGit(['worktree', 'add', '-b', branchName, worktreePath, unambiguousBase], { cwd });
806
+ }
807
+
808
+ return worktreePath;
809
+ } finally {
810
+ await releaseLock(lockFile);
811
+ }
812
+ }
813
+
814
+ /**
815
+ * Prune orphaned worktrees
816
+ */
817
+ export function pruneWorktrees(options: { cwd?: string } = {}): void {
818
+ runGit(['worktree', 'prune'], { cwd: options.cwd });
819
+ }
820
+
821
+ /**
822
+ * Get worktree for a specific path
823
+ */
824
+ export function getWorktreeForPath(targetPath: string, cwd?: string): WorktreeInfo | null {
825
+ const worktrees = listWorktrees(cwd);
826
+ return worktrees.find(wt => wt.path === targetPath) || null;
827
+ }
828
+
829
+ /**
830
+ * Sync branch with remote (fetch + merge or rebase)
831
+ */
832
+ export function syncWithRemote(branch: string, options: {
833
+ cwd?: string;
834
+ strategy?: 'merge' | 'rebase';
835
+ createIfMissing?: boolean;
836
+ } = {}): { success: boolean; error?: string } {
837
+ const { cwd, strategy = 'merge', createIfMissing = false } = options;
838
+
839
+ // Fetch the branch
840
+ const fetchResult = runGitResult(['fetch', 'origin', branch], { cwd });
841
+
842
+ if (!fetchResult.success) {
843
+ if (createIfMissing && fetchResult.stderr.includes('not found')) {
844
+ // Branch doesn't exist on remote, nothing to sync
845
+ return { success: true };
846
+ }
847
+ return { success: false, error: fetchResult.stderr };
848
+ }
849
+
850
+ // Merge or rebase
851
+ if (strategy === 'rebase') {
852
+ const result = runGitResult(['rebase', `origin/${branch}`], { cwd });
853
+ if (!result.success) {
854
+ // Abort rebase on failure
855
+ runGitResult(['rebase', '--abort'], { cwd });
856
+ return { success: false, error: result.stderr };
857
+ }
858
+ } else {
859
+ const mergeResult = safeMerge(`origin/${branch}`, {
860
+ cwd,
861
+ message: `chore: sync with origin/${branch}`,
862
+ abortOnConflict: true,
863
+ });
864
+
865
+ if (!mergeResult.success) {
866
+ return { success: false, error: mergeResult.error };
867
+ }
868
+ }
869
+
870
+ return { success: true };
871
+ }