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.
package/dist/index.js CHANGED
@@ -2,11 +2,14 @@
2
2
 
3
3
  // src/index.ts
4
4
  import fs2 from "fs";
5
+ import readline2 from "readline";
5
6
  import { Command } from "commander";
6
- import chalk4 from "chalk";
7
+ import chalk8 from "chalk";
7
8
 
8
- // src/monitors.ts
9
+ // src/monitors/collectors.ts
9
10
  import chalk from "chalk";
11
+
12
+ // src/monitors/run.ts
10
13
  async function run(cmd, fallback = "") {
11
14
  try {
12
15
  const { execSync } = await import("child_process");
@@ -15,6 +18,8 @@ async function run(cmd, fallback = "") {
15
18
  return fallback;
16
19
  }
17
20
  }
21
+
22
+ // src/monitors/smc.ts
18
23
  var smcCache = null;
19
24
  async function getSmcMetrics() {
20
25
  const now = Date.now();
@@ -53,17 +58,53 @@ async function getSmcMetrics() {
53
58
  function parseSuffix(s) {
54
59
  return s.toUpperCase() === "K" ? 1024 : s.toUpperCase() === "M" ? 1024 * 1024 : s.toUpperCase() === "G" ? 1024 * 1024 * 1024 : 1;
55
60
  }
61
+
62
+ // src/monitors/collectors.ts
63
+ var BATTERY_WINDOW_MS = 15 * 60 * 1e3;
64
+ var MIN_ESTIMATE_WINDOW_MS = 30 * 1e3;
65
+ var prevBattery = { samples: [] };
66
+ function estimateBatteryLife(level, state2) {
67
+ const now = Date.now();
68
+ if (state2 === "charged" || state2 === "charging") {
69
+ prevBattery.samples = [];
70
+ return {};
71
+ }
72
+ prevBattery.samples.push({ level, ts: now });
73
+ prevBattery.samples = prevBattery.samples.filter((s) => now - s.ts <= BATTERY_WINDOW_MS);
74
+ const first = prevBattery.samples[0];
75
+ const elapsedMs = now - first.ts;
76
+ if (elapsedMs < MIN_ESTIMATE_WINDOW_MS) {
77
+ return {};
78
+ }
79
+ const dtHours = Math.max(elapsedMs / 1e3 / 3600, 1e-4);
80
+ const discharged = first.level - level;
81
+ if (discharged <= 0) {
82
+ return { estimatedTimeToEmpty: "calculating" };
83
+ }
84
+ const dischargeRate = discharged / dtHours;
85
+ const remainingLevel = Math.max(level, 0);
86
+ const hoursToEmpty = remainingLevel / dischargeRate;
87
+ const h = Math.floor(hoursToEmpty);
88
+ const m = Math.round((hoursToEmpty - h) * 60);
89
+ const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : "calculating";
90
+ const ASSUMED_CAPACITY_WH = 55;
91
+ const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
92
+ return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
93
+ }
56
94
  async function collectAll(opts2 = {}) {
57
- const [cpu, memory, disk, battery, thermal, network, processes, system, power] = await Promise.all([
95
+ const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
58
96
  collectCpu(),
97
+ collectGpu(opts2.detailed),
59
98
  collectMemory(),
60
99
  collectDisk(),
61
100
  collectBattery(),
62
101
  collectThermal(opts2.detailed),
63
102
  collectNetwork(),
64
- collectProcesses(opts2.processLimit ?? 10),
103
+ collectProcesses(opts2.processLimit),
65
104
  collectSystem(),
66
- collectPower()
105
+ collectPower(),
106
+ collectPackets(),
107
+ collectTasks()
67
108
  ]);
68
109
  return {
69
110
  header: {
@@ -73,6 +114,7 @@ async function collectAll(opts2 = {}) {
73
114
  uptime: system.uptime
74
115
  },
75
116
  cpu,
117
+ gpu,
76
118
  memory,
77
119
  disk,
78
120
  battery,
@@ -80,14 +122,79 @@ async function collectAll(opts2 = {}) {
80
122
  network,
81
123
  processes,
82
124
  power,
83
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
125
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
126
+ packets,
127
+ tasks
84
128
  };
85
129
  }
86
130
  async function collectPower() {
87
131
  try {
88
132
  const { power } = await getSmcMetrics();
89
- if (power.cpu === void 0 && power.gpu === void 0 && power.combined === void 0) return null;
90
- return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
133
+ if (power.cpu !== void 0 || power.gpu !== void 0 || power.combined !== void 0) {
134
+ return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
135
+ }
136
+ } catch {
137
+ }
138
+ try {
139
+ const ioreg = (await run("ioreg -rn AppleSmartBattery 2>/dev/null")).trim();
140
+ const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
141
+ const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
142
+ if (ampMatch && voltMatch) {
143
+ let ampsBig = BigInt(ampMatch[1]);
144
+ if (!ampMatch[1].startsWith("-")) {
145
+ const UINT64_MAX_PLUS_1 = 1n << 64n;
146
+ const INT64_MAX = (1n << 63n) - 1n;
147
+ if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
148
+ }
149
+ const amps = Number(ampsBig);
150
+ const volts = parseInt(voltMatch[1], 10) / 1e3;
151
+ const watts = Math.abs(amps / 1e3 * volts);
152
+ if (Number.isFinite(watts) && watts < 500) {
153
+ return { combinedWatts: Math.round(watts * 100) / 100 };
154
+ }
155
+ }
156
+ } catch {
157
+ }
158
+ return null;
159
+ }
160
+ async function collectGpu(detailed) {
161
+ try {
162
+ const spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
163
+ if (!spRaw) return null;
164
+ const modelMatch = spRaw.match(/Chipset Model:\s*(.+)/i);
165
+ const model = modelMatch ? modelMatch[1].trim() : "Unknown";
166
+ const memMatch = spRaw.match(/VRAM \(Total\):\s*(.+)/i);
167
+ let memory = 0;
168
+ if (memMatch) {
169
+ const memStr = memMatch[1].trim();
170
+ const memNum = parseFloat(memStr);
171
+ if (memStr.includes("GB")) memory = memNum * 1024 * 1024 * 1024;
172
+ else if (memStr.includes("MB")) memory = memNum * 1024 * 1024;
173
+ }
174
+ let utilization = 0;
175
+ let temperature;
176
+ let gpuProcesses = 0;
177
+ if (detailed) {
178
+ try {
179
+ const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
180
+ const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
181
+ if (gpuPowerMatch) {
182
+ utilization = Math.min(100, Math.round(parseFloat(gpuPowerMatch[1]) / 10));
183
+ }
184
+ } catch {
185
+ }
186
+ try {
187
+ const { temps } = await getSmcMetrics();
188
+ if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
189
+ } catch {
190
+ }
191
+ try {
192
+ const psRaw = (await run('ps aux | grep -i "[g]pu" | wc -l')).trim();
193
+ gpuProcesses = parseInt(psRaw) || 0;
194
+ } catch {
195
+ }
196
+ }
197
+ return { model, memory, utilization, temperature, processes: gpuProcesses };
91
198
  } catch {
92
199
  return null;
93
200
  }
@@ -113,7 +220,9 @@ async function collectCpu() {
113
220
  try {
114
221
  const { freq } = await getSmcMetrics();
115
222
  const values = Object.values(freq);
116
- if (values.length) frequency = Math.round(Math.max(...values));
223
+ if (values.length) {
224
+ frequency = Math.round(Math.max(...values) / 1e6);
225
+ }
117
226
  } catch {
118
227
  }
119
228
  }
@@ -200,10 +309,10 @@ async function collectBattery() {
200
309
  if (!raw.includes("Battery Power") && !raw.includes("AC Power")) return null;
201
310
  const levelMatch = raw.match(/(\d+)%/);
202
311
  const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
203
- const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i);
312
+ const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i) || raw.match(/(\d+:\d+)\s+remaining/i);
204
313
  const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
205
314
  const level = levelMatch ? parseInt(levelMatch[1]) : 0;
206
- const state = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
315
+ const state2 = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
207
316
  const timeRemaining = timeMatch ? timeMatch[1] : powerPlugged ? "\u221E" : "calculating";
208
317
  let cycles;
209
318
  let condition;
@@ -233,50 +342,52 @@ async function collectBattery() {
233
342
  if (maxCapacityPercent !== void 0) healthParts.push(`${maxCapacityPercent}% capacity`);
234
343
  if (cycles !== void 0) healthParts.push(`${cycles} cycles`);
235
344
  const health = healthParts.length ? healthParts.join(", ") : "unknown";
345
+ const estimate = estimateBatteryLife(level, state2);
236
346
  return {
237
347
  level,
238
- state,
348
+ state: state2,
239
349
  timeRemaining,
240
350
  health,
241
351
  powerSource: powerPlugged ? "AC" : "Battery",
242
352
  cycles,
243
353
  condition,
244
- maxCapacityPercent
354
+ maxCapacityPercent,
355
+ ...estimate
245
356
  };
246
357
  } catch {
247
358
  return null;
248
359
  }
249
360
  }
250
- function pressureFromState(state) {
251
- const s = state.toLowerCase();
361
+ function pressureFromState(state2) {
362
+ const s = state2.toLowerCase();
252
363
  if (s.includes("critical")) return 3;
253
364
  if (s.includes("serious") || s.includes("heavy")) return 2;
254
365
  if (s.includes("fair") || s.includes("moderate")) return 1;
255
366
  return 0;
256
367
  }
257
- async function collectThermal(detailed2) {
368
+ async function collectThermal(detailed) {
258
369
  try {
259
370
  const therm = (await run("pmset -g therm", "")).trim();
260
- let state = "Unknown";
371
+ let state2 = "Unknown";
261
372
  let detail = therm || "No pmset thermal data";
262
373
  const stateMatch = therm.match(/Thermal state:\s*(.+)/i);
263
374
  if (stateMatch) {
264
375
  const raw = stateMatch[1].trim();
265
- state = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
266
- detail = state;
376
+ state2 = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
377
+ detail = state2;
267
378
  } else {
268
379
  const limitMatch = therm.match(
269
380
  /CPU_(?:Speed|Scheduler)_Limit\s*(?:=|:)\s*([\d.]+)/i
270
381
  );
271
382
  if (limitMatch) {
272
383
  const limit = parseFloat(limitMatch[1]);
273
- if (limit >= 100) state = "Nominal";
274
- else if (limit >= 85) state = "Fair";
275
- else if (limit >= 50) state = "Serious";
276
- else state = "Critical";
277
- detail = `${state} (CPU limit ${limit}%)`;
384
+ if (limit >= 100) state2 = "Nominal";
385
+ else if (limit >= 85) state2 = "Fair";
386
+ else if (limit >= 50) state2 = "Serious";
387
+ else state2 = "Critical";
388
+ detail = `${state2} (CPU limit ${limit}%)`;
278
389
  } else if (/no thermal warning level has been recorded/i.test(therm)) {
279
- state = "Nominal";
390
+ state2 = "Nominal";
280
391
  detail = "Nominal (no thermal warning recorded)";
281
392
  }
282
393
  }
@@ -285,7 +396,7 @@ async function collectThermal(detailed2) {
285
396
  try {
286
397
  const { temps } = await getSmcMetrics();
287
398
  cpuDieTemp = temps.cpu_die;
288
- if (detailed2) {
399
+ if (detailed) {
289
400
  if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
290
401
  if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
291
402
  if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
@@ -294,19 +405,19 @@ async function collectThermal(detailed2) {
294
405
  }
295
406
  }
296
407
  } catch {
297
- if (detailed2) detail += " (powermetrics unavailable)";
408
+ if (detailed) detail += " (powermetrics unavailable)";
298
409
  }
299
- if (state === "Unknown" && cpuDieTemp !== void 0) {
300
- if (cpuDieTemp >= 100) state = "Critical";
301
- else if (cpuDieTemp >= 90) state = "Serious";
302
- else if (cpuDieTemp >= 80) state = "Fair";
303
- else state = "Nominal";
304
- detail = `${state} (estimated from ${cpuDieTemp}\xB0C)`;
410
+ if (state2 === "Unknown" && cpuDieTemp !== void 0) {
411
+ if (cpuDieTemp >= 100) state2 = "Critical";
412
+ else if (cpuDieTemp >= 90) state2 = "Serious";
413
+ else if (cpuDieTemp >= 80) state2 = "Fair";
414
+ else state2 = "Nominal";
415
+ detail = `${state2} (estimated from ${cpuDieTemp}\xB0C)`;
305
416
  }
306
417
  return {
307
- state,
418
+ state: state2,
308
419
  detail,
309
- pressureLevel: pressureFromState(state),
420
+ pressureLevel: pressureFromState(state2),
310
421
  temperatures: Object.keys(temperatures).length ? temperatures : void 0
311
422
  };
312
423
  } catch {
@@ -346,43 +457,125 @@ async function collectNetwork() {
346
457
  return { interface: "unknown", ip: "0.0.0.0", rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0 };
347
458
  }
348
459
  }
349
- async function collectProcesses(limit = 10) {
460
+ async function collectProcesses(limit) {
350
461
  try {
351
- const raw = (await run(`ps -Ao pid,user,pcpu,pmem,comm -r | head -n ${limit + 1}`)).trim();
462
+ const isMac = process.platform === "darwin";
463
+ const sortArg = isMac ? "-r" : "--sort=-pcpu";
464
+ const threadsArg = isMac ? "threads" : "nlwp";
465
+ const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : "";
466
+ const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,${threadsArg},time,comm ${sortArg}${headClause}`)).trim();
352
467
  const lines = raw.split("\n").slice(1);
353
468
  return lines.map((line) => {
354
- const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(.+)/);
469
+ const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(\d+)\s+([\d:]+)\s+(.+)/);
355
470
  if (!parts) return null;
356
- return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), command: parts[5] };
471
+ const runtimeSec = parts[8].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
472
+ return { pid: parseInt(parts[1]), ppid: parseInt(parts[2]), user: parts[3], cpu: parseFloat(parts[4]), mem: parseFloat(parts[5]), state: parts[6], threads: parseInt(parts[7]), runtime: runtimeSec, command: parts[9] };
357
473
  }).filter((p) => p !== null && p.pid > 0);
358
474
  } catch {
359
475
  return [];
360
476
  }
361
477
  }
362
-
363
- // src/formatters.ts
364
- import chalk2 from "chalk";
365
-
366
- // src/sparkline.ts
367
- var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
368
- function sparkline(values, opts2 = {}) {
369
- if (!values.length) return "";
370
- const min = opts2.min ?? Math.min(...values);
371
- const max = opts2.max ?? Math.max(...values);
372
- const range = max - min || 1;
373
- return values.map((v) => {
374
- const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
375
- const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
376
- return BLOCKS[clamped];
377
- }).join("");
478
+ async function collectPackets() {
479
+ try {
480
+ const netStat = (await run("netstat -ib")).trim();
481
+ const lines = netStat.split("\n").slice(1);
482
+ let totalRxPackets = 0;
483
+ let totalTxPackets = 0;
484
+ let totalRxBytes = 0;
485
+ let totalTxBytes = 0;
486
+ const ifaceStats = [];
487
+ for (const line of lines) {
488
+ const parts = line.split(/\s+/);
489
+ if (parts.length >= 11 && parts[0] && !parts[0].startsWith("Name")) {
490
+ const iface = parts[0];
491
+ const rxPackets = parseInt(parts[4]) || 0;
492
+ const rxBytes = parseInt(parts[5]) || 0;
493
+ const txPackets = parseInt(parts[6]) || 0;
494
+ const txBytes = parseInt(parts[7]) || 0;
495
+ totalRxPackets += rxPackets;
496
+ totalTxPackets += txPackets;
497
+ totalRxBytes += rxBytes;
498
+ totalTxBytes += txBytes;
499
+ if (iface && !iface.includes("lo")) {
500
+ ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
501
+ }
502
+ }
503
+ }
504
+ let connections = 0;
505
+ try {
506
+ const tcpConns = (await run("netstat -an | grep ESTABLISHED | wc -l", "0")).trim();
507
+ connections = parseInt(tcpConns) || 0;
508
+ } catch {
509
+ connections = 0;
510
+ }
511
+ let topProcesses = [];
512
+ let allProcesses = [];
513
+ try {
514
+ const lsof = (await run("lsof -i -n -P -r 1 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
515
+ const lsofLines = lsof.split("\n");
516
+ topProcesses = lsofLines.map((line) => {
517
+ const parts = line.trim().split(/\s+/);
518
+ if (parts.length >= 3) {
519
+ return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
520
+ }
521
+ return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
522
+ }).filter((p) => p.pid > 0);
523
+ const allLsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
524
+ const allLsofLines = allLsof.split("\n");
525
+ allProcesses = allLsofLines.map((line) => {
526
+ const parts = line.trim().split(/\s+/);
527
+ if (parts.length >= 3) {
528
+ return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
529
+ }
530
+ return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
531
+ }).filter((p) => p.pid > 0);
532
+ } catch {
533
+ topProcesses = [];
534
+ allProcesses = [];
535
+ }
536
+ return {
537
+ totalPackets: totalRxPackets + totalTxPackets,
538
+ rxPackets: totalRxPackets,
539
+ txPackets: totalTxPackets,
540
+ rxRate: 0,
541
+ txRate: 0,
542
+ connections,
543
+ topProcesses,
544
+ interfaces: ifaceStats,
545
+ allProcesses
546
+ };
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+ async function collectTasks(limit = 12) {
552
+ try {
553
+ const isMac = process.platform === "darwin";
554
+ const sortArg = isMac ? "-r" : "--sort=-pcpu";
555
+ const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
556
+ const lines = raw.split("\n").slice(1);
557
+ return lines.map((line) => {
558
+ const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:]+)\s+(.+)/);
559
+ if (!parts) return null;
560
+ const runtimeSec = parts[6].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
561
+ return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
562
+ }).filter((t) => t !== null && t.pid > 0);
563
+ } catch {
564
+ return [];
565
+ }
378
566
  }
379
567
 
380
- // src/formatters.ts
568
+ // src/formatters/render.ts
569
+ import chalk3 from "chalk";
570
+
571
+ // src/formatters/themes.ts
572
+ import chalk2 from "chalk";
381
573
  var THEMES = {
382
574
  default: {
383
575
  border: chalk2.hex("#4b5563"),
384
576
  cpu: chalk2.hex("#22d3ee"),
385
577
  mem: chalk2.hex("#a78bfa"),
578
+ gpu: chalk2.hex("#f472b6"),
386
579
  power: chalk2.hex("#fbbf24"),
387
580
  battery: chalk2.hex("#34d399"),
388
581
  thermal: chalk2.hex("#fb923c"),
@@ -395,6 +588,7 @@ var THEMES = {
395
588
  border: chalk2.hex("#6272a4"),
396
589
  cpu: chalk2.hex("#8be9fd"),
397
590
  mem: chalk2.hex("#bd93f9"),
591
+ gpu: chalk2.hex("#ff5555"),
398
592
  power: chalk2.hex("#f1fa8c"),
399
593
  battery: chalk2.hex("#50fa7b"),
400
594
  thermal: chalk2.hex("#ffb86c"),
@@ -407,6 +601,7 @@ var THEMES = {
407
601
  border: chalk2.hex("#ff0055"),
408
602
  cpu: chalk2.hex("#00ffcc"),
409
603
  mem: chalk2.hex("#ff00ff"),
604
+ gpu: chalk2.hex("#ffff00"),
410
605
  power: chalk2.hex("#ffff00"),
411
606
  battery: chalk2.hex("#00ff00"),
412
607
  thermal: chalk2.hex("#ff6600"),
@@ -419,6 +614,7 @@ var THEMES = {
419
614
  border: chalk2.gray,
420
615
  cpu: chalk2.white.bold,
421
616
  mem: chalk2.white.bold,
617
+ gpu: chalk2.white.bold,
422
618
  power: chalk2.white.bold,
423
619
  battery: chalk2.white.bold,
424
620
  thermal: chalk2.white.bold,
@@ -426,17 +622,36 @@ var THEMES = {
426
622
  disk: chalk2.white.bold,
427
623
  graphs: chalk2.white.bold,
428
624
  process: chalk2.white.bold
625
+ },
626
+ nord: {
627
+ border: chalk2.hex("#4c566a"),
628
+ cpu: chalk2.hex("#88c0d0"),
629
+ mem: chalk2.hex("#b48ead"),
630
+ gpu: chalk2.hex("#bf616a"),
631
+ power: chalk2.hex("#ebcb8b"),
632
+ battery: chalk2.hex("#a3be8c"),
633
+ thermal: chalk2.hex("#d08770"),
634
+ network: chalk2.hex("#81a1c1"),
635
+ disk: chalk2.hex("#b48ead"),
636
+ graphs: chalk2.hex("#88c0d0"),
637
+ process: chalk2.hex("#eceff4")
638
+ },
639
+ gruvbox: {
640
+ border: chalk2.hex("#928374"),
641
+ cpu: chalk2.hex("#fb4934"),
642
+ mem: chalk2.hex("#b8bb26"),
643
+ gpu: chalk2.hex("#d3869b"),
644
+ power: chalk2.hex("#fabd2f"),
645
+ battery: chalk2.hex("#b8bb26"),
646
+ thermal: chalk2.hex("#fb4934"),
647
+ network: chalk2.hex("#83a598"),
648
+ disk: chalk2.hex("#d3869b"),
649
+ graphs: chalk2.hex("#fb4934"),
650
+ process: chalk2.hex("#ebdbb2")
429
651
  }
430
652
  };
431
- function clampWidth(width) {
432
- if (!width || Number.isNaN(width)) return 80;
433
- return Math.max(60, Math.min(width, 240));
434
- }
435
- function gridColumns(width) {
436
- if (width >= 150) return 3;
437
- if (width >= 96) return 2;
438
- return 1;
439
- }
653
+
654
+ // src/formatters/render.ts
440
655
  function visLen(s) {
441
656
  return s.replace(/\x1b\[[0-9;]*m/g, "").length;
442
657
  }
@@ -453,9 +668,8 @@ function panel(title, lines, width, accent, borderAccent, height) {
453
668
  const h = height ?? lines.length;
454
669
  const contentWidth = Math.max(1, width - 4);
455
670
  const label = ` ${title} `;
456
- const dashLen = Math.max(0, width - 3 - label.length);
457
- const top = borderAccent("\u256D\u2500" + label + "\u2500".repeat(dashLen) + "\u256E");
458
- const bottom = borderAccent("\u2570" + "\u2500".repeat(width - 2) + "\u256F");
671
+ const top = borderAccent("\u250C" + label + "\u2500".repeat(width - label.length - 2) + "\u2510");
672
+ const bottom = borderAccent("\u2514" + "\u2500".repeat(width - 2) + "\u2518");
459
673
  const body = [];
460
674
  for (let i = 0; i < h; i++) {
461
675
  const line = lines[i] ?? "";
@@ -481,31 +695,28 @@ function hstack(blocks, gap = 2) {
481
695
  }
482
696
  function statRow(label, value, width = 9) {
483
697
  const w = Math.max(width, label.length + 1);
484
- return `${chalk2.dim(label.padEnd(w))}${value}`;
485
- }
486
- function bar(percent, width = 20) {
487
- const filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * width);
488
- const empty = width - filled;
489
- const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
490
- const color = percent > 90 ? chalk2.red : percent > 70 ? chalk2.yellow : chalk2.green;
491
- return color(barStr);
698
+ return `${chalk3.dim(label.padEnd(w))}${value}`;
492
699
  }
493
700
  function gaugeRow(label, percent, contentWidth) {
494
701
  const labelW = 9;
495
702
  const pctText = `${Math.round(percent)}%`;
496
703
  const barW = Math.max(6, contentWidth - labelW - pctText.length - 1);
497
- return `${chalk2.dim(label.padEnd(labelW))}${bar(percent, barW)} ${pctText}`;
704
+ const filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * barW);
705
+ const empty = barW - filled;
706
+ const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
707
+ const color = percent > 90 ? chalk3.red : percent > 70 ? chalk3.yellow : chalk3.green;
708
+ return `${chalk3.dim(label.padEnd(labelW))}${color(barStr)} ${pctText}`;
498
709
  }
499
710
  function thermalColor(pressureLevel) {
500
- if (pressureLevel >= 3) return chalk2.red;
501
- if (pressureLevel === 2) return chalk2.yellow;
502
- if (pressureLevel === 1) return chalk2.yellowBright;
503
- return chalk2.green;
711
+ if (pressureLevel >= 3) return chalk3.red;
712
+ if (pressureLevel === 2) return chalk3.yellow;
713
+ if (pressureLevel === 1) return chalk3.yellowBright;
714
+ return chalk3.green;
504
715
  }
505
716
  function capacityColor(percent) {
506
- if (percent < 80) return chalk2.red;
507
- if (percent < 90) return chalk2.yellow;
508
- return chalk2.green;
717
+ if (percent < 80) return chalk3.red;
718
+ if (percent < 90) return chalk3.yellow;
719
+ return chalk3.green;
509
720
  }
510
721
  function formatBytes(bytes) {
511
722
  if (bytes < 1024) return `${bytes} B`;
@@ -525,7 +736,7 @@ function cpuCard(data, contentWidth) {
525
736
  statRow("Cores", `${data.cpu.physicalCores}/${data.cpu.cores} phys/log`),
526
737
  statRow("Freq", `${data.cpu.frequency} MHz`),
527
738
  gaugeRow("Usage", data.cpu.usage, contentWidth),
528
- statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" \xB7 "))
739
+ statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" "))
529
740
  ];
530
741
  if (data.cpu.temperature) lines.push(statRow("Temp", formatTemp(data.cpu.temperature)));
531
742
  return lines;
@@ -534,17 +745,32 @@ function memCard(data, contentWidth) {
534
745
  return [
535
746
  statRow("Total", formatBytes(data.memory.total)),
536
747
  statRow("Used", formatBytes(data.memory.used)),
748
+ statRow("Cached", formatBytes(data.memory.total - data.memory.free - data.memory.used)),
537
749
  statRow("Free", formatBytes(data.memory.free)),
538
750
  gaugeRow("Usage", data.memory.usagePercent, contentWidth),
539
751
  statRow("Swap", `${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}`)
540
752
  ];
541
753
  }
754
+ function gpuCard(data, contentWidth) {
755
+ if (!data.gpu) return null;
756
+ const lines = [];
757
+ lines.push(statRow("Model", truncatePlain(data.gpu.model, Math.max(4, contentWidth - 8))));
758
+ lines.push(statRow("Memory", formatBytes(data.gpu.memory)));
759
+ lines.push(statRow("Util", `${data.gpu.utilization}%`));
760
+ if (data.gpu.temperature) lines.push(statRow("Temp", formatTemp(data.gpu.temperature)));
761
+ lines.push(statRow("Procs", String(data.gpu.processes)));
762
+ return lines;
763
+ }
542
764
  function powerCard(data) {
543
- if (!data.power) return null;
544
765
  const lines = [];
545
- if (data.power.cpuWatts !== void 0) lines.push(statRow("CPU", `${data.power.cpuWatts.toFixed(2)} W`));
546
- if (data.power.gpuWatts !== void 0) lines.push(statRow("GPU", `${data.power.gpuWatts.toFixed(2)} W`));
547
- if (data.power.combinedWatts !== void 0) lines.push(statRow("Total", `${data.power.combinedWatts.toFixed(2)} W`));
766
+ if (data.power) {
767
+ if (data.power.cpuWatts !== void 0) lines.push(statRow("CPU", `${data.power.cpuWatts.toFixed(2)} W`));
768
+ if (data.power.gpuWatts !== void 0) lines.push(statRow("GPU", `${data.power.gpuWatts.toFixed(2)} W`));
769
+ if (data.power.combinedWatts !== void 0) lines.push(statRow("Total", `${data.power.combinedWatts.toFixed(2)} W`));
770
+ }
771
+ if (data.battery?.powerWatts !== void 0 && data.battery.powerWatts > 0) {
772
+ lines.push(statRow("Battery", `${data.battery.powerWatts.toFixed(2)} W`));
773
+ }
548
774
  return lines.length ? lines : null;
549
775
  }
550
776
  function batteryCard(data, contentWidth) {
@@ -553,12 +779,19 @@ function batteryCard(data, contentWidth) {
553
779
  const lines = [
554
780
  statRow("Level", `${data.battery.level}% (${data.battery.state})`),
555
781
  statRow("Time", data.battery.timeRemaining),
556
- statRow("Health", truncatePlain(data.battery.health, valW))
782
+ statRow("Cycles", String(data.battery.cycles ?? "N/A")),
783
+ statRow("Condition", data.battery.condition ?? "N/A")
557
784
  ];
558
785
  if (data.battery.maxCapacityPercent !== void 0) {
559
786
  lines.push(statRow("Capacity", capacityColor(data.battery.maxCapacityPercent)(`${data.battery.maxCapacityPercent}%`)));
560
787
  }
561
788
  lines.push(statRow("Source", data.battery.powerSource));
789
+ if (data.battery.estimatedTimeToEmpty && data.battery.state !== "charged") {
790
+ lines.push(statRow("Est. Empty", data.battery.estimatedTimeToEmpty));
791
+ }
792
+ if (data.battery.dischargeRatePerHour !== void 0 && data.battery.state !== "charged") {
793
+ lines.push(statRow("Disch. Rate", `${data.battery.dischargeRatePerHour}%/h`));
794
+ }
562
795
  return lines;
563
796
  }
564
797
  function thermalCard(data, contentWidth) {
@@ -576,50 +809,251 @@ function networkCard(data, contentWidth) {
576
809
  const valW = Math.max(6, contentWidth - 8);
577
810
  return [
578
811
  statRow("Iface", truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)),
579
- statRow("RX", `${formatBytes(data.network.rxBytes)} \xB7 ${data.network.rxPackets} pkts`),
580
- statRow("TX", `${formatBytes(data.network.txBytes)} \xB7 ${data.network.txPackets} pkts`)
812
+ statRow("RX", `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`),
813
+ statRow("TX", `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)
581
814
  ];
582
815
  }
816
+ function packetCard(data, contentWidth) {
817
+ if (!data.packets) return null;
818
+ const lines = [];
819
+ lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
820
+ lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
821
+ lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
822
+ lines.push(statRow("Conns", `${data.packets.connections} TCP`));
823
+ if (data.packets.topProcesses && data.packets.topProcesses.length) {
824
+ lines.push(statRow("Top Proc", truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
825
+ }
826
+ if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
827
+ lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} processes`));
828
+ }
829
+ if (data.packets.interfaces && data.packets.interfaces.length) {
830
+ lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
831
+ }
832
+ return lines;
833
+ }
834
+ function tasksCard(data, contentWidth) {
835
+ if (!data.tasks || !data.tasks.length) return null;
836
+ const lines = [];
837
+ for (const task of data.tasks.slice(0, 6)) {
838
+ const cmd = truncatePlain(task.command, Math.max(4, contentWidth - 28));
839
+ lines.push(statRow(`${task.pid}`, `${task.user.padEnd(8)} ${task.cpu.toFixed(1).padStart(5)}% ${task.mem.toFixed(1).padStart(5)}% ${task.state} ${cmd}`));
840
+ }
841
+ return lines;
842
+ }
583
843
  function header(data, width, badges = [], borderAccent = THEMES.default.border) {
584
- const left = `${chalk2.hex("#ff5500").bold(" PYRE ")} ${chalk2.bold(data.header.hostname)} ${chalk2.dim(data.header.os)} ${chalk2.dim("up " + data.header.uptime)}`;
585
- const right = badges.join(" ");
844
+ const now = (/* @__PURE__ */ new Date()).toLocaleTimeString();
845
+ const left = `${chalk3.hex("#ff5500").bold(" PYRE ")} ${chalk3.bold(data.header.hostname)} ${chalk3.dim(data.header.os)} ${chalk3.dim("up " + data.header.uptime)}`;
846
+ const right = `${chalk3.dim(now)} ${badges.join(" ")}`;
586
847
  const gap = Math.max(1, width - visLen(left) - visLen(right));
587
848
  const line = left + " ".repeat(gap) + right;
588
849
  return [line, borderAccent("\u2500".repeat(width))];
589
850
  }
851
+ var TAB_DEFS = [
852
+ { id: "cpu", label: "CPU", key: "1" },
853
+ { id: "mem", label: "Memory", key: "2" },
854
+ { id: "gpu", label: "GPU", key: "3" },
855
+ { id: "power", label: "Power", key: "4" },
856
+ { id: "battery", label: "Battery", key: "5" },
857
+ { id: "thermal", label: "Thermal", key: "6" },
858
+ { id: "network", label: "Network", key: "7" },
859
+ { id: "packets", label: "Packets", key: "8" },
860
+ { id: "tasks", label: "Tasks", key: "9" },
861
+ { id: "disk", label: "Disk", key: "0" },
862
+ { id: "process", label: "Process", key: "p" }
863
+ ];
864
+ var TAB_BAR_INDENT = 2;
865
+ var TAB_SEPARATOR_WIDTH = 3;
866
+ function getTabHitboxes() {
867
+ const boxes = [];
868
+ let col = TAB_BAR_INDENT;
869
+ TAB_DEFS.forEach((t, i) => {
870
+ const text = `[${t.key}]${t.label}`;
871
+ const start = col;
872
+ const end = col + text.length;
873
+ boxes.push({ id: t.id, start, end });
874
+ col = end;
875
+ if (i < TAB_DEFS.length - 1) col += TAB_SEPARATOR_WIDTH;
876
+ });
877
+ return boxes;
878
+ }
879
+ var TAB_BAR_ROW = 4;
880
+ function tabBar(activePanel, width, theme) {
881
+ const rendered = TAB_DEFS.map((t) => {
882
+ const isActive = activePanel === t.id;
883
+ const label = isActive ? chalk3.bold(t.label) : chalk3.dim(t.label);
884
+ const keyHint = chalk3.dim(`[${t.key}]`);
885
+ return `${keyHint}${label}`;
886
+ });
887
+ const joined = rendered.join(chalk3.dim(" \u2502 "));
888
+ const padded = visLen(joined) < width ? joined + " ".repeat(width - visLen(joined)) : joined;
889
+ return chalk3.dim(" ") + padded;
890
+ }
590
891
  function sortProcesses(processes, sortBy) {
591
892
  const copy = [...processes];
592
- if (sortBy === "pid") return copy.sort((a, b) => a.pid - b.pid);
593
- if (sortBy === "mem") return copy.sort((a, b) => b.mem - a.mem);
594
- return copy.sort((a, b) => b.cpu - a.cpu);
893
+ switch (sortBy) {
894
+ case "pid":
895
+ return copy.sort((a, b) => a.pid - b.pid);
896
+ case "mem":
897
+ return copy.sort((a, b) => b.mem - a.mem);
898
+ case "user":
899
+ return copy.sort((a, b) => a.user.localeCompare(b.user));
900
+ case "command":
901
+ return copy.sort((a, b) => a.command.localeCompare(b.command));
902
+ case "state":
903
+ return copy.sort((a, b) => a.state.localeCompare(b.state));
904
+ case "threads":
905
+ return copy.sort((a, b) => b.threads - a.threads);
906
+ case "runtime":
907
+ return copy.sort((a, b) => b.runtime - a.runtime);
908
+ default:
909
+ return copy.sort((a, b) => b.cpu - a.cpu);
910
+ }
595
911
  }
596
912
  function pctColor(v) {
597
- return v > 80 ? chalk2.red : v > 50 ? chalk2.yellow : chalk2.dim;
913
+ return v > 80 ? chalk3.red : v > 50 ? chalk3.yellow : chalk3.dim;
598
914
  }
599
915
  function processTableLines(processes, contentWidth, borderAccent = THEMES.default.border) {
600
- const pidW = 7, userW = 10, cpuW = 7, memW = 7;
601
- const cmdW = Math.max(10, contentWidth - pidW - userW - cpuW - memW - 4);
602
- const head = chalk2.bold.dim("PID".padEnd(pidW)) + chalk2.bold.dim("USER".padEnd(userW)) + chalk2.bold.dim("CPU%".padEnd(cpuW)) + chalk2.bold.dim("MEM%".padEnd(memW)) + chalk2.bold.dim("COMMAND");
916
+ const pidW = 8, ppidW = 8, userW = 12, cpuW = 8, memW = 8, stateW = 8, thrW = 6, rtW = 10;
917
+ const cmdW = Math.max(10, contentWidth - pidW - ppidW - userW - cpuW - memW - stateW - thrW - rtW - 8);
918
+ const head = chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" PID ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" PPID ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" USER ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" CPU% ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" MEM% ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" STATE ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" THREADS") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" RUNTIME ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" COMMAND".padEnd(cmdW + 1));
603
919
  const rows = processes.map((p) => {
604
920
  const cpuStr = pctColor(p.cpu)(`${p.cpu.toFixed(1)}`.padEnd(cpuW));
605
921
  const memStr = pctColor(p.mem)(`${p.mem.toFixed(1)}`.padEnd(memW));
606
- return String(p.pid).padEnd(pidW) + truncatePlain(p.user, userW - 1).padEnd(userW) + cpuStr + memStr + truncatePlain(p.command, cmdW);
922
+ const stateStr = chalk3.dim(p.state.padEnd(stateW));
923
+ const thrStr = String(p.threads).padEnd(thrW);
924
+ const rtStr = formatRuntime(p.runtime).padEnd(rtW);
925
+ return String(p.pid).padEnd(pidW) + String(p.ppid).padEnd(ppidW) + truncatePlain(p.user, userW - 1).padEnd(userW) + cpuStr + memStr + stateStr + thrStr + rtStr + truncatePlain(p.command, cmdW);
607
926
  });
608
927
  return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
609
928
  }
929
+ function formatRuntime(seconds) {
930
+ if (seconds < 60) return `${seconds}s`;
931
+ const mins = Math.floor(seconds / 60);
932
+ if (mins < 60) return `${mins}m ${seconds % 60}s`;
933
+ const hrs = Math.floor(mins / 60);
934
+ return `${hrs}h ${mins % 60}m`;
935
+ }
936
+ function buildProcessTree(processes) {
937
+ const map = /* @__PURE__ */ new Map();
938
+ const roots = [];
939
+ for (const p of processes) map.set(p.pid, p);
940
+ for (const p of processes) {
941
+ if (p.ppid === 0 || !map.has(p.ppid)) roots.push(p);
942
+ }
943
+ const lines = [];
944
+ const visited = /* @__PURE__ */ new Set();
945
+ function renderNode(proc, prefix, isLast) {
946
+ if (visited.has(proc.pid)) return;
947
+ visited.add(proc.pid);
948
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
949
+ const pidStr = String(proc.pid).padEnd(8);
950
+ const cpuStr = pctColor(proc.cpu)(`${proc.cpu.toFixed(1)}%`.padEnd(8));
951
+ const memStr = pctColor(proc.mem)(`${proc.mem.toFixed(1)}%`.padEnd(8));
952
+ const stateStr = chalk3.dim(proc.state.padEnd(8));
953
+ const thrStr = String(proc.threads).padEnd(6);
954
+ const cmd = truncatePlain(proc.command, Math.max(10, 100 - pidStr.length - cpuStr.length - memStr.length - stateStr.length - thrStr.length - 10));
955
+ lines.push(`${prefix}${connector}${pidStr}${cpuStr}${memStr}${stateStr}${thrStr} ${cmd}`);
956
+ const children = processes.filter((p) => p.ppid === proc.pid && !visited.has(p.pid));
957
+ children.sort((a, b) => b.cpu - a.cpu);
958
+ children.forEach((child, i) => {
959
+ const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
960
+ renderNode(child, nextPrefix, i === children.length - 1);
961
+ });
962
+ }
963
+ roots.sort((a, b) => b.cpu - a.cpu);
964
+ roots.forEach((root, i) => renderNode(root, "", i === roots.length - 1));
965
+ return lines;
966
+ }
610
967
  function diskTableLines(disks, contentWidth, borderAccent = THEMES.default.border) {
611
- const fsW = 16, sizeW = 8, usedW = 8, availW = 8, capW = 7;
968
+ const fsW = 20, sizeW = 10, usedW = 10, availW = 10, capW = 8;
612
969
  const mountW = Math.max(8, contentWidth - fsW - sizeW - usedW - availW - capW - 5);
613
- const head = chalk2.bold.dim("FILESYSTEM".padEnd(fsW)) + chalk2.bold.dim("SIZE".padEnd(sizeW)) + chalk2.bold.dim("USED".padEnd(usedW)) + chalk2.bold.dim("AVAIL".padEnd(availW)) + chalk2.bold.dim("CAP".padEnd(capW)) + chalk2.bold.dim("MOUNT");
614
- const rows = disks.slice(0, 8).map((d) => {
970
+ const head = chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" FILESYSTEM ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" SIZE ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" USED ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" AVAIL ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" CAP ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" MOUNT".padEnd(mountW + 1));
971
+ const rows = disks.map((d) => {
615
972
  const capNum = parseInt(d.capacity, 10) || 0;
616
973
  const capStr = pctColor(capNum)(d.capacity.padEnd(capW));
617
974
  return truncatePlain(d.filesystem, fsW - 1).padEnd(fsW) + d.size.padEnd(sizeW) + d.used.padEnd(usedW) + d.available.padEnd(availW) + capStr + truncatePlain(d.mountpoint, mountW);
618
975
  });
619
976
  return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
620
977
  }
978
+ function detailPanelTitle(panel2) {
979
+ const map = {
980
+ cpu: "CPU",
981
+ mem: "Memory",
982
+ gpu: "GPU",
983
+ power: "Power",
984
+ battery: "Battery",
985
+ thermal: "Thermal",
986
+ network: "Network",
987
+ packets: "Packets",
988
+ tasks: "Tasks",
989
+ disk: "Disk",
990
+ process: "Processes"
991
+ };
992
+ return map[panel2] || panel2;
993
+ }
994
+ function activeDetailLines(data, activePanel, width) {
995
+ const contentWidth = width - 4;
996
+ switch (activePanel) {
997
+ case "cpu":
998
+ return cpuCard(data, contentWidth);
999
+ case "mem":
1000
+ return memCard(data, contentWidth);
1001
+ case "gpu": {
1002
+ const gpu = gpuCard(data, contentWidth);
1003
+ return gpu ?? ["No GPU data available"];
1004
+ }
1005
+ case "power": {
1006
+ const power = powerCard(data);
1007
+ if (!power && !data.battery) return ["No power data available"];
1008
+ const lines = [];
1009
+ if (power) lines.push(...power);
1010
+ if (data.battery?.powerWatts !== void 0 && data.battery.powerWatts > 0) {
1011
+ lines.push(statRow("Battery", `${data.battery.powerWatts.toFixed(2)} W`));
1012
+ }
1013
+ return lines.length ? lines : ["No power data available"];
1014
+ }
1015
+ case "battery":
1016
+ return data.battery ? batteryCard(data, contentWidth) : ["No battery data available"];
1017
+ case "thermal":
1018
+ return thermalCard(data, contentWidth);
1019
+ case "network":
1020
+ return networkCard(data, contentWidth);
1021
+ case "packets": {
1022
+ if (!data.packets) return ["No packet data available"];
1023
+ const lines = [];
1024
+ lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
1025
+ lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
1026
+ lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
1027
+ lines.push(statRow("Conns", `${data.packets.connections} TCP`));
1028
+ if (data.packets.interfaces && data.packets.interfaces.length) {
1029
+ lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
1030
+ for (const iface of data.packets.interfaces.slice(0, 8)) {
1031
+ lines.push(statRow(` ${iface.iface}`, `RX ${formatBytes(iface.rxBytes)} / TX ${formatBytes(iface.txBytes)}`));
1032
+ }
1033
+ }
1034
+ if (data.packets.allProcesses && data.packets.allProcesses.length) {
1035
+ lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} total`));
1036
+ for (const proc of data.packets.allProcesses.slice(0, 10)) {
1037
+ lines.push(statRow(` ${proc.pid}`, `${truncatePlain(proc.command, 20)} TX ${formatBytes(proc.txBytes)}`));
1038
+ }
1039
+ }
1040
+ return lines;
1041
+ }
1042
+ case "tasks":
1043
+ return data.tasks?.length ? tasksCard(data, contentWidth) : ["No task data available"];
1044
+ case "disk":
1045
+ return data.disk.length ? diskTableLines(data.disk, width - 4, THEMES.default.border) : ["No disk data available"];
1046
+ case "process": {
1047
+ const sorted = sortProcesses([...data.processes], "cpu");
1048
+ return processTableLines(sorted.slice(0, 50), width - 4, THEMES.default.border);
1049
+ }
1050
+ default:
1051
+ return null;
1052
+ }
1053
+ }
621
1054
  function formatTable(data, opts2 = {}) {
622
1055
  const width = clampWidth(opts2.width);
1056
+ const activePanel = opts2.activePanel || "grid";
623
1057
  const columns = gridColumns(width);
624
1058
  const gap = 2;
625
1059
  const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
@@ -630,6 +1064,15 @@ function formatTable(data, opts2 = {}) {
630
1064
  const out = [];
631
1065
  out.push(...header(data, width, [], theme.border));
632
1066
  out.push("");
1067
+ out.push(tabBar(activePanel, width, theme));
1068
+ out.push("");
1069
+ if (activePanel !== "grid") {
1070
+ const lines = activeDetailLines(data, activePanel, width);
1071
+ if (lines) {
1072
+ out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel] ?? theme.cpu, theme.border));
1073
+ }
1074
+ return out.join("\n");
1075
+ }
633
1076
  const cards = [];
634
1077
  if (vis.cpu !== false) {
635
1078
  cards.push({ title: "CPU", accent: theme.cpu, lines: cpuCard(data, contentWidth) });
@@ -637,6 +1080,10 @@ function formatTable(data, opts2 = {}) {
637
1080
  if (vis.mem !== false) {
638
1081
  cards.push({ title: "Memory", accent: theme.mem, lines: memCard(data, contentWidth) });
639
1082
  }
1083
+ if (vis.gpu !== false) {
1084
+ const gpu = gpuCard(data, contentWidth);
1085
+ if (gpu) cards.push({ title: "GPU", accent: theme.gpu, lines: gpu });
1086
+ }
640
1087
  if (vis.power !== false) {
641
1088
  const power = powerCard(data);
642
1089
  if (power) cards.push({ title: "Power", accent: theme.power, lines: power });
@@ -651,6 +1098,14 @@ function formatTable(data, opts2 = {}) {
651
1098
  if (vis.network !== false) {
652
1099
  cards.push({ title: "Network", accent: theme.network, lines: networkCard(data, contentWidth) });
653
1100
  }
1101
+ if (vis.packets !== false) {
1102
+ const packets = packetCard(data, contentWidth);
1103
+ if (packets) cards.push({ title: "Packets", accent: theme.network, lines: packets });
1104
+ }
1105
+ if (vis.tasks !== false) {
1106
+ const tasks = tasksCard(data, contentWidth);
1107
+ if (tasks) cards.push({ title: "Tasks", accent: theme.process, lines: tasks });
1108
+ }
654
1109
  for (let i = 0; i < cards.length; i += columns) {
655
1110
  const row = cards.slice(i, i + columns);
656
1111
  const rowHeight = Math.max(...row.map((c) => c.lines.length));
@@ -668,14 +1123,49 @@ function formatTable(data, opts2 = {}) {
668
1123
  const sortedProcesses = sortProcesses(filteredProcesses, opts2.sortBy ?? "cpu");
669
1124
  const limited = opts2.processLimit ? sortedProcesses.slice(0, opts2.processLimit) : sortedProcesses;
670
1125
  const procTitle = opts2.filter ? `Processes \xB7 filter "${opts2.filter}" \xB7 sort ${opts2.sortBy ?? "cpu"}` : `Processes \xB7 sort ${opts2.sortBy ?? "cpu"}`;
671
- if (limited.length) {
1126
+ if (opts2.treeView) {
1127
+ const treeLines = buildProcessTree(limited);
1128
+ if (treeLines.length) {
1129
+ out.push(...panel(procTitle + " \xB7 tree", treeLines, width, theme.process, theme.border));
1130
+ } else if (opts2.filter) {
1131
+ out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
1132
+ }
1133
+ } else if (limited.length) {
672
1134
  out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
673
1135
  } else if (opts2.filter) {
674
- out.push(...panel(procTitle, [chalk2.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
1136
+ out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
675
1137
  }
676
1138
  }
677
1139
  return out.join("\n");
678
1140
  }
1141
+ function clampWidth(width) {
1142
+ if (!width || Number.isNaN(width)) return 80;
1143
+ return Math.max(60, Math.min(width, 240));
1144
+ }
1145
+ function gridColumns(width) {
1146
+ if (width >= 150) return 3;
1147
+ if (width >= 96) return 2;
1148
+ return 1;
1149
+ }
1150
+
1151
+ // src/formatters/output.ts
1152
+ import chalk4 from "chalk";
1153
+
1154
+ // src/sparkline.ts
1155
+ var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1156
+ function sparkline(values, opts2 = {}) {
1157
+ if (!values.length) return "";
1158
+ const min = opts2.min ?? Math.min(...values);
1159
+ const max = opts2.max ?? Math.max(...values);
1160
+ const range = max - min || 1;
1161
+ return values.map((v) => {
1162
+ const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
1163
+ const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
1164
+ return BLOCKS[clamped];
1165
+ }).join("");
1166
+ }
1167
+
1168
+ // src/formatters/output.ts
679
1169
  function formatJson(data) {
680
1170
  return JSON.stringify(data, null, 2);
681
1171
  }
@@ -697,15 +1187,21 @@ function formatCsv(data) {
697
1187
  [
698
1188
  "thermal",
699
1189
  "cpuTempF",
700
- data.thermal.temperatures?.cpu_die !== void 0 ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""
1190
+ data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""
701
1191
  ],
702
1192
  ["network", "rxBytes", String(data.network.rxBytes)],
703
1193
  ["network", "txBytes", String(data.network.txBytes)],
1194
+ ["network", "rxPackets", String(data.network.rxPackets)],
1195
+ ["network", "txPackets", String(data.network.txPackets)],
1196
+ ["network", "connections", data.network.connections ? String(data.network.connections) : ""],
704
1197
  ["battery", "level", data.battery ? String(data.battery.level) : ""],
705
1198
  ["battery", "powerSource", data.battery ? data.battery.powerSource : ""],
706
1199
  ["battery", "condition", data.battery?.condition ?? ""],
707
1200
  ["battery", "maxCapacityPercent", data.battery?.maxCapacityPercent !== void 0 ? String(data.battery.maxCapacityPercent) : ""],
708
1201
  ["battery", "cycles", data.battery?.cycles !== void 0 ? String(data.battery.cycles) : ""],
1202
+ ["battery", "estimatedTimeToEmpty", data.battery?.estimatedTimeToEmpty ?? ""],
1203
+ ["battery", "dischargeRatePerHour", data.battery?.dischargeRatePerHour !== void 0 ? String(data.battery.dischargeRatePerHour) : ""],
1204
+ ["battery", "powerWatts", data.battery?.powerWatts !== void 0 ? String(data.battery.powerWatts) : ""],
709
1205
  ["power", "cpuWatts", data.power?.cpuWatts !== void 0 ? String(data.power.cpuWatts) : ""],
710
1206
  ["power", "gpuWatts", data.power?.gpuWatts !== void 0 ? String(data.power.gpuWatts) : ""],
711
1207
  ["power", "combinedWatts", data.power?.combinedWatts !== void 0 ? String(data.power.combinedWatts) : ""],
@@ -713,94 +1209,199 @@ function formatCsv(data) {
713
1209
  ["system", "uptime", data.header.uptime],
714
1210
  ["system", "timestamp", data.timestamp]
715
1211
  ];
1212
+ if (data.packets) {
1213
+ rows.push(["packets", "totalPackets", String(data.packets.totalPackets)]);
1214
+ rows.push(["packets", "rxPackets", String(data.packets.rxPackets)]);
1215
+ rows.push(["packets", "txPackets", String(data.packets.txPackets)]);
1216
+ rows.push(["packets", "connections", String(data.packets.connections)]);
1217
+ for (const iface of data.packets.interfaces || []) {
1218
+ rows.push(["packets_iface", iface.iface, `${iface.rxPackets},${iface.txPackets},${iface.rxBytes},${iface.txBytes}`]);
1219
+ }
1220
+ for (const proc of data.packets.allProcesses || []) {
1221
+ rows.push(["packets_proc", `pid=${proc.pid}`, `${proc.command},${proc.rxBytes},${proc.txBytes}`]);
1222
+ }
1223
+ }
1224
+ if (data.gpu) {
1225
+ rows.push(["gpu", "model", data.gpu.model]);
1226
+ rows.push(["gpu", "memory", String(data.gpu.memory)]);
1227
+ rows.push(["gpu", "utilization", String(data.gpu.utilization)]);
1228
+ if (data.gpu.temperature) rows.push(["gpu", "tempC", String(data.gpu.temperature)]);
1229
+ rows.push(["gpu", "processes", String(data.gpu.processes)]);
1230
+ }
1231
+ for (const p of data.processes) {
1232
+ rows.push(["process", `pid=${p.pid}`, `${p.pid}`]);
1233
+ rows.push(["process", `ppid=${p.pid}`, String(p.ppid)]);
1234
+ rows.push(["process", `user=${p.pid}`, p.user]);
1235
+ rows.push(["process", `cpu=${p.pid}`, String(p.cpu)]);
1236
+ rows.push(["process", `mem=${p.pid}`, String(p.mem)]);
1237
+ rows.push(["process", `state=${p.pid}`, p.state]);
1238
+ rows.push(["process", `threads=${p.pid}`, String(p.threads)]);
1239
+ rows.push(["process", `runtime=${p.pid}`, String(p.runtime)]);
1240
+ rows.push(["process", `command=${p.pid}`, p.command]);
1241
+ }
716
1242
  return rows.map((r) => r.map((item) => `"${String(item).replace(/"/g, '""')}"`).join(",")).join("\n");
717
1243
  }
718
1244
  function formatTsv(data) {
719
1245
  return formatCsv(data).split("\n").map((r) => r.split(",").join(" ")).join("\n");
720
1246
  }
721
- function formatGraphs(history2, width = 80, themeName = "default") {
1247
+ function formatGraphs(history, width = 80, themeName = "default", graphMode = "spark") {
722
1248
  const theme = THEMES[themeName] || THEMES.default;
723
1249
  const contentWidth = width - 4;
724
- const sampleCount = history2.cpuUsage.length;
1250
+ const sampleCount = history.cpuUsage.length;
725
1251
  const sparkWidth = Math.max(10, contentWidth - 28);
726
1252
  const lines = [];
727
- lines.push(graphRow("CPU %", history2.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth));
728
- lines.push(graphRow("Mem %", history2.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth));
1253
+ lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, graphMode));
1254
+ lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, graphMode));
729
1255
  lines.push(
730
- graphRow("Temp", history2.temp, {}, formatTemp, sparkWidth, "no sensor access (needs sudo powermetrics)")
1256
+ graphRow("Temp", history.temp, {}, formatTemp, sparkWidth, "no sensor access (needs sudo powermetrics)", graphMode)
731
1257
  );
732
- lines.push(graphRow("Net RX/s", history2.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth));
733
- lines.push(graphRow("Net TX/s", history2.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth));
734
- const title = `Graphs \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
1258
+ lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
1259
+ lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
1260
+ if (history.powerWatts.length) {
1261
+ lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, graphMode));
1262
+ }
1263
+ if (history.rxPacketRate.length) {
1264
+ lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
1265
+ }
1266
+ if (history.txPacketRate.length) {
1267
+ lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
1268
+ }
1269
+ const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
735
1270
  return panel(title, lines, width, theme.graphs, theme.border).join("\n");
736
1271
  }
737
- function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...") {
1272
+ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark") {
738
1273
  if (!values.length) {
739
- return `${chalk2.dim(label.padEnd(10))} ${chalk2.dim(emptyMessage)}`;
1274
+ return `${chalk4.dim(label.padEnd(10))} ${chalk4.dim(emptyMessage)}`;
740
1275
  }
741
1276
  const visible = values.slice(-sparkWidth);
742
- const spark = sparkline(visible, bounds);
1277
+ const spark = mode === "spark" ? sparkline(visible, bounds) : barGraph(visible, bounds, sparkWidth);
743
1278
  const current = values[values.length - 1];
744
- let color = chalk2.cyan;
1279
+ let color = chalk4.cyan;
745
1280
  if (bounds.min !== void 0 && bounds.max !== void 0) {
746
1281
  const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
747
- color = pct > 90 ? chalk2.red : pct > 70 ? chalk2.yellow : chalk2.green;
1282
+ color = pct > 90 ? chalk4.red : pct > 70 ? chalk4.yellow : chalk4.green;
748
1283
  }
749
- return `${chalk2.dim(label.padEnd(10))}${color(spark)} ${chalk2.bold(fmt(current))}`;
1284
+ return `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
1285
+ }
1286
+ function barGraph(values, bounds, width) {
1287
+ const min = bounds.min ?? Math.min(...values);
1288
+ const max = bounds.max ?? Math.max(...values);
1289
+ const range = max - min || 1;
1290
+ return values.map((v) => {
1291
+ const pct = (v - min) / range;
1292
+ if (pct > 0.8) return "\u2588";
1293
+ if (pct > 0.6) return "\u2587";
1294
+ if (pct > 0.4) return "\u2586";
1295
+ if (pct > 0.2) return "\u2584";
1296
+ return " ";
1297
+ }).join("");
750
1298
  }
751
1299
 
752
- // src/live.ts
753
- import fs from "fs";
754
- import path from "path";
1300
+ // src/live/input.ts
755
1301
  import readline from "readline";
756
- import chalk3 from "chalk";
757
1302
 
758
1303
  // src/history.ts
759
1304
  var History = class {
1305
+ /** Maximum number of samples retained in the rolling window. */
760
1306
  maxLen;
1307
+ /** Rolling window of CPU usage percentages (0–100). */
761
1308
  cpuUsage = [];
1309
+ /** Rolling window of memory usage percentages (0–100). */
762
1310
  memUsage = [];
1311
+ /** Rolling window of CPU die temperatures in °C. */
763
1312
  temp = [];
1313
+ /** Rolling window of network receive rates in bytes/sec. */
764
1314
  netRxRate = [];
1315
+ /** Rolling window of network transmit rates in bytes/sec. */
765
1316
  netTxRate = [];
1317
+ /** Rolling window of GPU utilization percentages (0–100). */
1318
+ gpuUtil = [];
1319
+ /** Rolling window of combined power draw in watts. */
1320
+ powerWatts = [];
1321
+ /** Rolling window of network receive packet rates in packets/sec. */
1322
+ rxPacketRate = [];
1323
+ /** Rolling window of network transmit packet rates in packets/sec. */
1324
+ txPacketRate = [];
1325
+ /** Rolling window of active TCP connections. */
1326
+ connections = [];
766
1327
  lastRxBytes = 0;
767
1328
  lastTxBytes = 0;
1329
+ lastRxPackets = 0;
1330
+ lastTxPackets = 0;
768
1331
  lastTs = 0;
1332
+ /**
1333
+ * Create a new History instance.
1334
+ * @param maxLen - Maximum number of samples to retain. Defaults to 40.
1335
+ */
769
1336
  constructor(maxLen = 40) {
770
1337
  this.maxLen = maxLen;
771
1338
  }
1339
+ /**
1340
+ * Push a new sample into the rolling window.
1341
+ *
1342
+ * Network rates are computed as bytes/sec from the cumulative
1343
+ * counters. The first sample always reports 0 for rates
1344
+ * because there is no prior sample to compute a delta from.
1345
+ */
772
1346
  push(sample) {
773
1347
  const now = Date.now();
774
1348
  let rxRate = 0;
775
1349
  let txRate = 0;
1350
+ let rxPktRate = 0;
1351
+ let txPktRate = 0;
776
1352
  if (this.lastTs > 0) {
777
1353
  const dtSeconds = Math.max((now - this.lastTs) / 1e3, 1e-3);
778
1354
  rxRate = Math.max(0, (sample.rxBytes - this.lastRxBytes) / dtSeconds);
779
1355
  txRate = Math.max(0, (sample.txBytes - this.lastTxBytes) / dtSeconds);
1356
+ if (sample.rxPackets !== void 0) {
1357
+ rxPktRate = Math.max(0, (sample.rxPackets - this.lastRxPackets) / dtSeconds);
1358
+ }
1359
+ if (sample.txPackets !== void 0) {
1360
+ txPktRate = Math.max(0, (sample.txPackets - this.lastTxPackets) / dtSeconds);
1361
+ }
780
1362
  }
781
1363
  this.lastRxBytes = sample.rxBytes;
782
1364
  this.lastTxBytes = sample.txBytes;
1365
+ if (sample.rxPackets !== void 0) this.lastRxPackets = sample.rxPackets;
1366
+ if (sample.txPackets !== void 0) this.lastTxPackets = sample.txPackets;
783
1367
  this.lastTs = now;
784
1368
  this.pushBounded(this.cpuUsage, sample.cpuUsage);
785
1369
  this.pushBounded(this.memUsage, sample.memUsage);
786
1370
  if (sample.temp !== null) this.pushBounded(this.temp, sample.temp);
787
1371
  this.pushBounded(this.netRxRate, rxRate);
788
1372
  this.pushBounded(this.netTxRate, txRate);
1373
+ if (sample.gpuUtil !== void 0) this.pushBounded(this.gpuUtil, sample.gpuUtil);
1374
+ if (sample.powerWatts !== void 0) this.pushBounded(this.powerWatts, sample.powerWatts);
1375
+ if (sample.rxPackets !== void 0) this.pushBounded(this.rxPacketRate, rxPktRate);
1376
+ if (sample.txPackets !== void 0) this.pushBounded(this.txPacketRate, txPktRate);
1377
+ if (sample.connections !== void 0) this.pushBounded(this.connections, sample.connections);
789
1378
  }
1379
+ /** Clear all rolling windows and reset cumulative byte counters. */
790
1380
  reset() {
791
1381
  this.cpuUsage = [];
792
1382
  this.memUsage = [];
793
1383
  this.temp = [];
794
1384
  this.netRxRate = [];
795
1385
  this.netTxRate = [];
1386
+ this.gpuUtil = [];
1387
+ this.powerWatts = [];
1388
+ this.rxPacketRate = [];
1389
+ this.txPacketRate = [];
1390
+ this.connections = [];
796
1391
  this.lastRxBytes = 0;
797
1392
  this.lastTxBytes = 0;
1393
+ this.lastRxPackets = 0;
1394
+ this.lastTxPackets = 0;
798
1395
  this.lastTs = 0;
799
1396
  }
800
- /** Grow or shrink the rolling window (e.g. on terminal resize), trimming from the oldest samples. */
1397
+ /**
1398
+ * Grow or shrink the rolling window (e.g. on terminal resize),
1399
+ * trimming from the oldest samples.
1400
+ * @param n - New maximum length; clamped to at least 1.
1401
+ */
801
1402
  setMaxLen(n) {
802
1403
  this.maxLen = Math.max(1, n);
803
- for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate]) {
1404
+ for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate, this.gpuUtil, this.powerWatts, this.rxPacketRate, this.txPacketRate, this.connections]) {
804
1405
  while (arr.length > this.maxLen) arr.shift();
805
1406
  }
806
1407
  }
@@ -810,62 +1411,94 @@ var History = class {
810
1411
  }
811
1412
  };
812
1413
 
813
- // src/live.ts
814
- var intervalHandle = null;
815
- var running = false;
816
- var paused = false;
817
- var detailed = false;
818
- var interval = 2;
819
- var showGraphs = true;
820
- var exportFormat = "json";
821
- var exportDir = "./pyre-exports";
822
- var currentTheme = "default";
823
- var visiblePanels = {
824
- cpu: true,
825
- mem: true,
826
- power: true,
827
- battery: true,
828
- thermal: true,
829
- network: true,
830
- disk: true,
831
- process: true
1414
+ // src/live/state.ts
1415
+ var SIGNAL_OPTIONS = ["SIGTERM", "SIGKILL", "SIGINT", "SIGHUP", "SIGSTOP", "SIGCONT"];
1416
+ var state = {
1417
+ intervalHandle: null,
1418
+ running: false,
1419
+ paused: false,
1420
+ detailed: false,
1421
+ interval: 2,
1422
+ showGraphs: true,
1423
+ graphMode: "spark",
1424
+ exportFormat: "json",
1425
+ exportDir: "./pyre-exports",
1426
+ currentTheme: "default",
1427
+ visiblePanels: {
1428
+ cpu: true,
1429
+ mem: true,
1430
+ gpu: true,
1431
+ power: true,
1432
+ battery: true,
1433
+ thermal: true,
1434
+ network: true,
1435
+ packets: true,
1436
+ tasks: true,
1437
+ disk: true,
1438
+ process: true
1439
+ },
1440
+ logging: false,
1441
+ logStream: null,
1442
+ statusMessage: "",
1443
+ statusTimer: null,
1444
+ lastData: null,
1445
+ history: new History(40),
1446
+ keypressHandler: null,
1447
+ termWidth: process.stdout.columns || 80,
1448
+ termHeight: process.stdout.rows || 24,
1449
+ sortMode: "cpu",
1450
+ processFilter: "",
1451
+ inputMode: null,
1452
+ inputBuffer: "",
1453
+ treeView: false,
1454
+ SIGNAL_OPTIONS,
1455
+ customizerIndex: 0,
1456
+ CUSTOMIZER_OPTIONS: [
1457
+ "Theme",
1458
+ "Graph Mode",
1459
+ "Toggle CPU",
1460
+ "Toggle Memory",
1461
+ "Toggle GPU",
1462
+ "Toggle Power",
1463
+ "Toggle Battery",
1464
+ "Toggle Thermal",
1465
+ "Toggle Network",
1466
+ "Toggle Packets",
1467
+ "Toggle Tasks",
1468
+ "Toggle Disk",
1469
+ "Toggle Processes",
1470
+ "Toggle Tree View"
1471
+ ],
1472
+ CPU_ALERT_PCT: 90,
1473
+ TEMP_ALERT_C: 95,
1474
+ alerted: false,
1475
+ activePanel: "grid",
1476
+ mouseEnabled: true,
1477
+ PANEL_TABS: [
1478
+ { id: "cpu", label: "CPU", key: "1" },
1479
+ { id: "mem", label: "Memory", key: "2" },
1480
+ { id: "gpu", label: "GPU", key: "3" },
1481
+ { id: "power", label: "Power", key: "4" },
1482
+ { id: "battery", label: "Battery", key: "5" },
1483
+ { id: "thermal", label: "Thermal", key: "6" },
1484
+ { id: "network", label: "Network", key: "7" },
1485
+ { id: "packets", label: "Packets", key: "8" },
1486
+ { id: "tasks", label: "Tasks", key: "9" },
1487
+ { id: "disk", label: "Disk", key: "0" },
1488
+ { id: "process", label: "Process", key: "P" }
1489
+ ]
832
1490
  };
833
- var logging = false;
834
- var logStream = null;
835
- var statusMessage = "";
836
- var statusTimer = null;
837
- var lastData = null;
838
- var history = new History(40);
839
- var keypressHandler = null;
840
- var termWidth = process.stdout.columns || 80;
841
- var termHeight = process.stdout.rows || 24;
842
- var sortMode = "cpu";
843
- var processFilter = "";
844
- var inputMode = null;
845
- var inputBuffer = "";
846
- var customizerIndex = 0;
847
- var CUSTOMIZER_OPTIONS = [
848
- "Theme",
849
- "Toggle CPU",
850
- "Toggle Memory",
851
- "Toggle Power",
852
- "Toggle Battery",
853
- "Toggle Thermal",
854
- "Toggle Network",
855
- "Toggle Disk",
856
- "Toggle Processes"
857
- ];
858
- var CPU_ALERT_PCT = 90;
859
- var TEMP_ALERT_C = 95;
860
- var alerted = false;
861
1491
  function setStatus(msg, ms = 3e3) {
862
- statusMessage = msg;
863
- if (statusTimer) clearTimeout(statusTimer);
864
- statusTimer = setTimeout(() => {
865
- statusMessage = "";
866
- render();
1492
+ state.statusMessage = msg;
1493
+ if (state.statusTimer) clearTimeout(state.statusTimer);
1494
+ state.statusTimer = setTimeout(() => {
1495
+ state.statusMessage = "";
867
1496
  }, ms);
868
1497
  }
1498
+
1499
+ // src/live/export.ts
1500
+ import fs from "fs";
1501
+ import path from "path";
869
1502
  function ensureDir(dir) {
870
1503
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
871
1504
  }
@@ -873,14 +1506,14 @@ function timestampForFile() {
873
1506
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
874
1507
  }
875
1508
  function exportSnapshot() {
876
- if (!lastData) {
1509
+ if (!state.lastData) {
877
1510
  setStatus("No data yet \u2014 try again in a moment");
878
1511
  return;
879
1512
  }
880
1513
  try {
881
- ensureDir(exportDir);
882
- const file = path.join(exportDir, `pyre-${timestampForFile()}.${exportFormat}`);
883
- const content = exportFormat === "json" ? formatJson(lastData) : exportFormat === "csv" ? formatCsv(lastData) : formatTsv(lastData);
1514
+ ensureDir(state.exportDir);
1515
+ const file = path.join(state.exportDir, `pyre-${timestampForFile()}.${state.exportFormat}`);
1516
+ const content = state.exportFormat === "json" ? formatJson(state.lastData) : state.exportFormat === "csv" ? formatCsv(state.lastData) : formatTsv(state.lastData);
884
1517
  fs.writeFileSync(file, content);
885
1518
  setStatus(`Exported snapshot -> ${file}`);
886
1519
  } catch (err) {
@@ -889,86 +1522,106 @@ function exportSnapshot() {
889
1522
  }
890
1523
  function startLogging() {
891
1524
  try {
892
- ensureDir(exportDir);
893
- const file = path.join(exportDir, `pyre-log-${timestampForFile()}.csv`);
894
- logStream = fs.createWriteStream(file, { flags: "a" });
895
- logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,thermal_state\n");
896
- logging = true;
1525
+ ensureDir(state.exportDir);
1526
+ const file = path.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
1527
+ state.logStream = fs.createWriteStream(file, { flags: "a" });
1528
+ 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");
1529
+ state.logging = true;
897
1530
  setStatus(`Logging every tick -> ${file}`);
898
1531
  } catch (err) {
899
1532
  setStatus(`Logging failed to start: ${err.message}`);
900
1533
  }
901
1534
  }
902
1535
  function stopLogging() {
903
- logging = false;
904
- logStream?.end();
905
- logStream = null;
1536
+ state.logging = false;
1537
+ state.logStream?.end();
1538
+ state.logStream = null;
906
1539
  setStatus("Logging stopped");
907
1540
  }
908
1541
  function toggleLogging() {
909
- if (logging) stopLogging();
1542
+ if (state.logging) stopLogging();
910
1543
  else startLogging();
911
1544
  }
912
1545
  function writeLogRow(data) {
913
- if (!logging || !logStream) return;
1546
+ if (!state.logging || !state.logStream) return;
914
1547
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
915
- logStream.write(
916
- `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${data.thermal.state}
1548
+ const rxPackets = data.network.rxPackets ?? 0;
1549
+ const txPackets = data.network.txPackets ?? 0;
1550
+ const connections = data.network.connections ?? 0;
1551
+ state.logStream.write(
1552
+ `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}
917
1553
  `
918
1554
  );
919
1555
  }
1556
+
1557
+ // src/live/render.ts
1558
+ import chalk5 from "chalk";
920
1559
  function processRowBudget() {
921
- const columns = gridColumns(termWidth);
922
- const cardRows = Math.ceil(6 / columns);
923
- const reserved = 2 + cardRows * 8 + 11 + (showGraphs ? 9 : 0) + 4;
924
- return Math.max(3, Math.min(40, termHeight - reserved));
1560
+ const columns = gridColumns(state.termWidth);
1561
+ const visibleCards = Object.values(state.visiblePanels).filter((v) => v !== false).length;
1562
+ const cardRows = Math.ceil(visibleCards / columns);
1563
+ const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
1564
+ return Math.max(3, Math.min(40, state.termHeight - reserved));
925
1565
  }
926
1566
  function renderCustomizerOverlay() {
927
1567
  const themesList = Object.keys(THEMES);
928
1568
  const lines = [
929
- chalk3.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ")
1569
+ chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ")
930
1570
  ];
931
- CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
932
- const isSelected = idx === customizerIndex;
933
- const prefix = isSelected ? chalk3.yellow("\u25B6 ") : " ";
1571
+ state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
1572
+ const isSelected = idx === state.customizerIndex;
1573
+ const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
934
1574
  if (opt === "Theme") {
935
- lines.push(`${prefix}${opt}: ${chalk3.bold.green(currentTheme)} [${themesList.join(", ")}]`);
1575
+ lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
1576
+ } else if (opt === "Graph Mode") {
1577
+ lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.graphMode)} [spark, bar]`);
1578
+ } else if (opt === "Toggle Tree View") {
1579
+ const status = state.treeView ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
1580
+ lines.push(`${prefix}${opt}: ${status}`);
936
1581
  } else {
937
1582
  const key = opt.replace("Toggle ", "").toLowerCase();
938
- const isVisible = visiblePanels[key] !== false;
939
- const status = isVisible ? chalk3.green("[VISIBLE]") : chalk3.red("[HIDDEN]");
1583
+ const isVisible = state.visiblePanels[key] !== false;
1584
+ const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
940
1585
  lines.push(`${prefix}${opt}: ${status}`);
941
1586
  }
942
1587
  });
943
1588
  return lines.map((l) => ` ${l}`).join("\n");
944
1589
  }
945
1590
  function render() {
946
- if (!lastData) return;
1591
+ if (!state.lastData) return;
947
1592
  const lines = [];
948
1593
  lines.push(
949
- formatTable(lastData, {
950
- width: termWidth,
951
- sortBy: sortMode,
952
- filter: processFilter || void 0,
1594
+ formatTable(state.lastData, {
1595
+ width: state.termWidth,
1596
+ sortBy: state.sortMode,
1597
+ filter: state.processFilter || void 0,
953
1598
  processLimit: processRowBudget(),
954
- theme: currentTheme,
955
- visible: visiblePanels
1599
+ theme: state.currentTheme,
1600
+ visible: state.visiblePanels,
1601
+ treeView: state.treeView,
1602
+ activePanel: state.activePanel
956
1603
  })
957
1604
  );
958
- if (showGraphs) {
1605
+ if (state.showGraphs) {
959
1606
  lines.push("");
960
- lines.push(formatGraphs(history, termWidth, currentTheme));
1607
+ lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
961
1608
  }
962
1609
  lines.push("");
963
1610
  lines.push(footerLine());
964
- if (inputMode === "customizer") {
1611
+ if (state.inputMode === "customizer") {
965
1612
  lines.push(renderCustomizerOverlay());
966
- } else if (inputMode === "filter") {
967
- lines.push(chalk3.cyan(` Filter processes: ${inputBuffer}_`));
968
- } else if (inputMode === "kill") {
969
- lines.push(chalk3.cyan(` Kill PID: ${inputBuffer}_ (enter to confirm, esc to cancel)`));
970
- } else if (statusMessage) {
971
- lines.push(` ${statusMessage}`);
1613
+ } else if (state.inputMode === "filter") {
1614
+ lines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
1615
+ } else if (state.inputMode === "kill") {
1616
+ lines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
1617
+ } else if (state.inputMode === "signal") {
1618
+ const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
1619
+ const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk5.yellow(s) : chalk5.dim(s)).join(" ");
1620
+ lines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
1621
+ lines.push(chalk5.dim(` ${sigList}`));
1622
+ lines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
1623
+ } else if (state.statusMessage) {
1624
+ lines.push(` ${state.statusMessage}`);
972
1625
  }
973
1626
  process.stdout.write("\x1B[2J\x1B[H");
974
1627
  process.stdout.write(lines.join("\n"));
@@ -977,141 +1630,178 @@ function footerLine() {
977
1630
  const keys = [
978
1631
  ["q", "quit"],
979
1632
  ["c", "customize UI"],
980
- ["p", paused ? "resume" : "pause"],
981
- ["g", showGraphs ? "hide graphs" : "show graphs"],
982
- ["d", detailed ? "basic" : "detailed"],
983
- ["s", `sort:${sortMode}`],
1633
+ ["p", state.paused ? "resume" : "pause"],
1634
+ ["g", state.showGraphs ? "hide graphs" : "show graphs"],
1635
+ ["b", `graph:${state.graphMode}`],
1636
+ ["d", state.detailed ? "basic" : "detailed"],
1637
+ ["\u2190/\u2192", "tab"],
1638
+ ["1-9", "panels"],
1639
+ ["s", `sort:${state.sortMode}`],
984
1640
  ["/", "filter"],
985
1641
  ["k", "kill"],
1642
+ ["S", "signal"],
1643
+ ["t", state.treeView ? "flat" : "tree"],
986
1644
  ["e", "export"],
987
- ["l", logging ? "stop log" : "log"],
988
- ["f", exportFormat],
989
- ["+/-", `${interval}s`]
1645
+ ["l", state.logging ? "stop log" : "log"],
1646
+ ["f", state.exportFormat],
1647
+ ["+/-", `${state.interval}s`]
990
1648
  ];
991
- const keyStr = keys.map(([k, label]) => `${chalk3.cyan.bold(k)} ${chalk3.dim(label)}`).join(chalk3.dim(" \xB7 "));
1649
+ const keyStr = keys.map(([k, label]) => `${chalk5.hex("#50fa7b").bold(k)} ${chalk5.dim(label)}`).join(chalk5.dim(" | "));
992
1650
  const badges = [];
993
- if (paused) badges.push(chalk3.yellow.bold("\u23F8 PAUSED"));
994
- if (logging) badges.push(chalk3.red.bold("\u25CF REC"));
1651
+ if (state.paused) badges.push(chalk5.yellow.bold("\u23F8 PAUSED"));
1652
+ if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
1653
+ if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
995
1654
  const badgeStr = badges.join(" ");
996
1655
  return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
997
1656
  }
998
1657
  function checkAlerts(data) {
999
1658
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1000
- const hot = data.cpu.usage >= CPU_ALERT_PCT || temp !== null && temp >= TEMP_ALERT_C;
1001
- if (hot && !alerted) {
1002
- alerted = true;
1659
+ const hot = data.cpu.usage >= state.CPU_ALERT_PCT || temp !== null && temp >= state.TEMP_ALERT_C;
1660
+ if (hot && !state.alerted) {
1661
+ state.alerted = true;
1003
1662
  process.stdout.write("\x07");
1004
- const reason = data.cpu.usage >= CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}\xB0C`;
1005
- setStatus(chalk3.red(`\u26A0 Alert: ${reason}`), 5e3);
1663
+ const reason = data.cpu.usage >= state.CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}\xB0C`;
1664
+ setStatus(chalk5.red(`\u26A0 Alert: ${reason}`), 5e3);
1006
1665
  } else if (!hot) {
1007
- alerted = false;
1008
- }
1009
- }
1010
- async function tick() {
1011
- if (paused) return;
1012
- try {
1013
- const data = await collectAll({ detailed, processLimit: processRowBudget() });
1014
- lastData = data;
1015
- const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1016
- history.push({
1017
- cpuUsage: data.cpu.usage,
1018
- memUsage: data.memory.usagePercent,
1019
- temp,
1020
- rxBytes: data.network.rxBytes,
1021
- txBytes: data.network.txBytes
1022
- });
1023
- writeLogRow(data);
1024
- checkAlerts(data);
1025
- render();
1026
- } catch {
1027
- }
1028
- }
1029
- function killProcess(pidStr) {
1030
- const pid = parseInt(pidStr, 10);
1031
- if (!pid || pid <= 0) {
1032
- setStatus(`Invalid PID: ${pidStr}`);
1033
- return;
1034
- }
1035
- try {
1036
- process.kill(pid, "SIGTERM");
1037
- setStatus(`Sent SIGTERM to PID ${pid}`);
1038
- } catch (err) {
1039
- setStatus(`Failed to kill ${pid}: ${err.message}`);
1666
+ state.alerted = false;
1040
1667
  }
1041
1668
  }
1669
+
1670
+ // src/live/input.ts
1042
1671
  function handleInputModeKey(str, key) {
1043
1672
  if (key.name === "escape") {
1044
- inputMode = null;
1045
- inputBuffer = "";
1673
+ state.inputMode = null;
1674
+ state.inputBuffer = "";
1046
1675
  render();
1047
1676
  return;
1048
1677
  }
1049
- if (inputMode === "customizer") {
1678
+ if (state.inputMode === "customizer") {
1050
1679
  const themesList = Object.keys(THEMES);
1051
1680
  if (key.name === "up" || str === "k") {
1052
- customizerIndex = (customizerIndex - 1 + CUSTOMIZER_OPTIONS.length) % CUSTOMIZER_OPTIONS.length;
1681
+ state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
1053
1682
  } else if (key.name === "down" || str === "j") {
1054
- customizerIndex = (customizerIndex + 1) % CUSTOMIZER_OPTIONS.length;
1683
+ state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
1055
1684
  } else if (key.name === "return" || str === " ") {
1056
- const selected = CUSTOMIZER_OPTIONS[customizerIndex];
1685
+ const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
1057
1686
  if (selected === "Theme") {
1058
- const nextIdx = (themesList.indexOf(currentTheme) + 1) % themesList.length;
1059
- currentTheme = themesList[nextIdx];
1687
+ const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
1688
+ state.currentTheme = themesList[nextIdx];
1689
+ } else if (selected === "Toggle Tree View") {
1690
+ state.treeView = !state.treeView;
1691
+ } else if (selected === "Graph Mode") {
1692
+ state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
1060
1693
  } else {
1061
1694
  const itemKey = selected.replace("Toggle ", "").toLowerCase();
1062
- visiblePanels[itemKey] = !visiblePanels[itemKey];
1695
+ state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
1063
1696
  }
1064
1697
  }
1065
1698
  render();
1066
1699
  return;
1067
1700
  }
1701
+ if (state.inputMode === "signal") {
1702
+ if (key.name === "up" || str === "k") {
1703
+ const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
1704
+ state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
1705
+ } else if (key.name === "down" || str === "j") {
1706
+ const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
1707
+ state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
1708
+ } else if (key.name === "return" || str === " ") {
1709
+ killProcess(state.inputBuffer.trim(), state.inputBuffer.trim());
1710
+ state.inputMode = null;
1711
+ state.inputBuffer = "";
1712
+ render();
1713
+ return;
1714
+ }
1715
+ render();
1716
+ return;
1717
+ }
1068
1718
  if (key.name === "return") {
1069
- if (inputMode === "filter") {
1070
- processFilter = inputBuffer.trim();
1071
- setStatus(processFilter ? `Filtering: "${processFilter}"` : "Filter cleared");
1072
- } else if (inputMode === "kill") {
1073
- killProcess(inputBuffer.trim());
1719
+ if (state.inputMode === "filter") {
1720
+ state.processFilter = state.inputBuffer.trim();
1721
+ setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : "Filter cleared");
1722
+ } else if (state.inputMode === "kill") {
1723
+ killProcess(state.inputBuffer.trim());
1074
1724
  }
1075
- inputMode = null;
1076
- inputBuffer = "";
1725
+ state.inputMode = null;
1726
+ state.inputBuffer = "";
1077
1727
  render();
1078
1728
  return;
1079
1729
  }
1080
1730
  if (key.name === "backspace") {
1081
- inputBuffer = inputBuffer.slice(0, -1);
1731
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
1082
1732
  render();
1083
1733
  return;
1084
1734
  }
1085
1735
  if (str && str.length === 1 && !key.ctrl && !key.meta) {
1086
- inputBuffer += str;
1736
+ state.inputBuffer += str;
1087
1737
  render();
1088
1738
  }
1089
1739
  }
1740
+ function killProcess(pidStr, signal = "SIGTERM") {
1741
+ const pid = parseInt(pidStr, 10);
1742
+ if (!pid || pid <= 0) {
1743
+ setStatus(`Invalid PID: ${pidStr}`);
1744
+ return;
1745
+ }
1746
+ try {
1747
+ process.kill(pid, signal);
1748
+ setStatus(`Sent ${signal} to PID ${pid}`);
1749
+ } catch (err) {
1750
+ setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
1751
+ }
1752
+ }
1090
1753
  function onResize() {
1091
- termWidth = process.stdout.columns || 80;
1092
- termHeight = process.stdout.rows || 24;
1093
- history.setMaxLen(Math.max(20, Math.min(200, termWidth - 30)));
1754
+ state.termWidth = process.stdout.columns || 80;
1755
+ state.termHeight = process.stdout.rows || 24;
1756
+ state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
1094
1757
  render();
1095
1758
  }
1096
1759
  function restartTicker() {
1097
- if (intervalHandle) clearInterval(intervalHandle);
1098
- intervalHandle = setInterval(tick, interval * 1e3);
1760
+ if (state.intervalHandle) clearInterval(state.intervalHandle);
1761
+ state.intervalHandle = setInterval(tick, state.interval * 1e3);
1762
+ }
1763
+ async function tick() {
1764
+ if (state.paused) return;
1765
+ try {
1766
+ const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
1767
+ state.lastData = data;
1768
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1769
+ state.history.push({
1770
+ cpuUsage: data.cpu.usage,
1771
+ memUsage: data.memory.usagePercent,
1772
+ temp,
1773
+ rxBytes: data.network.rxBytes,
1774
+ txBytes: data.network.txBytes,
1775
+ gpuUtil: data.gpu?.utilization,
1776
+ powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
1777
+ rxPackets: data.network.rxPackets,
1778
+ txPackets: data.network.txPackets,
1779
+ connections: data.network.connections ?? 0
1780
+ });
1781
+ writeLogRow(data);
1782
+ checkAlerts(data);
1783
+ render();
1784
+ } catch {
1785
+ }
1099
1786
  }
1100
1787
  async function startLive(opts2) {
1101
- if (running) return;
1102
- running = true;
1103
- paused = false;
1104
- detailed = !!opts2.detailed;
1105
- interval = opts2.interval;
1106
- if (opts2.exportDir) exportDir = opts2.exportDir;
1107
- termWidth = process.stdout.columns || 80;
1108
- termHeight = process.stdout.rows || 24;
1109
- history.reset();
1110
- history.setMaxLen(Math.max(20, Math.min(200, termWidth - 30)));
1788
+ if (state.running) return;
1789
+ state.running = true;
1790
+ state.paused = false;
1791
+ state.detailed = !!opts2.detailed;
1792
+ state.interval = opts2.interval;
1793
+ if (opts2.exportDir) state.exportDir = opts2.exportDir;
1794
+ state.termWidth = process.stdout.columns || 80;
1795
+ state.termHeight = process.stdout.rows || 24;
1796
+ state.history.reset();
1797
+ state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
1111
1798
  process.stdout.write("\x1B[?1049h");
1112
1799
  process.stdout.write("\x1B[?25l");
1113
1800
  process.stdout.write("\x1B[2J\x1B[H");
1114
1801
  process.title = "pyre";
1802
+ if (state.mouseEnabled) {
1803
+ process.stdout.write("\x1B[?1000h");
1804
+ }
1115
1805
  await tick();
1116
1806
  restartTicker();
1117
1807
  if (opts2.autoLog) startLogging();
@@ -1119,63 +1809,150 @@ async function startLive(opts2) {
1119
1809
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
1120
1810
  process.stdin.resume();
1121
1811
  process.stdout.on("resize", onResize);
1122
- keypressHandler = (str, key) => {
1812
+ state.keypressHandler = (str, key) => {
1123
1813
  if (!key) return;
1124
1814
  if (key.ctrl && key.name === "c") {
1125
1815
  stopLive();
1126
1816
  return;
1127
1817
  }
1128
- if (inputMode) {
1818
+ if (state.inputMode) {
1129
1819
  handleInputModeKey(str, key);
1130
1820
  return;
1131
1821
  }
1822
+ if (key.mouse) {
1823
+ handleMouseClick(key.mouse);
1824
+ return;
1825
+ }
1826
+ if (key.sequence && key.sequence.startsWith("\x1B[M")) {
1827
+ handleNormalMouse(key.sequence);
1828
+ return;
1829
+ }
1830
+ if (key.sequence && key.sequence.startsWith("\x1B[<")) {
1831
+ handleSGRMouse(key.sequence);
1832
+ return;
1833
+ }
1132
1834
  switch (key.sequence) {
1133
1835
  case "+":
1134
- interval += 1;
1836
+ state.interval += 1;
1135
1837
  restartTicker();
1136
- setStatus(`Interval set to ${interval}s`);
1838
+ setStatus(`Interval set to ${state.interval}s`);
1137
1839
  return;
1138
1840
  case "-":
1139
- interval = Math.max(1, interval - 1);
1841
+ state.interval = Math.max(1, state.interval - 1);
1140
1842
  restartTicker();
1141
- setStatus(`Interval set to ${interval}s`);
1843
+ setStatus(`Interval set to ${state.interval}s`);
1142
1844
  return;
1143
1845
  case "/":
1144
- inputMode = "filter";
1145
- inputBuffer = processFilter;
1846
+ state.inputMode = "filter";
1847
+ state.inputBuffer = state.processFilter;
1848
+ render();
1849
+ return;
1850
+ case "0":
1851
+ case "1":
1852
+ case "2":
1853
+ case "3":
1854
+ case "4":
1855
+ case "5":
1856
+ case "6":
1857
+ case "7":
1858
+ case "8":
1859
+ case "9": {
1860
+ const tabMap = {
1861
+ "0": "disk",
1862
+ "1": "cpu",
1863
+ "2": "mem",
1864
+ "3": "gpu",
1865
+ "4": "power",
1866
+ "5": "battery",
1867
+ "6": "thermal",
1868
+ "7": "network",
1869
+ "8": "packets",
1870
+ "9": "tasks"
1871
+ };
1872
+ const panelId = tabMap[key.sequence];
1873
+ if (panelId) {
1874
+ state.activePanel = state.activePanel === panelId ? "grid" : panelId;
1875
+ setStatus(state.activePanel === "grid" ? "Grid view" : `${panelId} panel`);
1876
+ render();
1877
+ }
1878
+ return;
1879
+ }
1880
+ case "P": {
1881
+ state.activePanel = state.activePanel === "process" ? "grid" : "process";
1882
+ setStatus(state.activePanel === "grid" ? "Grid view" : "Process panel");
1146
1883
  render();
1147
1884
  return;
1885
+ }
1148
1886
  }
1149
1887
  switch (key.name) {
1150
1888
  case "q":
1151
1889
  stopLive();
1152
1890
  break;
1891
+ case "left": {
1892
+ const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
1893
+ if (currentIdx > 0) {
1894
+ state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
1895
+ } else if (currentIdx === -1) {
1896
+ state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
1897
+ }
1898
+ setStatus(`Panel: ${state.activePanel}`);
1899
+ render();
1900
+ break;
1901
+ }
1902
+ case "right": {
1903
+ const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
1904
+ if (currentIdx < state.PANEL_TABS.length - 1) {
1905
+ state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
1906
+ } else if (currentIdx === -1) {
1907
+ state.activePanel = state.PANEL_TABS[0].id;
1908
+ }
1909
+ setStatus(`Panel: ${state.activePanel}`);
1910
+ render();
1911
+ break;
1912
+ }
1153
1913
  case "c":
1154
- inputMode = "customizer";
1155
- customizerIndex = 0;
1914
+ state.inputMode = "customizer";
1915
+ state.customizerIndex = 0;
1156
1916
  render();
1157
1917
  break;
1158
1918
  case "p":
1159
- paused = !paused;
1160
- setStatus(paused ? "Paused" : "Resumed");
1919
+ state.paused = !state.paused;
1920
+ setStatus(state.paused ? "Paused" : "Resumed");
1161
1921
  render();
1162
1922
  break;
1163
1923
  case "g":
1164
- showGraphs = !showGraphs;
1924
+ state.showGraphs = !state.showGraphs;
1925
+ render();
1926
+ break;
1927
+ case "b":
1928
+ state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
1929
+ setStatus(`Graph mode: ${state.graphMode}`);
1165
1930
  render();
1166
1931
  break;
1167
1932
  case "d":
1168
- detailed = !detailed;
1169
- setStatus(`Detailed sensor mode: ${detailed ? "on" : "off"}`);
1933
+ state.detailed = !state.detailed;
1934
+ setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
1170
1935
  break;
1171
1936
  case "s":
1172
- sortMode = sortMode === "cpu" ? "mem" : sortMode === "mem" ? "pid" : "cpu";
1173
- setStatus(`Sorting by ${sortMode}`);
1937
+ const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
1938
+ const curIdx = sortCycle.indexOf(state.sortMode);
1939
+ state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
1940
+ setStatus(`Sorting by ${state.sortMode}`);
1174
1941
  render();
1175
1942
  break;
1176
1943
  case "k":
1177
- inputMode = "kill";
1178
- inputBuffer = "";
1944
+ state.inputMode = "kill";
1945
+ state.inputBuffer = "";
1946
+ render();
1947
+ break;
1948
+ case "S":
1949
+ state.inputMode = "signal";
1950
+ state.inputBuffer = "SIGTERM";
1951
+ render();
1952
+ break;
1953
+ case "t":
1954
+ state.treeView = !state.treeView;
1955
+ setStatus(state.treeView ? "Tree view enabled" : "Flat view enabled");
1179
1956
  render();
1180
1957
  break;
1181
1958
  case "e":
@@ -1187,46 +1964,585 @@ async function startLive(opts2) {
1187
1964
  render();
1188
1965
  break;
1189
1966
  case "f":
1190
- exportFormat = exportFormat === "json" ? "csv" : exportFormat === "csv" ? "tsv" : "json";
1191
- setStatus(`Export format: ${exportFormat}`);
1967
+ state.exportFormat = state.exportFormat === "json" ? "csv" : state.exportFormat === "csv" ? "tsv" : "json";
1968
+ setStatus(`Export format: ${state.exportFormat}`);
1192
1969
  render();
1193
1970
  break;
1971
+ case "tab":
1972
+ cycleTab(1);
1973
+ break;
1974
+ case "left":
1975
+ cycleTab(-1);
1976
+ break;
1977
+ case "right":
1978
+ cycleTab(1);
1979
+ break;
1980
+ default:
1981
+ if (str && str.length === 1) {
1982
+ const tabId = tabKeyToId(str);
1983
+ if (tabId) {
1984
+ state.activePanel = tabId;
1985
+ setStatus(`Panel: ${tabId.toUpperCase()}`);
1986
+ render();
1987
+ }
1988
+ }
1194
1989
  }
1195
1990
  };
1196
- process.stdin.on("keypress", keypressHandler);
1991
+ process.stdin.on("keypress", state.keypressHandler);
1197
1992
  process.once("SIGINT", () => stopLive());
1198
1993
  }
1994
+ function tabKeyToId(str) {
1995
+ const map = { "1": "cpu", "2": "mem", "3": "gpu", "4": "power", "5": "battery", "6": "thermal", "7": "network", "8": "packets", "9": "tasks", "0": "disk", "p": "process" };
1996
+ return map[str] ?? null;
1997
+ }
1998
+ function cycleTab(direction) {
1999
+ const tabs = state.PANEL_TABS.map((t) => t.id);
2000
+ const current = state.activePanel === "grid" ? -1 : tabs.indexOf(state.activePanel);
2001
+ const next = (current + direction + tabs.length) % tabs.length;
2002
+ state.activePanel = tabs[next];
2003
+ setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
2004
+ render();
2005
+ }
2006
+ function handleNormalMouse(seq) {
2007
+ if (seq.length < 6) return;
2008
+ const cb = seq.charCodeAt(3) - 32;
2009
+ const cx = seq.charCodeAt(4) - 32;
2010
+ const cy = seq.charCodeAt(5) - 32;
2011
+ if (cb === 0 || cb === 1) {
2012
+ handleMouseClick(cy, cx);
2013
+ }
2014
+ }
2015
+ function handleSGRMouse(seq) {
2016
+ const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
2017
+ if (!match) return;
2018
+ const button = parseInt(match[1]);
2019
+ const cx = parseInt(match[2]);
2020
+ const cy = parseInt(match[3]);
2021
+ if (button === 0 || button === 1) {
2022
+ handleMouseClick(cy, cx);
2023
+ }
2024
+ }
2025
+ function handleMouseClick(y, x) {
2026
+ if (y !== TAB_BAR_ROW) return;
2027
+ const hitboxes = getTabHitboxes();
2028
+ const hit = hitboxes.find((h) => x >= h.start && x < h.end);
2029
+ if (hit) {
2030
+ state.activePanel = state.activePanel === hit.id ? "grid" : hit.id;
2031
+ setStatus(state.activePanel === "grid" ? "Grid view" : `Panel: ${hit.id.toUpperCase()}`);
2032
+ render();
2033
+ }
2034
+ }
1199
2035
  function stopLive() {
1200
- if (intervalHandle) {
1201
- clearInterval(intervalHandle);
1202
- intervalHandle = null;
2036
+ if (state.intervalHandle) {
2037
+ clearInterval(state.intervalHandle);
2038
+ state.intervalHandle = null;
1203
2039
  }
1204
- if (statusTimer) {
1205
- clearTimeout(statusTimer);
1206
- statusTimer = null;
2040
+ if (state.statusTimer) {
2041
+ clearTimeout(state.statusTimer);
2042
+ state.statusTimer = null;
1207
2043
  }
1208
- if (logStream) {
1209
- logStream.end();
1210
- logStream = null;
2044
+ if (state.logStream) {
2045
+ state.logStream.end();
2046
+ state.logStream = null;
1211
2047
  }
1212
- if (keypressHandler) {
1213
- process.stdin.removeListener("keypress", keypressHandler);
1214
- keypressHandler = null;
2048
+ if (state.keypressHandler) {
2049
+ process.stdin.removeListener("keypress", state.keypressHandler);
2050
+ state.keypressHandler = null;
1215
2051
  }
1216
2052
  process.stdout.removeListener("resize", onResize);
1217
- running = false;
1218
- logging = false;
2053
+ state.running = false;
2054
+ state.logging = false;
1219
2055
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1220
2056
  process.stdin.pause();
2057
+ if (state.mouseEnabled) {
2058
+ process.stdout.write("\x1B[?1000l");
2059
+ }
1221
2060
  process.stdout.write("\x1B[?25h");
1222
2061
  process.stdout.write("\x1B[?1049l");
1223
2062
  process.stdout.write("\x1B[2J\x1B[H");
1224
2063
  process.exit(0);
1225
2064
  }
1226
2065
 
2066
+ // src/splash.ts
2067
+ import chalk6 from "chalk";
2068
+ var MAXV = 36;
2069
+ var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
2070
+ var palette = [
2071
+ "#0a0505",
2072
+ "#1a0806",
2073
+ "#2a0b06",
2074
+ "#3f1004",
2075
+ "#591404",
2076
+ "#791804",
2077
+ "#9e1c04",
2078
+ "#c92004",
2079
+ "#d63004",
2080
+ "#e34004",
2081
+ "#f05004",
2082
+ "#fd6004",
2083
+ "#ff7004",
2084
+ "#ff8004",
2085
+ "#ff9004",
2086
+ "#ffa004",
2087
+ "#ffb004",
2088
+ "#ffc004",
2089
+ "#ffd004",
2090
+ "#ffe004",
2091
+ "#fff004",
2092
+ "#fff820",
2093
+ "#ffff40",
2094
+ "#ffff60",
2095
+ "#ffff80",
2096
+ "#ffffa0",
2097
+ "#ffffc0",
2098
+ "#ffffe0",
2099
+ "#ffffff",
2100
+ "#ffffff",
2101
+ "#ffffff",
2102
+ "#ffffff",
2103
+ "#ffffff",
2104
+ "#ffffff",
2105
+ "#ffffff",
2106
+ "#ffffff"
2107
+ ];
2108
+ function getRgb(hex) {
2109
+ const r = parseInt(hex.slice(1, 3), 16);
2110
+ const g = parseInt(hex.slice(3, 5), 16);
2111
+ const b = parseInt(hex.slice(5, 7), 16);
2112
+ return { r, g, b };
2113
+ }
2114
+ var rgbPalette = palette.map(getRgb);
2115
+ function getChar(val) {
2116
+ const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
2117
+ return CHAR_RAMP[idx];
2118
+ }
2119
+ async function showSplash() {
2120
+ const cols = 60;
2121
+ const rows = 15;
2122
+ let fire = new Uint8Array(cols * rows);
2123
+ process.stdout.write("\x1B[2J");
2124
+ const termRows = process.stdout.rows || 24;
2125
+ const termCols = process.stdout.columns || 80;
2126
+ const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
2127
+ const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
2128
+ return new Promise((resolve) => {
2129
+ const interval = setInterval(() => {
2130
+ for (let x = 0; x < cols; x++) {
2131
+ fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
2132
+ }
2133
+ for (let y = rows - 1; y >= 1; y--) {
2134
+ for (let x = 0; x < cols; x++) {
2135
+ const src = y * cols + x;
2136
+ const val = fire[src];
2137
+ if (val === 0) {
2138
+ fire[(y - 1) * cols + x] = 0;
2139
+ continue;
2140
+ }
2141
+ const decay = Math.floor(Math.random() * 3);
2142
+ const drift = Math.floor(Math.random() * 3) - 1;
2143
+ let nx = x + drift;
2144
+ if (nx < 0) nx = 0;
2145
+ else if (nx >= cols) nx = cols - 1;
2146
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
2147
+ }
2148
+ }
2149
+ let output = "";
2150
+ for (let i = 0; i < startRow; i++) output += "\n";
2151
+ for (let y = 0; y < rows; y++) {
2152
+ output += " ".repeat(startCol);
2153
+ for (let x = 0; x < cols; x++) {
2154
+ const v = fire[y * cols + x];
2155
+ const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
2156
+ output += chalk6.rgb(color.r, color.g, color.b)(getChar(v));
2157
+ }
2158
+ output += "\n";
2159
+ }
2160
+ output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk6.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
2161
+ output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk6.dim("[Press Enter to continue]");
2162
+ process.stdout.write(`\x1B[H${output}`);
2163
+ }, 65);
2164
+ const onEnter = () => {
2165
+ process.stdin.removeListener("data", onEnter);
2166
+ clearInterval(interval);
2167
+ process.stdout.write("\x1B[2J\x1B[H");
2168
+ resolve();
2169
+ };
2170
+ process.stdin.setRawMode(true);
2171
+ process.stdin.resume();
2172
+ process.stdin.on("data", onEnter);
2173
+ setTimeout(onEnter, 5e3);
2174
+ });
2175
+ }
2176
+
2177
+ // src/p2p/server.ts
2178
+ import net from "net";
2179
+ import chalk7 from "chalk";
2180
+
2181
+ // src/p2p/protocol.ts
2182
+ var HEADER_SIZE = 4;
2183
+ var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
2184
+ function buildMessage(kind, payload) {
2185
+ const envelope = { kind, payload };
2186
+ const body = Buffer.from(JSON.stringify(envelope), "utf-8");
2187
+ const header2 = Buffer.alloc(HEADER_SIZE);
2188
+ header2.writeUInt32BE(body.length, 0);
2189
+ return Buffer.concat([header2, body]);
2190
+ }
2191
+ function encodeAuth(password) {
2192
+ return buildMessage("auth", { password });
2193
+ }
2194
+ function encodeAuthOk() {
2195
+ return buildMessage("auth-ok", { ok: true });
2196
+ }
2197
+ function encodeAuthFail(reason) {
2198
+ return buildMessage("auth-fail", { ok: false, reason });
2199
+ }
2200
+ function encodeData(stats) {
2201
+ return buildMessage("data", { stats });
2202
+ }
2203
+ function encodePing() {
2204
+ return buildMessage("ping", { ts: Date.now() });
2205
+ }
2206
+ function encodePong(ts) {
2207
+ return buildMessage("pong", { ts });
2208
+ }
2209
+ function encodeDisconnect(reason) {
2210
+ return buildMessage("disconnect", { reason });
2211
+ }
2212
+
2213
+ // src/p2p/server.ts
2214
+ var HEADER_SIZE2 = 4;
2215
+ function readMessageFromPeer(peer) {
2216
+ while (peer.readBuffer.length > 0) {
2217
+ if (peer.readExpectingHeader) {
2218
+ if (peer.readBuffer.length >= HEADER_SIZE2) {
2219
+ peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
2220
+ peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE2);
2221
+ peer.readExpectingHeader = false;
2222
+ } else {
2223
+ break;
2224
+ }
2225
+ }
2226
+ if (!peer.readExpectingHeader) {
2227
+ if (peer.readBuffer.length >= peer.readMessageLength) {
2228
+ const body = peer.readBuffer.subarray(0, peer.readMessageLength);
2229
+ peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
2230
+ peer.readExpectingHeader = true;
2231
+ return body;
2232
+ } else {
2233
+ break;
2234
+ }
2235
+ }
2236
+ }
2237
+ return null;
2238
+ }
2239
+ var P2PServer = class {
2240
+ server = null;
2241
+ peers = /* @__PURE__ */ new Map();
2242
+ options;
2243
+ running = false;
2244
+ constructor(options) {
2245
+ this.options = options;
2246
+ }
2247
+ get info() {
2248
+ return {
2249
+ host: this.options.host,
2250
+ port: this.options.port,
2251
+ peersConnected: this.peers.size
2252
+ };
2253
+ }
2254
+ async start() {
2255
+ return new Promise((resolve, reject) => {
2256
+ this.server = net.createServer((socket) => this.onConnection(socket));
2257
+ this.server.on("error", (err) => reject(err));
2258
+ this.server.listen(this.options.port, this.options.host, () => {
2259
+ this.running = true;
2260
+ const addr = this.server?.address();
2261
+ const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
2262
+ console.log(chalk7.green(`P2P server listening on ${bound}`));
2263
+ console.log(chalk7.dim(` Password: ${this.options.password}`));
2264
+ console.log(chalk7.dim(` Interval: ${this.options.intervalMs}ms`));
2265
+ resolve();
2266
+ });
2267
+ });
2268
+ }
2269
+ stop() {
2270
+ this.running = false;
2271
+ for (const peer of this.peers.values()) {
2272
+ this.disconnectPeer(peer, "Server shutting down");
2273
+ }
2274
+ this.peers.clear();
2275
+ if (this.server) {
2276
+ this.server.close();
2277
+ this.server = null;
2278
+ }
2279
+ }
2280
+ onConnection(socket) {
2281
+ const peer = {
2282
+ socket,
2283
+ authenticated: false,
2284
+ intervalHandle: null,
2285
+ pingHandle: null,
2286
+ readBuffer: Buffer.alloc(0),
2287
+ readExpectingHeader: true,
2288
+ readMessageLength: 0
2289
+ };
2290
+ this.peers.set(socket, peer);
2291
+ const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
2292
+ console.log(chalk7.cyan(`Peer connected: ${remoteAddr}`));
2293
+ socket.on("data", (data) => this.onData(peer, data));
2294
+ socket.on("close", () => this.onPeerClose(peer));
2295
+ socket.on("error", (err) => this.onPeerError(peer, err));
2296
+ socket.setTimeout(3e4);
2297
+ socket.on("timeout", () => {
2298
+ if (!peer.authenticated) {
2299
+ this.disconnectPeer(peer, "Authentication timeout");
2300
+ }
2301
+ });
2302
+ }
2303
+ onData(peer, data) {
2304
+ peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
2305
+ if (!peer.authenticated) {
2306
+ const body = readMessageFromPeer(peer);
2307
+ if (body) {
2308
+ this.handleAuthMessage(peer, body);
2309
+ }
2310
+ return;
2311
+ }
2312
+ }
2313
+ handleAuthMessage(peer, body) {
2314
+ try {
2315
+ const msg = JSON.parse(body.toString("utf-8"));
2316
+ if (msg.kind === "auth" && typeof msg.payload?.password === "string") {
2317
+ if (msg.payload.password === this.options.password) {
2318
+ peer.authenticated = true;
2319
+ peer.socket.write(encodeAuthOk());
2320
+ console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
2321
+ this.startDataStream(peer);
2322
+ this.startPing(peer);
2323
+ } else {
2324
+ peer.socket.write(encodeAuthFail("Invalid password"));
2325
+ this.disconnectPeer(peer, "Authentication failed");
2326
+ }
2327
+ } else {
2328
+ peer.socket.write(encodeAuthFail("Expected auth message first"));
2329
+ this.disconnectPeer(peer, "Expected auth message");
2330
+ }
2331
+ } catch {
2332
+ peer.socket.write(encodeAuthFail("Invalid message format"));
2333
+ this.disconnectPeer(peer, "Invalid message format");
2334
+ }
2335
+ }
2336
+ startDataStream(peer) {
2337
+ const send = async () => {
2338
+ if (!peer.authenticated || peer.socket.destroyed) return;
2339
+ try {
2340
+ const data = await collectAll({ detailed: this.options.detailed });
2341
+ peer.socket.write(encodeData(data));
2342
+ } catch {
2343
+ }
2344
+ };
2345
+ send();
2346
+ peer.intervalHandle = setInterval(send, this.options.intervalMs);
2347
+ }
2348
+ startPing(peer) {
2349
+ peer.pingHandle = setInterval(() => {
2350
+ if (peer.socket.destroyed) return;
2351
+ peer.socket.write(encodePing());
2352
+ }, 5e3);
2353
+ }
2354
+ onPeerClose(peer) {
2355
+ this.cleanupPeer(peer);
2356
+ console.log(chalk7.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
2357
+ }
2358
+ onPeerError(peer, err) {
2359
+ const code = err.code;
2360
+ if (code === "ECONNRESET") {
2361
+ console.log(chalk7.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
2362
+ } else {
2363
+ console.log(chalk7.red(`Peer error: ${err.message}`));
2364
+ }
2365
+ this.cleanupPeer(peer);
2366
+ }
2367
+ disconnectPeer(peer, reason) {
2368
+ try {
2369
+ peer.socket.write(encodeDisconnect(reason));
2370
+ } catch {
2371
+ }
2372
+ peer.socket.destroy();
2373
+ }
2374
+ cleanupPeer(peer) {
2375
+ if (peer.intervalHandle) {
2376
+ clearInterval(peer.intervalHandle);
2377
+ peer.intervalHandle = null;
2378
+ }
2379
+ if (peer.pingHandle) {
2380
+ clearInterval(peer.pingHandle);
2381
+ peer.pingHandle = null;
2382
+ }
2383
+ this.peers.delete(peer.socket);
2384
+ }
2385
+ };
2386
+
2387
+ // src/p2p/client.ts
2388
+ import net2 from "net";
2389
+ var P2PClient = class {
2390
+ socket = null;
2391
+ options;
2392
+ running = false;
2393
+ readState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
2394
+ onDataCallback = null;
2395
+ onStatusCallback = null;
2396
+ reconnectTimer = null;
2397
+ pingHandle = null;
2398
+ constructor(options) {
2399
+ this.options = options;
2400
+ }
2401
+ setOnData(cb) {
2402
+ this.onDataCallback = cb;
2403
+ }
2404
+ setOnStatus(cb) {
2405
+ this.onStatusCallback = cb;
2406
+ }
2407
+ async connect() {
2408
+ return new Promise((resolve, reject) => {
2409
+ this.socket = new net2.Socket();
2410
+ this.socket.on("data", (data) => this.onData(data));
2411
+ this.socket.on("close", () => this.onClose());
2412
+ this.socket.on("error", (err) => this.onError(err));
2413
+ this.socket.on("timeout", () => this.onTimeout());
2414
+ this.socket.setTimeout(1e4);
2415
+ this.socket.connect(this.options.port, this.options.host, () => {
2416
+ this.running = true;
2417
+ this.socket?.setTimeout(0);
2418
+ this.socket?.write(encodeAuth(this.options.password));
2419
+ this.status("Connecting...");
2420
+ resolve();
2421
+ });
2422
+ this.socket.once("error", (err) => {
2423
+ reject(err);
2424
+ });
2425
+ });
2426
+ }
2427
+ disconnect() {
2428
+ this.running = false;
2429
+ if (this.reconnectTimer) {
2430
+ clearTimeout(this.reconnectTimer);
2431
+ this.reconnectTimer = null;
2432
+ }
2433
+ if (this.pingHandle) {
2434
+ clearInterval(this.pingHandle);
2435
+ this.pingHandle = null;
2436
+ }
2437
+ if (this.socket) {
2438
+ try {
2439
+ this.socket.write(encodeDisconnect("Client closing"));
2440
+ } catch {
2441
+ }
2442
+ this.socket.destroy();
2443
+ this.socket = null;
2444
+ }
2445
+ this.status("Disconnected");
2446
+ }
2447
+ onData(data) {
2448
+ this.readState.buffer = Buffer.concat([this.readState.buffer, data]);
2449
+ while (this.readState.buffer.length > 0) {
2450
+ if (this.readState.expectingHeader) {
2451
+ if (this.readState.buffer.length >= 4) {
2452
+ this.readState.messageLength = this.readState.buffer.readUInt32BE(0);
2453
+ this.readState.buffer = this.readState.buffer.subarray(4);
2454
+ this.readState.expectingHeader = false;
2455
+ } else {
2456
+ break;
2457
+ }
2458
+ }
2459
+ if (!this.readState.expectingHeader) {
2460
+ if (this.readState.buffer.length >= this.readState.messageLength) {
2461
+ const body = this.readState.buffer.subarray(0, this.readState.messageLength);
2462
+ this.readState.buffer = this.readState.buffer.subarray(this.readState.messageLength);
2463
+ this.readState.expectingHeader = true;
2464
+ this.handleMessage(body);
2465
+ } else {
2466
+ break;
2467
+ }
2468
+ }
2469
+ }
2470
+ }
2471
+ handleMessage(body) {
2472
+ try {
2473
+ const msg = JSON.parse(body.toString("utf-8"));
2474
+ switch (msg.kind) {
2475
+ case "auth-ok":
2476
+ this.status("Authenticated \u2014 receiving live data");
2477
+ this.startPing();
2478
+ break;
2479
+ case "auth-fail":
2480
+ this.status(`Authentication failed: ${msg.payload.reason}`);
2481
+ this.disconnect();
2482
+ break;
2483
+ case "data":
2484
+ if (this.onDataCallback && msg.payload?.stats) {
2485
+ this.onDataCallback(msg.payload.stats);
2486
+ }
2487
+ break;
2488
+ case "ping":
2489
+ if (this.socket && !this.socket.destroyed) {
2490
+ const ts = msg.payload.ts;
2491
+ this.socket.write(encodePong(ts));
2492
+ }
2493
+ break;
2494
+ case "pong":
2495
+ break;
2496
+ case "disconnect":
2497
+ this.status(`Server disconnected: ${msg.payload.reason}`);
2498
+ this.disconnect();
2499
+ break;
2500
+ default:
2501
+ break;
2502
+ }
2503
+ } catch {
2504
+ }
2505
+ }
2506
+ startPing() {
2507
+ this.pingHandle = setInterval(() => {
2508
+ if (this.socket && !this.socket.destroyed) {
2509
+ this.socket.write(encodePing());
2510
+ }
2511
+ }, 15e3);
2512
+ }
2513
+ onClose() {
2514
+ if (this.running) {
2515
+ this.status("Connection closed \u2014 reconnecting in 3s");
2516
+ this.reconnectTimer = setTimeout(() => {
2517
+ if (this.running) this.connect().catch(() => {
2518
+ });
2519
+ }, 3e3);
2520
+ }
2521
+ }
2522
+ onError(err) {
2523
+ const code = err.code;
2524
+ if (code === "ECONNREFUSED") {
2525
+ this.status("Connection refused \u2014 is the server running?");
2526
+ } else if (code === "ETIMEDOUT") {
2527
+ this.status("Connection timed out");
2528
+ } else {
2529
+ this.status(`Connection error: ${err.message}`);
2530
+ }
2531
+ }
2532
+ onTimeout() {
2533
+ this.status("Connection timed out");
2534
+ this.disconnect();
2535
+ }
2536
+ status(msg) {
2537
+ if (this.onStatusCallback) {
2538
+ this.onStatusCallback(msg);
2539
+ }
2540
+ }
2541
+ };
2542
+
1227
2543
  // src/index.ts
1228
2544
  var program = new Command();
1229
- program.name("pyre").version("1.1.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts");
2545
+ program.name("pyre").version("2.0.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", "cpu").option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)", "0.0.0.0").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication");
1230
2546
  program.parse(process.argv);
1231
2547
  var opts = program.opts();
1232
2548
  function isExportMode() {
@@ -1236,32 +2552,129 @@ function stripAnsi(s) {
1236
2552
  return s.replace(/\x1b\[[0-9;]*m/g, "");
1237
2553
  }
1238
2554
  async function main() {
1239
- const requestedLive = program.args[0] === "live" || !isExportMode() && !opts.once;
1240
- if (requestedLive) {
1241
- const interval2 = parseFloat(opts.interval) || 2;
2555
+ const cmd = program.args[0];
2556
+ if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
2557
+ if (cmd === "live") {
2558
+ program.args.shift();
2559
+ }
2560
+ await showSplash();
2561
+ const interval = parseFloat(opts.interval) || 2;
1242
2562
  await startLive({
1243
- interval: interval2,
2563
+ interval,
1244
2564
  detailed: opts.detailed,
1245
2565
  exportDir: opts.exportDir,
1246
2566
  autoLog: opts.log
1247
2567
  });
1248
2568
  return;
1249
2569
  }
1250
- const data = await collectAll({ detailed: opts.detailed });
2570
+ if (cmd === "p2p") {
2571
+ const subcommand = program.args[1];
2572
+ if (subcommand === "server") {
2573
+ await runP2PServer();
2574
+ return;
2575
+ } else if (subcommand === "connect") {
2576
+ await runP2PConnect();
2577
+ return;
2578
+ } else {
2579
+ console.log(chalk8.red("Usage: pyre p2p <server|connect> [options]"));
2580
+ process.exit(1);
2581
+ }
2582
+ }
2583
+ const requestedLimit = parseInt(opts.limit, 10);
2584
+ const processLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : void 0;
2585
+ const data = await collectAll({ detailed: opts.detailed, processLimit });
1251
2586
  let output;
1252
2587
  if (opts.json) output = formatJson(data);
1253
2588
  else if (opts.csv) output = formatCsv(data);
1254
2589
  else if (opts.tsv) output = formatTsv(data);
1255
- else output = formatTable(data, { width: process.stdout.columns || 80 });
2590
+ else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
1256
2591
  console.log(output);
1257
2592
  if (opts.out) {
1258
2593
  fs2.writeFileSync(opts.out, stripAnsi(output));
1259
- console.log(chalk4.dim(`
2594
+ console.log(chalk8.dim(`
1260
2595
  Saved to ${opts.out}`));
1261
2596
  }
1262
2597
  }
1263
2598
  main().catch((err) => {
1264
- console.error(chalk4.red(`Error: ${err.message}`));
1265
- stopLive();
2599
+ console.error(chalk8.red(`Error: ${err.message}`));
1266
2600
  process.exit(1);
1267
2601
  });
2602
+ async function runP2PServer() {
2603
+ const host = opts.p2pHost || "0.0.0.0";
2604
+ const port = parseInt(opts.p2pPort, 10) || 9876;
2605
+ const password = opts.p2pPassword || "";
2606
+ const interval = parseFloat(opts.interval) || 2;
2607
+ const detailed = !!opts.detailed;
2608
+ if (!password) {
2609
+ console.log(chalk8.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
2610
+ process.exit(1);
2611
+ }
2612
+ const server = new P2PServer({ host, port, password, intervalMs: interval * 1e3, detailed });
2613
+ console.log(chalk8.bold(`
2614
+ pyre P2P Server`));
2615
+ console.log(chalk8.dim(` Host: ${host}`));
2616
+ console.log(chalk8.dim(` Port: ${port}`));
2617
+ console.log(chalk8.dim(` Password: ${password}`));
2618
+ console.log(chalk8.dim(` Interval: ${interval}s`));
2619
+ console.log(chalk8.dim(` Detailed: ${detailed ? "on" : "off"}`));
2620
+ console.log(chalk8.dim(` Waiting for peers...
2621
+ `));
2622
+ await server.start();
2623
+ const rl = readline2.createInterface({ input: process.stdin });
2624
+ rl.on("line", (line) => {
2625
+ const trimmed = line.trim().toLowerCase();
2626
+ if (trimmed === "q" || trimmed === "quit" || trimmed === "exit") {
2627
+ server.stop();
2628
+ rl.close();
2629
+ process.exit(0);
2630
+ } else if (trimmed === "status") {
2631
+ const info = server.info;
2632
+ console.log(chalk8.dim(` Peers connected: ${info.peersConnected}`));
2633
+ }
2634
+ });
2635
+ process.once("SIGINT", () => {
2636
+ server.stop();
2637
+ rl.close();
2638
+ process.exit(0);
2639
+ });
2640
+ }
2641
+ async function runP2PConnect() {
2642
+ const host = opts.p2pHost || "127.0.0.1";
2643
+ const port = parseInt(opts.p2pPort, 10) || 9876;
2644
+ const password = opts.p2pPassword || "";
2645
+ if (!password) {
2646
+ console.log(chalk8.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
2647
+ process.exit(1);
2648
+ }
2649
+ const client = new P2PClient({ host, port, password });
2650
+ client.setOnStatus((msg) => {
2651
+ process.stdout.write(`\r${chalk8.dim(msg)}${" ".repeat(40)}\r`);
2652
+ });
2653
+ client.setOnData((data) => {
2654
+ process.stdout.write("\x1B[2J\x1B[H");
2655
+ console.log(chalk8.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
2656
+ console.log(chalk8.dim(` ${data.timestamp}`));
2657
+ console.log();
2658
+ console.log(formatTable(data, {
2659
+ width: process.stdout.columns || 80,
2660
+ sortBy: "cpu",
2661
+ treeView: false,
2662
+ visible: { packets: true }
2663
+ }));
2664
+ });
2665
+ console.log(chalk8.bold(`
2666
+ pyre P2P Client`));
2667
+ console.log(chalk8.dim(` Connecting to ${host}:${port}...`));
2668
+ console.log(chalk8.dim(` Press Ctrl+C to disconnect
2669
+ `));
2670
+ try {
2671
+ await client.connect();
2672
+ } catch (err) {
2673
+ console.log(chalk8.red(`Connection failed: ${err.message}`));
2674
+ process.exit(1);
2675
+ }
2676
+ process.once("SIGINT", () => {
2677
+ client.disconnect();
2678
+ process.exit(0);
2679
+ });
2680
+ }