pyre-cli 1.4.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.
package/src/index.ts CHANGED
@@ -4,32 +4,40 @@
4
4
  * Monitors temps, cpu, memory, disk, battery, and system stats.
5
5
  * Live mode is a full interactive dashboard: graphs, pause/resume,
6
6
  * on-demand snapshot export, and continuous CSV logging.
7
+ * P2P mode enables live data streaming between two systems over TCP.
7
8
  */
8
9
  import fs from 'node:fs';
10
+ import readline from 'node:readline';
9
11
  import { Command } from 'commander';
10
12
  import chalk from 'chalk';
11
13
  import { collectAll } from './monitors/index.js';
12
14
  import { formatTable, formatJson, formatCsv, formatTsv } from './formatters/index.js';
13
15
  import { startLive, stopLive } from './live/index.js';
14
16
  import { showSplash } from './splash.js';
17
+ import { P2PServer, P2PClient } from './p2p/index.js';
15
18
 
16
19
  const program = new Command();
17
20
 
18
21
  program
19
- .name('pyre')
20
- .version('2.0.0')
21
- .description('Mac system monitoring CLI: interactive live dashboard, stats, graphs, export')
22
- .option('-j, --json', 'Output as JSON')
23
- .option('-c, --csv', 'Output as CSV')
24
- .option('-t, --tsv', 'Output as TSV')
25
- .option('--detailed', 'Include detailed system info and sensor readings')
26
- .option('--interval <seconds>', 'Refresh interval for live mode', '2')
27
- .option('--once', 'Show a single static snapshot instead of live feed')
28
- .option('--out <file>', 'Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)')
29
- .option('--export-dir <dir>', 'Directory used for live-mode snapshot exports and logs', './pyre-exports')
30
- .option('--log', 'Start continuous CSV logging immediately when live mode starts')
31
- .option('--tree', 'Show process tree view instead of flat list')
32
- .option('--sort <key>', 'Sort processes by: cpu, mem, pid, user, command, state, threads, runtime', 'cpu');
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');
33
41
 
34
42
  program.parse(process.argv);
35
43
 
@@ -45,8 +53,12 @@ function stripAnsi(s: string): string {
45
53
  }
46
54
 
47
55
  async function main() {
48
- const requestedLive = program.args[0] === 'live' || (!isExportMode() && !opts.once);
49
- if (requestedLive) {
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
+ }
50
62
  await showSplash();
51
63
  const interval = parseFloat(opts.interval) || 2;
52
64
  await startLive({
@@ -58,13 +70,32 @@ async function main() {
58
70
  return;
59
71
  }
60
72
 
61
- const data = await collectAll({ detailed: opts.detailed });
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 });
62
93
 
63
94
  let output: string;
64
95
  if (opts.json) output = formatJson(data);
65
96
  else if (opts.csv) output = formatCsv(data);
66
97
  else if (opts.tsv) output = formatTsv(data);
67
- else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree });
98
+ else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : undefined } });
68
99
 
69
100
  console.log(output);
70
101
 
@@ -76,6 +107,95 @@ async function main() {
76
107
 
77
108
  main().catch((err) => {
78
109
  console.error(chalk.red(`Error: ${err.message}`));
79
- stopLive();
80
110
  process.exit(1);
81
- });
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
+ }
@@ -42,7 +42,7 @@ function startLogging() {
42
42
  ensureDir(state.exportDir);
43
43
  const file = path.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
44
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,thermal_state\n');
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
46
  state.logging = true;
47
47
  setStatus(`Logging every tick -> ${file}`);
48
48
  } catch (err: any) {
@@ -65,8 +65,11 @@ function toggleLogging() {
65
65
  function writeLogRow(data: any) {
66
66
  if (!state.logging || !state.logStream) return;
67
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;
68
71
  state.logStream.write(
69
- `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${data.thermal.state}\n`
72
+ `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}\n`
70
73
  );
71
74
  }
72
75