@litmers/cursorflow-orchestrator 0.2.9 → 0.2.10
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/commands/cursorflow-status.md +157 -0
- package/dist/cli/add.js +118 -0
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/resume.js +67 -13
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/status.d.ts +5 -0
- package/dist/cli/status.js +500 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/core/orchestrator.js +40 -7
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/utils/task-service.js +2 -1
- package/dist/utils/task-service.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/add.ts +145 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/resume.ts +70 -14
- package/src/cli/status.ts +551 -0
- package/src/core/orchestrator.ts +44 -7
- package/src/utils/task-service.ts +2 -1
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CursorFlow status command - View detailed lane status for a run
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as logger from '../utils/logger';
|
|
8
|
+
import { loadConfig, getLogsDir } from '../utils/config';
|
|
9
|
+
import { loadState } from '../utils/state';
|
|
10
|
+
import { LaneState } from '../types';
|
|
11
|
+
import { safeJoin } from '../utils/path';
|
|
12
|
+
import { RunService } from '../utils/run-service';
|
|
13
|
+
|
|
14
|
+
interface StatusOptions {
|
|
15
|
+
runId: string | null;
|
|
16
|
+
lane: string | null;
|
|
17
|
+
json: boolean;
|
|
18
|
+
watch: boolean;
|
|
19
|
+
help: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DetailedLaneStatus {
|
|
23
|
+
name: string;
|
|
24
|
+
status: string;
|
|
25
|
+
progress: string;
|
|
26
|
+
currentTask: number;
|
|
27
|
+
totalTasks: number;
|
|
28
|
+
branch: string | null;
|
|
29
|
+
worktree: string | null;
|
|
30
|
+
pid: number | null;
|
|
31
|
+
processAlive: boolean;
|
|
32
|
+
startTime: number | null;
|
|
33
|
+
endTime: number | null;
|
|
34
|
+
duration: string;
|
|
35
|
+
error: string | null;
|
|
36
|
+
waitingFor: string[];
|
|
37
|
+
chatId: string | null;
|
|
38
|
+
completedTasks: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface RunStatusSummary {
|
|
42
|
+
runId: string;
|
|
43
|
+
runDir: string;
|
|
44
|
+
taskName: string;
|
|
45
|
+
status: string;
|
|
46
|
+
startTime: number;
|
|
47
|
+
duration: string;
|
|
48
|
+
lanes: DetailedLaneStatus[];
|
|
49
|
+
summary: {
|
|
50
|
+
total: number;
|
|
51
|
+
running: number;
|
|
52
|
+
completed: number;
|
|
53
|
+
failed: number;
|
|
54
|
+
pending: number;
|
|
55
|
+
waiting: number;
|
|
56
|
+
paused: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Status indicator colors
|
|
62
|
+
*/
|
|
63
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
64
|
+
completed: '\x1b[32m', // green
|
|
65
|
+
running: '\x1b[36m', // cyan
|
|
66
|
+
pending: '\x1b[33m', // yellow
|
|
67
|
+
failed: '\x1b[31m', // red
|
|
68
|
+
paused: '\x1b[35m', // magenta
|
|
69
|
+
waiting: '\x1b[33m', // yellow
|
|
70
|
+
unknown: '\x1b[90m', // gray
|
|
71
|
+
};
|
|
72
|
+
const RESET = '\x1b[0m';
|
|
73
|
+
const BOLD = '\x1b[1m';
|
|
74
|
+
const DIM = '\x1b[2m';
|
|
75
|
+
|
|
76
|
+
function printHelp(): void {
|
|
77
|
+
console.log(`
|
|
78
|
+
Usage: cursorflow status [run-id] [options]
|
|
79
|
+
|
|
80
|
+
View detailed status of lanes in a run.
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
[run-id] Run ID (default: latest run)
|
|
84
|
+
--lane <name> Show detailed status for specific lane
|
|
85
|
+
--json Output as JSON
|
|
86
|
+
--watch Watch mode - refresh every 2 seconds
|
|
87
|
+
--help, -h Show help
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
cursorflow status # Status of latest run
|
|
91
|
+
cursorflow status run-1234567890 # Status of specific run
|
|
92
|
+
cursorflow status --lane backend # Detailed status of specific lane
|
|
93
|
+
cursorflow status --json # JSON output for scripting
|
|
94
|
+
cursorflow status --watch # Live status updates
|
|
95
|
+
`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseArgs(args: string[]): StatusOptions {
|
|
99
|
+
const laneIdx = args.indexOf('--lane');
|
|
100
|
+
|
|
101
|
+
// Find run-id (first arg that starts with 'run-' or isn't an option)
|
|
102
|
+
let runId: string | null = null;
|
|
103
|
+
for (const arg of args) {
|
|
104
|
+
if (arg.startsWith('run-')) {
|
|
105
|
+
runId = arg;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (!arg.startsWith('--') && !arg.startsWith('-') && args.indexOf(arg) === 0) {
|
|
109
|
+
// First positional argument could be run-id or 'latest'
|
|
110
|
+
if (arg !== 'latest') {
|
|
111
|
+
runId = arg;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
runId,
|
|
119
|
+
lane: laneIdx >= 0 ? args[laneIdx + 1] || null : null,
|
|
120
|
+
json: args.includes('--json'),
|
|
121
|
+
watch: args.includes('--watch'),
|
|
122
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if a process is alive by its PID
|
|
128
|
+
*/
|
|
129
|
+
function isProcessAlive(pid: number): boolean {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format duration in human readable form
|
|
140
|
+
*/
|
|
141
|
+
function formatDuration(ms: number): string {
|
|
142
|
+
if (ms < 0) return '-';
|
|
143
|
+
|
|
144
|
+
const seconds = Math.floor(ms / 1000);
|
|
145
|
+
const minutes = Math.floor(seconds / 60);
|
|
146
|
+
const hours = Math.floor(minutes / 60);
|
|
147
|
+
|
|
148
|
+
if (hours > 0) {
|
|
149
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
150
|
+
} else if (minutes > 0) {
|
|
151
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
152
|
+
} else {
|
|
153
|
+
return `${seconds}s`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Find the latest run directory
|
|
159
|
+
*/
|
|
160
|
+
function findLatestRunDir(logsDir: string): string | null {
|
|
161
|
+
const runsDir = safeJoin(logsDir, 'runs');
|
|
162
|
+
if (!fs.existsSync(runsDir)) return null;
|
|
163
|
+
|
|
164
|
+
const runs = fs.readdirSync(runsDir)
|
|
165
|
+
.filter(d => d.startsWith('run-'))
|
|
166
|
+
.sort()
|
|
167
|
+
.reverse();
|
|
168
|
+
|
|
169
|
+
return runs.length > 0 ? runs[0]! : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get detailed lane status from lane directory
|
|
174
|
+
*/
|
|
175
|
+
function getDetailedLaneStatus(laneDir: string, laneName: string): DetailedLaneStatus {
|
|
176
|
+
const statePath = safeJoin(laneDir, 'state.json');
|
|
177
|
+
const state = loadState<LaneState>(statePath);
|
|
178
|
+
|
|
179
|
+
if (!state) {
|
|
180
|
+
return {
|
|
181
|
+
name: laneName,
|
|
182
|
+
status: 'pending',
|
|
183
|
+
progress: '0%',
|
|
184
|
+
currentTask: 0,
|
|
185
|
+
totalTasks: 0,
|
|
186
|
+
branch: null,
|
|
187
|
+
worktree: null,
|
|
188
|
+
pid: null,
|
|
189
|
+
processAlive: false,
|
|
190
|
+
startTime: null,
|
|
191
|
+
endTime: null,
|
|
192
|
+
duration: '-',
|
|
193
|
+
error: null,
|
|
194
|
+
waitingFor: [],
|
|
195
|
+
chatId: null,
|
|
196
|
+
completedTasks: [],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const progress = state.totalTasks > 0
|
|
201
|
+
? Math.round((state.currentTaskIndex / state.totalTasks) * 100)
|
|
202
|
+
: 0;
|
|
203
|
+
|
|
204
|
+
const processAlive = state.pid ? isProcessAlive(state.pid) : false;
|
|
205
|
+
|
|
206
|
+
let duration = 0;
|
|
207
|
+
if (state.startTime) {
|
|
208
|
+
if (state.endTime) {
|
|
209
|
+
duration = state.endTime - state.startTime;
|
|
210
|
+
} else if (state.status === 'running') {
|
|
211
|
+
duration = Date.now() - state.startTime;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
name: laneName,
|
|
217
|
+
status: state.status || 'unknown',
|
|
218
|
+
progress: `${progress}%`,
|
|
219
|
+
currentTask: state.currentTaskIndex || 0,
|
|
220
|
+
totalTasks: state.totalTasks || 0,
|
|
221
|
+
branch: state.pipelineBranch,
|
|
222
|
+
worktree: state.worktreeDir,
|
|
223
|
+
pid: state.pid || null,
|
|
224
|
+
processAlive,
|
|
225
|
+
startTime: state.startTime || null,
|
|
226
|
+
endTime: state.endTime || null,
|
|
227
|
+
duration: formatDuration(duration),
|
|
228
|
+
error: state.error,
|
|
229
|
+
waitingFor: state.waitingFor || [],
|
|
230
|
+
chatId: state.chatId || null,
|
|
231
|
+
completedTasks: state.completedTasks || [],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get full run status with all lanes
|
|
237
|
+
*/
|
|
238
|
+
function getRunStatus(runDir: string, runId: string): RunStatusSummary {
|
|
239
|
+
const lanesDir = safeJoin(runDir, 'lanes');
|
|
240
|
+
const statePath = safeJoin(runDir, 'state.json');
|
|
241
|
+
|
|
242
|
+
let taskName = 'Unknown';
|
|
243
|
+
let runStartTime = 0;
|
|
244
|
+
|
|
245
|
+
// Try to get task name from orchestrator state
|
|
246
|
+
if (fs.existsSync(statePath)) {
|
|
247
|
+
try {
|
|
248
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
249
|
+
taskName = state.taskName || 'Unknown';
|
|
250
|
+
runStartTime = state.startTime || extractTimestampFromRunId(runId);
|
|
251
|
+
} catch {
|
|
252
|
+
runStartTime = extractTimestampFromRunId(runId);
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
runStartTime = extractTimestampFromRunId(runId);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const lanes: DetailedLaneStatus[] = [];
|
|
259
|
+
const summary = {
|
|
260
|
+
total: 0,
|
|
261
|
+
running: 0,
|
|
262
|
+
completed: 0,
|
|
263
|
+
failed: 0,
|
|
264
|
+
pending: 0,
|
|
265
|
+
waiting: 0,
|
|
266
|
+
paused: 0,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (fs.existsSync(lanesDir)) {
|
|
270
|
+
const laneDirs = fs.readdirSync(lanesDir)
|
|
271
|
+
.filter(f => fs.statSync(safeJoin(lanesDir, f)).isDirectory())
|
|
272
|
+
.sort();
|
|
273
|
+
|
|
274
|
+
for (const laneName of laneDirs) {
|
|
275
|
+
const laneDir = safeJoin(lanesDir, laneName);
|
|
276
|
+
const laneStatus = getDetailedLaneStatus(laneDir, laneName);
|
|
277
|
+
lanes.push(laneStatus);
|
|
278
|
+
|
|
279
|
+
summary.total++;
|
|
280
|
+
switch (laneStatus.status) {
|
|
281
|
+
case 'running': summary.running++; break;
|
|
282
|
+
case 'completed': summary.completed++; break;
|
|
283
|
+
case 'failed': summary.failed++; break;
|
|
284
|
+
case 'pending': summary.pending++; break;
|
|
285
|
+
case 'waiting': summary.waiting++; break;
|
|
286
|
+
case 'paused': summary.paused++; break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Calculate overall run status
|
|
292
|
+
let runStatus = 'pending';
|
|
293
|
+
if (summary.running > 0) {
|
|
294
|
+
runStatus = 'running';
|
|
295
|
+
} else if (summary.completed === summary.total && summary.total > 0) {
|
|
296
|
+
runStatus = 'completed';
|
|
297
|
+
} else if (summary.failed > 0) {
|
|
298
|
+
runStatus = summary.completed > 0 ? 'partial' : 'failed';
|
|
299
|
+
} else if (summary.waiting > 0 || summary.paused > 0) {
|
|
300
|
+
runStatus = 'waiting';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Calculate run duration
|
|
304
|
+
let runDuration = 0;
|
|
305
|
+
if (runStartTime) {
|
|
306
|
+
const latestEndTime = lanes
|
|
307
|
+
.filter(l => l.endTime)
|
|
308
|
+
.reduce((max, l) => Math.max(max, l.endTime!), 0);
|
|
309
|
+
|
|
310
|
+
if (runStatus === 'completed' || runStatus === 'failed') {
|
|
311
|
+
runDuration = latestEndTime - runStartTime;
|
|
312
|
+
} else {
|
|
313
|
+
runDuration = Date.now() - runStartTime;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
runId,
|
|
319
|
+
runDir,
|
|
320
|
+
taskName,
|
|
321
|
+
status: runStatus,
|
|
322
|
+
startTime: runStartTime,
|
|
323
|
+
duration: formatDuration(runDuration),
|
|
324
|
+
lanes,
|
|
325
|
+
summary,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function extractTimestampFromRunId(runId: string): number {
|
|
330
|
+
const match = runId.match(/run-(\d+)/);
|
|
331
|
+
return match ? parseInt(match[1], 10) : Date.now();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Print lane status table
|
|
336
|
+
*/
|
|
337
|
+
function printLaneTable(runStatus: RunStatusSummary): void {
|
|
338
|
+
const { lanes, summary, runId, taskName, status, duration, startTime } = runStatus;
|
|
339
|
+
|
|
340
|
+
// Header
|
|
341
|
+
console.log('');
|
|
342
|
+
console.log(`${BOLD}📊 Run Status${RESET}`);
|
|
343
|
+
console.log(`${'─'.repeat(80)}`);
|
|
344
|
+
console.log(` ${DIM}Run ID:${RESET} ${runId}`);
|
|
345
|
+
console.log(` ${DIM}Task:${RESET} ${taskName}`);
|
|
346
|
+
console.log(` ${DIM}Status:${RESET} ${STATUS_COLORS[status] || ''}${status}${RESET}`);
|
|
347
|
+
console.log(` ${DIM}Started:${RESET} ${startTime ? new Date(startTime).toLocaleString() : '-'}`);
|
|
348
|
+
console.log(` ${DIM}Duration:${RESET} ${duration}`);
|
|
349
|
+
console.log('');
|
|
350
|
+
|
|
351
|
+
// Summary bar
|
|
352
|
+
const barWidth = 40;
|
|
353
|
+
const completedWidth = Math.round((summary.completed / summary.total) * barWidth) || 0;
|
|
354
|
+
const runningWidth = Math.round((summary.running / summary.total) * barWidth) || 0;
|
|
355
|
+
const failedWidth = Math.round((summary.failed / summary.total) * barWidth) || 0;
|
|
356
|
+
const remainingWidth = barWidth - completedWidth - runningWidth - failedWidth;
|
|
357
|
+
|
|
358
|
+
const progressBar =
|
|
359
|
+
'\x1b[42m' + ' '.repeat(completedWidth) + '\x1b[0m' + // green for completed
|
|
360
|
+
'\x1b[46m' + ' '.repeat(runningWidth) + '\x1b[0m' + // cyan for running
|
|
361
|
+
'\x1b[41m' + ' '.repeat(failedWidth) + '\x1b[0m' + // red for failed
|
|
362
|
+
'\x1b[47m' + ' '.repeat(remainingWidth) + '\x1b[0m'; // white for pending
|
|
363
|
+
|
|
364
|
+
console.log(` [${progressBar}] ${summary.completed}/${summary.total} completed`);
|
|
365
|
+
console.log('');
|
|
366
|
+
|
|
367
|
+
if (lanes.length === 0) {
|
|
368
|
+
console.log(' No lanes found.');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Table header
|
|
373
|
+
console.log(`${BOLD}${'─'.repeat(80)}${RESET}`);
|
|
374
|
+
console.log(` ${'Lane'.padEnd(20)} ${'Status'.padEnd(12)} ${'Progress'.padEnd(10)} ${'Duration'.padEnd(12)} ${'PID'.padEnd(8)} Branch`);
|
|
375
|
+
console.log(`${'─'.repeat(80)}`);
|
|
376
|
+
|
|
377
|
+
// Lane rows
|
|
378
|
+
for (const lane of lanes) {
|
|
379
|
+
const statusColor = STATUS_COLORS[lane.status] || STATUS_COLORS.unknown;
|
|
380
|
+
const progressStr = `${lane.currentTask}/${lane.totalTasks}`;
|
|
381
|
+
const pidStr = lane.pid
|
|
382
|
+
? (lane.processAlive ? `${lane.pid}` : `${DIM}${lane.pid}†${RESET}`)
|
|
383
|
+
: '-';
|
|
384
|
+
const branchStr = lane.branch || '-';
|
|
385
|
+
|
|
386
|
+
console.log(` ${lane.name.padEnd(20)} ${statusColor}${lane.status.padEnd(12)}${RESET} ${progressStr.padEnd(10)} ${lane.duration.padEnd(12)} ${pidStr.padEnd(8)} ${branchStr}`);
|
|
387
|
+
|
|
388
|
+
// Show error if failed
|
|
389
|
+
if (lane.status === 'failed' && lane.error) {
|
|
390
|
+
const errorMsg = lane.error.length > 60 ? lane.error.substring(0, 57) + '...' : lane.error;
|
|
391
|
+
console.log(` ${' '.repeat(20)} ${DIM}└─ \x1b[31m${errorMsg}${RESET}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Show waiting dependencies
|
|
395
|
+
if (lane.waitingFor && lane.waitingFor.length > 0) {
|
|
396
|
+
console.log(` ${' '.repeat(20)} ${DIM}└─ Waiting: ${lane.waitingFor.join(', ')}${RESET}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
console.log(`${'─'.repeat(80)}`);
|
|
401
|
+
|
|
402
|
+
// Summary
|
|
403
|
+
console.log('');
|
|
404
|
+
const summaryParts = [];
|
|
405
|
+
if (summary.completed > 0) summaryParts.push(`\x1b[32m${summary.completed} completed\x1b[0m`);
|
|
406
|
+
if (summary.running > 0) summaryParts.push(`\x1b[36m${summary.running} running\x1b[0m`);
|
|
407
|
+
if (summary.failed > 0) summaryParts.push(`\x1b[31m${summary.failed} failed\x1b[0m`);
|
|
408
|
+
if (summary.pending > 0) summaryParts.push(`\x1b[33m${summary.pending} pending\x1b[0m`);
|
|
409
|
+
if (summary.waiting > 0) summaryParts.push(`\x1b[33m${summary.waiting} waiting\x1b[0m`);
|
|
410
|
+
if (summary.paused > 0) summaryParts.push(`\x1b[35m${summary.paused} paused\x1b[0m`);
|
|
411
|
+
|
|
412
|
+
console.log(` ${BOLD}Summary:${RESET} ${summaryParts.join(' | ')}`);
|
|
413
|
+
|
|
414
|
+
// Tips
|
|
415
|
+
if (summary.failed > 0 || summary.paused > 0) {
|
|
416
|
+
console.log('');
|
|
417
|
+
console.log(` ${DIM}Tip: Run \x1b[32mcursorflow resume --all\x1b[0m${DIM} to resume incomplete lanes${RESET}`);
|
|
418
|
+
}
|
|
419
|
+
if (summary.running > 0) {
|
|
420
|
+
console.log('');
|
|
421
|
+
console.log(` ${DIM}Tip: Run \x1b[32mcursorflow monitor\x1b[0m${DIM} for interactive monitoring${RESET}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Print detailed status for a single lane
|
|
427
|
+
*/
|
|
428
|
+
function printDetailedLaneStatus(laneStatus: DetailedLaneStatus, runId: string): void {
|
|
429
|
+
console.log('');
|
|
430
|
+
console.log(`${BOLD}🔍 Lane Details: ${laneStatus.name}${RESET}`);
|
|
431
|
+
console.log(`${'─'.repeat(60)}`);
|
|
432
|
+
console.log(` ${DIM}Run:${RESET} ${runId}`);
|
|
433
|
+
console.log(` ${DIM}Status:${RESET} ${STATUS_COLORS[laneStatus.status] || ''}${laneStatus.status}${RESET}`);
|
|
434
|
+
console.log(` ${DIM}Progress:${RESET} ${laneStatus.currentTask}/${laneStatus.totalTasks} (${laneStatus.progress})`);
|
|
435
|
+
console.log(` ${DIM}Branch:${RESET} ${laneStatus.branch || '-'}`);
|
|
436
|
+
console.log(` ${DIM}Worktree:${RESET} ${laneStatus.worktree || '-'}`);
|
|
437
|
+
console.log(` ${DIM}PID:${RESET} ${laneStatus.pid || '-'}${laneStatus.pid && !laneStatus.processAlive ? ' (dead)' : ''}`);
|
|
438
|
+
console.log(` ${DIM}Duration:${RESET} ${laneStatus.duration}`);
|
|
439
|
+
console.log(` ${DIM}Started:${RESET} ${laneStatus.startTime ? new Date(laneStatus.startTime).toLocaleString() : '-'}`);
|
|
440
|
+
console.log(` ${DIM}Ended:${RESET} ${laneStatus.endTime ? new Date(laneStatus.endTime).toLocaleString() : '-'}`);
|
|
441
|
+
console.log(` ${DIM}Chat ID:${RESET} ${laneStatus.chatId || '-'}`);
|
|
442
|
+
|
|
443
|
+
if (laneStatus.waitingFor && laneStatus.waitingFor.length > 0) {
|
|
444
|
+
console.log(` ${DIM}Waiting For:${RESET} ${laneStatus.waitingFor.join(', ')}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (laneStatus.completedTasks && laneStatus.completedTasks.length > 0) {
|
|
448
|
+
console.log(` ${DIM}Completed Tasks:${RESET} ${laneStatus.completedTasks.join(', ')}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (laneStatus.error) {
|
|
452
|
+
console.log('');
|
|
453
|
+
console.log(` ${BOLD}\x1b[31mError:${RESET}`);
|
|
454
|
+
console.log(` ${laneStatus.error}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
console.log(`${'─'.repeat(60)}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Clear console and move cursor to top
|
|
462
|
+
*/
|
|
463
|
+
function clearScreen(): void {
|
|
464
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function status(args: string[]): Promise<void> {
|
|
468
|
+
const options = parseArgs(args);
|
|
469
|
+
|
|
470
|
+
if (options.help) {
|
|
471
|
+
printHelp();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const config = loadConfig();
|
|
476
|
+
const logsDir = getLogsDir(config);
|
|
477
|
+
const runsDir = safeJoin(logsDir, 'runs');
|
|
478
|
+
|
|
479
|
+
// Find run directory
|
|
480
|
+
let runId = options.runId;
|
|
481
|
+
if (!runId) {
|
|
482
|
+
runId = findLatestRunDir(logsDir);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!runId) {
|
|
486
|
+
logger.warn('No runs found. Run a flow first with: cursorflow run <flow-name>');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Support both run ID and full path
|
|
491
|
+
let runDir = runId.includes(path.sep) ? runId : safeJoin(runsDir, runId);
|
|
492
|
+
if (!runId.startsWith('run-') && !fs.existsSync(runDir)) {
|
|
493
|
+
// Try adding run- prefix
|
|
494
|
+
runDir = safeJoin(runsDir, `run-${runId}`);
|
|
495
|
+
runId = `run-${runId}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!fs.existsSync(runDir)) {
|
|
499
|
+
throw new Error(`Run not found: ${runId}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Extract just the run ID from the path if needed
|
|
503
|
+
runId = path.basename(runDir);
|
|
504
|
+
|
|
505
|
+
const displayStatus = () => {
|
|
506
|
+
const runStatus = getRunStatus(runDir, runId!);
|
|
507
|
+
|
|
508
|
+
if (options.json) {
|
|
509
|
+
console.log(JSON.stringify(runStatus, null, 2));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (options.lane) {
|
|
514
|
+
const lane = runStatus.lanes.find(l => l.name === options.lane);
|
|
515
|
+
if (!lane) {
|
|
516
|
+
throw new Error(`Lane '${options.lane}' not found in run ${runId}`);
|
|
517
|
+
}
|
|
518
|
+
printDetailedLaneStatus(lane, runId!);
|
|
519
|
+
} else {
|
|
520
|
+
printLaneTable(runStatus);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
if (options.watch) {
|
|
525
|
+
// Watch mode
|
|
526
|
+
const refreshInterval = 2000;
|
|
527
|
+
|
|
528
|
+
const refresh = () => {
|
|
529
|
+
clearScreen();
|
|
530
|
+
console.log(`${DIM}[Auto-refresh every ${refreshInterval/1000}s - Press Ctrl+C to exit]${RESET}`);
|
|
531
|
+
displayStatus();
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
refresh();
|
|
535
|
+
const interval = setInterval(refresh, refreshInterval);
|
|
536
|
+
|
|
537
|
+
// Handle Ctrl+C gracefully
|
|
538
|
+
process.on('SIGINT', () => {
|
|
539
|
+
clearInterval(interval);
|
|
540
|
+
console.log('\n');
|
|
541
|
+
process.exit(0);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Keep process alive
|
|
545
|
+
await new Promise(() => {});
|
|
546
|
+
} else {
|
|
547
|
+
displayStatus();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export = status;
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -977,23 +977,60 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
977
977
|
printLaneStatus(lanes, laneRunDirs);
|
|
978
978
|
}, options.pollInterval || 60000);
|
|
979
979
|
|
|
980
|
-
// Handle process interruption
|
|
981
|
-
const
|
|
982
|
-
logger.warn(
|
|
980
|
+
// Handle process interruption - ensure proper state cleanup
|
|
981
|
+
const handleGracefulShutdown = async (signal: string) => {
|
|
982
|
+
logger.warn(`\n⚠️ Orchestration interrupted by ${signal}! Stopping all lanes...`);
|
|
983
|
+
|
|
984
|
+
// 1. Stop running lanes and update their state to 'paused'
|
|
983
985
|
for (const [name, info] of running.entries()) {
|
|
984
986
|
logger.info(`Stopping lane: ${name}`);
|
|
985
987
|
try {
|
|
988
|
+
// Update state to 'paused' (not 'completed'!) before killing
|
|
989
|
+
const state = loadState<LaneState>(info.statePath);
|
|
990
|
+
if (state && state.status === 'running') {
|
|
991
|
+
state.status = 'paused';
|
|
992
|
+
state.error = `Orchestration interrupted by ${signal}`;
|
|
993
|
+
state.endTime = Date.now();
|
|
994
|
+
saveState(info.statePath, state);
|
|
995
|
+
}
|
|
996
|
+
|
|
986
997
|
info.child.kill('SIGTERM');
|
|
987
|
-
} catch {
|
|
988
|
-
// Ignore kill errors
|
|
998
|
+
} catch (e) {
|
|
999
|
+
// Ignore kill errors but log them for debugging
|
|
1000
|
+
logger.debug(`Error stopping lane ${name}: ${e}`);
|
|
989
1001
|
}
|
|
990
1002
|
}
|
|
1003
|
+
|
|
1004
|
+
// 2. Update any pending (not started) lanes to 'pending' with clear status
|
|
1005
|
+
for (const lane of lanes) {
|
|
1006
|
+
if (!completedLanes.has(lane.name) && !failedLanes.has(lane.name) && !running.has(lane.name)) {
|
|
1007
|
+
const statePath = safeJoin(laneRunDirs[lane.name]!, 'state.json');
|
|
1008
|
+
try {
|
|
1009
|
+
const state = loadState<LaneState>(statePath);
|
|
1010
|
+
if (state) {
|
|
1011
|
+
// Only update if not already completed/failed
|
|
1012
|
+
if (state.status !== 'completed' && state.status !== 'failed') {
|
|
1013
|
+
state.status = 'pending';
|
|
1014
|
+
state.error = `Orchestration interrupted before lane started`;
|
|
1015
|
+
saveState(statePath, state);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
} catch {
|
|
1019
|
+
// State file might not exist yet
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
991
1024
|
printLaneStatus(lanes, laneRunDirs);
|
|
1025
|
+
logger.info('\n💡 To resume: cursorflow resume --all');
|
|
992
1026
|
process.exit(130);
|
|
993
1027
|
};
|
|
994
1028
|
|
|
1029
|
+
const sigIntHandler = () => handleGracefulShutdown('SIGINT');
|
|
1030
|
+
const sigTermHandler = () => handleGracefulShutdown('SIGTERM');
|
|
1031
|
+
|
|
995
1032
|
process.on('SIGINT', sigIntHandler);
|
|
996
|
-
process.on('SIGTERM',
|
|
1033
|
+
process.on('SIGTERM', sigTermHandler);
|
|
997
1034
|
|
|
998
1035
|
let lastStallCheck = Date.now();
|
|
999
1036
|
|
|
@@ -1313,7 +1350,7 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
1313
1350
|
} finally {
|
|
1314
1351
|
clearInterval(monitorInterval);
|
|
1315
1352
|
process.removeListener('SIGINT', sigIntHandler);
|
|
1316
|
-
process.removeListener('SIGTERM',
|
|
1353
|
+
process.removeListener('SIGTERM', sigTermHandler);
|
|
1317
1354
|
}
|
|
1318
1355
|
|
|
1319
1356
|
printLaneStatus(lanes, laneRunDirs);
|
|
@@ -287,7 +287,8 @@ export class TaskService {
|
|
|
287
287
|
try {
|
|
288
288
|
const filePath = path.join(taskInfo.path, lane.fileName);
|
|
289
289
|
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
290
|
-
|
|
290
|
+
// Use the extracted laneName which already handles numeric prefix removal
|
|
291
|
+
const laneName = lane.laneName;
|
|
291
292
|
|
|
292
293
|
if (Array.isArray(content.tasks)) {
|
|
293
294
|
for (const task of content.tasks) {
|