pyre-cli 2.0.0 → 3.1.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 +98 -13
- package/dist/index.js +1278 -824
- package/package.json +1 -1
- package/src/formatters/index.ts +1 -1
- package/src/formatters/render.ts +2 -1
- package/src/index.ts +63 -3
- package/src/live/input.ts +99 -1
- package/src/live/render.ts +70 -32
- package/src/live/state.ts +22 -15
- package/src/live/types.ts +1 -1
- package/src/monitors/collectors.ts +1 -1
- package/src/p2p/client.ts +99 -25
- package/src/p2p/protocol.ts +45 -4
- package/src/p2p/server.ts +237 -29
- package/src/p2p/types.ts +30 -2
package/package.json
CHANGED
package/src/formatters/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - `render.ts` — dashboard layout, cards, tables, and the main `formatTable`
|
|
12
12
|
* - `output.ts` — JSON/CSV/TSV export and sparkline graph rendering
|
|
13
13
|
*/
|
|
14
|
-
export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getTabHitboxes, TAB_BAR_ROW, TAB_DEFS } from './render.js';
|
|
14
|
+
export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getTabHitboxes, TAB_BAR_ROW, TAB_DEFS, panel } from './render.js';
|
|
15
15
|
export { formatJson, formatCsv, formatTsv, formatGraphs } from './output.js';
|
|
16
16
|
export type { ThemeName, ThemeColors, VisibleItems, TableOptions } from './types.js';
|
|
17
17
|
export { THEMES } from './themes.js';
|
package/src/formatters/render.ts
CHANGED
|
@@ -275,7 +275,8 @@ export const TAB_DEFS: { id: string; label: string; key: string }[] = [
|
|
|
275
275
|
{ id: 'packets', label: 'Packets', key: '8' },
|
|
276
276
|
{ id: 'tasks', label: 'Tasks', key: '9' },
|
|
277
277
|
{ id: 'disk', label: 'Disk', key: '0' },
|
|
278
|
-
{ id: 'process', label: 'Process', key: '
|
|
278
|
+
{ id: 'process', label: 'Process', key: 'P' },
|
|
279
|
+
{ id: 'p2p', label: 'P2P', key: 'R' },
|
|
279
280
|
];
|
|
280
281
|
|
|
281
282
|
/** The number of leading columns of indent before the tab bar's first character (see `tabBar`). */
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,17 @@ program
|
|
|
37
37
|
.option('--limit <n>', 'Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)', '10')
|
|
38
38
|
.option('--p2p-host <host>', 'P2P host address (server: bind address, client: server address)', '0.0.0.0')
|
|
39
39
|
.option('--p2p-port <port>', 'P2P port number', '9876')
|
|
40
|
-
.option('--p2p-password <password>', 'Password for P2P authentication')
|
|
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)');
|
|
41
51
|
|
|
42
52
|
program.parse(process.argv);
|
|
43
53
|
|
|
@@ -116,20 +126,56 @@ async function runP2PServer(): Promise<void> {
|
|
|
116
126
|
const password = opts.p2pPassword || '';
|
|
117
127
|
const interval = parseFloat(opts.interval) || 2;
|
|
118
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) : [];
|
|
119
135
|
|
|
120
136
|
if (!password) {
|
|
121
137
|
console.log(chalk.red('Error: a password is required for P2P server mode. Use --p2p-password <password>'));
|
|
122
138
|
process.exit(1);
|
|
123
139
|
}
|
|
124
140
|
|
|
125
|
-
|
|
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);
|
|
126
168
|
|
|
127
169
|
console.log(chalk.bold(`\n pyre P2P Server`));
|
|
128
170
|
console.log(chalk.dim(` Host: ${host}`));
|
|
129
171
|
console.log(chalk.dim(` Port: ${port}`));
|
|
172
|
+
console.log(chalk.dim(` Protocol: ${tlsEnabled ? 'TLS' : 'TCP'}`));
|
|
130
173
|
console.log(chalk.dim(` Password: ${password}`));
|
|
131
174
|
console.log(chalk.dim(` Interval: ${interval}s`));
|
|
132
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(', ')}`));
|
|
133
179
|
console.log(chalk.dim(` Waiting for peers...\n`));
|
|
134
180
|
|
|
135
181
|
await server.start();
|
|
@@ -158,13 +204,25 @@ async function runP2PConnect(): Promise<void> {
|
|
|
158
204
|
const host = opts.p2pHost || '127.0.0.1';
|
|
159
205
|
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
160
206
|
const password = opts.p2pPassword || '';
|
|
207
|
+
const useTLS = !!opts.p2pTls;
|
|
208
|
+
const tlsCA = opts.p2pCA;
|
|
209
|
+
const tlsInsecure = !!opts.p2pInsecure;
|
|
161
210
|
|
|
162
211
|
if (!password) {
|
|
163
212
|
console.log(chalk.red('Error: a password is required for P2P connect mode. Use --p2p-password <password>'));
|
|
164
213
|
process.exit(1);
|
|
165
214
|
}
|
|
166
215
|
|
|
167
|
-
const client = new P2PClient({
|
|
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
|
+
});
|
|
168
226
|
|
|
169
227
|
client.setOnStatus((msg) => {
|
|
170
228
|
process.stdout.write(`\r${chalk.dim(msg)}${' '.repeat(40)}\r`);
|
|
@@ -183,7 +241,9 @@ async function runP2PConnect(): Promise<void> {
|
|
|
183
241
|
}));
|
|
184
242
|
});
|
|
185
243
|
|
|
244
|
+
const protocolLabel = useTLS ? 'TLS' : 'TCP';
|
|
186
245
|
console.log(chalk.bold(`\n pyre P2P Client`));
|
|
246
|
+
console.log(chalk.dim(` Protocol: ${protocolLabel}`));
|
|
187
247
|
console.log(chalk.dim(` Connecting to ${host}:${port}...`));
|
|
188
248
|
console.log(chalk.dim(` Press Ctrl+C to disconnect\n`));
|
|
189
249
|
|
package/src/live/input.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { state, setStatus } from './state.js';
|
|
|
14
14
|
import { exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow } from './export.js';
|
|
15
15
|
import { render, footerLine, checkAlerts, processRowBudget } from './render.js';
|
|
16
16
|
import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
|
|
17
|
+
import { startP2PServer } from '../p2p/index.js';
|
|
17
18
|
|
|
18
19
|
function handleInputModeKey(str: string, key: readline.Key) {
|
|
19
20
|
if (key.name === 'escape') {
|
|
@@ -67,6 +68,36 @@ function handleInputModeKey(str: string, key: readline.Key) {
|
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
if (state.inputMode === 'p2p') {
|
|
72
|
+
if (key.name === 'escape') {
|
|
73
|
+
state.inputMode = null;
|
|
74
|
+
state.inputBuffer = '';
|
|
75
|
+
render();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (key.name === 'return') {
|
|
79
|
+
const password = state.inputBuffer.trim();
|
|
80
|
+
if (password) {
|
|
81
|
+
state.p2pPassword = password;
|
|
82
|
+
startP2PServerFromTUI();
|
|
83
|
+
}
|
|
84
|
+
state.inputMode = null;
|
|
85
|
+
state.inputBuffer = '';
|
|
86
|
+
render();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.name === 'backspace') {
|
|
90
|
+
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
91
|
+
render();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
95
|
+
state.inputBuffer += str;
|
|
96
|
+
render();
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
70
101
|
if (key.name === 'return') {
|
|
71
102
|
if (state.inputMode === 'filter') {
|
|
72
103
|
state.processFilter = state.inputBuffer.trim();
|
|
@@ -106,6 +137,56 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
|
|
140
|
+
async function startP2PServerFromTUI() {
|
|
141
|
+
if (state.p2pServerRunning) return;
|
|
142
|
+
try {
|
|
143
|
+
const server = await startP2PServer({
|
|
144
|
+
host: '0.0.0.0',
|
|
145
|
+
port: state.p2pPort,
|
|
146
|
+
password: state.p2pPassword,
|
|
147
|
+
intervalMs: state.interval * 1000,
|
|
148
|
+
detailed: state.detailed,
|
|
149
|
+
onLog: (msg: string) => setStatus(msg, 5000),
|
|
150
|
+
onPeerEvent: (evt) => {
|
|
151
|
+
state.p2pEvents.push(evt);
|
|
152
|
+
if (state.p2pEvents.length > 200) state.p2pEvents.shift();
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
state.p2pServer = server as any;
|
|
156
|
+
state.p2pServerRunning = true;
|
|
157
|
+
state.p2pBind = (server as any).boundAddress || '0.0.0.0';
|
|
158
|
+
state.p2pEvents = [];
|
|
159
|
+
setStatus(`P2P server started on ${state.p2pBind}:${state.p2pPort}`);
|
|
160
|
+
render();
|
|
161
|
+
} catch (err: any) {
|
|
162
|
+
setStatus(`P2P server failed: ${err.message}`);
|
|
163
|
+
state.p2pServerRunning = false;
|
|
164
|
+
state.p2pServer = null;
|
|
165
|
+
state.p2pBind = '';
|
|
166
|
+
render();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function syncP2PEvents() {
|
|
171
|
+
if (!state.p2pServer) return;
|
|
172
|
+
const history = state.p2pServer.peerEventHistory;
|
|
173
|
+
if (history && history !== state.p2pEvents) {
|
|
174
|
+
state.p2pEvents = history;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stopP2PServer() {
|
|
179
|
+
if (!state.p2pServer) return;
|
|
180
|
+
try {
|
|
181
|
+
state.p2pServer.stop();
|
|
182
|
+
} catch {
|
|
183
|
+
// ignore stop errors
|
|
184
|
+
}
|
|
185
|
+
state.p2pServer = null;
|
|
186
|
+
state.p2pServerRunning = false;
|
|
187
|
+
setStatus('P2P server stopped');
|
|
188
|
+
}
|
|
189
|
+
|
|
109
190
|
function onResize() {
|
|
110
191
|
state.termWidth = process.stdout.columns || 80;
|
|
111
192
|
state.termHeight = process.stdout.rows || 24;
|
|
@@ -140,6 +221,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
140
221
|
|
|
141
222
|
writeLogRow(data);
|
|
142
223
|
checkAlerts(data);
|
|
224
|
+
syncP2PEvents();
|
|
143
225
|
render();
|
|
144
226
|
} catch {
|
|
145
227
|
// skip bad tick
|
|
@@ -252,6 +334,17 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
252
334
|
}
|
|
253
335
|
|
|
254
336
|
switch (key.name) {
|
|
337
|
+
case 'r':
|
|
338
|
+
if (state.p2pServerRunning) {
|
|
339
|
+
stopP2PServer();
|
|
340
|
+
setStatus('P2P server stopped');
|
|
341
|
+
render();
|
|
342
|
+
} else {
|
|
343
|
+
state.inputMode = 'p2p';
|
|
344
|
+
state.inputBuffer = state.p2pPassword;
|
|
345
|
+
render();
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
255
348
|
case 'q':
|
|
256
349
|
stopLive();
|
|
257
350
|
break;
|
|
@@ -362,7 +455,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
362
455
|
}
|
|
363
456
|
|
|
364
457
|
function tabKeyToId(str: string): string | null {
|
|
365
|
-
const map: Record<string, string> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process' };
|
|
458
|
+
const map: Record<string, string> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process', 'r': 'p2p' };
|
|
366
459
|
return map[str] ?? null;
|
|
367
460
|
}
|
|
368
461
|
|
|
@@ -420,6 +513,11 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
420
513
|
state.logStream.end();
|
|
421
514
|
state.logStream = null;
|
|
422
515
|
}
|
|
516
|
+
if (state.p2pServer) {
|
|
517
|
+
state.p2pServer.stop();
|
|
518
|
+
state.p2pServer = null;
|
|
519
|
+
state.p2pServerRunning = false;
|
|
520
|
+
}
|
|
423
521
|
if (state.keypressHandler) {
|
|
424
522
|
process.stdin.removeListener('keypress', state.keypressHandler);
|
|
425
523
|
state.keypressHandler = null;
|
package/src/live/render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `state.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName } from '../formatters/index.js';
|
|
10
|
+
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel } from '../formatters/index.js';
|
|
11
11
|
import { state, setStatus, SIGNAL_OPTIONS } from './state.js';
|
|
12
12
|
import type { StatsData } from '../monitors/index.js';
|
|
13
13
|
|
|
@@ -25,6 +25,33 @@ function processRowBudget(): number {
|
|
|
25
25
|
return Math.max(3, Math.min(40, state.termHeight - reserved));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function p2pPanelLines(): { title: string; body: string[] } {
|
|
29
|
+
const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
|
|
30
|
+
const body: string[] = [];
|
|
31
|
+
const bindAddr = state.p2pBind || `0.0.0.0:${state.p2pPort}`;
|
|
32
|
+
|
|
33
|
+
body.push(`Status: ${state.p2pServerRunning ? chalk.green('Running') : chalk.red('Stopped')}`);
|
|
34
|
+
body.push(`Bind: ${bindAddr}`);
|
|
35
|
+
body.push(`Password: ${state.p2pPassword}`);
|
|
36
|
+
body.push('');
|
|
37
|
+
|
|
38
|
+
const typeColor = (t: string) => {
|
|
39
|
+
if (t === 'auth') return chalk.green;
|
|
40
|
+
if (t === 'disconnect') return chalk.yellow;
|
|
41
|
+
if (t === 'error' || t === 'blocked' || t === 'rate_limited') return chalk.red;
|
|
42
|
+
return chalk.cyan;
|
|
43
|
+
};
|
|
44
|
+
body.push(chalk.dim('Events (most recent):'));
|
|
45
|
+
const recent = events.slice(-20).reverse();
|
|
46
|
+
for (const evt of recent) {
|
|
47
|
+
const time = new Date(evt.ts).toLocaleTimeString();
|
|
48
|
+
body.push(`${chalk.dim(time)} ${typeColor(evt.type)(evt.type.padEnd(14))} ${evt.detail}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const title = `P2P · ${state.p2pServerRunning ? 'ON' : 'OFF'} · ${events.length} events`;
|
|
52
|
+
return { title, body };
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
function renderCustomizerOverlay(): string {
|
|
29
56
|
const themesList = Object.keys(THEMES) as ThemeName[];
|
|
30
57
|
const lines = [
|
|
@@ -57,20 +84,27 @@ function renderCustomizerOverlay(): string {
|
|
|
57
84
|
function render() {
|
|
58
85
|
if (!state.lastData) return;
|
|
59
86
|
const lines: string[] = [];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
88
|
+
|
|
89
|
+
if (state.activePanel === 'p2p') {
|
|
90
|
+
const panelLines = p2pPanelLines();
|
|
91
|
+
lines.push(...panel(panelLines.title, panelLines.body, state.termWidth, theme.process, theme.border));
|
|
92
|
+
} else {
|
|
93
|
+
lines.push(
|
|
94
|
+
formatTable(state.lastData, {
|
|
95
|
+
width: state.termWidth,
|
|
96
|
+
sortBy: state.sortMode,
|
|
97
|
+
filter: state.processFilter || undefined,
|
|
98
|
+
processLimit: processRowBudget(),
|
|
99
|
+
theme: state.currentTheme,
|
|
100
|
+
visible: state.visiblePanels,
|
|
101
|
+
treeView: state.treeView,
|
|
102
|
+
activePanel: state.activePanel,
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (state.showGraphs && state.activePanel !== 'p2p') {
|
|
74
108
|
lines.push('');
|
|
75
109
|
lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
|
|
76
110
|
}
|
|
@@ -90,9 +124,11 @@ function render() {
|
|
|
90
124
|
lines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
|
|
91
125
|
lines.push(chalk.dim(` ${sigList}`));
|
|
92
126
|
lines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
|
|
127
|
+
} else if (state.inputMode === 'p2p') {
|
|
128
|
+
lines.push(chalk.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
|
|
93
129
|
} else if (state.statusMessage) {
|
|
94
130
|
lines.push(` ${state.statusMessage}`);
|
|
95
|
-
|
|
131
|
+
}
|
|
96
132
|
|
|
97
133
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
98
134
|
process.stdout.write(lines.join('\n'));
|
|
@@ -102,22 +138,23 @@ function footerLine(): string {
|
|
|
102
138
|
const keys: [string, string][] = [
|
|
103
139
|
['q', 'quit'],
|
|
104
140
|
['c', 'customize UI'],
|
|
105
|
-
['
|
|
106
|
-
['
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
['r', state.p2pServerRunning ? 'stop p2p' : 'start p2p'],
|
|
142
|
+
['p', state.paused ? 'resume' : 'pause'],
|
|
143
|
+
['g', state.showGraphs ? 'hide graphs' : 'show graphs'],
|
|
144
|
+
['b', `graph:${state.graphMode}`],
|
|
145
|
+
['d', state.detailed ? 'basic' : 'detailed'],
|
|
146
|
+
['←/→', 'tab'],
|
|
147
|
+
['1-9', 'panels'],
|
|
148
|
+
['s', `sort:${state.sortMode}`],
|
|
149
|
+
['/', 'filter'],
|
|
150
|
+
['k', 'kill'],
|
|
151
|
+
['S', 'signal'],
|
|
152
|
+
['t', state.treeView ? 'flat' : 'tree'],
|
|
153
|
+
['e', 'export'],
|
|
154
|
+
['l', state.logging ? 'stop log' : 'log'],
|
|
155
|
+
['f', state.exportFormat],
|
|
156
|
+
['+/-', `${state.interval}s`],
|
|
157
|
+
];
|
|
121
158
|
const keyStr = keys.map(([k, label]) => `${chalk.hex('#50fa7b').bold(k)} ${chalk.dim(label)}`).join(chalk.dim(' | '));
|
|
122
159
|
|
|
123
160
|
|
|
@@ -125,6 +162,7 @@ function footerLine(): string {
|
|
|
125
162
|
if (state.paused) badges.push(chalk.yellow.bold('⏸ PAUSED'));
|
|
126
163
|
if (state.logging) badges.push(chalk.red.bold('● REC'));
|
|
127
164
|
if (state.activePanel !== 'grid') badges.push(chalk.cyan.bold(`◉ ${state.activePanel.toUpperCase()}`));
|
|
165
|
+
if (state.p2pServerRunning) badges.push(chalk.green.bold(`P2P ${state.p2pBind || '0.0.0.0'}:${state.p2pPort}`));
|
|
128
166
|
const badgeStr = badges.join(' ');
|
|
129
167
|
|
|
130
168
|
return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
|
package/src/live/state.ts
CHANGED
|
@@ -75,21 +75,28 @@ const state = {
|
|
|
75
75
|
CPU_ALERT_PCT: 90,
|
|
76
76
|
TEMP_ALERT_C: 95,
|
|
77
77
|
alerted: false,
|
|
78
|
-
activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process',
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p',
|
|
79
|
+
mouseEnabled: true,
|
|
80
|
+
PANEL_TABS: [
|
|
81
|
+
{ id: 'cpu', label: 'CPU', key: '1' },
|
|
82
|
+
{ id: 'mem', label: 'Memory', key: '2' },
|
|
83
|
+
{ id: 'gpu', label: 'GPU', key: '3' },
|
|
84
|
+
{ id: 'power', label: 'Power', key: '4' },
|
|
85
|
+
{ id: 'battery', label: 'Battery', key: '5' },
|
|
86
|
+
{ id: 'thermal', label: 'Thermal', key: '6' },
|
|
87
|
+
{ id: 'network', label: 'Network', key: '7' },
|
|
88
|
+
{ id: 'packets', label: 'Packets', key: '8' },
|
|
89
|
+
{ id: 'tasks', label: 'Tasks', key: '9' },
|
|
90
|
+
{ id: 'disk', label: 'Disk', key: '0' },
|
|
91
|
+
{ id: 'process', label: 'Process', key: 'P' },
|
|
92
|
+
{ id: 'p2p', label: 'P2P', key: 'R' },
|
|
93
|
+
] as const,
|
|
94
|
+
p2pEvents: [] as { type: string; detail: string; ts: number }[],
|
|
95
|
+
p2pBind: '',
|
|
96
|
+
p2pServer: null as { stop: () => void; peerEventHistory: { type: string; detail: string; ts: number }[] } | null,
|
|
97
|
+
p2pServerRunning: false,
|
|
98
|
+
p2pPort: 9876,
|
|
99
|
+
p2pPassword: 'pyre',
|
|
93
100
|
};
|
|
94
101
|
|
|
95
102
|
function setStatus(msg: string, ms = 3000) {
|
package/src/live/types.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface LiveOptions {
|
|
|
14
14
|
|
|
15
15
|
export type ExportFormat = 'json' | 'csv' | 'tsv';
|
|
16
16
|
|
|
17
|
-
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer';
|
|
17
|
+
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p';
|
|
18
18
|
|
|
19
19
|
type SortMode = 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
|
|
20
20
|
|
|
@@ -627,7 +627,7 @@ export async function collectPackets(): Promise<PacketData | null> {
|
|
|
627
627
|
let topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[] = [];
|
|
628
628
|
let allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[] = [];
|
|
629
629
|
try {
|
|
630
|
-
|
|
630
|
+
const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '')).trim();
|
|
631
631
|
const lsofLines = lsof.split('\n');
|
|
632
632
|
topProcesses = lsofLines.map(line => {
|
|
633
633
|
const parts = line.trim().split(/\s+/);
|