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