@litmers/cursorflow-orchestrator 0.1.5 → 0.1.6
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 +15 -6
- package/README.md +33 -2
- package/commands/cursorflow-doctor.md +24 -0
- package/commands/cursorflow-signal.md +19 -0
- package/dist/cli/doctor.d.ts +15 -0
- package/dist/cli/doctor.js +139 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/resume.d.ts +1 -1
- package/dist/cli/resume.js +80 -10
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +59 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/setup-commands.d.ts +4 -0
- package/dist/cli/setup-commands.js +16 -0
- package/dist/cli/setup-commands.js.map +1 -1
- package/dist/cli/signal.d.ts +7 -0
- package/dist/cli/signal.js +99 -0
- package/dist/cli/signal.js.map +1 -0
- package/dist/core/runner.d.ts +3 -1
- package/dist/core/runner.js +54 -30
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/doctor.d.ts +63 -0
- package/dist/utils/doctor.js +280 -0
- package/dist/utils/doctor.js.map +1 -0
- package/dist/utils/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli/doctor.ts +127 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/resume.ts +94 -12
- package/src/cli/run.ts +62 -7
- package/src/cli/setup-commands.ts +19 -0
- package/src/cli/signal.ts +89 -0
- package/src/core/runner.ts +57 -30
- package/src/utils/doctor.ts +312 -0
- package/src/utils/types.ts +1 -0
package/src/core/runner.ts
CHANGED
|
@@ -371,7 +371,9 @@ export async function runTask({
|
|
|
371
371
|
/**
|
|
372
372
|
* Run all tasks in sequence
|
|
373
373
|
*/
|
|
374
|
-
export async function runTasks(config: RunnerConfig, runDir: string): Promise<TaskExecutionResult[]> {
|
|
374
|
+
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
|
|
375
|
+
const startIndex = options.startIndex || 0;
|
|
376
|
+
|
|
375
377
|
// Ensure cursor-agent is installed
|
|
376
378
|
ensureCursorAgent();
|
|
377
379
|
|
|
@@ -400,44 +402,67 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
|
|
|
400
402
|
logger.success('✓ Cursor authentication OK');
|
|
401
403
|
|
|
402
404
|
const repoRoot = git.getRepoRoot();
|
|
403
|
-
const pipelineBranch = config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
|
|
404
|
-
const worktreeDir = path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
|
|
405
405
|
|
|
406
|
-
|
|
406
|
+
// Load existing state if resuming
|
|
407
|
+
const statePath = path.join(runDir, 'state.json');
|
|
408
|
+
let state: LaneState | null = null;
|
|
409
|
+
|
|
410
|
+
if (startIndex > 0 && fs.existsSync(statePath)) {
|
|
411
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
|
|
415
|
+
const worktreeDir = state?.worktreeDir || path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
|
|
416
|
+
|
|
417
|
+
if (startIndex === 0) {
|
|
418
|
+
logger.section('🚀 Starting Pipeline');
|
|
419
|
+
} else {
|
|
420
|
+
logger.section(`🔁 Resuming Pipeline from task ${startIndex + 1}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
407
423
|
logger.info(`Pipeline Branch: ${pipelineBranch}`);
|
|
408
424
|
logger.info(`Worktree: ${worktreeDir}`);
|
|
409
425
|
logger.info(`Tasks: ${config.tasks.length}`);
|
|
410
426
|
|
|
411
|
-
// Create worktree
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
427
|
+
// Create worktree only if starting fresh
|
|
428
|
+
if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
|
|
429
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
430
|
+
baseBranch: config.baseBranch || 'main',
|
|
431
|
+
cwd: repoRoot,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
416
434
|
|
|
417
435
|
// Create chat
|
|
418
436
|
logger.info('Creating chat session...');
|
|
419
437
|
const chatId = cursorAgentCreateChat();
|
|
420
438
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
439
|
+
// Initialize state if not loaded
|
|
440
|
+
if (!state) {
|
|
441
|
+
state = {
|
|
442
|
+
status: 'running',
|
|
443
|
+
pipelineBranch,
|
|
444
|
+
worktreeDir,
|
|
445
|
+
totalTasks: config.tasks.length,
|
|
446
|
+
currentTaskIndex: 0,
|
|
447
|
+
label: pipelineBranch,
|
|
448
|
+
startTime: Date.now(),
|
|
449
|
+
endTime: null,
|
|
450
|
+
error: null,
|
|
451
|
+
dependencyRequest: null,
|
|
452
|
+
tasksFile, // Store tasks file for resume
|
|
453
|
+
};
|
|
454
|
+
} else {
|
|
455
|
+
state.status = 'running';
|
|
456
|
+
state.error = null;
|
|
457
|
+
state.dependencyRequest = null;
|
|
458
|
+
}
|
|
434
459
|
|
|
435
|
-
saveState(
|
|
460
|
+
saveState(statePath, state);
|
|
436
461
|
|
|
437
462
|
// Run tasks
|
|
438
463
|
const results: TaskExecutionResult[] = [];
|
|
439
464
|
|
|
440
|
-
for (let i =
|
|
465
|
+
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
441
466
|
const task = config.tasks[i]!;
|
|
442
467
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
443
468
|
|
|
@@ -456,13 +481,13 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
|
|
|
456
481
|
|
|
457
482
|
// Update state
|
|
458
483
|
state.currentTaskIndex = i + 1;
|
|
459
|
-
saveState(
|
|
484
|
+
saveState(statePath, state);
|
|
460
485
|
|
|
461
486
|
// Handle blocked or error
|
|
462
487
|
if (result.status === 'BLOCKED_DEPENDENCY') {
|
|
463
|
-
state.status = 'failed';
|
|
488
|
+
state.status = 'failed';
|
|
464
489
|
state.dependencyRequest = result.dependencyRequest || null;
|
|
465
|
-
saveState(
|
|
490
|
+
saveState(statePath, state);
|
|
466
491
|
logger.warn('Task blocked on dependency change');
|
|
467
492
|
process.exit(2);
|
|
468
493
|
}
|
|
@@ -470,7 +495,7 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
|
|
|
470
495
|
if (result.status !== 'FINISHED') {
|
|
471
496
|
state.status = 'failed';
|
|
472
497
|
state.error = result.error || 'Unknown error';
|
|
473
|
-
saveState(
|
|
498
|
+
saveState(statePath, state);
|
|
474
499
|
logger.error(`Task failed: ${result.error}`);
|
|
475
500
|
process.exit(1);
|
|
476
501
|
}
|
|
@@ -484,7 +509,7 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
|
|
|
484
509
|
// Complete
|
|
485
510
|
state.status = 'completed';
|
|
486
511
|
state.endTime = Date.now();
|
|
487
|
-
saveState(
|
|
512
|
+
saveState(statePath, state);
|
|
488
513
|
|
|
489
514
|
logger.success('All tasks completed!');
|
|
490
515
|
return results;
|
|
@@ -503,9 +528,11 @@ if (require.main === module) {
|
|
|
503
528
|
|
|
504
529
|
const tasksFile = args[0]!;
|
|
505
530
|
const runDirIdx = args.indexOf('--run-dir');
|
|
531
|
+
const startIdxIdx = args.indexOf('--start-index');
|
|
506
532
|
// const executorIdx = args.indexOf('--executor');
|
|
507
533
|
|
|
508
534
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
535
|
+
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
509
536
|
// const executor = executorIdx >= 0 ? args[executorIdx + 1] : 'cursor-agent';
|
|
510
537
|
|
|
511
538
|
if (!fs.existsSync(tasksFile)) {
|
|
@@ -529,7 +556,7 @@ if (require.main === module) {
|
|
|
529
556
|
};
|
|
530
557
|
|
|
531
558
|
// Run tasks
|
|
532
|
-
runTasks(config, runDir)
|
|
559
|
+
runTasks(tasksFile, config, runDir, { startIndex })
|
|
533
560
|
.then(() => {
|
|
534
561
|
process.exit(0);
|
|
535
562
|
})
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CursorFlow Doctor - environment and preflight checks
|
|
3
|
+
*
|
|
4
|
+
* This module provides actionable diagnostics for common run failures:
|
|
5
|
+
* - Not inside a Git work tree
|
|
6
|
+
* - Missing `origin` remote (required for pushing lane branches)
|
|
7
|
+
* - Missing Git worktree support
|
|
8
|
+
* - Missing base branch referenced by lane task files
|
|
9
|
+
* - Missing/invalid tasks directory
|
|
10
|
+
* - Missing Cursor Agent setup (optional)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
import * as git from './git';
|
|
17
|
+
import { checkCursorAgentInstalled, checkCursorAuth } from './cursor-agent';
|
|
18
|
+
import { areCommandsInstalled } from '../cli/setup-commands';
|
|
19
|
+
|
|
20
|
+
export type DoctorSeverity = 'error' | 'warn';
|
|
21
|
+
|
|
22
|
+
export interface DoctorIssue {
|
|
23
|
+
/**
|
|
24
|
+
* Stable identifier for machines (NOCC) and tests.
|
|
25
|
+
*/
|
|
26
|
+
id: string;
|
|
27
|
+
severity: DoctorSeverity;
|
|
28
|
+
title: string;
|
|
29
|
+
message: string;
|
|
30
|
+
/**
|
|
31
|
+
* Suggested commands or steps to fix the issue.
|
|
32
|
+
*/
|
|
33
|
+
fixes?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Optional technical details (stderr, stdout, etc.)
|
|
36
|
+
*/
|
|
37
|
+
details?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DoctorContext {
|
|
41
|
+
cwd: string;
|
|
42
|
+
repoRoot?: string;
|
|
43
|
+
tasksDir?: string;
|
|
44
|
+
executor?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DoctorReport {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
issues: DoctorIssue[];
|
|
50
|
+
context: DoctorContext;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DoctorOptions {
|
|
54
|
+
cwd?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Optional tasks directory (used for `cursorflow run` preflight).
|
|
57
|
+
*/
|
|
58
|
+
tasksDir?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Executor type ('cursor-agent' | 'cloud' | ...).
|
|
61
|
+
*/
|
|
62
|
+
executor?: string;
|
|
63
|
+
/**
|
|
64
|
+
* When true (default), include Cursor Agent install/auth checks.
|
|
65
|
+
*/
|
|
66
|
+
includeCursorAgentChecks?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function addIssue(issues: DoctorIssue[], issue: DoctorIssue): void {
|
|
70
|
+
issues.push(issue);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveRepoRoot(cwd: string): string | null {
|
|
74
|
+
const res = git.runGitResult(['rev-parse', '--show-toplevel'], { cwd });
|
|
75
|
+
if (!res.success || !res.stdout) return null;
|
|
76
|
+
return res.stdout;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isInsideGitWorktree(cwd: string): boolean {
|
|
80
|
+
const res = git.runGitResult(['rev-parse', '--is-inside-work-tree'], { cwd });
|
|
81
|
+
return res.success && res.stdout.trim() === 'true';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasAtLeastOneCommit(repoRoot: string): boolean {
|
|
85
|
+
const res = git.runGitResult(['rev-parse', '--verify', 'HEAD'], { cwd: repoRoot });
|
|
86
|
+
return res.success;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasOriginRemote(repoRoot: string): boolean {
|
|
90
|
+
const res = git.runGitResult(['remote', 'get-url', 'origin'], { cwd: repoRoot });
|
|
91
|
+
return res.success && !!res.stdout;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasWorktreeSupport(repoRoot: string): { ok: boolean; details?: string } {
|
|
95
|
+
const res = git.runGitResult(['worktree', 'list'], { cwd: repoRoot });
|
|
96
|
+
if (res.success) return { ok: true };
|
|
97
|
+
return { ok: false, details: res.stderr || res.stdout || 'git worktree failed' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function branchExists(repoRoot: string, branchName: string): boolean {
|
|
101
|
+
// rev-parse --verify works for both branches and tags; restrict to heads first.
|
|
102
|
+
const headRes = git.runGitResult(['show-ref', '--verify', `refs/heads/${branchName}`], { cwd: repoRoot });
|
|
103
|
+
if (headRes.success) return true;
|
|
104
|
+
const anyRes = git.runGitResult(['rev-parse', '--verify', branchName], { cwd: repoRoot });
|
|
105
|
+
return anyRes.success;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readLaneJsonFiles(tasksDir: string): { path: string; json: any }[] {
|
|
109
|
+
const files = fs
|
|
110
|
+
.readdirSync(tasksDir)
|
|
111
|
+
.filter(f => f.endsWith('.json'))
|
|
112
|
+
.sort()
|
|
113
|
+
.map(f => path.join(tasksDir, f));
|
|
114
|
+
|
|
115
|
+
return files.map(p => {
|
|
116
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
117
|
+
const json = JSON.parse(raw);
|
|
118
|
+
return { path: p, json };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function collectBaseBranchesFromLanes(lanes: { path: string; json: any }[], defaultBaseBranch: string): string[] {
|
|
123
|
+
const set = new Set<string>();
|
|
124
|
+
for (const lane of lanes) {
|
|
125
|
+
const baseBranch = String(lane.json?.baseBranch || defaultBaseBranch || 'main').trim();
|
|
126
|
+
if (baseBranch) set.add(baseBranch);
|
|
127
|
+
}
|
|
128
|
+
return Array.from(set);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Run doctor checks.
|
|
133
|
+
*
|
|
134
|
+
* If `tasksDir` is provided, additional preflight checks are performed:
|
|
135
|
+
* - tasks directory existence and JSON validity
|
|
136
|
+
* - baseBranch referenced by lanes exists locally
|
|
137
|
+
*/
|
|
138
|
+
export function runDoctor(options: DoctorOptions = {}): DoctorReport {
|
|
139
|
+
const cwd = options.cwd || process.cwd();
|
|
140
|
+
const issues: DoctorIssue[] = [];
|
|
141
|
+
|
|
142
|
+
const context: DoctorContext = {
|
|
143
|
+
cwd,
|
|
144
|
+
executor: options.executor,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// 1) Git repository checks
|
|
148
|
+
if (!isInsideGitWorktree(cwd)) {
|
|
149
|
+
addIssue(issues, {
|
|
150
|
+
id: 'git.not_repo',
|
|
151
|
+
severity: 'error',
|
|
152
|
+
title: 'Not a Git repository',
|
|
153
|
+
message: 'Current directory is not inside a Git work tree. CursorFlow requires Git to create worktrees and branches.',
|
|
154
|
+
fixes: [
|
|
155
|
+
'cd into your repository root (or any subdirectory inside it)',
|
|
156
|
+
'If this is a new project: git init && git commit --allow-empty -m "chore: initial commit"',
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return { ok: false, issues, context };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const repoRoot = resolveRepoRoot(cwd) || undefined;
|
|
164
|
+
context.repoRoot = repoRoot;
|
|
165
|
+
const gitCwd = repoRoot || cwd;
|
|
166
|
+
|
|
167
|
+
if (!hasAtLeastOneCommit(gitCwd)) {
|
|
168
|
+
addIssue(issues, {
|
|
169
|
+
id: 'git.no_commits',
|
|
170
|
+
severity: 'error',
|
|
171
|
+
title: 'Repository has no commits',
|
|
172
|
+
message: 'CursorFlow needs at least one commit (HEAD) to create worktrees and new branches.',
|
|
173
|
+
fixes: ['git commit --allow-empty -m "chore: initial commit"'],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!hasOriginRemote(gitCwd)) {
|
|
178
|
+
addIssue(issues, {
|
|
179
|
+
id: 'git.no_origin',
|
|
180
|
+
severity: 'error',
|
|
181
|
+
title: "Missing remote 'origin'",
|
|
182
|
+
message: "Remote 'origin' is not configured. CursorFlow pushes lane branches to 'origin' during execution.",
|
|
183
|
+
fixes: [
|
|
184
|
+
'git remote add origin <your-repo-url>',
|
|
185
|
+
'git remote -v # verify remotes',
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const wt = hasWorktreeSupport(gitCwd);
|
|
191
|
+
if (!wt.ok) {
|
|
192
|
+
addIssue(issues, {
|
|
193
|
+
id: 'git.worktree_unavailable',
|
|
194
|
+
severity: 'error',
|
|
195
|
+
title: 'Git worktree not available',
|
|
196
|
+
message: 'Git worktree support is required, but `git worktree` failed.',
|
|
197
|
+
fixes: [
|
|
198
|
+
'Upgrade Git (worktrees require Git >= 2.5)',
|
|
199
|
+
'Ensure the repository is not corrupted',
|
|
200
|
+
],
|
|
201
|
+
details: wt.details,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 2) Tasks-dir checks (optional; used by `cursorflow run` preflight)
|
|
206
|
+
if (options.tasksDir) {
|
|
207
|
+
const tasksDirAbs = path.isAbsolute(options.tasksDir)
|
|
208
|
+
? options.tasksDir
|
|
209
|
+
: path.resolve(cwd, options.tasksDir);
|
|
210
|
+
context.tasksDir = tasksDirAbs;
|
|
211
|
+
|
|
212
|
+
if (!fs.existsSync(tasksDirAbs)) {
|
|
213
|
+
addIssue(issues, {
|
|
214
|
+
id: 'tasks.missing_dir',
|
|
215
|
+
severity: 'error',
|
|
216
|
+
title: 'Tasks directory not found',
|
|
217
|
+
message: `Tasks directory does not exist: ${tasksDirAbs}`,
|
|
218
|
+
fixes: [
|
|
219
|
+
'Double-check the path you passed to `cursorflow run`',
|
|
220
|
+
'If needed, run: cursorflow init --example',
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
let lanes: { path: string; json: any }[] = [];
|
|
225
|
+
try {
|
|
226
|
+
lanes = readLaneJsonFiles(tasksDirAbs);
|
|
227
|
+
} catch (error: any) {
|
|
228
|
+
addIssue(issues, {
|
|
229
|
+
id: 'tasks.invalid_json',
|
|
230
|
+
severity: 'error',
|
|
231
|
+
title: 'Invalid lane JSON file',
|
|
232
|
+
message: `Failed to read/parse lane JSON files in: ${tasksDirAbs}`,
|
|
233
|
+
details: error?.message ? String(error.message) : String(error),
|
|
234
|
+
fixes: ['Validate JSON syntax for each lane file (*.json)'],
|
|
235
|
+
});
|
|
236
|
+
lanes = [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (lanes.length === 0) {
|
|
240
|
+
addIssue(issues, {
|
|
241
|
+
id: 'tasks.no_lanes',
|
|
242
|
+
severity: 'error',
|
|
243
|
+
title: 'No lane files found',
|
|
244
|
+
message: `No lane task files (*.json) found in: ${tasksDirAbs}`,
|
|
245
|
+
fixes: ['Ensure the tasks directory contains one or more lane JSON files'],
|
|
246
|
+
});
|
|
247
|
+
} else {
|
|
248
|
+
const baseBranches = collectBaseBranchesFromLanes(lanes, 'main');
|
|
249
|
+
for (const baseBranch of baseBranches) {
|
|
250
|
+
if (!branchExists(gitCwd, baseBranch)) {
|
|
251
|
+
addIssue(issues, {
|
|
252
|
+
id: `git.missing_base_branch.${baseBranch}`,
|
|
253
|
+
severity: 'error',
|
|
254
|
+
title: `Missing base branch: ${baseBranch}`,
|
|
255
|
+
message: `Lane files reference baseBranch "${baseBranch}", but it does not exist locally.`,
|
|
256
|
+
fixes: [
|
|
257
|
+
'git fetch origin --prune',
|
|
258
|
+
`Or update lane JSON baseBranch to an existing branch`,
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 3) Cursor Agent checks (optional)
|
|
268
|
+
const includeCursor = options.includeCursorAgentChecks !== false;
|
|
269
|
+
if (includeCursor && (options.executor || 'cursor-agent') === 'cursor-agent') {
|
|
270
|
+
if (!checkCursorAgentInstalled()) {
|
|
271
|
+
addIssue(issues, {
|
|
272
|
+
id: 'cursor_agent.missing',
|
|
273
|
+
severity: 'error',
|
|
274
|
+
title: 'cursor-agent CLI not installed',
|
|
275
|
+
message: 'cursor-agent is required for local execution.',
|
|
276
|
+
fixes: ['npm install -g @cursor/agent', 'cursor-agent --version'],
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
const auth = checkCursorAuth();
|
|
280
|
+
if (!auth.authenticated) {
|
|
281
|
+
addIssue(issues, {
|
|
282
|
+
id: 'cursor_agent.not_authenticated',
|
|
283
|
+
severity: 'error',
|
|
284
|
+
title: 'Cursor authentication required',
|
|
285
|
+
message: auth.message,
|
|
286
|
+
details: auth.details || auth.error,
|
|
287
|
+
fixes: [
|
|
288
|
+
'Open Cursor IDE and sign in',
|
|
289
|
+
'Verify AI features work in the IDE',
|
|
290
|
+
'Re-run: cursorflow doctor',
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 4) IDE Integration checks
|
|
298
|
+
if (!areCommandsInstalled()) {
|
|
299
|
+
addIssue(issues, {
|
|
300
|
+
id: 'ide.commands_missing',
|
|
301
|
+
severity: 'warn',
|
|
302
|
+
title: 'Cursor IDE commands not installed',
|
|
303
|
+
message: 'CursorFlow slash commands (e.g., /cursorflow-run) are missing or outdated.',
|
|
304
|
+
fixes: ['cursorflow-setup --force', 'or run `cursorflow run` to auto-install'],
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const ok = issues.every(i => i.severity !== 'error');
|
|
309
|
+
return { ok, issues, context };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
package/src/utils/types.ts
CHANGED