pyre-cli 5.0.0 → 6.0.0
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/README.md +197 -255
- package/dist/index.js +1454 -281
- package/package.json +1 -1
- package/src/anomalies.ts +102 -0
- package/src/formatters/index.ts +3 -3
- package/src/formatters/output.ts +355 -21
- package/src/formatters/render.ts +316 -97
- package/src/formatters/types.ts +33 -17
- package/src/history.ts +57 -13
- package/src/index.ts +226 -13
- package/src/live/export.ts +64 -7
- package/src/live/input.ts +84 -39
- package/src/live/render.ts +175 -118
- package/src/live/state.ts +76 -70
- package/src/live/types.ts +10 -2
- package/src/monitors/collectors.ts +142 -35
- package/src/monitors/smc.ts +60 -12
- package/src/monitors/types.ts +1 -0
- package/src/p2p/server.test.ts +2 -2
- package/src/sparkline.ts +82 -13
- package/src/state/config.ts +130 -0
- package/Formula/pyre.rb +0 -19
package/src/formatters/render.ts
CHANGED
|
@@ -10,22 +10,58 @@
|
|
|
10
10
|
* all layout-related code is co-located.
|
|
11
11
|
*/
|
|
12
12
|
import chalk from 'chalk';
|
|
13
|
-
import { sparkline } from '../sparkline.js';
|
|
13
|
+
import { sparkline, multiRowSparkline } from '../sparkline.js';
|
|
14
14
|
import type { History } from '../history.js';
|
|
15
|
-
import type { StatsData, VisibleItems, TableOptions } from './types.js';
|
|
16
|
-
import { THEMES, type ThemeName } from './themes.js';
|
|
15
|
+
import type { StatsData, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
|
|
16
|
+
import { THEMES, type ThemeName, type ThemeColors } from './themes.js';
|
|
17
|
+
|
|
17
18
|
|
|
18
19
|
// --- low-level box drawing ---------------------------------------------------
|
|
19
20
|
|
|
20
|
-
function visLen(s: string): number {
|
|
21
|
+
export function visLen(s: string): number {
|
|
21
22
|
// eslint-disable-next-line no-control-regex
|
|
22
23
|
return s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
+
export function fitVisible(s: string, width: number): string {
|
|
27
|
+
const w = Math.max(0, width);
|
|
26
28
|
const len = visLen(s);
|
|
27
|
-
if (len
|
|
28
|
-
return s + ' '.repeat(
|
|
29
|
+
if (len === w) return s;
|
|
30
|
+
if (len < w) return s + ' '.repeat(w - len);
|
|
31
|
+
|
|
32
|
+
let visibleCount = 0;
|
|
33
|
+
let out = '';
|
|
34
|
+
let inAnsi = false;
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < s.length; i++) {
|
|
37
|
+
const char = s[i];
|
|
38
|
+
if (char === '\x1b') {
|
|
39
|
+
inAnsi = true;
|
|
40
|
+
out += char;
|
|
41
|
+
} else if (inAnsi) {
|
|
42
|
+
out += char;
|
|
43
|
+
if ((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')) {
|
|
44
|
+
inAnsi = false;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
if (visibleCount < w) {
|
|
48
|
+
out += char;
|
|
49
|
+
visibleCount++;
|
|
50
|
+
} else {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
out += '\x1b[0m';
|
|
56
|
+
const resLen = visLen(out);
|
|
57
|
+
if (resLen < w) {
|
|
58
|
+
out += ' '.repeat(w - resLen);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function padVisible(s: string, width: number): string {
|
|
64
|
+
return fitVisible(s, width);
|
|
29
65
|
}
|
|
30
66
|
|
|
31
67
|
function truncatePlain(s: string, width: number): string {
|
|
@@ -40,29 +76,50 @@ export function panel(
|
|
|
40
76
|
width: number,
|
|
41
77
|
accent: (s: string) => string,
|
|
42
78
|
borderAccent: (s: string) => string,
|
|
43
|
-
height?: number
|
|
79
|
+
height?: number,
|
|
80
|
+
rightTitle?: string
|
|
44
81
|
): string[] {
|
|
45
82
|
const h = height ?? lines.length;
|
|
46
83
|
const contentWidth = Math.max(1, width - 4);
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
84
|
+
const leftLabel = title ? ` ${title} ` : '';
|
|
85
|
+
const rightLabel = rightTitle ? ` ${rightTitle} ` : '';
|
|
86
|
+
const innerW = Math.max(0, width - 2);
|
|
87
|
+
let topInner = leftLabel + '─'.repeat(Math.max(0, innerW - visLen(leftLabel) - visLen(rightLabel))) + rightLabel;
|
|
88
|
+
if (visLen(topInner) > innerW) {
|
|
89
|
+
topInner = fitVisible(topInner, innerW);
|
|
90
|
+
} else if (visLen(topInner) < innerW) {
|
|
91
|
+
topInner = topInner + '─'.repeat(innerW - visLen(topInner));
|
|
92
|
+
}
|
|
93
|
+
const top = borderAccent('┌' + topInner + '┐');
|
|
94
|
+
const bottom = borderAccent('└' + '─'.repeat(innerW) + '┘');
|
|
50
95
|
const body: string[] = [];
|
|
51
96
|
for (let i = 0; i < h; i++) {
|
|
52
97
|
const line = lines[i] ?? '';
|
|
53
|
-
body.push(`${borderAccent('│')} ${
|
|
98
|
+
body.push(`${borderAccent('│')} ${fitVisible(line, contentWidth)} ${borderAccent('│')}`);
|
|
54
99
|
}
|
|
55
100
|
return [top, ...body, bottom];
|
|
56
101
|
}
|
|
57
102
|
|
|
103
|
+
export function gaugeBar(percent: number, width = 12): string {
|
|
104
|
+
const w = Math.max(1, width);
|
|
105
|
+
const pct = Math.min(100, Math.max(0, percent));
|
|
106
|
+
const filled = Math.round((pct / 100) * w);
|
|
107
|
+
const empty = w - filled;
|
|
108
|
+
const filledStr = '█'.repeat(filled);
|
|
109
|
+
const emptyStr = '░'.repeat(empty);
|
|
110
|
+
const color = pct > 85 ? chalk.red : pct > 65 ? chalk.yellow : chalk.cyan;
|
|
111
|
+
return color(filledStr) + chalk.dim(emptyStr);
|
|
112
|
+
}
|
|
113
|
+
|
|
58
114
|
/** Join equal-or-uneven-height panel blocks side by side with a gap. */
|
|
59
115
|
function hstack(blocks: string[][], gap = 2, targetWidth?: number): string[] {
|
|
60
116
|
if (!blocks.length) return [];
|
|
61
117
|
const height = Math.max(...blocks.map(b => b.length));
|
|
62
|
-
const widths = blocks.map(b =>
|
|
118
|
+
const widths = blocks.map(b => Math.max(0, ...b.map(l => visLen(l))));
|
|
63
119
|
const padded = blocks.map((b, i) => {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
120
|
+
const w = widths[i];
|
|
121
|
+
const filler = ' '.repeat(w);
|
|
122
|
+
const arr = b.map(line => fitVisible(line, w));
|
|
66
123
|
while (arr.length < height) arr.push(filler);
|
|
67
124
|
return arr;
|
|
68
125
|
});
|
|
@@ -70,14 +127,14 @@ function hstack(blocks: string[][], gap = 2, targetWidth?: number): string[] {
|
|
|
70
127
|
for (let row = 0; row < height; row++) {
|
|
71
128
|
let line = padded.map(a => a[row]).join(' '.repeat(gap));
|
|
72
129
|
if (targetWidth !== undefined) {
|
|
73
|
-
|
|
74
|
-
line += ' '.repeat(pad);
|
|
130
|
+
line = fitVisible(line, targetWidth);
|
|
75
131
|
}
|
|
76
132
|
out.push(line);
|
|
77
133
|
}
|
|
78
134
|
return out;
|
|
79
135
|
}
|
|
80
136
|
|
|
137
|
+
|
|
81
138
|
function statRow(label: string, value: string, width = 9): string {
|
|
82
139
|
const w = Math.max(width, label.length + 1);
|
|
83
140
|
return `${chalk.dim(label.padEnd(w))}${value}`;
|
|
@@ -117,7 +174,8 @@ export function capacityColor(percent: number) {
|
|
|
117
174
|
}
|
|
118
175
|
|
|
119
176
|
export function formatBytes(bytes: number): string {
|
|
120
|
-
if (bytes <
|
|
177
|
+
if (isNaN(bytes) || bytes < 0) return '0 B';
|
|
178
|
+
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
|
121
179
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
122
180
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
123
181
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
@@ -132,6 +190,14 @@ export function formatTemp(c: number): string {
|
|
|
132
190
|
return `${c.toFixed(1)}°C / ${celsiusToFahrenheit(c).toFixed(1)}°F`;
|
|
133
191
|
}
|
|
134
192
|
|
|
193
|
+
/** Render a Celsius reading in the requested unit. */
|
|
194
|
+
export function formatTempUnit(c: number, unit: 'c' | 'f' = 'c'): string {
|
|
195
|
+
if (unit === 'f') {
|
|
196
|
+
return `${(c * (9 / 5) + 32).toFixed(1)}°F`;
|
|
197
|
+
}
|
|
198
|
+
return `${c.toFixed(1)}°C`;
|
|
199
|
+
}
|
|
200
|
+
|
|
135
201
|
// --- cards -------------------------------------------------------------------
|
|
136
202
|
|
|
137
203
|
function cpuCard(data: StatsData, contentWidth: number): string[] {
|
|
@@ -142,6 +208,18 @@ function cpuCard(data: StatsData, contentWidth: number): string[] {
|
|
|
142
208
|
gaugeRow('Usage', data.cpu.usage, contentWidth),
|
|
143
209
|
statRow('Load', data.cpu.loadAvg.map(l => l.toFixed(2)).join(' ')),
|
|
144
210
|
];
|
|
211
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
212
|
+
const slotW = 2;
|
|
213
|
+
const maxCores = Math.max(1, Math.floor((contentWidth - 10) / (slotW + 1)));
|
|
214
|
+
const cores = data.cpu.coreUsage.slice(0, maxCores);
|
|
215
|
+
const bars = cores.map(u => {
|
|
216
|
+
const fill = Math.round((Math.min(100, u) / 100) * slotW);
|
|
217
|
+
const str = '█'.repeat(Math.max(0, fill)) + '░'.repeat(slotW - fill);
|
|
218
|
+
const color = u > 90 ? chalk.red : u > 70 ? chalk.yellow : chalk.green;
|
|
219
|
+
return color(str);
|
|
220
|
+
});
|
|
221
|
+
lines.push(statRow('Cores', bars.join(' ')));
|
|
222
|
+
}
|
|
145
223
|
if (data.cpu.temperature) lines.push(statRow('Temp', formatTemp(data.cpu.temperature)));
|
|
146
224
|
return lines;
|
|
147
225
|
}
|
|
@@ -260,7 +338,7 @@ function header(data: StatsData, width: number, badges: string[] = [], borderAcc
|
|
|
260
338
|
const left = `${chalk.hex('#ff5500').bold(' PYRE ')} ${chalk.bold(data.header.hostname)} ${chalk.dim(data.header.os)} ${chalk.dim('up ' + data.header.uptime)}`;
|
|
261
339
|
const right = `${chalk.dim(now)} ${badges.join(' ')}`;
|
|
262
340
|
const gap = Math.max(1, width - visLen(left) - visLen(right));
|
|
263
|
-
const line = left + ' '.repeat(gap) + right;
|
|
341
|
+
const line = fitVisible(left + ' '.repeat(gap) + right, width);
|
|
264
342
|
return [line, borderAccent('─'.repeat(width))];
|
|
265
343
|
}
|
|
266
344
|
|
|
@@ -322,9 +400,27 @@ function tabBar(activePanel: string, width: number, theme: ThemeColors): string
|
|
|
322
400
|
return `${keyHint}${label}`;
|
|
323
401
|
});
|
|
324
402
|
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
403
|
+
const separator = chalk.dim(' │ ');
|
|
404
|
+
const joined = rendered.join(separator);
|
|
405
|
+
const indent = ' ';
|
|
406
|
+
const available = width - visLen(indent);
|
|
407
|
+
let result = joined;
|
|
408
|
+
if (visLen(joined) > available) {
|
|
409
|
+
let used = 0;
|
|
410
|
+
const kept: string[] = [];
|
|
411
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
412
|
+
const tabW = visLen(rendered[i]) + (kept.length > 0 ? visLen(separator) : 0);
|
|
413
|
+
if (used + tabW > available && kept.length > 0) break;
|
|
414
|
+
kept.push(rendered[i]);
|
|
415
|
+
used += tabW;
|
|
416
|
+
}
|
|
417
|
+
if (kept.length < rendered.length) {
|
|
418
|
+
result = kept.join(separator) + chalk.dim('⋯');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const pad = Math.max(0, available - visLen(result));
|
|
423
|
+
return fitVisible(chalk.dim(indent) + result + ' '.repeat(pad), width);
|
|
328
424
|
}
|
|
329
425
|
|
|
330
426
|
// --- process / disk tables ---------------------------------------------------
|
|
@@ -571,21 +667,196 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number,
|
|
|
571
667
|
}
|
|
572
668
|
}
|
|
573
669
|
|
|
670
|
+
function anomalyPanel(anomalies: AnomalyAlert[], width: number, theme: ThemeColors): string[] {
|
|
671
|
+
const lines: string[] = [];
|
|
672
|
+
for (const a of anomalies) {
|
|
673
|
+
const color = a.severity === 'critical' ? chalk.red.bold : chalk.yellow;
|
|
674
|
+
const direction = a.zScore > 0 ? '↑ spike' : '↓ drop';
|
|
675
|
+
const valStr = a.metric.toLowerCase().includes('net') ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
|
|
676
|
+
lines.push(color(` ⚠ ${a.metric}: ${valStr} (${direction}, σ=${a.zScore.toFixed(1)})`));
|
|
677
|
+
}
|
|
678
|
+
return lines;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function btopCpuBox(data: StatsData, width: number, theme: ThemeColors, opts: TableOptions): string[] {
|
|
682
|
+
const contentWidth = width - 4;
|
|
683
|
+
const rightTitle = `${data.header.hostname} · up ${data.header.uptime} · ${data.cpu.usage.toFixed(0)}%`;
|
|
684
|
+
|
|
685
|
+
if (contentWidth >= 65) {
|
|
686
|
+
const leftW = Math.floor(contentWidth * 0.48);
|
|
687
|
+
const rightW = contentWidth - leftW - 2;
|
|
688
|
+
|
|
689
|
+
const leftLines: string[] = [];
|
|
690
|
+
leftLines.push(fitVisible(theme.cpu('CPU Usage History'), leftW));
|
|
691
|
+
|
|
692
|
+
const cpuHistory = opts.history?.cpuUsage || [];
|
|
693
|
+
if (cpuHistory.length > 1) {
|
|
694
|
+
const graphRows = multiRowSparkline(cpuHistory, 3, { min: 0, max: 100 });
|
|
695
|
+
for (const gr of graphRows) {
|
|
696
|
+
leftLines.push(theme.cpu(fitVisible(gr, leftW)));
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
leftLines.push(theme.cpu(fitVisible(gaugeBar(data.cpu.usage, leftW), leftW)));
|
|
700
|
+
leftLines.push(' '.repeat(leftW));
|
|
701
|
+
leftLines.push(' '.repeat(leftW));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const rightLines: string[] = [];
|
|
705
|
+
rightLines.push(fitVisible(chalk.bold(truncatePlain(data.cpu.brand, rightW)), rightW));
|
|
706
|
+
|
|
707
|
+
const cpuBarW = Math.max(6, rightW - 18);
|
|
708
|
+
const tempStr = data.cpu.temperature ? formatTempUnit(data.cpu.temperature, opts.tempUnit) : '';
|
|
709
|
+
rightLines.push(fitVisible(`CPU ${gaugeBar(data.cpu.usage, cpuBarW)} ${data.cpu.usage.toFixed(0).padStart(3)}% ${tempStr}`, rightW));
|
|
710
|
+
|
|
711
|
+
const loadStr = data.cpu.loadAvg.map(l => l.toFixed(2)).join(' ');
|
|
712
|
+
rightLines.push(fitVisible(`${chalk.dim('Load:')} ${loadStr}`, rightW));
|
|
713
|
+
|
|
714
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
715
|
+
const coreColW = Math.floor((rightW - 1) / 2);
|
|
716
|
+
const half = Math.ceil(data.cpu.coreUsage.length / 2);
|
|
717
|
+
for (let i = 0; i < Math.min(2, half); i++) {
|
|
718
|
+
const u1 = data.cpu.coreUsage[i];
|
|
719
|
+
const u2 = data.cpu.coreUsage[i + half];
|
|
720
|
+
const c1BarW = Math.max(3, coreColW - 8);
|
|
721
|
+
const c1Str = `C${i} ${gaugeBar(u1, c1BarW)} ${Math.round(u1)}%`;
|
|
722
|
+
const c2Str = u2 !== undefined ? `C${i + half} ${gaugeBar(u2, c1BarW)} ${Math.round(u2)}%` : '';
|
|
723
|
+
rightLines.push(fitVisible(fitVisible(c1Str, coreColW) + ' ' + fitVisible(c2Str, coreColW), rightW));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (data.gpu) {
|
|
728
|
+
rightLines.push(fitVisible(`GPU ${gaugeBar(data.gpu.utilization, cpuBarW)} ${data.gpu.utilization}% ${formatBytes(data.gpu.memory)}`, rightW));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const combinedBody = hstack([leftLines, rightLines], 2, contentWidth);
|
|
732
|
+
return panel('1 cpu', combinedBody, width, theme.cpu, theme.border, combinedBody.length, rightTitle);
|
|
733
|
+
} else {
|
|
734
|
+
const lines: string[] = [];
|
|
735
|
+
lines.push(chalk.bold(truncatePlain(data.cpu.brand, contentWidth)));
|
|
736
|
+
lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
|
|
737
|
+
if (opts.history?.cpuUsage.length) {
|
|
738
|
+
lines.push(theme.cpu(sparkline(opts.history.cpuUsage, { min: 0, max: 100 })));
|
|
739
|
+
}
|
|
740
|
+
lines.push(`Load: ${data.cpu.loadAvg.map(l => l.toFixed(2)).join(' ')}`);
|
|
741
|
+
return panel('1 cpu', lines, width, theme.cpu, theme.border, lines.length, rightTitle);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function btopLeftColumn(data: StatsData, width: number, theme: ThemeColors, opts: TableOptions): string[] {
|
|
746
|
+
const contentWidth = width - 4;
|
|
747
|
+
const out: string[] = [];
|
|
748
|
+
|
|
749
|
+
// --- Box 1: 2 mem ---
|
|
750
|
+
const memLines: string[] = [];
|
|
751
|
+
memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
|
|
752
|
+
memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
|
|
753
|
+
memLines.push(`Free: ${formatBytes(data.memory.free)} · Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
|
|
754
|
+
if (data.memory.swapTotal > 0) {
|
|
755
|
+
const swapPct = Math.round((data.memory.swapUsed / data.memory.swapTotal) * 100);
|
|
756
|
+
memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
|
|
757
|
+
}
|
|
758
|
+
out.push(...panel('2 mem', memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
|
|
759
|
+
|
|
760
|
+
// --- Box 2: 0 disk ---
|
|
761
|
+
if (data.disk.length) {
|
|
762
|
+
const diskLines: string[] = [];
|
|
763
|
+
for (const d of data.disk.slice(0, 2)) {
|
|
764
|
+
const capNum = parseInt(d.capacity, 10) || 0;
|
|
765
|
+
const mountShort = truncatePlain(d.mountpoint, 8);
|
|
766
|
+
diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18))}] ${d.capacity}`);
|
|
767
|
+
}
|
|
768
|
+
out.push(...panel('0 disk io', diskLines, width, theme.disk, theme.border, diskLines.length));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// --- Box 3: 7 net ---
|
|
772
|
+
const netLines: string[] = [];
|
|
773
|
+
const rxRate = opts.history?.netRxRate.length ? opts.history.netRxRate[opts.history.netRxRate.length - 1] : 0;
|
|
774
|
+
const txRate = opts.history?.netTxRate.length ? opts.history.netTxRate[opts.history.netTxRate.length - 1] : 0;
|
|
775
|
+
|
|
776
|
+
netLines.push(`down ▼ ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
|
|
777
|
+
if (opts.history?.netRxRate.length) {
|
|
778
|
+
netLines.push(chalk.cyan(fitVisible(sparkline(opts.history.netRxRate, {}), contentWidth)));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
netLines.push(`up ▲ ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
|
|
782
|
+
if (opts.history?.netTxRate.length) {
|
|
783
|
+
netLines.push(theme.network(fitVisible(sparkline(opts.history.netTxRate, {}), contentWidth)));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
out.push(...panel('7 net', netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
|
|
787
|
+
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function btopProcBox(data: StatsData, width: number, targetHeight: number, theme: ThemeColors, opts: TableOptions): string[] {
|
|
792
|
+
const contentWidth = width - 4;
|
|
793
|
+
const filtered = opts.filter
|
|
794
|
+
? data.processes.filter(p => p.command.toLowerCase().includes(opts.filter!.toLowerCase()))
|
|
795
|
+
: data.processes;
|
|
796
|
+
const sorted = sortProcesses(filtered, opts.sortBy ?? 'cpu');
|
|
797
|
+
|
|
798
|
+
const headerRowOverhead = 2; // top & bottom border
|
|
799
|
+
const tableHeaderOverhead = 2; // header + separator line
|
|
800
|
+
const availableRows = Math.max(1, targetHeight - headerRowOverhead - tableHeaderOverhead);
|
|
801
|
+
|
|
802
|
+
const limited = sorted.slice(0, availableRows);
|
|
803
|
+
|
|
804
|
+
const pidW = 6, userW = 7, thrW = 3, memW = 6, cpuW = 6;
|
|
805
|
+
const fixedW = pidW + userW + thrW + memW + cpuW + 5;
|
|
806
|
+
const cmdW = Math.max(8, contentWidth - fixedW);
|
|
807
|
+
|
|
808
|
+
const head =
|
|
809
|
+
chalk.bold('PID'.padEnd(pidW)) + ' ' +
|
|
810
|
+
chalk.bold('PROGRAM'.padEnd(cmdW)) + ' ' +
|
|
811
|
+
chalk.bold('USER'.padEnd(userW)) + ' ' +
|
|
812
|
+
chalk.bold('THR'.padStart(thrW)) + ' ' +
|
|
813
|
+
chalk.bold('MEM%'.padStart(memW)) + ' ' +
|
|
814
|
+
chalk.bold('CPU%'.padStart(cpuW));
|
|
815
|
+
|
|
816
|
+
if (opts.treeView) {
|
|
817
|
+
const treeLines = buildProcessTree(limited);
|
|
818
|
+
const rightTitle = opts.filter ? `filter: "${opts.filter}"` : `tree · sort: ${opts.sortBy || 'cpu'}`;
|
|
819
|
+
return panel('P proc', [head, theme.border('─'.repeat(contentWidth)), ...treeLines], width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const rows = limited.map(p => {
|
|
823
|
+
const cpuVal = p.cpu.toFixed(1) + '%';
|
|
824
|
+
const cpuStr = p.cpu > 50 ? chalk.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk.yellow(cpuVal.padStart(cpuW)) : chalk.green(cpuVal.padStart(cpuW));
|
|
825
|
+
const memStr = (p.mem.toFixed(1) + '%').padStart(memW);
|
|
826
|
+
const userStr = truncatePlain(p.user, userW).padEnd(userW);
|
|
827
|
+
const thrStr = String(p.threads).padStart(thrW);
|
|
828
|
+
const pidStr = String(p.pid).padEnd(pidW);
|
|
829
|
+
const cmdStr = truncatePlain(p.command, cmdW).padEnd(cmdW);
|
|
830
|
+
|
|
831
|
+
return `${pidStr} ${cmdStr} ${userStr} ${thrStr} ${memStr} ${cpuStr}`;
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
const lines = [head, theme.border('─'.repeat(contentWidth)), ...rows];
|
|
835
|
+
if (rows.length === 0) {
|
|
836
|
+
lines.push(chalk.dim(`No processes match "${opts.filter || ''}"`));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const rightTitle = opts.filter ? `filter: "${opts.filter}"` : `sort: ${opts.sortBy || 'cpu'}`;
|
|
840
|
+
return panel('P proc', lines, width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
841
|
+
}
|
|
842
|
+
|
|
574
843
|
export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
575
844
|
const width = clampWidth(opts.width);
|
|
576
845
|
const activePanel = opts.activePanel || 'grid';
|
|
577
|
-
const columns = gridColumns(width);
|
|
578
|
-
const gap = 2;
|
|
579
|
-
const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
|
|
580
|
-
const contentWidth = cardWidth - 4;
|
|
581
846
|
|
|
582
847
|
const themeName = opts.theme || 'default';
|
|
583
848
|
const theme = THEMES[themeName] || THEMES.default;
|
|
584
|
-
const vis = opts.visible ?? {};
|
|
585
849
|
|
|
586
850
|
const out: string[] = [];
|
|
587
851
|
out.push(...header(data, width, [], theme.border));
|
|
588
852
|
out.push('');
|
|
853
|
+
|
|
854
|
+
const anomalies = opts.anomalies ?? [];
|
|
855
|
+
if (anomalies.length && activePanel === 'grid') {
|
|
856
|
+
out.push(...anomalyPanel(anomalies, width, theme));
|
|
857
|
+
out.push('');
|
|
858
|
+
}
|
|
859
|
+
|
|
589
860
|
out.push(tabBar(activePanel, width, theme));
|
|
590
861
|
out.push('');
|
|
591
862
|
|
|
@@ -597,88 +868,36 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
|
597
868
|
return out.join('\n');
|
|
598
869
|
}
|
|
599
870
|
|
|
600
|
-
|
|
601
|
-
|
|
871
|
+
// --- btop Dashboard Grid ---
|
|
872
|
+
// 1. Top CPU & System Overview Box
|
|
873
|
+
out.push(...btopCpuBox(data, width, theme, opts));
|
|
874
|
+
out.push('');
|
|
602
875
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
cards.push({ title: 'Memory', accent: theme.mem, lines: memCard(data, contentWidth) });
|
|
608
|
-
}
|
|
609
|
-
if (vis.gpu !== false) {
|
|
610
|
-
const gpu = gpuCard(data, contentWidth);
|
|
611
|
-
if (gpu) cards.push({ title: 'GPU', accent: theme.gpu, lines: gpu });
|
|
612
|
-
}
|
|
613
|
-
if (vis.power !== false) {
|
|
614
|
-
const power = powerCard(data);
|
|
615
|
-
if (power) cards.push({ title: 'Power', accent: theme.power, lines: power });
|
|
616
|
-
}
|
|
617
|
-
if (vis.battery !== false) {
|
|
618
|
-
const battery = batteryCard(data, contentWidth);
|
|
619
|
-
if (battery) cards.push({ title: 'Battery', accent: theme.battery, lines: battery });
|
|
620
|
-
}
|
|
621
|
-
if (vis.thermal !== false) {
|
|
622
|
-
cards.push({ title: 'Thermal', accent: theme.thermal, lines: thermalCard(data, contentWidth) });
|
|
623
|
-
}
|
|
624
|
-
if (vis.network !== false) {
|
|
625
|
-
cards.push({ title: 'Network', accent: theme.network, lines: networkCard(data, contentWidth) });
|
|
626
|
-
}
|
|
627
|
-
if (vis.packets !== false) {
|
|
628
|
-
const packets = packetCard(data, contentWidth);
|
|
629
|
-
if (packets) cards.push({ title: 'Packets', accent: theme.network, lines: packets });
|
|
630
|
-
}
|
|
631
|
-
if (vis.tasks !== false) {
|
|
632
|
-
const tasks = tasksCard(data, contentWidth);
|
|
633
|
-
if (tasks) cards.push({ title: 'Tasks', accent: theme.process, lines: tasks });
|
|
634
|
-
}
|
|
876
|
+
// 2. Bottom Split Section
|
|
877
|
+
if (width >= 85) {
|
|
878
|
+
const leftColW = Math.max(32, Math.floor(width * 0.38));
|
|
879
|
+
const rightColW = width - leftColW - 2;
|
|
635
880
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const rowHeight = Math.max(...row.map(c => c.lines.length));
|
|
639
|
-
const blocks = row.map(c => panel(c.title, c.lines, cardWidth, c.accent, theme.border, rowHeight));
|
|
640
|
-
out.push(...hstack(blocks, gap, width));
|
|
641
|
-
out.push('');
|
|
642
|
-
}
|
|
881
|
+
const leftLines = btopLeftColumn(data, leftColW, theme, opts);
|
|
882
|
+
const procLines = btopProcBox(data, rightColW, leftLines.length, theme, opts);
|
|
643
883
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
884
|
+
out.push(...hstack([leftLines, procLines], 2, width));
|
|
885
|
+
} else {
|
|
886
|
+
const leftLines = btopLeftColumn(data, width, theme, opts);
|
|
887
|
+
out.push(...leftLines);
|
|
647
888
|
out.push('');
|
|
889
|
+
const procLines = btopProcBox(data, width, 12, theme, opts);
|
|
890
|
+
out.push(...procLines);
|
|
648
891
|
}
|
|
649
892
|
|
|
650
|
-
if (vis.process !== false) {
|
|
651
|
-
const filteredProcesses = opts.filter
|
|
652
|
-
? data.processes.filter(p => p.command.toLowerCase().includes(opts.filter!.toLowerCase()))
|
|
653
|
-
: data.processes;
|
|
654
|
-
const sortedProcesses = sortProcesses(filteredProcesses, opts.sortBy ?? 'cpu');
|
|
655
|
-
const limited = opts.processLimit ? sortedProcesses.slice(0, opts.processLimit) : sortedProcesses;
|
|
656
|
-
|
|
657
|
-
const procTitle = opts.filter
|
|
658
|
-
? `Processes · filter "${opts.filter}" · sort ${opts.sortBy ?? 'cpu'}`
|
|
659
|
-
: `Processes · sort ${opts.sortBy ?? 'cpu'}`;
|
|
660
|
-
|
|
661
|
-
if (opts.treeView) {
|
|
662
|
-
const treeLines = buildProcessTree(limited);
|
|
663
|
-
if (treeLines.length) {
|
|
664
|
-
out.push(...panel(procTitle + ' · tree', treeLines, width, theme.process, theme.border));
|
|
665
|
-
} else if (opts.filter) {
|
|
666
|
-
out.push(...panel(procTitle, [chalk.dim(`No processes match "${opts.filter}"`)], width, theme.process, theme.border));
|
|
667
|
-
}
|
|
668
|
-
} else if (limited.length) {
|
|
669
|
-
out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
|
|
670
|
-
} else if (opts.filter) {
|
|
671
|
-
out.push(...panel(procTitle, [chalk.dim(`No processes match "${opts.filter}"`)], width, theme.process, theme.border));
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
893
|
return out.join('\n');
|
|
676
894
|
}
|
|
677
895
|
|
|
896
|
+
|
|
678
897
|
/** Clamp/normalize a terminal width to a sane range, defaulting to 80 cols. */
|
|
679
898
|
export function clampWidth(width?: number): number {
|
|
680
899
|
if (!width || Number.isNaN(width)) return 80;
|
|
681
|
-
return Math.max(
|
|
900
|
+
return Math.max(40, Math.min(width, 240));
|
|
682
901
|
}
|
|
683
902
|
|
|
684
903
|
/**
|
package/src/formatters/types.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ThemeName } from './themes.js';
|
|
10
10
|
import type { StatsData } from '../monitors/types.js';
|
|
11
|
+
import type { History } from '../history.js';
|
|
11
12
|
|
|
12
13
|
export type { ThemeName };
|
|
13
14
|
|
|
@@ -42,21 +43,36 @@ export interface VisibleItems {
|
|
|
42
43
|
tree?: boolean;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export interface AnomalyAlert {
|
|
47
|
+
metric: string;
|
|
48
|
+
value: number;
|
|
49
|
+
zScore: number;
|
|
50
|
+
severity: 'warning' | 'critical';
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
export interface TableOptions {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
/** Terminal width in columns; controls layout, bar sizing, and column count. Defaults to 80. */
|
|
55
|
+
width?: number;
|
|
56
|
+
/** Process sort key. Defaults to 'cpu'. */
|
|
57
|
+
sortBy?: 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
|
|
58
|
+
/** Case-insensitive substring filter applied to the process command. */
|
|
59
|
+
filter?: string;
|
|
60
|
+
/** Max process rows to show (the live dashboard sizes this to available terminal height). */
|
|
61
|
+
processLimit?: number;
|
|
62
|
+
/** Selected visual theme. */
|
|
63
|
+
theme?: ThemeName;
|
|
64
|
+
/** Visibility toggle settings for individual cards. */
|
|
65
|
+
visible?: VisibleItems;
|
|
66
|
+
/** Show process tree view instead of flat list. */
|
|
67
|
+
treeView?: boolean;
|
|
68
|
+
/** Active panel for detail/focus view. 'grid' means show all panels. */
|
|
69
|
+
activePanel?: 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p';
|
|
70
|
+
/** Active anomaly alerts from the statistical detector. */
|
|
71
|
+
anomalies?: AnomalyAlert[];
|
|
72
|
+
/** Temperature display unit: 'c' or 'f'. */
|
|
73
|
+
tempUnit?: 'c' | 'f';
|
|
74
|
+
/** Rolling history instance for live graphs. */
|
|
75
|
+
history?: History;
|
|
76
|
+
/** Graph mode: 'spark' or 'bar'. */
|
|
77
|
+
graphMode?: 'spark' | 'bar';
|
|
78
|
+
}
|
package/src/history.ts
CHANGED
|
@@ -47,21 +47,65 @@ export class History {
|
|
|
47
47
|
txPacketRate: number[] = [];
|
|
48
48
|
|
|
49
49
|
/** Rolling window of active TCP connections. */
|
|
50
|
-
|
|
50
|
+
connections: number[] = [];
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
private lastRxBytes = 0;
|
|
53
|
+
private lastTxBytes = 0;
|
|
54
|
+
private lastRxPackets = 0;
|
|
55
|
+
private lastTxPackets = 0;
|
|
56
|
+
private lastTs = 0;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Create a new History instance.
|
|
60
|
+
* @param maxLen - Maximum number of samples to retain. Defaults to 40.
|
|
61
|
+
*/
|
|
62
|
+
constructor(maxLen = 40) {
|
|
63
|
+
this.maxLen = maxLen;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compute the arithmetic mean of a numeric array.
|
|
68
|
+
* Returns 0 if the array is empty.
|
|
69
|
+
*/
|
|
70
|
+
static mean(arr: number[]): number {
|
|
71
|
+
if (!arr.length) return 0;
|
|
72
|
+
return arr.reduce((s, v) => s + v, 0) / arr.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compute the population standard deviation of a numeric array.
|
|
77
|
+
* Returns 0 if the array has fewer than 2 elements.
|
|
78
|
+
*/
|
|
79
|
+
static stdDev(arr: number[]): number {
|
|
80
|
+
if (arr.length < 2) return 0;
|
|
81
|
+
const m = History.mean(arr);
|
|
82
|
+
const variance = arr.reduce((s, v) => s + (v - m) ** 2, 0) / arr.length;
|
|
83
|
+
return Math.sqrt(variance);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Compute the z-score of a value against a reference array.
|
|
88
|
+
* Returns 0 when the array is empty or has zero std dev.
|
|
89
|
+
*/
|
|
90
|
+
static zScore(value: number, arr: number[]): number {
|
|
91
|
+
const m = History.mean(arr);
|
|
92
|
+
const sd = History.stdDev(arr);
|
|
93
|
+
if (sd === 0) return 0;
|
|
94
|
+
return (value - m) / sd;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check whether a value is a statistical outlier against a
|
|
99
|
+
* reference array.
|
|
100
|
+
* @param value - The current reading
|
|
101
|
+
* @param arr - The rolling history window
|
|
102
|
+
* @param threshold - Z-score threshold (default 2.5)
|
|
103
|
+
* @returns true if the value deviates from the baseline by more than `threshold` standard deviations
|
|
104
|
+
*/
|
|
105
|
+
static isAnomaly(value: number, arr: number[], threshold = 2.5): boolean {
|
|
106
|
+
if (arr.length < 5) return false;
|
|
107
|
+
return Math.abs(History.zScore(value, arr)) > threshold;
|
|
108
|
+
}
|
|
65
109
|
|
|
66
110
|
/**
|
|
67
111
|
* Push a new sample into the rolling window.
|