@litmers/cursorflow-orchestrator 0.1.6 ā 0.1.8
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/dist/cli/monitor.d.ts +1 -1
- package/dist/cli/monitor.js +640 -145
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/run.js +1 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/core/orchestrator.d.ts +4 -2
- package/dist/core/orchestrator.js +92 -23
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner.d.ts +6 -2
- package/dist/core/runner.js +128 -58
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/cli/monitor.ts +693 -185
- package/src/cli/run.ts +1 -0
- package/src/core/orchestrator.ts +102 -27
- package/src/core/runner.ts +147 -70
- package/src/utils/types.ts +2 -0
package/src/cli/monitor.ts
CHANGED
|
@@ -1,229 +1,737 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CursorFlow monitor command
|
|
2
|
+
* CursorFlow interactive monitor command
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
+
import * as readline from 'readline';
|
|
7
8
|
import * as logger from '../utils/logger';
|
|
8
|
-
import { loadState } from '../utils/state';
|
|
9
|
-
import { LaneState } from '../utils/types';
|
|
9
|
+
import { loadState, readLog } from '../utils/state';
|
|
10
|
+
import { LaneState, ConversationEntry } from '../utils/types';
|
|
10
11
|
import { loadConfig } from '../utils/config';
|
|
11
12
|
|
|
13
|
+
interface LaneWithDeps {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
dependsOn: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
interface MonitorOptions {
|
|
13
20
|
runDir?: string;
|
|
14
|
-
watch: boolean;
|
|
15
21
|
interval: number;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
runDir,
|
|
28
|
-
watch,
|
|
29
|
-
interval,
|
|
30
|
-
};
|
|
24
|
+
enum View {
|
|
25
|
+
LIST,
|
|
26
|
+
LANE_DETAIL,
|
|
27
|
+
MESSAGE_DETAIL,
|
|
28
|
+
FLOW,
|
|
29
|
+
TERMINAL,
|
|
30
|
+
INTERVENE
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
class InteractiveMonitor {
|
|
34
|
+
private runDir: string;
|
|
35
|
+
private interval: number;
|
|
36
|
+
private view: View = View.LIST;
|
|
37
|
+
private selectedLaneIndex: number = 0;
|
|
38
|
+
private selectedMessageIndex: number = 0;
|
|
39
|
+
private selectedLaneName: string | null = null;
|
|
40
|
+
private lanes: LaneWithDeps[] = [];
|
|
41
|
+
private currentLogs: ConversationEntry[] = [];
|
|
42
|
+
private timer: NodeJS.Timeout | null = null;
|
|
43
|
+
private scrollOffset: number = 0;
|
|
44
|
+
private terminalScrollOffset: number = 0;
|
|
45
|
+
private lastTerminalTotalLines: number = 0;
|
|
46
|
+
private interventionInput: string = '';
|
|
47
|
+
|
|
48
|
+
constructor(runDir: string, interval: number) {
|
|
49
|
+
this.runDir = runDir;
|
|
50
|
+
this.interval = interval;
|
|
41
51
|
}
|
|
42
|
-
|
|
43
|
-
const runs = fs.readdirSync(runsDir)
|
|
44
|
-
.filter(d => d.startsWith('run-'))
|
|
45
|
-
.map(d => ({
|
|
46
|
-
name: d,
|
|
47
|
-
path: path.join(runsDir, d),
|
|
48
|
-
mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime(),
|
|
49
|
-
}))
|
|
50
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
51
|
-
|
|
52
|
-
return runs.length > 0 ? runs[0]!.path : null;
|
|
53
|
-
}
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const lanesDir = path.join(runDir, 'lanes');
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(lanesDir)) {
|
|
62
|
-
return [];
|
|
53
|
+
public async start() {
|
|
54
|
+
this.setupTerminal();
|
|
55
|
+
this.refresh();
|
|
56
|
+
this.timer = setInterval(() => this.refresh(), this.interval * 1000);
|
|
63
57
|
}
|
|
64
|
-
|
|
65
|
-
return fs.readdirSync(lanesDir)
|
|
66
|
-
.filter(d => {
|
|
67
|
-
const stat = fs.statSync(path.join(lanesDir, d));
|
|
68
|
-
return stat.isDirectory();
|
|
69
|
-
})
|
|
70
|
-
.map(name => ({
|
|
71
|
-
name,
|
|
72
|
-
path: path.join(lanesDir, name),
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
58
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
59
|
+
private setupTerminal() {
|
|
60
|
+
if (process.stdin.isTTY) {
|
|
61
|
+
process.stdin.setRawMode(true);
|
|
62
|
+
}
|
|
63
|
+
readline.emitKeypressEvents(process.stdin);
|
|
64
|
+
process.stdin.on('keypress', (str, key) => {
|
|
65
|
+
// Handle Ctrl+C
|
|
66
|
+
if (key && key.ctrl && key.name === 'c') {
|
|
67
|
+
this.stop();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Safeguard against missing key object
|
|
72
|
+
const keyName = key ? key.name : str;
|
|
73
|
+
|
|
74
|
+
if (this.view === View.LIST) {
|
|
75
|
+
this.handleListKey(keyName);
|
|
76
|
+
} else if (this.view === View.LANE_DETAIL) {
|
|
77
|
+
this.handleDetailKey(keyName);
|
|
78
|
+
} else if (this.view === View.FLOW) {
|
|
79
|
+
this.handleFlowKey(keyName);
|
|
80
|
+
} else if (this.view === View.TERMINAL) {
|
|
81
|
+
this.handleTerminalKey(keyName);
|
|
82
|
+
} else if (this.view === View.INTERVENE) {
|
|
83
|
+
this.handleInterveneKey(str, key);
|
|
84
|
+
} else {
|
|
85
|
+
this.handleMessageDetailKey(keyName);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Hide cursor
|
|
90
|
+
process.stdout.write('\x1B[?25l');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private stop() {
|
|
94
|
+
if (this.timer) clearInterval(this.timer);
|
|
95
|
+
// Show cursor and clear screen
|
|
96
|
+
process.stdout.write('\x1B[?25h');
|
|
97
|
+
process.stdout.write('\x1Bc');
|
|
98
|
+
console.log('\nš Monitoring stopped\n');
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private handleListKey(key: string) {
|
|
103
|
+
switch (key) {
|
|
104
|
+
case 'up':
|
|
105
|
+
this.selectedLaneIndex = Math.max(0, this.selectedLaneIndex - 1);
|
|
106
|
+
this.render();
|
|
107
|
+
break;
|
|
108
|
+
case 'down':
|
|
109
|
+
this.selectedLaneIndex = Math.min(this.lanes.length - 1, this.selectedLaneIndex + 1);
|
|
110
|
+
this.render();
|
|
111
|
+
break;
|
|
112
|
+
case 'right':
|
|
113
|
+
case 'return':
|
|
114
|
+
case 'enter':
|
|
115
|
+
if (this.lanes[this.selectedLaneIndex]) {
|
|
116
|
+
this.selectedLaneName = this.lanes[this.selectedLaneIndex]!.name;
|
|
117
|
+
this.view = View.LANE_DETAIL;
|
|
118
|
+
this.selectedMessageIndex = 0;
|
|
119
|
+
this.scrollOffset = 0;
|
|
120
|
+
this.refreshLogs();
|
|
121
|
+
this.render();
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
case 'left':
|
|
125
|
+
case 'f':
|
|
126
|
+
this.view = View.FLOW;
|
|
127
|
+
this.render();
|
|
128
|
+
break;
|
|
129
|
+
case 'q':
|
|
130
|
+
this.stop();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private handleDetailKey(key: string) {
|
|
136
|
+
switch (key) {
|
|
137
|
+
case 'up':
|
|
138
|
+
this.selectedMessageIndex = Math.max(0, this.selectedMessageIndex - 1);
|
|
139
|
+
this.render();
|
|
140
|
+
break;
|
|
141
|
+
case 'down':
|
|
142
|
+
this.selectedMessageIndex = Math.min(this.currentLogs.length - 1, this.selectedMessageIndex + 1);
|
|
143
|
+
this.render();
|
|
144
|
+
break;
|
|
145
|
+
case 'right':
|
|
146
|
+
case 'return':
|
|
147
|
+
case 'enter':
|
|
148
|
+
if (this.currentLogs[this.selectedMessageIndex]) {
|
|
149
|
+
this.view = View.MESSAGE_DETAIL;
|
|
150
|
+
this.render();
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
case 't':
|
|
154
|
+
this.view = View.TERMINAL;
|
|
155
|
+
this.terminalScrollOffset = 0;
|
|
156
|
+
this.render();
|
|
157
|
+
break;
|
|
158
|
+
case 'i':
|
|
159
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
160
|
+
if (lane) {
|
|
161
|
+
const status = this.getLaneStatus(lane.path, lane.name);
|
|
162
|
+
if (status.status === 'running') {
|
|
163
|
+
this.view = View.INTERVENE;
|
|
164
|
+
this.interventionInput = '';
|
|
165
|
+
this.render();
|
|
166
|
+
} else {
|
|
167
|
+
// Show a temporary message that it's only for running lanes
|
|
168
|
+
console.log(`\n\x1b[31m Intervention is only available for RUNNING lanes.\x1b[0m`);
|
|
169
|
+
setTimeout(() => this.render(), 1500);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case 'escape':
|
|
174
|
+
case 'backspace':
|
|
175
|
+
case 'left':
|
|
176
|
+
this.view = View.LIST;
|
|
177
|
+
this.selectedLaneName = null;
|
|
178
|
+
this.render();
|
|
179
|
+
break;
|
|
180
|
+
case 'q':
|
|
181
|
+
this.stop();
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private handleMessageDetailKey(key: string) {
|
|
187
|
+
switch (key) {
|
|
188
|
+
case 'escape':
|
|
189
|
+
case 'backspace':
|
|
190
|
+
case 'left':
|
|
191
|
+
this.view = View.LANE_DETAIL;
|
|
192
|
+
this.render();
|
|
193
|
+
break;
|
|
194
|
+
case 'q':
|
|
195
|
+
this.stop();
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private handleTerminalKey(key: string) {
|
|
201
|
+
switch (key) {
|
|
202
|
+
case 'up':
|
|
203
|
+
this.terminalScrollOffset++;
|
|
204
|
+
this.render();
|
|
205
|
+
break;
|
|
206
|
+
case 'down':
|
|
207
|
+
this.terminalScrollOffset = Math.max(0, this.terminalScrollOffset - 1);
|
|
208
|
+
this.render();
|
|
209
|
+
break;
|
|
210
|
+
case 't':
|
|
211
|
+
case 'escape':
|
|
212
|
+
case 'backspace':
|
|
213
|
+
case 'left':
|
|
214
|
+
this.view = View.LANE_DETAIL;
|
|
215
|
+
this.render();
|
|
216
|
+
break;
|
|
217
|
+
case 'i':
|
|
218
|
+
this.view = View.INTERVENE;
|
|
219
|
+
this.interventionInput = '';
|
|
220
|
+
this.render();
|
|
221
|
+
break;
|
|
222
|
+
case 'q':
|
|
223
|
+
this.stop();
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private handleInterveneKey(str: string, key: any) {
|
|
229
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
230
|
+
if (this.interventionInput.trim()) {
|
|
231
|
+
this.sendIntervention(this.interventionInput.trim());
|
|
232
|
+
}
|
|
233
|
+
this.view = View.LANE_DETAIL;
|
|
234
|
+
this.render();
|
|
235
|
+
} else if (key.name === 'escape') {
|
|
236
|
+
this.view = View.LANE_DETAIL;
|
|
237
|
+
this.render();
|
|
238
|
+
} else if (key.name === 'backspace') {
|
|
239
|
+
this.interventionInput = this.interventionInput.slice(0, -1);
|
|
240
|
+
this.render();
|
|
241
|
+
} else if (str && str.length === 1) {
|
|
242
|
+
this.interventionInput += str;
|
|
243
|
+
this.render();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private sendIntervention(message: string) {
|
|
248
|
+
if (!this.selectedLaneName) return;
|
|
249
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
250
|
+
if (!lane) return;
|
|
251
|
+
|
|
252
|
+
// Write to intervention.txt which runner.ts is watching
|
|
253
|
+
const interventionPath = path.join(path.dirname(lane.path), 'intervention.txt');
|
|
254
|
+
fs.writeFileSync(interventionPath, message, 'utf8');
|
|
255
|
+
|
|
256
|
+
// Also log it to the conversation
|
|
257
|
+
const convoPath = path.join(lane.path, 'conversation.jsonl');
|
|
258
|
+
const entry = {
|
|
259
|
+
timestamp: new Date().toISOString(),
|
|
260
|
+
role: 'user',
|
|
261
|
+
task: 'INTERVENTION',
|
|
262
|
+
fullText: `[HUMAN INTERVENTION]: ${message}`,
|
|
263
|
+
textLength: message.length + 20,
|
|
264
|
+
model: 'manual'
|
|
265
|
+
};
|
|
266
|
+
fs.appendFileSync(convoPath, JSON.stringify(entry) + '\n', 'utf8');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private handleFlowKey(key: string) {
|
|
270
|
+
switch (key) {
|
|
271
|
+
case 'f':
|
|
272
|
+
case 'escape':
|
|
273
|
+
case 'backspace':
|
|
274
|
+
case 'right':
|
|
275
|
+
case 'return':
|
|
276
|
+
case 'enter':
|
|
277
|
+
case 'left':
|
|
278
|
+
this.view = View.LIST;
|
|
279
|
+
this.render();
|
|
280
|
+
break;
|
|
281
|
+
case 'q':
|
|
282
|
+
this.stop();
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private refreshLogs() {
|
|
288
|
+
if (!this.selectedLaneName) return;
|
|
289
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
290
|
+
if (!lane) return;
|
|
291
|
+
const convoPath = path.join(lane.path, 'conversation.jsonl');
|
|
292
|
+
this.currentLogs = readLog<ConversationEntry>(convoPath);
|
|
293
|
+
// Keep selection in bounds after refresh
|
|
294
|
+
if (this.selectedMessageIndex >= this.currentLogs.length) {
|
|
295
|
+
this.selectedMessageIndex = Math.max(0, this.currentLogs.length - 1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private refresh() {
|
|
300
|
+
this.lanes = this.listLanesWithDeps(this.runDir);
|
|
301
|
+
if (this.view !== View.LIST) {
|
|
302
|
+
this.refreshLogs();
|
|
303
|
+
}
|
|
304
|
+
this.render();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private render() {
|
|
308
|
+
// Clear screen
|
|
309
|
+
process.stdout.write('\x1Bc');
|
|
310
|
+
|
|
311
|
+
switch (this.view) {
|
|
312
|
+
case View.LIST:
|
|
313
|
+
this.renderList();
|
|
314
|
+
break;
|
|
315
|
+
case View.LANE_DETAIL:
|
|
316
|
+
this.renderLaneDetail();
|
|
317
|
+
break;
|
|
318
|
+
case View.MESSAGE_DETAIL:
|
|
319
|
+
this.renderMessageDetail();
|
|
320
|
+
break;
|
|
321
|
+
case View.FLOW:
|
|
322
|
+
this.renderFlow();
|
|
323
|
+
break;
|
|
324
|
+
case View.TERMINAL:
|
|
325
|
+
this.renderTerminal();
|
|
326
|
+
break;
|
|
327
|
+
case View.INTERVENE:
|
|
328
|
+
this.renderIntervene();
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private renderList() {
|
|
334
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
335
|
+
console.log(`š CursorFlow Monitor - Run: ${path.basename(this.runDir)}`);
|
|
336
|
+
console.log(`š Updated: ${new Date().toLocaleTimeString()} | [ā/ā/ā] Nav [ā] Flow [Q] Quit`);
|
|
337
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
338
|
+
|
|
339
|
+
if (this.lanes.length === 0) {
|
|
340
|
+
console.log(' No lanes found\n');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const laneStatuses: Record<string, any> = {};
|
|
345
|
+
this.lanes.forEach(l => laneStatuses[l.name] = this.getLaneStatus(l.path, l.name));
|
|
346
|
+
|
|
347
|
+
const maxNameLen = Math.max(...this.lanes.map(l => l.name.length), 15);
|
|
348
|
+
console.log(` ${'Lane'.padEnd(maxNameLen)} Status Progress Time Tasks Next Action`);
|
|
349
|
+
console.log(` ${'ā'.repeat(maxNameLen)} ${'ā'.repeat(18)} ${'ā'.repeat(8)} ${'ā'.repeat(8)} ${'ā'.repeat(6)} ${'ā'.repeat(20)}`);
|
|
350
|
+
|
|
351
|
+
this.lanes.forEach((lane, i) => {
|
|
352
|
+
const isSelected = i === this.selectedLaneIndex;
|
|
353
|
+
const status = laneStatuses[lane.name];
|
|
354
|
+
const statusIcon = this.getStatusIcon(status.status);
|
|
355
|
+
const statusText = `${statusIcon} ${status.status}`.padEnd(18);
|
|
356
|
+
const progressText = status.progress.padEnd(8);
|
|
357
|
+
const timeText = this.formatDuration(status.duration).padEnd(8);
|
|
358
|
+
|
|
359
|
+
let tasksDisplay = '-';
|
|
360
|
+
if (typeof status.totalTasks === 'number') {
|
|
361
|
+
tasksDisplay = `${status.currentTask}/${status.totalTasks}`;
|
|
362
|
+
}
|
|
363
|
+
const tasksText = tasksDisplay.padEnd(6);
|
|
364
|
+
|
|
365
|
+
// Determine "Next Action"
|
|
366
|
+
let nextAction = '-';
|
|
367
|
+
if (status.status === 'completed') {
|
|
368
|
+
const dependents = this.lanes.filter(l => laneStatuses[l.name].dependsOn.includes(lane.name));
|
|
369
|
+
if (dependents.length > 0) {
|
|
370
|
+
nextAction = `Unlock: ${dependents.map(d => d.name).join(', ')}`;
|
|
371
|
+
} else {
|
|
372
|
+
nextAction = 'š Done';
|
|
373
|
+
}
|
|
374
|
+
} else if (status.status === 'waiting') {
|
|
375
|
+
const missingDeps = status.dependsOn.filter((d: string) => laneStatuses[d] && laneStatuses[d].status !== 'completed');
|
|
376
|
+
nextAction = `Wait for: ${missingDeps.join(', ')}`;
|
|
377
|
+
} else if (status.status === 'running') {
|
|
378
|
+
nextAction = 'š Working...';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const prefix = isSelected ? ' ā¶ ' : ' ';
|
|
382
|
+
const line = `${prefix}${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${timeText} ${tasksText} ${nextAction}`;
|
|
383
|
+
|
|
384
|
+
if (isSelected) {
|
|
385
|
+
process.stdout.write(`\x1b[36m${line}\x1b[0m\n`);
|
|
386
|
+
} else {
|
|
387
|
+
process.stdout.write(`${line}\n`);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private renderLaneDetail() {
|
|
395
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
396
|
+
if (!lane) {
|
|
397
|
+
this.view = View.LIST;
|
|
398
|
+
this.render();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const status = this.getLaneStatus(lane.path, lane.name);
|
|
403
|
+
const logPath = path.join(lane.path, 'terminal.log');
|
|
404
|
+
let liveLog = '(No live terminal output)';
|
|
405
|
+
if (fs.existsSync(logPath)) {
|
|
406
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
407
|
+
liveLog = content.split('\n').slice(-15).join('\n');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
411
|
+
console.log(`š Lane: ${lane.name}`);
|
|
412
|
+
console.log(`š Updated: ${new Date().toLocaleTimeString()} | [ā/ā/ā] Browse History [Esc/ā] Back`);
|
|
413
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
414
|
+
|
|
415
|
+
process.stdout.write(` Status: ${this.getStatusIcon(status.status)} ${status.status}\n`);
|
|
416
|
+
process.stdout.write(` Progress: ${status.progress} (${status.currentTask}/${status.totalTasks} tasks)\n`);
|
|
417
|
+
process.stdout.write(` Time: ${this.formatDuration(status.duration)}\n`);
|
|
418
|
+
process.stdout.write(` Branch: ${status.pipelineBranch}\n`);
|
|
419
|
+
process.stdout.write(` Chat ID: ${status.chatId}\n`);
|
|
420
|
+
process.stdout.write(` Depends: ${status.dependsOn.join(', ') || 'None'}\n`);
|
|
421
|
+
|
|
422
|
+
if (status.error) {
|
|
423
|
+
process.stdout.write(`\x1b[31m Error: ${status.error}\x1b[0m\n`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
console.log('\nš„ļø Live Terminal Output (Last 15 lines):');
|
|
427
|
+
console.log('ā'.repeat(80));
|
|
428
|
+
console.log(`\x1b[90m${liveLog}\x1b[0m`);
|
|
429
|
+
|
|
430
|
+
console.log('\nš¬ Conversation History (Select to see full details):');
|
|
431
|
+
console.log('ā'.repeat(80));
|
|
432
|
+
process.stdout.write(' [ā/ā] Browse | [ā/Enter] Full Msg | [I] Intervene | [T] Live Terminal | [Esc/ā] Back\n\n');
|
|
433
|
+
|
|
434
|
+
if (this.currentLogs.length === 0) {
|
|
435
|
+
console.log(' (No messages yet)');
|
|
436
|
+
} else {
|
|
437
|
+
// Simple windowed view for long histories
|
|
438
|
+
const maxVisible = 15; // Number of messages to show
|
|
439
|
+
if (this.selectedMessageIndex < this.scrollOffset) {
|
|
440
|
+
this.scrollOffset = this.selectedMessageIndex;
|
|
441
|
+
} else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
|
|
442
|
+
this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const visibleLogs = this.currentLogs.slice(this.scrollOffset, this.scrollOffset + maxVisible);
|
|
446
|
+
|
|
447
|
+
visibleLogs.forEach((log, i) => {
|
|
448
|
+
const actualIndex = i + this.scrollOffset;
|
|
449
|
+
const isSelected = actualIndex === this.selectedMessageIndex;
|
|
450
|
+
const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
|
|
451
|
+
const role = log.role.toUpperCase().padEnd(10);
|
|
452
|
+
|
|
453
|
+
const prefix = isSelected ? 'ā¶ ' : ' ';
|
|
454
|
+
const header = `${prefix}${roleColor}${role}\x1b[0m [${new Date(log.timestamp).toLocaleTimeString()}]`;
|
|
455
|
+
|
|
456
|
+
if (isSelected) {
|
|
457
|
+
process.stdout.write(`\x1b[48;5;236m${header}\x1b[0m\n`);
|
|
458
|
+
} else {
|
|
459
|
+
process.stdout.write(`${header}\n`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const lines = log.fullText.split('\n').filter(l => l.trim());
|
|
463
|
+
const preview = lines[0]?.substring(0, 70) || '...';
|
|
464
|
+
process.stdout.write(` ${preview}${log.fullText.length > 70 ? '...' : ''}\n\n`);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (this.currentLogs.length > maxVisible) {
|
|
468
|
+
console.log(` -- (${this.currentLogs.length - maxVisible} more messages, use ā/ā to scroll) --`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private renderMessageDetail() {
|
|
476
|
+
const log = this.currentLogs[this.selectedMessageIndex];
|
|
477
|
+
if (!log) {
|
|
478
|
+
this.view = View.LANE_DETAIL;
|
|
479
|
+
this.render();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
484
|
+
console.log(`š Full Message Detail - ${log.role.toUpperCase()}`);
|
|
485
|
+
console.log(`š ${new Date(log.timestamp).toLocaleString()} | [Esc/ā] Back to History`);
|
|
486
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
487
|
+
|
|
488
|
+
const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
|
|
489
|
+
process.stdout.write(`${roleColor}ROLE: ${log.role.toUpperCase()}\x1b[0m\n`);
|
|
490
|
+
if (log.model) process.stdout.write(`MODEL: ${log.model}\n`);
|
|
491
|
+
if (log.task) process.stdout.write(`TASK: ${log.task}\n`);
|
|
492
|
+
console.log('ā'.repeat(40));
|
|
493
|
+
console.log(log.fullText);
|
|
494
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private renderFlow() {
|
|
498
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
499
|
+
console.log(`āļø Task Dependency Flow`);
|
|
500
|
+
console.log(`š Updated: ${new Date().toLocaleTimeString()} | [ā/Enter/Esc] Back to List`);
|
|
501
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
502
|
+
|
|
503
|
+
const laneMap = new Map<string, any>();
|
|
504
|
+
this.lanes.forEach(lane => {
|
|
505
|
+
laneMap.set(lane.name, this.getLaneStatus(lane.path, lane.name));
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Enhanced visualization with box-like structure and clear connections
|
|
509
|
+
this.lanes.forEach(lane => {
|
|
510
|
+
const status = laneMap.get(lane.name);
|
|
511
|
+
const statusIcon = this.getStatusIcon(status.status);
|
|
512
|
+
|
|
513
|
+
let statusColor = '\x1b[90m'; // Grey for pending/waiting
|
|
514
|
+
if (status.status === 'completed') statusColor = '\x1b[32m'; // Green
|
|
515
|
+
if (status.status === 'running') statusColor = '\x1b[36m'; // Cyan
|
|
516
|
+
if (status.status === 'failed') statusColor = '\x1b[31m'; // Red
|
|
517
|
+
|
|
518
|
+
// Render the node
|
|
519
|
+
const nodeText = `[ ${statusIcon} ${lane.name.padEnd(18)} ]`;
|
|
520
|
+
process.stdout.write(` ${statusColor}${nodeText}\x1b[0m`);
|
|
521
|
+
|
|
522
|
+
// Render dependencies
|
|
523
|
+
if (status.dependsOn && status.dependsOn.length > 0) {
|
|
524
|
+
process.stdout.write(` \x1b[90māāāā\x1b[0m \x1b[33m( ${status.dependsOn.join(', ')} )\x1b[0m`);
|
|
525
|
+
}
|
|
526
|
+
process.stdout.write('\n');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
console.log('\n\x1b[90m (Lanes wait for their dependencies to complete before starting)\x1b[0m');
|
|
530
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private renderTerminal() {
|
|
534
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
535
|
+
if (!lane) {
|
|
536
|
+
this.view = View.LIST;
|
|
537
|
+
this.render();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const logPath = path.join(lane.path, 'terminal.log');
|
|
542
|
+
let logLines: string[] = [];
|
|
543
|
+
if (fs.existsSync(logPath)) {
|
|
544
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
545
|
+
logLines = content.split('\n');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const maxVisible = 40;
|
|
549
|
+
const totalLines = logLines.length;
|
|
550
|
+
|
|
551
|
+
// Sticky scroll logic: if new lines arrived and we are already scrolled up,
|
|
552
|
+
// increase the offset to stay on the same content.
|
|
553
|
+
if (this.terminalScrollOffset > 0 && this.lastTerminalTotalLines > 0 && totalLines > this.lastTerminalTotalLines) {
|
|
554
|
+
this.terminalScrollOffset += (totalLines - this.lastTerminalTotalLines);
|
|
555
|
+
}
|
|
556
|
+
this.lastTerminalTotalLines = totalLines;
|
|
557
|
+
|
|
558
|
+
// Clamp scroll offset
|
|
559
|
+
const maxScroll = Math.max(0, totalLines - maxVisible);
|
|
560
|
+
if (this.terminalScrollOffset > maxScroll) {
|
|
561
|
+
this.terminalScrollOffset = maxScroll;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Slice based on scroll (0 means bottom, >0 means scrolled up)
|
|
565
|
+
const end = totalLines - this.terminalScrollOffset;
|
|
566
|
+
const start = Math.max(0, end - maxVisible);
|
|
567
|
+
const visibleLines = logLines.slice(start, end);
|
|
568
|
+
|
|
569
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
570
|
+
console.log(`š„ļø Full Live Terminal: ${lane.name}`);
|
|
571
|
+
console.log(`š Streaming... | [ā/ā] Scroll (${this.terminalScrollOffset > 0 ? `Scrolled Up ${this.terminalScrollOffset}` : 'Bottom'}) | [I] Intervene | [T/Esc/ā] Back`);
|
|
572
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
573
|
+
|
|
574
|
+
visibleLines.forEach(line => {
|
|
575
|
+
let formattedLine = line;
|
|
576
|
+
// Highlight human intervention
|
|
577
|
+
if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
|
|
578
|
+
formattedLine = `\x1b[33m\x1b[1m${line}\x1b[0m`;
|
|
579
|
+
}
|
|
580
|
+
// Highlight agent execution starts
|
|
581
|
+
else if (line.includes('Executing cursor-agent')) {
|
|
582
|
+
formattedLine = `\x1b[36m\x1b[1m${line}\x1b[0m`;
|
|
583
|
+
}
|
|
584
|
+
// Highlight task headers
|
|
585
|
+
else if (line.includes('=== Task:')) {
|
|
586
|
+
formattedLine = `\x1b[32m\x1b[1m${line}\x1b[0m`;
|
|
587
|
+
}
|
|
588
|
+
// Highlight errors
|
|
589
|
+
else if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
|
|
590
|
+
formattedLine = `\x1b[31m${line}\x1b[0m`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
process.stdout.write(` ${formattedLine}\n`);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private renderIntervene() {
|
|
600
|
+
console.clear();
|
|
601
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
602
|
+
console.log(`š HUMAN INTERVENTION: ${this.selectedLaneName}`);
|
|
603
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
604
|
+
console.log('\n Type your message to the agent. This will be sent as a direct prompt.');
|
|
605
|
+
console.log(` Press \x1b[1mENTER\x1b[0m to send, \x1b[1mESC\x1b[0m to cancel.\n`);
|
|
606
|
+
console.log(`\x1b[33m > \x1b[0m${this.interventionInput}\x1b[37mā\x1b[0m`);
|
|
607
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private listLanesWithDeps(runDir: string): LaneWithDeps[] {
|
|
611
|
+
const lanesDir = path.join(runDir, 'lanes');
|
|
612
|
+
if (!fs.existsSync(lanesDir)) return [];
|
|
613
|
+
|
|
614
|
+
const config = loadConfig();
|
|
615
|
+
const tasksDir = path.join(config.projectRoot, config.tasksDir);
|
|
616
|
+
|
|
617
|
+
const laneConfigs = this.listLaneFilesFromDir(tasksDir);
|
|
618
|
+
|
|
619
|
+
return fs.readdirSync(lanesDir)
|
|
620
|
+
.filter(d => fs.statSync(path.join(lanesDir, d)).isDirectory())
|
|
621
|
+
.map(name => {
|
|
622
|
+
const config = laneConfigs.find(c => c.name === name);
|
|
623
|
+
return {
|
|
624
|
+
name,
|
|
625
|
+
path: path.join(lanesDir, name),
|
|
626
|
+
dependsOn: config?.dependsOn || [],
|
|
627
|
+
};
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private listLaneFilesFromDir(tasksDir: string): { name: string; dependsOn: string[] }[] {
|
|
632
|
+
if (!fs.existsSync(tasksDir)) return [];
|
|
633
|
+
return fs.readdirSync(tasksDir)
|
|
634
|
+
.filter(f => f.endsWith('.json'))
|
|
635
|
+
.map(f => {
|
|
636
|
+
const filePath = path.join(tasksDir, f);
|
|
637
|
+
try {
|
|
638
|
+
const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
639
|
+
return { name: path.basename(f, '.json'), dependsOn: config.dependsOn || [] };
|
|
640
|
+
} catch {
|
|
641
|
+
return { name: path.basename(f, '.json'), dependsOn: [] };
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private getLaneStatus(lanePath: string, laneName: string) {
|
|
647
|
+
const statePath = path.join(lanePath, 'state.json');
|
|
648
|
+
const state = loadState<LaneState & { chatId?: string }>(statePath);
|
|
649
|
+
|
|
650
|
+
const laneInfo = this.lanes.find(l => l.name === laneName);
|
|
651
|
+
const dependsOn = state?.dependsOn || laneInfo?.dependsOn || [];
|
|
652
|
+
|
|
653
|
+
if (!state) {
|
|
654
|
+
return { status: 'pending', currentTask: 0, totalTasks: '?', progress: '0%', dependsOn, duration: 0, pipelineBranch: '-', chatId: '-' };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const progress = state.totalTasks > 0 ? Math.round((state.currentTaskIndex / state.totalTasks) * 100) : 0;
|
|
658
|
+
|
|
659
|
+
const duration = state.startTime ? (state.endTime
|
|
660
|
+
? state.endTime - state.startTime
|
|
661
|
+
: (state.status === 'running' || state.status === 'reviewing' ? Date.now() - state.startTime : 0)) : 0;
|
|
662
|
+
|
|
91
663
|
return {
|
|
92
|
-
status:
|
|
93
|
-
currentTask:
|
|
94
|
-
totalTasks: '?',
|
|
95
|
-
progress:
|
|
664
|
+
status: state.status || 'unknown',
|
|
665
|
+
currentTask: state.currentTaskIndex || 0,
|
|
666
|
+
totalTasks: state.totalTasks || '?',
|
|
667
|
+
progress: `${progress}%`,
|
|
668
|
+
pipelineBranch: state.pipelineBranch || '-',
|
|
669
|
+
chatId: state.chatId || '-',
|
|
670
|
+
dependsOn,
|
|
671
|
+
duration,
|
|
672
|
+
error: state.error,
|
|
96
673
|
};
|
|
97
674
|
}
|
|
98
|
-
|
|
99
|
-
const progress = state.totalTasks > 0
|
|
100
|
-
? Math.round((state.currentTaskIndex / state.totalTasks) * 100)
|
|
101
|
-
: 0;
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
status: state.status || 'unknown',
|
|
105
|
-
currentTask: (state.currentTaskIndex || 0) + 1,
|
|
106
|
-
totalTasks: state.totalTasks || '?',
|
|
107
|
-
progress: `${progress}%`,
|
|
108
|
-
pipelineBranch: state.pipelineBranch || '-',
|
|
109
|
-
chatId: state.chatId || '-',
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
675
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
676
|
+
private formatDuration(ms: number): string {
|
|
677
|
+
if (ms <= 0) return '-';
|
|
678
|
+
const seconds = Math.floor((ms / 1000) % 60);
|
|
679
|
+
const minutes = Math.floor((ms / (1000 * 60)) % 60);
|
|
680
|
+
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
681
|
+
|
|
682
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
683
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
684
|
+
return `${seconds}s`;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private getStatusIcon(status: string): string {
|
|
688
|
+
const icons: Record<string, string> = {
|
|
689
|
+
'running': 'š',
|
|
690
|
+
'waiting': 'ā³',
|
|
691
|
+
'completed': 'ā
',
|
|
692
|
+
'failed': 'ā',
|
|
693
|
+
'blocked_dependency': 'š«',
|
|
694
|
+
'pending': 'āŖ',
|
|
695
|
+
'reviewing': 'š',
|
|
696
|
+
};
|
|
697
|
+
return icons[status] || 'ā';
|
|
698
|
+
}
|
|
126
699
|
}
|
|
127
700
|
|
|
128
701
|
/**
|
|
129
|
-
*
|
|
702
|
+
* Find the latest run directory
|
|
130
703
|
*/
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Calculate column widths
|
|
142
|
-
const maxNameLen = Math.max(...lanes.map(l => l.name.length), 10);
|
|
143
|
-
|
|
144
|
-
// Header
|
|
145
|
-
console.log(` ${'Lane'.padEnd(maxNameLen)} Status Progress Tasks`);
|
|
146
|
-
console.log(` ${'ā'.repeat(maxNameLen)} ${'ā'.repeat(18)} ${'ā'.repeat(8)} ${'ā'.repeat(10)}`);
|
|
147
|
-
|
|
148
|
-
// Lanes
|
|
149
|
-
for (const lane of lanes) {
|
|
150
|
-
const status = getLaneStatus(lane.path);
|
|
151
|
-
const statusIcon = getStatusIcon(status.status);
|
|
152
|
-
const statusText = `${statusIcon} ${status.status}`.padEnd(18);
|
|
153
|
-
const progressText = status.progress.padEnd(8);
|
|
154
|
-
const tasksText = `${status.currentTask}/${status.totalTasks}`;
|
|
155
|
-
|
|
156
|
-
console.log(` ${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${tasksText}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
console.log();
|
|
704
|
+
function findLatestRunDir(logsDir: string): string | null {
|
|
705
|
+
const runsDir = path.join(logsDir, 'runs');
|
|
706
|
+
if (!fs.existsSync(runsDir)) return null;
|
|
707
|
+
const runs = fs.readdirSync(runsDir)
|
|
708
|
+
.filter(d => d.startsWith('run-'))
|
|
709
|
+
.map(d => ({ name: d, path: path.join(runsDir, d), mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime() }))
|
|
710
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
711
|
+
return runs.length > 0 ? runs[0]!.path : null;
|
|
160
712
|
}
|
|
161
713
|
|
|
162
714
|
/**
|
|
163
715
|
* Monitor lanes
|
|
164
716
|
*/
|
|
165
717
|
async function monitor(args: string[]): Promise<void> {
|
|
166
|
-
|
|
718
|
+
const watchIdx = args.indexOf('--watch');
|
|
719
|
+
const intervalIdx = args.indexOf('--interval');
|
|
720
|
+
const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
|
|
167
721
|
|
|
168
|
-
const
|
|
722
|
+
const runDirArg = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
|
|
169
723
|
const config = loadConfig();
|
|
170
724
|
|
|
171
|
-
|
|
172
|
-
let runDir = options.runDir;
|
|
173
|
-
|
|
725
|
+
let runDir = runDirArg;
|
|
174
726
|
if (!runDir || runDir === 'latest') {
|
|
175
727
|
runDir = findLatestRunDir(config.logsDir) || undefined;
|
|
176
|
-
|
|
177
|
-
if (!runDir) {
|
|
178
|
-
logger.error(`Runs directory: ${path.join(config.logsDir, 'runs')}`);
|
|
179
|
-
throw new Error('No run directories found');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
logger.info(`Using latest run: ${path.basename(runDir)}`);
|
|
728
|
+
if (!runDir) throw new Error('No run directories found');
|
|
183
729
|
}
|
|
184
730
|
|
|
185
|
-
if (!fs.existsSync(runDir)) {
|
|
186
|
-
throw new Error(`Run directory not found: ${runDir}`);
|
|
187
|
-
}
|
|
731
|
+
if (!fs.existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
|
|
188
732
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
logger.info(`Watch mode: every ${options.interval}s (Ctrl+C to stop)\n`);
|
|
192
|
-
|
|
193
|
-
let iteration = 0;
|
|
194
|
-
|
|
195
|
-
const refresh = () => {
|
|
196
|
-
if (iteration > 0) {
|
|
197
|
-
// Clear screen
|
|
198
|
-
process.stdout.write('\x1Bc');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const lanes = listLanes(runDir!);
|
|
202
|
-
displayStatus(runDir!, lanes);
|
|
203
|
-
|
|
204
|
-
iteration++;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// Initial display
|
|
208
|
-
refresh();
|
|
209
|
-
|
|
210
|
-
// Set up interval
|
|
211
|
-
const intervalId = setInterval(refresh, options.interval * 1000);
|
|
212
|
-
|
|
213
|
-
// Handle Ctrl+C
|
|
214
|
-
return new Promise((_, reject) => {
|
|
215
|
-
process.on('SIGINT', () => {
|
|
216
|
-
clearInterval(intervalId);
|
|
217
|
-
console.log('\nš Monitoring stopped\n');
|
|
218
|
-
process.exit(0);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
} else {
|
|
223
|
-
// Single shot
|
|
224
|
-
const lanes = listLanes(runDir);
|
|
225
|
-
displayStatus(runDir, lanes);
|
|
226
|
-
}
|
|
733
|
+
const monitor = new InteractiveMonitor(runDir, interval);
|
|
734
|
+
await monitor.start();
|
|
227
735
|
}
|
|
228
736
|
|
|
229
737
|
export = monitor;
|