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,115 @@
1
+ /**
2
+ * Visual theme definitions for the pyre dashboard.
3
+ *
4
+ * Each theme maps UI element roles (border, cpu, mem, …) to a
5
+ * chalk colour function. The {@link THEMES} record provides four
6
+ * built-in themes; additional themes can be added by extending
7
+ * the {@link ThemeName} union and adding an entry here.
8
+ */
9
+ import chalk from 'chalk';
10
+
11
+ export type ThemeName = 'default' | 'dracula' | 'cyberpunk' | 'monochrome' | 'nord' | 'gruvbox';
12
+
13
+ export interface ThemeColors {
14
+ border: (s: string) => string;
15
+ cpu: (s: string) => string;
16
+ mem: (s: string) => string;
17
+ gpu: (s: string) => string;
18
+ power: (s: string) => string;
19
+ battery: (s: string) => string;
20
+ thermal: (s: string) => string;
21
+ network: (s: string) => string;
22
+ disk: (s: string) => string;
23
+ graphs: (s: string) => string;
24
+ process: (s: string) => string;
25
+ }
26
+
27
+ /**
28
+ * Built-in colour themes.
29
+ *
30
+ * - **default** — muted greys and cyan accents, safe for any terminal.
31
+ * - **dracula** — the popular Dracula dark palette.
32
+ * - **cyberpunk** — high-contrast neon on black.
33
+ * - **monochrome** — white-only, useful for colour-blind users or
34
+ * terminals without 256-colour support.
35
+ */
36
+ export const THEMES: Record<ThemeName, ThemeColors> = {
37
+ default: {
38
+ border: chalk.hex('#4b5563'),
39
+ cpu: chalk.hex('#22d3ee'),
40
+ mem: chalk.hex('#a78bfa'),
41
+ gpu: chalk.hex('#f472b6'),
42
+ power: chalk.hex('#fbbf24'),
43
+ battery: chalk.hex('#34d399'),
44
+ thermal: chalk.hex('#fb923c'),
45
+ network: chalk.hex('#60a5fa'),
46
+ disk: chalk.hex('#f472b6'),
47
+ graphs: chalk.hex('#22d3ee'),
48
+ process: chalk.hex('#e5e7eb'),
49
+ },
50
+ dracula: {
51
+ border: chalk.hex('#6272a4'),
52
+ cpu: chalk.hex('#8be9fd'),
53
+ mem: chalk.hex('#bd93f9'),
54
+ gpu: chalk.hex('#ff5555'),
55
+ power: chalk.hex('#f1fa8c'),
56
+ battery: chalk.hex('#50fa7b'),
57
+ thermal: chalk.hex('#ffb86c'),
58
+ network: chalk.hex('#ff79c6'),
59
+ disk: chalk.hex('#ff5555'),
60
+ graphs: chalk.hex('#8be9fd'),
61
+ process: chalk.hex('#f8f8f2'),
62
+ },
63
+ cyberpunk: {
64
+ border: chalk.hex('#ff0055'),
65
+ cpu: chalk.hex('#00ffcc'),
66
+ mem: chalk.hex('#ff00ff'),
67
+ gpu: chalk.hex('#ffff00'),
68
+ power: chalk.hex('#ffff00'),
69
+ battery: chalk.hex('#00ff00'),
70
+ thermal: chalk.hex('#ff6600'),
71
+ network: chalk.hex('#00ffff'),
72
+ disk: chalk.hex('#ff007f'),
73
+ graphs: chalk.hex('#00ffcc'),
74
+ process: chalk.hex('#ffffff'),
75
+ },
76
+ monochrome: {
77
+ border: chalk.gray,
78
+ cpu: chalk.white.bold,
79
+ mem: chalk.white.bold,
80
+ gpu: chalk.white.bold,
81
+ power: chalk.white.bold,
82
+ battery: chalk.white.bold,
83
+ thermal: chalk.white.bold,
84
+ network: chalk.white.bold,
85
+ disk: chalk.white.bold,
86
+ graphs: chalk.white.bold,
87
+ process: chalk.white.bold,
88
+ },
89
+ nord: {
90
+ border: chalk.hex('#4c566a'),
91
+ cpu: chalk.hex('#88c0d0'),
92
+ mem: chalk.hex('#b48ead'),
93
+ gpu: chalk.hex('#bf616a'),
94
+ power: chalk.hex('#ebcb8b'),
95
+ battery: chalk.hex('#a3be8c'),
96
+ thermal: chalk.hex('#d08770'),
97
+ network: chalk.hex('#81a1c1'),
98
+ disk: chalk.hex('#b48ead'),
99
+ graphs: chalk.hex('#88c0d0'),
100
+ process: chalk.hex('#eceff4'),
101
+ },
102
+ gruvbox: {
103
+ border: chalk.hex('#928374'),
104
+ cpu: chalk.hex('#fb4934'),
105
+ mem: chalk.hex('#b8bb26'),
106
+ gpu: chalk.hex('#d3869b'),
107
+ power: chalk.hex('#fabd2f'),
108
+ battery: chalk.hex('#b8bb26'),
109
+ thermal: chalk.hex('#fb4934'),
110
+ network: chalk.hex('#83a598'),
111
+ disk: chalk.hex('#d3869b'),
112
+ graphs: chalk.hex('#fb4934'),
113
+ process: chalk.hex('#ebdbb2'),
114
+ },
115
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Public-facing type definitions for the formatters module.
3
+ *
4
+ * These interfaces control how the dashboard is laid out and
5
+ * which panels are visible. They are consumed by both the
6
+ * static `formatTable` function and the live dashboard in
7
+ * `live/`.
8
+ */
9
+ import type { ThemeName } from './themes.js';
10
+ import type { StatsData } from '../monitors/types.js';
11
+
12
+ export type { ThemeName };
13
+
14
+ export interface ThemeColors {
15
+ border: (s: string) => string;
16
+ cpu: (s: string) => string;
17
+ mem: (s: string) => string;
18
+ gpu: (s: string) => string;
19
+ power: (s: string) => string;
20
+ battery: (s: string) => string;
21
+ thermal: (s: string) => string;
22
+ network: (s: string) => string;
23
+ disk: (s: string) => string;
24
+ graphs: (s: string) => string;
25
+ process: (s: string) => string;
26
+ }
27
+
28
+ export type { StatsData };
29
+
30
+ export interface VisibleItems {
31
+ cpu?: boolean;
32
+ mem?: boolean;
33
+ gpu?: boolean;
34
+ power?: boolean;
35
+ battery?: boolean;
36
+ thermal?: boolean;
37
+ network?: boolean;
38
+ packets?: boolean;
39
+ tasks?: boolean;
40
+ disk?: boolean;
41
+ process?: boolean;
42
+ tree?: boolean;
43
+ }
44
+
45
+ export interface TableOptions {
46
+ /** Terminal width in columns; controls layout, bar sizing, and column count. Defaults to 80. */
47
+ width?: number;
48
+ /** Process sort key. Defaults to 'cpu'. */
49
+ sortBy?: 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
50
+ /** Case-insensitive substring filter applied to the process command. */
51
+ filter?: string;
52
+ /** Max process rows to show (the live dashboard sizes this to available terminal height). */
53
+ processLimit?: number;
54
+ /** Selected visual theme. */
55
+ theme?: ThemeName;
56
+ /** Visibility toggle settings for individual cards. */
57
+ visible?: VisibleItems;
58
+ /** Show process tree view instead of flat list. */
59
+ treeView?: boolean;
60
+ /** Active panel for detail/focus view. 'grid' means show all panels. */
61
+ activePanel?: 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process';
62
+ }
package/src/history.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Rolling window of recent metric samples, used to render
3
+ * sparkline graphs in the live dashboard.
4
+ *
5
+ * Network rates (bytes/sec) are derived from the cumulative
6
+ * counters returned by the monitors module by computing the
7
+ * delta between consecutive samples.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const h = new History(60);
12
+ * h.push({ cpuUsage: 42, memUsage: 55, temp: 62, rxBytes: 1024, txBytes: 512 });
13
+ * ```
14
+ */
15
+ export class History {
16
+ /** Maximum number of samples retained in the rolling window. */
17
+ maxLen: number;
18
+
19
+ /** Rolling window of CPU usage percentages (0–100). */
20
+ cpuUsage: number[] = [];
21
+
22
+ /** Rolling window of memory usage percentages (0–100). */
23
+ memUsage: number[] = [];
24
+
25
+ /** Rolling window of CPU die temperatures in °C. */
26
+ temp: number[] = [];
27
+
28
+ /** Rolling window of network receive rates in bytes/sec. */
29
+ netRxRate: number[] = [];
30
+
31
+ /** Rolling window of network transmit rates in bytes/sec. */
32
+ netTxRate: number[] = [];
33
+
34
+ /** Rolling window of GPU utilization percentages (0–100). */
35
+ gpuUtil: number[] = [];
36
+
37
+ /** Rolling window of combined power draw in watts. */
38
+ powerWatts: number[] = [];
39
+
40
+ /** Rolling window of network receive packet rates in packets/sec. */
41
+ rxPacketRate: number[] = [];
42
+
43
+ /** Rolling window of network transmit packet rates in packets/sec. */
44
+ txPacketRate: number[] = [];
45
+
46
+ /** Rolling window of active TCP connections. */
47
+ connections: number[] = [];
48
+
49
+ private lastRxBytes = 0;
50
+ private lastTxBytes = 0;
51
+ private lastRxPackets = 0;
52
+ private lastTxPackets = 0;
53
+ private lastTs = 0;
54
+
55
+ /**
56
+ * Create a new History instance.
57
+ * @param maxLen - Maximum number of samples to retain. Defaults to 40.
58
+ */
59
+ constructor(maxLen = 40) {
60
+ this.maxLen = maxLen;
61
+ }
62
+
63
+ /**
64
+ * Push a new sample into the rolling window.
65
+ *
66
+ * Network rates are computed as bytes/sec from the cumulative
67
+ * counters. The first sample always reports 0 for rates
68
+ * because there is no prior sample to compute a delta from.
69
+ */
70
+ push(sample: {
71
+ cpuUsage: number;
72
+ memUsage: number;
73
+ temp: number | null;
74
+ rxBytes: number;
75
+ txBytes: number;
76
+ gpuUtil?: number;
77
+ powerWatts?: number;
78
+ rxPackets?: number;
79
+ txPackets?: number;
80
+ connections?: number;
81
+ }) {
82
+ const now = Date.now();
83
+ let rxRate = 0;
84
+ let txRate = 0;
85
+ let rxPktRate = 0;
86
+ let txPktRate = 0;
87
+
88
+ if (this.lastTs > 0) {
89
+ const dtSeconds = Math.max((now - this.lastTs) / 1000, 0.001);
90
+ rxRate = Math.max(0, (sample.rxBytes - this.lastRxBytes) / dtSeconds);
91
+ txRate = Math.max(0, (sample.txBytes - this.lastTxBytes) / dtSeconds);
92
+ if (sample.rxPackets !== undefined) {
93
+ rxPktRate = Math.max(0, (sample.rxPackets - this.lastRxPackets) / dtSeconds);
94
+ }
95
+ if (sample.txPackets !== undefined) {
96
+ txPktRate = Math.max(0, (sample.txPackets - this.lastTxPackets) / dtSeconds);
97
+ }
98
+ }
99
+
100
+ this.lastRxBytes = sample.rxBytes;
101
+ this.lastTxBytes = sample.txBytes;
102
+ if (sample.rxPackets !== undefined) this.lastRxPackets = sample.rxPackets;
103
+ if (sample.txPackets !== undefined) this.lastTxPackets = sample.txPackets;
104
+ this.lastTs = now;
105
+
106
+ this.pushBounded(this.cpuUsage, sample.cpuUsage);
107
+ this.pushBounded(this.memUsage, sample.memUsage);
108
+ if (sample.temp !== null) this.pushBounded(this.temp, sample.temp);
109
+ this.pushBounded(this.netRxRate, rxRate);
110
+ this.pushBounded(this.netTxRate, txRate);
111
+ if (sample.gpuUtil !== undefined) this.pushBounded(this.gpuUtil, sample.gpuUtil);
112
+ if (sample.powerWatts !== undefined) this.pushBounded(this.powerWatts, sample.powerWatts);
113
+ if (sample.rxPackets !== undefined) this.pushBounded(this.rxPacketRate, rxPktRate);
114
+ if (sample.txPackets !== undefined) this.pushBounded(this.txPacketRate, txPktRate);
115
+ if (sample.connections !== undefined) this.pushBounded(this.connections, sample.connections);
116
+ }
117
+
118
+ /** Clear all rolling windows and reset cumulative byte counters. */
119
+ reset() {
120
+ this.cpuUsage = [];
121
+ this.memUsage = [];
122
+ this.temp = [];
123
+ this.netRxRate = [];
124
+ this.netTxRate = [];
125
+ this.gpuUtil = [];
126
+ this.powerWatts = [];
127
+ this.rxPacketRate = [];
128
+ this.txPacketRate = [];
129
+ this.connections = [];
130
+ this.lastRxBytes = 0;
131
+ this.lastTxBytes = 0;
132
+ this.lastRxPackets = 0;
133
+ this.lastTxPackets = 0;
134
+ this.lastTs = 0;
135
+ }
136
+
137
+ /**
138
+ * Grow or shrink the rolling window (e.g. on terminal resize),
139
+ * trimming from the oldest samples.
140
+ * @param n - New maximum length; clamped to at least 1.
141
+ */
142
+ setMaxLen(n: number) {
143
+ this.maxLen = Math.max(1, n);
144
+ for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate, this.gpuUtil, this.powerWatts, this.rxPacketRate, this.txPacketRate, this.connections]) {
145
+ while (arr.length > this.maxLen) arr.shift();
146
+ }
147
+ }
148
+
149
+ private pushBounded(arr: number[], v: number) {
150
+ arr.push(v);
151
+ if (arr.length > this.maxLen) arr.shift();
152
+ }
153
+ }
package/src/index.ts ADDED
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pyre - Mac system monitoring CLI
4
+ * Monitors temps, cpu, memory, disk, battery, and system stats.
5
+ * Live mode is a full interactive dashboard: graphs, pause/resume,
6
+ * on-demand snapshot export, and continuous CSV logging.
7
+ * P2P mode enables live data streaming between two systems over TCP.
8
+ */
9
+ import fs from 'node:fs';
10
+ import readline from 'node:readline';
11
+ import { Command } from 'commander';
12
+ import chalk from 'chalk';
13
+ import { collectAll } from './monitors/index.js';
14
+ import { formatTable, formatJson, formatCsv, formatTsv } from './formatters/index.js';
15
+ import { startLive, stopLive } from './live/index.js';
16
+ import { showSplash } from './splash.js';
17
+ import { P2PServer, P2PClient } from './p2p/index.js';
18
+
19
+ const program = new Command();
20
+
21
+ program
22
+ .name('pyre')
23
+ .version('2.0.0')
24
+ .description('Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor')
25
+ .option('-j, --json', 'Output as JSON')
26
+ .option('-c, --csv', 'Output as CSV')
27
+ .option('-t, --tsv', 'Output as TSV')
28
+ .option('--detailed', 'Include detailed system info and sensor readings')
29
+ .option('--interval <seconds>', 'Refresh interval for live mode', '2')
30
+ .option('--once', 'Show a single static snapshot instead of live feed')
31
+ .option('--out <file>', 'Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)')
32
+ .option('--export-dir <dir>', 'Directory used for live-mode snapshot exports and logs', './pyre-exports')
33
+ .option('--log', 'Start continuous CSV logging immediately when live mode starts')
34
+ .option('--tree', 'Show process tree view instead of flat list')
35
+ .option('--sort <key>', 'Sort processes by: cpu, mem, pid, user, command, state, threads, runtime', 'cpu')
36
+ .option('--packets', 'Include packet monitor panel in output')
37
+ .option('--limit <n>', 'Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)', '10')
38
+ .option('--p2p-host <host>', 'P2P host address (server: bind address, client: server address)', '0.0.0.0')
39
+ .option('--p2p-port <port>', 'P2P port number', '9876')
40
+ .option('--p2p-password <password>', 'Password for P2P authentication');
41
+
42
+ program.parse(process.argv);
43
+
44
+ const opts = program.opts();
45
+
46
+ function isExportMode() {
47
+ return opts.json || opts.csv || opts.tsv;
48
+ }
49
+
50
+ function stripAnsi(s: string): string {
51
+ // eslint-disable-next-line no-control-regex
52
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
53
+ }
54
+
55
+ async function main() {
56
+ const cmd = program.args[0];
57
+
58
+ if (cmd === 'live' || (!isExportMode() && !opts.once && cmd !== 'p2p')) {
59
+ if (cmd === 'live') {
60
+ program.args.shift();
61
+ }
62
+ await showSplash();
63
+ const interval = parseFloat(opts.interval) || 2;
64
+ await startLive({
65
+ interval,
66
+ detailed: opts.detailed,
67
+ exportDir: opts.exportDir,
68
+ autoLog: opts.log,
69
+ });
70
+ return;
71
+ }
72
+
73
+ if (cmd === 'p2p') {
74
+ const subcommand = program.args[1];
75
+ if (subcommand === 'server') {
76
+ await runP2PServer();
77
+ return;
78
+ } else if (subcommand === 'connect') {
79
+ await runP2PConnect();
80
+ return;
81
+ } else {
82
+ console.log(chalk.red('Usage: pyre p2p <server|connect> [options]'));
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ // `0` means "no cap" — collectProcesses treats an undefined limit as
88
+ // unlimited by grabbing every row `ps` returns instead of head-limiting it.
89
+ const requestedLimit = parseInt(opts.limit, 10);
90
+ const processLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : undefined;
91
+
92
+ const data = await collectAll({ detailed: opts.detailed, processLimit });
93
+
94
+ let output: string;
95
+ if (opts.json) output = formatJson(data);
96
+ else if (opts.csv) output = formatCsv(data);
97
+ else if (opts.tsv) output = formatTsv(data);
98
+ else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : undefined } });
99
+
100
+ console.log(output);
101
+
102
+ if (opts.out) {
103
+ fs.writeFileSync(opts.out, stripAnsi(output));
104
+ console.log(chalk.dim(`\nSaved to ${opts.out}`));
105
+ }
106
+ }
107
+
108
+ main().catch((err) => {
109
+ console.error(chalk.red(`Error: ${err.message}`));
110
+ process.exit(1);
111
+ });
112
+
113
+ async function runP2PServer(): Promise<void> {
114
+ const host = opts.p2pHost || '0.0.0.0';
115
+ const port = parseInt(opts.p2pPort, 10) || 9876;
116
+ const password = opts.p2pPassword || '';
117
+ const interval = parseFloat(opts.interval) || 2;
118
+ const detailed = !!opts.detailed;
119
+
120
+ if (!password) {
121
+ console.log(chalk.red('Error: a password is required for P2P server mode. Use --p2p-password <password>'));
122
+ process.exit(1);
123
+ }
124
+
125
+ const server = new P2PServer({ host, port, password, intervalMs: interval * 1000, detailed });
126
+
127
+ console.log(chalk.bold(`\n pyre P2P Server`));
128
+ console.log(chalk.dim(` Host: ${host}`));
129
+ console.log(chalk.dim(` Port: ${port}`));
130
+ console.log(chalk.dim(` Password: ${password}`));
131
+ console.log(chalk.dim(` Interval: ${interval}s`));
132
+ console.log(chalk.dim(` Detailed: ${detailed ? 'on' : 'off'}`));
133
+ console.log(chalk.dim(` Waiting for peers...\n`));
134
+
135
+ await server.start();
136
+
137
+ const rl = readline.createInterface({ input: process.stdin });
138
+ rl.on('line', (line) => {
139
+ const trimmed = line.trim().toLowerCase();
140
+ if (trimmed === 'q' || trimmed === 'quit' || trimmed === 'exit') {
141
+ server.stop();
142
+ rl.close();
143
+ process.exit(0);
144
+ } else if (trimmed === 'status') {
145
+ const info = server.info;
146
+ console.log(chalk.dim(` Peers connected: ${info.peersConnected}`));
147
+ }
148
+ });
149
+
150
+ process.once('SIGINT', () => {
151
+ server.stop();
152
+ rl.close();
153
+ process.exit(0);
154
+ });
155
+ }
156
+
157
+ async function runP2PConnect(): Promise<void> {
158
+ const host = opts.p2pHost || '127.0.0.1';
159
+ const port = parseInt(opts.p2pPort, 10) || 9876;
160
+ const password = opts.p2pPassword || '';
161
+
162
+ if (!password) {
163
+ console.log(chalk.red('Error: a password is required for P2P connect mode. Use --p2p-password <password>'));
164
+ process.exit(1);
165
+ }
166
+
167
+ const client = new P2PClient({ host, port, password });
168
+
169
+ client.setOnStatus((msg) => {
170
+ process.stdout.write(`\r${chalk.dim(msg)}${' '.repeat(40)}\r`);
171
+ });
172
+
173
+ client.setOnData((data) => {
174
+ process.stdout.write('\x1b[2J\x1b[H');
175
+ console.log(chalk.bold(` pyre P2P Client — ${data.header.hostname}`));
176
+ console.log(chalk.dim(` ${data.timestamp}`));
177
+ console.log();
178
+ console.log(formatTable(data, {
179
+ width: process.stdout.columns || 80,
180
+ sortBy: 'cpu',
181
+ treeView: false,
182
+ visible: { packets: true },
183
+ }));
184
+ });
185
+
186
+ console.log(chalk.bold(`\n pyre P2P Client`));
187
+ console.log(chalk.dim(` Connecting to ${host}:${port}...`));
188
+ console.log(chalk.dim(` Press Ctrl+C to disconnect\n`));
189
+
190
+ try {
191
+ await client.connect();
192
+ } catch (err: any) {
193
+ console.log(chalk.red(`Connection failed: ${err.message}`));
194
+ process.exit(1);
195
+ }
196
+
197
+ process.once('SIGINT', () => {
198
+ client.disconnect();
199
+ process.exit(0);
200
+ });
201
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Live dashboard export and logging helpers.
3
+ *
4
+ * Handles snapshot export (JSON/CSV/TSV), continuous
5
+ * CSV logging, and the supporting file-system utilities.
6
+ * All state variables are imported from `state.ts`.
7
+ */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { formatJson, formatCsv, formatTsv } from '../formatters/index.js';
11
+ import { state, setStatus } from './state.js';
12
+
13
+ /** Ensure a directory exists, creating it recursively if needed. */
14
+ function ensureDir(dir: string) {
15
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+
18
+ /** Generate a filesystem-safe timestamp string for export filenames. */
19
+ function timestampForFile(): string {
20
+ return new Date().toISOString().replace(/[:.]/g, '-');
21
+ }
22
+
23
+ function exportSnapshot() {
24
+ if (!state.lastData) {
25
+ setStatus('No data yet — try again in a moment');
26
+ return;
27
+ }
28
+ try {
29
+ ensureDir(state.exportDir);
30
+ const file = path.join(state.exportDir, `pyre-${timestampForFile()}.${state.exportFormat}`);
31
+ const content =
32
+ state.exportFormat === 'json' ? formatJson(state.lastData) : state.exportFormat === 'csv' ? formatCsv(state.lastData) : formatTsv(state.lastData);
33
+ fs.writeFileSync(file, content);
34
+ setStatus(`Exported snapshot -> ${file}`);
35
+ } catch (err: any) {
36
+ setStatus(`Export failed: ${err.message}`);
37
+ }
38
+ }
39
+
40
+ function startLogging() {
41
+ try {
42
+ ensureDir(state.exportDir);
43
+ const file = path.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
44
+ state.logStream = fs.createWriteStream(file, { flags: 'a' });
45
+ state.logStream.write('timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n');
46
+ state.logging = true;
47
+ setStatus(`Logging every tick -> ${file}`);
48
+ } catch (err: any) {
49
+ setStatus(`Logging failed to start: ${err.message}`);
50
+ }
51
+ }
52
+
53
+ function stopLogging() {
54
+ state.logging = false;
55
+ state.logStream?.end();
56
+ state.logStream = null;
57
+ setStatus('Logging stopped');
58
+ }
59
+
60
+ function toggleLogging() {
61
+ if (state.logging) stopLogging();
62
+ else startLogging();
63
+ }
64
+
65
+ function writeLogRow(data: any) {
66
+ if (!state.logging || !state.logStream) return;
67
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? '';
68
+ const rxPackets = data.network.rxPackets ?? 0;
69
+ const txPackets = data.network.txPackets ?? 0;
70
+ const connections = data.network.connections ?? 0;
71
+ state.logStream.write(
72
+ `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}\n`
73
+ );
74
+ }
75
+
76
+ export { ensureDir, timestampForFile, exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * pyre live dashboard module.
3
+ *
4
+ * Provides the interactive live monitoring mode with
5
+ * keyboard-driven customization, process filtering,
6
+ * snapshot export, and continuous CSV logging.
7
+ *
8
+ * The module is organised into five sub-files:
9
+ * - `types.ts` — type definitions
10
+ * - `state.ts` — shared mutable session state
11
+ * - `render.ts` — screen rendering and alert checking
12
+ * - `input.ts` — keypress handling and session lifecycle
13
+ * - `export.ts` — snapshot export and CSV logging
14
+ */
15
+ export { startLive, stopLive } from './input.js';
16
+ export type { LiveOptions } from './types.js';