@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26

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 (234) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +25 -7
  3. package/commands/cursorflow-clean.md +19 -0
  4. package/commands/cursorflow-runs.md +59 -0
  5. package/commands/cursorflow-stop.md +55 -0
  6. package/dist/cli/clean.js +178 -6
  7. package/dist/cli/clean.js.map +1 -1
  8. package/dist/cli/index.js +12 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/init.js +8 -7
  11. package/dist/cli/init.js.map +1 -1
  12. package/dist/cli/logs.js +126 -77
  13. package/dist/cli/logs.js.map +1 -1
  14. package/dist/cli/monitor.d.ts +7 -0
  15. package/dist/cli/monitor.js +1021 -202
  16. package/dist/cli/monitor.js.map +1 -1
  17. package/dist/cli/prepare.js +39 -21
  18. package/dist/cli/prepare.js.map +1 -1
  19. package/dist/cli/resume.js +268 -163
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +11 -5
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/runs.d.ts +5 -0
  24. package/dist/cli/runs.js +214 -0
  25. package/dist/cli/runs.js.map +1 -0
  26. package/dist/cli/setup-commands.js +0 -0
  27. package/dist/cli/signal.js +8 -8
  28. package/dist/cli/signal.js.map +1 -1
  29. package/dist/cli/stop.d.ts +5 -0
  30. package/dist/cli/stop.js +215 -0
  31. package/dist/cli/stop.js.map +1 -0
  32. package/dist/cli/tasks.d.ts +10 -0
  33. package/dist/cli/tasks.js +165 -0
  34. package/dist/cli/tasks.js.map +1 -0
  35. package/dist/core/auto-recovery.d.ts +212 -0
  36. package/dist/core/auto-recovery.js +737 -0
  37. package/dist/core/auto-recovery.js.map +1 -0
  38. package/dist/core/failure-policy.d.ts +156 -0
  39. package/dist/core/failure-policy.js +488 -0
  40. package/dist/core/failure-policy.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +16 -2
  42. package/dist/core/orchestrator.js +439 -105
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/reviewer.d.ts +2 -0
  45. package/dist/core/reviewer.js +2 -0
  46. package/dist/core/reviewer.js.map +1 -1
  47. package/dist/core/runner.d.ts +33 -10
  48. package/dist/core/runner.js +374 -164
  49. package/dist/core/runner.js.map +1 -1
  50. package/dist/services/logging/buffer.d.ts +67 -0
  51. package/dist/services/logging/buffer.js +309 -0
  52. package/dist/services/logging/buffer.js.map +1 -0
  53. package/dist/services/logging/console.d.ts +89 -0
  54. package/dist/services/logging/console.js +169 -0
  55. package/dist/services/logging/console.js.map +1 -0
  56. package/dist/services/logging/file-writer.d.ts +71 -0
  57. package/dist/services/logging/file-writer.js +516 -0
  58. package/dist/services/logging/file-writer.js.map +1 -0
  59. package/dist/services/logging/formatter.d.ts +39 -0
  60. package/dist/services/logging/formatter.js +227 -0
  61. package/dist/services/logging/formatter.js.map +1 -0
  62. package/dist/services/logging/index.d.ts +11 -0
  63. package/dist/services/logging/index.js +30 -0
  64. package/dist/services/logging/index.js.map +1 -0
  65. package/dist/services/logging/parser.d.ts +31 -0
  66. package/dist/services/logging/parser.js +222 -0
  67. package/dist/services/logging/parser.js.map +1 -0
  68. package/dist/services/process/index.d.ts +59 -0
  69. package/dist/services/process/index.js +257 -0
  70. package/dist/services/process/index.js.map +1 -0
  71. package/dist/types/agent.d.ts +20 -0
  72. package/dist/types/agent.js +6 -0
  73. package/dist/types/agent.js.map +1 -0
  74. package/dist/types/config.d.ts +65 -0
  75. package/dist/types/config.js +6 -0
  76. package/dist/types/config.js.map +1 -0
  77. package/dist/types/events.d.ts +125 -0
  78. package/dist/types/events.js +6 -0
  79. package/dist/types/events.js.map +1 -0
  80. package/dist/types/index.d.ts +12 -0
  81. package/dist/types/index.js +37 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/lane.d.ts +43 -0
  84. package/dist/types/lane.js +6 -0
  85. package/dist/types/lane.js.map +1 -0
  86. package/dist/types/logging.d.ts +71 -0
  87. package/dist/types/logging.js +16 -0
  88. package/dist/types/logging.js.map +1 -0
  89. package/dist/types/review.d.ts +17 -0
  90. package/dist/types/review.js +6 -0
  91. package/dist/types/review.js.map +1 -0
  92. package/dist/types/run.d.ts +32 -0
  93. package/dist/types/run.js +6 -0
  94. package/dist/types/run.js.map +1 -0
  95. package/dist/types/task.d.ts +71 -0
  96. package/dist/types/task.js +6 -0
  97. package/dist/types/task.js.map +1 -0
  98. package/dist/ui/components.d.ts +134 -0
  99. package/dist/ui/components.js +389 -0
  100. package/dist/ui/components.js.map +1 -0
  101. package/dist/ui/log-viewer.d.ts +49 -0
  102. package/dist/ui/log-viewer.js +449 -0
  103. package/dist/ui/log-viewer.js.map +1 -0
  104. package/dist/utils/checkpoint.d.ts +87 -0
  105. package/dist/utils/checkpoint.js +317 -0
  106. package/dist/utils/checkpoint.js.map +1 -0
  107. package/dist/utils/config.d.ts +4 -0
  108. package/dist/utils/config.js +18 -8
  109. package/dist/utils/config.js.map +1 -1
  110. package/dist/utils/cursor-agent.js.map +1 -1
  111. package/dist/utils/dependency.d.ts +74 -0
  112. package/dist/utils/dependency.js +420 -0
  113. package/dist/utils/dependency.js.map +1 -0
  114. package/dist/utils/doctor.js +17 -11
  115. package/dist/utils/doctor.js.map +1 -1
  116. package/dist/utils/enhanced-logger.d.ts +10 -33
  117. package/dist/utils/enhanced-logger.js +108 -20
  118. package/dist/utils/enhanced-logger.js.map +1 -1
  119. package/dist/utils/git.d.ts +121 -0
  120. package/dist/utils/git.js +484 -11
  121. package/dist/utils/git.js.map +1 -1
  122. package/dist/utils/health.d.ts +91 -0
  123. package/dist/utils/health.js +556 -0
  124. package/dist/utils/health.js.map +1 -0
  125. package/dist/utils/lock.d.ts +95 -0
  126. package/dist/utils/lock.js +332 -0
  127. package/dist/utils/lock.js.map +1 -0
  128. package/dist/utils/log-buffer.d.ts +17 -0
  129. package/dist/utils/log-buffer.js +14 -0
  130. package/dist/utils/log-buffer.js.map +1 -0
  131. package/dist/utils/log-constants.d.ts +23 -0
  132. package/dist/utils/log-constants.js +28 -0
  133. package/dist/utils/log-constants.js.map +1 -0
  134. package/dist/utils/log-formatter.d.ts +25 -0
  135. package/dist/utils/log-formatter.js +237 -0
  136. package/dist/utils/log-formatter.js.map +1 -0
  137. package/dist/utils/log-service.d.ts +19 -0
  138. package/dist/utils/log-service.js +47 -0
  139. package/dist/utils/log-service.js.map +1 -0
  140. package/dist/utils/logger.d.ts +46 -27
  141. package/dist/utils/logger.js +82 -60
  142. package/dist/utils/logger.js.map +1 -1
  143. package/dist/utils/path.d.ts +19 -0
  144. package/dist/utils/path.js +77 -0
  145. package/dist/utils/path.js.map +1 -0
  146. package/dist/utils/process-manager.d.ts +21 -0
  147. package/dist/utils/process-manager.js +138 -0
  148. package/dist/utils/process-manager.js.map +1 -0
  149. package/dist/utils/retry.d.ts +121 -0
  150. package/dist/utils/retry.js +374 -0
  151. package/dist/utils/retry.js.map +1 -0
  152. package/dist/utils/run-service.d.ts +88 -0
  153. package/dist/utils/run-service.js +412 -0
  154. package/dist/utils/run-service.js.map +1 -0
  155. package/dist/utils/state.d.ts +62 -3
  156. package/dist/utils/state.js +317 -11
  157. package/dist/utils/state.js.map +1 -1
  158. package/dist/utils/task-service.d.ts +82 -0
  159. package/dist/utils/task-service.js +348 -0
  160. package/dist/utils/task-service.js.map +1 -0
  161. package/dist/utils/template.d.ts +14 -0
  162. package/dist/utils/template.js +122 -0
  163. package/dist/utils/template.js.map +1 -0
  164. package/dist/utils/types.d.ts +2 -271
  165. package/dist/utils/types.js +16 -0
  166. package/dist/utils/types.js.map +1 -1
  167. package/package.json +38 -23
  168. package/scripts/ai-security-check.js +0 -1
  169. package/scripts/local-security-gate.sh +0 -0
  170. package/scripts/monitor-lanes.sh +94 -0
  171. package/scripts/patches/test-cursor-agent.js +0 -1
  172. package/scripts/release.sh +0 -0
  173. package/scripts/setup-security.sh +0 -0
  174. package/scripts/stream-logs.sh +72 -0
  175. package/scripts/verify-and-fix.sh +0 -0
  176. package/src/cli/clean.ts +187 -6
  177. package/src/cli/index.ts +12 -1
  178. package/src/cli/init.ts +8 -7
  179. package/src/cli/logs.ts +124 -77
  180. package/src/cli/monitor.ts +1815 -898
  181. package/src/cli/prepare.ts +41 -21
  182. package/src/cli/resume.ts +753 -626
  183. package/src/cli/run.ts +12 -5
  184. package/src/cli/runs.ts +212 -0
  185. package/src/cli/setup-commands.ts +0 -0
  186. package/src/cli/signal.ts +8 -7
  187. package/src/cli/stop.ts +209 -0
  188. package/src/cli/tasks.ts +154 -0
  189. package/src/core/auto-recovery.ts +909 -0
  190. package/src/core/failure-policy.ts +592 -0
  191. package/src/core/orchestrator.ts +1131 -704
  192. package/src/core/reviewer.ts +4 -0
  193. package/src/core/runner.ts +444 -180
  194. package/src/services/logging/buffer.ts +326 -0
  195. package/src/services/logging/console.ts +193 -0
  196. package/src/services/logging/file-writer.ts +526 -0
  197. package/src/services/logging/formatter.ts +268 -0
  198. package/src/services/logging/index.ts +16 -0
  199. package/src/services/logging/parser.ts +232 -0
  200. package/src/services/process/index.ts +261 -0
  201. package/src/types/agent.ts +24 -0
  202. package/src/types/config.ts +79 -0
  203. package/src/types/events.ts +156 -0
  204. package/src/types/index.ts +29 -0
  205. package/src/types/lane.ts +56 -0
  206. package/src/types/logging.ts +96 -0
  207. package/src/types/review.ts +20 -0
  208. package/src/types/run.ts +37 -0
  209. package/src/types/task.ts +79 -0
  210. package/src/ui/components.ts +430 -0
  211. package/src/ui/log-viewer.ts +485 -0
  212. package/src/utils/checkpoint.ts +374 -0
  213. package/src/utils/config.ts +18 -8
  214. package/src/utils/cursor-agent.ts +1 -1
  215. package/src/utils/dependency.ts +482 -0
  216. package/src/utils/doctor.ts +18 -11
  217. package/src/utils/enhanced-logger.ts +122 -60
  218. package/src/utils/git.ts +517 -11
  219. package/src/utils/health.ts +596 -0
  220. package/src/utils/lock.ts +346 -0
  221. package/src/utils/log-buffer.ts +28 -0
  222. package/src/utils/log-constants.ts +26 -0
  223. package/src/utils/log-formatter.ts +245 -0
  224. package/src/utils/log-service.ts +49 -0
  225. package/src/utils/logger.ts +100 -51
  226. package/src/utils/path.ts +45 -0
  227. package/src/utils/process-manager.ts +100 -0
  228. package/src/utils/retry.ts +413 -0
  229. package/src/utils/run-service.ts +433 -0
  230. package/src/utils/state.ts +385 -11
  231. package/src/utils/task-service.ts +370 -0
  232. package/src/utils/template.ts +92 -0
  233. package/src/utils/types.ts +2 -314
  234. package/templates/basic.json +21 -0
