@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.
- package/CHANGELOG.md +20 -0
- package/commands/cursorflow-clean.md +19 -0
- package/commands/cursorflow-runs.md +59 -0
- package/commands/cursorflow-stop.md +55 -0
- package/dist/cli/clean.js +171 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +83 -42
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1007 -189
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +87 -3
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +188 -236
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +125 -3
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.d.ts +5 -0
- package/dist/cli/runs.js +214 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/setup-commands.js +0 -0
- package/dist/cli/signal.js +1 -1
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/stop.d.ts +5 -0
- package/dist/cli/stop.js +215 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/cli/tasks.d.ts +10 -0
- package/dist/cli/tasks.js +165 -0
- package/dist/cli/tasks.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +212 -0
- package/dist/core/auto-recovery.js +737 -0
- package/dist/core/auto-recovery.js.map +1 -0
- package/dist/core/failure-policy.d.ts +156 -0
- package/dist/core/failure-policy.js +488 -0
- package/dist/core/failure-policy.js.map +1 -0
- package/dist/core/orchestrator.d.ts +15 -2
- package/dist/core/orchestrator.js +397 -15
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +2 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +33 -10
- package/dist/core/runner.js +321 -146
- package/dist/core/runner.js.map +1 -1
- package/dist/services/logging/buffer.d.ts +67 -0
- package/dist/services/logging/buffer.js +309 -0
- package/dist/services/logging/buffer.js.map +1 -0
- package/dist/services/logging/console.d.ts +89 -0
- package/dist/services/logging/console.js +169 -0
- package/dist/services/logging/console.js.map +1 -0
- package/dist/services/logging/file-writer.d.ts +71 -0
- package/dist/services/logging/file-writer.js +516 -0
- package/dist/services/logging/file-writer.js.map +1 -0
- package/dist/services/logging/formatter.d.ts +39 -0
- package/dist/services/logging/formatter.js +227 -0
- package/dist/services/logging/formatter.js.map +1 -0
- package/dist/services/logging/index.d.ts +11 -0
- package/dist/services/logging/index.js +30 -0
- package/dist/services/logging/index.js.map +1 -0
- package/dist/services/logging/parser.d.ts +31 -0
- package/dist/services/logging/parser.js +222 -0
- package/dist/services/logging/parser.js.map +1 -0
- package/dist/services/process/index.d.ts +59 -0
- package/dist/services/process/index.js +257 -0
- package/dist/services/process/index.js.map +1 -0
- package/dist/types/agent.d.ts +20 -0
- package/dist/types/agent.js +6 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/config.d.ts +65 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/events.d.ts +125 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lane.d.ts +43 -0
- package/dist/types/lane.js +6 -0
- package/dist/types/lane.js.map +1 -0
- package/dist/types/logging.d.ts +71 -0
- package/dist/types/logging.js +16 -0
- package/dist/types/logging.js.map +1 -0
- package/dist/types/review.d.ts +17 -0
- package/dist/types/review.js +6 -0
- package/dist/types/review.js.map +1 -0
- package/dist/types/run.d.ts +32 -0
- package/dist/types/run.js +6 -0
- package/dist/types/run.js.map +1 -0
- package/dist/types/task.d.ts +71 -0
- package/dist/types/task.js +6 -0
- package/dist/types/task.js.map +1 -0
- package/dist/ui/components.d.ts +134 -0
- package/dist/ui/components.js +389 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/log-viewer.d.ts +49 -0
- package/dist/ui/log-viewer.js +449 -0
- package/dist/ui/log-viewer.js.map +1 -0
- package/dist/utils/checkpoint.d.ts +87 -0
- package/dist/utils/checkpoint.js +317 -0
- package/dist/utils/checkpoint.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +11 -2
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/dependency.d.ts +74 -0
- package/dist/utils/dependency.js +420 -0
- package/dist/utils/dependency.js.map +1 -0
- package/dist/utils/doctor.js +10 -5
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +94 -9
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +322 -2
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.d.ts +91 -0
- package/dist/utils/health.js +556 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/lock.d.ts +95 -0
- package/dist/utils/lock.js +332 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/log-buffer.d.ts +17 -0
- package/dist/utils/log-buffer.js +14 -0
- package/dist/utils/log-buffer.js.map +1 -0
- package/dist/utils/log-constants.d.ts +23 -0
- package/dist/utils/log-constants.js +28 -0
- package/dist/utils/log-constants.js.map +1 -0
- package/dist/utils/log-formatter.d.ts +9 -0
- package/dist/utils/log-formatter.js +113 -70
- package/dist/utils/log-formatter.js.map +1 -1
- package/dist/utils/log-service.d.ts +19 -0
- package/dist/utils/log-service.js +47 -0
- package/dist/utils/log-service.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -27
- package/dist/utils/logger.js +82 -60
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/process-manager.d.ts +21 -0
- package/dist/utils/process-manager.js +138 -0
- package/dist/utils/process-manager.js.map +1 -0
- package/dist/utils/retry.d.ts +121 -0
- package/dist/utils/retry.js +374 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/run-service.d.ts +88 -0
- package/dist/utils/run-service.js +412 -0
- package/dist/utils/run-service.js.map +1 -0
- package/dist/utils/state.d.ts +58 -2
- package/dist/utils/state.js +306 -3
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +82 -0
- package/dist/utils/task-service.js +348 -0
- package/dist/utils/task-service.js.map +1 -0
- package/dist/utils/types.d.ts +2 -272
- package/dist/utils/types.js +16 -0
- package/dist/utils/types.js.map +1 -1
- package/package.json +38 -23
- package/scripts/ai-security-check.js +0 -1
- package/scripts/local-security-gate.sh +0 -0
- package/scripts/monitor-lanes.sh +94 -0
- package/scripts/patches/test-cursor-agent.js +0 -1
- package/scripts/release.sh +0 -0
- package/scripts/setup-security.sh +0 -0
- package/scripts/stream-logs.sh +72 -0
- package/scripts/verify-and-fix.sh +0 -0
- package/src/cli/clean.ts +180 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/init.ts +1 -1
- package/src/cli/logs.ts +79 -42
- package/src/cli/monitor.ts +1815 -899
- package/src/cli/prepare.ts +97 -3
- package/src/cli/resume.ts +220 -277
- package/src/cli/run.ts +154 -3
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +1 -1
- package/src/cli/stop.ts +209 -0
- package/src/cli/tasks.ts +154 -0
- package/src/core/auto-recovery.ts +909 -0
- package/src/core/failure-policy.ts +592 -0
- package/src/core/orchestrator.ts +1136 -675
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +1443 -1217
- package/src/services/logging/buffer.ts +326 -0
- package/src/services/logging/console.ts +193 -0
- package/src/services/logging/file-writer.ts +526 -0
- package/src/services/logging/formatter.ts +268 -0
- package/src/services/logging/index.ts +16 -0
- package/src/services/logging/parser.ts +232 -0
- package/src/services/process/index.ts +261 -0
- package/src/types/agent.ts +24 -0
- package/src/types/config.ts +79 -0
- package/src/types/events.ts +156 -0
- package/src/types/index.ts +29 -0
- package/src/types/lane.ts +56 -0
- package/src/types/logging.ts +96 -0
- package/src/types/review.ts +20 -0
- package/src/types/run.ts +37 -0
- package/src/types/task.ts +79 -0
- package/src/ui/components.ts +430 -0
- package/src/ui/log-viewer.ts +485 -0
- package/src/utils/checkpoint.ts +374 -0
- package/src/utils/config.ts +11 -2
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +11 -5
- package/src/utils/enhanced-logger.ts +108 -49
- package/src/utils/git.ts +871 -499
- package/src/utils/health.ts +596 -0
- package/src/utils/lock.ts +346 -0
- package/src/utils/log-buffer.ts +28 -0
- package/src/utils/log-constants.ts +26 -0
- package/src/utils/log-formatter.ts +120 -37
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- package/src/utils/process-manager.ts +100 -0
- package/src/utils/retry.ts +413 -0
- package/src/utils/run-service.ts +433 -0
- package/src/utils/state.ts +369 -3
- package/src/utils/task-service.ts +370 -0
- 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
|
-
*
|
|
200
|
-
*/
|
|
201
|
-
export function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
*
|
|
218
|
-
*/
|
|
219
|
-
export function
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
*
|
|
392
|
-
*/
|
|
393
|
-
export function
|
|
394
|
-
const { cwd,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|