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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyre-cli",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "CLI Mac monitoring tool: temps, system stats, live dashboard, packet monitor, battery predictor, export",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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';
@@ -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: 'p' },
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
- await showSplash();
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 tick();
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;
@@ -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
- lines.push(
61
- formatTable(state.lastData, {
62
- width: state.termWidth,
63
- sortBy: state.sortMode,
64
- filter: state.processFilter || undefined,
65
- processLimit: processRowBudget(),
66
- theme: state.currentTheme,
67
- visible: state.visiblePanels,
68
- treeView: state.treeView,
69
- activePanel: state.activePanel,
70
- })
71
- );
72
-
73
- if (state.showGraphs) {
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
- ['p', state.paused ? 'resume' : 'pause'],
106
- ['g', state.showGraphs ? 'hide graphs' : 'show graphs'],
107
- ['b', `graph:${state.graphMode}`],
108
- ['d', state.detailed ? 'basic' : 'detailed'],
109
- ['←/→', 'tab'],
110
- ['1-9', 'panels'],
111
- ['s', `sort:${state.sortMode}`],
112
- ['/', 'filter'],
113
- ['k', 'kill'],
114
- ['S', 'signal'],
115
- ['t', state.treeView ? 'flat' : 'tree'],
116
- ['e', 'export'],
117
- ['l', state.logging ? 'stop log' : 'log'],
118
- ['f', state.exportFormat],
119
- ['+/-', `${state.interval}s`],
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
- 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
- ] as const,
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
- console.log(chalk.yellow('Warning: could not open audit log'));
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
- console.log(chalk.green(`P2P server listening on ${bound} (${protocol})`));
135
- console.log(chalk.dim(` Password: ${this.options.password}`));
136
- console.log(chalk.dim(` Interval: ${this.options.intervalMs}ms`));
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) console.log(chalk.dim(` Allowed IPs: ${allowedIPs.join(', ')}`));
140
- if (deniedIPs.length > 0) console.log(chalk.dim(` Denied IPs: ${deniedIPs.join(', ')}`));
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
- console.log(chalk.yellow(`Blocked IP: ${ip}`));
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
- console.log(chalk.yellow(`Rate limit exceeded for IP: ${ip}`));
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
- console.log(chalk.cyan(`Peer connected: ${remoteAddr}`));
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
- console.log(chalk.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
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
- console.log(chalk.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
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
- console.log(chalk.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
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
- console.log(chalk.red(`Peer error: ${err.message}`));
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 {