package/src/utils/git.ts CHANGED
@@ -3,6 +3,77 @@
3
3
  */
4
4
 
5
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
+ }
6
77
 
7
78
  export interface GitRunOptions {
8
79
  cwd?: string;
@@ -36,6 +107,25 @@ export interface CommitInfo {
36
107
  subject: string;
37
108
  }
38
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
+
39
129
  /**
40
130
  * Run git command and return output
41
131
  */
@@ -43,12 +133,21 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
43
133
  const { cwd, silent = false } = options;
44
134
 
45
135
  try {
136
+ const stdioMode = silent ? 'pipe' : ['inherit', 'inherit', 'pipe'];
137
+
46
138
  const result = spawnSync('git', args, {
47
139
  cwd: cwd || process.cwd(),
48
140
  encoding: 'utf8',
49
- stdio: silent ? 'pipe' : 'inherit',
141
+ stdio: stdioMode as any,
50
142
  });
51
143
 
144
+ if (!silent && result.stderr) {
145
+ const filteredStderr = filterGitStderr(result.stderr);
146
+ if (filteredStderr) {
147
+ process.stderr.write(filteredStderr);
148
+ }
149
+ }
150
+
52
151
  if (result.status !== 0 && !silent) {
53
152
  throw new Error(`Git command failed: git ${args.join(' ')}\n${result.stderr || ''}`);
54
153
  }
@@ -96,6 +195,24 @@ export function getRepoRoot(cwd?: string): string {
96
195
  return runGit(['rev-parse', '--show-toplevel'], { cwd, silent: true });
97
196
  }
98
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
+
99
216
  /**
100
217
  * Check if directory is a git repository
101
218
  */
@@ -118,20 +235,64 @@ export function worktreeExists(worktreePath: string, cwd?: string): boolean {
118
235
  * Create worktree
119
236
  */
120
237
  export function createWorktree(worktreePath: string, branchName: string, options: { cwd?: string; baseBranch?: string } = {}): string {
121
- const { cwd, baseBranch = 'main' } = options;
238
+ let { cwd, baseBranch } = options;
122
239
 
123
- // Check if branch already exists
124
- const branchExists = runGitResult(['rev-parse', '--verify', branchName], { cwd }).success;
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
+ }
125
271
 
126
- if (branchExists) {
127
- // Branch exists, checkout to worktree
128
- runGit(['worktree', 'add', worktreePath, branchName], { cwd });
129
- } else {
130
- // Create new branch from base
131
- runGit(['worktree', 'add', '-b', branchName, worktreePath, baseBranch], { cwd });
272
+ if (!acquired) {
273
+ throw new Error('Failed to acquire worktree lock after multiple retries');
132
274
  }
133
275
 
134
- return worktreePath;
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
+ }
135
296
  }
136
297
 
137
298
  /**
@@ -362,4 +523,349 @@ export function getLastOperationStats(cwd?: string): string {
362
523
  } catch (e) {
363
524
  return '';
364
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 };
365
871
  }