pyre-cli 1.4.0 → 3.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 +78 -11
- package/dist/index.js +1325 -33
- package/package.json +2 -2
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +47 -19
- package/src/formatters/render.ts +207 -16
- package/src/formatters/types.ts +4 -0
- package/src/history.ts +85 -47
- package/src/index.ts +200 -20
- package/src/live/export.ts +5 -2
- package/src/live/input.ts +296 -164
- package/src/live/render.ts +32 -28
- package/src/live/state.ts +41 -21
- package/src/monitors/collectors.ts +220 -17
- package/src/monitors/index.ts +2 -2
- package/src/monitors/types.ts +42 -1
- package/src/p2p/client.ts +271 -0
- package/src/p2p/index.ts +3 -0
- package/src/p2p/protocol.ts +112 -0
- package/src/p2p/server.ts +390 -0
- package/src/p2p/types.ts +97 -0
package/src/index.ts
CHANGED
|
@@ -4,32 +4,50 @@
|
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
.option('--p2p-tls', 'Enable TLS encryption for P2P connections')
|
|
42
|
+
.option('--p2p-cert <file>', 'TLS certificate file (PEM) for P2P server')
|
|
43
|
+
.option('--p2p-key <file>', 'TLS private key file (PEM) for P2P server')
|
|
44
|
+
.option('--p2p-ca <file>', 'TLS CA certificate file (PEM) for P2P client')
|
|
45
|
+
.option('--p2p-insecure', 'Skip TLS certificate verification (client only)')
|
|
46
|
+
.option('--p2p-rate-limit <n>', 'Max auth attempts per IP per minute (default: 5)', '5')
|
|
47
|
+
.option('--p2p-allow <ips>', 'Comma-separated list of allowed IPs (empty = all)')
|
|
48
|
+
.option('--p2p-deny <ips>', 'Comma-separated list of denied IPs')
|
|
49
|
+
.option('--p2p-audit-log <dir>', 'Directory for P2P audit logs')
|
|
50
|
+
.option('--p2p-hmac-key <key>', 'HMAC key for message signing (default: derived from password)');
|
|
33
51
|
|
|
34
52
|
program.parse(process.argv);
|
|
35
53
|
|
|
@@ -45,8 +63,12 @@ function stripAnsi(s: string): string {
|
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
async function main() {
|
|
48
|
-
const
|
|
49
|
-
|
|
66
|
+
const cmd = program.args[0];
|
|
67
|
+
|
|
68
|
+
if (cmd === 'live' || (!isExportMode() && !opts.once && cmd !== 'p2p')) {
|
|
69
|
+
if (cmd === 'live') {
|
|
70
|
+
program.args.shift();
|
|
71
|
+
}
|
|
50
72
|
await showSplash();
|
|
51
73
|
const interval = parseFloat(opts.interval) || 2;
|
|
52
74
|
await startLive({
|
|
@@ -58,13 +80,32 @@ async function main() {
|
|
|
58
80
|
return;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
|
|
83
|
+
if (cmd === 'p2p') {
|
|
84
|
+
const subcommand = program.args[1];
|
|
85
|
+
if (subcommand === 'server') {
|
|
86
|
+
await runP2PServer();
|
|
87
|
+
return;
|
|
88
|
+
} else if (subcommand === 'connect') {
|
|
89
|
+
await runP2PConnect();
|
|
90
|
+
return;
|
|
91
|
+
} else {
|
|
92
|
+
console.log(chalk.red('Usage: pyre p2p <server|connect> [options]'));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `0` means "no cap" — collectProcesses treats an undefined limit as
|
|
98
|
+
// unlimited by grabbing every row `ps` returns instead of head-limiting it.
|
|
99
|
+
const requestedLimit = parseInt(opts.limit, 10);
|
|
100
|
+
const processLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : undefined;
|
|
101
|
+
|
|
102
|
+
const data = await collectAll({ detailed: opts.detailed, processLimit });
|
|
62
103
|
|
|
63
104
|
let output: string;
|
|
64
105
|
if (opts.json) output = formatJson(data);
|
|
65
106
|
else if (opts.csv) output = formatCsv(data);
|
|
66
107
|
else if (opts.tsv) output = formatTsv(data);
|
|
67
|
-
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree });
|
|
108
|
+
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : undefined } });
|
|
68
109
|
|
|
69
110
|
console.log(output);
|
|
70
111
|
|
|
@@ -76,6 +117,145 @@ async function main() {
|
|
|
76
117
|
|
|
77
118
|
main().catch((err) => {
|
|
78
119
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
79
|
-
stopLive();
|
|
80
120
|
process.exit(1);
|
|
81
|
-
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
async function runP2PServer(): Promise<void> {
|
|
124
|
+
const host = opts.p2pHost || '0.0.0.0';
|
|
125
|
+
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
126
|
+
const password = opts.p2pPassword || '';
|
|
127
|
+
const interval = parseFloat(opts.interval) || 2;
|
|
128
|
+
const detailed = !!opts.detailed;
|
|
129
|
+
const tlsCert = opts.p2pCert;
|
|
130
|
+
const tlsKey = opts.p2pKey;
|
|
131
|
+
const tlsCA = opts.p2pCA;
|
|
132
|
+
const rateLimit = parseInt(opts.p2pRateLimit, 10) || 5;
|
|
133
|
+
const allowList = opts.p2pAllow ? opts.p2pAllow.split(',').map((s: string) => s.trim()).filter(Boolean) : [];
|
|
134
|
+
const denyList = opts.p2pDeny ? opts.p2pDeny.split(',').map((s: string) => s.trim()).filter(Boolean) : [];
|
|
135
|
+
|
|
136
|
+
if (!password) {
|
|
137
|
+
console.log(chalk.red('Error: a password is required for P2P server mode. Use --p2p-password <password>'));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (tlsCert && !tlsKey) {
|
|
142
|
+
console.log(chalk.red('Error: --p2p-key is required when --p2p-cert is specified'));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
if (tlsKey && !tlsCert) {
|
|
146
|
+
console.log(chalk.red('Error: --p2p-cert is required when --p2p-key is specified'));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const server = new P2PServer({
|
|
151
|
+
host,
|
|
152
|
+
port,
|
|
153
|
+
password,
|
|
154
|
+
intervalMs: interval * 1000,
|
|
155
|
+
detailed,
|
|
156
|
+
tlsCert,
|
|
157
|
+
tlsKey,
|
|
158
|
+
tlsCA,
|
|
159
|
+
rateLimitMaxAttempts: rateLimit,
|
|
160
|
+
rateLimitWindowMs: 60000,
|
|
161
|
+
allowedIPs: allowList,
|
|
162
|
+
deniedIPs: denyList,
|
|
163
|
+
auditLog: opts.p2pAuditLog,
|
|
164
|
+
hmacKey: opts.p2pHmacKey,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
168
|
+
|
|
169
|
+
console.log(chalk.bold(`\n pyre P2P Server`));
|
|
170
|
+
console.log(chalk.dim(` Host: ${host}`));
|
|
171
|
+
console.log(chalk.dim(` Port: ${port}`));
|
|
172
|
+
console.log(chalk.dim(` Protocol: ${tlsEnabled ? 'TLS' : 'TCP'}`));
|
|
173
|
+
console.log(chalk.dim(` Password: ${password}`));
|
|
174
|
+
console.log(chalk.dim(` Interval: ${interval}s`));
|
|
175
|
+
console.log(chalk.dim(` Detailed: ${detailed ? 'on' : 'off'}`));
|
|
176
|
+
console.log(chalk.dim(` Rate limit: ${rateLimit} attempts/min`));
|
|
177
|
+
if (allowList.length > 0) console.log(chalk.dim(` Allowed IPs: ${allowList.join(', ')}`));
|
|
178
|
+
if (denyList.length > 0) console.log(chalk.dim(` Denied IPs: ${denyList.join(', ')}`));
|
|
179
|
+
console.log(chalk.dim(` Waiting for peers...\n`));
|
|
180
|
+
|
|
181
|
+
await server.start();
|
|
182
|
+
|
|
183
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
184
|
+
rl.on('line', (line) => {
|
|
185
|
+
const trimmed = line.trim().toLowerCase();
|
|
186
|
+
if (trimmed === 'q' || trimmed === 'quit' || trimmed === 'exit') {
|
|
187
|
+
server.stop();
|
|
188
|
+
rl.close();
|
|
189
|
+
process.exit(0);
|
|
190
|
+
} else if (trimmed === 'status') {
|
|
191
|
+
const info = server.info;
|
|
192
|
+
console.log(chalk.dim(` Peers connected: ${info.peersConnected}`));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
process.once('SIGINT', () => {
|
|
197
|
+
server.stop();
|
|
198
|
+
rl.close();
|
|
199
|
+
process.exit(0);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function runP2PConnect(): Promise<void> {
|
|
204
|
+
const host = opts.p2pHost || '127.0.0.1';
|
|
205
|
+
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
206
|
+
const password = opts.p2pPassword || '';
|
|
207
|
+
const useTLS = !!opts.p2pTls;
|
|
208
|
+
const tlsCA = opts.p2pCA;
|
|
209
|
+
const tlsInsecure = !!opts.p2pInsecure;
|
|
210
|
+
|
|
211
|
+
if (!password) {
|
|
212
|
+
console.log(chalk.red('Error: a password is required for P2P connect mode. Use --p2p-password <password>'));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const client = new P2PClient({
|
|
217
|
+
host,
|
|
218
|
+
port,
|
|
219
|
+
password,
|
|
220
|
+
tls: useTLS,
|
|
221
|
+
tlsCA,
|
|
222
|
+
tlsInsecure,
|
|
223
|
+
auditLog: opts.p2pAuditLog,
|
|
224
|
+
hmacKey: opts.p2pHmacKey,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
client.setOnStatus((msg) => {
|
|
228
|
+
process.stdout.write(`\r${chalk.dim(msg)}${' '.repeat(40)}\r`);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
client.setOnData((data) => {
|
|
232
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
233
|
+
console.log(chalk.bold(` pyre P2P Client — ${data.header.hostname}`));
|
|
234
|
+
console.log(chalk.dim(` ${data.timestamp}`));
|
|
235
|
+
console.log();
|
|
236
|
+
console.log(formatTable(data, {
|
|
237
|
+
width: process.stdout.columns || 80,
|
|
238
|
+
sortBy: 'cpu',
|
|
239
|
+
treeView: false,
|
|
240
|
+
visible: { packets: true },
|
|
241
|
+
}));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const protocolLabel = useTLS ? 'TLS' : 'TCP';
|
|
245
|
+
console.log(chalk.bold(`\n pyre P2P Client`));
|
|
246
|
+
console.log(chalk.dim(` Protocol: ${protocolLabel}`));
|
|
247
|
+
console.log(chalk.dim(` Connecting to ${host}:${port}...`));
|
|
248
|
+
console.log(chalk.dim(` Press Ctrl+C to disconnect\n`));
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await client.connect();
|
|
252
|
+
} catch (err: any) {
|
|
253
|
+
console.log(chalk.red(`Connection failed: ${err.message}`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
process.once('SIGINT', () => {
|
|
258
|
+
client.disconnect();
|
|
259
|
+
process.exit(0);
|
|
260
|
+
});
|
|
261
|
+
}
|
package/src/live/export.ts
CHANGED
|
@@ -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
|
|