@litmers/cursorflow-orchestrator 0.1.34 → 0.1.37
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 +35 -0
- package/README.md +38 -7
- package/commands/cursorflow-doctor.md +45 -23
- package/commands/cursorflow-run.md +60 -111
- package/dist/cli/doctor.js +47 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/logs.js +10 -1
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +12 -4
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/resume.js +46 -21
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +33 -8
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/stop.js +6 -0
- package/dist/cli/stop.js.map +1 -1
- package/dist/cli/tasks.d.ts +5 -3
- package/dist/cli/tasks.js +180 -27
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/orchestrator.js +6 -5
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.js +3 -1
- package/dist/core/runner/agent.js.map +1 -1
- package/dist/services/logging/console.js +2 -1
- package/dist/services/logging/console.js.map +1 -1
- package/dist/utils/config.d.ts +5 -1
- package/dist/utils/config.js +8 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +40 -8
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +1 -1
- package/dist/utils/enhanced-logger.js +3 -2
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/flow.d.ts +9 -0
- package/dist/utils/flow.js +73 -0
- package/dist/utils/flow.js.map +1 -0
- package/package.json +2 -1
- package/src/cli/doctor.ts +48 -4
- package/src/cli/logs.ts +13 -2
- package/src/cli/monitor.ts +16 -5
- package/src/cli/resume.ts +48 -20
- package/src/cli/run.ts +31 -9
- package/src/cli/stop.ts +8 -0
- package/src/cli/tasks.ts +199 -19
- package/src/core/orchestrator.ts +6 -5
- package/src/core/runner/agent.ts +4 -1
- package/src/services/logging/console.ts +2 -1
- package/src/utils/config.ts +8 -1
- package/src/utils/doctor.ts +36 -8
- package/src/utils/enhanced-logger.ts +3 -2
- package/src/utils/flow.ts +42 -0
package/src/cli/tasks.ts
CHANGED
|
@@ -2,36 +2,55 @@
|
|
|
2
2
|
* CursorFlow tasks command
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* cursorflow tasks # List all tasks
|
|
6
|
-
* cursorflow tasks --
|
|
7
|
-
* cursorflow tasks
|
|
5
|
+
* cursorflow tasks # List all flows (new) and tasks (legacy)
|
|
6
|
+
* cursorflow tasks --flows # List only flows
|
|
7
|
+
* cursorflow tasks --legacy # List only legacy tasks
|
|
8
|
+
* cursorflow tasks --validate # List with validation
|
|
9
|
+
* cursorflow tasks <name> # Show detailed info
|
|
8
10
|
*/
|
|
9
11
|
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
10
14
|
import * as logger from '../utils/logger';
|
|
11
15
|
import { TaskService, TaskDirInfo, ValidationStatus } from '../utils/task-service';
|
|
12
|
-
import { findProjectRoot, loadConfig, getTasksDir } from '../utils/config';
|
|
16
|
+
import { findProjectRoot, loadConfig, getTasksDir, getFlowsDir } from '../utils/config';
|
|
17
|
+
import { safeJoin } from '../utils/path';
|
|
13
18
|
|
|
14
19
|
const COLORS = logger.COLORS;
|
|
15
20
|
|
|
21
|
+
interface FlowInfo {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
path: string;
|
|
25
|
+
timestamp: Date;
|
|
26
|
+
lanes: string[];
|
|
27
|
+
status: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
interface TasksCliOptions {
|
|
17
31
|
validate: boolean;
|
|
18
32
|
taskName: string | null;
|
|
33
|
+
flowsOnly: boolean;
|
|
34
|
+
legacyOnly: boolean;
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
function printHelp(): void {
|
|
22
38
|
console.log(`
|
|
23
|
-
Usage: cursorflow tasks [options] [
|
|
39
|
+
Usage: cursorflow tasks [options] [name]
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
List and view flows (new) and prepared tasks (legacy).
|
|
26
42
|
|
|
27
43
|
Options:
|
|
28
|
-
--
|
|
44
|
+
--flows Show only flows (from _cursorflow/flows/)
|
|
45
|
+
--legacy Show only legacy tasks (from _cursorflow/tasks/)
|
|
46
|
+
--validate Run validation before listing
|
|
29
47
|
--help, -h Show help
|
|
30
48
|
|
|
31
49
|
Examples:
|
|
32
|
-
cursorflow tasks
|
|
33
|
-
cursorflow tasks --
|
|
34
|
-
cursorflow tasks
|
|
50
|
+
cursorflow tasks # List all flows and tasks
|
|
51
|
+
cursorflow tasks --flows # List only flows
|
|
52
|
+
cursorflow tasks TestFeature # Show flow or task details
|
|
53
|
+
cursorflow tasks --validate # Validate all entries
|
|
35
54
|
`);
|
|
36
55
|
}
|
|
37
56
|
|
|
@@ -39,6 +58,8 @@ function parseArgs(args: string[]): TasksCliOptions {
|
|
|
39
58
|
const options: TasksCliOptions = {
|
|
40
59
|
validate: args.includes('--validate'),
|
|
41
60
|
taskName: null,
|
|
61
|
+
flowsOnly: args.includes('--flows'),
|
|
62
|
+
legacyOnly: args.includes('--legacy'),
|
|
42
63
|
};
|
|
43
64
|
|
|
44
65
|
const nameArg = args.find(arg => !arg.startsWith('-'));
|
|
@@ -54,6 +75,136 @@ function parseArgs(args: string[]): TasksCliOptions {
|
|
|
54
75
|
return options;
|
|
55
76
|
}
|
|
56
77
|
|
|
78
|
+
/**
|
|
79
|
+
* List flows from _cursorflow/flows/
|
|
80
|
+
*/
|
|
81
|
+
function listFlows(flowsDir: string): FlowInfo[] {
|
|
82
|
+
if (!fs.existsSync(flowsDir)) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dirs = fs.readdirSync(flowsDir)
|
|
87
|
+
.filter(name => {
|
|
88
|
+
const dirPath = safeJoin(flowsDir, name);
|
|
89
|
+
return fs.statSync(dirPath).isDirectory() && !name.startsWith('.');
|
|
90
|
+
})
|
|
91
|
+
.sort((a, b) => b.localeCompare(a)); // Most recent first
|
|
92
|
+
|
|
93
|
+
return dirs.map(name => {
|
|
94
|
+
const flowPath = safeJoin(flowsDir, name);
|
|
95
|
+
const metaPath = safeJoin(flowPath, 'flow.meta.json');
|
|
96
|
+
|
|
97
|
+
let meta: any = {};
|
|
98
|
+
try {
|
|
99
|
+
if (fs.existsSync(metaPath)) {
|
|
100
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
|
|
104
|
+
// Parse flow name from directory (e.g., "001_TestFeature" -> "TestFeature")
|
|
105
|
+
const match = name.match(/^(\d+)_(.+)$/);
|
|
106
|
+
const id = match ? match[1] : name;
|
|
107
|
+
const flowName = match ? match[2] : name;
|
|
108
|
+
|
|
109
|
+
// Get lane files
|
|
110
|
+
const laneFiles = fs.readdirSync(flowPath)
|
|
111
|
+
.filter(f => f.endsWith('.json') && f !== 'flow.meta.json')
|
|
112
|
+
.map(f => {
|
|
113
|
+
const laneMatch = f.match(/^\d+-([^.]+)\.json$/);
|
|
114
|
+
return laneMatch ? laneMatch[1] : f.replace('.json', '');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
id,
|
|
119
|
+
name: flowName,
|
|
120
|
+
path: flowPath,
|
|
121
|
+
timestamp: meta.createdAt ? new Date(meta.createdAt) : new Date(),
|
|
122
|
+
lanes: laneFiles,
|
|
123
|
+
status: meta.status || 'pending',
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get flow info by name
|
|
130
|
+
*/
|
|
131
|
+
function getFlowInfo(flowsDir: string, flowName: string): FlowInfo | null {
|
|
132
|
+
const flows = listFlows(flowsDir);
|
|
133
|
+
return flows.find(f => f.name === flowName || `${f.id}_${f.name}` === flowName) || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Print flows list
|
|
138
|
+
*/
|
|
139
|
+
function printFlowsList(flows: FlowInfo[]): void {
|
|
140
|
+
if (flows.length === 0) {
|
|
141
|
+
logger.info('No flows found in _cursorflow/flows/');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`${COLORS.bold}Flows:${COLORS.reset}`);
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < flows.length; i++) {
|
|
148
|
+
const flow = flows[i]!;
|
|
149
|
+
const prefix = i === 0 ? ' ▶' : ' ';
|
|
150
|
+
const name = `${flow.id}_${flow.name}`.padEnd(30);
|
|
151
|
+
const lanes = `${flow.lanes.length} lane${flow.lanes.length !== 1 ? 's' : ''}`.padEnd(10);
|
|
152
|
+
const status = flow.status.padEnd(10);
|
|
153
|
+
const date = formatDate(flow.timestamp);
|
|
154
|
+
|
|
155
|
+
let color = COLORS.reset;
|
|
156
|
+
if (flow.status === 'completed') color = COLORS.green;
|
|
157
|
+
else if (flow.status === 'running') color = COLORS.cyan;
|
|
158
|
+
else if (flow.status === 'failed') color = COLORS.red;
|
|
159
|
+
|
|
160
|
+
console.log(`${color}${prefix} ${name} ${lanes} ${status} ${date}${COLORS.reset}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Print flow details
|
|
166
|
+
*/
|
|
167
|
+
function printFlowDetail(flow: FlowInfo, flowsDir: string): void {
|
|
168
|
+
console.log(`${COLORS.bold}Flow: ${flow.name}${COLORS.reset}`);
|
|
169
|
+
console.log(`${COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
170
|
+
console.log(` ID: ${flow.id}`);
|
|
171
|
+
console.log(` Status: ${flow.status}`);
|
|
172
|
+
console.log(` Path: ${flow.path}`);
|
|
173
|
+
console.log('');
|
|
174
|
+
|
|
175
|
+
if (flow.lanes.length === 0) {
|
|
176
|
+
console.log('No lanes defined.');
|
|
177
|
+
} else {
|
|
178
|
+
console.log(`${COLORS.bold}Lanes:${COLORS.reset}`);
|
|
179
|
+
for (const laneName of flow.lanes) {
|
|
180
|
+
// Read lane file for task info
|
|
181
|
+
const laneFiles = fs.readdirSync(flow.path)
|
|
182
|
+
.filter(f => f.endsWith('.json') && f !== 'flow.meta.json');
|
|
183
|
+
|
|
184
|
+
const laneFile = laneFiles.find(f => {
|
|
185
|
+
const match = f.match(/^\d+-([^.]+)\.json$/);
|
|
186
|
+
return match && match[1] === laneName;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (laneFile) {
|
|
190
|
+
try {
|
|
191
|
+
const laneData = JSON.parse(fs.readFileSync(safeJoin(flow.path, laneFile), 'utf-8'));
|
|
192
|
+
const tasks = laneData.tasks || [];
|
|
193
|
+
const taskFlow = tasks.map((t: any) => t.name).join(' → ');
|
|
194
|
+
console.log(` ${laneName.padEnd(18)} ${COLORS.blue}[${tasks.length} tasks]${COLORS.reset} ${taskFlow}`);
|
|
195
|
+
} catch {
|
|
196
|
+
console.log(` ${laneName.padEnd(18)} ${COLORS.yellow}[error reading]${COLORS.reset}`);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
console.log(` ${laneName}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(`${COLORS.gray}Run: cursorflow run ${flow.name}${COLORS.reset}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
57
208
|
function formatDate(date: Date): string {
|
|
58
209
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
59
210
|
return `${months[date.getMonth()]} ${date.getDate()}`;
|
|
@@ -118,25 +269,37 @@ async function tasks(args: string[]): Promise<void> {
|
|
|
118
269
|
const options = parseArgs(args);
|
|
119
270
|
const projectRoot = findProjectRoot();
|
|
120
271
|
const config = loadConfig(projectRoot);
|
|
272
|
+
const flowsDir = getFlowsDir(config);
|
|
121
273
|
const tasksDir = getTasksDir(config);
|
|
122
274
|
const taskService = new TaskService(tasksDir);
|
|
123
275
|
|
|
276
|
+
// Check for specific flow/task by name
|
|
124
277
|
if (options.taskName) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
278
|
+
// First try to find as a flow
|
|
279
|
+
const flowInfo = getFlowInfo(flowsDir, options.taskName);
|
|
280
|
+
if (flowInfo) {
|
|
281
|
+
printFlowDetail(flowInfo, flowsDir);
|
|
282
|
+
return;
|
|
129
283
|
}
|
|
130
284
|
|
|
131
|
-
//
|
|
285
|
+
// Then try as a legacy task
|
|
286
|
+
const taskInfo = taskService.getTaskDirInfo(options.taskName);
|
|
287
|
+
if (taskInfo) {
|
|
132
288
|
taskService.validateTaskDir(options.taskName);
|
|
133
289
|
const updatedInfo = taskService.getTaskDirInfo(options.taskName)!;
|
|
134
|
-
|
|
135
290
|
printTaskDetail(updatedInfo);
|
|
136
|
-
|
|
137
|
-
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
logger.error(`Flow or task not found: ${options.taskName}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// List flows and/or tasks
|
|
299
|
+
const flows = options.legacyOnly ? [] : listFlows(flowsDir);
|
|
300
|
+
let taskList = options.flowsOnly ? [] : taskService.listTaskDirs();
|
|
138
301
|
|
|
139
|
-
|
|
302
|
+
if (options.validate && taskList.length > 0) {
|
|
140
303
|
const spinner = logger.createSpinner('Validating tasks...');
|
|
141
304
|
spinner.start();
|
|
142
305
|
for (const task of taskList) {
|
|
@@ -146,8 +309,25 @@ async function tasks(args: string[]): Promise<void> {
|
|
|
146
309
|
taskList = taskService.listTaskDirs();
|
|
147
310
|
}
|
|
148
311
|
|
|
312
|
+
// Print results
|
|
313
|
+
if (flows.length > 0) {
|
|
314
|
+
printFlowsList(flows);
|
|
315
|
+
if (taskList.length > 0) {
|
|
316
|
+
console.log('');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (taskList.length > 0) {
|
|
321
|
+
console.log(`${COLORS.gray}Legacy Tasks:${COLORS.reset}`);
|
|
149
322
|
printTasksList(taskList);
|
|
150
323
|
}
|
|
324
|
+
|
|
325
|
+
if (flows.length === 0 && taskList.length === 0) {
|
|
326
|
+
logger.info('No flows or tasks found.');
|
|
327
|
+
console.log('');
|
|
328
|
+
console.log('Create a new flow:');
|
|
329
|
+
console.log(' cursorflow new <FlowName> --lanes "lane1,lane2"');
|
|
330
|
+
}
|
|
151
331
|
}
|
|
152
332
|
|
|
153
333
|
export = tasks;
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -278,11 +278,12 @@ export function spawnLane({
|
|
|
278
278
|
};
|
|
279
279
|
|
|
280
280
|
if (logConfig.enabled) {
|
|
281
|
-
// Helper to get dynamic lane label like [
|
|
281
|
+
// Helper to get dynamic lane label like [L1-T1-lanename10]
|
|
282
282
|
const getDynamicLabel = () => {
|
|
283
|
-
const laneNum = `L${
|
|
284
|
-
const taskPart = info.currentTaskIndex ? `-T${info.currentTaskIndex
|
|
285
|
-
|
|
283
|
+
const laneNum = `L${laneIndex + 1}`;
|
|
284
|
+
const taskPart = info.currentTaskIndex ? `-T${info.currentTaskIndex}` : '';
|
|
285
|
+
const shortLaneName = laneName.substring(0, 10);
|
|
286
|
+
return `[${laneNum}${taskPart}-${shortLaneName}]`;
|
|
286
287
|
};
|
|
287
288
|
|
|
288
289
|
// Create callback for clean console output
|
|
@@ -474,7 +475,7 @@ export function listLaneFiles(tasksDir: string): LaneInfo[] {
|
|
|
474
475
|
|
|
475
476
|
const files = fs.readdirSync(tasksDir);
|
|
476
477
|
return files
|
|
477
|
-
.filter(f => f.endsWith('.json'))
|
|
478
|
+
.filter(f => f.endsWith('.json') && f !== 'flow.meta.json')
|
|
478
479
|
.sort()
|
|
479
480
|
.map(f => {
|
|
480
481
|
const filePath = safeJoin(tasksDir, f);
|
package/src/core/runner/agent.ts
CHANGED
|
@@ -126,9 +126,12 @@ async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalD
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
if (outputFormat === 'json') {
|
|
129
|
-
args.push('--format', 'json');
|
|
129
|
+
args.push('--print', '--output-format', 'json');
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Ensure non-interactive execution with automatic approvals
|
|
133
|
+
args.push('--force', '--approve-mcps');
|
|
134
|
+
|
|
132
135
|
// Add worktree context if provided
|
|
133
136
|
if (workspaceDir) {
|
|
134
137
|
args.push('--workspace', workspaceDir);
|
|
@@ -143,7 +143,8 @@ export function withContext(context: string) {
|
|
|
143
143
|
*/
|
|
144
144
|
export function laneOutput(laneName: string, message: string, isError = false): void {
|
|
145
145
|
const timestamp = `${COLORS.gray}[${formatTimestamp()}]${COLORS.reset}`;
|
|
146
|
-
const
|
|
146
|
+
const shortName = laneName.substring(0, 10).padEnd(10);
|
|
147
|
+
const laneLabel = `${COLORS.magenta}${shortName}${COLORS.reset}`;
|
|
147
148
|
const output = isError ? `${COLORS.red}${message}${COLORS.reset}` : message;
|
|
148
149
|
|
|
149
150
|
if (isError) {
|
package/src/utils/config.ts
CHANGED
|
@@ -112,12 +112,19 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Get absolute path for tasks directory
|
|
115
|
+
* Get absolute path for tasks directory (legacy)
|
|
116
116
|
*/
|
|
117
117
|
export function getTasksDir(config: CursorFlowConfig): string {
|
|
118
118
|
return safeJoin(config.projectRoot, config.tasksDir);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Get absolute path for flows directory (new architecture)
|
|
123
|
+
*/
|
|
124
|
+
export function getFlowsDir(config: CursorFlowConfig): string {
|
|
125
|
+
return safeJoin(config.projectRoot, config.flowsDir);
|
|
126
|
+
}
|
|
127
|
+
|
|
121
128
|
/**
|
|
122
129
|
* Get absolute path for logs directory
|
|
123
130
|
*/
|
package/src/utils/doctor.ts
CHANGED
|
@@ -19,6 +19,8 @@ import * as git from './git';
|
|
|
19
19
|
import { checkCursorAgentInstalled, checkCursorAuth } from './cursor-agent';
|
|
20
20
|
import { areCommandsInstalled } from '../cli/setup-commands';
|
|
21
21
|
import { safeJoin } from './path';
|
|
22
|
+
import { findFlowDir } from './flow';
|
|
23
|
+
import { loadConfig, getFlowsDir } from './config';
|
|
22
24
|
|
|
23
25
|
export type DoctorSeverity = 'error' | 'warn';
|
|
24
26
|
|
|
@@ -150,7 +152,7 @@ function branchExists(repoRoot: string, branchName: string): boolean {
|
|
|
150
152
|
function readLaneJsonFiles(tasksDir: string): { path: string; json: any; fileName: string }[] {
|
|
151
153
|
const files = fs
|
|
152
154
|
.readdirSync(tasksDir)
|
|
153
|
-
.filter(f => f.endsWith('.json'))
|
|
155
|
+
.filter(f => f.endsWith('.json') && f !== 'flow.meta.json')
|
|
154
156
|
.sort()
|
|
155
157
|
.map(f => safeJoin(tasksDir, f));
|
|
156
158
|
|
|
@@ -858,20 +860,46 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
|
|
|
858
860
|
|
|
859
861
|
// 2) Tasks-dir checks (optional; used by `cursorflow run` preflight)
|
|
860
862
|
if (options.tasksDir) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
863
|
+
// Resolve tasks dir with flow name support:
|
|
864
|
+
// 1. Absolute paths are used as-is
|
|
865
|
+
// 2. Relative paths that exist are used as-is
|
|
866
|
+
// 3. Try finding as a flow name in flowsDir
|
|
867
|
+
// 4. Fall back to relative path from cwd
|
|
868
|
+
let tasksDirAbs = '';
|
|
869
|
+
if (path.isAbsolute(options.tasksDir)) {
|
|
870
|
+
tasksDirAbs = options.tasksDir;
|
|
871
|
+
} else {
|
|
872
|
+
const relPath = safeJoin(cwd, options.tasksDir);
|
|
873
|
+
if (fs.existsSync(relPath)) {
|
|
874
|
+
tasksDirAbs = relPath;
|
|
875
|
+
} else {
|
|
876
|
+
// Try finding as a flow name
|
|
877
|
+
try {
|
|
878
|
+
const config = loadConfig(repoRoot || cwd);
|
|
879
|
+
const flowsDir = getFlowsDir(config);
|
|
880
|
+
const foundFlow = findFlowDir(flowsDir, options.tasksDir);
|
|
881
|
+
if (foundFlow) {
|
|
882
|
+
tasksDirAbs = foundFlow;
|
|
883
|
+
} else {
|
|
884
|
+
tasksDirAbs = relPath;
|
|
885
|
+
}
|
|
886
|
+
} catch {
|
|
887
|
+
tasksDirAbs = relPath;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
864
891
|
context.tasksDir = tasksDirAbs;
|
|
865
892
|
|
|
866
893
|
if (!fs.existsSync(tasksDirAbs)) {
|
|
867
894
|
addIssue(issues, {
|
|
868
895
|
id: 'tasks.missing_dir',
|
|
869
896
|
severity: 'error',
|
|
870
|
-
title: 'Tasks directory not found',
|
|
871
|
-
message: `Tasks directory does not exist: ${tasksDirAbs}`,
|
|
897
|
+
title: 'Tasks or Flow directory not found',
|
|
898
|
+
message: `Tasks/Flow directory does not exist: ${options.tasksDir} (resolved to: ${tasksDirAbs})`,
|
|
872
899
|
fixes: [
|
|
873
|
-
'Double-check the path
|
|
874
|
-
'
|
|
900
|
+
'Double-check the path or flow name you passed',
|
|
901
|
+
'Use: cursorflow new <FlowName> --lanes "lane1,lane2" to create a new flow',
|
|
902
|
+
'Or run: cursorflow init --example for legacy tasks',
|
|
875
903
|
],
|
|
876
904
|
});
|
|
877
905
|
} else {
|
|
@@ -131,12 +131,13 @@ export class EnhancedLogManager {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* Get lane-task label like [
|
|
134
|
+
* Get lane-task label like [L1-T2-lanename10]
|
|
135
135
|
*/
|
|
136
136
|
private getLaneTaskLabel(): string {
|
|
137
137
|
const laneNum = (this.session.laneIndex ?? 0) + 1;
|
|
138
138
|
const taskNum = (this.session.taskIndex ?? 0) + 1;
|
|
139
|
-
|
|
139
|
+
const shortLaneName = this.session.laneName.substring(0, 10);
|
|
140
|
+
return `L${laneNum}-T${taskNum}-${shortLaneName}`;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
/**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { safeJoin } from './path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find flow directory by name in the flows directory.
|
|
7
|
+
* Matches by exact name or by suffix (ignoring ID prefix like '001_').
|
|
8
|
+
*
|
|
9
|
+
* @param flowsDir The base flows directory (e.g., _cursorflow/flows)
|
|
10
|
+
* @param flowName The name of the flow to find
|
|
11
|
+
* @returns The absolute path to the flow directory, or null if not found
|
|
12
|
+
*/
|
|
13
|
+
export function findFlowDir(flowsDir: string, flowName: string): string | null {
|
|
14
|
+
if (!fs.existsSync(flowsDir)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const dirs = fs.readdirSync(flowsDir)
|
|
19
|
+
.filter(name => {
|
|
20
|
+
const dirPath = safeJoin(flowsDir, name);
|
|
21
|
+
try {
|
|
22
|
+
return fs.statSync(dirPath).isDirectory();
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.filter(name => {
|
|
28
|
+
// Match by exact name or by suffix (ignoring ID prefix)
|
|
29
|
+
const match = name.match(/^\d+_(.+)$/);
|
|
30
|
+
return match ? match[1] === flowName : name === flowName;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (dirs.length === 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return the most recent one (highest ID / alphabetical)
|
|
38
|
+
dirs.sort((a, b) => b.localeCompare(a));
|
|
39
|
+
return safeJoin(flowsDir, dirs[0]!);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|