pyre-cli 7.0.0 → 8.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
@@ -1,783 +1,36 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ collectAll
4
+ } from "./chunk-KBKXU2ZV.js";
5
+ import {
6
+ CONFIG_FILE,
7
+ readConfig,
8
+ writeConfig
9
+ } from "./chunk-RVGKEJXT.js";
10
+ import "./chunk-GLH7EKAX.js";
2
11
 
3
12
  // src/index.ts
4
- import fs9 from "fs";
5
- import os5 from "os";
6
- import path6 from "path";
13
+ import fs8 from "fs";
14
+ import os3 from "os";
15
+ import path5 from "path";
7
16
  import readline2 from "readline";
8
17
  import { Command } from "commander";
9
- import chalk13 from "chalk";
10
-
11
- // src/monitors/collectors.ts
12
- import chalk from "chalk";
13
- import os from "os";
14
-
15
- // src/monitors/run.ts
16
- import { exec } from "child_process";
17
- import { promisify } from "util";
18
- var execAsync = promisify(exec);
19
- async function run(cmd, fallback = "", timeout = 5e3) {
20
- try {
21
- return (await execAsync(cmd, { encoding: "utf8", timeout })).stdout.trim();
22
- } catch {
23
- return fallback;
24
- }
25
- }
26
-
27
- // src/monitors/smc.ts
28
- var smcCache = null;
29
- async function getSmcMetrics() {
30
- const now = Date.now();
31
- if (smcCache && now - smcCache.ts < 4e3) return smcCache.data;
32
- const result = { temps: {}, power: {}, freq: {} };
33
- try {
34
- const pm = (await run(
35
- "sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null",
36
- ""
37
- )).trim();
38
- if (pm) {
39
- const cpuTemp = pm.match(/CPU die temperature:\s*([\d.]+)/i);
40
- if (cpuTemp) result.temps["cpu_die"] = parseFloat(cpuTemp[1]);
41
- const gpuTemp = pm.match(/GPU die temperature:\s*([\d.]+)/i);
42
- if (gpuTemp) result.temps["gpu_die"] = parseFloat(gpuTemp[1]);
43
- const smcTemp = pm.match(/SMC die temperature:\s*([\d.]+)/i);
44
- if (smcTemp) result.temps["smc_die"] = parseFloat(smcTemp[1]);
45
- const cpuPower = pm.match(/CPU Power:\s*([\d.]+)\s*mW/i);
46
- if (cpuPower) result.power.cpu = parseFloat(cpuPower[1]) / 1e3;
47
- const gpuPower = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
48
- if (gpuPower) result.power.gpu = parseFloat(gpuPower[1]) / 1e3;
49
- const combinedPower = pm.match(/Combined Power \(CPU \+ GPU.*?\):\s*([\d.]+)\s*mW/i);
50
- if (combinedPower) result.power.combined = parseFloat(combinedPower[1]) / 1e3;
51
- const eFreq = pm.match(/E-Cluster HW active frequency:\s*([\d.]+)/i);
52
- if (eFreq) result.freq["e_cluster"] = parseFloat(eFreq[1]);
53
- const pFreq = pm.match(/P-Cluster HW active frequency:\s*([\d.]+)/i);
54
- if (pFreq) result.freq["p_cluster"] = parseFloat(pFreq[1]);
55
- const cpuFreq = pm.match(/CPU HW active frequency:\s*([\d.]+)/i);
56
- if (cpuFreq) result.freq["cpu"] = parseFloat(cpuFreq[1]);
57
- }
58
- } catch {
59
- }
60
- if (result.temps["cpu_die"] === void 0) {
61
- try {
62
- const ioregRaw = await run("ioreg -r -c AppleDeviceManagementHIDEventService 2>/dev/null", "");
63
- if (ioregRaw) {
64
- const matches = ioregRaw.matchAll(/"(?:PrimaryTemperature|Temperature)"\s*=\s*(\d+)/g);
65
- for (const match of matches) {
66
- let val = parseFloat(match[1]);
67
- if (val > 1e3) val = val / 100;
68
- if (val >= 15 && val <= 115) {
69
- if (result.temps["cpu_die"] === void 0) result.temps["cpu_die"] = Math.round(val * 10) / 10;
70
- else if (result.temps["gpu_die"] === void 0) result.temps["gpu_die"] = Math.round(val * 10) / 10;
71
- else if (result.temps["smc_die"] === void 0) result.temps["smc_die"] = Math.round(val * 10) / 10;
72
- }
73
- }
74
- }
75
- } catch {
76
- }
77
- }
78
- if (result.temps["cpu_die"] === void 0) {
79
- try {
80
- const tz0 = (await run("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null", "")).trim();
81
- if (tz0) {
82
- const rawVal = parseFloat(tz0);
83
- const val = rawVal > 1e3 ? rawVal / 1e3 : rawVal;
84
- if (val >= 10 && val <= 120) {
85
- result.temps["cpu_die"] = Math.round(val * 10) / 10;
86
- }
87
- }
88
- } catch {
89
- }
90
- }
91
- if (result.temps["cpu_die"] === void 0) {
92
- let loadUsage = 0;
93
- try {
94
- const top = await run("top -l 1 -n 0 2>/dev/null", "");
95
- const m = top.match(/CPU usage:\s*[\d.]+\%\s*user,\s*[\d.]+\%\s*sys,\s*([\d.]+)\%\s*idle/);
96
- if (m) loadUsage = Math.max(0, 100 - parseFloat(m[1]));
97
- } catch {
98
- }
99
- const estCpu = Math.round((38 + loadUsage * 0.42) * 10) / 10;
100
- result.temps["cpu_die"] = estCpu;
101
- if (result.temps["gpu_die"] === void 0) {
102
- result.temps["gpu_die"] = Math.max(32, Math.round((estCpu - 3) * 10) / 10);
103
- }
104
- if (result.temps["smc_die"] === void 0) {
105
- result.temps["smc_die"] = Math.max(30, Math.round((estCpu - 6) * 10) / 10);
106
- }
107
- }
108
- smcCache = { data: result, ts: now };
109
- return result;
110
- }
111
- function parseSuffix(s) {
112
- return s.toUpperCase() === "K" ? 1024 : s.toUpperCase() === "M" ? 1024 * 1024 : s.toUpperCase() === "G" ? 1024 * 1024 * 1024 : 1;
113
- }
114
-
115
- // src/monitors/collectors.ts
116
- var SP_TTL_MS = 1e4;
117
- var NETSTAT_TTL_MS = 1e3;
118
- var prevCpuTimes = null;
119
- var ROUTE_TTL_MS = 5e3;
120
- var SYSCTL_TTL_MS = 6e4;
121
- var spGpuCache = null;
122
- var spBatteryCache = null;
123
- var netstatIbCache = null;
124
- var routeCache = null;
125
- var sysctlCache = /* @__PURE__ */ new Map();
126
- async function cachedSysctl(key, fallback) {
127
- const now = Date.now();
128
- const hit = sysctlCache.get(key);
129
- if (hit && now - hit.ts < SYSCTL_TTL_MS) return hit.value;
130
- const value = await run(`sysctl -n ${key}`, fallback);
131
- sysctlCache.set(key, { value, ts: now });
132
- return value;
133
- }
134
- async function cachedNetstatIb() {
135
- const now = Date.now();
136
- if (netstatIbCache && now - netstatIbCache.ts < NETSTAT_TTL_MS) return netstatIbCache.raw;
137
- const raw = await run("netstat -ib");
138
- netstatIbCache = { raw, ts: now };
139
- return raw;
140
- }
141
- async function cachedRouteIface() {
142
- const now = Date.now();
143
- if (routeCache && now - routeCache.ts < ROUTE_TTL_MS) return routeCache;
144
- const primary = (await run("route -n get default")).trim();
145
- const ifaceMatch = primary.match(/interface:\s+(\S+)/);
146
- const iface = ifaceMatch?.[1] || "en0";
147
- let ip = "0.0.0.0";
148
- try {
149
- const ifconfig = (await run(`ifconfig ${iface}`)).trim();
150
- const ipMatch = ifconfig.match(/inet\s+([\d.]+)\s+netmask/);
151
- if (ipMatch) ip = ipMatch[1];
152
- } catch {
153
- }
154
- routeCache = { iface, ip, ts: now };
155
- return routeCache;
156
- }
157
- var BATTERY_WINDOW_MS = 15 * 60 * 1e3;
158
- var MIN_ESTIMATE_WINDOW_MS = 30 * 1e3;
159
- var prevBattery = { samples: [] };
160
- function estimateBatteryLife(level, state2) {
161
- const now = Date.now();
162
- if (state2 === "charged" || state2 === "charging") {
163
- prevBattery.samples = [];
164
- return {};
165
- }
166
- prevBattery.samples.push({ level, ts: now });
167
- prevBattery.samples = prevBattery.samples.filter((s) => now - s.ts <= BATTERY_WINDOW_MS);
168
- const first = prevBattery.samples[0];
169
- const elapsedMs = now - first.ts;
170
- if (elapsedMs < MIN_ESTIMATE_WINDOW_MS) {
171
- return {};
172
- }
173
- const dtHours = Math.max(elapsedMs / 1e3 / 3600, 1e-4);
174
- const discharged = first.level - level;
175
- if (discharged <= 0) {
176
- return { estimatedTimeToEmpty: "calculating" };
177
- }
178
- const dischargeRate = discharged / dtHours;
179
- const remainingLevel = Math.max(level, 0);
180
- const hoursToEmpty = remainingLevel / dischargeRate;
181
- const h = Math.floor(hoursToEmpty);
182
- const m = Math.round((hoursToEmpty - h) * 60);
183
- const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : "calculating";
184
- const ASSUMED_CAPACITY_WH = 55;
185
- const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
186
- return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
187
- }
188
- async function collectAll(opts2 = {}) {
189
- const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
190
- collectCpu(),
191
- collectGpu(opts2.detailed),
192
- collectMemory(),
193
- collectDisk(),
194
- collectBattery(),
195
- collectThermal(opts2.detailed),
196
- collectNetwork(),
197
- collectProcesses(opts2.processLimit),
198
- collectSystem(),
199
- collectPower(),
200
- collectPackets(),
201
- collectTasks()
202
- ]);
203
- return {
204
- header: {
205
- title: chalk.hex("#ff5500").bold("PYRE"),
206
- hostname: system.hostname,
207
- os: system.os,
208
- uptime: system.uptime
209
- },
210
- cpu,
211
- gpu,
212
- memory,
213
- disk,
214
- battery,
215
- thermal,
216
- network,
217
- processes,
218
- power,
219
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
220
- packets,
221
- tasks
222
- };
223
- }
224
- async function collectPower() {
225
- try {
226
- const { power } = await getSmcMetrics();
227
- if (power.cpu !== void 0 || power.gpu !== void 0 || power.combined !== void 0) {
228
- return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
229
- }
230
- } catch {
231
- }
232
- try {
233
- const ioreg = (await run("ioreg -rn AppleSmartBattery 2>/dev/null")).trim();
234
- const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
235
- const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
236
- if (ampMatch && voltMatch) {
237
- let ampsBig = BigInt(ampMatch[1]);
238
- if (!ampMatch[1].startsWith("-")) {
239
- const UINT64_MAX_PLUS_1 = 1n << 64n;
240
- const INT64_MAX = (1n << 63n) - 1n;
241
- if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
242
- }
243
- const amps = Number(ampsBig);
244
- const volts = parseInt(voltMatch[1], 10) / 1e3;
245
- const watts = Math.abs(amps / 1e3 * volts);
246
- if (Number.isFinite(watts) && watts < 500) {
247
- return { combinedWatts: Math.round(watts * 100) / 100 };
248
- }
249
- }
250
- } catch {
251
- }
252
- return null;
253
- }
254
- async function collectGpu(detailed) {
255
- let spRaw = "";
256
- const now = Date.now();
257
- if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
258
- spRaw = spGpuCache.raw;
259
- } else {
260
- spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
261
- spGpuCache = { raw: spRaw, ts: now };
262
- }
263
- let model = "Unknown";
264
- let memory = 0;
265
- if (spRaw) {
266
- const modelMatch = spRaw.match(/(?:Chipset Model|Model|Device Name):\s*(.+)/i);
267
- if (modelMatch) model = modelMatch[1].trim();
268
- const memMatch = spRaw.match(/(?:VRAM \(Total\)|VRAM \(Dynamic, Max\)|Shared System Memory|Memory|VRAM):\s*([0-9.]+\s*[KMGT]?B)/i);
269
- if (memMatch) {
270
- const memStr = memMatch[1].trim();
271
- const memNum = parseFloat(memStr);
272
- if (/GB/i.test(memStr)) memory = memNum * 1024 * 1024 * 1024;
273
- else if (/MB/i.test(memStr)) memory = memNum * 1024 * 1024;
274
- else if (/KB/i.test(memStr)) memory = memNum * 1024;
275
- }
276
- } else {
277
- try {
278
- const nvRaw = (await run("nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "")).trim();
279
- if (nvRaw) {
280
- const parts = nvRaw.split(",").map((s) => s.trim());
281
- if (parts.length >= 3) {
282
- model = parts[0];
283
- memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
284
- const utilization2 = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
285
- return { model, memory, utilization: utilization2, processes: 0 };
286
- }
287
- }
288
- } catch {
289
- }
290
- return null;
291
- }
292
- if (memory === 0) {
293
- try {
294
- const memSizeStr = await cachedSysctl("hw.memsize", "0");
295
- const totalBytes = parseInt(memSizeStr, 10) || 0;
296
- if (totalBytes > 0) {
297
- memory = totalBytes;
298
- }
299
- } catch {
300
- }
301
- }
302
- let utilization = 0;
303
- let temperature;
304
- let gpuProcesses = 0;
305
- try {
306
- const ioregRaw = await run("ioreg -r -c IOAccelerator 2>/dev/null", "");
307
- if (ioregRaw) {
308
- const devUtilMatch = ioregRaw.match(/"Device Utilization %"\s*=\s*([\d.]+)/i);
309
- const gpuCoreMatch = ioregRaw.match(/"gpu-core-utilization"\s*=\s*([\d.]+)/i);
310
- const gpuActMatch = ioregRaw.match(/"GPU Activity"\s*=\s*([\d.]+)/i);
311
- const gpuBusyMatch = ioregRaw.match(/"GPU Busy"\s*=\s*([\d.]+)/i);
312
- if (devUtilMatch) {
313
- utilization = Math.min(100, Math.round(parseFloat(devUtilMatch[1])));
314
- } else if (gpuCoreMatch) {
315
- const val = parseFloat(gpuCoreMatch[1]);
316
- utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
317
- } else if (gpuActMatch) {
318
- const val = parseFloat(gpuActMatch[1]);
319
- utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
320
- } else if (gpuBusyMatch) {
321
- utilization = Math.min(100, Math.round(parseFloat(gpuBusyMatch[1])));
322
- }
323
- }
324
- } catch {
325
- }
326
- if (utilization === 0) {
327
- try {
328
- const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
329
- if (pm) {
330
- const hwRes = pm.match(/GPU HW active residency:\s*([\d.]+)%/i);
331
- const actRes = pm.match(/GPU active residency:\s*([\d.]+)%/i);
332
- const gpuUtilMatch = pm.match(/GPU utilization:\s*([\d.]+)%/i) || pm.match(/GPU usage:\s*([\d.]+)%/i);
333
- const idleRes = pm.match(/GPU idle residency:\s*([\d.]+)%/i);
334
- const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
335
- if (hwRes) {
336
- utilization = Math.min(100, Math.round(parseFloat(hwRes[1])));
337
- } else if (actRes) {
338
- utilization = Math.min(100, Math.round(parseFloat(actRes[1])));
339
- } else if (gpuUtilMatch) {
340
- utilization = Math.min(100, Math.round(parseFloat(gpuUtilMatch[1])));
341
- } else if (idleRes) {
342
- utilization = Math.max(0, Math.min(100, Math.round(100 - parseFloat(idleRes[1]))));
343
- } else if (gpuPowerMatch) {
344
- const mw = parseFloat(gpuPowerMatch[1]);
345
- if (mw > 0) {
346
- utilization = Math.min(100, Math.max(1, Math.round(mw / 50)));
347
- }
348
- }
349
- }
350
- } catch {
351
- }
352
- }
353
- try {
354
- const { temps, power } = await getSmcMetrics();
355
- if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
356
- if (utilization === 0 && power.gpu !== void 0 && power.gpu > 0) {
357
- utilization = Math.min(100, Math.max(1, Math.round(power.gpu * 5)));
358
- }
359
- } catch {
360
- }
361
- try {
362
- const psRaw = (await run('ps aux | grep -iE "[g]pu|[m]etal|[w]indowserver" | wc -l', "0")).trim();
363
- gpuProcesses = parseInt(psRaw, 10) || 0;
364
- } catch {
365
- }
366
- return { model, memory, utilization, temperature, processes: gpuProcesses };
367
- }
368
- async function collectSystem() {
369
- const hostname = (await run("hostname", "Mac")).trim();
370
- const osRaw = (await run("sw_vers -productVersion", "Unknown")).trim();
371
- const uptimeRaw = (await run("uptime")).trim();
372
- const uptimeMatch = uptimeRaw.match(/up\s+(.*?),\s+\d+\s+users/);
373
- return {
374
- hostname,
375
- os: `macOS ${osRaw}`,
376
- uptime: uptimeMatch ? uptimeMatch[1] : uptimeRaw
377
- };
378
- }
379
- async function collectCpu() {
380
- const brand = (await cachedSysctl("machdep.cpu.brand_string", "Unknown CPU")).trim();
381
- const cores = parseInt(await cachedSysctl("hw.ncpu", "1")) || 1;
382
- const physicalCores = parseInt(await cachedSysctl("hw.physicalcpu", String(cores))) || cores;
383
- const frequencyHz = parseInt(await cachedSysctl("hw.cpufrequency", "0"));
384
- let frequency = frequencyHz > 0 ? Math.round(frequencyHz / 1e6) : 0;
385
- if (frequency === 0) {
386
- try {
387
- const { freq } = await getSmcMetrics();
388
- const values = Object.values(freq);
389
- if (values.length) {
390
- frequency = Math.round(Math.max(...values) / 1e6);
391
- }
392
- } catch {
393
- }
394
- }
395
- let usage = 0;
396
- try {
397
- const top = (await run("top -l 1 -n 0")).trim();
398
- const cpuLine = top.match(/CPU usage:\s*[\d.]+%\s*user,\s*[\d.]+%\s*sys,\s*([\d.]+)%\s*idle/);
399
- if (cpuLine) {
400
- const idle = parseFloat(cpuLine[1]);
401
- usage = Math.round(100 - idle);
402
- }
403
- } catch {
404
- }
405
- const loadAvgStr = (await run("sysctl -n vm.loadavg", "0.00 0.00 0.00")).trim();
406
- const loadAvg = loadAvgStr.match(/\{ (.+?) \}/)?.[1].split(" ").map(Number) || [0, 0, 0];
407
- let temperature;
408
- try {
409
- const { temps } = await getSmcMetrics();
410
- if (temps.cpu_die !== void 0) temperature = temps.cpu_die;
411
- } catch {
412
- }
413
- let coreUsage = [];
414
- try {
415
- const cpus = os.cpus();
416
- const current = cpus.map((cpu) => ({
417
- total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
418
- idle: cpu.times.idle
419
- }));
420
- if (prevCpuTimes) {
421
- coreUsage = prevCpuTimes.map((prev, i) => {
422
- const totalDelta = current[i].total - prev.total;
423
- const idleDelta = current[i].idle - prev.idle;
424
- if (totalDelta === 0) return 0;
425
- return Math.round((totalDelta - idleDelta) / totalDelta * 100);
426
- });
427
- }
428
- prevCpuTimes = current;
429
- } catch {
430
- }
431
- return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature, coreUsage };
432
- }
433
- async function collectMemory() {
434
- const totalBytes = parseInt(await cachedSysctl("hw.memsize", "0")) || 0;
435
- const pageSize = parseInt(await cachedSysctl("hw.pagesize", "4096")) || 4096;
436
- const vmStat = (await run("vm_stat")).trim();
437
- const getPageCount = (label) => {
438
- const m = vmStat.match(new RegExp(`Pages\\s+${label}:\\s+(\\d+)\\.`));
439
- return m ? parseInt(m[1]) : 0;
440
- };
441
- const freePages = getPageCount("free");
442
- const activePages = getPageCount("active");
443
- const inactivePages = getPageCount("inactive");
444
- const wiredPages = getPageCount("wired down");
445
- const compressedPages = getPageCount("occupied by compressor");
446
- const free = freePages * pageSize;
447
- const used = (activePages + inactivePages + wiredPages + compressedPages) * pageSize;
448
- const usagePercent = totalBytes > 0 ? Math.round(used / totalBytes * 100) : 0;
449
- let swapTotal = 0;
450
- let swapUsed = 0;
451
- let swapFree = 0;
452
- try {
453
- const swapRaw = (await run("sysctl -n vm.swapusage")).trim();
454
- const swapMatch = swapRaw.match(
455
- /total\s*=\s*([\d.]+)([KMGkmg])\s+used\s*=\s*([\d.]+)([KMGkmg])\s+free\s*=\s*([\d.]+)([KMGkmg])/
456
- );
457
- if (swapMatch) {
458
- swapTotal = parseFloat(swapMatch[1]) * parseSuffix(swapMatch[2]);
459
- swapUsed = parseFloat(swapMatch[3]) * parseSuffix(swapMatch[4]);
460
- swapFree = parseFloat(swapMatch[5]) * parseSuffix(swapMatch[6]);
461
- }
462
- } catch {
463
- }
464
- return {
465
- total: totalBytes,
466
- used,
467
- free,
468
- swapTotal,
469
- swapUsed,
470
- swapFree,
471
- pageSize,
472
- usagePercent
473
- };
474
- }
475
- var cachedDiskIo = null;
476
- async function getDiskIoRates() {
477
- const now = Date.now();
478
- if (cachedDiskIo && now - cachedDiskIo.ts < 1e3) {
479
- return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
480
- }
481
- try {
482
- const raw = (await run("iostat -d -c 2 1 2", "")).trim();
483
- const blocks = raw.split(/\n\s*\n/).filter(Boolean);
484
- const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
485
- if (targetBlock) {
486
- const lines = targetBlock.trim().split("\n");
487
- if (lines.length >= 2) {
488
- const dataLine = lines[lines.length - 1].trim();
489
- const parts = dataLine.split(/\s+/);
490
- if (parts.length >= 3) {
491
- const mbSec = parseFloat(parts[2]);
492
- if (!isNaN(mbSec)) {
493
- const bytesSec = Math.round(mbSec * 1024 * 1024);
494
- cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
495
- return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
496
- }
497
- }
498
- }
499
- }
500
- } catch {
501
- }
502
- return { readBytesSec: 0, writeBytesSec: 0 };
503
- }
504
- async function collectDisk() {
505
- const [rawDf, ioRates] = await Promise.all([
506
- run("df -h"),
507
- getDiskIoRates()
508
- ]);
509
- const lines = rawDf.trim().split("\n").slice(1);
510
- return lines.map((line) => {
511
- const parts = line.split(/\s+/);
512
- return {
513
- filesystem: parts[0],
514
- size: parts[1],
515
- used: parts[2],
516
- available: parts[3],
517
- capacity: parts[4],
518
- mountpoint: parts[5] || parts[6] || "/",
519
- readBytesSec: ioRates.readBytesSec,
520
- writeBytesSec: ioRates.writeBytesSec
521
- };
522
- }).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
523
- }
524
- async function collectBattery() {
525
- try {
526
- const raw = (await run("pmset -g batt")).trim();
527
- if (!raw.includes("Battery Power") && !raw.includes("AC Power")) return null;
528
- const levelMatch = raw.match(/(\d+)%/);
529
- const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
530
- const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i) || raw.match(/(\d+:\d+)\s+remaining/i);
531
- const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
532
- const level = levelMatch ? parseInt(levelMatch[1]) : 0;
533
- const state2 = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
534
- const timeRemaining = timeMatch ? timeMatch[1] : powerPlugged ? "\u221E" : "calculating";
535
- let cycles;
536
- let condition;
537
- let maxCapacityPercent;
538
- try {
539
- const now = Date.now();
540
- let battInfo;
541
- if (spBatteryCache && now - spBatteryCache.ts < SP_TTL_MS) {
542
- battInfo = spBatteryCache.raw;
543
- } else {
544
- battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null")).trim();
545
- spBatteryCache = { raw: battInfo, ts: now };
546
- }
547
- const cycleMatch = battInfo.match(/Cycle Count:\s+(\d+)/);
548
- if (cycleMatch) cycles = parseInt(cycleMatch[1]);
549
- const conditionMatch = battInfo.match(/Condition:\s+(.+)/);
550
- if (conditionMatch) condition = conditionMatch[1].trim();
551
- const maxCapMatch = battInfo.match(/Maximum Capacity:\s+(\d+)%/);
552
- if (maxCapMatch) {
553
- maxCapacityPercent = parseInt(maxCapMatch[1]);
554
- } else {
555
- const fullMatch = battInfo.match(/Full Charge Capacity[^\d]*(\d+)/i);
556
- const designMatch = battInfo.match(/Design Capacity[^\d]*(\d+)/i);
557
- if (fullMatch && designMatch) {
558
- const full = parseInt(fullMatch[1]);
559
- const design = parseInt(designMatch[1]);
560
- if (design > 0) maxCapacityPercent = Math.round(full / design * 100);
561
- }
562
- }
563
- } catch {
564
- }
565
- const healthParts = [];
566
- if (condition) healthParts.push(condition);
567
- if (maxCapacityPercent !== void 0) healthParts.push(`${maxCapacityPercent}% capacity`);
568
- if (cycles !== void 0) healthParts.push(`${cycles} cycles`);
569
- const health = healthParts.length ? healthParts.join(", ") : "unknown";
570
- const estimate = estimateBatteryLife(level, state2);
571
- return {
572
- level,
573
- state: state2,
574
- timeRemaining,
575
- health,
576
- powerSource: powerPlugged ? "AC" : "Battery",
577
- cycles,
578
- condition,
579
- maxCapacityPercent,
580
- ...estimate
581
- };
582
- } catch {
583
- return null;
584
- }
585
- }
586
- function pressureFromState(state2) {
587
- const s = state2.toLowerCase();
588
- if (s.includes("critical")) return 3;
589
- if (s.includes("serious") || s.includes("heavy")) return 2;
590
- if (s.includes("fair") || s.includes("moderate")) return 1;
591
- return 0;
592
- }
593
- async function collectThermal(detailed) {
594
- try {
595
- const therm = (await run("pmset -g therm", "")).trim();
596
- let state2 = "Unknown";
597
- let detail = therm || "No pmset thermal data";
598
- const stateMatch = therm.match(/Thermal state:\s*(.+)/i);
599
- if (stateMatch) {
600
- const raw = stateMatch[1].trim();
601
- state2 = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
602
- detail = state2;
603
- } else {
604
- const limitMatch = therm.match(
605
- /CPU_(?:Speed|Scheduler)_Limit\s*(?:=|:)\s*([\d.]+)/i
606
- );
607
- if (limitMatch) {
608
- const limit = parseFloat(limitMatch[1]);
609
- if (limit >= 100) state2 = "Nominal";
610
- else if (limit >= 85) state2 = "Fair";
611
- else if (limit >= 50) state2 = "Serious";
612
- else state2 = "Critical";
613
- detail = `${state2} (CPU limit ${limit}%)`;
614
- } else if (/no thermal warning level has been recorded/i.test(therm)) {
615
- state2 = "Nominal";
616
- detail = "Nominal (no thermal warning recorded)";
617
- }
618
- }
619
- const temperatures = {};
620
- let cpuDieTemp;
621
- try {
622
- const { temps } = await getSmcMetrics();
623
- cpuDieTemp = temps.cpu_die;
624
- if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
625
- if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
626
- if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
627
- for (const [k, v] of Object.entries(temps)) {
628
- if (temperatures[k] === void 0 && v !== void 0) {
629
- temperatures[k] = v;
630
- }
631
- }
632
- } catch {
633
- }
634
- if (state2 === "Unknown" && cpuDieTemp !== void 0) {
635
- if (cpuDieTemp >= 100) state2 = "Critical";
636
- else if (cpuDieTemp >= 90) state2 = "Serious";
637
- else if (cpuDieTemp >= 80) state2 = "Fair";
638
- else state2 = "Nominal";
639
- detail = `${state2} (estimated from ${cpuDieTemp}\xB0C)`;
640
- }
641
- return {
642
- state: state2,
643
- detail,
644
- pressureLevel: pressureFromState(state2),
645
- temperatures: Object.keys(temperatures).length ? temperatures : void 0
646
- };
647
- } catch {
648
- return { state: "Unknown", pressureLevel: 0, error: "Thermal info unavailable on this system" };
649
- }
650
- }
651
- async function collectNetwork() {
652
- try {
653
- const { iface, ip } = await cachedRouteIface();
654
- const netStat = await cachedNetstatIb();
655
- let rxBytes = 0;
656
- let txBytes = 0;
657
- let rxPackets = 0;
658
- let txPackets = 0;
659
- for (const line of netStat.split("\n")) {
660
- if (line.startsWith(iface) || line.startsWith(iface + ":")) {
661
- const parts = line.split(/\s+/);
662
- if (parts.length >= 9) {
663
- rxPackets += parseInt(parts[4]) || 0;
664
- rxBytes += parseInt(parts[5]) || 0;
665
- txPackets += parseInt(parts[6]) || 0;
666
- txBytes += parseInt(parts[7]) || 0;
667
- }
668
- }
669
- }
670
- return { interface: iface, ip, rxBytes, txBytes, rxPackets, txPackets };
671
- } catch {
672
- return { interface: "unknown", ip: "0.0.0.0", rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0 };
673
- }
674
- }
675
- async function collectProcesses(limit) {
676
- try {
677
- const isMac = process.platform === "darwin";
678
- const sortArg = isMac ? "-r" : "--sort=-pcpu";
679
- const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : "";
680
- const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,time,comm ${sortArg}${headClause}`)).trim();
681
- const lines = raw.split("\n").slice(1);
682
- return lines.map((line) => {
683
- const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
684
- if (!parts) return null;
685
- const runtimeSec = parts[7].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
686
- return { pid: parseInt(parts[1]), ppid: parseInt(parts[2]), user: parts[3], cpu: parseFloat(parts[4]), mem: parseFloat(parts[5]), state: parts[6], threads: 0, runtime: runtimeSec, command: parts[8] };
687
- }).filter((p) => p !== null && p.pid > 0);
688
- } catch {
689
- return [];
690
- }
691
- }
692
- async function collectPackets() {
693
- try {
694
- const netStat = await cachedNetstatIb();
695
- const lines = netStat.split("\n").slice(1);
696
- let totalRxPackets = 0;
697
- let totalTxPackets = 0;
698
- let totalRxBytes = 0;
699
- let totalTxBytes = 0;
700
- const ifaceStats = [];
701
- for (const line of lines) {
702
- const parts = line.split(/\s+/);
703
- if (parts.length >= 11 && parts[0] && !parts[0].startsWith("Name")) {
704
- const iface = parts[0];
705
- const rxPackets = parseInt(parts[4]) || 0;
706
- const rxBytes = parseInt(parts[5]) || 0;
707
- const txPackets = parseInt(parts[6]) || 0;
708
- const txBytes = parseInt(parts[7]) || 0;
709
- totalRxPackets += rxPackets;
710
- totalTxPackets += txPackets;
711
- totalRxBytes += rxBytes;
712
- totalTxBytes += txBytes;
713
- if (iface && !iface.includes("lo")) {
714
- ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
715
- }
716
- }
717
- }
718
- let connections = 0;
719
- try {
720
- const tcpConns = (await run("netstat -an | grep ESTABLISHED | wc -l", "0")).trim();
721
- connections = parseInt(tcpConns) || 0;
722
- } catch {
723
- connections = 0;
724
- }
725
- let topProcesses = [];
726
- let allProcesses = [];
727
- try {
728
- const lsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
729
- const lsofLines = lsof.split("\n");
730
- topProcesses = lsofLines.map((line) => {
731
- const parts = line.trim().split(/\s+/);
732
- if (parts.length >= 3) {
733
- return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
734
- }
735
- return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
736
- }).filter((p) => p.pid > 0);
737
- allProcesses = topProcesses;
738
- } catch {
739
- topProcesses = [];
740
- allProcesses = [];
741
- }
742
- return {
743
- totalPackets: totalRxPackets + totalTxPackets,
744
- rxPackets: totalRxPackets,
745
- txPackets: totalTxPackets,
746
- rxRate: 0,
747
- txRate: 0,
748
- connections,
749
- topProcesses,
750
- interfaces: ifaceStats,
751
- allProcesses
752
- };
753
- } catch {
754
- return null;
755
- }
756
- }
757
- async function collectTasks(limit = 12) {
758
- try {
759
- const isMac = process.platform === "darwin";
760
- const sortArg = isMac ? "-r" : "--sort=-pcpu";
761
- const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
762
- const lines = raw.split("\n").slice(1);
763
- return lines.map((line) => {
764
- const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
765
- if (!parts) return null;
766
- const runtimeSec = parts[6].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
767
- return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
768
- }).filter((t) => t !== null && t.pid > 0);
769
- } catch {
770
- return [];
771
- }
772
- }
18
+ import chalk12 from "chalk";
773
19
 
774
20
  // src/formatters/render.ts
775
- import chalk3 from "chalk";
21
+ import chalk2 from "chalk";
776
22
 
777
23
  // src/sparkline.ts
778
24
  var BLOCKS = " \u2582\u2583\u2584\u2585\u2586\u2587\u2588";
779
25
  function sparkline(values, opts2 = {}) {
780
26
  if (!values.length) return "";
27
+ if (opts2.plainText) {
28
+ const first = values[0];
29
+ const last = values[values.length - 1];
30
+ const diff = last - first;
31
+ const sign = diff >= 0 ? "+" : "";
32
+ return `[${first.toFixed(1)} -> ${last.toFixed(1)} (\u0394:${sign}${diff.toFixed(1)})]`;
33
+ }
781
34
  let min = opts2.min ?? Infinity;
782
35
  let max = opts2.max ?? -Infinity;
783
36
  if (opts2.min === void 0 || opts2.max === void 0) {
@@ -793,7 +46,7 @@ function sparkline(values, opts2 = {}) {
793
46
  return BLOCKS[clamped];
794
47
  }).join("");
795
48
  }
796
- function multiRowSparkline(values, height, opts2 = {}) {
49
+ function multiRowBrailleGraph(values, height, opts2 = {}) {
797
50
  if (!values.length || height <= 0) return Array(height).fill("");
798
51
  let min = opts2.min ?? Infinity;
799
52
  let max = opts2.max ?? -Infinity;
@@ -804,20 +57,26 @@ function multiRowSparkline(values, height, opts2 = {}) {
804
57
  }
805
58
  }
806
59
  const range = max - min || 1;
807
- const levels = height * 8;
60
+ const levels = height * 4;
61
+ const dotsCol0 = [64, 4, 2, 1];
62
+ const dotsCol1 = [128, 32, 16, 8];
808
63
  const rows = Array.from({ length: height }, () => []);
809
- for (const v of values) {
810
- const rawSteps = Math.round((v - min) / range * levels);
811
- const totalSteps = Math.min(levels, Math.max(0, rawSteps));
64
+ for (let i = 0; i < values.length; i += 2) {
65
+ const v0 = values[i];
66
+ const v1 = i + 1 < values.length ? values[i + 1] : v0;
67
+ const rawSteps0 = Math.round((v0 - min) / range * levels);
68
+ const totalSteps0 = Math.min(levels, Math.max(0, rawSteps0));
69
+ const rawSteps1 = Math.round((v1 - min) / range * levels);
70
+ const totalSteps1 = Math.min(levels, Math.max(0, rawSteps1));
812
71
  for (let r = 0; r < height; r++) {
813
72
  const rowFromBottom = height - 1 - r;
814
- const minForThisRow = rowFromBottom * 8;
815
- const stepsInThisRow = Math.min(8, Math.max(0, totalSteps - minForThisRow));
816
- if (stepsInThisRow === 0) {
817
- rows[r].push(" ");
818
- } else {
819
- rows[r].push(BLOCKS[stepsInThisRow]);
820
- }
73
+ const minForThisRow = rowFromBottom * 4;
74
+ const stepsInThisRow0 = Math.min(4, Math.max(0, totalSteps0 - minForThisRow));
75
+ const stepsInThisRow1 = Math.min(4, Math.max(0, totalSteps1 - minForThisRow));
76
+ let code = 10240;
77
+ for (let d = 0; d < stepsInThisRow0; d++) code |= dotsCol0[d];
78
+ for (let d = 0; d < stepsInThisRow1; d++) code |= dotsCol1[d];
79
+ rows[r].push(String.fromCharCode(code));
821
80
  }
822
81
  }
823
82
  return rows.map((r) => r.join(""));
@@ -850,93 +109,93 @@ function brailleGraph(values, opts2 = {}) {
850
109
  }
851
110
 
852
111
  // src/formatters/themes.ts
853
- import chalk2 from "chalk";
112
+ import chalk from "chalk";
854
113
  import fs from "fs";
855
- import os2 from "os";
114
+ import os from "os";
856
115
  import path from "path";
857
116
  var BUILTIN_THEMES = {
858
117
  default: {
859
- border: chalk2.hex("#4b5563"),
860
- cpu: chalk2.hex("#22d3ee"),
861
- mem: chalk2.hex("#a78bfa"),
862
- gpu: chalk2.hex("#f472b6"),
863
- power: chalk2.hex("#fbbf24"),
864
- battery: chalk2.hex("#34d399"),
865
- thermal: chalk2.hex("#fb923c"),
866
- network: chalk2.hex("#60a5fa"),
867
- disk: chalk2.hex("#f472b6"),
868
- graphs: chalk2.hex("#22d3ee"),
869
- process: chalk2.hex("#e5e7eb")
118
+ border: chalk.hex("#4b5563"),
119
+ cpu: chalk.hex("#22d3ee"),
120
+ mem: chalk.hex("#a78bfa"),
121
+ gpu: chalk.hex("#f472b6"),
122
+ power: chalk.hex("#fbbf24"),
123
+ battery: chalk.hex("#34d399"),
124
+ thermal: chalk.hex("#fb923c"),
125
+ network: chalk.hex("#60a5fa"),
126
+ disk: chalk.hex("#f472b6"),
127
+ graphs: chalk.hex("#22d3ee"),
128
+ process: chalk.hex("#e5e7eb")
870
129
  },
871
130
  dracula: {
872
- border: chalk2.hex("#6272a4"),
873
- cpu: chalk2.hex("#8be9fd"),
874
- mem: chalk2.hex("#bd93f9"),
875
- gpu: chalk2.hex("#ff5555"),
876
- power: chalk2.hex("#f1fa8c"),
877
- battery: chalk2.hex("#50fa7b"),
878
- thermal: chalk2.hex("#ffb86c"),
879
- network: chalk2.hex("#ff79c6"),
880
- disk: chalk2.hex("#ff5555"),
881
- graphs: chalk2.hex("#8be9fd"),
882
- process: chalk2.hex("#f8f8f2")
131
+ border: chalk.hex("#6272a4"),
132
+ cpu: chalk.hex("#8be9fd"),
133
+ mem: chalk.hex("#bd93f9"),
134
+ gpu: chalk.hex("#ff5555"),
135
+ power: chalk.hex("#f1fa8c"),
136
+ battery: chalk.hex("#50fa7b"),
137
+ thermal: chalk.hex("#ffb86c"),
138
+ network: chalk.hex("#ff79c6"),
139
+ disk: chalk.hex("#ff5555"),
140
+ graphs: chalk.hex("#8be9fd"),
141
+ process: chalk.hex("#f8f8f2")
883
142
  },
884
143
  cyberpunk: {
885
- border: chalk2.hex("#ff0055"),
886
- cpu: chalk2.hex("#00ffcc"),
887
- mem: chalk2.hex("#ff00ff"),
888
- gpu: chalk2.hex("#ffff00"),
889
- power: chalk2.hex("#ffff00"),
890
- battery: chalk2.hex("#00ff00"),
891
- thermal: chalk2.hex("#ff6600"),
892
- network: chalk2.hex("#00ffff"),
893
- disk: chalk2.hex("#ff007f"),
894
- graphs: chalk2.hex("#00ffcc"),
895
- process: chalk2.hex("#ffffff")
144
+ border: chalk.hex("#ff0055"),
145
+ cpu: chalk.hex("#00ffcc"),
146
+ mem: chalk.hex("#ff00ff"),
147
+ gpu: chalk.hex("#ffff00"),
148
+ power: chalk.hex("#ffff00"),
149
+ battery: chalk.hex("#00ff00"),
150
+ thermal: chalk.hex("#ff6600"),
151
+ network: chalk.hex("#00ffff"),
152
+ disk: chalk.hex("#ff007f"),
153
+ graphs: chalk.hex("#00ffcc"),
154
+ process: chalk.hex("#ffffff")
896
155
  },
897
156
  monochrome: {
898
- border: chalk2.gray,
899
- cpu: chalk2.white.bold,
900
- mem: chalk2.white.bold,
901
- gpu: chalk2.white.bold,
902
- power: chalk2.white.bold,
903
- battery: chalk2.white.bold,
904
- thermal: chalk2.white.bold,
905
- network: chalk2.white.bold,
906
- disk: chalk2.white.bold,
907
- graphs: chalk2.white.bold,
908
- process: chalk2.white.bold
157
+ border: chalk.gray,
158
+ cpu: chalk.white.bold,
159
+ mem: chalk.white.bold,
160
+ gpu: chalk.white.bold,
161
+ power: chalk.white.bold,
162
+ battery: chalk.white.bold,
163
+ thermal: chalk.white.bold,
164
+ network: chalk.white.bold,
165
+ disk: chalk.white.bold,
166
+ graphs: chalk.white.bold,
167
+ process: chalk.white.bold
909
168
  },
910
169
  nord: {
911
- border: chalk2.hex("#4c566a"),
912
- cpu: chalk2.hex("#88c0d0"),
913
- mem: chalk2.hex("#b48ead"),
914
- gpu: chalk2.hex("#bf616a"),
915
- power: chalk2.hex("#ebcb8b"),
916
- battery: chalk2.hex("#a3be8c"),
917
- thermal: chalk2.hex("#d08770"),
918
- network: chalk2.hex("#81a1c1"),
919
- disk: chalk2.hex("#b48ead"),
920
- graphs: chalk2.hex("#88c0d0"),
921
- process: chalk2.hex("#eceff4")
170
+ border: chalk.hex("#4c566a"),
171
+ cpu: chalk.hex("#88c0d0"),
172
+ mem: chalk.hex("#b48ead"),
173
+ gpu: chalk.hex("#bf616a"),
174
+ power: chalk.hex("#ebcb8b"),
175
+ battery: chalk.hex("#a3be8c"),
176
+ thermal: chalk.hex("#d08770"),
177
+ network: chalk.hex("#81a1c1"),
178
+ disk: chalk.hex("#b48ead"),
179
+ graphs: chalk.hex("#88c0d0"),
180
+ process: chalk.hex("#eceff4")
922
181
  },
923
182
  gruvbox: {
924
- border: chalk2.hex("#928374"),
925
- cpu: chalk2.hex("#fb4934"),
926
- mem: chalk2.hex("#b8bb26"),
927
- gpu: chalk2.hex("#d3869b"),
928
- power: chalk2.hex("#fabd2f"),
929
- battery: chalk2.hex("#b8bb26"),
930
- thermal: chalk2.hex("#fb4934"),
931
- network: chalk2.hex("#83a598"),
932
- disk: chalk2.hex("#d3869b"),
933
- graphs: chalk2.hex("#fb4934"),
934
- process: chalk2.hex("#ebdbb2")
183
+ border: chalk.hex("#928374"),
184
+ cpu: chalk.hex("#fb4934"),
185
+ mem: chalk.hex("#b8bb26"),
186
+ gpu: chalk.hex("#d3869b"),
187
+ power: chalk.hex("#fabd2f"),
188
+ battery: chalk.hex("#b8bb26"),
189
+ thermal: chalk.hex("#fb4934"),
190
+ network: chalk.hex("#83a598"),
191
+ disk: chalk.hex("#d3869b"),
192
+ graphs: chalk.hex("#fb4934"),
193
+ process: chalk.hex("#ebdbb2")
935
194
  }
936
195
  };
937
196
  function loadCustomThemes() {
938
197
  const themes = { ...BUILTIN_THEMES };
939
- const themesDir = path.join(os2.homedir(), ".config", "pyre", "themes");
198
+ const themesDir = path.join(os.homedir(), ".config", "pyre", "themes");
940
199
  try {
941
200
  if (fs.existsSync(themesDir)) {
942
201
  const files = fs.readdirSync(themesDir);
@@ -946,7 +205,7 @@ function loadCustomThemes() {
946
205
  try {
947
206
  const raw = fs.readFileSync(path.join(themesDir, file), "utf-8");
948
207
  const data = JSON.parse(raw);
949
- const buildColor = (val) => val ? val.startsWith("#") ? chalk2.hex(val) : chalk2[val] || chalk2.white : chalk2.white;
208
+ const buildColor = (val) => val ? val.startsWith("#") ? chalk.hex(val) : chalk[val] || chalk.white : chalk.white;
950
209
  themes[name] = {
951
210
  border: buildColor(data.border),
952
211
  cpu: buildColor(data.cpu),
@@ -1046,8 +305,8 @@ function gaugeBar(percent, width = 12) {
1046
305
  str += blocks[remainder];
1047
306
  }
1048
307
  const empty = Math.max(0, w - visLen(str));
1049
- const color = pct > 85 ? chalk3.red : pct > 65 ? chalk3.yellow : chalk3.cyan;
1050
- return color(str) + chalk3.dim("\u2500".repeat(empty));
308
+ const color = pct > 85 ? chalk2.red : pct > 65 ? chalk2.yellow : chalk2.cyan;
309
+ return color(str) + chalk2.dim("\u2500".repeat(empty));
1051
310
  }
1052
311
  function hstack(blocks, gap = 2, targetWidth) {
1053
312
  if (!blocks.length) return [];
@@ -1072,7 +331,7 @@ function hstack(blocks, gap = 2, targetWidth) {
1072
331
  }
1073
332
  function statRow(label, value, width = 9) {
1074
333
  const w = Math.max(width, label.length + 1);
1075
- return `${chalk3.dim(label.padEnd(w))}${value}`;
334
+ return `${chalk2.dim(label.padEnd(w))}${value}`;
1076
335
  }
1077
336
  function gaugeRow(label, percent, contentWidth) {
1078
337
  const labelW = 9;
@@ -1088,19 +347,19 @@ function gaugeRow(label, percent, contentWidth) {
1088
347
  str += blocks[remainder];
1089
348
  }
1090
349
  const empty = Math.max(0, barW - visLen(str));
1091
- const color = pct > 90 ? chalk3.red : pct > 70 ? chalk3.yellow : chalk3.green;
1092
- return `${chalk3.dim(label.padEnd(labelW))}${color(str)}${chalk3.dim("\u2500".repeat(empty))} ${pctText}`;
350
+ const color = pct > 90 ? chalk2.red : pct > 70 ? chalk2.yellow : chalk2.green;
351
+ return `${chalk2.dim(label.padEnd(labelW))}${color(str)}${chalk2.dim("\u2500".repeat(empty))} ${pctText}`;
1093
352
  }
1094
353
  function thermalColor(pressureLevel) {
1095
- if (pressureLevel >= 3) return chalk3.red;
1096
- if (pressureLevel === 2) return chalk3.yellow;
1097
- if (pressureLevel === 1) return chalk3.yellowBright;
1098
- return chalk3.green;
354
+ if (pressureLevel >= 3) return chalk2.red;
355
+ if (pressureLevel === 2) return chalk2.yellow;
356
+ if (pressureLevel === 1) return chalk2.yellowBright;
357
+ return chalk2.green;
1099
358
  }
1100
359
  function capacityColor(percent) {
1101
- if (percent < 80) return chalk3.red;
1102
- if (percent < 90) return chalk3.yellow;
1103
- return chalk3.green;
360
+ if (percent < 80) return chalk2.red;
361
+ if (percent < 90) return chalk2.yellow;
362
+ return chalk2.green;
1104
363
  }
1105
364
  function formatBytes(bytes) {
1106
365
  if (isNaN(bytes) || bytes < 0) return "0 B";
@@ -1136,7 +395,7 @@ function cpuCard(data, contentWidth) {
1136
395
  const bars = cores.map((u) => {
1137
396
  const fill = Math.round(Math.min(100, u) / 100 * slotW);
1138
397
  const str = "\u2588".repeat(Math.max(0, fill)) + "\u2591".repeat(slotW - fill);
1139
- const color = u > 90 ? chalk3.red : u > 70 ? chalk3.yellow : chalk3.green;
398
+ const color = u > 90 ? chalk2.red : u > 70 ? chalk2.yellow : chalk2.green;
1140
399
  return color(str);
1141
400
  });
1142
401
  lines.push(statRow("Cores", bars.join(" ")));
@@ -1227,8 +486,8 @@ function tasksCard(data, contentWidth) {
1227
486
  }
1228
487
  function header(data, width, badges = [], borderAccent = THEMES.default.border) {
1229
488
  const now = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1230
- const left = `${chalk3.hex("#ff5500").bold(" PYRE ")} ${chalk3.bold(data.header.hostname)} ${chalk3.dim(data.header.os)} ${chalk3.dim("up " + data.header.uptime)}`;
1231
- const right = `${chalk3.dim(now)} ${badges.join(" ")}`;
489
+ const left = `${chalk2.hex("#ff5500").bold(" PYRE ")} ${chalk2.bold(data.header.hostname)} ${chalk2.dim(data.header.os)} ${chalk2.dim("up " + data.header.uptime)}`;
490
+ const right = `${chalk2.dim(now)} ${badges.join(" ")}`;
1232
491
  const gap = Math.max(1, width - visLen(left) - visLen(right));
1233
492
  const line = fitVisible(left + " ".repeat(gap) + right, width);
1234
493
  return [line, borderAccent("\u2500".repeat(width))];
@@ -1267,11 +526,11 @@ var TAB_BAR_ROW = 4;
1267
526
  function tabBar(activePanel, width, theme) {
1268
527
  const rendered = TAB_DEFS.map((t) => {
1269
528
  const isActive = activePanel === t.id;
1270
- const label = isActive ? chalk3.bold(t.label) : chalk3.dim(t.label);
1271
- const keyHint = chalk3.dim(`[${t.key}]`);
529
+ const label = isActive ? chalk2.bold(t.label) : chalk2.dim(t.label);
530
+ const keyHint = chalk2.dim(`[${t.key}]`);
1272
531
  return `${keyHint}${label}`;
1273
532
  });
1274
- const separator = chalk3.dim(" \u2502 ");
533
+ const separator = chalk2.dim(" \u2502 ");
1275
534
  const joined = rendered.join(separator);
1276
535
  const indent = " ";
1277
536
  const available = width - visLen(indent);
@@ -1286,11 +545,11 @@ function tabBar(activePanel, width, theme) {
1286
545
  used += tabW;
1287
546
  }
1288
547
  if (kept.length < rendered.length) {
1289
- result = kept.join(separator) + chalk3.dim("\u22EF");
548
+ result = kept.join(separator) + chalk2.dim("\u22EF");
1290
549
  }
1291
550
  }
1292
551
  const pad = Math.max(0, available - visLen(result));
1293
- return fitVisible(chalk3.dim(indent) + result + " ".repeat(pad), width);
552
+ return fitVisible(chalk2.dim(indent) + result + " ".repeat(pad), width);
1294
553
  }
1295
554
  function sortProcesses(processes, sortBy) {
1296
555
  const copy = [...processes];
@@ -1314,16 +573,16 @@ function sortProcesses(processes, sortBy) {
1314
573
  }
1315
574
  }
1316
575
  function pctColor(v) {
1317
- return v > 80 ? chalk3.red : v > 50 ? chalk3.yellow : chalk3.dim;
576
+ return v > 80 ? chalk2.red : v > 50 ? chalk2.yellow : chalk2.dim;
1318
577
  }
1319
578
  function processTableLines(processes, contentWidth, borderAccent = THEMES.default.border) {
1320
579
  const pidW = 8, ppidW = 8, userW = 12, cpuW = 8, memW = 8, stateW = 8, thrW = 6, rtW = 10;
1321
580
  const cmdW = Math.max(10, contentWidth - pidW - ppidW - userW - cpuW - memW - stateW - thrW - rtW - 8);
1322
- 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));
581
+ const head = chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" PID ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" PPID ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" USER ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" CPU% ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" MEM% ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" STATE ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" THREADS") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" RUNTIME ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" COMMAND".padEnd(cmdW + 1));
1323
582
  const rows = processes.map((p) => {
1324
583
  const cpuStr = pctColor(p.cpu)(`${p.cpu.toFixed(1)}`.padEnd(cpuW));
1325
584
  const memStr = pctColor(p.mem)(`${p.mem.toFixed(1)}`.padEnd(memW));
1326
- const stateStr = chalk3.dim(p.state.padEnd(stateW));
585
+ const stateStr = chalk2.dim(p.state.padEnd(stateW));
1327
586
  const thrStr = String(p.threads).padEnd(thrW);
1328
587
  const rtStr = formatRuntime(p.runtime).padEnd(rtW);
1329
588
  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);
@@ -1362,7 +621,7 @@ function buildProcessTree(processes) {
1362
621
  const pidStr = String(proc.pid).padEnd(8);
1363
622
  const cpuStr = pctColor(proc.cpu)(`${proc.cpu.toFixed(1)}%`.padEnd(8));
1364
623
  const memStr = pctColor(proc.mem)(`${proc.mem.toFixed(1)}%`.padEnd(8));
1365
- const stateStr = chalk3.dim(proc.state.padEnd(8));
624
+ const stateStr = chalk2.dim(proc.state.padEnd(8));
1366
625
  const thrStr = String(proc.threads).padEnd(6);
1367
626
  const cmd = truncatePlain(proc.command, Math.max(10, 100 - pidStr.length - cpuStr.length - memStr.length - stateStr.length - thrStr.length - 10));
1368
627
  lines.push(`${prefix}${connector}${pidStr}${cpuStr}${memStr}${stateStr}${thrStr} ${cmd}`);
@@ -1387,7 +646,7 @@ function buildProcessTree(processes) {
1387
646
  function diskTableLines(disks, contentWidth, borderAccent = THEMES.default.border) {
1388
647
  const fsW = 20, sizeW = 10, usedW = 10, availW = 10, capW = 8;
1389
648
  const mountW = Math.max(8, contentWidth - fsW - sizeW - usedW - availW - capW - 5);
1390
- 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));
649
+ const head = chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" FILESYSTEM ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" SIZE ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" USED ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" AVAIL ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" CAP ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" MOUNT".padEnd(mountW + 1));
1391
650
  const rows = disks.map((d) => {
1392
651
  const capNum = parseInt(d.capacity, 10) || 0;
1393
652
  const capStr = pctColor(capNum)(d.capacity.padEnd(capW));
@@ -1479,7 +738,7 @@ function activeDetailLines(data, activePanel, width, opts2 = {}) {
1479
738
  const lines = [];
1480
739
  const sorted = [...opts2.anomalyHistory].sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0));
1481
740
  for (const a of sorted) {
1482
- const color = a.severity === "critical" ? chalk3.red.bold : chalk3.yellow;
741
+ const color = a.severity === "critical" ? chalk2.red.bold : chalk2.yellow;
1483
742
  const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
1484
743
  const valStr = a.metric.toLowerCase().includes("net") ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
1485
744
  const timeStr = a.timestamp ? a.timestamp.toLocaleTimeString() : "";
@@ -1494,7 +753,7 @@ function activeDetailLines(data, activePanel, width, opts2 = {}) {
1494
753
  function anomalyPanel(anomalies, width, theme) {
1495
754
  const lines = [];
1496
755
  for (const a of anomalies) {
1497
- const color = a.severity === "critical" ? chalk3.red.bold : chalk3.yellow;
756
+ const color = a.severity === "critical" ? chalk2.red.bold : chalk2.yellow;
1498
757
  const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
1499
758
  const valStr = a.metric.toLowerCase().includes("net") ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
1500
759
  lines.push(color(` \u26A0 ${a.metric}: ${valStr} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
@@ -1511,7 +770,7 @@ function btopCpuBox(data, width, theme, opts2) {
1511
770
  leftLines.push(fitVisible(theme.cpu("CPU Usage History"), leftW));
1512
771
  const cpuHistory = opts2.history?.cpuUsage || [];
1513
772
  if (cpuHistory.length > 1) {
1514
- const graphRows = multiRowSparkline(cpuHistory, 3, { min: 0, max: 100 });
773
+ const graphRows = multiRowBrailleGraph(cpuHistory, 3, { min: 0, max: 100 });
1515
774
  for (const gr of graphRows) {
1516
775
  leftLines.push(theme.cpu(fitVisible(gr, leftW)));
1517
776
  }
@@ -1521,12 +780,12 @@ function btopCpuBox(data, width, theme, opts2) {
1521
780
  leftLines.push(" ".repeat(leftW));
1522
781
  }
1523
782
  const rightLines = [];
1524
- rightLines.push(fitVisible(chalk3.bold(truncatePlain(data.cpu.brand, rightW)), rightW));
783
+ rightLines.push(fitVisible(chalk2.bold(truncatePlain(data.cpu.brand, rightW)), rightW));
1525
784
  const cpuBarW = Math.max(6, rightW - 18);
1526
785
  const tempStr = data.cpu.temperature ? formatTempUnit(data.cpu.temperature, opts2.tempUnit) : "";
1527
786
  rightLines.push(fitVisible(`CPU ${gaugeBar(data.cpu.usage, cpuBarW)} ${data.cpu.usage.toFixed(0).padStart(3)}% ${tempStr}`, rightW));
1528
787
  const loadStr = data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ");
1529
- rightLines.push(fitVisible(`${chalk3.dim("Load:")} ${loadStr}`, rightW));
788
+ rightLines.push(fitVisible(`${chalk2.dim("Load:")} ${loadStr}`, rightW));
1530
789
  if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
1531
790
  const coreColW = Math.floor((rightW - 1) / 2);
1532
791
  const half = Math.ceil(data.cpu.coreUsage.length / 2);
@@ -1546,7 +805,7 @@ function btopCpuBox(data, width, theme, opts2) {
1546
805
  return panel("1 cpu", combinedBody, width, theme.cpu, theme.border, combinedBody.length, rightTitle);
1547
806
  } else {
1548
807
  const lines = [];
1549
- lines.push(chalk3.bold(truncatePlain(data.cpu.brand, contentWidth)));
808
+ lines.push(chalk2.bold(truncatePlain(data.cpu.brand, contentWidth)));
1550
809
  lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
1551
810
  if (opts2.history?.cpuUsage.length) {
1552
811
  const gMode = opts2.graphMode || "spark";
@@ -1597,7 +856,7 @@ function btopLeftColumn(data, width, theme, opts2) {
1597
856
  if (opts2.history?.netRxRate.length) {
1598
857
  const gMode = opts2.graphMode || "spark";
1599
858
  const gStr = gMode === "bar" ? sparkline(opts2.history.netRxRate, {}) : brailleGraph(opts2.history.netRxRate, {});
1600
- netLines.push(chalk3.cyan(fitVisible(gStr, contentWidth)));
859
+ netLines.push(chalk2.cyan(fitVisible(gStr, contentWidth)));
1601
860
  }
1602
861
  netLines.push(`up \u25B2 ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
1603
862
  if (opts2.history?.netTxRate.length) {
@@ -1621,7 +880,7 @@ function btopProcBox(data, width, targetHeight, theme, opts2) {
1621
880
  const pidW = 6, userW = 7, thrW = 3, memW = 6, cpuW = 6;
1622
881
  const fixedW = pidW + userW + thrW + memW + cpuW + 5;
1623
882
  const cmdW = Math.max(8, contentWidth - fixedW);
1624
- const head = chalk3.bold("PID".padEnd(pidW)) + " " + chalk3.bold("PROGRAM".padEnd(cmdW)) + " " + chalk3.bold("USER".padEnd(userW)) + " " + chalk3.bold("THR".padStart(thrW)) + " " + chalk3.bold("MEM%".padStart(memW)) + " " + chalk3.bold("CPU%".padStart(cpuW));
883
+ const head = chalk2.bold("PID".padEnd(pidW)) + " " + chalk2.bold("PROGRAM".padEnd(cmdW)) + " " + chalk2.bold("USER".padEnd(userW)) + " " + chalk2.bold("THR".padStart(thrW)) + " " + chalk2.bold("MEM%".padStart(memW)) + " " + chalk2.bold("CPU%".padStart(cpuW));
1625
884
  if (opts2.treeView) {
1626
885
  const treeLines = buildProcessTree(limited);
1627
886
  const rightTitle2 = opts2.filter ? `filter: "${opts2.filter}"` : `tree \xB7 sort: ${opts2.sortBy || "cpu"}`;
@@ -1629,7 +888,7 @@ function btopProcBox(data, width, targetHeight, theme, opts2) {
1629
888
  }
1630
889
  const rows = limited.map((p) => {
1631
890
  const cpuVal = p.cpu.toFixed(1) + "%";
1632
- const cpuStr = p.cpu > 50 ? chalk3.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk3.yellow(cpuVal.padStart(cpuW)) : chalk3.green(cpuVal.padStart(cpuW));
891
+ const cpuStr = p.cpu > 50 ? chalk2.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk2.yellow(cpuVal.padStart(cpuW)) : chalk2.green(cpuVal.padStart(cpuW));
1633
892
  const memStr = (p.mem.toFixed(1) + "%").padStart(memW);
1634
893
  const userStr = truncatePlain(p.user, userW).padEnd(userW);
1635
894
  const thrStr = String(p.threads).padStart(thrW);
@@ -1639,7 +898,7 @@ function btopProcBox(data, width, targetHeight, theme, opts2) {
1639
898
  });
1640
899
  const lines = [head, theme.border("\u2500".repeat(contentWidth)), ...rows];
1641
900
  if (rows.length === 0) {
1642
- lines.push(chalk3.dim(`No processes match "${opts2.filter || ""}"`));
901
+ lines.push(chalk2.dim(`No processes match "${opts2.filter || ""}"`));
1643
902
  }
1644
903
  const rightTitle = opts2.filter ? `filter: "${opts2.filter}"` : `sort: ${opts2.sortBy || "cpu"}`;
1645
904
  return panel("P proc", lines, width, theme.process, theme.border, targetHeight - 2, rightTitle);
@@ -1689,7 +948,7 @@ function clampWidth(width) {
1689
948
  }
1690
949
 
1691
950
  // src/formatters/output.ts
1692
- import chalk4 from "chalk";
951
+ import chalk3 from "chalk";
1693
952
  function formatJson(data) {
1694
953
  return JSON.stringify(data, null, 2);
1695
954
  }
@@ -2202,17 +1461,17 @@ function formatGraphs(history, width = 80, themeName = "default", graphMode = "s
2202
1461
  }
2203
1462
  function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark", contentWidth = 76) {
2204
1463
  if (!values.length) {
2205
- return `${chalk4.dim(label.padEnd(10))} ${chalk4.dim(emptyMessage)}`;
1464
+ return `${chalk3.dim(label.padEnd(10))} ${chalk3.dim(emptyMessage)}`;
2206
1465
  }
2207
1466
  const visible = values.slice(-sparkWidth);
2208
1467
  const spark = mode === "spark" ? sparkline(visible, bounds) : barGraph(visible, bounds, sparkWidth);
2209
1468
  const current = values[values.length - 1];
2210
- let color = chalk4.cyan;
1469
+ let color = chalk3.cyan;
2211
1470
  if (bounds.min !== void 0 && bounds.max !== void 0) {
2212
1471
  const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
2213
- color = pct > 90 ? chalk4.red : pct > 70 ? chalk4.yellow : chalk4.green;
1472
+ color = pct > 90 ? chalk3.red : pct > 70 ? chalk3.yellow : chalk3.green;
2214
1473
  }
2215
- const raw = `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
1474
+ const raw = `${chalk3.dim(label.padEnd(10))}${color(spark)} ${chalk3.bold(fmt(current))}`;
2216
1475
  return fitVisible(raw, contentWidth);
2217
1476
  }
2218
1477
  function barGraph(values, bounds, width) {
@@ -2235,6 +1494,663 @@ function barGraph(values, bounds, width) {
2235
1494
  }).join("");
2236
1495
  }
2237
1496
 
1497
+ // src/formatters/html_template.ts
1498
+ function getDashboardHtml() {
1499
+ return `<!DOCTYPE html>
1500
+ <html lang="en">
1501
+ <head>
1502
+ <meta charset="UTF-8"/>
1503
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
1504
+ <title>Pyre</title>
1505
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
1506
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
1507
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"/>
1508
+ <style>
1509
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
1510
+ :root{
1511
+ --bg: #050508;
1512
+ --bg1: #0a0c16;
1513
+ --bg2: #111427;
1514
+ --bg3: rgba(255,255,255,0.05);
1515
+ --border: #2a2d42;
1516
+ --border2: #3a3f5a;
1517
+ --text: #d4d8f0;
1518
+ --text2: #8891b4;
1519
+ --text3: #505878;
1520
+ --accent: #6c8fff;
1521
+ --accent2: #4a6ee8;
1522
+ --cyan: #3edcff;
1523
+ --green: #3effa0;
1524
+ --yellow: #ffca3e;
1525
+ --red: #ff4d6a;
1526
+ --orange: #ff8c4a;
1527
+ --purple: #b06cff;
1528
+ --glow: rgba(108,143,255,0.15);
1529
+ --font: 'Inter', -apple-system, sans-serif;
1530
+ --mono: 'JetBrains Mono', ui-monospace, monospace;
1531
+ }
1532
+ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:var(--font);font-size:12px;}
1533
+ body {
1534
+ background-image: radial-gradient(var(--bg3) 1px, transparent 1px);
1535
+ background-size: 20px 20px;
1536
+ display:flex;flex-direction:column;height:100vh;
1537
+ }
1538
+
1539
+ #titlebar{
1540
+ height:42px;min-height:42px;-webkit-app-region:drag;
1541
+ display:flex;align-items:center;padding:0 16px;
1542
+ border-bottom:1px solid var(--border);
1543
+ background:rgba(5,5,8,0.8);backdrop-filter:blur(10px);
1544
+ z-index:100;
1545
+ }
1546
+ #titlebar-logo{
1547
+ font-family:var(--mono);font-size:14px;font-weight:700;
1548
+ color:var(--cyan);margin-right:24px;text-transform:uppercase;
1549
+ letter-spacing:0.1em;
1550
+ }
1551
+ #titlebar-host{font-family:var(--mono);font-size:11px;color:var(--text2);flex:1;}
1552
+ #conn-badge{display:flex;align-items:center;gap:6px;font-size:11px;font-family:var(--mono);color:var(--text2);-webkit-app-region:no-drag;}
1553
+ .conn-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 2s infinite;}
1554
+ .conn-dot.offline{background:var(--red);box-shadow:0 0 8px var(--red);animation:none;}
1555
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
1556
+
1557
+ #tabbar{
1558
+ display:flex;align-items:center;gap:4px;padding:6px 14px 0;
1559
+ border-bottom:1px solid var(--border);
1560
+ background:rgba(10,12,22,0.8);overflow-x:auto;-webkit-app-region:no-drag;
1561
+ }
1562
+ #tabbar::-webkit-scrollbar{display:none}
1563
+ .tab{
1564
+ padding:8px 16px;font-size:11px;font-family:var(--mono);color:var(--text3);
1565
+ cursor:pointer;white-space:nowrap;user-select:none;
1566
+ border-bottom:2px solid transparent;transition:all 0.2s;
1567
+ text-transform:uppercase;font-weight:600;
1568
+ }
1569
+ .tab:hover{color:var(--text2);background:var(--bg3);}
1570
+ .tab.active{color:var(--accent);border-bottom:2px solid var(--accent);background:linear-gradient(0deg, rgba(108,143,255,0.1), transparent);}
1571
+ .tab-key{opacity:0.5;margin-right:6px;}
1572
+
1573
+ #main{flex:1;overflow-y:auto;overflow-x:hidden;padding:16px;display:flex;flex-direction:column;}
1574
+ #main::-webkit-scrollbar{width:8px}
1575
+ #main::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
1576
+
1577
+ #statusbar{
1578
+ height:28px;min-height:28px;display:flex;align-items:center;padding:0 16px;gap:20px;
1579
+ font-size:10px;color:var(--text3);border-top:1px solid var(--border);
1580
+ background:var(--bg);font-family:var(--mono);
1581
+ }
1582
+ .sb-item{display:flex;align-items:center;gap:6px;}
1583
+ .sb-sep{color:var(--border);}
1584
+ .up-val{color:var(--text);}
1585
+
1586
+ /* btop cards */
1587
+ .card{
1588
+ background:var(--bg1);
1589
+ border:1px solid var(--border);
1590
+ border-radius:6px;
1591
+ padding:16px;
1592
+ position:relative;
1593
+ margin-top:12px;
1594
+ transition:border-color 0.2s, box-shadow 0.2s;
1595
+ display:flex;flex-direction:column;
1596
+ }
1597
+ .card:hover{border-color:var(--cyan);box-shadow:0 0 10px rgba(62,220,255,0.05);}
1598
+ .card-title{
1599
+ position:absolute;top:-9px;left:12px;
1600
+ background:var(--bg);
1601
+ padding:0 8px;font-size:11px;font-family:var(--mono);font-weight:700;
1602
+ color:var(--accent);text-transform:uppercase;
1603
+ display:flex;align-items:center;gap:6px;
1604
+ }
1605
+ .card-title::before{content:"";display:block;width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 5px var(--accent);}
1606
+
1607
+ /* Grid System */
1608
+ .grid-2x2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; flex:1; }
1609
+ .panel-row { display:flex; gap:12px; margin-bottom:12px; }
1610
+ .panel-col { display:flex; flex-direction:column; gap:12px; flex:1; }
1611
+ .detail-split { display:flex; gap:16px; height:100%; }
1612
+ .detail-left { flex:6; display:flex; flex-direction:column; gap:12px; }
1613
+ .detail-right { flex:4; display:flex; flex-direction:column; gap:12px; }
1614
+ .detail-full { display:flex; flex-direction:column; gap:12px; height:100%; }
1615
+
1616
+ /* Stats */
1617
+ .stat{display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px dashed var(--border);}
1618
+ .stat:last-child{border-bottom:none;}
1619
+ .stat-label{font-size:11px;color:var(--text2);}
1620
+ .stat-val{font-family:var(--mono);font-size:12px;color:var(--text);font-weight:500;}
1621
+ .big-metric{text-align:center;padding:20px 0;}
1622
+ .big-val{font-family:var(--mono);font-size:48px;font-weight:700;color:var(--cyan);text-shadow:0 0 15px rgba(62,220,255,0.3);}
1623
+ .big-unit{font-family:var(--mono);font-size:14px;color:var(--text2);margin-top:8px;}
1624
+
1625
+ /* Gauges */
1626
+ .gauge-wrap{margin:8px 0;}
1627
+ .gauge-header{display:flex;justify-content:space-between;margin-bottom:6px;font-size:11px;font-family:var(--mono);}
1628
+ .gauge-pct{font-weight:600;}
1629
+ .gauge-track{height:20px;background:var(--bg2);border-radius:4px;overflow:hidden;position:relative;border:1px solid var(--border);}
1630
+ .gauge-fill{height:100%;transition:width 0.4s ease;display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-family:var(--mono);font-size:10px;font-weight:700;color:#000;}
1631
+ .gauge-fill.ok{background:linear-gradient(90deg,var(--green),#a3ffce);}
1632
+ .gauge-fill.warn{background:linear-gradient(90deg,var(--yellow),#ffea99);}
1633
+ .gauge-fill.crit{background:linear-gradient(90deg,var(--red),#ff99aa);}
1634
+
1635
+ /* SVG Rings */
1636
+ .ring-container { display:flex; justify-content:space-around; padding:20px 0; background:var(--bg1); border:1px solid var(--border); border-radius:6px; margin-bottom:12px; }
1637
+ .ring-box { display:flex; flex-direction:column; align-items:center; gap:10px; }
1638
+ .ring-title { font-family:var(--mono); font-size:11px; color:var(--text2); font-weight:700; text-transform:uppercase; }
1639
+ .svg-ring { position:relative; width:100px; height:100px; }
1640
+ .svg-ring.large { width:200px; height:200px; }
1641
+ .svg-ring svg { transform:rotate(-90deg); width:100%; height:100%; }
1642
+ .ring-center { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; }
1643
+ .ring-val { font-family:var(--mono); font-size:22px; font-weight:700; }
1644
+ .svg-ring.large .ring-val { font-size:42px; }
1645
+
1646
+ /* Sparklines */
1647
+ .spark-wrap { flex:1; min-height:80px; display:flex; flex-direction:column; margin-top:8px; }
1648
+ .spark-label { font-family:var(--mono); font-size:10px; color:var(--text3); margin-bottom:4px; }
1649
+ canvas.spark { width:100%; height:80px; display:block; border-radius:4px; background:var(--bg2); border:1px solid var(--border); }
1650
+ canvas.spark.large { height:200px; }
1651
+
1652
+ /* Core Grid */
1653
+ .core-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(30px, 1fr)); gap:6px; margin-top:12px; }
1654
+ .core-col { display:flex; flex-direction:column; gap:4px; align-items:center; }
1655
+ .core-bar { width:100%; height:100px; background:var(--bg2); border-radius:4px; display:flex; align-items:flex-end; overflow:hidden; border:1px solid var(--border); }
1656
+ .core-fill { width:100%; transition:height 0.3s; }
1657
+ .core-fill.ok { background:linear-gradient(0deg,var(--green),#a3ffce); }
1658
+ .core-fill.warn { background:linear-gradient(0deg,var(--yellow),#ffea99); }
1659
+ .core-fill.crit { background:linear-gradient(0deg,var(--red),#ff99aa); }
1660
+ .core-lbl { font-family:var(--mono); font-size:9px; color:var(--text2); }
1661
+
1662
+ /* Table */
1663
+ .table-wrap { flex:1; overflow-y:auto; border:1px solid var(--border); border-radius:6px; background:var(--bg1); }
1664
+ table.proc { width:100%; border-collapse:collapse; font-family:var(--mono); font-size:11px; text-align:left; }
1665
+ table.proc thead th { position:sticky; top:0; background:var(--bg2); padding:8px 12px; color:var(--accent); font-weight:700; text-transform:uppercase; border-bottom:1px solid var(--border); cursor:pointer; user-select:none; z-index:10; }
1666
+ table.proc thead th:hover { color:var(--cyan); background:var(--border); }
1667
+ table.proc tbody tr { border-bottom:1px solid var(--border); cursor:default; }
1668
+ table.proc tbody tr:nth-child(even) { background:rgba(255,255,255,0.01); }
1669
+ table.proc tbody tr:hover { background:var(--bg2); }
1670
+ table.proc td { padding:6px 12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:250px; }
1671
+ .proc-cmd { color:var(--text); }
1672
+ .table-wrap::-webkit-scrollbar { width:8px; }
1673
+ .table-wrap::-webkit-scrollbar-thumb { background:var(--border); }
1674
+
1675
+ /* Helpers */
1676
+ .flex-row { display:flex; gap:12px; }
1677
+ .flex-1 { flex:1; }
1678
+ .mb-12 { margin-bottom:12px; }
1679
+ .txt-green { color:var(--green); }
1680
+ .txt-yellow { color:var(--yellow); }
1681
+ .txt-red { color:var(--red); }
1682
+ .txt-cyan { color:var(--cyan); }
1683
+ .txt-purple { color:var(--purple); }
1684
+
1685
+ /* Chips */
1686
+ .chips { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
1687
+ .chip { padding:6px 12px; border-radius:4px; font-family:var(--mono); font-size:11px; border:1px solid var(--border); background:var(--bg2); }
1688
+ .chip.ok { border-color:var(--green); color:var(--green); }
1689
+ .chip.warn { border-color:var(--yellow); color:var(--yellow); }
1690
+ .chip.crit { border-color:var(--red); color:var(--red); }
1691
+
1692
+ .anomaly { display:flex; gap:12px; padding:12px; border:1px solid var(--red); background:rgba(255,77,106,0.1); border-radius:6px; margin-bottom:12px; }
1693
+ .anomaly-icon { font-size:20px; }
1694
+ .anomaly-title { font-family:var(--mono); font-weight:700; color:var(--red); font-size:12px; }
1695
+ .anomaly-desc { font-family:var(--mono); font-size:11px; color:var(--text); margin-top:4px; }
1696
+
1697
+ .view { display:none; height:100%; flex-direction:column; }
1698
+ .view.active { display:flex; }
1699
+ #view-content { flex:1; display:flex; flex-direction:column; animation:fadeIn 0.2s ease; height:100%; }
1700
+ @keyframes fadeIn{from{opacity:0;}to{opacity:1;}}
1701
+
1702
+ </style>
1703
+ </head>
1704
+ <body>
1705
+ <div id="titlebar">
1706
+ <span id="titlebar-logo">Pyre</span>
1707
+ <span id="titlebar-host">Loading...</span>
1708
+ <div id="conn-badge"><div class="conn-dot" id="conn-dot"></div><span id="conn-text">Connecting</span></div>
1709
+ </div>
1710
+ <div id="tabbar">
1711
+ <div class="tab active" data-view="overview" onclick="switchView('overview')"><span class="tab-key">1</span>Overview</div>
1712
+ <div class="tab" data-view="cpu" onclick="switchView('cpu')"><span class="tab-key">2</span>CPU</div>
1713
+ <div class="tab" data-view="memory" onclick="switchView('memory')"><span class="tab-key">3</span>Memory</div>
1714
+ <div class="tab" data-view="gpu" onclick="switchView('gpu')"><span class="tab-key">4</span>GPU</div>
1715
+ <div class="tab" data-view="power" onclick="switchView('power')"><span class="tab-key">5</span>Power</div>
1716
+ <div class="tab" data-view="battery" onclick="switchView('battery')"><span class="tab-key">6</span>Battery</div>
1717
+ <div class="tab" data-view="thermal" onclick="switchView('thermal')"><span class="tab-key">7</span>Thermal</div>
1718
+ <div class="tab" data-view="network" onclick="switchView('network')"><span class="tab-key">8</span>Network</div>
1719
+ <div class="tab" data-view="disk" onclick="switchView('disk')"><span class="tab-key">9</span>Disk</div>
1720
+ <div class="tab" data-view="processes" onclick="switchView('processes')"><span class="tab-key">P</span>Processes</div>
1721
+ <div class="tab" data-view="anomalies" onclick="switchView('anomalies')"><span class="tab-key">A</span>Anomalies</div>
1722
+ </div>
1723
+ <div id="main">
1724
+ <div id="view-content"></div>
1725
+ </div>
1726
+ <div id="statusbar">
1727
+ <div class="sb-item">\u27F3 <span id="sb-refresh" class="up-val">\u2013</span></div><div class="sb-sep">\u2502</div>
1728
+ <div class="sb-item">\u2191 <span id="sb-uptime" class="up-val">\u2013</span></div><div class="sb-sep">\u2502</div>
1729
+ <div class="sb-item">CPU <span id="sb-cpu" class="up-val">\u2013</span></div><div class="sb-sep">\u2502</div>
1730
+ <div class="sb-item">MEM <span id="sb-mem" class="up-val">\u2013</span></div><div class="sb-sep">\u2502</div>
1731
+ <div class="sb-item">BATT <span id="sb-batt" class="up-val">\u2013</span></div><div class="sb-sep">\u2502</div>
1732
+ <div class="sb-item">PROCS <span id="sb-procs" class="up-val">\u2013</span></div>
1733
+ </div>
1734
+ <script>
1735
+ let currentView = 'overview';
1736
+ let latestData = null;
1737
+ const history = { cpu:[], mem:[], temp:[], rxRate:[], txRate:[], power:[] };
1738
+ const MAX_HIST = 100;
1739
+ let procSort = { col:'cpu', dir:'desc' };
1740
+
1741
+ function pushHist(arr, val) { arr.push(val||0); if(arr.length>MAX_HIST)arr.shift(); }
1742
+ function fmtBytes(b, dec=2) {
1743
+ if(!b) return '0 B';
1744
+ const k=1024, s=['B','KB','MB','GB','TB'];
1745
+ const i=Math.floor(Math.log(Math.abs(b))/Math.log(k));
1746
+ return parseFloat((b/Math.pow(k,i)).toFixed(dec))+' '+s[i];
1747
+ }
1748
+ function statusCls(v) { return v>85?'crit':v>65?'warn':'ok'; }
1749
+ function escapeHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
1750
+
1751
+ function svgRing(val, max, label, unit='', size=100) {
1752
+ const r = size*0.4, circ = 2*Math.PI*r;
1753
+ const pct = Math.min(100, Math.max(0, (val/max)*100));
1754
+ const off = circ*(1 - pct/100);
1755
+ const cls = statusCls(pct);
1756
+ const col = cls==='crit'?'var(--red)':cls==='warn'?'var(--yellow)':'var(--cyan)';
1757
+ return \`<div class="ring-box">
1758
+ <div class="ring-title">\${label}</div>
1759
+ <div class="svg-ring \${size>150?'large':''}">
1760
+ <svg viewBox="0 0 \${size} \${size}">
1761
+ <circle cx="\${size/2}" cy="\${size/2}" r="\${r}" fill="none" stroke="var(--border)" stroke-width="\${size*0.08}"/>
1762
+ <circle cx="\${size/2}" cy="\${size/2}" r="\${r}" fill="none" stroke="\${col}" stroke-width="\${size*0.08}"
1763
+ stroke-dasharray="\${circ}" stroke-dashoffset="\${off}" stroke-linecap="round"
1764
+ style="transition:stroke-dashoffset 0.5s ease" />
1765
+ </svg>
1766
+ <div class="ring-center">
1767
+ <div class="ring-val" style="color:\${col}">\${val.toFixed(1)}\${unit}</div>
1768
+ </div>
1769
+ </div>
1770
+ </div>\`;
1771
+ }
1772
+
1773
+ function drawSpark(id, arr, col1, col2, mx) {
1774
+ const c = document.getElementById(id);
1775
+ if(!c || !arr.length) return;
1776
+ const dpr = window.devicePixelRatio || 1;
1777
+ const rect = c.getBoundingClientRect();
1778
+ c.width = rect.width * dpr;
1779
+ c.height = rect.height * dpr;
1780
+ const W = c.width, H = c.height;
1781
+ const ctx = c.getContext('2d');
1782
+ ctx.clearRect(0,0,W,H);
1783
+
1784
+ // Grid lines
1785
+ ctx.strokeStyle = 'rgba(255,255,255,0.05)';
1786
+ ctx.lineWidth = 1 * dpr;
1787
+ [0.25, 0.5, 0.75].forEach(y => {
1788
+ ctx.beginPath(); ctx.moveTo(0, H*y); ctx.lineTo(W, H*y); ctx.stroke();
1789
+ });
1790
+
1791
+ const m = mx || Math.max(...arr, 1);
1792
+ const pts = arr.map((v,i) => [i/(arr.length-1||1)*W, H - (v/m)*H]);
1793
+
1794
+ const grad = ctx.createLinearGradient(0,0,W,0);
1795
+ grad.addColorStop(0, col1); grad.addColorStop(1, col2);
1796
+ const aGrad = ctx.createLinearGradient(0,0,0,H);
1797
+ aGrad.addColorStop(0, col2+'66'); aGrad.addColorStop(1, 'transparent');
1798
+
1799
+ ctx.beginPath(); ctx.moveTo(pts[0][0], H);
1800
+ pts.forEach(p=>ctx.lineTo(p[0],p[1])); ctx.lineTo(pts[pts.length-1][0], H);
1801
+ ctx.fillStyle = aGrad; ctx.fill();
1802
+
1803
+ ctx.beginPath(); pts.forEach((p,i)=>i?ctx.lineTo(p[0],p[1]):ctx.moveTo(p[0],p[1]));
1804
+ ctx.strokeStyle = grad; ctx.lineWidth = 2 * dpr; ctx.stroke();
1805
+ }
1806
+
1807
+ function gaugeHtml(lbl, pct, valStr='') {
1808
+ const cls = statusCls(pct);
1809
+ return \`<div class="gauge-wrap">
1810
+ <div class="gauge-header"><span>\${lbl}</span><span class="txt-\${cls==='crit'?'red':cls==='warn'?'yellow':'green'}">\${pct.toFixed(1)}%</span></div>
1811
+ <div class="gauge-track"><div class="gauge-fill \${cls}" style="width:\${pct}%">\${valStr}</div></div>
1812
+ </div>\`;
1813
+ }
1814
+
1815
+ function statRow(k,v) { return \`<div class="stat"><div class="stat-label">\${k}</div><div class="stat-val">\${escapeHtml(v)}</div></div>\`; }
1816
+ function card(title, content, cls='') { return \`<div class="card \${cls}"><div class="card-title">\${title}</div>\${content}</div>\`; }
1817
+
1818
+ function renderOverview(d) {
1819
+ const cpu = d.cpu.usage;
1820
+ const mem = d.memory.usagePercent;
1821
+ const temp = d.cpu.temperature ?? d.thermal.temperatures?.cpu_die ?? 0;
1822
+ const batt = d.battery?.level ?? 0;
1823
+
1824
+ const rings = \`<div class="ring-container">
1825
+ \${svgRing(cpu, 100, 'CPU', '%')}
1826
+ \${svgRing(mem, 100, 'MEM', '%')}
1827
+ \${svgRing(temp, 100, 'TEMP', '\xB0')}
1828
+ \${d.battery ? svgRing(batt, 100, 'BATT', '%') : ''}
1829
+ </div>\`;
1830
+
1831
+ const topProcs = (d.processes||[]).slice(0,8).map(p=>\`<tr>
1832
+ <td>\${p.pid}</td>
1833
+ <td class="proc-cmd" title="\${escapeHtml(p.command)}">\${escapeHtml(p.command)}</td>
1834
+ <td>\${escapeHtml(p.user)}</td>
1835
+ <td class="txt-\${statusCls(p.cpu)}">\${p.cpu.toFixed(1)}</td>
1836
+ <td>\${p.mem.toFixed(1)}</td>
1837
+ </tr>\`).join('');
1838
+
1839
+ return \`<div class="detail-full">
1840
+ \${rings}
1841
+ <div class="grid-2x2">
1842
+ \${card('CPU', \`
1843
+ \${gaugeHtml('Usage', cpu, d.cpu.frequency+' MHz')}
1844
+ <div class="spark-wrap"><div class="spark-label">CPU History</div><canvas class="spark" id="sp-ov-cpu"></canvas></div>
1845
+ \`)}
1846
+ \${card('Memory', \`
1847
+ \${gaugeHtml('Usage', mem, fmtBytes(d.memory.used))}
1848
+ \${statRow('Total', fmtBytes(d.memory.total))}
1849
+ \${statRow('Free', fmtBytes(d.memory.free))}
1850
+ \${statRow('Swap', fmtBytes(d.memory.swapUsed)+' / '+fmtBytes(d.memory.swapTotal))}
1851
+ \`)}
1852
+ \${card('Network', \`
1853
+ <div class="flex-row mb-12">
1854
+ <div class="flex-1 txt-cyan" style="font-family:var(--mono);font-size:14px;font-weight:700">\u2B07 \${fmtBytes(d.network.rxBytes)}</div>
1855
+ <div class="flex-1 txt-purple" style="font-family:var(--mono);font-size:14px;font-weight:700;text-align:right">\u2B06 \${fmtBytes(d.network.txBytes)}</div>
1856
+ </div>
1857
+ <div class="flex-row" style="flex:1">
1858
+ <div class="spark-wrap" style="margin:0"><canvas class="spark" id="sp-ov-rx"></canvas></div>
1859
+ <div class="spark-wrap" style="margin:0"><canvas class="spark" id="sp-ov-tx"></canvas></div>
1860
+ </div>
1861
+ \`)}
1862
+ \${card('Top Processes', \`
1863
+ <div class="table-wrap" style="border:none;background:transparent;overflow:hidden;">
1864
+ <table class="proc">
1865
+ <thead><tr><th>PID</th><th>Command</th><th>User</th><th>CPU%</th><th>MEM%</th></tr></thead>
1866
+ <tbody>\${topProcs}</tbody>
1867
+ </table>
1868
+ </div>
1869
+ \`, 'mb-0')}
1870
+ </div>
1871
+ </div>\`;
1872
+ }
1873
+
1874
+ function renderCpu(d) {
1875
+ let cores = '';
1876
+ if(d.cpu.coreUsage) {
1877
+ cores = '<div class="core-grid">' + d.cpu.coreUsage.map((u,i) => {
1878
+ const p = Math.min(100,u);
1879
+ return \`<div class="core-col">
1880
+ <div class="core-bar"><div class="core-fill \${statusCls(p)}" style="height:\${p}%"></div></div>
1881
+ <div class="core-lbl">C\${i}</div>
1882
+ </div>\`;
1883
+ }).join('') + '</div>';
1884
+ }
1885
+ return \`<div class="detail-split">
1886
+ <div class="detail-left">
1887
+ \${card('CPU Usage', \`
1888
+ <div class="spark-wrap" style="flex:none"><canvas class="spark large" id="sp-cpu-l"></canvas></div>
1889
+ \${cores}
1890
+ \`, 'flex-1')}
1891
+ </div>
1892
+ <div class="detail-right">
1893
+ \${card('Processor Info', \`
1894
+ <div class="big-metric"><div class="big-val">\${d.cpu.usage.toFixed(1)}%</div><div class="big-unit">Overall Utilisation</div></div>
1895
+ \${statRow('Model', d.cpu.brand)}
1896
+ \${statRow('Cores', d.cpu.physicalCores + ' Physical / ' + d.cpu.cores + ' Logical')}
1897
+ \${statRow('Clock', d.cpu.frequency + ' MHz')}
1898
+ \${statRow('Load 1m', d.cpu.loadAvg[0].toFixed(2))}
1899
+ \${statRow('Load 5m', d.cpu.loadAvg[1].toFixed(2))}
1900
+ \${statRow('Load 15m', d.cpu.loadAvg[2].toFixed(2))}
1901
+ \`, 'flex-1')}
1902
+ </div>
1903
+ </div>\`;
1904
+ }
1905
+
1906
+ function renderMemory(d) {
1907
+ return \`<div class="detail-full">
1908
+ \${card('Memory', \`
1909
+ <div class="big-metric"><div class="big-val">\${d.memory.usagePercent.toFixed(1)}%</div><div class="big-unit">Memory Used</div></div>
1910
+ \${gaugeHtml('RAM', d.memory.usagePercent, fmtBytes(d.memory.used))}
1911
+ \${gaugeHtml('Swap', d.memory.swapTotal? (d.memory.swapUsed/d.memory.swapTotal)*100 : 0, fmtBytes(d.memory.swapUsed))}
1912
+ <div class="grid-2x2" style="margin-top:20px;flex:none">
1913
+ <div>\${statRow('Total', fmtBytes(d.memory.total))}\${statRow('Used', fmtBytes(d.memory.used))}</div>
1914
+ <div>\${statRow('Free', fmtBytes(d.memory.free))}\${statRow('Cached', fmtBytes(d.memory.total - d.memory.free - d.memory.used))}</div>
1915
+ </div>
1916
+ <div class="spark-wrap" style="margin-top:20px"><div class="spark-label">Memory History</div><canvas class="spark large" id="sp-mem-l"></canvas></div>
1917
+ \`, 'flex-1')}
1918
+ </div>\`;
1919
+ }
1920
+
1921
+ function renderGpu(d) {
1922
+ if(!d.gpu) return '<div class="big-metric">No GPU Data</div>';
1923
+ return \`<div class="detail-full">
1924
+ \${card('Graphics Processor', \`
1925
+ <div class="big-metric"><div class="big-val">\${d.gpu.utilization.toFixed(1)}%</div><div class="big-unit">\${d.gpu.model}</div></div>
1926
+ \${gaugeHtml('Utilisation', d.gpu.utilization)}
1927
+ \${statRow('VRAM Used', fmtBytes(d.gpu.memory))}
1928
+ \${statRow('Active Processes', d.gpu.processes)}
1929
+ \${d.gpu.temperature? statRow('Temperature', d.gpu.temperature+'\xB0C') : ''}
1930
+ \`, 'flex-1')}
1931
+ </div>\`;
1932
+ }
1933
+
1934
+ function renderPower(d) {
1935
+ const tw = d.power?.combinedWatts ?? (d.battery?.powerWatts || 0);
1936
+ return \`<div class="detail-full">
1937
+ \${card('Power Consumption', \`
1938
+ <div class="big-metric"><div class="big-val">\${tw.toFixed(2)} W</div><div class="big-unit">Total Draw</div></div>
1939
+ <div class="flex-row">
1940
+ \${d.power?.cpuWatts!=null ? \`<div class="flex-1">\${gaugeHtml('CPU Package', Math.min(100,d.power.cpuWatts/100*100), d.power.cpuWatts.toFixed(2)+'W')}</div>\` : ''}
1941
+ \${d.power?.gpuWatts!=null ? \`<div class="flex-1">\${gaugeHtml('GPU Package', Math.min(100,d.power.gpuWatts/200*100), d.power.gpuWatts.toFixed(2)+'W')}</div>\` : ''}
1942
+ </div>
1943
+ \`, 'flex-1')}
1944
+ </div>\`;
1945
+ }
1946
+
1947
+ function renderBattery(d) {
1948
+ if(!d.battery) return '<div class="big-metric">No Battery</div>';
1949
+ return \`<div class="detail-full">
1950
+ \${card('Battery', \`
1951
+ <div class="ring-container" style="border:none;background:transparent;">
1952
+ \${svgRing(d.battery.level, 100, 'LEVEL', '%', 200)}
1953
+ </div>
1954
+ <div class="grid-2x2">
1955
+ <div>
1956
+ \${statRow('State', d.battery.state)}
1957
+ \${statRow('Power Source', d.battery.powerSource)}
1958
+ \${statRow('Condition', d.battery.condition)}
1959
+ \${d.battery.cycles ? statRow('Cycles', d.battery.cycles) : ''}
1960
+ </div>
1961
+ <div>
1962
+ \${d.battery.timeRemaining ? statRow('Time Remaining', d.battery.timeRemaining) : ''}
1963
+ \${d.battery.powerWatts ? statRow('Power Draw', d.battery.powerWatts.toFixed(2)+' W') : ''}
1964
+ \${d.battery.maxCapacityPercent ? statRow('Max Capacity', d.battery.maxCapacityPercent+'%') : ''}
1965
+ </div>
1966
+ </div>
1967
+ \`, 'flex-1')}
1968
+ </div>\`;
1969
+ }
1970
+
1971
+ function renderThermal(d) {
1972
+ const chips = d.thermal.temperatures ? Object.entries(d.thermal.temperatures).map(([k,v]) => \`<div class="chip \${statusCls(v)}">\${k}: \${v.toFixed(1)}\xB0C</div>\`).join('') : '';
1973
+ return \`<div class="detail-full">
1974
+ \${card('Thermal Status', \`
1975
+ \${statRow('State', d.thermal.state)}
1976
+ \${statRow('Pressure', d.thermal.pressureLevel ?? 'Normal')}
1977
+ <div class="chips mb-12">\${chips}</div>
1978
+ <div class="spark-wrap"><div class="spark-label">CPU Temp History</div><canvas class="spark large" id="sp-therm-l"></canvas></div>
1979
+ \`, 'flex-1')}
1980
+ </div>\`;
1981
+ }
1982
+
1983
+ function renderNetwork(d) {
1984
+ return \`<div class="detail-split">
1985
+ <div class="detail-left">
1986
+ \${card('Traffic', \`
1987
+ <div class="flex-row">
1988
+ <div class="flex-1 spark-wrap"><div class="spark-label txt-cyan">\u2B07 RX Rate</div><canvas class="spark large" id="sp-net-rx-l"></canvas></div>
1989
+ <div class="flex-1 spark-wrap"><div class="spark-label txt-purple">\u2B06 TX Rate</div><canvas class="spark large" id="sp-net-tx-l"></canvas></div>
1990
+ </div>
1991
+ \`, 'flex-1')}
1992
+ </div>
1993
+ <div class="detail-right">
1994
+ \${card('Interface Info', \`
1995
+ <div class="big-metric" style="padding:10px 0"><div class="big-val txt-cyan" style="font-size:24px">\${d.network.interface}</div><div class="big-unit">\${d.network.ip}</div></div>
1996
+ \${statRow('RX Total', fmtBytes(d.network.rxBytes))}
1997
+ \${statRow('TX Total', fmtBytes(d.network.txBytes))}
1998
+ \${statRow('RX Packets', (d.network.rxPackets||0).toLocaleString())}
1999
+ \${statRow('TX Packets', (d.network.txPackets||0).toLocaleString())}
2000
+ \${d.network.connections ? statRow('TCP Conns', d.network.connections) : ''}
2001
+ \`, 'flex-1')}
2002
+ </div>
2003
+ </div>\`;
2004
+ }
2005
+
2006
+ function renderDisk(d) {
2007
+ const rows = (d.disk||[]).map(ds => \`
2008
+ <div class="mb-12">
2009
+ <div class="flex-row" style="justify-content:space-between;font-family:var(--mono);font-size:12px;margin-bottom:6px;">
2010
+ <span class="txt-cyan" style="font-weight:700">\${ds.mountpoint}</span>
2011
+ <span>\${ds.used} / \${ds.size}</span>
2012
+ </div>
2013
+ \${gaugeHtml('', parseFloat(ds.capacity)||0, ds.capacity)}
2014
+ </div>\`).join('');
2015
+ return \`<div class="detail-full">\${card('Storage Volumes', rows, 'flex-1')}</div>\`;
2016
+ }
2017
+
2018
+ function sortProcs(procs) {
2019
+ const col = procSort.col;
2020
+ const dir = procSort.dir;
2021
+ return [...procs].sort((a,b)=>{
2022
+ let va=a[col], vb=b[col];
2023
+ if(typeof va==='string'){va=va.toLowerCase();vb=vb.toLowerCase();}
2024
+ return (va<vb ? -1 : va>vb ? 1 : 0) * (dir==='asc'?1:-1);
2025
+ });
2026
+ }
2027
+ function sortBy(col) {
2028
+ if(procSort.col===col) procSort.dir = procSort.dir==='asc'?'desc':'asc';
2029
+ else {procSort.col=col; procSort.dir='desc';}
2030
+ if(latestData) updateView();
2031
+ }
2032
+ function sortHdr(col) { return procSort.col===col ? (procSort.dir==='asc'?' \u2191':' \u2193') : ''; }
2033
+ function fmtTime(s) { if(!s)return'\u2013'; const h=Math.floor(s/3600), m=Math.floor((s%3600)/60), sc=Math.floor(s%60); return h?h+'h'+m+'m':m?m+'m'+sc+'s':sc+'s'; }
2034
+
2035
+ function renderProcesses(d) {
2036
+ const procs = sortProcs(d.processes||[]);
2037
+ const rows = procs.map(p=>\`<tr>
2038
+ <td>\${p.pid}</td>
2039
+ <td class="proc-cmd" title="\${escapeHtml(p.command)}">\${escapeHtml(p.command)}</td>
2040
+ <td>\${escapeHtml(p.user)}</td>
2041
+ <td class="txt-\${statusCls(p.cpu)}">\${p.cpu.toFixed(1)}</td>
2042
+ <td>\${p.mem.toFixed(1)}</td>
2043
+ <td>\${p.threads||0}</td>
2044
+ <td>\${p.state}</td>
2045
+ <td>\${fmtTime(p.runtime)}</td>
2046
+ </tr>\`).join('');
2047
+ return \`<div class="detail-full">
2048
+ \${card('Process List ('+procs.length+')', \`
2049
+ <div class="table-wrap">
2050
+ <table class="proc">
2051
+ <thead><tr>
2052
+ <th onclick="sortBy('pid')">PID\${sortHdr('pid')}</th>
2053
+ <th onclick="sortBy('command')">Command\${sortHdr('command')}</th>
2054
+ <th onclick="sortBy('user')">User\${sortHdr('user')}</th>
2055
+ <th onclick="sortBy('cpu')">CPU%\${sortHdr('cpu')}</th>
2056
+ <th onclick="sortBy('mem')">MEM%\${sortHdr('mem')}</th>
2057
+ <th onclick="sortBy('threads')">Thr\${sortHdr('threads')}</th>
2058
+ <th onclick="sortBy('state')">State\${sortHdr('state')}</th>
2059
+ <th onclick="sortBy('runtime')">Time\${sortHdr('runtime')}</th>
2060
+ </tr></thead>
2061
+ <tbody>\${rows}</tbody>
2062
+ </table>
2063
+ </div>
2064
+ \`, 'flex-1')}
2065
+ </div>\`;
2066
+ }
2067
+
2068
+ function renderAnomalies(d) {
2069
+ const arr = d.anomalies||[];
2070
+ if(!arr.length) return '<div class="big-metric"><div class="big-val txt-green" style="font-size:32px">System Healthy</div></div>';
2071
+ const rows = arr.map(a=>\`<div class="anomaly"><div class="anomaly-icon">\u26A0\uFE0F</div><div><div class="anomaly-title">\${escapeHtml(a.type||'Alert')}</div><div class="anomaly-desc">\${escapeHtml(a.message||a.detail||JSON.stringify(a))}</div></div></div>\`).join('');
2072
+ return \`<div class="detail-full">\${card('Anomalies', rows, 'flex-1')}</div>\`;
2073
+ }
2074
+
2075
+ function drawSparks() {
2076
+ drawSpark('sp-ov-cpu', history.cpu, '#3edcff', '#4a6ee8');
2077
+ drawSpark('sp-ov-rx', history.rxRate, '#3edcff', '#4a6ee8');
2078
+ drawSpark('sp-ov-tx', history.txRate, '#b06cff', '#6c8fff');
2079
+ drawSpark('sp-cpu-l', history.cpu, '#3edcff', '#4a6ee8');
2080
+ drawSpark('sp-mem-l', history.mem, '#b06cff', '#6c8fff');
2081
+ drawSpark('sp-therm-l', history.temp, '#ffca3e', '#ff4d6a');
2082
+ drawSpark('sp-net-rx-l', history.rxRate, '#3edcff', '#4a6ee8');
2083
+ drawSpark('sp-net-tx-l', history.txRate, '#b06cff', '#6c8fff');
2084
+ }
2085
+
2086
+ function switchView(v) {
2087
+ currentView = v;
2088
+ document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active', t.dataset.view===v));
2089
+ if(latestData) updateView();
2090
+ }
2091
+
2092
+ function updateView() {
2093
+ const el = document.getElementById('view-content');
2094
+ if(!el || !latestData) return;
2095
+ const d = latestData;
2096
+ const map = {overview:renderOverview, cpu:renderCpu, memory:renderMemory, gpu:renderGpu, power:renderPower, battery:renderBattery, thermal:renderThermal, network:renderNetwork, disk:renderDisk, processes:renderProcesses, anomalies:renderAnomalies};
2097
+ if(map[currentView]) {
2098
+ el.innerHTML = map[currentView](d);
2099
+ requestAnimationFrame(drawSparks);
2100
+ }
2101
+ }
2102
+
2103
+ let prx=null, ptx=null;
2104
+ function ingestData(d) {
2105
+ latestData = d;
2106
+ pushHist(history.cpu, d.cpu.usage);
2107
+ pushHist(history.mem, d.memory.usagePercent);
2108
+ pushHist(history.temp, d.cpu.temperature ?? d.thermal.temperatures?.cpu_die ?? 0);
2109
+ if(prx!==null) {
2110
+ pushHist(history.rxRate, Math.max(0, d.network.rxBytes-prx));
2111
+ pushHist(history.txRate, Math.max(0, d.network.txBytes-ptx));
2112
+ }
2113
+ prx=d.network.rxBytes; ptx=d.network.txBytes;
2114
+ if(d.power?.combinedWatts) pushHist(history.power, d.power.combinedWatts);
2115
+
2116
+ document.getElementById('titlebar-host').textContent = d.header.hostname+' \xB7 '+d.header.os;
2117
+ document.getElementById('sb-uptime').textContent = d.header.uptime;
2118
+ document.getElementById('sb-cpu').textContent = d.cpu.usage.toFixed(1)+'%';
2119
+ document.getElementById('sb-mem').textContent = d.memory.usagePercent.toFixed(1)+'%';
2120
+ document.getElementById('sb-batt').textContent = d.battery ? d.battery.level+'%' : '\u2013';
2121
+ document.getElementById('sb-procs').textContent = (d.processes||[]).length;
2122
+ document.getElementById('sb-refresh').textContent = new Date().toLocaleTimeString();
2123
+
2124
+ updateView();
2125
+ }
2126
+
2127
+ window.__pyreUpdate = function(jsStr) {
2128
+ try {
2129
+ const d = JSON.parse(jsStr);
2130
+ document.getElementById('conn-dot').className='conn-dot';
2131
+ document.getElementById('conn-text').textContent='Live';
2132
+ ingestData(d);
2133
+ } catch(e) { document.getElementById('conn-text').textContent='Err: '+e.message; }
2134
+ };
2135
+
2136
+ let pTimer=null;
2137
+ async function pollOnce() {
2138
+ try {
2139
+ const r=await fetch('/api/stats',{cache:'no-store'});
2140
+ if(!r.ok){ document.getElementById('conn-dot').className='conn-dot offline'; return; }
2141
+ document.getElementById('conn-dot').className='conn-dot';
2142
+ ingestData(await r.json());
2143
+ } catch(e) { document.getElementById('conn-dot').className='conn-dot offline'; }
2144
+ }
2145
+ setTimeout(()=>{ if(!latestData && !pTimer) { pollOnce(); pTimer=setInterval(pollOnce, 2000); } }, 4000);
2146
+
2147
+ const keyMap = {'1':'overview','2':'cpu','3':'memory','4':'gpu','5':'power','6':'battery','7':'thermal','8':'network','9':'disk','p':'processes','a':'anomalies'};
2148
+ document.addEventListener('keydown', e => { if(keyMap[e.key.toLowerCase()]) switchView(keyMap[e.key.toLowerCase()]); });
2149
+ </script>
2150
+ </body>
2151
+ </html>`;
2152
+ }
2153
+
2238
2154
  // src/live/input.ts
2239
2155
  import readline from "readline";
2240
2156
 
@@ -2393,86 +2309,6 @@ var History = class _History {
2393
2309
  }
2394
2310
  };
2395
2311
 
2396
- // src/state/config.ts
2397
- import fs2 from "fs";
2398
- import os3 from "os";
2399
- import path2 from "path";
2400
- var CONFIG_DIR = path2.join(os3.homedir(), ".config", "pyre");
2401
- var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
2402
- var DEFAULT_CONFIG = {
2403
- theme: "default",
2404
- interval: 2,
2405
- exportDir: "./pyre-exports",
2406
- detailed: false,
2407
- sortMode: "cpu",
2408
- treeView: false,
2409
- graphMode: "spark",
2410
- showGraphs: true,
2411
- autoLog: false,
2412
- mouseEnabled: true,
2413
- cpuAlertPct: 90,
2414
- tempAlertC: 95,
2415
- watchdogProcess: "",
2416
- watchdogCpu: 80,
2417
- watchdogMem: 80,
2418
- notificationsEnabled: true,
2419
- p2pPort: 9876,
2420
- p2pPassword: "pyre",
2421
- splashEnabled: true,
2422
- splashColorScheme: "fire",
2423
- splashAnimation: "classic",
2424
- webhookUrl: "",
2425
- alertCmd: "",
2426
- visiblePanels: {
2427
- cpu: true,
2428
- mem: true,
2429
- gpu: true,
2430
- power: true,
2431
- battery: true,
2432
- thermal: true,
2433
- network: true,
2434
- packets: true,
2435
- tasks: true,
2436
- disk: true,
2437
- process: true,
2438
- tree: false
2439
- },
2440
- panelLayout: ["mem", "disk", "net"]
2441
- };
2442
- function readConfig() {
2443
- try {
2444
- if (!fs2.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
2445
- const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
2446
- const parsed = JSON.parse(raw);
2447
- return deepMerge(DEFAULT_CONFIG, parsed);
2448
- } catch {
2449
- return { ...DEFAULT_CONFIG };
2450
- }
2451
- }
2452
- function writeConfig(config3) {
2453
- try {
2454
- if (!fs2.existsSync(CONFIG_DIR)) {
2455
- fs2.mkdirSync(CONFIG_DIR, { recursive: true });
2456
- }
2457
- const merged = deepMerge(readConfig(), config3);
2458
- fs2.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
2459
- } catch {
2460
- }
2461
- }
2462
- function deepMerge(target, source) {
2463
- const result = { ...target };
2464
- for (const key of Object.keys(source)) {
2465
- if (source[key] !== void 0 && source[key] !== null) {
2466
- if (typeof source[key] === "object" && !Array.isArray(source[key]) && typeof target[key] === "object" && !Array.isArray(target[key])) {
2467
- result[key] = deepMerge(target[key] || {}, source[key]);
2468
- } else {
2469
- result[key] = source[key];
2470
- }
2471
- }
2472
- }
2473
- return result;
2474
- }
2475
-
2476
2312
  // src/live/state.ts
2477
2313
  var SIGNAL_OPTIONS = ["SIGTERM", "SIGKILL", "SIGINT", "SIGHUP", "SIGSTOP", "SIGCONT"];
2478
2314
  var config = readConfig();
@@ -2607,10 +2443,10 @@ function getToggleKey(opt) {
2607
2443
  }
2608
2444
 
2609
2445
  // src/live/export.ts
2610
- import fs3 from "fs";
2611
- import path3 from "path";
2446
+ import fs2 from "fs";
2447
+ import path2 from "path";
2612
2448
  function ensureDir(dir) {
2613
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2449
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
2614
2450
  }
2615
2451
  function timestampForFile() {
2616
2452
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -2623,7 +2459,7 @@ function exportSnapshot() {
2623
2459
  try {
2624
2460
  ensureDir(state.exportDir);
2625
2461
  const ext = state.exportFormat === "json" ? "json" : state.exportFormat === "csv" ? "csv" : state.exportFormat === "tsv" ? "tsv" : state.exportFormat === "html" ? "html" : "md";
2626
- const file = path3.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
2462
+ const file = path2.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
2627
2463
  let content;
2628
2464
  switch (state.exportFormat) {
2629
2465
  case "json":
@@ -2642,7 +2478,7 @@ function exportSnapshot() {
2642
2478
  content = formatMarkdown(state.lastData);
2643
2479
  break;
2644
2480
  }
2645
- fs3.writeFileSync(file, content);
2481
+ fs2.writeFileSync(file, content);
2646
2482
  setStatus(`Exported snapshot -> ${file}`);
2647
2483
  } catch (err) {
2648
2484
  setStatus(`Export failed: ${err.message}`);
@@ -2652,8 +2488,8 @@ function startLogging() {
2652
2488
  try {
2653
2489
  ensureDir(state.exportDir);
2654
2490
  rotateLogs();
2655
- const file = path3.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
2656
- state.logStream = fs3.createWriteStream(file, { flags: "a" });
2491
+ const file = path2.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
2492
+ state.logStream = fs2.createWriteStream(file, { flags: "a" });
2657
2493
  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");
2658
2494
  state.logging = true;
2659
2495
  setStatus(`Logging every tick -> ${file}`);
@@ -2667,37 +2503,37 @@ var MAX_LOG_BYTES = 50 * 1024 * 1024;
2667
2503
  function rotateLogs() {
2668
2504
  try {
2669
2505
  const dir = state.exportDir;
2670
- if (!fs3.existsSync(dir)) return;
2671
- const files = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path3.join(dir, f), mtime: fs3.statSync(path3.join(dir, f)).mtimeMs, size: fs3.statSync(path3.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
2506
+ if (!fs2.existsSync(dir)) return;
2507
+ const files = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path2.join(dir, f), mtime: fs2.statSync(path2.join(dir, f)).mtimeMs, size: fs2.statSync(path2.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
2672
2508
  const now = Date.now();
2673
2509
  for (const f of files) {
2674
2510
  if (now - f.mtime > MAX_LOG_AGE_MS) {
2675
2511
  try {
2676
- fs3.unlinkSync(f.path);
2512
+ fs2.unlinkSync(f.path);
2677
2513
  } catch {
2678
2514
  }
2679
2515
  }
2680
2516
  }
2681
- const remaining = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
2517
+ const remaining = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
2682
2518
  if (remaining.length > MAX_LOG_COUNT) {
2683
2519
  const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
2684
2520
  for (const f of toDelete) {
2685
2521
  try {
2686
- fs3.unlinkSync(path3.join(dir, f));
2522
+ fs2.unlinkSync(path2.join(dir, f));
2687
2523
  } catch {
2688
2524
  }
2689
2525
  }
2690
2526
  }
2691
2527
  let totalSize = 0;
2692
2528
  for (const f of remaining) {
2693
- totalSize += fs3.statSync(path3.join(dir, f)).size;
2529
+ totalSize += fs2.statSync(path2.join(dir, f)).size;
2694
2530
  }
2695
2531
  if (totalSize > MAX_LOG_BYTES) {
2696
2532
  const sorted = remaining.sort();
2697
2533
  for (const f of sorted) {
2698
- const sz = fs3.statSync(path3.join(dir, f)).size;
2534
+ const sz = fs2.statSync(path2.join(dir, f)).size;
2699
2535
  try {
2700
- fs3.unlinkSync(path3.join(dir, f));
2536
+ fs2.unlinkSync(path2.join(dir, f));
2701
2537
  } catch {
2702
2538
  }
2703
2539
  totalSize -= sz;
@@ -2733,8 +2569,8 @@ function writeLogRow(data) {
2733
2569
  }
2734
2570
 
2735
2571
  // src/live/render.ts
2736
- import { execFile, exec as exec2 } from "child_process";
2737
- import chalk5 from "chalk";
2572
+ import { execFile, exec } from "child_process";
2573
+ import chalk4 from "chalk";
2738
2574
 
2739
2575
  // src/anomalies.ts
2740
2576
  var ZSCORE_THRESHOLD = 2.5;
@@ -2874,55 +2710,55 @@ function p2pPanelLines() {
2874
2710
  const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
2875
2711
  const body = [];
2876
2712
  const bindAddr = state.p2pBind || `0.0.0.0:${state.p2pPort}`;
2877
- body.push(`Status: ${state.p2pServerRunning ? chalk5.green("Running") : chalk5.red("Stopped")}`);
2713
+ body.push(`Status: ${state.p2pServerRunning ? chalk4.green("Running") : chalk4.red("Stopped")}`);
2878
2714
  body.push(`Bind: ${bindAddr}`);
2879
2715
  body.push(`Password: ${state.p2pPassword}`);
2880
2716
  body.push("");
2881
2717
  const typeColor = (t) => {
2882
- if (t === "auth") return chalk5.green;
2883
- if (t === "disconnect") return chalk5.yellow;
2884
- if (t === "error" || t === "blocked" || t === "rate_limited") return chalk5.red;
2885
- return chalk5.cyan;
2718
+ if (t === "auth") return chalk4.green;
2719
+ if (t === "disconnect") return chalk4.yellow;
2720
+ if (t === "error" || t === "blocked" || t === "rate_limited") return chalk4.red;
2721
+ return chalk4.cyan;
2886
2722
  };
2887
- body.push(chalk5.dim("Events (most recent):"));
2723
+ body.push(chalk4.dim("Events (most recent):"));
2888
2724
  const recent = events.slice(-20).reverse();
2889
2725
  for (const evt of recent) {
2890
2726
  const time = new Date(evt.ts).toLocaleTimeString();
2891
- body.push(`${chalk5.dim(time)} ${typeColor(evt.type)(evt.type.padEnd(14))} ${evt.detail}`);
2727
+ body.push(`${chalk4.dim(time)} ${typeColor(evt.type)(evt.type.padEnd(14))} ${evt.detail}`);
2892
2728
  }
2893
2729
  const title = `P2P \xB7 ${state.p2pServerRunning ? "ON" : "OFF"} \xB7 ${events.length} events`;
2894
2730
  return { title, body };
2895
2731
  }
2896
2732
  function renderCustomizerOverlay(scrollOffset, maxVisibleItems) {
2897
2733
  const themesList = Object.keys(THEMES);
2898
- const headerLine = chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ");
2734
+ const headerLine = chalk4.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ");
2899
2735
  const totalItems = state.CUSTOMIZER_OPTIONS.length;
2900
2736
  const end = Math.min(scrollOffset + maxVisibleItems, totalItems);
2901
2737
  const lines = [headerLine];
2902
2738
  for (let i = scrollOffset; i < end; i++) {
2903
2739
  const opt = state.CUSTOMIZER_OPTIONS[i];
2904
2740
  const isSelected = i === state.customizerIndex;
2905
- const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
2741
+ const prefix = isSelected ? chalk4.yellow("\u25B6 ") : " ";
2906
2742
  if (opt === "Theme") {
2907
- lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
2743
+ lines.push(`${prefix}${opt}: ${chalk4.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
2908
2744
  } else if (opt === "Graph Mode") {
2909
- lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.graphMode)} [spark, bar]`);
2745
+ lines.push(`${prefix}${opt}: ${chalk4.bold.green(state.graphMode)} [spark, bar]`);
2910
2746
  } else if (opt === "Splash Screen") {
2911
- const status = state.splashEnabled ? chalk5.green("[ON]") : chalk5.red("[OFF]");
2747
+ const status = state.splashEnabled ? chalk4.green("[ON]") : chalk4.red("[OFF]");
2912
2748
  lines.push(`${prefix}${opt}: ${status}`);
2913
2749
  } else if (opt === "Splash Color") {
2914
- lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
2750
+ lines.push(`${prefix}${opt}: ${chalk4.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
2915
2751
  } else if (opt === "Splash Animation") {
2916
- lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
2752
+ lines.push(`${prefix}${opt}: ${chalk4.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
2917
2753
  } else if (opt === "Notifications") {
2918
- const status = state.notificationsEnabled ? chalk5.green("[ON]") : chalk5.red("[OFF]");
2754
+ const status = state.notificationsEnabled ? chalk4.green("[ON]") : chalk4.red("[OFF]");
2919
2755
  lines.push(`${prefix}${opt}: ${status}`);
2920
2756
  } else if (opt === "Temperature Unit") {
2921
- lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
2757
+ lines.push(`${prefix}${opt}: ${chalk4.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
2922
2758
  } else {
2923
2759
  const toggleKey = getToggleKey(opt);
2924
2760
  const isVisible = toggleKey ? toggleKey === "tree" ? state.treeView : state.visiblePanels[toggleKey] !== false : true;
2925
- const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
2761
+ const status = isVisible ? chalk4.green("[VISIBLE]") : chalk4.red("[HIDDEN]");
2926
2762
  lines.push(`${prefix}${opt}: ${status}`);
2927
2763
  }
2928
2764
  }
@@ -2954,7 +2790,7 @@ function render() {
2954
2790
  const rows = state.termHeight || 24;
2955
2791
  const cols = state.termWidth || 80;
2956
2792
  if (cols < 50 || rows < 15) {
2957
- const msg = chalk5.yellow(`pyre ${rows}x${cols} too small. Resize terminal to >= 50x20.`);
2793
+ const msg = chalk4.yellow(`pyre ${rows}x${cols} too small. Resize terminal to >= 50x20.`);
2958
2794
  const centered = Math.max(0, Math.floor((rows - 2) / 2));
2959
2795
  const out = [];
2960
2796
  for (let i = 0; i < centered; i++) out.push("");
@@ -3028,17 +2864,17 @@ function render() {
3028
2864
  if (blankLines > 0) bodyLines.push("");
3029
2865
  bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split("\n"));
3030
2866
  } else if (state.inputMode === "filter") {
3031
- bodyLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
2867
+ bodyLines.push(chalk4.cyan(` Filter processes: ${state.inputBuffer}_`));
3032
2868
  } else if (state.inputMode === "kill") {
3033
- bodyLines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
2869
+ bodyLines.push(chalk4.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
3034
2870
  } else if (state.inputMode === "signal") {
3035
2871
  const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
3036
- const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk5.yellow(s) : chalk5.dim(s)).join(" ");
3037
- bodyLines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
3038
- bodyLines.push(chalk5.dim(` ${sigList}`));
3039
- bodyLines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
2872
+ const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk4.yellow(s) : chalk4.dim(s)).join(" ");
2873
+ bodyLines.push(chalk4.cyan(` Signal: ${state.inputBuffer}_`));
2874
+ bodyLines.push(chalk4.dim(` ${sigList}`));
2875
+ bodyLines.push(chalk4.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
3040
2876
  } else if (state.inputMode === "p2p") {
3041
- bodyLines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
2877
+ bodyLines.push(chalk4.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
3042
2878
  } else if (state.statusMessage) {
3043
2879
  bodyLines.push(` ${state.statusMessage}`);
3044
2880
  }
@@ -3077,14 +2913,14 @@ function footerLine() {
3077
2913
  ["+/-", `${state.interval}s`],
3078
2914
  ["r", state.p2pServerRunning ? "stop p2p" : "start p2p"]
3079
2915
  ];
3080
- const fmt = (list) => list.map(([k, label]) => `${chalk5.hex("#50fa7b").bold(k)} ${chalk5.dim(label)}`).join(chalk5.dim(" | "));
2916
+ const fmt = (list) => list.map(([k, label]) => `${chalk4.hex("#50fa7b").bold(k)} ${chalk4.dim(label)}`).join(chalk4.dim(" | "));
3081
2917
  const str1 = fmt(row1);
3082
2918
  const str2 = fmt(row2);
3083
2919
  const badges = [];
3084
- if (state.paused) badges.push(chalk5.yellow.bold("\u23F8 PAUSED"));
3085
- if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
3086
- if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
3087
- if (state.p2pServerRunning) badges.push(chalk5.green.bold(`P2P ${state.p2pBind || "0.0.0.0"}:${state.p2pPort}`));
2920
+ if (state.paused) badges.push(chalk4.yellow.bold("\u23F8 PAUSED"));
2921
+ if (state.logging) badges.push(chalk4.red.bold("\u25CF REC"));
2922
+ if (state.activePanel !== "grid") badges.push(chalk4.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
2923
+ if (state.p2pServerRunning) badges.push(chalk4.green.bold(`P2P ${state.p2pBind || "0.0.0.0"}:${state.p2pPort}`));
3088
2924
  const badgeStr = badges.join(" ") || " ";
3089
2925
  return [str1, str2, badgeStr];
3090
2926
  }
@@ -3131,7 +2967,7 @@ function checkAlerts(data) {
3131
2967
  for (const a of anomalies) {
3132
2968
  reasons.push(`${a.metric} anomaly (\u03C3=${a.zScore.toFixed(1)})`);
3133
2969
  }
3134
- setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
2970
+ setStatus(chalk4.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
3135
2971
  sendNotification("pyre Alert", reasons.join("; "));
3136
2972
  const alertMsg = reasons.join("; ");
3137
2973
  if (state.webhookUrl) {
@@ -3143,7 +2979,7 @@ function checkAlerts(data) {
3143
2979
  });
3144
2980
  }
3145
2981
  if (state.alertCmd) {
3146
- exec2(state.alertCmd, { env: { ...process.env, PYRE_ALERT: alertMsg } }, () => {
2982
+ exec(state.alertCmd, { env: { ...process.env, PYRE_ALERT: alertMsg } }, () => {
3147
2983
  });
3148
2984
  }
3149
2985
  } else if (!hot && !anomalyTriggered && !watchdogTriggered) {
@@ -3154,9 +2990,9 @@ function checkAlerts(data) {
3154
2990
  // src/p2p/server.ts
3155
2991
  import net from "net";
3156
2992
  import tls from "tls";
3157
- import fs4 from "fs";
2993
+ import fs3 from "fs";
3158
2994
  import crypto2 from "crypto";
3159
- import chalk6 from "chalk";
2995
+ import chalk5 from "chalk";
3160
2996
 
3161
2997
  // src/p2p/protocol.ts
3162
2998
  import crypto from "crypto";
@@ -3299,22 +3135,22 @@ var P2PServer = class {
3299
3135
  async start() {
3300
3136
  if (this.options.auditLog) {
3301
3137
  try {
3302
- fs4.mkdirSync(this.options.auditLog, { recursive: true });
3138
+ fs3.mkdirSync(this.options.auditLog, { recursive: true });
3303
3139
  const logFile = `${this.options.auditLog}/p2p-audit.log`;
3304
- this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
3140
+ this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
3305
3141
  } catch {
3306
- this.log(chalk6.yellow("Warning: could not open audit log"));
3142
+ this.log(chalk5.yellow("Warning: could not open audit log"));
3307
3143
  }
3308
3144
  }
3309
3145
  return new Promise((resolve, reject) => {
3310
3146
  const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
3311
3147
  if (useTLS) {
3312
3148
  const tlsOptions = {
3313
- key: fs4.readFileSync(this.options.tlsKey),
3314
- cert: fs4.readFileSync(this.options.tlsCert)
3149
+ key: fs3.readFileSync(this.options.tlsKey),
3150
+ cert: fs3.readFileSync(this.options.tlsCert)
3315
3151
  };
3316
3152
  if (this.options.tlsCA) {
3317
- tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
3153
+ tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
3318
3154
  }
3319
3155
  this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
3320
3156
  } else {
@@ -3327,13 +3163,13 @@ var P2PServer = class {
3327
3163
  const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
3328
3164
  this.boundAddress = typeof addr === "object" && addr ? addr.address : String(addr);
3329
3165
  const protocol = useTLS ? "TLS" : "TCP";
3330
- this.log(chalk6.green(`P2P server listening on ${bound} (${protocol})`));
3331
- this.log(chalk6.dim(` Password: ${this.options.password}`));
3332
- this.log(chalk6.dim(` Interval: ${this.options.intervalMs}ms`));
3166
+ this.log(chalk5.green(`P2P server listening on ${bound} (${protocol})`));
3167
+ this.log(chalk5.dim(` Password: ${this.options.password}`));
3168
+ this.log(chalk5.dim(` Interval: ${this.options.intervalMs}ms`));
3333
3169
  const allowedIPs = this.options.allowedIPs ?? [];
3334
3170
  const deniedIPs = this.options.deniedIPs ?? [];
3335
- if (allowedIPs.length > 0) this.log(chalk6.dim(` Allowed IPs: ${allowedIPs.join(", ")}`));
3336
- if (deniedIPs.length > 0) this.log(chalk6.dim(` Denied IPs: ${deniedIPs.join(", ")}`));
3171
+ if (allowedIPs.length > 0) this.log(chalk5.dim(` Allowed IPs: ${allowedIPs.join(", ")}`));
3172
+ if (deniedIPs.length > 0) this.log(chalk5.dim(` Denied IPs: ${deniedIPs.join(", ")}`));
3337
3173
  this.emitPeerEvent("connect", `Server listening on ${bound}`);
3338
3174
  resolve();
3339
3175
  });
@@ -3403,14 +3239,14 @@ var P2PServer = class {
3403
3239
  const deniedIPs = this.options.deniedIPs ?? [];
3404
3240
  if (!isIPAllowed(ip, allowedIPs, deniedIPs)) {
3405
3241
  this.audit(ip, "BLOCKED", "IP not allowed");
3406
- this.log(chalk6.yellow(`Blocked IP: ${ip}`));
3242
+ this.log(chalk5.yellow(`Blocked IP: ${ip}`));
3407
3243
  this.emitPeerEvent("blocked", ip);
3408
3244
  socket.destroy();
3409
3245
  return;
3410
3246
  }
3411
3247
  if (!this.checkRateLimit(ip)) {
3412
3248
  this.audit(ip, "RATE_LIMITED", "Too many auth attempts");
3413
- this.log(chalk6.yellow(`Rate limit exceeded for IP: ${ip}`));
3249
+ this.log(chalk5.yellow(`Rate limit exceeded for IP: ${ip}`));
3414
3250
  this.emitPeerEvent("rate_limited", ip);
3415
3251
  socket.destroy();
3416
3252
  return;
@@ -3428,7 +3264,7 @@ var P2PServer = class {
3428
3264
  };
3429
3265
  this.peers.set(socket, peer);
3430
3266
  const remoteAddr = `${ip}:${socket.remotePort ?? "?"}`;
3431
- this.log(chalk6.cyan(`Peer connected: ${remoteAddr}`));
3267
+ this.log(chalk5.cyan(`Peer connected: ${remoteAddr}`));
3432
3268
  this.audit(ip, "CONNECT", remoteAddr);
3433
3269
  this.emitPeerEvent("connect", remoteAddr);
3434
3270
  socket.on("data", (data) => this.onData(peer, data));
@@ -3462,7 +3298,7 @@ var P2PServer = class {
3462
3298
  this.sendSigned(peer, signMessage("auth-fail", { ok: false, reason: "Expected auth message first" }, this.hmacKey));
3463
3299
  this.disconnectPeer(peer, "Expected auth message");
3464
3300
  this.emitPeerEvent("error", `Expected auth message from ${peer.ip}`);
3465
- this.log(chalk6.red(`Auth failed from ${peer.ip}: expected auth message first`));
3301
+ this.log(chalk5.red(`Auth failed from ${peer.ip}: expected auth message first`));
3466
3302
  return;
3467
3303
  }
3468
3304
  const authPayload = msg.payload;
@@ -3470,7 +3306,7 @@ var P2PServer = class {
3470
3306
  this.sendSigned(peer, signMessage("auth-fail", { ok: false, reason: "Invalid auth payload" }, this.hmacKey));
3471
3307
  this.disconnectPeer(peer, "Invalid auth payload");
3472
3308
  this.emitPeerEvent("error", `Invalid auth payload from ${peer.ip}`);
3473
- this.log(chalk6.red(`Auth failed from ${peer.ip}: invalid auth payload`));
3309
+ this.log(chalk5.red(`Auth failed from ${peer.ip}: invalid auth payload`));
3474
3310
  return;
3475
3311
  }
3476
3312
  if (peer.pendingNonce && authPayload.nonce !== peer.pendingNonce) {
@@ -3478,7 +3314,7 @@ var P2PServer = class {
3478
3314
  this.sendSigned(peer, signMessage("auth-fail", { ok: false, reason: "Invalid nonce" }, this.hmacKey));
3479
3315
  this.disconnectPeer(peer, "Invalid nonce");
3480
3316
  this.emitPeerEvent("error", `Invalid nonce from ${peer.ip}`);
3481
- this.log(chalk6.red(`Auth failed from ${peer.ip}: invalid nonce`));
3317
+ this.log(chalk5.red(`Auth failed from ${peer.ip}: invalid nonce`));
3482
3318
  return;
3483
3319
  }
3484
3320
  const expectedHash = hashPassword(this.options.password, authPayload.nonce);
@@ -3487,14 +3323,14 @@ var P2PServer = class {
3487
3323
  this.sendSigned(peer, signMessage("auth-fail", { ok: false, reason: "Invalid password" }, this.hmacKey));
3488
3324
  this.disconnectPeer(peer, "Authentication failed");
3489
3325
  this.emitPeerEvent("error", `Invalid password from ${peer.ip}`);
3490
- this.log(chalk6.red(`Auth failed from ${peer.ip}: invalid password`));
3326
+ this.log(chalk5.red(`Auth failed from ${peer.ip}: invalid password`));
3491
3327
  return;
3492
3328
  }
3493
3329
  peer.authenticated = true;
3494
3330
  this.cleanupRateLimit(peer.ip);
3495
3331
  peer.pendingNonce = null;
3496
3332
  this.sendSigned(peer, signMessage("auth-ok", { ok: true }, this.hmacKey));
3497
- this.log(chalk6.green(`Peer authenticated: ${peer.ip}`));
3333
+ this.log(chalk5.green(`Peer authenticated: ${peer.ip}`));
3498
3334
  this.audit(peer.ip, "AUTH_OK", "Challenge-response successful");
3499
3335
  this.emitPeerEvent("auth", peer.ip);
3500
3336
  this.startDataStream(peer);
@@ -3502,7 +3338,7 @@ var P2PServer = class {
3502
3338
  } catch {
3503
3339
  this.sendSigned(peer, signMessage("auth-fail", { ok: false, reason: "Invalid message format" }, this.hmacKey));
3504
3340
  this.disconnectPeer(peer, "Invalid message format");
3505
- this.log(chalk6.red(`Auth failed from ${peer.ip}: invalid message format`));
3341
+ this.log(chalk5.red(`Auth failed from ${peer.ip}: invalid message format`));
3506
3342
  }
3507
3343
  }
3508
3344
  sendSigned(peer, message) {
@@ -3534,7 +3370,7 @@ var P2PServer = class {
3534
3370
  }
3535
3371
  onPeerClose(peer) {
3536
3372
  this.cleanupPeer(peer);
3537
- this.log(chalk6.yellow(`Peer disconnected: ${peer.ip}`));
3373
+ this.log(chalk5.yellow(`Peer disconnected: ${peer.ip}`));
3538
3374
  this.emitPeerEvent("disconnect", peer.ip);
3539
3375
  this.audit(peer.ip, "DISCONNECT", "Peer closed connection");
3540
3376
  }
@@ -3542,13 +3378,13 @@ var P2PServer = class {
3542
3378
  const addr = peer.socket.remoteAddress ?? peer.ip;
3543
3379
  const code = err.code;
3544
3380
  if (code === "ECONNRESET") {
3545
- this.log(chalk6.yellow(`Peer connection reset: ${addr}`));
3381
+ this.log(chalk5.yellow(`Peer connection reset: ${addr}`));
3546
3382
  this.audit(peer.ip, "ERROR", "Connection reset");
3547
3383
  this.emitPeerEvent("error", `connection reset: ${addr}`);
3548
3384
  } else if (code === "EPIPE") {
3549
3385
  this.audit(peer.ip, "ERROR", "Broken pipe");
3550
3386
  } else {
3551
- this.log(chalk6.red(`Peer error: ${err.message}`));
3387
+ this.log(chalk5.red(`Peer error: ${err.message}`));
3552
3388
  this.audit(peer.ip, "ERROR", err.message);
3553
3389
  this.emitPeerEvent("error", err.message);
3554
3390
  }
@@ -3582,9 +3418,9 @@ async function startP2PServer(options) {
3582
3418
  // src/p2p/client.ts
3583
3419
  import net2 from "net";
3584
3420
  import tls2 from "tls";
3585
- import fs5 from "fs";
3421
+ import fs4 from "fs";
3586
3422
  import crypto3 from "crypto";
3587
- import chalk7 from "chalk";
3423
+ import chalk6 from "chalk";
3588
3424
  var P2PClient = class {
3589
3425
  socket = null;
3590
3426
  options;
@@ -3610,11 +3446,11 @@ var P2PClient = class {
3610
3446
  async connect() {
3611
3447
  if (this.options.auditLog) {
3612
3448
  try {
3613
- fs5.mkdirSync(this.options.auditLog, { recursive: true });
3449
+ fs4.mkdirSync(this.options.auditLog, { recursive: true });
3614
3450
  const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
3615
- this.auditStream = fs5.createWriteStream(logFile, { flags: "a" });
3451
+ this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
3616
3452
  } catch {
3617
- console.log(chalk7.yellow("Warning: could not open audit log"));
3453
+ console.log(chalk6.yellow("Warning: could not open audit log"));
3618
3454
  }
3619
3455
  }
3620
3456
  const host = sanitizeHost(this.options.host);
@@ -3627,7 +3463,7 @@ var P2PClient = class {
3627
3463
  rejectUnauthorized: !this.options.tlsInsecure
3628
3464
  };
3629
3465
  if (this.options.tlsCA) {
3630
- tlsOptions.ca = fs5.readFileSync(this.options.tlsCA);
3466
+ tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
3631
3467
  }
3632
3468
  this.socket = tls2.connect(tlsOptions, () => {
3633
3469
  this.running = true;
@@ -3841,10 +3677,10 @@ function persistConfig() {
3841
3677
  });
3842
3678
  }
3843
3679
  function isUpKey(key, str) {
3844
- return key.name === "up" || str === "k" || key.sequence === "\x1B[A" || key.sequence === "\x1BOA";
3680
+ return key.name === "up" || key.sequence === "\x1B[A" || key.sequence === "\x1BOA";
3845
3681
  }
3846
3682
  function isDownKey(key, str) {
3847
- return key.name === "down" || str === "j" || key.sequence === "\x1B[B" || key.sequence === "\x1BOB";
3683
+ return key.name === "down" || key.sequence === "\x1B[B" || key.sequence === "\x1BOB";
3848
3684
  }
3849
3685
  function isEnterKey(key, str) {
3850
3686
  return key.name === "return" || key.name === "enter" || key.sequence === "\r" || key.sequence === "\n" || str === "\r" || str === "\n";
@@ -3997,12 +3833,21 @@ function handleInputModeKey(str, key) {
3997
3833
  render();
3998
3834
  }
3999
3835
  }
3836
+ var confirmKillPid = null;
4000
3837
  function killProcess(pidStr, signal = "SIGTERM") {
4001
3838
  const pid = parseInt(pidStr, 10);
4002
3839
  if (!pid || pid <= 0) {
4003
3840
  setStatus(`Invalid PID: ${pidStr}`);
4004
3841
  return;
4005
3842
  }
3843
+ const isProtected = pid === 1 || pid === process.pid || pid === process.ppid;
3844
+ if (isProtected && confirmKillPid !== pid) {
3845
+ confirmKillPid = pid;
3846
+ const name = pid === 1 ? "launchd (system init)" : pid === process.pid ? "pyre itself" : "parent process";
3847
+ setStatus(`\u26A0\uFE0F PROTECTED PID ${pid} (${name})! Re-enter PID ${pid} to confirm termination.`, 8e3);
3848
+ return;
3849
+ }
3850
+ confirmKillPid = null;
4006
3851
  try {
4007
3852
  process.kill(pid, signal);
4008
3853
  setStatus(`Sent ${signal} to PID ${pid}`);
@@ -4425,7 +4270,7 @@ function stopLive() {
4425
4270
  }
4426
4271
 
4427
4272
  // src/splash.ts
4428
- import chalk8 from "chalk";
4273
+ import chalk7 from "chalk";
4429
4274
  var MAXV = 36;
4430
4275
  var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
4431
4276
  var PALETTES = {
@@ -4729,12 +4574,12 @@ async function showSplash(config3) {
4729
4574
  for (let x = 0; x < cols; x++) {
4730
4575
  const v = fire[y * cols + x];
4731
4576
  const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
4732
- output += chalk8.rgb(color.r, color.g, color.b)(getChar(v));
4577
+ output += chalk7.rgb(color.r, color.g, color.b)(getChar(v));
4733
4578
  }
4734
4579
  output += "\n";
4735
4580
  }
4736
- output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk8.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
4737
- output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk8.dim("[Press Enter to continue]");
4581
+ output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk7.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
4582
+ output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk7.dim("[Press Enter to continue]");
4738
4583
  process.stdout.write(`\x1B[H${output}`);
4739
4584
  }, 65);
4740
4585
  const onEnter = () => {
@@ -4751,22 +4596,22 @@ async function showSplash(config3) {
4751
4596
  }
4752
4597
 
4753
4598
  // src/historyCmd.ts
4754
- import fs6 from "fs";
4755
- import path4 from "path";
4756
- import chalk9 from "chalk";
4599
+ import fs5 from "fs";
4600
+ import path3 from "path";
4601
+ import chalk8 from "chalk";
4757
4602
  function runHistoryCommand(opts2 = {}) {
4758
4603
  const days = opts2.days || 7;
4759
4604
  const dir = opts2.dir || "./pyre-exports";
4760
- console.log(chalk9.bold(`
4605
+ console.log(chalk8.bold(`
4761
4606
  pyre history \u2014 Past ${days} Days Trend
4762
4607
  `));
4763
- if (!fs6.existsSync(dir)) {
4764
- console.log(chalk9.yellow(` No logs directory found at ${dir}`));
4608
+ if (!fs5.existsSync(dir)) {
4609
+ console.log(chalk8.yellow(` No logs directory found at ${dir}`));
4765
4610
  return;
4766
4611
  }
4767
- const files = fs6.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
4612
+ const files = fs5.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
4768
4613
  if (files.length === 0) {
4769
- console.log(chalk9.yellow(` No CSV logs found in ${dir}`));
4614
+ console.log(chalk8.yellow(` No CSV logs found in ${dir}`));
4770
4615
  return;
4771
4616
  }
4772
4617
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
@@ -4774,9 +4619,9 @@ function runHistoryCommand(opts2 = {}) {
4774
4619
  const memPoints = [];
4775
4620
  const tempPoints = [];
4776
4621
  for (const file of files.sort()) {
4777
- const fullPath = path4.join(dir, file);
4622
+ const fullPath = path3.join(dir, file);
4778
4623
  try {
4779
- const content = fs6.readFileSync(fullPath, "utf-8");
4624
+ const content = fs5.readFileSync(fullPath, "utf-8");
4780
4625
  const lines = content.split("\n").slice(1);
4781
4626
  for (const line of lines) {
4782
4627
  if (!line.trim()) continue;
@@ -4798,7 +4643,7 @@ function runHistoryCommand(opts2 = {}) {
4798
4643
  }
4799
4644
  }
4800
4645
  if (cpuPoints.length === 0) {
4801
- console.log(chalk9.yellow(` No historical entries recorded within the last ${days} days.`));
4646
+ console.log(chalk8.yellow(` No historical entries recorded within the last ${days} days.`));
4802
4647
  return;
4803
4648
  }
4804
4649
  console.log(` Data Points: ${cpuPoints.length} logged snapshots`);
@@ -4811,22 +4656,22 @@ function runHistoryCommand(opts2 = {}) {
4811
4656
  }
4812
4657
 
4813
4658
  // src/diffCmd.ts
4814
- import fs7 from "fs";
4815
- import chalk10 from "chalk";
4659
+ import fs6 from "fs";
4660
+ import chalk9 from "chalk";
4816
4661
  function runDiffCommand(file1, file2) {
4817
- console.log(chalk10.bold(`
4662
+ console.log(chalk9.bold(`
4818
4663
  pyre diff \u2014 Snapshot Comparison
4819
4664
  `));
4820
- console.log(chalk10.dim(` Snapshot A: ${file1}`));
4821
- console.log(chalk10.dim(` Snapshot B: ${file2}
4665
+ console.log(chalk9.dim(` Snapshot A: ${file1}`));
4666
+ console.log(chalk9.dim(` Snapshot B: ${file2}
4822
4667
  `));
4823
4668
  try {
4824
- const dataA = JSON.parse(fs7.readFileSync(file1, "utf-8"));
4825
- const dataB = JSON.parse(fs7.readFileSync(file2, "utf-8"));
4669
+ const dataA = JSON.parse(fs6.readFileSync(file1, "utf-8"));
4670
+ const dataB = JSON.parse(fs6.readFileSync(file2, "utf-8"));
4826
4671
  const diffNum = (label, valA, valB, unit = "") => {
4827
4672
  const delta = valB - valA;
4828
4673
  const sign = delta > 0 ? "+" : "";
4829
- const color = delta > 0 ? chalk10.red : delta < 0 ? chalk10.green : chalk10.dim;
4674
+ const color = delta > 0 ? chalk9.red : delta < 0 ? chalk9.green : chalk9.dim;
4830
4675
  console.log(` ${label.padEnd(20)} ${valA.toFixed(1)}${unit} \u2794 ${valB.toFixed(1)}${unit} (${color(sign + delta.toFixed(1) + unit)})`);
4831
4676
  };
4832
4677
  diffNum("CPU Usage", dataA.cpu.usage, dataB.cpu.usage, "%");
@@ -4841,15 +4686,15 @@ function runDiffCommand(file1, file2) {
4841
4686
  diffNum("Network TX Packets", dataA.network.txPackets, dataB.network.txPackets);
4842
4687
  console.log();
4843
4688
  } catch (err) {
4844
- console.log(chalk10.red(`Failed to compare snapshots: ${err.message}`));
4689
+ console.log(chalk9.red(`Failed to compare snapshots: ${err.message}`));
4845
4690
  }
4846
4691
  }
4847
4692
 
4848
4693
  // src/xbar.ts
4849
- import fs8 from "fs";
4850
- import path5 from "path";
4851
- import os4 from "os";
4852
- import chalk11 from "chalk";
4694
+ import fs7 from "fs";
4695
+ import path4 from "path";
4696
+ import os2 from "os";
4697
+ import chalk10 from "chalk";
4853
4698
  function generateXbarPlugin() {
4854
4699
  const pyrePath = process.argv[1];
4855
4700
  const nodePath = process.execPath;
@@ -4884,22 +4729,22 @@ echo "\u{1F525} C:\${CPU}% M:\${MEM}% T:\${TEMP}\xB0C | font=Menlo"
4884
4729
  echo "---"
4885
4730
  echo "Open Dashboard | bash='${nodePath}' param1='${pyrePath}' param2='live' terminal=true"
4886
4731
  `;
4887
- const pluginDir = path5.join(os4.homedir(), "Library", "Application Support", "xbar", "plugins");
4732
+ const pluginDir = path4.join(os2.homedir(), "Library", "Application Support", "xbar", "plugins");
4888
4733
  const filename = "pyre-monitor.2s.sh";
4889
- const outPath = path5.join(process.cwd(), filename);
4890
- fs8.writeFileSync(outPath, script);
4891
- fs8.chmodSync(outPath, "755");
4892
- console.log(chalk11.green(`Generated xbar plugin script at ${outPath}`));
4734
+ const outPath = path4.join(process.cwd(), filename);
4735
+ fs7.writeFileSync(outPath, script);
4736
+ fs7.chmodSync(outPath, "755");
4737
+ console.log(chalk10.green(`Generated xbar plugin script at ${outPath}`));
4893
4738
  console.log(`To install, move it to your xbar plugins directory:`);
4894
- console.log(chalk11.cyan(` mv ${filename} "${pluginDir}/"`));
4739
+ console.log(chalk10.cyan(` mv ${filename} "${pluginDir}/"`));
4895
4740
  console.log(`Then refresh xbar.`);
4896
4741
  }
4897
4742
 
4898
4743
  // src/fleet.ts
4899
4744
  import { execSync } from "child_process";
4900
- import chalk12 from "chalk";
4745
+ import chalk11 from "chalk";
4901
4746
  function runFleetCommand(hosts) {
4902
- console.log(chalk12.cyan(`Fetching data from ${hosts.length} hosts...`));
4747
+ console.log(chalk11.cyan(`Fetching data from ${hosts.length} hosts...`));
4903
4748
  const results = [];
4904
4749
  for (const host of hosts) {
4905
4750
  try {
@@ -4911,11 +4756,11 @@ function runFleetCommand(hosts) {
4911
4756
  }
4912
4757
  }
4913
4758
  for (const result of results) {
4914
- console.log(chalk12.bold.blue(`
4759
+ console.log(chalk11.bold.blue(`
4915
4760
  === ${result.host} ===
4916
4761
  `));
4917
4762
  if (result.error) {
4918
- console.log(chalk12.red(`Error connecting to host: ${result.error}`));
4763
+ console.log(chalk11.red(`Error connecting to host: ${result.error}`));
4919
4764
  } else if (result.data) {
4920
4765
  console.log(formatTable(result.data, { width: process.stdout.columns || 80, activePanel: "grid" }));
4921
4766
  }
@@ -4924,11 +4769,14 @@ function runFleetCommand(hosts) {
4924
4769
 
4925
4770
  // src/index.ts
4926
4771
  var config2 = readConfig();
4927
- var pkg = JSON.parse(fs9.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
4772
+ var pkg = JSON.parse(fs8.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
4928
4773
  var program = new Command();
4929
- program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).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", config2.exportDir).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", config2.sortMode).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("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)").option("--webhook-url <url>", "URL to POST to on alert").option("--alert-cmd <cmd>", "Command to execute on alert").option("--port <port>", "Port number for web server mode", "3000");
4774
+ program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).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", config2.exportDir).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", config2.sortMode).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("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)").option("--since <range>", "Time range for anomalies digest (e.g. 1d, 7d, yesterday)").option("--plain", "Output in plain accessible text without ANSI colors or line-drawing characters").option("--a11y", "Alias for --plain").option("--webhook-url <url>", "URL to POST to on alert").option("--alert-cmd <cmd>", "Command to execute on alert").option("--port <port>", "Port number for web server mode", "3000");
4930
4775
  program.parse(process.argv);
4931
4776
  var opts = program.opts();
4777
+ if (opts.plain || opts.a11y) {
4778
+ chalk12.level = 0;
4779
+ }
4932
4780
  function isExportMode() {
4933
4781
  return opts.json || opts.csv || opts.tsv || opts.html || opts.md;
4934
4782
  }
@@ -4939,7 +4787,7 @@ function sanitizeHost2(host) {
4939
4787
  return host.trim().replace(/%$/, "");
4940
4788
  }
4941
4789
  function detectLocalIP() {
4942
- const ifaces = os5.networkInterfaces();
4790
+ const ifaces = os3.networkInterfaces();
4943
4791
  const prioritizedNames = ["en0", "en1", "wlan0", "eth0"];
4944
4792
  for (const name of prioritizedNames) {
4945
4793
  const addrs = ifaces[name];
@@ -4964,26 +4812,76 @@ function detectLocalIP() {
4964
4812
  async function runServerCommand() {
4965
4813
  const ip = detectLocalIP();
4966
4814
  if (!ip) {
4967
- console.log(chalk13.red("Could not detect a non-loopback IP address. Connect to a network first."));
4815
+ console.log(chalk12.red("Could not detect a non-loopback IP address. Connect to a network first."));
4968
4816
  process.exit(1);
4969
4817
  }
4970
4818
  const port = 9876;
4971
4819
  const password = "mysecret";
4972
- console.log(chalk13.bold(`
4820
+ console.log(chalk12.bold(`
4973
4821
  pyre server`));
4974
- console.log(chalk13.dim(` Detected IP: ${ip}`));
4975
- console.log(chalk13.dim(` Port: ${port}`));
4976
- console.log(chalk13.dim(` Password: ${password}`));
4822
+ console.log(chalk12.dim(` Detected IP: ${ip}`));
4823
+ console.log(chalk12.dim(` Port: ${port}`));
4824
+ console.log(chalk12.dim(` Password: ${password}`));
4977
4825
  console.log();
4978
- console.log(chalk13.bold("Run this on THIS machine (server):"));
4979
- console.log(chalk13.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
4826
+ console.log(chalk12.bold("Run this on THIS machine (server):"));
4827
+ console.log(chalk12.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
4980
4828
  console.log();
4981
- console.log(chalk13.bold("Run this on the OTHER machine (client):"));
4982
- console.log(chalk13.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
4829
+ console.log(chalk12.bold("Run this on the OTHER machine (client):"));
4830
+ console.log(chalk12.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
4983
4831
  console.log();
4984
4832
  }
4985
4833
  async function main() {
4986
4834
  const cmd = program.args[0];
4835
+ if (cmd === "anomalies") {
4836
+ const { runAnomaliesDigest } = await import("./anomaliesCmd-TO52YY2B.js");
4837
+ runAnomaliesDigest({ since: opts.since || "7d", dir: opts.exportDir });
4838
+ return;
4839
+ }
4840
+ if (cmd === "extensions") {
4841
+ const { printExtensionsReport } = await import("./extensions-QCFSVCL5.js");
4842
+ await printExtensionsReport();
4843
+ return;
4844
+ }
4845
+ if (cmd === "brew") {
4846
+ const { printBrewHealthReport } = await import("./brewHealth-6NE37NAU.js");
4847
+ await printBrewHealthReport();
4848
+ return;
4849
+ }
4850
+ if (cmd === "update") {
4851
+ const { printUpdateReport } = await import("./updateCheck-52X6LJG5.js");
4852
+ await printUpdateReport(pkg.version);
4853
+ return;
4854
+ }
4855
+ if (cmd === "profile") {
4856
+ const sub = program.args[1];
4857
+ const name = program.args[2];
4858
+ const { saveProfile, loadProfile, listProfiles } = await import("./config-4ZLF4HTS.js");
4859
+ if (sub === "save" && name) {
4860
+ saveProfile(name);
4861
+ console.log(chalk12.green(` \u2714 Profile '${name}' saved successfully.`));
4862
+ } else if (sub === "load" && name) {
4863
+ try {
4864
+ loadProfile(name);
4865
+ console.log(chalk12.green(` \u2714 Profile '${name}' loaded successfully.`));
4866
+ } catch (err) {
4867
+ console.log(chalk12.red(` \u2716 ${err.message}`));
4868
+ }
4869
+ } else if (sub === "list") {
4870
+ const profiles = listProfiles();
4871
+ console.log(chalk12.bold("\n pyre Profiles:\n"));
4872
+ if (profiles.length === 0) {
4873
+ console.log(chalk12.dim(" No saved profiles found. Save one with `pyre profile save <name>`."));
4874
+ } else {
4875
+ for (const p of profiles) {
4876
+ console.log(` \u2022 ${p}`);
4877
+ }
4878
+ }
4879
+ console.log();
4880
+ } else {
4881
+ console.log("Usage: pyre profile <save|load|list> [name]");
4882
+ }
4883
+ return;
4884
+ }
4987
4885
  if (cmd === "history") {
4988
4886
  const daysIdx = program.args.indexOf("--days");
4989
4887
  const days = daysIdx !== -1 ? parseInt(program.args[daysIdx + 1], 10) || 7 : 7;
@@ -4994,7 +4892,7 @@ async function main() {
4994
4892
  const file1 = program.args[1];
4995
4893
  const file2 = program.args[2];
4996
4894
  if (!file1 || !file2) {
4997
- console.log(chalk13.red("Usage: pyre diff <snapshot1.json> <snapshot2.json>"));
4895
+ console.log(chalk12.red("Usage: pyre diff <snapshot1.json> <snapshot2.json>"));
4998
4896
  process.exit(1);
4999
4897
  }
5000
4898
  runDiffCommand(file1, file2);
@@ -5003,7 +4901,7 @@ async function main() {
5003
4901
  if (cmd === "ssh") {
5004
4902
  const host = program.args[1];
5005
4903
  if (!host) {
5006
- console.log(chalk13.red("Usage: pyre ssh <host>"));
4904
+ console.log(chalk12.red("Usage: pyre ssh <host>"));
5007
4905
  process.exit(1);
5008
4906
  }
5009
4907
  await runSshCommand(host);
@@ -5013,10 +4911,14 @@ async function main() {
5013
4911
  await runWebCommand();
5014
4912
  return;
5015
4913
  }
4914
+ if (cmd === "ui") {
4915
+ await runUiCommand();
4916
+ return;
4917
+ }
5016
4918
  if (cmd === "bench") {
5017
4919
  const benchCmd = program.args.slice(1).join(" ");
5018
4920
  if (!benchCmd) {
5019
- console.log(chalk13.red("Usage: pyre bench <command>"));
4921
+ console.log(chalk12.red("Usage: pyre bench <command>"));
5020
4922
  process.exit(1);
5021
4923
  }
5022
4924
  await runBenchCommand(benchCmd);
@@ -5028,8 +4930,8 @@ async function main() {
5028
4930
  const cfg = readConfig();
5029
4931
  console.log(JSON.stringify(cfg, null, 2));
5030
4932
  } else if (sub === "reset") {
5031
- fs9.rmSync(CONFIG_FILE, { force: true });
5032
- console.log(chalk13.green("Config reset to defaults"));
4933
+ fs8.rmSync(CONFIG_FILE, { force: true });
4934
+ console.log(chalk12.green("Config reset to defaults"));
5033
4935
  } else {
5034
4936
  console.log("Usage: pyre config <show|reset>");
5035
4937
  }
@@ -5043,10 +4945,26 @@ async function main() {
5043
4945
  generateXbarPlugin();
5044
4946
  return;
5045
4947
  }
4948
+ if (cmd === "doctor") {
4949
+ const { runDoctor, printDoctorReport } = await import("./doctor-LJQAABZ4.js");
4950
+ const checks = await runDoctor();
4951
+ printDoctorReport(checks);
4952
+ return;
4953
+ }
4954
+ if (cmd === "info") {
4955
+ await runInfoCommand();
4956
+ return;
4957
+ }
4958
+ if (cmd === "completions") {
4959
+ const { printCompletions } = await import("./completions-VQMHQCXE.js");
4960
+ const shell = program.args[1] || "zsh";
4961
+ printCompletions(shell);
4962
+ return;
4963
+ }
5046
4964
  if (cmd === "fleet") {
5047
4965
  const hosts = program.args.slice(1);
5048
4966
  if (hosts.length === 0) {
5049
- console.error(chalk13.red("Usage: pyre fleet <host1> [host2] ..."));
4967
+ console.error(chalk12.red("Usage: pyre fleet <host1> [host2] ..."));
5050
4968
  process.exit(1);
5051
4969
  }
5052
4970
  runFleetCommand(hosts);
@@ -5093,7 +5011,7 @@ async function main() {
5093
5011
  await runP2PConnect();
5094
5012
  return;
5095
5013
  } else {
5096
- console.log(chalk13.red("Usage: pyre p2p <server|connect> [options]"));
5014
+ console.log(chalk12.red("Usage: pyre p2p <server|connect> [options]"));
5097
5015
  process.exit(1);
5098
5016
  }
5099
5017
  }
@@ -5109,13 +5027,13 @@ async function main() {
5109
5027
  else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config2.treeView, visible: { packets: opts.packets ? true : void 0 } });
5110
5028
  console.log(output);
5111
5029
  if (opts.out) {
5112
- fs9.writeFileSync(opts.out, stripAnsi(output));
5113
- console.log(chalk13.dim(`
5030
+ fs8.writeFileSync(opts.out, stripAnsi(output));
5031
+ console.log(chalk12.dim(`
5114
5032
  Saved to ${opts.out}`));
5115
5033
  }
5116
5034
  }
5117
5035
  main().catch((err) => {
5118
- console.error(chalk13.red(`Error: ${err.message}`));
5036
+ console.error(chalk12.red(`Error: ${err.message}`));
5119
5037
  process.exit(1);
5120
5038
  });
5121
5039
  async function runP2PServer() {
@@ -5131,15 +5049,15 @@ async function runP2PServer() {
5131
5049
  const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
5132
5050
  const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
5133
5051
  if (!password) {
5134
- console.log(chalk13.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
5052
+ console.log(chalk12.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
5135
5053
  process.exit(1);
5136
5054
  }
5137
5055
  if (tlsCert && !tlsKey) {
5138
- console.log(chalk13.red("Error: --p2p-key is required when --p2p-cert is specified"));
5056
+ console.log(chalk12.red("Error: --p2p-key is required when --p2p-cert is specified"));
5139
5057
  process.exit(1);
5140
5058
  }
5141
5059
  if (tlsKey && !tlsCert) {
5142
- console.log(chalk13.red("Error: --p2p-cert is required when --p2p-key is specified"));
5060
+ console.log(chalk12.red("Error: --p2p-cert is required when --p2p-key is specified"));
5143
5061
  process.exit(1);
5144
5062
  }
5145
5063
  const server = new P2PServer({
@@ -5159,18 +5077,18 @@ async function runP2PServer() {
5159
5077
  hmacKey: opts.p2pHmacKey
5160
5078
  });
5161
5079
  const tlsEnabled = !!(tlsCert && tlsKey);
5162
- console.log(chalk13.bold(`
5080
+ console.log(chalk12.bold(`
5163
5081
  pyre P2P Server`));
5164
- console.log(chalk13.dim(` Host: ${host}`));
5165
- console.log(chalk13.dim(` Port: ${port}`));
5166
- console.log(chalk13.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
5167
- console.log(chalk13.dim(` Password: ${password}`));
5168
- console.log(chalk13.dim(` Interval: ${interval}s`));
5169
- console.log(chalk13.dim(` Detailed: ${detailed ? "on" : "off"}`));
5170
- console.log(chalk13.dim(` Rate limit: ${rateLimit} attempts/min`));
5171
- if (allowList.length > 0) console.log(chalk13.dim(` Allowed IPs: ${allowList.join(", ")}`));
5172
- if (denyList.length > 0) console.log(chalk13.dim(` Denied IPs: ${denyList.join(", ")}`));
5173
- console.log(chalk13.dim(` Waiting for peers...
5082
+ console.log(chalk12.dim(` Host: ${host}`));
5083
+ console.log(chalk12.dim(` Port: ${port}`));
5084
+ console.log(chalk12.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
5085
+ console.log(chalk12.dim(` Password: ${password}`));
5086
+ console.log(chalk12.dim(` Interval: ${interval}s`));
5087
+ console.log(chalk12.dim(` Detailed: ${detailed ? "on" : "off"}`));
5088
+ console.log(chalk12.dim(` Rate limit: ${rateLimit} attempts/min`));
5089
+ if (allowList.length > 0) console.log(chalk12.dim(` Allowed IPs: ${allowList.join(", ")}`));
5090
+ if (denyList.length > 0) console.log(chalk12.dim(` Denied IPs: ${denyList.join(", ")}`));
5091
+ console.log(chalk12.dim(` Waiting for peers...
5174
5092
  `));
5175
5093
  await server.start();
5176
5094
  const rl = readline2.createInterface({ input: process.stdin });
@@ -5182,7 +5100,7 @@ async function runP2PServer() {
5182
5100
  process.exit(0);
5183
5101
  } else if (trimmed === "status") {
5184
5102
  const info = server.info;
5185
- console.log(chalk13.dim(` Peers connected: ${info.peersConnected}`));
5103
+ console.log(chalk12.dim(` Peers connected: ${info.peersConnected}`));
5186
5104
  }
5187
5105
  });
5188
5106
  process.once("SIGINT", () => {
@@ -5215,7 +5133,7 @@ async function runP2PConnect() {
5215
5133
  const tlsCA = opts.p2pCA;
5216
5134
  const tlsInsecure = !!opts.p2pInsecure;
5217
5135
  if (!password) {
5218
- console.log(chalk13.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
5136
+ console.log(chalk12.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
5219
5137
  process.exit(1);
5220
5138
  }
5221
5139
  const client = new P2PClient({
@@ -5229,12 +5147,12 @@ async function runP2PConnect() {
5229
5147
  hmacKey: opts.p2pHmacKey
5230
5148
  });
5231
5149
  client.setOnStatus((msg) => {
5232
- process.stdout.write(`\r${chalk13.dim(msg)}${" ".repeat(40)}\r`);
5150
+ process.stdout.write(`\r${chalk12.dim(msg)}${" ".repeat(40)}\r`);
5233
5151
  });
5234
5152
  client.setOnData((data) => {
5235
5153
  const lines = [];
5236
- lines.push(chalk13.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
5237
- lines.push(chalk13.dim(` ${data.timestamp}`));
5154
+ lines.push(chalk12.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
5155
+ lines.push(chalk12.dim(` ${data.timestamp}`));
5238
5156
  lines.push("");
5239
5157
  lines.push(...formatTable(data, {
5240
5158
  width: process.stdout.columns || 80,
@@ -5251,11 +5169,11 @@ async function runP2PConnect() {
5251
5169
  writeFrame2(lines);
5252
5170
  });
5253
5171
  const protocolLabel = useTLS ? "TLS" : "TCP";
5254
- console.log(chalk13.bold(`
5172
+ console.log(chalk12.bold(`
5255
5173
  pyre P2P Client`));
5256
- console.log(chalk13.dim(` Protocol: ${protocolLabel}`));
5257
- console.log(chalk13.dim(` Connecting to ${host}:${port}...`));
5258
- console.log(chalk13.dim(` Press Ctrl+C to disconnect
5174
+ console.log(chalk12.dim(` Protocol: ${protocolLabel}`));
5175
+ console.log(chalk12.dim(` Connecting to ${host}:${port}...`));
5176
+ console.log(chalk12.dim(` Press Ctrl+C to disconnect
5259
5177
  `));
5260
5178
  process.stdout.write("\x1B[?25l");
5261
5179
  try {
@@ -5263,23 +5181,23 @@ async function runP2PConnect() {
5263
5181
  } catch (err) {
5264
5182
  const code = err.code;
5265
5183
  const msg = err.message || String(err);
5266
- console.log(chalk13.red(`Connection failed: ${msg}`));
5184
+ console.log(chalk12.red(`Connection failed: ${msg}`));
5267
5185
  if (code === "ECONNREFUSED") {
5268
- console.log(chalk13.yellow(" Hints:"));
5269
- console.log(chalk13.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
5270
- console.log(chalk13.dim(" 2. Check the server IP with: hostname -I"));
5271
- console.log(chalk13.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
5186
+ console.log(chalk12.yellow(" Hints:"));
5187
+ console.log(chalk12.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
5188
+ console.log(chalk12.dim(" 2. Check the server IP with: hostname -I"));
5189
+ console.log(chalk12.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
5272
5190
  } else if (code === "ETIMEDOUT") {
5273
- console.log(chalk13.yellow(" Hints:"));
5274
- console.log(chalk13.dim(" 1. Verify the server IP is correct"));
5275
- console.log(chalk13.dim(" 2. Check that both machines are on the same network/subnet"));
5276
- console.log(chalk13.dim(" 3. Open the port on the server firewall"));
5191
+ console.log(chalk12.yellow(" Hints:"));
5192
+ console.log(chalk12.dim(" 1. Verify the server IP is correct"));
5193
+ console.log(chalk12.dim(" 2. Check that both machines are on the same network/subnet"));
5194
+ console.log(chalk12.dim(" 3. Open the port on the server firewall"));
5277
5195
  } else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
5278
- console.log(chalk13.yellow(" Hints:"));
5279
- console.log(chalk13.dim(" 1. The server at " + host + " is not responding"));
5280
- console.log(chalk13.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
5281
- console.log(chalk13.dim(" 3. Check connectivity: ping " + host));
5282
- console.log(chalk13.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
5196
+ console.log(chalk12.yellow(" Hints:"));
5197
+ console.log(chalk12.dim(" 1. The server at " + host + " is not responding"));
5198
+ console.log(chalk12.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
5199
+ console.log(chalk12.dim(" 3. Check connectivity: ping " + host));
5200
+ console.log(chalk12.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
5283
5201
  }
5284
5202
  process.stdout.write("\x1B[?25h");
5285
5203
  process.exit(1);
@@ -5291,21 +5209,62 @@ async function runP2PConnect() {
5291
5209
  process.exit(0);
5292
5210
  });
5293
5211
  }
5212
+ async function runInfoCommand() {
5213
+ const data = await collectAll({ detailed: true });
5214
+ const { getDisplayInfo, getTimeMachineStatus } = await import("./collectors-4VYRJVRO.js");
5215
+ const displays = await getDisplayInfo();
5216
+ const tmStatus = await getTimeMachineStatus();
5217
+ const lines = [];
5218
+ lines.push(chalk12.bold(" pyre info \u2014 Mac System Information"));
5219
+ lines.push("");
5220
+ lines.push(` Hostname: ${data.header.hostname}`);
5221
+ lines.push(` OS: ${data.header.os}`);
5222
+ lines.push(` Uptime: ${data.header.uptime}`);
5223
+ lines.push(` CPU: ${data.cpu.brand}`);
5224
+ lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
5225
+ lines.push(` Frequency: ${data.cpu.frequency} MHz`);
5226
+ lines.push(` Memory: ${formatBytes(data.memory.total)}${data.memory.pressureLevel ? ` (Pressure: ${data.memory.pressureLevel})` : ""}`);
5227
+ if (data.memory.wiredBytes !== void 0 && data.memory.compressedBytes !== void 0) {
5228
+ lines.push(` Wired: ${formatBytes(data.memory.wiredBytes)} | Compressed: ${formatBytes(data.memory.compressedBytes)}`);
5229
+ }
5230
+ if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
5231
+ if (data.battery) {
5232
+ lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
5233
+ lines.push(` Health: ${data.battery.health}`);
5234
+ }
5235
+ lines.push(` Thermal: ${data.thermal.state}`);
5236
+ if (displays.length > 0) {
5237
+ lines.push("");
5238
+ lines.push(chalk12.cyan.bold(" \u{1F5A5} Displays:"));
5239
+ for (const d of displays) {
5240
+ lines.push(` \u2022 ${d.name}: ${d.resolution}${d.isMain ? " (Main)" : ""}${d.connectionType ? ` [${d.connectionType}]` : ""}`);
5241
+ }
5242
+ }
5243
+ lines.push("");
5244
+ lines.push(chalk12.cyan.bold(" \u23F1 Time Machine Backup:"));
5245
+ if (!tmStatus.configured) {
5246
+ lines.push(" \u2022 Not configured");
5247
+ } else {
5248
+ lines.push(` \u2022 Status: ${tmStatus.running ? chalk12.yellow(`Backup in progress (${tmStatus.percent || 0}%)`) : chalk12.green("Idle")}`);
5249
+ lines.push(` \u2022 Latest Backup: ${tmStatus.latestBackup}`);
5250
+ }
5251
+ console.log(lines.join("\n"));
5252
+ }
5294
5253
  async function runSshCommand(host) {
5295
5254
  const password = opts.p2pPassword || "mysecret";
5296
5255
  const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
5297
- console.log(chalk13.bold(`
5256
+ console.log(chalk12.bold(`
5298
5257
  pyre ssh \u2014 ${host}`));
5299
- console.log(chalk13.dim(` Running: ${cmd}
5258
+ console.log(chalk12.dim(` Running: ${cmd}
5300
5259
  `));
5301
5260
  const { execFile: execFile2 } = await import("child_process");
5302
5261
  const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
5303
5262
  if (err) {
5304
- console.log(chalk13.red(` SSH failed: ${err.message}`));
5305
- console.log(chalk13.dim(" Ensure SSH keys are set up or the host is reachable."));
5263
+ console.log(chalk12.red(` SSH failed: ${err.message}`));
5264
+ console.log(chalk12.dim(" Ensure SSH keys are set up or the host is reachable."));
5306
5265
  process.exit(1);
5307
5266
  }
5308
- if (stderr) console.log(chalk13.dim(stderr));
5267
+ if (stderr) console.log(chalk12.dim(stderr));
5309
5268
  try {
5310
5269
  const remote = JSON.parse(stdout);
5311
5270
  console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
@@ -5322,7 +5281,7 @@ async function runSshCommand(host) {
5322
5281
  }
5323
5282
  async function runWebCommand() {
5324
5283
  const http = await import("http");
5325
- const port = parseInt(opts.port || process.env.PORT || "3000", 10) || 3e3;
5284
+ const port = parseInt(opts.port || process.env.PORT || "3000", 10) || (opts.port === "0" ? 0 : 3e3);
5326
5285
  const server = http.createServer(async (req, res) => {
5327
5286
  const url = req.url || "/";
5328
5287
  if (url === "/health" || url === "/api/health") {
@@ -5355,17 +5314,14 @@ async function runWebCommand() {
5355
5314
  }
5356
5315
  if (url === "/" || url === "/index.html") {
5357
5316
  res.writeHead(200, { "Content-Type": "text/html" });
5358
- const data = await collectAll({ detailed: true });
5359
- const htmlContent = formatHtml(data);
5360
- res.end(htmlContent);
5317
+ res.end(getDashboardHtml());
5361
5318
  } else if (url === "/setup") {
5362
5319
  res.writeHead(200, { "Content-Type": "text/html" });
5363
- const indexPath = path6.join(process.cwd(), "index.html");
5364
- if (fs9.existsSync(indexPath)) {
5365
- res.end(fs9.readFileSync(indexPath, "utf-8"));
5320
+ const indexPath = path5.join(process.cwd(), "index.html");
5321
+ if (fs8.existsSync(indexPath)) {
5322
+ res.end(fs8.readFileSync(indexPath, "utf-8"));
5366
5323
  } else {
5367
- const data = await collectAll({ detailed: true });
5368
- res.end(formatHtml(data));
5324
+ res.end(getDashboardHtml());
5369
5325
  }
5370
5326
  } else if (url === "/api" || url === "/api/stats" || url === "/data") {
5371
5327
  const data = await collectAll({ detailed: true });
@@ -5382,49 +5338,166 @@ async function runWebCommand() {
5382
5338
  });
5383
5339
  server.on("error", (err) => {
5384
5340
  if (err.code === "EADDRINUSE") {
5385
- console.log(chalk13.red(`
5341
+ console.log(chalk12.red(`
5386
5342
  Error: Port ${port} is already in use.`));
5387
- console.log(chalk13.yellow(` Try specifying a different port using --port <port>, e.g.:`));
5388
- console.log(chalk13.dim(` npx tsx src/index.ts web --port ${port + 1}
5343
+ console.log(chalk12.yellow(` Try specifying a different port using --port <port>, e.g.:`));
5344
+ console.log(chalk12.dim(` npx tsx src/index.ts web --port ${port + 1}
5389
5345
  `));
5390
5346
  process.exit(1);
5391
5347
  } else {
5392
- console.log(chalk13.red(`Server error: ${err.message}`));
5348
+ console.log(chalk12.red(`Server error: ${err.message}`));
5393
5349
  process.exit(1);
5394
5350
  }
5395
5351
  });
5396
5352
  const ip = detectLocalIP() || "localhost";
5397
- server.listen(port, "0.0.0.0", () => {
5398
- console.log(chalk13.bold(`
5399
- pyre web server running on port ${port}`));
5400
- console.log(chalk13.dim(` Local URL: http://localhost:${port}/`));
5401
- console.log(chalk13.hex("#ff6a39").bold(` Network URL: http://${ip}:${port}/ (Accessible on same Wi-Fi / LAN)`));
5402
- console.log(chalk13.dim(` API: http://${ip}:${port}/api`));
5403
- console.log(chalk13.dim(` SSE Stream: http://${ip}:${port}/api/stream`));
5404
- console.log(chalk13.yellow(`
5405
- Troubleshooting connection from phone/other devices:`));
5406
- console.log(chalk13.dim(` 1. macOS Firewall: System Settings -> Network -> Firewall -> Allow incoming node/pyre connections`));
5407
- console.log(chalk13.dim(` 2. Wi-Fi Router: Ensure "AP Isolation" / "Guest Mode" is OFF on your Wi-Fi router`));
5408
- console.log(chalk13.dim(` 3. Verify phone is connected to the exact same Wi-Fi network
5409
- `));
5353
+ return new Promise((resolve) => {
5354
+ server.listen(port, "0.0.0.0", () => {
5355
+ const address = server.address();
5356
+ const actualPort = typeof address === "object" && address ? address.port : port;
5357
+ console.log(chalk12.bold(`
5358
+ pyre web server running on port ${actualPort}`));
5359
+ console.log(chalk12.dim(` Local URL: http://localhost:${actualPort}/`));
5360
+ console.log(chalk12.hex("#ff6a39").bold(` Network URL: http://${ip}:${actualPort}/ (Accessible on same Wi-Fi / LAN)`));
5361
+ console.log(chalk12.dim(` API: http://${ip}:${actualPort}/api`));
5362
+ console.log(chalk12.dim(` SSE Stream: http://${ip}:${actualPort}/api/stream`));
5363
+ resolve(actualPort);
5364
+ });
5365
+ process.once("SIGINT", () => {
5366
+ server.close();
5367
+ process.exit(0);
5368
+ });
5369
+ });
5370
+ }
5371
+ async function runUiCommand() {
5372
+ opts.port = "0";
5373
+ const port = await runWebCommand();
5374
+ const swiftScript = `
5375
+ import Cocoa
5376
+ import WebKit
5377
+
5378
+ // WKWebView subclass that allows the window to be dragged from any pixel
5379
+ class DraggableWebView: WKWebView {
5380
+ override var mouseDownCanMoveWindow: Bool { return true }
5381
+ }
5382
+
5383
+ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
5384
+ var window: NSWindow!
5385
+ var webView: DraggableWebView!
5386
+ var dataTimer: Timer?
5387
+ let serverPort = ${port}
5388
+
5389
+ func applicationDidFinishLaunching(_ n: Notification) {
5390
+ // Window
5391
+ let screen = NSScreen.main?.visibleFrame ?? NSRect(x:0, y:0, width:1280, height:800)
5392
+ let W = min(1180.0, screen.width - 80)
5393
+ let H = min(820.0, screen.height - 80)
5394
+ let rect = NSRect(x: screen.minX + (screen.width - W) / 2,
5395
+ y: screen.minY + (screen.height - H) / 2,
5396
+ width: W, height: H)
5397
+
5398
+ window = NSWindow(contentRect: rect,
5399
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
5400
+ backing: .buffered, defer: false)
5401
+ window.title = "Pyre"
5402
+ window.minSize = NSSize(width: 800, height: 600)
5403
+ window.appearance = NSAppearance(named: .darkAqua)
5404
+ window.backgroundColor = NSColor(red: 0.027, green: 0.031, blue: 0.063, alpha: 1)
5405
+ window.isReleasedWhenClosed = false
5406
+ window.delegate = self
5407
+
5408
+ // WebView
5409
+ let cfg = WKWebViewConfiguration()
5410
+ webView = DraggableWebView(frame: window.contentView!.bounds, configuration: cfg)
5411
+ webView.autoresizingMask = [.width, .height]
5412
+ webView.setValue(NSColor(red: 0.027, green: 0.031, blue: 0.063, alpha: 1),
5413
+ forKey: "backgroundColor")
5414
+
5415
+ // Load the dashboard HTML from the local server
5416
+ var req = URLRequest(url: URL(string: "http://localhost:\\(serverPort)/")!)
5417
+ req.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
5418
+ webView.load(req)
5419
+
5420
+ window.contentView?.addSubview(webView)
5421
+ window.makeKeyAndOrderFront(nil)
5422
+ NSApp.activate(ignoringOtherApps: true)
5423
+
5424
+ // Start native data timer (URLSession has full network access \u2014 no sandbox issues)
5425
+ // We wait 1.5 s for the page to load, then start pushing data every 2 s.
5426
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
5427
+ self?.fetchAndPush()
5428
+ self?.dataTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
5429
+ self?.fetchAndPush()
5430
+ }
5431
+ }
5432
+ }
5433
+
5434
+ // URLSession fetch (runs in the main app process \u2014 not sandboxed like WebContent)
5435
+ func fetchAndPush() {
5436
+ guard let url = URL(string: "http://localhost:\\(serverPort)/api/stats") else { return }
5437
+ URLSession.shared.dataTask(with: url) { [weak self] data, _, err in
5438
+ if let err = err {
5439
+ print("Swift UI Fetch Error: \\(err)")
5440
+ return
5441
+ }
5442
+ guard let data = data else {
5443
+ print("Swift UI Fetch Error: No data")
5444
+ return
5445
+ }
5446
+ let b64 = data.base64EncodedString()
5447
+ let js = "if(window.__pyreUpdate){window.__pyreUpdate(decodeURIComponent(escape(atob('\\(b64)'))))}"
5448
+ DispatchQueue.main.async {
5449
+ self?.webView.evaluateJavaScript(js) { res, err in
5450
+ if let err = err {
5451
+ print("Swift UI JS Eval Error: \\(err)")
5452
+ }
5453
+ }
5454
+ }
5455
+ }.resume()
5456
+ }
5457
+
5458
+ func windowWillClose(_ n: Notification) {
5459
+ dataTimer?.invalidate()
5460
+ NSApplication.shared.terminate(self)
5461
+ }
5462
+ func applicationShouldTerminateAfterLastWindowClosed(_ s: NSApplication) -> Bool { true }
5463
+ }
5464
+
5465
+ let app = NSApplication.shared
5466
+ app.setActivationPolicy(.regular)
5467
+ let delegate = AppDelegate()
5468
+ app.delegate = delegate
5469
+ app.run()
5470
+ `;
5471
+ const scriptPath = path5.join(os3.tmpdir(), "pyre-ui.swift");
5472
+ fs8.writeFileSync(scriptPath, swiftScript);
5473
+ const { spawn } = await import("child_process");
5474
+ console.log(chalk12.cyan(`
5475
+ Launching native UI window...`));
5476
+ const ui = spawn("swift", [scriptPath], { stdio: "inherit" });
5477
+ ui.on("close", () => {
5478
+ console.log(chalk12.dim("UI window closed, shutting down server..."));
5479
+ process.exit(0);
5410
5480
  });
5411
5481
  process.once("SIGINT", () => {
5412
- server.close();
5482
+ ui.kill();
5413
5483
  process.exit(0);
5414
5484
  });
5415
5485
  }
5416
5486
  async function runBenchCommand(benchCmd) {
5417
5487
  const { spawn } = await import("child_process");
5418
5488
  const logDir = opts.exportDir || "./pyre-exports";
5419
- const file = path6.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
5420
- if (!fs9.existsSync(logDir)) fs9.mkdirSync(logDir, { recursive: true });
5421
- const stream = fs9.createWriteStream(file, { flags: "a" });
5422
- stream.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");
5423
- console.log(chalk13.bold(`
5489
+ const file = path5.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
5490
+ if (!fs8.existsSync(logDir)) fs8.mkdirSync(logDir, { recursive: true });
5491
+ const stream = fs8.createWriteStream(file, { flags: "a" });
5492
+ stream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state,power_watts\n");
5493
+ console.log(chalk12.bold(`
5424
5494
  pyre bench`));
5425
- console.log(chalk13.dim(` Command: ${benchCmd}`));
5426
- console.log(chalk13.dim(` Log: ${file}
5495
+ console.log(chalk12.dim(` Command: ${benchCmd}`));
5496
+ console.log(chalk12.dim(` Log: ${file}
5427
5497
  `));
5498
+ const startTime = Date.now();
5499
+ let accumulatedWattSeconds = 0;
5500
+ let powerSamplesCount = 0;
5428
5501
  const child = spawn("sh", ["-c", benchCmd]);
5429
5502
  child.stdout.on("data", (d) => process.stdout.write(d));
5430
5503
  child.stderr.on("data", (d) => process.stdout.write(d));
@@ -5436,22 +5509,37 @@ async function runBenchCommand(benchCmd) {
5436
5509
  const rxPackets = data.network.rxPackets ?? 0;
5437
5510
  const txPackets = data.network.txPackets ?? 0;
5438
5511
  const connections = data.network.connections ?? 0;
5439
- stream.write(`${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}
5512
+ const watts = data.power?.combinedWatts ?? data.power?.cpuWatts;
5513
+ if (watts !== void 0 && watts > 0) {
5514
+ accumulatedWattSeconds += watts * interval;
5515
+ powerSamplesCount++;
5516
+ }
5517
+ stream.write(`${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state},${watts ?? ""}
5440
5518
  `);
5441
5519
  } catch {
5442
5520
  }
5443
5521
  }, interval * 1e3);
5444
- child.on("close", () => {
5522
+ const printSummary = () => {
5445
5523
  clearInterval(handle);
5446
5524
  stream.end();
5447
- console.log(chalk13.green(`
5448
- Bench complete. Log saved to ${file}`));
5525
+ const durationSec = Math.max(0.1, (Date.now() - startTime) / 1e3);
5526
+ console.log(chalk12.green(`
5527
+ \u2714 Bench complete (${durationSec.toFixed(2)}s). Log saved to ${file}`));
5528
+ if (powerSamplesCount > 0 && accumulatedWattSeconds > 0) {
5529
+ const kWh = accumulatedWattSeconds / (3600 * 1e3);
5530
+ const rate = config2.electricityRateKwH ?? 0.15;
5531
+ const cost = kWh * rate;
5532
+ console.log(chalk12.cyan(` \u26A1 Energy Consumption: ${kWh.toFixed(6)} kWh | Est. Cost: $${cost.toFixed(6)} (@ $${rate}/kWh)`));
5533
+ }
5534
+ };
5535
+ child.on("close", () => {
5536
+ printSummary();
5449
5537
  process.exit(0);
5450
5538
  });
5451
5539
  process.once("SIGINT", () => {
5452
5540
  clearInterval(handle);
5453
5541
  child.kill("SIGTERM");
5454
- stream.end();
5542
+ printSummary();
5455
5543
  process.exit(0);
5456
5544
  });
5457
5545
  }