pyre-cli 1.3.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.
@@ -0,0 +1,219 @@
1
+ import net from 'node:net';
2
+ import chalk from 'chalk';
3
+ import { collectAll } from '../monitors/index.js';
4
+ import { encodeAuthOk, encodeAuthFail, encodeData, encodePing, encodePong, encodeDisconnect } from './protocol.js';
5
+ import type { P2PServerOptions, P2PConnectionInfo } from './types.js';
6
+
7
+ interface Peer {
8
+ socket: net.Socket;
9
+ authenticated: boolean;
10
+ intervalHandle: NodeJS.Timeout | null;
11
+ pingHandle: NodeJS.Timeout | null;
12
+ readBuffer: Buffer;
13
+ readExpectingHeader: boolean;
14
+ readMessageLength: number;
15
+ }
16
+
17
+ const HEADER_SIZE = 4;
18
+
19
+ function readMessageFromPeer(peer: Peer): Buffer | null {
20
+ while (peer.readBuffer.length > 0) {
21
+ if (peer.readExpectingHeader) {
22
+ if (peer.readBuffer.length >= HEADER_SIZE) {
23
+ peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
24
+ peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE);
25
+ peer.readExpectingHeader = false;
26
+ } else {
27
+ break;
28
+ }
29
+ }
30
+
31
+ if (!peer.readExpectingHeader) {
32
+ if (peer.readBuffer.length >= peer.readMessageLength) {
33
+ const body = peer.readBuffer.subarray(0, peer.readMessageLength);
34
+ peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
35
+ peer.readExpectingHeader = true;
36
+ return body;
37
+ } else {
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ export class P2PServer {
46
+ private server: net.Server | null = null;
47
+ private peers: Map<net.Socket, Peer> = new Map();
48
+ private options: P2PServerOptions;
49
+ private running = false;
50
+
51
+ constructor(options: P2PServerOptions) {
52
+ this.options = options;
53
+ }
54
+
55
+ get info(): P2PConnectionInfo {
56
+ return {
57
+ host: this.options.host,
58
+ port: this.options.port,
59
+ peersConnected: this.peers.size,
60
+ };
61
+ }
62
+
63
+ async start(): Promise<void> {
64
+ return new Promise((resolve, reject) => {
65
+ this.server = net.createServer((socket) => this.onConnection(socket));
66
+ this.server.on('error', (err) => reject(err));
67
+ this.server.listen(this.options.port, this.options.host, () => {
68
+ this.running = true;
69
+ const addr = this.server?.address();
70
+ const bound = addr ? (typeof addr === 'object' ? `${addr.address}:${addr.port}` : String(addr)) : 'unknown';
71
+ console.log(chalk.green(`P2P server listening on ${bound}`));
72
+ console.log(chalk.dim(` Password: ${this.options.password}`));
73
+ console.log(chalk.dim(` Interval: ${this.options.intervalMs}ms`));
74
+ resolve();
75
+ });
76
+ });
77
+ }
78
+
79
+ stop(): void {
80
+ this.running = false;
81
+ for (const peer of this.peers.values()) {
82
+ this.disconnectPeer(peer, 'Server shutting down');
83
+ }
84
+ this.peers.clear();
85
+ if (this.server) {
86
+ this.server.close();
87
+ this.server = null;
88
+ }
89
+ }
90
+
91
+ private onConnection(socket: net.Socket): void {
92
+ const peer: Peer = {
93
+ socket,
94
+ authenticated: false,
95
+ intervalHandle: null,
96
+ pingHandle: null,
97
+ readBuffer: Buffer.alloc(0),
98
+ readExpectingHeader: true,
99
+ readMessageLength: 0,
100
+ };
101
+ this.peers.set(socket, peer);
102
+
103
+ const remoteAddr = socket.remoteAddress
104
+ ? `${socket.remoteAddress}:${socket.remotePort}`
105
+ : 'unknown';
106
+ console.log(chalk.cyan(`Peer connected: ${remoteAddr}`));
107
+
108
+ socket.on('data', (data) => this.onData(peer, data));
109
+ socket.on('close', () => this.onPeerClose(peer));
110
+ socket.on('error', (err) => this.onPeerError(peer, err));
111
+
112
+ socket.setTimeout(30000);
113
+ socket.on('timeout', () => {
114
+ if (!peer.authenticated) {
115
+ this.disconnectPeer(peer, 'Authentication timeout');
116
+ }
117
+ });
118
+ }
119
+
120
+ private onData(peer: Peer, data: Buffer): void {
121
+ peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
122
+
123
+ if (!peer.authenticated) {
124
+ const body = readMessageFromPeer(peer);
125
+ if (body) {
126
+ this.handleAuthMessage(peer, body);
127
+ }
128
+ return;
129
+ }
130
+ }
131
+
132
+ private handleAuthMessage(peer: Peer, body: Buffer): void {
133
+ try {
134
+ const msg = JSON.parse(body.toString('utf-8'));
135
+ if (msg.kind === 'auth' && typeof msg.payload?.password === 'string') {
136
+ if (msg.payload.password === this.options.password) {
137
+ peer.authenticated = true;
138
+ peer.socket.write(encodeAuthOk());
139
+ console.log(chalk.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
140
+ this.startDataStream(peer);
141
+ this.startPing(peer);
142
+ } else {
143
+ peer.socket.write(encodeAuthFail('Invalid password'));
144
+ this.disconnectPeer(peer, 'Authentication failed');
145
+ }
146
+ } else {
147
+ peer.socket.write(encodeAuthFail('Expected auth message first'));
148
+ this.disconnectPeer(peer, 'Expected auth message');
149
+ }
150
+ } catch {
151
+ peer.socket.write(encodeAuthFail('Invalid message format'));
152
+ this.disconnectPeer(peer, 'Invalid message format');
153
+ }
154
+ }
155
+
156
+ private startDataStream(peer: Peer): void {
157
+ const send = async () => {
158
+ if (!peer.authenticated || peer.socket.destroyed) return;
159
+ try {
160
+ const data = await collectAll({ detailed: this.options.detailed });
161
+ peer.socket.write(encodeData(data));
162
+ } catch {
163
+ // skip failed tick
164
+ }
165
+ };
166
+
167
+ send();
168
+ peer.intervalHandle = setInterval(send, this.options.intervalMs);
169
+ }
170
+
171
+ private startPing(peer: Peer): void {
172
+ peer.pingHandle = setInterval(() => {
173
+ if (peer.socket.destroyed) return;
174
+ peer.socket.write(encodePing());
175
+ }, 5000);
176
+ }
177
+
178
+ private onPeerClose(peer: Peer): void {
179
+ this.cleanupPeer(peer);
180
+ console.log(chalk.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
181
+ }
182
+
183
+ private onPeerError(peer: Peer, err: Error): void {
184
+ const code = (err as any).code;
185
+ if (code === 'ECONNRESET') {
186
+ console.log(chalk.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
187
+ } else {
188
+ console.log(chalk.red(`Peer error: ${err.message}`));
189
+ }
190
+ this.cleanupPeer(peer);
191
+ }
192
+
193
+ private disconnectPeer(peer: Peer, reason: string): void {
194
+ try {
195
+ peer.socket.write(encodeDisconnect(reason));
196
+ } catch {
197
+ // socket may already be closed
198
+ }
199
+ peer.socket.destroy();
200
+ }
201
+
202
+ private cleanupPeer(peer: Peer): void {
203
+ if (peer.intervalHandle) {
204
+ clearInterval(peer.intervalHandle);
205
+ peer.intervalHandle = null;
206
+ }
207
+ if (peer.pingHandle) {
208
+ clearInterval(peer.pingHandle);
209
+ peer.pingHandle = null;
210
+ }
211
+ this.peers.delete(peer.socket);
212
+ }
213
+ }
214
+
215
+ export async function startP2PServer(options: P2PServerOptions): Promise<P2PServer> {
216
+ const server = new P2PServer(options);
217
+ await server.start();
218
+ return server;
219
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * P2P live data transfer protocol types.
3
+ *
4
+ * Defines the message shapes exchanged between a pyre server
5
+ * (host) and a pyre client (peer) over a TCP connection.
6
+ */
7
+
8
+ export type P2PMessageKind = 'auth' | 'auth-ok' | 'auth-fail' | 'data' | 'ping' | 'pong' | 'disconnect';
9
+
10
+ export interface P2PMessage {
11
+ kind: P2PMessageKind;
12
+ payload: unknown;
13
+ }
14
+
15
+ export interface AuthMessage {
16
+ password: string;
17
+ }
18
+
19
+ export interface AuthOkMessage {
20
+ ok: true;
21
+ }
22
+
23
+ export interface AuthFailMessage {
24
+ ok: false;
25
+ reason: string;
26
+ }
27
+
28
+ export interface DataMessage {
29
+ stats: import('../monitors/types.js').StatsData;
30
+ }
31
+
32
+ export interface PingMessage {
33
+ ts: number;
34
+ }
35
+
36
+ export interface PongMessage {
37
+ ts: number;
38
+ }
39
+
40
+ export interface DisconnectMessage {
41
+ reason: string;
42
+ }
43
+
44
+ export type P2PPayload =
45
+ | AuthMessage
46
+ | AuthOkMessage
47
+ | AuthFailMessage
48
+ | DataMessage
49
+ | PingMessage
50
+ | PongMessage
51
+ | DisconnectMessage;
52
+
53
+ export interface P2PServerOptions {
54
+ host: string;
55
+ port: number;
56
+ password: string;
57
+ intervalMs: number;
58
+ detailed: boolean;
59
+ }
60
+
61
+ export interface P2PClientOptions {
62
+ host: string;
63
+ port: number;
64
+ password: string;
65
+ }
66
+
67
+ export interface P2PConnectionInfo {
68
+ host: string;
69
+ port: number;
70
+ peersConnected: number;
71
+ }
package/src/pyre_img ADDED
Binary file
@@ -0,0 +1,34 @@
1
+ const BLOCKS = '▁▂▃▄▅▆▇█';
2
+
3
+ /**
4
+ * Render a series of numbers as a compact unicode sparkline.
5
+ *
6
+ * Values are normalised against `min`/`max` (auto-computed
7
+ * from the data if not supplied). The output uses eight
8
+ * Unicode block characters to represent the range, making
9
+ * it suitable for inline terminal graphs.
10
+ *
11
+ * @param values - Numeric series to render.
12
+ * @param opts.min - Minimum value for normalisation.
13
+ * @param opts.max - Maximum value for normalisation.
14
+ * @returns A string of block characters whose length equals `values.length`.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * sparkline([10, 20, 15, 30, 25]); // "▁▂▃▅▄"
19
+ * ```
20
+ */
21
+ export function sparkline(values: number[], opts: { min?: number; max?: number } = {}): string {
22
+ if (!values.length) return '';
23
+ const min = opts.min ?? Math.min(...values);
24
+ const max = opts.max ?? Math.max(...values);
25
+ const range = max - min || 1;
26
+
27
+ return values
28
+ .map(v => {
29
+ const idx = Math.round(((v - min) / range) * (BLOCKS.length - 1));
30
+ const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
31
+ return BLOCKS[clamped];
32
+ })
33
+ .join('');
34
+ }
package/src/splash.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ASCII Splash Screen implementation based on the doom-fire effect.
3
+ */
4
+ import chalk from 'chalk';
5
+
6
+ const MAXV = 36;
7
+ const CHAR_RAMP = [' ', '.', '`', ',', ':', ';', '+', '*', '?', '%', '#', '@'];
8
+
9
+ // Colors for the fire effect
10
+ const palette = [
11
+ '#0a0505', '#1a0806', '#2a0b06', '#3f1004', '#591404', '#791804', '#9e1c04', '#c92004',
12
+ '#d63004', '#e34004', '#f05004', '#fd6004', '#ff7004', '#ff8004', '#ff9004', '#ffa004',
13
+ '#ffb004', '#ffc004', '#ffd004', '#ffe004', '#fff004', '#fff820', '#ffff40', '#ffff60',
14
+ '#ffff80', '#ffffa0', '#ffffc0', '#ffffe0', '#ffffff', '#ffffff', '#ffffff', '#ffffff',
15
+ '#ffffff', '#ffffff', '#ffffff', '#ffffff'
16
+ ];
17
+
18
+ function getRgb(hex: string) {
19
+ const r = parseInt(hex.slice(1, 3), 16);
20
+ const g = parseInt(hex.slice(3, 5), 16);
21
+ const b = parseInt(hex.slice(5, 7), 16);
22
+ return { r, g, b };
23
+ }
24
+
25
+ const rgbPalette = palette.map(getRgb);
26
+
27
+ function getChar(val: number) {
28
+ const idx = Math.min(CHAR_RAMP.length - 1, Math.floor((val / (MAXV + 1)) * CHAR_RAMP.length));
29
+ return CHAR_RAMP[idx];
30
+ }
31
+
32
+ export async function showSplash() {
33
+ const cols = 60;
34
+ const rows = 15;
35
+ let fire = new Uint8Array(cols * rows);
36
+
37
+ // Clear screen
38
+ process.stdout.write('\x1b[2J');
39
+ const termRows = process.stdout.rows || 24;
40
+ const termCols = process.stdout.columns || 80;
41
+ const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
42
+ const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
43
+
44
+ return new Promise<void>((resolve) => {
45
+ const interval = setInterval(() => {
46
+ // Bottom row heat
47
+ for (let x = 0; x < cols; x++) {
48
+ fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
49
+ }
50
+
51
+ // Spread fire
52
+ for (let y = rows - 1; y >= 1; y--) {
53
+ for (let x = 0; x < cols; x++) {
54
+ const src = y * cols + x;
55
+ const val = fire[src];
56
+ if (val === 0) {
57
+ fire[(y - 1) * cols + x] = 0;
58
+ continue;
59
+ }
60
+ const decay = Math.floor(Math.random() * 3);
61
+ const drift = Math.floor(Math.random() * 3) - 1;
62
+ let nx = x + drift;
63
+ if (nx < 0) nx = 0; else if (nx >= cols) nx = cols - 1;
64
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
65
+ }
66
+ }
67
+
68
+ // Draw
69
+ let output = '';
70
+ for (let i = 0; i < startRow; i++) output += '\n';
71
+
72
+ for (let y = 0; y < rows; y++) {
73
+ output += ' '.repeat(startCol);
74
+ for (let x = 0; x < cols; x++) {
75
+ const v = fire[y * cols + x];
76
+ const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
77
+ output += chalk.rgb(color.r, color.g, color.b)(getChar(v));
78
+ }
79
+ output += '\n';
80
+ }
81
+ output += ' '.repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk.bold.white('🔥 pyre — system monitor (warming up sensors...)\n');
82
+ output += ' '.repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk.dim('[Press Enter to continue]');
83
+
84
+ // Clear viewport area and redraw
85
+ process.stdout.write(`\x1b[H${output}`);
86
+ }, 65);
87
+
88
+ const onEnter = () => {
89
+ process.stdin.removeListener('data', onEnter);
90
+ clearInterval(interval);
91
+ process.stdout.write('\x1b[2J\x1b[H');
92
+ resolve();
93
+ };
94
+
95
+ process.stdin.setRawMode(true);
96
+ process.stdin.resume();
97
+ process.stdin.on('data', onEnter);
98
+
99
+ // Auto-exit after 5s
100
+ setTimeout(onEnter, 5000);
101
+ });
102
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": false,
14
+ "sourceMap": false,
15
+ "noEmit": false
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }