@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.
- package/CHANGELOG.md +25 -0
- package/README.md +25 -7
- 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 +178 -6
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +8 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +126 -77
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1021 -202
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +39 -21
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +268 -163
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +11 -5
- 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 +8 -8
- 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 +16 -2
- package/dist/core/orchestrator.js +439 -105
- 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 +374 -164
- 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 +18 -8
- 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 +17 -11
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +108 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +484 -11
- 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 +25 -0
- package/dist/utils/log-formatter.js +237 -0
- package/dist/utils/log-formatter.js.map +1 -0
- 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/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- 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 +62 -3
- package/dist/utils/state.js +317 -11
- 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/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +2 -271
- 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 +187 -6
- package/src/cli/index.ts +12 -1
- package/src/cli/init.ts +8 -7
- package/src/cli/logs.ts +124 -77
- package/src/cli/monitor.ts +1815 -898
- package/src/cli/prepare.ts +41 -21
- package/src/cli/resume.ts +753 -626
- package/src/cli/run.ts +12 -5
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +8 -7
- 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 +1131 -704
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +444 -180
- 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 +18 -8
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +18 -11
- package/src/utils/enhanced-logger.ts +122 -60
- package/src/utils/git.ts +517 -11
- 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 +245 -0
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- package/src/utils/path.ts +45 -0
- 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 +385 -11
- package/src/utils/task-service.ts +370 -0
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +2 -314
- 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:
|
|
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
|
-
|
|
238
|
+
let { cwd, baseBranch } = options;
|
|
122
239
|
|
|
123
|
-
|
|
124
|
-
|
|
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 (
|
|
127
|
-
|
|
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
|
-
|
|
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
|
}
|