pyre-cli 3.0.0 → 3.2.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 -12
- package/dist/index.js +1221 -1021
- package/package.json +1 -1
- package/src/formatters/index.ts +1 -1
- package/src/formatters/render.ts +2 -1
- package/src/index.ts +2 -2
- package/src/live/input.ts +136 -3
- package/src/live/render.ts +70 -32
- package/src/live/state.ts +22 -15
- package/src/live/types.ts +1 -1
- package/src/p2p/server.ts +50 -13
- package/src/p2p/types.ts +2 -0
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
|
@@ -69,14 +69,14 @@ async function main() {
|
|
|
69
69
|
if (cmd === 'live') {
|
|
70
70
|
program.args.shift();
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
const splashPromise = showSplash();
|
|
73
73
|
const interval = parseFloat(opts.interval) || 2;
|
|
74
74
|
await startLive({
|
|
75
75
|
interval,
|
|
76
76
|
detailed: opts.detailed,
|
|
77
77
|
exportDir: opts.exportDir,
|
|
78
78
|
autoLog: opts.log,
|
|
79
|
-
});
|
|
79
|
+
}, splashPromise);
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
|
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;
|
|
@@ -118,6 +199,33 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
118
199
|
state.intervalHandle = setInterval(tick, state.interval * 1000);
|
|
119
200
|
}
|
|
120
201
|
|
|
202
|
+
async function doWarmup() {
|
|
203
|
+
try {
|
|
204
|
+
const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
|
|
205
|
+
state.lastData = data;
|
|
206
|
+
|
|
207
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
208
|
+
state.history.push({
|
|
209
|
+
cpuUsage: data.cpu.usage,
|
|
210
|
+
memUsage: data.memory.usagePercent,
|
|
211
|
+
temp,
|
|
212
|
+
rxBytes: data.network.rxBytes,
|
|
213
|
+
txBytes: data.network.txBytes,
|
|
214
|
+
gpuUtil: data.gpu?.utilization,
|
|
215
|
+
powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
|
|
216
|
+
rxPackets: data.network.rxPackets,
|
|
217
|
+
txPackets: data.network.txPackets,
|
|
218
|
+
connections: data.network.connections ?? 0,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
writeLogRow(data);
|
|
222
|
+
checkAlerts(data);
|
|
223
|
+
syncP2PEvents();
|
|
224
|
+
} catch {
|
|
225
|
+
// skip bad warmup
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
121
229
|
async function tick() {
|
|
122
230
|
if (state.paused) return;
|
|
123
231
|
try {
|
|
@@ -140,6 +248,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
140
248
|
|
|
141
249
|
writeLogRow(data);
|
|
142
250
|
checkAlerts(data);
|
|
251
|
+
syncP2PEvents();
|
|
143
252
|
render();
|
|
144
253
|
} catch {
|
|
145
254
|
// skip bad tick
|
|
@@ -153,7 +262,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
153
262
|
* rendering interval. Idempotent — calling while already
|
|
154
263
|
* running is a no-op.
|
|
155
264
|
*/
|
|
156
|
-
export async function startLive(opts: LiveOptions) {
|
|
265
|
+
export async function startLive(opts: LiveOptions, splashPromise?: Promise<void>) {
|
|
157
266
|
if (state.running) return;
|
|
158
267
|
state.running = true;
|
|
159
268
|
state.paused = false;
|
|
@@ -165,6 +274,12 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
165
274
|
state.history.reset();
|
|
166
275
|
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
167
276
|
|
|
277
|
+
const warmupPromise = doWarmup();
|
|
278
|
+
|
|
279
|
+
if (splashPromise) {
|
|
280
|
+
await splashPromise;
|
|
281
|
+
}
|
|
282
|
+
|
|
168
283
|
process.stdout.write('\x1b[?1049h');
|
|
169
284
|
process.stdout.write('\x1b[?25l');
|
|
170
285
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
@@ -174,7 +289,9 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
174
289
|
process.stdout.write('\x1b[?1000h');
|
|
175
290
|
}
|
|
176
291
|
|
|
177
|
-
await
|
|
292
|
+
await warmupPromise;
|
|
293
|
+
render();
|
|
294
|
+
|
|
178
295
|
restartTicker();
|
|
179
296
|
|
|
180
297
|
if (opts.autoLog) startLogging();
|
|
@@ -252,6 +369,17 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
252
369
|
}
|
|
253
370
|
|
|
254
371
|
switch (key.name) {
|
|
372
|
+
case 'r':
|
|
373
|
+
if (state.p2pServerRunning) {
|
|
374
|
+
stopP2PServer();
|
|
375
|
+
setStatus('P2P server stopped');
|
|
376
|
+
render();
|
|
377
|
+
} else {
|
|
378
|
+
state.inputMode = 'p2p';
|
|
379
|
+
state.inputBuffer = state.p2pPassword;
|
|
380
|
+
render();
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
255
383
|
case 'q':
|
|
256
384
|
stopLive();
|
|
257
385
|
break;
|
|
@@ -362,7 +490,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
362
490
|
}
|
|
363
491
|
|
|
364
492
|
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' };
|
|
493
|
+
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
494
|
return map[str] ?? null;
|
|
367
495
|
}
|
|
368
496
|
|
|
@@ -420,6 +548,11 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
420
548
|
state.logStream.end();
|
|
421
549
|
state.logStream = null;
|
|
422
550
|
}
|
|
551
|
+
if (state.p2pServer) {
|
|
552
|
+
state.p2pServer.stop();
|
|
553
|
+
state.p2pServer = null;
|
|
554
|
+
state.p2pServerRunning = false;
|
|
555
|
+
}
|
|
423
556
|
if (state.keypressHandler) {
|
|
424
557
|
process.stdin.removeListener('keypress', state.keypressHandler);
|
|
425
558
|
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
|
|
package/src/p2p/server.ts
CHANGED
|
@@ -84,12 +84,35 @@ export class P2PServer {
|
|
|
84
84
|
private rateLimitMap: Map<string, RateLimitEntry> = new Map();
|
|
85
85
|
private auditStream: fs.WriteStream | null = null;
|
|
86
86
|
private hmacKey: string;
|
|
87
|
+
private peerEvents: { type: string; detail: string; ts: number }[] = [];
|
|
88
|
+
private boundAddress = '';
|
|
87
89
|
|
|
88
90
|
constructor(options: P2PServerOptions) {
|
|
89
91
|
this.options = options;
|
|
90
92
|
this.hmacKey = deriveHMACKey(options.password);
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
get peerEventHistory(): { type: string; detail: string; ts: number }[] {
|
|
96
|
+
return this.peerEvents;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private log(msg: string): void {
|
|
100
|
+
if (this.options.onLog) {
|
|
101
|
+
this.options.onLog(msg.replace(/\x1b\[[0-9;]*m/g, ''));
|
|
102
|
+
} else {
|
|
103
|
+
console.log(msg);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private emitPeerEvent(type: string, detail: string): void {
|
|
108
|
+
const evt = { type, detail, ts: Date.now() };
|
|
109
|
+
this.peerEvents.push(evt);
|
|
110
|
+
if (this.peerEvents.length > 200) this.peerEvents.shift();
|
|
111
|
+
if (this.options.onPeerEvent) {
|
|
112
|
+
this.options.onPeerEvent(evt as any);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
93
116
|
get info(): P2PConnectionInfo {
|
|
94
117
|
return {
|
|
95
118
|
host: this.options.host,
|
|
@@ -105,7 +128,7 @@ export class P2PServer {
|
|
|
105
128
|
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
106
129
|
this.auditStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
107
130
|
} catch {
|
|
108
|
-
|
|
131
|
+
this.log(chalk.yellow('Warning: could not open audit log'));
|
|
109
132
|
}
|
|
110
133
|
}
|
|
111
134
|
|
|
@@ -130,14 +153,16 @@ export class P2PServer {
|
|
|
130
153
|
this.running = true;
|
|
131
154
|
const addr = this.server?.address();
|
|
132
155
|
const bound = addr ? (typeof addr === 'object' ? `${addr.address}:${addr.port}` : String(addr)) : 'unknown';
|
|
156
|
+
this.boundAddress = typeof addr === 'object' && addr ? addr.address : String(addr);
|
|
133
157
|
const protocol = useTLS ? 'TLS' : 'TCP';
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
this.log(chalk.green(`P2P server listening on ${bound} (${protocol})`));
|
|
159
|
+
this.log(chalk.dim(` Password: ${this.options.password}`));
|
|
160
|
+
this.log(chalk.dim(` Interval: ${this.options.intervalMs}ms`));
|
|
137
161
|
const allowedIPs = this.options.allowedIPs ?? [];
|
|
138
162
|
const deniedIPs = this.options.deniedIPs ?? [];
|
|
139
|
-
if (allowedIPs.length > 0)
|
|
140
|
-
if (deniedIPs.length > 0)
|
|
163
|
+
if (allowedIPs.length > 0) this.log(chalk.dim(` Allowed IPs: ${allowedIPs.join(', ')}`));
|
|
164
|
+
if (deniedIPs.length > 0) this.log(chalk.dim(` Denied IPs: ${deniedIPs.join(', ')}`));
|
|
165
|
+
this.emitPeerEvent('connect', `Server listening on ${bound}`);
|
|
141
166
|
resolve();
|
|
142
167
|
});
|
|
143
168
|
});
|
|
@@ -150,6 +175,7 @@ export class P2PServer {
|
|
|
150
175
|
}
|
|
151
176
|
this.peers.clear();
|
|
152
177
|
this.rateLimitMap.clear();
|
|
178
|
+
this.peerEvents = [];
|
|
153
179
|
if (this.server) {
|
|
154
180
|
this.server.close();
|
|
155
181
|
this.server = null;
|
|
@@ -210,14 +236,16 @@ export class P2PServer {
|
|
|
210
236
|
|
|
211
237
|
if (!isIPAllowed(ip, allowedIPs, deniedIPs)) {
|
|
212
238
|
this.audit(ip, 'BLOCKED', 'IP not allowed');
|
|
213
|
-
|
|
239
|
+
this.log(chalk.yellow(`Blocked IP: ${ip}`));
|
|
240
|
+
this.emitPeerEvent('blocked', ip);
|
|
214
241
|
socket.destroy();
|
|
215
242
|
return;
|
|
216
243
|
}
|
|
217
244
|
|
|
218
245
|
if (!this.checkRateLimit(ip)) {
|
|
219
246
|
this.audit(ip, 'RATE_LIMITED', 'Too many auth attempts');
|
|
220
|
-
|
|
247
|
+
this.log(chalk.yellow(`Rate limit exceeded for IP: ${ip}`));
|
|
248
|
+
this.emitPeerEvent('rate_limited', ip);
|
|
221
249
|
socket.destroy();
|
|
222
250
|
return;
|
|
223
251
|
}
|
|
@@ -238,8 +266,9 @@ export class P2PServer {
|
|
|
238
266
|
const remoteAddr = socket.remoteAddress
|
|
239
267
|
? `${socket.remoteAddress}:${socket.remotePort}`
|
|
240
268
|
: 'unknown';
|
|
241
|
-
|
|
269
|
+
this.log(chalk.cyan(`Peer connected: ${remoteAddr}`));
|
|
242
270
|
this.audit(ip, 'CONNECT', remoteAddr);
|
|
271
|
+
this.emitPeerEvent('connect', remoteAddr);
|
|
243
272
|
|
|
244
273
|
socket.on('data', (data) => this.onData(peer, data));
|
|
245
274
|
socket.on('close', () => this.onPeerClose(peer));
|
|
@@ -276,6 +305,7 @@ export class P2PServer {
|
|
|
276
305
|
if (msg.kind !== 'auth') {
|
|
277
306
|
peer.socket.write(signMessage('auth-fail', { ok: false, reason: 'Expected auth message first' }, this.hmacKey));
|
|
278
307
|
this.disconnectPeer(peer, 'Expected auth message');
|
|
308
|
+
this.emitPeerEvent('error', `Expected auth message from ${peer.ip}`);
|
|
279
309
|
return;
|
|
280
310
|
}
|
|
281
311
|
|
|
@@ -283,6 +313,7 @@ export class P2PServer {
|
|
|
283
313
|
if (!authPayload.passwordHash || !authPayload.nonce) {
|
|
284
314
|
peer.socket.write(signMessage('auth-fail', { ok: false, reason: 'Invalid auth payload' }, this.hmacKey));
|
|
285
315
|
this.disconnectPeer(peer, 'Invalid auth payload');
|
|
316
|
+
this.emitPeerEvent('error', `Invalid auth payload from ${peer.ip}`);
|
|
286
317
|
return;
|
|
287
318
|
}
|
|
288
319
|
|
|
@@ -290,6 +321,7 @@ export class P2PServer {
|
|
|
290
321
|
this.audit(peer.ip, 'AUTH_FAIL', 'Nonce mismatch');
|
|
291
322
|
peer.socket.write(signMessage('auth-fail', { ok: false, reason: 'Invalid nonce' }, this.hmacKey));
|
|
292
323
|
this.disconnectPeer(peer, 'Invalid nonce');
|
|
324
|
+
this.emitPeerEvent('error', `Invalid nonce from ${peer.ip}`);
|
|
293
325
|
return;
|
|
294
326
|
}
|
|
295
327
|
|
|
@@ -298,6 +330,7 @@ export class P2PServer {
|
|
|
298
330
|
this.audit(peer.ip, 'AUTH_FAIL', 'Invalid password hash');
|
|
299
331
|
peer.socket.write(signMessage('auth-fail', { ok: false, reason: 'Invalid password' }, this.hmacKey));
|
|
300
332
|
this.disconnectPeer(peer, 'Authentication failed');
|
|
333
|
+
this.emitPeerEvent('error', `Invalid password from ${peer.ip}`);
|
|
301
334
|
return;
|
|
302
335
|
}
|
|
303
336
|
|
|
@@ -305,8 +338,9 @@ export class P2PServer {
|
|
|
305
338
|
this.cleanupRateLimit(peer.ip);
|
|
306
339
|
peer.pendingNonce = null;
|
|
307
340
|
peer.socket.write(signMessage('auth-ok', { ok: true }, this.hmacKey));
|
|
308
|
-
|
|
341
|
+
this.log(chalk.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
|
|
309
342
|
this.audit(peer.ip, 'AUTH_OK', 'Challenge-response successful');
|
|
343
|
+
this.emitPeerEvent('auth', peer.socket.remoteAddress ?? peer.ip);
|
|
310
344
|
this.startDataStream(peer);
|
|
311
345
|
this.startPing(peer);
|
|
312
346
|
} catch {
|
|
@@ -345,18 +379,21 @@ export class P2PServer {
|
|
|
345
379
|
|
|
346
380
|
private onPeerClose(peer: Peer): void {
|
|
347
381
|
this.cleanupPeer(peer);
|
|
348
|
-
|
|
382
|
+
this.log(chalk.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
|
|
383
|
+
this.emitPeerEvent('disconnect', peer.socket.remoteAddress ?? peer.ip);
|
|
349
384
|
this.audit(peer.ip, 'DISCONNECT', 'Peer closed connection');
|
|
350
385
|
}
|
|
351
386
|
|
|
352
387
|
private onPeerError(peer: Peer, err: Error): void {
|
|
353
388
|
const code = (err as any).code;
|
|
354
389
|
if (code === 'ECONNRESET') {
|
|
355
|
-
|
|
390
|
+
this.log(chalk.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
|
|
356
391
|
this.audit(peer.ip, 'ERROR', 'Connection reset');
|
|
392
|
+
this.emitPeerEvent('error', `connection reset: ${peer.socket.remoteAddress}`);
|
|
357
393
|
} else {
|
|
358
|
-
|
|
394
|
+
this.log(chalk.red(`Peer error: ${err.message}`));
|
|
359
395
|
this.audit(peer.ip, 'ERROR', err.message);
|
|
396
|
+
this.emitPeerEvent('error', err.message);
|
|
360
397
|
}
|
|
361
398
|
this.cleanupPeer(peer);
|
|
362
399
|
}
|
package/src/p2p/types.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface P2PServerOptions {
|
|
|
72
72
|
deniedIPs?: string[];
|
|
73
73
|
auditLog?: string;
|
|
74
74
|
hmacKey?: string;
|
|
75
|
+
onLog?: (msg: string) => void;
|
|
76
|
+
onPeerEvent?: (event: { type: 'connect' | 'auth' | 'disconnect' | 'error' | 'blocked' | 'rate_limited'; detail: string; ts: number }) => void;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
export interface P2PClientOptions {
|