@covibes/zeroshot 1.0.1 → 1.1.3
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 +46 -0
- package/README.md +2 -0
- package/cli/index.js +151 -208
- package/cli/message-formatter-utils.js +75 -0
- package/cli/message-formatters-normal.js +214 -0
- package/cli/message-formatters-watch.js +181 -0
- package/cluster-templates/base-templates/full-workflow.json +10 -5
- package/docker/zeroshot-cluster/Dockerfile +6 -0
- package/package.json +5 -2
- package/src/agent/agent-task-executor.js +237 -112
- package/src/isolation-manager.js +94 -51
- package/src/orchestrator.js +45 -10
- package/src/preflight.js +383 -0
- package/src/process-metrics.js +546 -0
- package/src/status-footer.js +543 -0
- package/task-lib/attachable-watcher.js +202 -0
- package/task-lib/commands/clean.js +50 -0
- package/task-lib/commands/get-log-path.js +23 -0
- package/task-lib/commands/kill.js +32 -0
- package/task-lib/commands/list.js +105 -0
- package/task-lib/commands/logs.js +411 -0
- package/task-lib/commands/resume.js +41 -0
- package/task-lib/commands/run.js +48 -0
- package/task-lib/commands/schedule.js +105 -0
- package/task-lib/commands/scheduler-cmd.js +96 -0
- package/task-lib/commands/schedules.js +98 -0
- package/task-lib/commands/status.js +44 -0
- package/task-lib/commands/unschedule.js +16 -0
- package/task-lib/completion.js +9 -0
- package/task-lib/config.js +10 -0
- package/task-lib/name-generator.js +230 -0
- package/task-lib/package.json +3 -0
- package/task-lib/runner.js +123 -0
- package/task-lib/scheduler.js +252 -0
- package/task-lib/store.js +217 -0
- package/task-lib/tui/formatters.js +166 -0
- package/task-lib/tui/index.js +197 -0
- package/task-lib/tui/layout.js +111 -0
- package/task-lib/tui/renderer.js +119 -0
- package/task-lib/tui.js +384 -0
- package/task-lib/watcher.js +162 -0
- package/cluster-templates/conductor-junior-bootstrap.json +0 -69
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusFooter - Persistent terminal status bar for live agent monitoring
|
|
3
|
+
*
|
|
4
|
+
* Displays:
|
|
5
|
+
* - Agent status icons (🟢 running, ⏳ waiting, 🔄 processing, etc.)
|
|
6
|
+
* - Real-time CPU, memory, network metrics per agent
|
|
7
|
+
* - Cluster summary stats
|
|
8
|
+
*
|
|
9
|
+
* Uses ANSI escape sequences to maintain a fixed footer while
|
|
10
|
+
* allowing normal terminal output to scroll above it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { getProcessMetrics } = require('./process-metrics');
|
|
14
|
+
|
|
15
|
+
// ANSI escape codes
|
|
16
|
+
const ESC = '\x1b';
|
|
17
|
+
const CSI = `${ESC}[`;
|
|
18
|
+
|
|
19
|
+
// Terminal manipulation
|
|
20
|
+
const SAVE_CURSOR = `${CSI}s`;
|
|
21
|
+
const RESTORE_CURSOR = `${CSI}u`;
|
|
22
|
+
const CLEAR_LINE = `${CSI}2K`;
|
|
23
|
+
const HIDE_CURSOR = `${CSI}?25l`;
|
|
24
|
+
const SHOW_CURSOR = `${CSI}?25h`;
|
|
25
|
+
|
|
26
|
+
// Colors
|
|
27
|
+
const COLORS = {
|
|
28
|
+
reset: `${CSI}0m`,
|
|
29
|
+
bold: `${CSI}1m`,
|
|
30
|
+
dim: `${CSI}2m`,
|
|
31
|
+
cyan: `${CSI}36m`,
|
|
32
|
+
green: `${CSI}32m`,
|
|
33
|
+
yellow: `${CSI}33m`,
|
|
34
|
+
red: `${CSI}31m`,
|
|
35
|
+
gray: `${CSI}90m`,
|
|
36
|
+
white: `${CSI}37m`,
|
|
37
|
+
bgBlack: `${CSI}40m`,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Object} AgentState
|
|
42
|
+
* @property {string} id - Agent ID
|
|
43
|
+
* @property {string} state - Agent state (idle, executing, etc.)
|
|
44
|
+
* @property {number|null} pid - Process ID if running
|
|
45
|
+
* @property {number} iteration - Current iteration
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
class StatusFooter {
|
|
49
|
+
/**
|
|
50
|
+
* @param {Object} options
|
|
51
|
+
* @param {number} [options.refreshInterval=1000] - Refresh interval in ms
|
|
52
|
+
* @param {boolean} [options.enabled=true] - Whether footer is enabled
|
|
53
|
+
* @param {number} [options.maxAgentRows=5] - Max agent rows to display
|
|
54
|
+
*/
|
|
55
|
+
constructor(options = {}) {
|
|
56
|
+
this.refreshInterval = options.refreshInterval || 1000;
|
|
57
|
+
this.enabled = options.enabled !== false;
|
|
58
|
+
this.maxAgentRows = options.maxAgentRows || 5;
|
|
59
|
+
this.intervalId = null;
|
|
60
|
+
this.agents = new Map(); // agentId -> AgentState
|
|
61
|
+
this.metricsCache = new Map(); // agentId -> ProcessMetrics
|
|
62
|
+
this.footerHeight = 3; // Minimum: header + 1 agent row + summary
|
|
63
|
+
this.lastFooterHeight = 3;
|
|
64
|
+
this.scrollRegionSet = false;
|
|
65
|
+
this.clusterId = null;
|
|
66
|
+
this.clusterState = 'initializing';
|
|
67
|
+
this.startTime = Date.now();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if we're in a TTY that supports the footer
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
isTTY() {
|
|
75
|
+
return process.stdout.isTTY === true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get terminal dimensions
|
|
80
|
+
* @returns {{ rows: number, cols: number }}
|
|
81
|
+
*/
|
|
82
|
+
getTerminalSize() {
|
|
83
|
+
return {
|
|
84
|
+
rows: process.stdout.rows || 24,
|
|
85
|
+
cols: process.stdout.columns || 80,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Move cursor to specific position
|
|
91
|
+
* @param {number} row - 1-based row
|
|
92
|
+
* @param {number} col - 1-based column
|
|
93
|
+
*/
|
|
94
|
+
moveTo(row, col) {
|
|
95
|
+
process.stdout.write(`${CSI}${row};${col}H`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set up scroll region to reserve space for footer
|
|
100
|
+
*/
|
|
101
|
+
setupScrollRegion() {
|
|
102
|
+
if (!this.isTTY()) return;
|
|
103
|
+
|
|
104
|
+
const { rows } = this.getTerminalSize();
|
|
105
|
+
const scrollEnd = rows - this.footerHeight;
|
|
106
|
+
|
|
107
|
+
// Set scroll region (lines 1 to scrollEnd)
|
|
108
|
+
process.stdout.write(`${CSI}1;${scrollEnd}r`);
|
|
109
|
+
|
|
110
|
+
// Move cursor to top of scroll region
|
|
111
|
+
process.stdout.write(`${CSI}1;1H`);
|
|
112
|
+
|
|
113
|
+
this.scrollRegionSet = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reset scroll region to full terminal
|
|
118
|
+
*/
|
|
119
|
+
resetScrollRegion() {
|
|
120
|
+
if (!this.isTTY()) return;
|
|
121
|
+
|
|
122
|
+
const { rows } = this.getTerminalSize();
|
|
123
|
+
process.stdout.write(`${CSI}1;${rows}r`);
|
|
124
|
+
this.scrollRegionSet = false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Register cluster for monitoring
|
|
129
|
+
* @param {string} clusterId
|
|
130
|
+
*/
|
|
131
|
+
setCluster(clusterId) {
|
|
132
|
+
this.clusterId = clusterId;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Update cluster state
|
|
137
|
+
* @param {string} state
|
|
138
|
+
*/
|
|
139
|
+
setClusterState(state) {
|
|
140
|
+
this.clusterState = state;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register an agent for monitoring
|
|
145
|
+
* @param {AgentState} agentState
|
|
146
|
+
*/
|
|
147
|
+
updateAgent(agentState) {
|
|
148
|
+
this.agents.set(agentState.id, {
|
|
149
|
+
...agentState,
|
|
150
|
+
lastUpdate: Date.now(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Remove an agent from monitoring
|
|
156
|
+
* @param {string} agentId
|
|
157
|
+
*/
|
|
158
|
+
removeAgent(agentId) {
|
|
159
|
+
this.agents.delete(agentId);
|
|
160
|
+
this.metricsCache.delete(agentId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get status icon for agent state
|
|
165
|
+
* @param {string} state
|
|
166
|
+
* @returns {string}
|
|
167
|
+
*/
|
|
168
|
+
getAgentIcon(state) {
|
|
169
|
+
switch (state) {
|
|
170
|
+
case 'idle':
|
|
171
|
+
return '⏳'; // Waiting for trigger
|
|
172
|
+
case 'evaluating':
|
|
173
|
+
return '🔍'; // Evaluating triggers
|
|
174
|
+
case 'building_context':
|
|
175
|
+
return '📝'; // Building context
|
|
176
|
+
case 'executing':
|
|
177
|
+
return '🔄'; // Running task
|
|
178
|
+
case 'stopped':
|
|
179
|
+
return '⏹️'; // Stopped
|
|
180
|
+
case 'error':
|
|
181
|
+
return '❌'; // Error
|
|
182
|
+
default:
|
|
183
|
+
return '⚪';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format duration in human-readable form
|
|
189
|
+
* @param {number} ms
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
192
|
+
formatDuration(ms) {
|
|
193
|
+
const seconds = Math.floor(ms / 1000);
|
|
194
|
+
const minutes = Math.floor(seconds / 60);
|
|
195
|
+
const hours = Math.floor(minutes / 60);
|
|
196
|
+
|
|
197
|
+
if (hours > 0) {
|
|
198
|
+
return `${hours}h${minutes % 60}m`;
|
|
199
|
+
}
|
|
200
|
+
if (minutes > 0) {
|
|
201
|
+
return `${minutes}m${seconds % 60}s`;
|
|
202
|
+
}
|
|
203
|
+
return `${seconds}s`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Format bytes in human-readable form
|
|
208
|
+
* @param {number} bytes
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
formatBytes(bytes) {
|
|
212
|
+
if (bytes < 1024) {
|
|
213
|
+
return `${bytes}B`;
|
|
214
|
+
}
|
|
215
|
+
if (bytes < 1024 * 1024) {
|
|
216
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
217
|
+
}
|
|
218
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
219
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
220
|
+
}
|
|
221
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Render the footer
|
|
226
|
+
*/
|
|
227
|
+
async render() {
|
|
228
|
+
if (!this.enabled || !this.isTTY()) return;
|
|
229
|
+
|
|
230
|
+
const { rows, cols } = this.getTerminalSize();
|
|
231
|
+
|
|
232
|
+
// Collect metrics for all agents with PIDs
|
|
233
|
+
for (const [agentId, agent] of this.agents) {
|
|
234
|
+
if (agent.pid) {
|
|
235
|
+
try {
|
|
236
|
+
const metrics = await getProcessMetrics(agent.pid, { samplePeriodMs: 500 });
|
|
237
|
+
this.metricsCache.set(agentId, metrics);
|
|
238
|
+
} catch {
|
|
239
|
+
// Process may have exited
|
|
240
|
+
this.metricsCache.delete(agentId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Get executing agents for display
|
|
246
|
+
const executingAgents = Array.from(this.agents.entries())
|
|
247
|
+
.filter(([, agent]) => agent.state === 'executing')
|
|
248
|
+
.slice(0, this.maxAgentRows);
|
|
249
|
+
|
|
250
|
+
// Calculate dynamic footer height: header + agent rows + separator + summary
|
|
251
|
+
// Minimum 3 lines (header + "no agents" message + summary)
|
|
252
|
+
const agentRowCount = Math.max(1, executingAgents.length);
|
|
253
|
+
const newHeight = 2 + agentRowCount + 1; // header + agents + summary (separator merged with summary)
|
|
254
|
+
|
|
255
|
+
// Update scroll region if height changed
|
|
256
|
+
if (newHeight !== this.footerHeight) {
|
|
257
|
+
this.footerHeight = newHeight;
|
|
258
|
+
this.setupScrollRegion();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Build footer lines
|
|
262
|
+
const headerLine = this.buildHeaderLine(cols);
|
|
263
|
+
const agentRows = this.buildAgentRows(executingAgents, cols);
|
|
264
|
+
const summaryLine = this.buildSummaryLine(cols);
|
|
265
|
+
|
|
266
|
+
// Save cursor, render footer, restore cursor
|
|
267
|
+
process.stdout.write(SAVE_CURSOR);
|
|
268
|
+
process.stdout.write(HIDE_CURSOR);
|
|
269
|
+
|
|
270
|
+
// Render from top of footer area
|
|
271
|
+
let currentRow = rows - this.footerHeight + 1;
|
|
272
|
+
|
|
273
|
+
// Header line
|
|
274
|
+
this.moveTo(currentRow++, 1);
|
|
275
|
+
process.stdout.write(CLEAR_LINE);
|
|
276
|
+
process.stdout.write(`${COLORS.bgBlack}${headerLine}${COLORS.reset}`);
|
|
277
|
+
|
|
278
|
+
// Agent rows
|
|
279
|
+
for (const agentRow of agentRows) {
|
|
280
|
+
this.moveTo(currentRow++, 1);
|
|
281
|
+
process.stdout.write(CLEAR_LINE);
|
|
282
|
+
process.stdout.write(`${COLORS.bgBlack}${agentRow}${COLORS.reset}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Summary line (with bottom border)
|
|
286
|
+
this.moveTo(currentRow, 1);
|
|
287
|
+
process.stdout.write(CLEAR_LINE);
|
|
288
|
+
process.stdout.write(`${COLORS.bgBlack}${summaryLine}${COLORS.reset}`);
|
|
289
|
+
|
|
290
|
+
process.stdout.write(RESTORE_CURSOR);
|
|
291
|
+
process.stdout.write(SHOW_CURSOR);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Build the header line with cluster ID
|
|
296
|
+
* @param {number} width - Terminal width
|
|
297
|
+
* @returns {string}
|
|
298
|
+
*/
|
|
299
|
+
buildHeaderLine(width) {
|
|
300
|
+
let content = `${COLORS.gray}┌─${COLORS.reset}`;
|
|
301
|
+
|
|
302
|
+
// Cluster ID
|
|
303
|
+
if (this.clusterId) {
|
|
304
|
+
const shortId = this.clusterId.replace('cluster-', '');
|
|
305
|
+
content += ` ${COLORS.cyan}${COLORS.bold}${shortId}${COLORS.reset} `;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Fill with border
|
|
309
|
+
const contentLen = this.stripAnsi(content).length;
|
|
310
|
+
const padding = Math.max(0, width - contentLen - 1);
|
|
311
|
+
return content + `${COLORS.gray}${'─'.repeat(padding)}┐${COLORS.reset}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build agent rows (one row per executing agent)
|
|
316
|
+
* @param {Array} executingAgents - Array of [agentId, agent] pairs
|
|
317
|
+
* @param {number} width - Terminal width
|
|
318
|
+
* @returns {Array<string>} Array of formatted rows
|
|
319
|
+
*/
|
|
320
|
+
buildAgentRows(executingAgents, width) {
|
|
321
|
+
if (executingAgents.length === 0) {
|
|
322
|
+
// No agents row
|
|
323
|
+
const content = `${COLORS.gray}│${COLORS.reset} ${COLORS.dim}No active agents${COLORS.reset}`;
|
|
324
|
+
const contentLen = this.stripAnsi(content).length;
|
|
325
|
+
const padding = Math.max(0, width - contentLen - 1);
|
|
326
|
+
return [content + ' '.repeat(padding) + `${COLORS.gray}│${COLORS.reset}`];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const rows = [];
|
|
330
|
+
for (const [agentId, agent] of executingAgents) {
|
|
331
|
+
const icon = this.getAgentIcon(agent.state);
|
|
332
|
+
const metrics = this.metricsCache.get(agentId);
|
|
333
|
+
|
|
334
|
+
// Build columns with fixed widths for alignment
|
|
335
|
+
const iconCol = icon;
|
|
336
|
+
const nameCol = agentId.padEnd(14).slice(0, 14); // Max 14 chars for name
|
|
337
|
+
|
|
338
|
+
let metricsStr = '';
|
|
339
|
+
if (metrics && metrics.exists) {
|
|
340
|
+
const cpuColor = metrics.cpuPercent > 50 ? COLORS.yellow : COLORS.green;
|
|
341
|
+
const cpuVal = `${metrics.cpuPercent}%`.padStart(4);
|
|
342
|
+
const ramVal = `${metrics.memoryMB}MB`.padStart(6);
|
|
343
|
+
|
|
344
|
+
metricsStr += `${COLORS.dim}CPU:${COLORS.reset}${cpuColor}${cpuVal}${COLORS.reset}`;
|
|
345
|
+
metricsStr += ` ${COLORS.dim}RAM:${COLORS.reset}${COLORS.gray}${ramVal}${COLORS.reset}`;
|
|
346
|
+
|
|
347
|
+
// Network bytes
|
|
348
|
+
const net = metrics.network;
|
|
349
|
+
if (net.bytesSent > 0 || net.bytesReceived > 0) {
|
|
350
|
+
const sent = this.formatBytes(net.bytesSent).padStart(7);
|
|
351
|
+
const recv = this.formatBytes(net.bytesReceived).padStart(7);
|
|
352
|
+
metricsStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${sent} ↓${recv}${COLORS.reset}`;
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
metricsStr = `${COLORS.dim}(starting...)${COLORS.reset}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Iteration number
|
|
359
|
+
const iterStr = agent.iteration > 0 ? `${COLORS.dim}#${agent.iteration}${COLORS.reset}` : '';
|
|
360
|
+
|
|
361
|
+
// Build the row
|
|
362
|
+
let content = `${COLORS.gray}│${COLORS.reset} ${iconCol} ${COLORS.white}${nameCol}${COLORS.reset} ${metricsStr}`;
|
|
363
|
+
if (iterStr) {
|
|
364
|
+
content += ` ${iterStr}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const contentLen = this.stripAnsi(content).length;
|
|
368
|
+
const padding = Math.max(0, width - contentLen - 1);
|
|
369
|
+
rows.push(content + ' '.repeat(padding) + `${COLORS.gray}│${COLORS.reset}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return rows;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Build a single status line for testing/display
|
|
377
|
+
* Alias for buildSummaryLine for backward compatibility
|
|
378
|
+
* @param {number} width - Terminal width
|
|
379
|
+
* @returns {string}
|
|
380
|
+
*/
|
|
381
|
+
buildStatusLine(width) {
|
|
382
|
+
return this.buildSummaryLine(width);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Build the summary line with aggregated metrics
|
|
387
|
+
* @param {number} width - Terminal width
|
|
388
|
+
* @returns {string}
|
|
389
|
+
*/
|
|
390
|
+
buildSummaryLine(width) {
|
|
391
|
+
const parts = [];
|
|
392
|
+
|
|
393
|
+
// Border with corner
|
|
394
|
+
parts.push(`${COLORS.gray}└─${COLORS.reset}`);
|
|
395
|
+
|
|
396
|
+
// Cluster state
|
|
397
|
+
const stateColor = this.clusterState === 'running' ? COLORS.green : COLORS.yellow;
|
|
398
|
+
parts.push(` ${stateColor}${this.clusterState}${COLORS.reset}`);
|
|
399
|
+
|
|
400
|
+
// Duration
|
|
401
|
+
const duration = this.formatDuration(Date.now() - this.startTime);
|
|
402
|
+
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.dim}${duration}${COLORS.reset}`);
|
|
403
|
+
|
|
404
|
+
// Agent counts
|
|
405
|
+
const executing = Array.from(this.agents.values()).filter(a => a.state === 'executing').length;
|
|
406
|
+
const total = this.agents.size;
|
|
407
|
+
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
|
|
408
|
+
|
|
409
|
+
// Aggregate metrics
|
|
410
|
+
let totalCpu = 0;
|
|
411
|
+
let totalMem = 0;
|
|
412
|
+
let totalBytesSent = 0;
|
|
413
|
+
let totalBytesReceived = 0;
|
|
414
|
+
for (const metrics of this.metricsCache.values()) {
|
|
415
|
+
if (metrics.exists) {
|
|
416
|
+
totalCpu += metrics.cpuPercent;
|
|
417
|
+
totalMem += metrics.memoryMB;
|
|
418
|
+
totalBytesSent += metrics.network.bytesSent || 0;
|
|
419
|
+
totalBytesReceived += metrics.network.bytesReceived || 0;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (totalCpu > 0 || totalMem > 0) {
|
|
424
|
+
parts.push(` ${COLORS.gray}│${COLORS.reset}`);
|
|
425
|
+
let aggregateStr = ` ${COLORS.cyan}Σ${COLORS.reset} `;
|
|
426
|
+
aggregateStr += `${COLORS.dim}CPU:${COLORS.reset}${totalCpu.toFixed(0)}%`;
|
|
427
|
+
aggregateStr += ` ${COLORS.dim}RAM:${COLORS.reset}${totalMem.toFixed(0)}MB`;
|
|
428
|
+
if (totalBytesSent > 0 || totalBytesReceived > 0) {
|
|
429
|
+
aggregateStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${this.formatBytes(totalBytesSent)} ↓${this.formatBytes(totalBytesReceived)}${COLORS.reset}`;
|
|
430
|
+
}
|
|
431
|
+
parts.push(aggregateStr);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Pad and close with bottom corner
|
|
435
|
+
const content = parts.join('');
|
|
436
|
+
const contentLen = this.stripAnsi(content).length;
|
|
437
|
+
const padding = Math.max(0, width - contentLen - 1);
|
|
438
|
+
return content + `${COLORS.gray}${'─'.repeat(padding)}┘${COLORS.reset}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Strip ANSI codes from string for length calculation
|
|
443
|
+
* @param {string} str
|
|
444
|
+
* @returns {string}
|
|
445
|
+
*/
|
|
446
|
+
stripAnsi(str) {
|
|
447
|
+
// eslint-disable-next-line no-control-regex
|
|
448
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Start the status footer
|
|
453
|
+
*/
|
|
454
|
+
start() {
|
|
455
|
+
if (!this.enabled || !this.isTTY()) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
this.setupScrollRegion();
|
|
460
|
+
|
|
461
|
+
// Handle terminal resize
|
|
462
|
+
process.stdout.on('resize', () => {
|
|
463
|
+
this.setupScrollRegion();
|
|
464
|
+
this.render();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Start refresh interval
|
|
468
|
+
this.intervalId = setInterval(() => {
|
|
469
|
+
this.render();
|
|
470
|
+
}, this.refreshInterval);
|
|
471
|
+
|
|
472
|
+
// Initial render
|
|
473
|
+
this.render();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Stop the status footer and cleanup
|
|
478
|
+
*/
|
|
479
|
+
stop() {
|
|
480
|
+
if (this.intervalId) {
|
|
481
|
+
clearInterval(this.intervalId);
|
|
482
|
+
this.intervalId = null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (this.isTTY()) {
|
|
486
|
+
// Reset scroll region
|
|
487
|
+
this.resetScrollRegion();
|
|
488
|
+
|
|
489
|
+
// Clear all footer lines (dynamic height)
|
|
490
|
+
const { rows } = this.getTerminalSize();
|
|
491
|
+
const startRow = rows - this.footerHeight + 1;
|
|
492
|
+
for (let row = startRow; row <= rows; row++) {
|
|
493
|
+
this.moveTo(row, 1);
|
|
494
|
+
process.stdout.write(CLEAR_LINE);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Move cursor to safe position
|
|
498
|
+
this.moveTo(startRow, 1);
|
|
499
|
+
process.stdout.write(SHOW_CURSOR);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Temporarily hide footer for clean output
|
|
505
|
+
*/
|
|
506
|
+
hide() {
|
|
507
|
+
if (!this.isTTY()) return;
|
|
508
|
+
|
|
509
|
+
this.resetScrollRegion();
|
|
510
|
+
|
|
511
|
+
// Clear all footer lines (dynamic height)
|
|
512
|
+
const { rows } = this.getTerminalSize();
|
|
513
|
+
const startRow = rows - this.footerHeight + 1;
|
|
514
|
+
for (let row = startRow; row <= rows; row++) {
|
|
515
|
+
this.moveTo(row, 1);
|
|
516
|
+
process.stdout.write(CLEAR_LINE);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Restore footer after hiding
|
|
522
|
+
*/
|
|
523
|
+
show() {
|
|
524
|
+
if (!this.isTTY()) return;
|
|
525
|
+
|
|
526
|
+
this.setupScrollRegion();
|
|
527
|
+
this.render();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Create a singleton footer instance
|
|
533
|
+
* @param {Object} options
|
|
534
|
+
* @returns {StatusFooter}
|
|
535
|
+
*/
|
|
536
|
+
function createStatusFooter(options = {}) {
|
|
537
|
+
return new StatusFooter(options);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
module.exports = {
|
|
541
|
+
StatusFooter,
|
|
542
|
+
createStatusFooter,
|
|
543
|
+
};
|