@litmers/cursorflow-orchestrator 0.1.3 → 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 +17 -7
- package/README.md +33 -2
- package/commands/cursorflow-doctor.md +24 -0
- package/commands/cursorflow-signal.md +19 -0
- package/dist/cli/clean.d.ts +5 -0
- package/dist/cli/clean.js +57 -0
- package/dist/cli/clean.js.map +1 -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.d.ts +6 -0
- package/dist/cli/index.js +125 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +302 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/monitor.d.ts +8 -0
- package/dist/cli/monitor.js +210 -0
- package/dist/cli/monitor.js.map +1 -0
- package/dist/cli/resume.d.ts +5 -0
- package/dist/cli/resume.js +128 -0
- package/dist/cli/resume.js.map +1 -0
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.js +128 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/setup-commands.d.ts +23 -0
- package/dist/cli/setup-commands.js +234 -0
- package/dist/cli/setup-commands.js.map +1 -0
- 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/orchestrator.d.ts +47 -0
- package/dist/core/orchestrator.js +192 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/reviewer.d.ts +60 -0
- package/dist/core/reviewer.js +239 -0
- package/dist/core/reviewer.js.map +1 -0
- package/dist/core/runner.d.ts +51 -0
- package/dist/core/runner.js +499 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/utils/config.d.ts +31 -0
- package/dist/utils/config.js +198 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/cursor-agent.d.ts +61 -0
- package/dist/utils/cursor-agent.js +263 -0
- package/dist/utils/cursor-agent.js.map +1 -0
- 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/git.d.ts +131 -0
- package/dist/utils/git.js +272 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +68 -0
- package/dist/utils/logger.js +158 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/state.d.ts +65 -0
- package/dist/utils/state.js +216 -0
- package/dist/utils/state.js.map +1 -0
- package/dist/utils/types.d.ts +118 -0
- package/dist/utils/types.js +6 -0
- package/dist/utils/types.js.map +1 -0
- package/examples/README.md +155 -0
- package/examples/demo-project/README.md +262 -0
- package/examples/demo-project/_cursorflow/tasks/demo-test/01-create-utils.json +18 -0
- package/examples/demo-project/_cursorflow/tasks/demo-test/02-add-tests.json +18 -0
- package/examples/demo-project/_cursorflow/tasks/demo-test/README.md +109 -0
- package/package.json +71 -61
- package/scripts/ai-security-check.js +11 -4
- package/scripts/local-security-gate.sh +76 -0
- package/src/cli/{clean.js → clean.ts} +11 -5
- package/src/cli/doctor.ts +127 -0
- package/src/cli/{index.js → index.ts} +27 -16
- package/src/cli/{init.js → init.ts} +26 -18
- package/src/cli/{monitor.js → monitor.ts} +57 -44
- package/src/cli/resume.ts +119 -0
- package/src/cli/run.ts +109 -0
- package/src/cli/{setup-commands.js → setup-commands.ts} +38 -18
- package/src/cli/signal.ts +89 -0
- package/src/core/{orchestrator.js → orchestrator.ts} +44 -26
- package/src/core/{reviewer.js → reviewer.ts} +36 -29
- package/src/core/{runner.js → runner.ts} +125 -76
- package/src/utils/{config.js → config.ts} +17 -25
- package/src/utils/{cursor-agent.js → cursor-agent.ts} +38 -47
- package/src/utils/doctor.ts +312 -0
- package/src/utils/{git.js → git.ts} +70 -56
- package/src/utils/{logger.js → logger.ts} +170 -178
- package/src/utils/{state.js → state.ts} +30 -38
- package/src/utils/types.ts +134 -0
- package/src/cli/resume.js +0 -31
- package/src/cli/run.js +0 -51
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
2
|
* CursorFlow monitor command
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as logger from '../utils/logger';
|
|
8
|
+
import { loadState } from '../utils/state';
|
|
9
|
+
import { LaneState } from '../utils/types';
|
|
10
|
+
import { loadConfig } from '../utils/config';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
interface MonitorOptions {
|
|
13
|
+
runDir?: string;
|
|
14
|
+
watch: boolean;
|
|
15
|
+
interval: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArgs(args: string[]): MonitorOptions {
|
|
13
19
|
const watch = args.includes('--watch');
|
|
14
20
|
const intervalIdx = args.indexOf('--interval');
|
|
15
|
-
const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1]) || 2 : 2;
|
|
21
|
+
const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
|
|
16
22
|
|
|
17
23
|
// Find run directory (first non-option argument)
|
|
18
24
|
const runDir = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
|
|
@@ -27,7 +33,7 @@ function parseArgs(args) {
|
|
|
27
33
|
/**
|
|
28
34
|
* Find the latest run directory
|
|
29
35
|
*/
|
|
30
|
-
function findLatestRunDir(logsDir) {
|
|
36
|
+
function findLatestRunDir(logsDir: string): string | null {
|
|
31
37
|
const runsDir = path.join(logsDir, 'runs');
|
|
32
38
|
|
|
33
39
|
if (!fs.existsSync(runsDir)) {
|
|
@@ -43,13 +49,13 @@ function findLatestRunDir(logsDir) {
|
|
|
43
49
|
}))
|
|
44
50
|
.sort((a, b) => b.mtime - a.mtime);
|
|
45
51
|
|
|
46
|
-
return runs.length > 0 ? runs[0]
|
|
52
|
+
return runs.length > 0 ? runs[0]!.path : null;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
56
|
* List all lanes in a run directory
|
|
51
57
|
*/
|
|
52
|
-
function listLanes(runDir) {
|
|
58
|
+
function listLanes(runDir: string): { name: string; path: string }[] {
|
|
53
59
|
const lanesDir = path.join(runDir, 'lanes');
|
|
54
60
|
|
|
55
61
|
if (!fs.existsSync(lanesDir)) {
|
|
@@ -70,9 +76,16 @@ function listLanes(runDir) {
|
|
|
70
76
|
/**
|
|
71
77
|
* Get lane status
|
|
72
78
|
*/
|
|
73
|
-
function getLaneStatus(lanePath) {
|
|
79
|
+
function getLaneStatus(lanePath: string): {
|
|
80
|
+
status: string;
|
|
81
|
+
currentTask: number | string;
|
|
82
|
+
totalTasks: number | string;
|
|
83
|
+
progress: string;
|
|
84
|
+
pipelineBranch?: string;
|
|
85
|
+
chatId?: string;
|
|
86
|
+
} {
|
|
74
87
|
const statePath = path.join(lanePath, 'state.json');
|
|
75
|
-
const state = loadState(statePath);
|
|
88
|
+
const state = loadState<LaneState & { chatId?: string }>(statePath);
|
|
76
89
|
|
|
77
90
|
if (!state) {
|
|
78
91
|
return {
|
|
@@ -89,7 +102,7 @@ function getLaneStatus(lanePath) {
|
|
|
89
102
|
|
|
90
103
|
return {
|
|
91
104
|
status: state.status || 'unknown',
|
|
92
|
-
currentTask: state.currentTaskIndex + 1,
|
|
105
|
+
currentTask: (state.currentTaskIndex || 0) + 1,
|
|
93
106
|
totalTasks: state.totalTasks || '?',
|
|
94
107
|
progress: `${progress}%`,
|
|
95
108
|
pipelineBranch: state.pipelineBranch || '-',
|
|
@@ -97,10 +110,25 @@ function getLaneStatus(lanePath) {
|
|
|
97
110
|
};
|
|
98
111
|
}
|
|
99
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Get status icon
|
|
115
|
+
*/
|
|
116
|
+
function getStatusIcon(status: string): string {
|
|
117
|
+
const icons: Record<string, string> = {
|
|
118
|
+
'running': '🔄',
|
|
119
|
+
'completed': '✅',
|
|
120
|
+
'failed': '❌',
|
|
121
|
+
'blocked_dependency': '🚫',
|
|
122
|
+
'no state': '⚪',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return icons[status] || '❓';
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
/**
|
|
101
129
|
* Display lane status table
|
|
102
130
|
*/
|
|
103
|
-
function displayStatus(runDir, lanes) {
|
|
131
|
+
function displayStatus(runDir: string, lanes: { name: string; path: string }[]): void {
|
|
104
132
|
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
105
133
|
console.log(`📊 Run: ${path.basename(runDir)}`);
|
|
106
134
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
@@ -131,25 +159,10 @@ function displayStatus(runDir, lanes) {
|
|
|
131
159
|
console.log();
|
|
132
160
|
}
|
|
133
161
|
|
|
134
|
-
/**
|
|
135
|
-
* Get status icon
|
|
136
|
-
*/
|
|
137
|
-
function getStatusIcon(status) {
|
|
138
|
-
const icons = {
|
|
139
|
-
'running': '🔄',
|
|
140
|
-
'completed': '✅',
|
|
141
|
-
'failed': '❌',
|
|
142
|
-
'blocked_dependency': '🚫',
|
|
143
|
-
'no state': '⚪',
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
return icons[status] || '❓';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
162
|
/**
|
|
150
163
|
* Monitor lanes
|
|
151
164
|
*/
|
|
152
|
-
async function monitor(args) {
|
|
165
|
+
async function monitor(args: string[]): Promise<void> {
|
|
153
166
|
logger.section('📡 Monitoring Lane Execution');
|
|
154
167
|
|
|
155
168
|
const options = parseArgs(args);
|
|
@@ -159,20 +172,18 @@ async function monitor(args) {
|
|
|
159
172
|
let runDir = options.runDir;
|
|
160
173
|
|
|
161
174
|
if (!runDir || runDir === 'latest') {
|
|
162
|
-
runDir = findLatestRunDir(config.logsDir);
|
|
175
|
+
runDir = findLatestRunDir(config.logsDir) || undefined;
|
|
163
176
|
|
|
164
177
|
if (!runDir) {
|
|
165
|
-
logger.error(
|
|
166
|
-
|
|
167
|
-
process.exit(1);
|
|
178
|
+
logger.error(`Runs directory: ${path.join(config.logsDir, 'runs')}`);
|
|
179
|
+
throw new Error('No run directories found');
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
logger.info(`Using latest run: ${path.basename(runDir)}`);
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
if (!fs.existsSync(runDir)) {
|
|
174
|
-
|
|
175
|
-
process.exit(1);
|
|
186
|
+
throw new Error(`Run directory not found: ${runDir}`);
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
// Watch mode
|
|
@@ -187,8 +198,8 @@ async function monitor(args) {
|
|
|
187
198
|
process.stdout.write('\x1Bc');
|
|
188
199
|
}
|
|
189
200
|
|
|
190
|
-
const lanes = listLanes(runDir);
|
|
191
|
-
displayStatus(runDir
|
|
201
|
+
const lanes = listLanes(runDir!);
|
|
202
|
+
displayStatus(runDir!, lanes);
|
|
192
203
|
|
|
193
204
|
iteration++;
|
|
194
205
|
};
|
|
@@ -200,10 +211,12 @@ async function monitor(args) {
|
|
|
200
211
|
const intervalId = setInterval(refresh, options.interval * 1000);
|
|
201
212
|
|
|
202
213
|
// Handle Ctrl+C
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
return new Promise((_, reject) => {
|
|
215
|
+
process.on('SIGINT', () => {
|
|
216
|
+
clearInterval(intervalId);
|
|
217
|
+
console.log('\n👋 Monitoring stopped\n');
|
|
218
|
+
process.exit(0);
|
|
219
|
+
});
|
|
207
220
|
});
|
|
208
221
|
|
|
209
222
|
} else {
|
|
@@ -213,4 +226,4 @@ async function monitor(args) {
|
|
|
213
226
|
}
|
|
214
227
|
}
|
|
215
228
|
|
|
216
|
-
|
|
229
|
+
export = monitor;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CursorFlow resume command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import * as logger from '../utils/logger';
|
|
9
|
+
import { loadConfig, getLogsDir } from '../utils/config';
|
|
10
|
+
import { loadState } from '../utils/state';
|
|
11
|
+
import { LaneState } from '../utils/types';
|
|
12
|
+
|
|
13
|
+
interface ResumeOptions {
|
|
14
|
+
lane: string | null;
|
|
15
|
+
runDir: string | null;
|
|
16
|
+
clean: boolean;
|
|
17
|
+
restart: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseArgs(args: string[]): ResumeOptions {
|
|
21
|
+
const runDirIdx = args.indexOf('--run-dir');
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
lane: args.find(a => !a.startsWith('--')) || null,
|
|
25
|
+
runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
|
|
26
|
+
clean: args.includes('--clean'),
|
|
27
|
+
restart: args.includes('--restart'),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find the latest run directory
|
|
33
|
+
*/
|
|
34
|
+
function findLatestRunDir(logsDir: string): string | null {
|
|
35
|
+
const runsDir = path.join(logsDir, 'runs');
|
|
36
|
+
if (!fs.existsSync(runsDir)) return null;
|
|
37
|
+
|
|
38
|
+
const runs = fs.readdirSync(runsDir)
|
|
39
|
+
.filter(d => d.startsWith('run-'))
|
|
40
|
+
.sort()
|
|
41
|
+
.reverse();
|
|
42
|
+
|
|
43
|
+
return runs.length > 0 ? path.join(runsDir, runs[0]!) : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function resume(args: string[]): Promise<void> {
|
|
47
|
+
const options = parseArgs(args);
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
const logsDir = getLogsDir(config);
|
|
50
|
+
|
|
51
|
+
if (!options.lane) {
|
|
52
|
+
throw new Error('Lane name required (e.g., cursorflow resume lane-1)');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let runDir = options.runDir;
|
|
56
|
+
if (!runDir) {
|
|
57
|
+
runDir = findLatestRunDir(logsDir);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!runDir || !fs.existsSync(runDir)) {
|
|
61
|
+
throw new Error(`Run directory not found: ${runDir || 'latest'}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const laneDir = path.join(runDir, 'lanes', options.lane);
|
|
65
|
+
const statePath = path.join(laneDir, 'state.json');
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(statePath)) {
|
|
68
|
+
throw new Error(`Lane state not found at ${statePath}. Is the lane name correct?`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const state = loadState<LaneState>(statePath);
|
|
72
|
+
if (!state) {
|
|
73
|
+
throw new Error(`Failed to load state from ${statePath}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!state.tasksFile || !fs.existsSync(state.tasksFile)) {
|
|
77
|
+
throw new Error(`Original tasks file not found: ${state.tasksFile}. Resume impossible without task definition.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.section(`🔁 Resuming Lane: ${options.lane}`);
|
|
81
|
+
logger.info(`Run: ${path.basename(runDir)}`);
|
|
82
|
+
logger.info(`Tasks: ${state.tasksFile}`);
|
|
83
|
+
logger.info(`Starting from task index: ${options.restart ? 0 : state.currentTaskIndex}`);
|
|
84
|
+
|
|
85
|
+
const runnerPath = require.resolve('../core/runner');
|
|
86
|
+
const runnerArgs = [
|
|
87
|
+
runnerPath,
|
|
88
|
+
state.tasksFile,
|
|
89
|
+
'--run-dir', laneDir,
|
|
90
|
+
'--start-index', options.restart ? '0' : String(state.currentTaskIndex),
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
logger.info(`Spawning runner process...`);
|
|
94
|
+
|
|
95
|
+
const child = spawn('node', runnerArgs, {
|
|
96
|
+
stdio: 'inherit',
|
|
97
|
+
env: process.env,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
child.on('exit', (code) => {
|
|
102
|
+
if (code === 0) {
|
|
103
|
+
logger.success(`Lane ${options.lane} completed successfully`);
|
|
104
|
+
resolve();
|
|
105
|
+
} else if (code === 2) {
|
|
106
|
+
logger.warn(`Lane ${options.lane} blocked on dependency change`);
|
|
107
|
+
resolve();
|
|
108
|
+
} else {
|
|
109
|
+
reject(new Error(`Lane ${options.lane} failed with exit code ${code}`));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
child.on('error', (error) => {
|
|
114
|
+
reject(new Error(`Failed to start runner: ${error.message}`));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export = resume;
|
package/src/cli/run.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CursorFlow run command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as logger from '../utils/logger';
|
|
8
|
+
import { orchestrate } from '../core/orchestrator';
|
|
9
|
+
import { getLogsDir, loadConfig } from '../utils/config';
|
|
10
|
+
import { runDoctor } from '../utils/doctor';
|
|
11
|
+
import { areCommandsInstalled, setupCommands } from './setup-commands';
|
|
12
|
+
|
|
13
|
+
interface RunOptions {
|
|
14
|
+
tasksDir?: string;
|
|
15
|
+
dryRun: boolean;
|
|
16
|
+
executor: string | null;
|
|
17
|
+
skipDoctor: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseArgs(args: string[]): RunOptions {
|
|
21
|
+
const tasksDir = args.find(a => !a.startsWith('--'));
|
|
22
|
+
const executorIdx = args.indexOf('--executor');
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
tasksDir,
|
|
26
|
+
dryRun: args.includes('--dry-run'),
|
|
27
|
+
executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
|
|
28
|
+
skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function run(args: string[]): Promise<void> {
|
|
33
|
+
const options = parseArgs(args);
|
|
34
|
+
|
|
35
|
+
// Auto-setup Cursor commands if missing or outdated
|
|
36
|
+
if (!areCommandsInstalled()) {
|
|
37
|
+
logger.info('Installing missing or outdated Cursor IDE commands...');
|
|
38
|
+
try {
|
|
39
|
+
setupCommands({ silent: true });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// Non-blocking
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!options.tasksDir) {
|
|
46
|
+
console.log('\nUsage: cursorflow run <tasks-dir> [options]');
|
|
47
|
+
throw new Error('Tasks directory required');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const config = loadConfig();
|
|
51
|
+
const logsDir = getLogsDir(config);
|
|
52
|
+
|
|
53
|
+
// Resolve tasks dir:
|
|
54
|
+
// - Prefer the exact path if it exists relative to cwd
|
|
55
|
+
// - Otherwise, fall back to projectRoot-relative path for better ergonomics
|
|
56
|
+
const tasksDir =
|
|
57
|
+
path.isAbsolute(options.tasksDir)
|
|
58
|
+
? options.tasksDir
|
|
59
|
+
: (fs.existsSync(options.tasksDir)
|
|
60
|
+
? path.resolve(process.cwd(), options.tasksDir)
|
|
61
|
+
: path.join(config.projectRoot, options.tasksDir));
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(tasksDir)) {
|
|
64
|
+
throw new Error(`Tasks directory not found: ${tasksDir}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Preflight checks (doctor)
|
|
68
|
+
if (!options.skipDoctor) {
|
|
69
|
+
const report = runDoctor({
|
|
70
|
+
cwd: process.cwd(),
|
|
71
|
+
tasksDir,
|
|
72
|
+
executor: options.executor || config.executor,
|
|
73
|
+
includeCursorAgentChecks: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!report.ok) {
|
|
77
|
+
logger.section('🛑 Pre-flight check failed');
|
|
78
|
+
for (const issue of report.issues) {
|
|
79
|
+
const header = `${issue.title} (${issue.id})`;
|
|
80
|
+
if (issue.severity === 'error') {
|
|
81
|
+
logger.error(header, '❌');
|
|
82
|
+
} else {
|
|
83
|
+
logger.warn(header, '⚠️');
|
|
84
|
+
}
|
|
85
|
+
console.log(` ${issue.message}`);
|
|
86
|
+
if (issue.details) console.log(` Details: ${issue.details}`);
|
|
87
|
+
if (issue.fixes?.length) {
|
|
88
|
+
console.log(' Fix:');
|
|
89
|
+
for (const fix of issue.fixes) console.log(` - ${fix}`);
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
}
|
|
93
|
+
throw new Error('Pre-flight checks failed. Run `cursorflow doctor` for details.');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await orchestrate(tasksDir, {
|
|
99
|
+
executor: options.executor || config.executor,
|
|
100
|
+
pollInterval: config.pollInterval * 1000,
|
|
101
|
+
runDir: path.join(logsDir, 'runs', `run-${Date.now()}`),
|
|
102
|
+
});
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
// Re-throw to be handled by the main entry point
|
|
105
|
+
throw new Error(`Orchestration failed: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export = run;
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
2
|
* Setup Cursor commands
|
|
4
3
|
*
|
|
5
4
|
* Installs CursorFlow commands to .cursor/commands/cursorflow/
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as logger from '../utils/logger';
|
|
10
|
+
import { findProjectRoot } from '../utils/config';
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
interface SetupOptions {
|
|
13
|
+
force?: boolean;
|
|
14
|
+
uninstall?: boolean;
|
|
15
|
+
silent?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArgs(args: string[]): SetupOptions {
|
|
19
|
+
const options: SetupOptions = {
|
|
15
20
|
force: false,
|
|
16
21
|
uninstall: false,
|
|
17
22
|
silent: false,
|
|
@@ -39,7 +44,7 @@ function parseArgs(args) {
|
|
|
39
44
|
return options;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
function printHelp() {
|
|
47
|
+
function printHelp(): void {
|
|
43
48
|
console.log(`
|
|
44
49
|
Usage: cursorflow-setup [options]
|
|
45
50
|
|
|
@@ -58,12 +63,12 @@ Examples:
|
|
|
58
63
|
`);
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
function getCommandsSourceDir() {
|
|
66
|
+
function getCommandsSourceDir(): string {
|
|
62
67
|
// Commands are in the package directory
|
|
63
68
|
return path.join(__dirname, '..', '..', 'commands');
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
function setupCommands(options = {}) {
|
|
71
|
+
export function setupCommands(options: SetupOptions = {}): { installed: number; backed: number; skipped: number } {
|
|
67
72
|
const projectRoot = findProjectRoot();
|
|
68
73
|
const targetDir = path.join(projectRoot, '.cursor', 'commands', 'cursorflow');
|
|
69
74
|
const sourceDir = getCommandsSourceDir();
|
|
@@ -139,7 +144,7 @@ function setupCommands(options = {}) {
|
|
|
139
144
|
return { installed, backed, skipped };
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
function uninstallCommands(options = {}) {
|
|
147
|
+
export function uninstallCommands(options: SetupOptions = {}): { removed: number } {
|
|
143
148
|
const projectRoot = findProjectRoot();
|
|
144
149
|
const targetDir = path.join(projectRoot, '.cursor', 'commands', 'cursorflow');
|
|
145
150
|
|
|
@@ -178,7 +183,26 @@ function uninstallCommands(options = {}) {
|
|
|
178
183
|
return { removed };
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Check if commands are already installed
|
|
188
|
+
*/
|
|
189
|
+
export function areCommandsInstalled(): boolean {
|
|
190
|
+
const projectRoot = findProjectRoot();
|
|
191
|
+
const targetDir = path.join(projectRoot, '.cursor', 'commands', 'cursorflow');
|
|
192
|
+
const sourceDir = getCommandsSourceDir();
|
|
193
|
+
|
|
194
|
+
if (!fs.existsSync(targetDir) || !fs.existsSync(sourceDir)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sourceFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith('.md'));
|
|
199
|
+
const targetFiles = fs.readdirSync(targetDir).filter(f => f.endsWith('.md'));
|
|
200
|
+
|
|
201
|
+
// Basic check: do we have all the files from source in target?
|
|
202
|
+
return sourceFiles.every(f => targetFiles.includes(f));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function main(args: string[]): Promise<any> {
|
|
182
206
|
const options = parseArgs(args);
|
|
183
207
|
|
|
184
208
|
try {
|
|
@@ -187,7 +211,7 @@ async function main(args) {
|
|
|
187
211
|
} else {
|
|
188
212
|
return setupCommands(options);
|
|
189
213
|
}
|
|
190
|
-
} catch (error) {
|
|
214
|
+
} catch (error: any) {
|
|
191
215
|
if (!options.silent) {
|
|
192
216
|
logger.error(error.message);
|
|
193
217
|
}
|
|
@@ -198,13 +222,9 @@ async function main(args) {
|
|
|
198
222
|
if (require.main === module) {
|
|
199
223
|
main(process.argv.slice(2)).catch(error => {
|
|
200
224
|
console.error('❌ Error:', error.message);
|
|
201
|
-
if (process.env
|
|
225
|
+
if (process.env['DEBUG']) {
|
|
202
226
|
console.error(error.stack);
|
|
203
227
|
}
|
|
204
228
|
process.exit(1);
|
|
205
229
|
});
|
|
206
230
|
}
|
|
207
|
-
|
|
208
|
-
module.exports = setupCommands;
|
|
209
|
-
module.exports.setupCommands = setupCommands;
|
|
210
|
-
module.exports.uninstallCommands = uninstallCommands;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CursorFlow signal command
|
|
3
|
+
*
|
|
4
|
+
* Send a direct message to a running lane
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as logger from '../utils/logger';
|
|
10
|
+
import { loadConfig, getLogsDir } from '../utils/config';
|
|
11
|
+
import { appendLog, createConversationEntry } from '../utils/state';
|
|
12
|
+
|
|
13
|
+
interface SignalOptions {
|
|
14
|
+
lane: string | null;
|
|
15
|
+
message: string | null;
|
|
16
|
+
runDir: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseArgs(args: string[]): SignalOptions {
|
|
20
|
+
const runDirIdx = args.indexOf('--run-dir');
|
|
21
|
+
|
|
22
|
+
// First non-option is lane, second (or rest joined) is message
|
|
23
|
+
const nonOptions = args.filter(a => !a.startsWith('--'));
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
lane: nonOptions[0] || null,
|
|
27
|
+
message: nonOptions.slice(1).join(' ') || null,
|
|
28
|
+
runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findLatestRunDir(logsDir: string): string | null {
|
|
33
|
+
const runsDir = path.join(logsDir, 'runs');
|
|
34
|
+
if (!fs.existsSync(runsDir)) return null;
|
|
35
|
+
|
|
36
|
+
const runs = fs.readdirSync(runsDir)
|
|
37
|
+
.filter(d => d.startsWith('run-'))
|
|
38
|
+
.sort()
|
|
39
|
+
.reverse();
|
|
40
|
+
|
|
41
|
+
return runs.length > 0 ? path.join(runsDir, runs[0]!) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function signal(args: string[]): Promise<void> {
|
|
45
|
+
const options = parseArgs(args);
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
const logsDir = getLogsDir(config);
|
|
48
|
+
|
|
49
|
+
if (!options.lane) {
|
|
50
|
+
throw new Error('Lane name required: cursorflow signal <lane> "<message>"');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!options.message) {
|
|
54
|
+
throw new Error('Message required: cursorflow signal <lane> "<message>"');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let runDir = options.runDir;
|
|
58
|
+
if (!runDir) {
|
|
59
|
+
runDir = findLatestRunDir(logsDir);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!runDir || !fs.existsSync(runDir)) {
|
|
63
|
+
throw new Error(`Run directory not found: ${runDir || 'latest'}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const convoPath = path.join(runDir, 'lanes', options.lane, 'conversation.jsonl');
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(convoPath)) {
|
|
69
|
+
throw new Error(`Conversation log not found at ${convoPath}. Is the lane running?`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.info(`Sending signal to lane: ${options.lane}`);
|
|
73
|
+
logger.info(`Message: "${options.message}"`);
|
|
74
|
+
|
|
75
|
+
// Append as a "commander" role message
|
|
76
|
+
// Note: We cast to 'system' or similar if 'commander' isn't in the enum,
|
|
77
|
+
// but let's use 'reviewer' or 'system' which agents usually respect,
|
|
78
|
+
// or update the type definition.
|
|
79
|
+
const entry = createConversationEntry('system', `[COMMANDER INTERVENTION]\n${options.message}`, {
|
|
80
|
+
task: 'DIRECT_SIGNAL'
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
appendLog(convoPath, entry);
|
|
84
|
+
|
|
85
|
+
logger.success('Signal sent successfully. The agent will see this message in its next turn or via file monitoring.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export = signal;
|
|
89
|
+
|