pyre-cli 1.3.0 → 2.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.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Data-export and graph-rendering helpers.
3
+ *
4
+ * Provides JSON, CSV, and TSV serialisation of a
5
+ * {@link StatsData} snapshot, as well as sparkline-graph
6
+ * rendering for the live dashboard.
7
+ */
8
+ import chalk from 'chalk';
9
+ import { sparkline } from '../sparkline.js';
10
+ import type { History } from '../history.js';
11
+ import type { StatsData } from './types.js';
12
+ import { THEMES, type ThemeName } from './themes.js';
13
+ import { formatBytes, celsiusToFahrenheit, formatTemp } from './render.js';
14
+ import { panel } from './render.js';
15
+
16
+ export function formatJson(data: StatsData): string {
17
+ return JSON.stringify(data, null, 2);
18
+ }
19
+
20
+ export function formatCsv(data: StatsData): string {
21
+ const rows: string[][] = [
22
+ ['property', 'key', 'value'],
23
+ ['cpu', 'brand', data.cpu.brand],
24
+ ['cpu', 'usage', String(data.cpu.usage)],
25
+ ['cpu', 'loadAvg_1m', String(data.cpu.loadAvg[0])],
26
+ ['cpu', 'loadAvg_5m', String(data.cpu.loadAvg[1])],
27
+ ['cpu', 'loadAvg_15m', String(data.cpu.loadAvg[2])],
28
+ ['memory', 'total', String(data.memory.total)],
29
+ ['memory', 'used', String(data.memory.used)],
30
+ ['memory', 'usagePercent', String(data.memory.usagePercent)],
31
+ ['memory', 'swapUsed', String(data.memory.swapUsed)],
32
+ ['memory', 'swapTotal', String(data.memory.swapTotal)],
33
+ ['thermal', 'state', data.thermal.state],
34
+ ['thermal', 'cpuTempC', data.thermal.temperatures?.cpu_die ? String(data.thermal.temperatures.cpu_die) : ''],
35
+ [
36
+ 'thermal',
37
+ 'cpuTempF',
38
+ data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : '',
39
+ ],
40
+ ['network', 'rxBytes', String(data.network.rxBytes)],
41
+ ['network', 'txBytes', String(data.network.txBytes)],
42
+ ['network', 'rxPackets', String(data.network.rxPackets)],
43
+ ['network', 'txPackets', String(data.network.txPackets)],
44
+ ['network', 'connections', data.network.connections ? String(data.network.connections) : ''],
45
+ ['battery', 'level', data.battery ? String(data.battery.level) : ''],
46
+ ['battery', 'powerSource', data.battery ? data.battery.powerSource : ''],
47
+ ['battery', 'condition', data.battery?.condition ?? ''],
48
+ ['battery', 'maxCapacityPercent', data.battery?.maxCapacityPercent !== undefined ? String(data.battery.maxCapacityPercent) : ''],
49
+ ['battery', 'cycles', data.battery?.cycles !== undefined ? String(data.battery.cycles) : ''],
50
+ ['battery', 'estimatedTimeToEmpty', data.battery?.estimatedTimeToEmpty ?? ''],
51
+ ['battery', 'dischargeRatePerHour', data.battery?.dischargeRatePerHour !== undefined ? String(data.battery.dischargeRatePerHour) : ''],
52
+ ['battery', 'powerWatts', data.battery?.powerWatts !== undefined ? String(data.battery.powerWatts) : ''],
53
+ ['power', 'cpuWatts', data.power?.cpuWatts !== undefined ? String(data.power.cpuWatts) : ''],
54
+ ['power', 'gpuWatts', data.power?.gpuWatts !== undefined ? String(data.power.gpuWatts) : ''],
55
+ ['power', 'combinedWatts', data.power?.combinedWatts !== undefined ? String(data.power.combinedWatts) : ''],
56
+ ['system', 'hostname', data.header.hostname],
57
+ ['system', 'uptime', data.header.uptime],
58
+ ['system', 'timestamp', data.timestamp],
59
+ ];
60
+
61
+ if (data.packets) {
62
+ rows.push(['packets', 'totalPackets', String(data.packets.totalPackets)]);
63
+ rows.push(['packets', 'rxPackets', String(data.packets.rxPackets)]);
64
+ rows.push(['packets', 'txPackets', String(data.packets.txPackets)]);
65
+ rows.push(['packets', 'connections', String(data.packets.connections)]);
66
+ for (const iface of data.packets.interfaces || []) {
67
+ rows.push(['packets_iface', iface.iface, `${iface.rxPackets},${iface.txPackets},${iface.rxBytes},${iface.txBytes}`]);
68
+ }
69
+ for (const proc of data.packets.allProcesses || []) {
70
+ rows.push(['packets_proc', `pid=${proc.pid}`, `${proc.command},${proc.rxBytes},${proc.txBytes}`]);
71
+ }
72
+ }
73
+
74
+ if (data.gpu) {
75
+ rows.push(['gpu', 'model', data.gpu.model]);
76
+ rows.push(['gpu', 'memory', String(data.gpu.memory)]);
77
+ rows.push(['gpu', 'utilization', String(data.gpu.utilization)]);
78
+ if (data.gpu.temperature) rows.push(['gpu', 'tempC', String(data.gpu.temperature)]);
79
+ rows.push(['gpu', 'processes', String(data.gpu.processes)]);
80
+ }
81
+
82
+ for (const p of data.processes) {
83
+ rows.push(['process', `pid=${p.pid}`, `${p.pid}`]);
84
+ rows.push(['process', `ppid=${p.pid}`, String(p.ppid)]);
85
+ rows.push(['process', `user=${p.pid}`, p.user]);
86
+ rows.push(['process', `cpu=${p.pid}`, String(p.cpu)]);
87
+ rows.push(['process', `mem=${p.pid}`, String(p.mem)]);
88
+ rows.push(['process', `state=${p.pid}`, p.state]);
89
+ rows.push(['process', `threads=${p.pid}`, String(p.threads)]);
90
+ rows.push(['process', `runtime=${p.pid}`, String(p.runtime)]);
91
+ rows.push(['process', `command=${p.pid}`, p.command]);
92
+ }
93
+ return rows.map(r => r.map(item => `"${String(item).replace(/"/g, '""')}"`).join(',')).join('\n');
94
+ }
95
+
96
+ export function formatTsv(data: StatsData): string {
97
+ return formatCsv(data).split('\n').map(r => r.split(',').join('\t')).join('\n');
98
+ }
99
+
100
+ export function formatGraphs(history: History, width = 80, themeName: ThemeName = 'default', graphMode: 'spark' | 'bar' = 'spark'): string {
101
+ const theme = THEMES[themeName] || THEMES.default;
102
+ const contentWidth = width - 4;
103
+ const sampleCount = history.cpuUsage.length;
104
+ const sparkWidth = Math.max(10, contentWidth - 28);
105
+
106
+ const lines: string[] = [];
107
+ lines.push(graphRow('CPU %', history.cpuUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, graphMode));
108
+ lines.push(graphRow('Mem %', history.memUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, graphMode));
109
+ lines.push(
110
+ graphRow('Temp', history.temp, {}, formatTemp, sparkWidth, 'no sensor access (needs sudo powermetrics)', graphMode)
111
+ );
112
+ lines.push(graphRow('Net RX/s', history.netRxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, graphMode));
113
+ lines.push(graphRow('Net TX/s', history.netTxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, graphMode));
114
+ if (history.powerWatts.length) {
115
+ lines.push(graphRow('Power W', history.powerWatts, {}, v => `${v.toFixed(1)} W`, sparkWidth, graphMode));
116
+ }
117
+ if (history.rxPacketRate.length) {
118
+ lines.push(graphRow('RX pkt/s', history.rxPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
119
+ }
120
+ if (history.txPacketRate.length) {
121
+ lines.push(graphRow('TX pkt/s', history.txPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
122
+ }
123
+
124
+ const title = `Graphs · mode:${graphMode} · last ${sampleCount} sample${sampleCount === 1 ? '' : 's'}`;
125
+ return panel(title, lines, width, theme.graphs, theme.border).join('\n');
126
+ }
127
+
128
+ function graphRow(
129
+ label: string,
130
+ values: number[],
131
+ bounds: { min?: number; max?: number },
132
+ fmt: (v: number) => string,
133
+ sparkWidth = 40,
134
+ emptyMessage = 'collecting data...',
135
+ mode: 'spark' | 'bar' = 'spark'
136
+ ): string {
137
+ if (!values.length) {
138
+ return `${chalk.dim(label.padEnd(10))} ${chalk.dim(emptyMessage)}`;
139
+ }
140
+ const visible = values.slice(-sparkWidth);
141
+ const spark = mode === 'spark' ? sparkline(visible, bounds) : barGraph(visible, bounds, sparkWidth);
142
+ const current = values[values.length - 1];
143
+
144
+ let color = chalk.cyan;
145
+ if (bounds.min !== undefined && bounds.max !== undefined) {
146
+ const pct = ((current - bounds.min) / (bounds.max - bounds.min || 1)) * 100;
147
+ color = pct > 90 ? chalk.red : pct > 70 ? chalk.yellow : chalk.green;
148
+ }
149
+
150
+ return `${chalk.dim(label.padEnd(10))}${color(spark)} ${chalk.bold(fmt(current))}`;
151
+ }
152
+
153
+ function barGraph(values: number[], bounds: { min?: number; max?: number }, width: number): string {
154
+ const min = bounds.min ?? Math.min(...values);
155
+ const max = bounds.max ?? Math.max(...values);
156
+ const range = max - min || 1;
157
+ return values.map(v => {
158
+ const pct = (v - min) / range;
159
+ if (pct > 0.8) return '█';
160
+ if (pct > 0.6) return '▇';
161
+ if (pct > 0.4) return '▆';
162
+ if (pct > 0.2) return '▄';
163
+ return ' ';
164
+ }).join('');
165
+ }