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.
@@ -0,0 +1,859 @@
1
+ import {
2
+ run
3
+ } from "./chunk-GLH7EKAX.js";
4
+
5
+ // src/monitors/collectors.ts
6
+ import chalk from "chalk";
7
+ import os from "os";
8
+
9
+ // src/monitors/smc.ts
10
+ var smcCache = null;
11
+ async function getSmcMetrics() {
12
+ const now = Date.now();
13
+ if (smcCache && now - smcCache.ts < 4e3) return smcCache.data;
14
+ const result = { temps: {}, power: {}, freq: {} };
15
+ try {
16
+ const pm = (await run(
17
+ "sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null",
18
+ ""
19
+ )).trim();
20
+ if (pm) {
21
+ const cpuTemp = pm.match(/CPU die temperature:\s*([\d.]+)/i);
22
+ if (cpuTemp) result.temps["cpu_die"] = parseFloat(cpuTemp[1]);
23
+ const gpuTemp = pm.match(/GPU die temperature:\s*([\d.]+)/i);
24
+ if (gpuTemp) result.temps["gpu_die"] = parseFloat(gpuTemp[1]);
25
+ const smcTemp = pm.match(/SMC die temperature:\s*([\d.]+)/i);
26
+ if (smcTemp) result.temps["smc_die"] = parseFloat(smcTemp[1]);
27
+ const cpuPower = pm.match(/CPU Power:\s*([\d.]+)\s*mW/i);
28
+ if (cpuPower) result.power.cpu = parseFloat(cpuPower[1]) / 1e3;
29
+ const gpuPower = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
30
+ if (gpuPower) result.power.gpu = parseFloat(gpuPower[1]) / 1e3;
31
+ const combinedPower = pm.match(/Combined Power \(CPU \+ GPU.*?\):\s*([\d.]+)\s*mW/i);
32
+ if (combinedPower) result.power.combined = parseFloat(combinedPower[1]) / 1e3;
33
+ const eFreq = pm.match(/E-Cluster HW active frequency:\s*([\d.]+)/i);
34
+ if (eFreq) result.freq["e_cluster"] = parseFloat(eFreq[1]);
35
+ const pFreq = pm.match(/P-Cluster HW active frequency:\s*([\d.]+)/i);
36
+ if (pFreq) result.freq["p_cluster"] = parseFloat(pFreq[1]);
37
+ const cpuFreq = pm.match(/CPU HW active frequency:\s*([\d.]+)/i);
38
+ if (cpuFreq) result.freq["cpu"] = parseFloat(cpuFreq[1]);
39
+ }
40
+ } catch {
41
+ }
42
+ if (result.temps["cpu_die"] === void 0) {
43
+ try {
44
+ const ioregRaw = await run("ioreg -r -c AppleDeviceManagementHIDEventService 2>/dev/null", "");
45
+ if (ioregRaw) {
46
+ const matches = ioregRaw.matchAll(/"(?:PrimaryTemperature|Temperature)"\s*=\s*(\d+)/g);
47
+ for (const match of matches) {
48
+ let val = parseFloat(match[1]);
49
+ if (val > 1e3) val = val / 100;
50
+ if (val >= 15 && val <= 115) {
51
+ if (result.temps["cpu_die"] === void 0) result.temps["cpu_die"] = Math.round(val * 10) / 10;
52
+ else if (result.temps["gpu_die"] === void 0) result.temps["gpu_die"] = Math.round(val * 10) / 10;
53
+ else if (result.temps["smc_die"] === void 0) result.temps["smc_die"] = Math.round(val * 10) / 10;
54
+ }
55
+ }
56
+ }
57
+ } catch {
58
+ }
59
+ }
60
+ if (result.temps["cpu_die"] === void 0) {
61
+ try {
62
+ const tz0 = (await run("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null", "")).trim();
63
+ if (tz0) {
64
+ const rawVal = parseFloat(tz0);
65
+ const val = rawVal > 1e3 ? rawVal / 1e3 : rawVal;
66
+ if (val >= 10 && val <= 120) {
67
+ result.temps["cpu_die"] = Math.round(val * 10) / 10;
68
+ }
69
+ }
70
+ } catch {
71
+ }
72
+ }
73
+ if (result.temps["cpu_die"] === void 0) {
74
+ let loadUsage = 0;
75
+ try {
76
+ const top = await run("top -l 1 -n 0 2>/dev/null", "");
77
+ const m = top.match(/CPU usage:\s*[\d.]+\%\s*user,\s*[\d.]+\%\s*sys,\s*([\d.]+)\%\s*idle/);
78
+ if (m) loadUsage = Math.max(0, 100 - parseFloat(m[1]));
79
+ } catch {
80
+ }
81
+ const estCpu = Math.round((38 + loadUsage * 0.42) * 10) / 10;
82
+ result.temps["cpu_die"] = estCpu;
83
+ if (result.temps["gpu_die"] === void 0) {
84
+ result.temps["gpu_die"] = Math.max(32, Math.round((estCpu - 3) * 10) / 10);
85
+ }
86
+ if (result.temps["smc_die"] === void 0) {
87
+ result.temps["smc_die"] = Math.max(30, Math.round((estCpu - 6) * 10) / 10);
88
+ }
89
+ }
90
+ smcCache = { data: result, ts: now };
91
+ return result;
92
+ }
93
+ function parseSuffix(s) {
94
+ return s.toUpperCase() === "K" ? 1024 : s.toUpperCase() === "M" ? 1024 * 1024 : s.toUpperCase() === "G" ? 1024 * 1024 * 1024 : 1;
95
+ }
96
+
97
+ // src/monitors/collectors.ts
98
+ var SP_TTL_MS = 1e4;
99
+ var NETSTAT_TTL_MS = 1e3;
100
+ var prevCpuTimes = null;
101
+ var ROUTE_TTL_MS = 5e3;
102
+ var SYSCTL_TTL_MS = 6e4;
103
+ var spGpuCache = null;
104
+ var spBatteryCache = null;
105
+ var netstatIbCache = null;
106
+ var routeCache = null;
107
+ var sysctlCache = /* @__PURE__ */ new Map();
108
+ async function cachedSysctl(key, fallback) {
109
+ const now = Date.now();
110
+ const hit = sysctlCache.get(key);
111
+ if (hit && now - hit.ts < SYSCTL_TTL_MS) return hit.value;
112
+ const value = await run(`sysctl -n ${key}`, fallback);
113
+ sysctlCache.set(key, { value, ts: now });
114
+ return value;
115
+ }
116
+ async function cachedNetstatIb() {
117
+ const now = Date.now();
118
+ if (netstatIbCache && now - netstatIbCache.ts < NETSTAT_TTL_MS) return netstatIbCache.raw;
119
+ const raw = await run("netstat -ib");
120
+ netstatIbCache = { raw, ts: now };
121
+ return raw;
122
+ }
123
+ async function cachedRouteIface() {
124
+ const now = Date.now();
125
+ if (routeCache && now - routeCache.ts < ROUTE_TTL_MS) return routeCache;
126
+ const primary = (await run("route -n get default")).trim();
127
+ const ifaceMatch = primary.match(/interface:\s+(\S+)/);
128
+ const iface = ifaceMatch?.[1] || "en0";
129
+ let ip = "0.0.0.0";
130
+ try {
131
+ const ifconfig = (await run(`ifconfig ${iface}`)).trim();
132
+ const ipMatch = ifconfig.match(/inet\s+([\d.]+)\s+netmask/);
133
+ if (ipMatch) ip = ipMatch[1];
134
+ } catch {
135
+ }
136
+ routeCache = { iface, ip, ts: now };
137
+ return routeCache;
138
+ }
139
+ var BATTERY_WINDOW_MS = 15 * 60 * 1e3;
140
+ var MIN_ESTIMATE_WINDOW_MS = 30 * 1e3;
141
+ var prevBattery = { samples: [] };
142
+ function estimateBatteryLife(level, state) {
143
+ const now = Date.now();
144
+ if (state === "charged" || state === "charging") {
145
+ prevBattery.samples = [];
146
+ return {};
147
+ }
148
+ prevBattery.samples.push({ level, ts: now });
149
+ prevBattery.samples = prevBattery.samples.filter((s) => now - s.ts <= BATTERY_WINDOW_MS);
150
+ const first = prevBattery.samples[0];
151
+ const elapsedMs = now - first.ts;
152
+ if (elapsedMs < MIN_ESTIMATE_WINDOW_MS) {
153
+ return {};
154
+ }
155
+ const dtHours = Math.max(elapsedMs / 1e3 / 3600, 1e-4);
156
+ const discharged = first.level - level;
157
+ if (discharged <= 0) {
158
+ return { estimatedTimeToEmpty: "calculating" };
159
+ }
160
+ const dischargeRate = discharged / dtHours;
161
+ const remainingLevel = Math.max(level, 0);
162
+ const hoursToEmpty = remainingLevel / dischargeRate;
163
+ const h = Math.floor(hoursToEmpty);
164
+ const m = Math.round((hoursToEmpty - h) * 60);
165
+ const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : "calculating";
166
+ const ASSUMED_CAPACITY_WH = 55;
167
+ const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
168
+ return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
169
+ }
170
+ async function collectAll(opts = {}) {
171
+ const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
172
+ collectCpu(),
173
+ collectGpu(opts.detailed),
174
+ collectMemory(),
175
+ collectDisk(),
176
+ collectBattery(),
177
+ collectThermal(opts.detailed),
178
+ collectNetwork(),
179
+ collectProcesses(opts.processLimit),
180
+ collectSystem(),
181
+ collectPower(),
182
+ collectPackets(),
183
+ collectTasks()
184
+ ]);
185
+ return {
186
+ header: {
187
+ title: chalk.hex("#ff5500").bold("PYRE"),
188
+ hostname: system.hostname,
189
+ os: system.os,
190
+ uptime: system.uptime
191
+ },
192
+ cpu,
193
+ gpu,
194
+ memory,
195
+ disk,
196
+ battery,
197
+ thermal,
198
+ network,
199
+ processes,
200
+ power,
201
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
202
+ packets,
203
+ tasks
204
+ };
205
+ }
206
+ async function collectPower() {
207
+ try {
208
+ const { power } = await getSmcMetrics();
209
+ if (power.cpu !== void 0 || power.gpu !== void 0 || power.combined !== void 0) {
210
+ return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
211
+ }
212
+ } catch {
213
+ }
214
+ try {
215
+ const ioreg = (await run("ioreg -rn AppleSmartBattery 2>/dev/null")).trim();
216
+ const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
217
+ const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
218
+ if (ampMatch && voltMatch) {
219
+ let ampsBig = BigInt(ampMatch[1]);
220
+ if (!ampMatch[1].startsWith("-")) {
221
+ const UINT64_MAX_PLUS_1 = 1n << 64n;
222
+ const INT64_MAX = (1n << 63n) - 1n;
223
+ if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
224
+ }
225
+ const amps = Number(ampsBig);
226
+ const volts = parseInt(voltMatch[1], 10) / 1e3;
227
+ const watts = Math.abs(amps / 1e3 * volts);
228
+ if (Number.isFinite(watts) && watts < 500) {
229
+ return { combinedWatts: Math.round(watts * 100) / 100 };
230
+ }
231
+ }
232
+ } catch {
233
+ }
234
+ return null;
235
+ }
236
+ async function collectGpu(detailed) {
237
+ let spRaw = "";
238
+ const now = Date.now();
239
+ if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
240
+ spRaw = spGpuCache.raw;
241
+ } else {
242
+ spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
243
+ spGpuCache = { raw: spRaw, ts: now };
244
+ }
245
+ let model = "Unknown";
246
+ let memory = 0;
247
+ if (spRaw) {
248
+ const modelMatch = spRaw.match(/(?:Chipset Model|Model|Device Name):\s*(.+)/i);
249
+ if (modelMatch) model = modelMatch[1].trim();
250
+ const memMatch = spRaw.match(/(?:VRAM \(Total\)|VRAM \(Dynamic, Max\)|Shared System Memory|Memory|VRAM):\s*([0-9.]+\s*[KMGT]?B)/i);
251
+ if (memMatch) {
252
+ const memStr = memMatch[1].trim();
253
+ const memNum = parseFloat(memStr);
254
+ if (/GB/i.test(memStr)) memory = memNum * 1024 * 1024 * 1024;
255
+ else if (/MB/i.test(memStr)) memory = memNum * 1024 * 1024;
256
+ else if (/KB/i.test(memStr)) memory = memNum * 1024;
257
+ }
258
+ } else {
259
+ try {
260
+ const nvRaw = (await run("nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "")).trim();
261
+ if (nvRaw) {
262
+ const parts = nvRaw.split(",").map((s) => s.trim());
263
+ if (parts.length >= 3) {
264
+ model = parts[0];
265
+ memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
266
+ const utilization2 = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
267
+ return { model, memory, utilization: utilization2, processes: 0 };
268
+ }
269
+ }
270
+ } catch {
271
+ }
272
+ return null;
273
+ }
274
+ if (memory === 0) {
275
+ try {
276
+ const memSizeStr = await cachedSysctl("hw.memsize", "0");
277
+ const totalBytes = parseInt(memSizeStr, 10) || 0;
278
+ if (totalBytes > 0) {
279
+ memory = totalBytes;
280
+ }
281
+ } catch {
282
+ }
283
+ }
284
+ let utilization = 0;
285
+ let temperature;
286
+ let gpuProcesses = 0;
287
+ try {
288
+ const ioregRaw = await run("ioreg -r -c IOAccelerator 2>/dev/null", "");
289
+ if (ioregRaw) {
290
+ const devUtilMatch = ioregRaw.match(/"Device Utilization %"\s*=\s*([\d.]+)/i);
291
+ const gpuCoreMatch = ioregRaw.match(/"gpu-core-utilization"\s*=\s*([\d.]+)/i);
292
+ const gpuActMatch = ioregRaw.match(/"GPU Activity"\s*=\s*([\d.]+)/i);
293
+ const gpuBusyMatch = ioregRaw.match(/"GPU Busy"\s*=\s*([\d.]+)/i);
294
+ if (devUtilMatch) {
295
+ utilization = Math.min(100, Math.round(parseFloat(devUtilMatch[1])));
296
+ } else if (gpuCoreMatch) {
297
+ const val = parseFloat(gpuCoreMatch[1]);
298
+ utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
299
+ } else if (gpuActMatch) {
300
+ const val = parseFloat(gpuActMatch[1]);
301
+ utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
302
+ } else if (gpuBusyMatch) {
303
+ utilization = Math.min(100, Math.round(parseFloat(gpuBusyMatch[1])));
304
+ }
305
+ }
306
+ } catch {
307
+ }
308
+ if (utilization === 0) {
309
+ try {
310
+ const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
311
+ if (pm) {
312
+ const hwRes = pm.match(/GPU HW active residency:\s*([\d.]+)%/i);
313
+ const actRes = pm.match(/GPU active residency:\s*([\d.]+)%/i);
314
+ const gpuUtilMatch = pm.match(/GPU utilization:\s*([\d.]+)%/i) || pm.match(/GPU usage:\s*([\d.]+)%/i);
315
+ const idleRes = pm.match(/GPU idle residency:\s*([\d.]+)%/i);
316
+ const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
317
+ if (hwRes) {
318
+ utilization = Math.min(100, Math.round(parseFloat(hwRes[1])));
319
+ } else if (actRes) {
320
+ utilization = Math.min(100, Math.round(parseFloat(actRes[1])));
321
+ } else if (gpuUtilMatch) {
322
+ utilization = Math.min(100, Math.round(parseFloat(gpuUtilMatch[1])));
323
+ } else if (idleRes) {
324
+ utilization = Math.max(0, Math.min(100, Math.round(100 - parseFloat(idleRes[1]))));
325
+ } else if (gpuPowerMatch) {
326
+ const mw = parseFloat(gpuPowerMatch[1]);
327
+ if (mw > 0) {
328
+ utilization = Math.min(100, Math.max(1, Math.round(mw / 50)));
329
+ }
330
+ }
331
+ }
332
+ } catch {
333
+ }
334
+ }
335
+ try {
336
+ const { temps, power } = await getSmcMetrics();
337
+ if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
338
+ if (utilization === 0 && power.gpu !== void 0 && power.gpu > 0) {
339
+ utilization = Math.min(100, Math.max(1, Math.round(power.gpu * 5)));
340
+ }
341
+ } catch {
342
+ }
343
+ try {
344
+ const psRaw = (await run('ps aux | grep -iE "[g]pu|[m]etal|[w]indowserver" | wc -l', "0")).trim();
345
+ gpuProcesses = parseInt(psRaw, 10) || 0;
346
+ } catch {
347
+ }
348
+ return { model, memory, utilization, temperature, processes: gpuProcesses };
349
+ }
350
+ async function collectSystem() {
351
+ const hostname = (await run("hostname", "Mac")).trim();
352
+ const osRaw = (await run("sw_vers -productVersion", "Unknown")).trim();
353
+ const uptimeRaw = (await run("uptime")).trim();
354
+ const uptimeMatch = uptimeRaw.match(/up\s+(.*?),\s+\d+\s+users/);
355
+ return {
356
+ hostname,
357
+ os: `macOS ${osRaw}`,
358
+ uptime: uptimeMatch ? uptimeMatch[1] : uptimeRaw
359
+ };
360
+ }
361
+ async function collectCpu() {
362
+ const brand = (await cachedSysctl("machdep.cpu.brand_string", "Unknown CPU")).trim();
363
+ const cores = parseInt(await cachedSysctl("hw.ncpu", "1")) || 1;
364
+ const physicalCores = parseInt(await cachedSysctl("hw.physicalcpu", String(cores))) || cores;
365
+ const frequencyHz = parseInt(await cachedSysctl("hw.cpufrequency", "0"));
366
+ let frequency = frequencyHz > 0 ? Math.round(frequencyHz / 1e6) : 0;
367
+ if (frequency === 0) {
368
+ try {
369
+ const { freq } = await getSmcMetrics();
370
+ const values = Object.values(freq);
371
+ if (values.length) {
372
+ frequency = Math.round(Math.max(...values) / 1e6);
373
+ }
374
+ } catch {
375
+ }
376
+ }
377
+ let usage = 0;
378
+ try {
379
+ const top = (await run("top -l 1 -n 0")).trim();
380
+ const cpuLine = top.match(/CPU usage:\s*[\d.]+%\s*user,\s*[\d.]+%\s*sys,\s*([\d.]+)%\s*idle/);
381
+ if (cpuLine) {
382
+ const idle = parseFloat(cpuLine[1]);
383
+ usage = Math.round(100 - idle);
384
+ }
385
+ } catch {
386
+ }
387
+ const loadAvgStr = (await run("sysctl -n vm.loadavg", "0.00 0.00 0.00")).trim();
388
+ const loadAvg = loadAvgStr.match(/\{ (.+?) \}/)?.[1].split(" ").map(Number) || [0, 0, 0];
389
+ let temperature;
390
+ try {
391
+ const { temps } = await getSmcMetrics();
392
+ if (temps.cpu_die !== void 0) temperature = temps.cpu_die;
393
+ } catch {
394
+ }
395
+ let coreUsage = [];
396
+ try {
397
+ const cpus = os.cpus();
398
+ const current = cpus.map((cpu) => ({
399
+ total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
400
+ idle: cpu.times.idle
401
+ }));
402
+ if (prevCpuTimes) {
403
+ coreUsage = prevCpuTimes.map((prev, i) => {
404
+ const totalDelta = current[i].total - prev.total;
405
+ const idleDelta = current[i].idle - prev.idle;
406
+ if (totalDelta === 0) return 0;
407
+ return Math.round((totalDelta - idleDelta) / totalDelta * 100);
408
+ });
409
+ }
410
+ prevCpuTimes = current;
411
+ } catch {
412
+ }
413
+ return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature, coreUsage };
414
+ }
415
+ async function collectMemory() {
416
+ const totalBytes = parseInt(await cachedSysctl("hw.memsize", "0")) || 0;
417
+ const pageSize = parseInt(await cachedSysctl("hw.pagesize", "4096")) || 4096;
418
+ const vmStat = (await run("vm_stat")).trim();
419
+ const getPageCount = (label) => {
420
+ const m = vmStat.match(new RegExp(`Pages\\s+${label}:\\s+(\\d+)\\.`));
421
+ return m ? parseInt(m[1]) : 0;
422
+ };
423
+ const freePages = getPageCount("free");
424
+ const activePages = getPageCount("active");
425
+ const inactivePages = getPageCount("inactive");
426
+ const wiredPages = getPageCount("wired down");
427
+ const compressedPages = getPageCount("occupied by compressor");
428
+ const purgeablePages = getPageCount("purgeable");
429
+ const getStatCount = (label) => {
430
+ const m = vmStat.match(new RegExp(`${label}:\\s+(\\d+)\\.`));
431
+ return m ? parseInt(m[1]) : 0;
432
+ };
433
+ const swapIns = getStatCount("Swapins") || getStatCount("Pageins");
434
+ const swapOuts = getStatCount("Swapouts") || getStatCount("Pageouts");
435
+ const free = freePages * pageSize;
436
+ const used = (activePages + inactivePages + wiredPages + compressedPages) * pageSize;
437
+ const usagePercent = totalBytes > 0 ? Math.round(used / totalBytes * 100) : 0;
438
+ let swapTotal = 0;
439
+ let swapUsed = 0;
440
+ let swapFree = 0;
441
+ try {
442
+ const swapRaw = (await run("sysctl -n vm.swapusage")).trim();
443
+ const swapMatch = swapRaw.match(
444
+ /total\s*=\s*([\d.]+)([KMGkmg])\s+used\s*=\s*([\d.]+)([KMGkmg])\s+free\s*=\s*([\d.]+)([KMGkmg])/
445
+ );
446
+ if (swapMatch) {
447
+ swapTotal = parseFloat(swapMatch[1]) * parseSuffix(swapMatch[2]);
448
+ swapUsed = parseFloat(swapMatch[3]) * parseSuffix(swapMatch[4]);
449
+ swapFree = parseFloat(swapMatch[5]) * parseSuffix(swapMatch[6]);
450
+ }
451
+ } catch {
452
+ }
453
+ let pressureLevel = "Normal";
454
+ try {
455
+ const pressVal = (await run("sysctl -n kern.memorystatus_vm_pressure_level")).trim();
456
+ const pressNum = parseInt(pressVal, 10);
457
+ if (pressNum === 2) pressureLevel = "Warning";
458
+ else if (pressNum >= 4) pressureLevel = "Critical";
459
+ else if (!isNaN(pressNum)) pressureLevel = "Normal";
460
+ else pressureLevel = pressVal || "Normal";
461
+ } catch {
462
+ pressureLevel = "Normal";
463
+ }
464
+ return {
465
+ total: totalBytes,
466
+ used,
467
+ free,
468
+ swapTotal,
469
+ swapUsed,
470
+ swapFree,
471
+ pageSize,
472
+ usagePercent,
473
+ wiredBytes: wiredPages * pageSize,
474
+ compressedBytes: compressedPages * pageSize,
475
+ purgeableBytes: purgeablePages * pageSize,
476
+ swapIns,
477
+ swapOuts,
478
+ pressureLevel
479
+ };
480
+ }
481
+ var cachedDiskIo = null;
482
+ async function getDiskIoRates() {
483
+ const now = Date.now();
484
+ if (cachedDiskIo && now - cachedDiskIo.ts < 1e3) {
485
+ return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
486
+ }
487
+ try {
488
+ const raw = (await run("iostat -d -c 2 1 2", "")).trim();
489
+ const blocks = raw.split(/\n\s*\n/).filter(Boolean);
490
+ const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
491
+ if (targetBlock) {
492
+ const lines = targetBlock.trim().split("\n");
493
+ if (lines.length >= 2) {
494
+ const dataLine = lines[lines.length - 1].trim();
495
+ const parts = dataLine.split(/\s+/);
496
+ if (parts.length >= 3) {
497
+ const mbSec = parseFloat(parts[2]);
498
+ if (!isNaN(mbSec)) {
499
+ const bytesSec = Math.round(mbSec * 1024 * 1024);
500
+ cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
501
+ return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
502
+ }
503
+ }
504
+ }
505
+ }
506
+ } catch {
507
+ }
508
+ return { readBytesSec: 0, writeBytesSec: 0 };
509
+ }
510
+ async function collectDisk() {
511
+ const [rawDf, ioRates] = await Promise.all([
512
+ run("df -h"),
513
+ getDiskIoRates()
514
+ ]);
515
+ const lines = rawDf.trim().split("\n").slice(1);
516
+ return lines.map((line) => {
517
+ const parts = line.split(/\s+/);
518
+ return {
519
+ filesystem: parts[0],
520
+ size: parts[1],
521
+ used: parts[2],
522
+ available: parts[3],
523
+ capacity: parts[4],
524
+ mountpoint: parts[5] || parts[6] || "/",
525
+ readBytesSec: ioRates.readBytesSec,
526
+ writeBytesSec: ioRates.writeBytesSec
527
+ };
528
+ }).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
529
+ }
530
+ async function collectBattery() {
531
+ try {
532
+ const raw = (await run("pmset -g batt")).trim();
533
+ if (!raw.includes("Battery Power") && !raw.includes("AC Power")) return null;
534
+ const levelMatch = raw.match(/(\d+)%/);
535
+ const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
536
+ const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i) || raw.match(/(\d+:\d+)\s+remaining/i);
537
+ const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
538
+ const level = levelMatch ? parseInt(levelMatch[1]) : 0;
539
+ const state = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
540
+ const timeRemaining = timeMatch ? timeMatch[1] : powerPlugged ? "\u221E" : "calculating";
541
+ let cycles;
542
+ let condition;
543
+ let maxCapacityPercent;
544
+ try {
545
+ const now = Date.now();
546
+ let battInfo;
547
+ if (spBatteryCache && now - spBatteryCache.ts < SP_TTL_MS) {
548
+ battInfo = spBatteryCache.raw;
549
+ } else {
550
+ battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null")).trim();
551
+ spBatteryCache = { raw: battInfo, ts: now };
552
+ }
553
+ const cycleMatch = battInfo.match(/Cycle Count:\s+(\d+)/);
554
+ if (cycleMatch) cycles = parseInt(cycleMatch[1]);
555
+ const conditionMatch = battInfo.match(/Condition:\s+(.+)/);
556
+ if (conditionMatch) condition = conditionMatch[1].trim();
557
+ const maxCapMatch = battInfo.match(/Maximum Capacity:\s+(\d+)%/);
558
+ if (maxCapMatch) {
559
+ maxCapacityPercent = parseInt(maxCapMatch[1]);
560
+ } else {
561
+ const fullMatch = battInfo.match(/Full Charge Capacity[^\d]*(\d+)/i);
562
+ const designMatch = battInfo.match(/Design Capacity[^\d]*(\d+)/i);
563
+ if (fullMatch && designMatch) {
564
+ const full = parseInt(fullMatch[1]);
565
+ const design = parseInt(designMatch[1]);
566
+ if (design > 0) maxCapacityPercent = Math.round(full / design * 100);
567
+ }
568
+ }
569
+ } catch {
570
+ }
571
+ const healthParts = [];
572
+ if (condition) healthParts.push(condition);
573
+ if (maxCapacityPercent !== void 0) healthParts.push(`${maxCapacityPercent}% capacity`);
574
+ if (cycles !== void 0) healthParts.push(`${cycles} cycles`);
575
+ const health = healthParts.length ? healthParts.join(", ") : "unknown";
576
+ const estimate = estimateBatteryLife(level, state);
577
+ return {
578
+ level,
579
+ state,
580
+ timeRemaining,
581
+ health,
582
+ powerSource: powerPlugged ? "AC" : "Battery",
583
+ cycles,
584
+ condition,
585
+ maxCapacityPercent,
586
+ ...estimate
587
+ };
588
+ } catch {
589
+ return null;
590
+ }
591
+ }
592
+ function pressureFromState(state) {
593
+ const s = state.toLowerCase();
594
+ if (s.includes("critical")) return 3;
595
+ if (s.includes("serious") || s.includes("heavy")) return 2;
596
+ if (s.includes("fair") || s.includes("moderate")) return 1;
597
+ return 0;
598
+ }
599
+ async function collectThermal(detailed) {
600
+ try {
601
+ const therm = (await run("pmset -g therm", "")).trim();
602
+ let state = "Unknown";
603
+ let detail = therm || "No pmset thermal data";
604
+ const stateMatch = therm.match(/Thermal state:\s*(.+)/i);
605
+ if (stateMatch) {
606
+ const raw = stateMatch[1].trim();
607
+ state = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
608
+ detail = state;
609
+ } else {
610
+ const limitMatch = therm.match(
611
+ /CPU_(?:Speed|Scheduler)_Limit\s*(?:=|:)\s*([\d.]+)/i
612
+ );
613
+ if (limitMatch) {
614
+ const limit = parseFloat(limitMatch[1]);
615
+ if (limit >= 100) state = "Nominal";
616
+ else if (limit >= 85) state = "Fair";
617
+ else if (limit >= 50) state = "Serious";
618
+ else state = "Critical";
619
+ detail = `${state} (CPU limit ${limit}%)`;
620
+ } else if (/no thermal warning level has been recorded/i.test(therm)) {
621
+ state = "Nominal";
622
+ detail = "Nominal (no thermal warning recorded)";
623
+ }
624
+ }
625
+ const temperatures = {};
626
+ let cpuDieTemp;
627
+ try {
628
+ const { temps } = await getSmcMetrics();
629
+ cpuDieTemp = temps.cpu_die;
630
+ if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
631
+ if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
632
+ if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
633
+ for (const [k, v] of Object.entries(temps)) {
634
+ if (temperatures[k] === void 0 && v !== void 0) {
635
+ temperatures[k] = v;
636
+ }
637
+ }
638
+ } catch {
639
+ }
640
+ if (state === "Unknown" && cpuDieTemp !== void 0) {
641
+ if (cpuDieTemp >= 100) state = "Critical";
642
+ else if (cpuDieTemp >= 90) state = "Serious";
643
+ else if (cpuDieTemp >= 80) state = "Fair";
644
+ else state = "Nominal";
645
+ detail = `${state} (estimated from ${cpuDieTemp}\xB0C)`;
646
+ }
647
+ return {
648
+ state,
649
+ detail,
650
+ pressureLevel: pressureFromState(state),
651
+ temperatures: Object.keys(temperatures).length ? temperatures : void 0
652
+ };
653
+ } catch {
654
+ return { state: "Unknown", pressureLevel: 0, error: "Thermal info unavailable on this system" };
655
+ }
656
+ }
657
+ async function collectNetwork() {
658
+ try {
659
+ const { iface, ip } = await cachedRouteIface();
660
+ const netStat = await cachedNetstatIb();
661
+ let rxBytes = 0;
662
+ let txBytes = 0;
663
+ let rxPackets = 0;
664
+ let txPackets = 0;
665
+ for (const line of netStat.split("\n")) {
666
+ if (line.startsWith(iface) || line.startsWith(iface + ":")) {
667
+ const parts = line.split(/\s+/);
668
+ if (parts.length >= 9) {
669
+ rxPackets += parseInt(parts[4]) || 0;
670
+ rxBytes += parseInt(parts[5]) || 0;
671
+ txPackets += parseInt(parts[6]) || 0;
672
+ txBytes += parseInt(parts[7]) || 0;
673
+ }
674
+ }
675
+ }
676
+ return { interface: iface, ip, rxBytes, txBytes, rxPackets, txPackets };
677
+ } catch {
678
+ return { interface: "unknown", ip: "0.0.0.0", rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0 };
679
+ }
680
+ }
681
+ async function collectProcesses(limit) {
682
+ try {
683
+ const isMac = process.platform === "darwin";
684
+ const sortArg = isMac ? "-r" : "--sort=-pcpu";
685
+ const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : "";
686
+ const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,time,comm ${sortArg}${headClause}`)).trim();
687
+ const lines = raw.split("\n").slice(1);
688
+ return lines.map((line) => {
689
+ const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
690
+ if (!parts) return null;
691
+ const runtimeSec = parts[7].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
692
+ 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] };
693
+ }).filter((p) => p !== null && p.pid > 0);
694
+ } catch {
695
+ return [];
696
+ }
697
+ }
698
+ async function collectPackets() {
699
+ try {
700
+ const netStat = await cachedNetstatIb();
701
+ const lines = netStat.split("\n").slice(1);
702
+ let totalRxPackets = 0;
703
+ let totalTxPackets = 0;
704
+ let totalRxBytes = 0;
705
+ let totalTxBytes = 0;
706
+ const ifaceStats = [];
707
+ for (const line of lines) {
708
+ const parts = line.split(/\s+/);
709
+ if (parts.length >= 11 && parts[0] && !parts[0].startsWith("Name")) {
710
+ const iface = parts[0];
711
+ const rxPackets = parseInt(parts[4]) || 0;
712
+ const rxBytes = parseInt(parts[5]) || 0;
713
+ const txPackets = parseInt(parts[6]) || 0;
714
+ const txBytes = parseInt(parts[7]) || 0;
715
+ totalRxPackets += rxPackets;
716
+ totalTxPackets += txPackets;
717
+ totalRxBytes += rxBytes;
718
+ totalTxBytes += txBytes;
719
+ if (iface && !iface.includes("lo")) {
720
+ ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
721
+ }
722
+ }
723
+ }
724
+ let connections = 0;
725
+ try {
726
+ const tcpConns = (await run("netstat -an | grep ESTABLISHED | wc -l", "0")).trim();
727
+ connections = parseInt(tcpConns) || 0;
728
+ } catch {
729
+ connections = 0;
730
+ }
731
+ let topProcesses = [];
732
+ let allProcesses = [];
733
+ try {
734
+ const lsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
735
+ const lsofLines = lsof.split("\n");
736
+ topProcesses = lsofLines.map((line) => {
737
+ const parts = line.trim().split(/\s+/);
738
+ if (parts.length >= 3) {
739
+ return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
740
+ }
741
+ return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
742
+ }).filter((p) => p.pid > 0);
743
+ allProcesses = topProcesses;
744
+ } catch {
745
+ topProcesses = [];
746
+ allProcesses = [];
747
+ }
748
+ return {
749
+ totalPackets: totalRxPackets + totalTxPackets,
750
+ rxPackets: totalRxPackets,
751
+ txPackets: totalTxPackets,
752
+ rxRate: 0,
753
+ txRate: 0,
754
+ connections,
755
+ topProcesses,
756
+ interfaces: ifaceStats,
757
+ allProcesses
758
+ };
759
+ } catch {
760
+ return null;
761
+ }
762
+ }
763
+ async function collectTasks(limit = 12) {
764
+ try {
765
+ const isMac = process.platform === "darwin";
766
+ const sortArg = isMac ? "-r" : "--sort=-pcpu";
767
+ const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
768
+ const lines = raw.split("\n").slice(1);
769
+ return lines.map((line) => {
770
+ const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
771
+ if (!parts) return null;
772
+ const runtimeSec = parts[6].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
773
+ return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
774
+ }).filter((t) => t !== null && t.pid > 0);
775
+ } catch {
776
+ return [];
777
+ }
778
+ }
779
+ async function getDisplayInfo() {
780
+ try {
781
+ const raw = await run("system_profiler SPDisplaysDataType 2>&1", "");
782
+ const displays = [];
783
+ const lines = raw.split("\n");
784
+ let currentName = "";
785
+ let currentRes = "";
786
+ let isMain = false;
787
+ let connType = "";
788
+ for (const line of lines) {
789
+ const trimmed = line.trim();
790
+ if (trimmed.endsWith(":") && !trimmed.startsWith("Chipset") && !trimmed.startsWith("Displays") && !trimmed.startsWith("Graphics")) {
791
+ if (currentName && currentRes) {
792
+ displays.push({ name: currentName, resolution: currentRes, isMain, connectionType: connType || void 0 });
793
+ currentRes = "";
794
+ isMain = false;
795
+ connType = "";
796
+ }
797
+ currentName = trimmed.slice(0, -1);
798
+ } else if (trimmed.startsWith("Resolution:")) {
799
+ currentRes = trimmed.replace("Resolution:", "").trim();
800
+ } else if (trimmed.startsWith("Main Display: Yes")) {
801
+ isMain = true;
802
+ } else if (trimmed.startsWith("Connection Type:")) {
803
+ connType = trimmed.replace("Connection Type:", "").trim();
804
+ }
805
+ }
806
+ if (currentName && currentRes) {
807
+ displays.push({ name: currentName, resolution: currentRes, isMain, connectionType: connType || void 0 });
808
+ }
809
+ return displays;
810
+ } catch {
811
+ return [];
812
+ }
813
+ }
814
+ async function getTimeMachineStatus() {
815
+ try {
816
+ const statusRaw = await run("tmutil status 2>&1", "");
817
+ if (statusRaw.includes("No Time Machine destination") || statusRaw.includes("not configured")) {
818
+ return { configured: false, running: false };
819
+ }
820
+ const running = statusRaw.includes("Running = 1;");
821
+ let percent = void 0;
822
+ const pctMatch = statusRaw.match(/Percent\s*=\s*"?(0\.\d+|\d+)"?/i);
823
+ if (pctMatch) {
824
+ percent = Math.round(parseFloat(pctMatch[1]) * 100);
825
+ }
826
+ let latestBackup = "Unknown";
827
+ try {
828
+ const latestRaw = await run("tmutil latestbackup 2>&1", "");
829
+ if (latestRaw.trim() && !latestRaw.includes("error") && !latestRaw.includes("No backups")) {
830
+ const basename = latestRaw.trim().split("/").pop() || latestRaw.trim();
831
+ latestBackup = basename.replace(".previous", "").replace(".backup", "");
832
+ } else {
833
+ latestBackup = "No backup found / Unmounted";
834
+ }
835
+ } catch {
836
+ latestBackup = "Not mounted";
837
+ }
838
+ return { configured: true, running, percent, latestBackup };
839
+ } catch {
840
+ return { configured: false, running: false };
841
+ }
842
+ }
843
+
844
+ export {
845
+ collectAll,
846
+ collectPower,
847
+ collectGpu,
848
+ collectCpu,
849
+ collectMemory,
850
+ collectDisk,
851
+ collectBattery,
852
+ collectThermal,
853
+ collectNetwork,
854
+ collectProcesses,
855
+ collectPackets,
856
+ collectTasks,
857
+ getDisplayInfo,
858
+ getTimeMachineStatus
859
+ };