pyre-cli 9.5.2 → 10.1.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/README.md +73 -16
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
- package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
- package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
- package/dist/chunk-GHRDW646.js +2506 -0
- package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
- package/dist/chunk-M73HCQD2.js +95 -0
- package/dist/chunk-MZO5QKUR.js +32 -0
- package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
- package/dist/doctor-GYHFTNZQ.js +387 -0
- package/dist/explainCmd-MF6PIMLW.js +222 -0
- package/dist/extensions-XJGHG7X3.js +134 -0
- package/dist/index.js +460 -88
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
- package/dist/profileProcCmd-N4H6MCBT.js +107 -0
- package/dist/prometheus-5BL5PVLC.js +6 -0
- package/dist/prometheusCmd-PL64BLN5.js +93 -0
- package/dist/replayCmd-Z3PYXY7V.js +164 -0
- package/dist/run-HOGQDDTA.js +8 -0
- package/dist/serveCmd-2H5VLF3P.js +153 -0
- package/dist/service-Q74BBILO.js +181 -0
- package/dist/smartCmd-ODWKSFNG.js +166 -0
- package/dist/syncCmd-A2W3QB3S.js +126 -0
- package/dist/topoCmd-DU2HHZNH.js +148 -0
- package/dist/watchdogCmd-UF53JPZB.js +161 -0
- package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
- package/install.ps1 +29 -0
- package/install.sh +10 -9
- package/package.json +7 -3
- package/src/ai/models.ts +138 -0
- package/src/alertChannels.ts +119 -0
- package/src/brewHealth.ts +157 -40
- package/src/buildCmd.ts +48 -0
- package/src/checkCmd.ts +5 -5
- package/src/completions.ts +35 -0
- package/src/doctor.ts +235 -7
- package/src/easy_features.test.ts +40 -0
- package/src/explainCmd.ts +261 -0
- package/src/extensions.ts +71 -7
- package/src/formatters/render.ts +34 -10
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +251 -36
- package/src/live/input.ts +60 -0
- package/src/live/render.ts +67 -0
- package/src/live/state.ts +5 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +342 -178
- package/src/monitors/ioreport.py +72 -0
- package/src/monitors/platform/linux.ts +756 -0
- package/src/monitors/platform/windows.ts +519 -0
- package/src/monitors/run.ts +27 -3
- package/src/monitors/smc.ts +21 -1
- package/src/monitors/types.ts +15 -0
- package/src/netUsageCmd.ts +121 -0
- package/src/platform.test.ts +99 -0
- package/src/plugins/index.ts +143 -0
- package/src/profileProcCmd.ts +133 -0
- package/src/prometheusCmd.ts +107 -0
- package/src/replayCmd.ts +186 -0
- package/src/serveCmd.ts +180 -0
- package/src/service.ts +125 -7
- package/src/smartCmd.ts +205 -0
- package/src/state/config.ts +58 -17
- package/src/syncCmd.ts +166 -0
- package/src/topoCmd.ts +176 -0
- package/src/watchdogCmd.ts +198 -0
- package/src/xbar.ts +22 -11
- package/dist/brewHealth-F2BXF5M6.js +0 -74
- package/dist/chunk-FH5WDJBF.js +0 -15
- package/dist/chunk-N4WJVF5M.js +0 -1184
- package/dist/doctor-UV7TWIHX.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
|
@@ -13,11 +13,44 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import chalk from 'chalk';
|
|
15
15
|
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import fs from 'node:fs';
|
|
16
19
|
import { run } from './run.js';
|
|
17
20
|
import { getSmcMetrics, parseSuffix } from './smc.js';
|
|
18
21
|
import { resolveIp } from './dns_cache.js';
|
|
19
22
|
import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData, ContainerData, NetworkConnection, BlenderRenderData, ProtocolStats, ConnectionStateStats, RemoteHostInfo } from './types.js';
|
|
20
23
|
import { collectBlenderRenders } from './blender.js';
|
|
24
|
+
import {
|
|
25
|
+
collectLinuxSystem,
|
|
26
|
+
collectLinuxCpu,
|
|
27
|
+
collectLinuxMemory,
|
|
28
|
+
collectLinuxDisk,
|
|
29
|
+
collectLinuxBattery,
|
|
30
|
+
collectLinuxThermal,
|
|
31
|
+
collectLinuxPower,
|
|
32
|
+
collectLinuxGpu,
|
|
33
|
+
collectLinuxNetwork,
|
|
34
|
+
collectLinuxPackets,
|
|
35
|
+
collectLinuxProcesses,
|
|
36
|
+
collectLinuxTasks,
|
|
37
|
+
getLinuxDisplayInfo,
|
|
38
|
+
} from './platform/linux.js';
|
|
39
|
+
import {
|
|
40
|
+
collectWindowsSystem,
|
|
41
|
+
collectWindowsCpu,
|
|
42
|
+
collectWindowsMemory,
|
|
43
|
+
collectWindowsDisk,
|
|
44
|
+
collectWindowsBattery,
|
|
45
|
+
collectWindowsThermal,
|
|
46
|
+
collectWindowsPower,
|
|
47
|
+
collectWindowsGpu,
|
|
48
|
+
collectWindowsNetwork,
|
|
49
|
+
collectWindowsPackets,
|
|
50
|
+
collectWindowsProcesses,
|
|
51
|
+
collectWindowsTasks,
|
|
52
|
+
getWindowsDisplayInfo,
|
|
53
|
+
} from './platform/windows.js';
|
|
21
54
|
|
|
22
55
|
const SP_TTL_MS = 10_000;
|
|
23
56
|
const NETSTAT_TTL_MS = 1000;
|
|
@@ -34,6 +67,31 @@ const sysctlCache = new Map<string, { value: string; ts: number }>();
|
|
|
34
67
|
|
|
35
68
|
let prevNetSample: { rxBytes: number; txBytes: number; rxPackets: number; txPackets: number; ts: number } | null = null;
|
|
36
69
|
|
|
70
|
+
let cachedIoReportPath: string | null | undefined = undefined;
|
|
71
|
+
function resolveIoReportScript(): string | null {
|
|
72
|
+
if (cachedIoReportPath !== undefined) return cachedIoReportPath;
|
|
73
|
+
try {
|
|
74
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
const candidates = [
|
|
76
|
+
path.join(dir, 'ioreport.py'),
|
|
77
|
+
path.join(dir, '..', 'src', 'monitors', 'ioreport.py'),
|
|
78
|
+
path.join(dir, 'monitors', 'ioreport.py'),
|
|
79
|
+
path.join(process.cwd(), 'src', 'monitors', 'ioreport.py'),
|
|
80
|
+
path.join(process.cwd(), 'dist', 'ioreport.py'),
|
|
81
|
+
];
|
|
82
|
+
for (const c of candidates) {
|
|
83
|
+
if (fs.existsSync(c)) {
|
|
84
|
+
cachedIoReportPath = c;
|
|
85
|
+
return c;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
cachedIoReportPath = null;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
37
95
|
async function cachedSysctl(key: string, fallback: string): Promise<string> {
|
|
38
96
|
const now = Date.now();
|
|
39
97
|
const hit = sysctlCache.get(key);
|
|
@@ -81,7 +139,7 @@ const MIN_ESTIMATE_WINDOW_MS = 30 * 1000;
|
|
|
81
139
|
|
|
82
140
|
let prevBattery: { samples: { level: number; ts: number }[] } = { samples: [] };
|
|
83
141
|
|
|
84
|
-
function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmpty?: string; dischargeRatePerHour?: number
|
|
142
|
+
function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmpty?: string; dischargeRatePerHour?: number } {
|
|
85
143
|
const now = Date.now();
|
|
86
144
|
|
|
87
145
|
if (state === 'charged' || state === 'charging') {
|
|
@@ -117,21 +175,18 @@ function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmp
|
|
|
117
175
|
const m = Math.round((hoursToEmpty - h) * 60);
|
|
118
176
|
const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : 'calculating';
|
|
119
177
|
|
|
120
|
-
|
|
121
|
-
// in Wh) / 100. This is only used as a last-resort estimate — collectPower()
|
|
122
|
-
// now prefers real ioreg-derived wattage when available (see below).
|
|
123
|
-
const ASSUMED_CAPACITY_WH = 55;
|
|
124
|
-
const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
|
|
125
|
-
|
|
126
|
-
return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
|
|
178
|
+
return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10 };
|
|
127
179
|
}
|
|
128
180
|
|
|
181
|
+
import { collectBuilds } from './buildTracker.js';
|
|
182
|
+
import { computeSmartBatteryPrediction } from './batteryPredictor.js';
|
|
183
|
+
|
|
129
184
|
/**
|
|
130
185
|
* Gather every metric category in parallel and return a
|
|
131
186
|
* complete {@link StatsData} snapshot.
|
|
132
187
|
*/
|
|
133
188
|
export async function collectAll(opts: { detailed?: boolean; processLimit?: number } = {}): Promise<StatsData> {
|
|
134
|
-
const [cpu, gpu, memory, disk,
|
|
189
|
+
const [cpu, gpu, memory, disk, rawBattery, thermal, network, processes, system, power, packets, tasks, containers, blenderRenders, activeBuilds] = await Promise.all([
|
|
135
190
|
collectCpu(),
|
|
136
191
|
collectGpu(opts.detailed),
|
|
137
192
|
collectMemory(),
|
|
@@ -146,8 +201,14 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
146
201
|
collectTasks(),
|
|
147
202
|
collectContainers(),
|
|
148
203
|
collectBlenderRenders(),
|
|
204
|
+
collectBuilds(),
|
|
149
205
|
]);
|
|
150
206
|
|
|
207
|
+
const battery = rawBattery ? {
|
|
208
|
+
...rawBattery,
|
|
209
|
+
smartPrediction: computeSmartBatteryPrediction(rawBattery, power) || undefined,
|
|
210
|
+
} : null;
|
|
211
|
+
|
|
151
212
|
return {
|
|
152
213
|
header: {
|
|
153
214
|
title: chalk.hex('#ff5500').bold('PYRE'),
|
|
@@ -169,6 +230,7 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
169
230
|
tasks,
|
|
170
231
|
containers,
|
|
171
232
|
blenderRenders,
|
|
233
|
+
activeBuilds,
|
|
172
234
|
};
|
|
173
235
|
}
|
|
174
236
|
|
|
@@ -201,57 +263,65 @@ export async function collectContainers(): Promise<ContainerData[]> {
|
|
|
201
263
|
* power data is available.
|
|
202
264
|
*/
|
|
203
265
|
export async function collectPower(): Promise<PowerData | null> {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
266
|
+
if (process.platform === 'linux') return collectLinuxPower();
|
|
267
|
+
if (process.platform === 'win32') return collectWindowsPower();
|
|
268
|
+
try {
|
|
269
|
+
const { power } = await getSmcMetrics();
|
|
270
|
+
if (power.cpu !== undefined || power.gpu !== undefined || power.combined !== undefined) {
|
|
271
|
+
return { cpuWatts: power.cpu, gpuWatts: power.gpu, aneWatts: 0, combinedWatts: power.combined };
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// ignore
|
|
275
|
+
}
|
|
212
276
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
let ampsBig = BigInt(ampMatch[1]);
|
|
234
|
-
if (!ampMatch[1].startsWith('-')) {
|
|
235
|
-
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
236
|
-
const INT64_MAX = (1n << 63n) - 1n;
|
|
237
|
-
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
238
|
-
}
|
|
239
|
-
const amps = Number(ampsBig);
|
|
240
|
-
const volts = parseInt(voltMatch[1], 10) / 1000;
|
|
241
|
-
const watts = Math.abs((amps / 1000) * volts);
|
|
242
|
-
// Sanity clamp — a real Mac never draws anywhere near this much;
|
|
243
|
-
// treat an out-of-range reading as "no usable data" rather than
|
|
244
|
-
// display nonsense.
|
|
245
|
-
if (Number.isFinite(watts) && watts < 500) {
|
|
246
|
-
return { combinedWatts: Math.round(watts * 100) / 100 };
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
} catch {
|
|
250
|
-
// ignore
|
|
251
|
-
}
|
|
277
|
+
// Apple Silicon IOReport (reads Energy Model channels GPU Energy, CPU Energy, ANE without root)
|
|
278
|
+
const scriptPath = resolveIoReportScript();
|
|
279
|
+
if (scriptPath && process.platform === 'darwin') {
|
|
280
|
+
try {
|
|
281
|
+
const pyOut = (await run(`python3 "${scriptPath}" 2>/dev/null`, '')).trim();
|
|
282
|
+
if (pyOut.startsWith('{') && pyOut.endsWith('}')) {
|
|
283
|
+
const parsed = JSON.parse(pyOut);
|
|
284
|
+
if (typeof parsed.cpuWatts === 'number' || typeof parsed.gpuWatts === 'number') {
|
|
285
|
+
return {
|
|
286
|
+
cpuWatts: parsed.cpuWatts,
|
|
287
|
+
gpuWatts: parsed.gpuWatts,
|
|
288
|
+
aneWatts: parsed.aneWatts ?? 0,
|
|
289
|
+
combinedWatts: parsed.combinedWatts ?? (Math.round(((parsed.cpuWatts ?? 0) + (parsed.gpuWatts ?? 0) + (parsed.aneWatts ?? 0)) * 100) / 100),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// ignore
|
|
295
|
+
}
|
|
296
|
+
}
|
|
252
297
|
|
|
253
|
-
|
|
254
|
-
|
|
298
|
+
// Fallback: ioreg's AppleSmartBattery entry exposes instantaneous
|
|
299
|
+
// amperage (mA, signed: negative while discharging) and voltage (mV)
|
|
300
|
+
// without any privileges, so we can derive real wattage from that.
|
|
301
|
+
try {
|
|
302
|
+
const ioreg = (await run('ioreg -rn AppleSmartBattery 2>/dev/null')).trim();
|
|
303
|
+
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
304
|
+
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
305
|
+
if (ampMatch && voltMatch) {
|
|
306
|
+
let ampsBig = BigInt(ampMatch[1]);
|
|
307
|
+
if (!ampMatch[1].startsWith('-')) {
|
|
308
|
+
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
309
|
+
const INT64_MAX = (1n << 63n) - 1n;
|
|
310
|
+
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
311
|
+
}
|
|
312
|
+
const amps = Number(ampsBig);
|
|
313
|
+
const volts = parseInt(voltMatch[1], 10) / 1000;
|
|
314
|
+
const watts = Math.abs((amps / 1000) * volts);
|
|
315
|
+
if (Number.isFinite(watts) && watts < 500) {
|
|
316
|
+
return { combinedWatts: Math.round(watts * 100) / 100 };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
// ignore
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
255
325
|
|
|
256
326
|
/**
|
|
257
327
|
* Gather GPU information from system_profiler and powermetrics.
|
|
@@ -263,6 +333,8 @@ export async function collectPower(): Promise<PowerData | null> {
|
|
|
263
333
|
* Returns null when GPU hardware is unavailable.
|
|
264
334
|
*/
|
|
265
335
|
export async function collectGpu(detailed?: boolean): Promise<GpuData | null> {
|
|
336
|
+
if (process.platform === 'linux') return collectLinuxGpu();
|
|
337
|
+
if (process.platform === 'win32') return collectWindowsGpu();
|
|
266
338
|
let spRaw = '';
|
|
267
339
|
const now = Date.now();
|
|
268
340
|
if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
|
|
@@ -401,7 +473,9 @@ export async function collectPower(): Promise<PowerData | null> {
|
|
|
401
473
|
return { model, memory, utilization, temperature, processes: gpuProcesses };
|
|
402
474
|
}
|
|
403
475
|
|
|
404
|
-
async function collectSystem(): Promise<{ hostname: string; os: string; uptime: string }> {
|
|
476
|
+
export async function collectSystem(): Promise<{ hostname: string; os: string; uptime: string }> {
|
|
477
|
+
if (process.platform === 'linux') return collectLinuxSystem();
|
|
478
|
+
if (process.platform === 'win32') return collectWindowsSystem();
|
|
405
479
|
const hostname = (await run('hostname', 'Mac')).trim();
|
|
406
480
|
const osRaw = (await run('sw_vers -productVersion', 'Unknown')).trim();
|
|
407
481
|
const uptimeRaw = (await run('uptime')).trim();
|
|
@@ -414,6 +488,17 @@ async function collectSystem(): Promise<{ hostname: string; os: string; uptime:
|
|
|
414
488
|
}
|
|
415
489
|
|
|
416
490
|
export async function collectCpu(): Promise<CpuData> {
|
|
491
|
+
const prevCpuTimesRef = { current: prevCpuTimes };
|
|
492
|
+
if (process.platform === 'linux') {
|
|
493
|
+
const res = await collectLinuxCpu(prevCpuTimesRef);
|
|
494
|
+
prevCpuTimes = prevCpuTimesRef.current;
|
|
495
|
+
return res;
|
|
496
|
+
}
|
|
497
|
+
if (process.platform === 'win32') {
|
|
498
|
+
const res = await collectWindowsCpu(prevCpuTimesRef);
|
|
499
|
+
prevCpuTimes = prevCpuTimesRef.current;
|
|
500
|
+
return res;
|
|
501
|
+
}
|
|
417
502
|
const brand = (await cachedSysctl('machdep.cpu.brand_string', 'Unknown CPU')).trim();
|
|
418
503
|
const cores = parseInt(await cachedSysctl('hw.ncpu', '1')) || 1;
|
|
419
504
|
const physicalCores = parseInt(await cachedSysctl('hw.physicalcpu', String(cores))) || cores;
|
|
@@ -533,6 +618,8 @@ export async function collectCpu(): Promise<CpuData> {
|
|
|
533
618
|
}
|
|
534
619
|
|
|
535
620
|
export async function collectMemory(): Promise<MemoryData> {
|
|
621
|
+
if (process.platform === 'linux') return collectLinuxMemory();
|
|
622
|
+
if (process.platform === 'win32') return collectWindowsMemory();
|
|
536
623
|
const totalBytes = parseInt(await cachedSysctl('hw.memsize', '0')) || 0;
|
|
537
624
|
const pageSize = parseInt(await cachedSysctl('hw.pagesize', '4096')) || 4096;
|
|
538
625
|
|
|
@@ -607,7 +694,7 @@ export async function collectMemory(): Promise<MemoryData> {
|
|
|
607
694
|
};
|
|
608
695
|
}
|
|
609
696
|
|
|
610
|
-
let prevDiskIoSample: { ts: number } | null = null;
|
|
697
|
+
let prevDiskIoSample: { totalBytes: number; ts: number } | null = null;
|
|
611
698
|
let cachedDiskIo: { readBytesSec: number; writeBytesSec: number; ts: number } | null = null;
|
|
612
699
|
|
|
613
700
|
async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec: number }> {
|
|
@@ -616,22 +703,23 @@ async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec:
|
|
|
616
703
|
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
617
704
|
}
|
|
618
705
|
try {
|
|
619
|
-
const raw = (await run('iostat -
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
if (
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
633
|
-
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
706
|
+
const raw = (await run('iostat -Id -c 1', '', 1000)).trim();
|
|
707
|
+
const lines = raw.split('\n');
|
|
708
|
+
if (lines.length >= 3) {
|
|
709
|
+
const dataLine = lines[lines.length - 1].trim();
|
|
710
|
+
const parts = dataLine.split(/\s+/);
|
|
711
|
+
if (parts.length >= 3) {
|
|
712
|
+
const totalMb = parseFloat(parts[2]);
|
|
713
|
+
if (!isNaN(totalMb)) {
|
|
714
|
+
const totalBytes = totalMb * 1024 * 1024;
|
|
715
|
+
let bytesSec = 0;
|
|
716
|
+
if (prevDiskIoSample && prevDiskIoSample.ts > 0) {
|
|
717
|
+
const dt = Math.max((now - prevDiskIoSample.ts) / 1000, 0.001);
|
|
718
|
+
bytesSec = Math.max(0, (totalBytes - prevDiskIoSample.totalBytes) / dt);
|
|
634
719
|
}
|
|
720
|
+
prevDiskIoSample = { totalBytes, ts: now };
|
|
721
|
+
cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
722
|
+
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
635
723
|
}
|
|
636
724
|
}
|
|
637
725
|
}
|
|
@@ -642,6 +730,8 @@ async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec:
|
|
|
642
730
|
}
|
|
643
731
|
|
|
644
732
|
export async function collectDisk(): Promise<DiskData[]> {
|
|
733
|
+
if (process.platform === 'linux') return collectLinuxDisk();
|
|
734
|
+
if (process.platform === 'win32') return collectWindowsDisk();
|
|
645
735
|
const [rawDf, ioRates] = await Promise.all([
|
|
646
736
|
run('df -h'),
|
|
647
737
|
getDiskIoRates(),
|
|
@@ -665,6 +755,8 @@ export async function collectDisk(): Promise<DiskData[]> {
|
|
|
665
755
|
}
|
|
666
756
|
|
|
667
757
|
export async function collectBattery(): Promise<BatteryData | null> {
|
|
758
|
+
if (process.platform === 'linux') return collectLinuxBattery();
|
|
759
|
+
if (process.platform === 'win32') return collectWindowsBattery();
|
|
668
760
|
try {
|
|
669
761
|
const raw = (await run('pmset -g batt')).trim();
|
|
670
762
|
if (!raw.includes('Battery Power') && !raw.includes('AC Power')) return null;
|
|
@@ -734,6 +826,31 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
734
826
|
|
|
735
827
|
const estimate = estimateBatteryLife(level, state);
|
|
736
828
|
|
|
829
|
+
let powerWatts: number | undefined;
|
|
830
|
+
if (!powerPlugged && state === 'discharging') {
|
|
831
|
+
try {
|
|
832
|
+
const ioreg = (await run('ioreg -rn AppleSmartBattery 2>/dev/null')).trim();
|
|
833
|
+
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
834
|
+
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
835
|
+
if (ampMatch && voltMatch) {
|
|
836
|
+
let ampsBig = BigInt(ampMatch[1]);
|
|
837
|
+
if (!ampMatch[1].startsWith('-')) {
|
|
838
|
+
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
839
|
+
const INT64_MAX = (1n << 63n) - 1n;
|
|
840
|
+
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
841
|
+
}
|
|
842
|
+
const amps = Number(ampsBig);
|
|
843
|
+
const volts = parseInt(voltMatch[1], 10) / 1000;
|
|
844
|
+
const watts = Math.abs((amps / 1000) * volts);
|
|
845
|
+
if (Number.isFinite(watts) && watts < 500) {
|
|
846
|
+
powerWatts = Math.round(watts * 100) / 100;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
} catch {
|
|
850
|
+
// ignore
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
737
854
|
return {
|
|
738
855
|
level,
|
|
739
856
|
state,
|
|
@@ -744,6 +861,7 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
744
861
|
condition,
|
|
745
862
|
maxCapacityPercent,
|
|
746
863
|
friendlySummary,
|
|
864
|
+
powerWatts,
|
|
747
865
|
...estimate,
|
|
748
866
|
};
|
|
749
867
|
} catch {
|
|
@@ -760,6 +878,8 @@ function pressureFromState(state: string): number {
|
|
|
760
878
|
}
|
|
761
879
|
|
|
762
880
|
export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
|
|
881
|
+
if (process.platform === 'linux') return collectLinuxThermal();
|
|
882
|
+
if (process.platform === 'win32') return collectWindowsThermal();
|
|
763
883
|
try {
|
|
764
884
|
const therm = (await run('pmset -g therm', '')).trim();
|
|
765
885
|
|
|
@@ -828,11 +948,21 @@ export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
|
|
|
828
948
|
detail = `${state} (estimated from ${cpuDieTemp}°C)`;
|
|
829
949
|
}
|
|
830
950
|
|
|
951
|
+
// Collect fan speeds from SMC (same cached call as temps above)
|
|
952
|
+
let fans: import('./types.js').FanData[] = [];
|
|
953
|
+
try {
|
|
954
|
+
const smcData = await getSmcMetrics();
|
|
955
|
+
fans = smcData.fans;
|
|
956
|
+
} catch {
|
|
957
|
+
// ignore — fans unavailable without sudo
|
|
958
|
+
}
|
|
959
|
+
|
|
831
960
|
return {
|
|
832
961
|
state,
|
|
833
962
|
detail,
|
|
834
963
|
pressureLevel: pressureFromState(state),
|
|
835
964
|
temperatures: Object.keys(temperatures).length ? temperatures : undefined,
|
|
965
|
+
fans: fans.length > 0 ? fans : undefined,
|
|
836
966
|
};
|
|
837
967
|
} catch {
|
|
838
968
|
return { state: 'Unknown', pressureLevel: 0, error: 'Thermal info unavailable on this system' };
|
|
@@ -840,6 +970,8 @@ export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
|
|
|
840
970
|
}
|
|
841
971
|
|
|
842
972
|
export async function collectNetwork(): Promise<NetworkData> {
|
|
973
|
+
if (process.platform === 'linux') return collectLinuxNetwork();
|
|
974
|
+
if (process.platform === 'win32') return collectWindowsNetwork();
|
|
843
975
|
try {
|
|
844
976
|
const { iface, ip } = await cachedRouteIface();
|
|
845
977
|
const netStat = await cachedNetstatIb();
|
|
@@ -1024,6 +1156,8 @@ async function getTopRemoteHosts(): Promise<RemoteHostInfo[]> {
|
|
|
1024
1156
|
}
|
|
1025
1157
|
|
|
1026
1158
|
export async function collectProcesses(limit?: number): Promise<ProcessData[]> {
|
|
1159
|
+
if (process.platform === 'linux') return collectLinuxProcesses(limit);
|
|
1160
|
+
if (process.platform === 'win32') return collectWindowsProcesses(limit);
|
|
1027
1161
|
try {
|
|
1028
1162
|
const isMac = process.platform === 'darwin';
|
|
1029
1163
|
const sortArg = isMac ? '-r' : '--sort=-pcpu';
|
|
@@ -1045,109 +1179,132 @@ export async function collectProcesses(limit?: number): Promise<ProcessData[]> {
|
|
|
1045
1179
|
}
|
|
1046
1180
|
}
|
|
1047
1181
|
|
|
1182
|
+
function parseBytes(str: string): number {
|
|
1183
|
+
if (!str) return 0;
|
|
1184
|
+
const match = str.trim().match(/([\d.]+)\s*([KMGTPE]?)i?B?/i);
|
|
1185
|
+
if (!match) return 0;
|
|
1186
|
+
let val = parseFloat(match[1]);
|
|
1187
|
+
const unit = match[2].toUpperCase();
|
|
1188
|
+
if (unit === 'K') val *= 1024;
|
|
1189
|
+
else if (unit === 'M') val *= 1024 * 1024;
|
|
1190
|
+
else if (unit === 'G') val *= 1024 * 1024 * 1024;
|
|
1191
|
+
else if (unit === 'T') val *= 1024 * 1024 * 1024 * 1024;
|
|
1192
|
+
return Math.round(val);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1048
1195
|
export async function collectPackets(): Promise<PacketData | null> {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1196
|
+
if (process.platform === 'linux') return collectLinuxPackets();
|
|
1197
|
+
if (process.platform === 'win32') return collectWindowsPackets();
|
|
1198
|
+
try {
|
|
1199
|
+
const netStat = await cachedNetstatIb();
|
|
1200
|
+
const lines = netStat.split('\n').slice(1);
|
|
1201
|
+
let totalRxPackets = 0;
|
|
1202
|
+
let totalTxPackets = 0;
|
|
1203
|
+
let totalRxBytes = 0;
|
|
1204
|
+
let totalTxBytes = 0;
|
|
1205
|
+
const ifaceStats: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[] = [];
|
|
1206
|
+
|
|
1207
|
+
for (const line of lines) {
|
|
1208
|
+
const parts = line.split(/\s+/);
|
|
1209
|
+
if (parts.length >= 11 && parts[0] && !parts[0].startsWith('Name')) {
|
|
1210
|
+
const iface = parts[0];
|
|
1211
|
+
const rxPackets = parseInt(parts[4]) || 0;
|
|
1212
|
+
const rxBytes = parseInt(parts[5]) || 0;
|
|
1213
|
+
const txPackets = parseInt(parts[6]) || 0;
|
|
1214
|
+
const txBytes = parseInt(parts[7]) || 0;
|
|
1215
|
+
totalRxPackets += rxPackets;
|
|
1216
|
+
totalTxPackets += txPackets;
|
|
1217
|
+
totalRxBytes += rxBytes;
|
|
1218
|
+
totalTxBytes += txBytes;
|
|
1219
|
+
if (iface && !iface.includes('lo')) {
|
|
1220
|
+
ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
let connections = 0;
|
|
1226
|
+
let allProcesses: NetworkProcess[] = [];
|
|
1227
|
+
|
|
1228
|
+
try {
|
|
1229
|
+
const nettop = (await run('script -q /dev/null nettop -l 1 -J bytes_in,bytes_out,state 2>/dev/null', '', 2000)).trim();
|
|
1230
|
+
const rows = nettop.split('\n');
|
|
1231
|
+
|
|
1232
|
+
let currentProc: NetworkProcess | null = null;
|
|
1233
|
+
|
|
1234
|
+
for (const row of rows) {
|
|
1235
|
+
if (!row || row.includes('bytes_in')) continue;
|
|
1236
|
+
|
|
1237
|
+
const parts = row.trimStart().split(/\s{2,}/);
|
|
1238
|
+
const nameCol = parts[0];
|
|
1239
|
+
|
|
1240
|
+
// If it doesn't contain <->, it's a process row
|
|
1241
|
+
if (!nameCol.includes('<->')) {
|
|
1242
|
+
if (currentProc) {
|
|
1243
|
+
if (currentProc.connections && currentProc.connections.length > 0) {
|
|
1244
|
+
allProcesses.push(currentProc);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Extract PID and Name (e.g., "syslogd.369")
|
|
1249
|
+
const lastDot = nameCol.lastIndexOf('.');
|
|
1250
|
+
let pid = 0;
|
|
1251
|
+
let command = nameCol;
|
|
1252
|
+
if (lastDot > 0) {
|
|
1253
|
+
const possiblePid = parseInt(nameCol.substring(lastDot + 1));
|
|
1254
|
+
if (!isNaN(possiblePid)) {
|
|
1255
|
+
pid = possiblePid;
|
|
1256
|
+
command = nameCol.substring(0, lastDot);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const rx = parseBytes(parts[1]);
|
|
1261
|
+
const tx = parseBytes(parts[2]);
|
|
1262
|
+
|
|
1263
|
+
currentProc = { pid, command, rxBytes: rx, txBytes: tx, connections: [] };
|
|
1264
|
+
} else {
|
|
1265
|
+
// Connection row
|
|
1266
|
+
if (currentProc) {
|
|
1267
|
+
let state = '';
|
|
1268
|
+
let rx = 0;
|
|
1269
|
+
let tx = 0;
|
|
1270
|
+
if (parts.length >= 4) {
|
|
1271
|
+
state = parts[1];
|
|
1272
|
+
rx = parseBytes(parts[2]);
|
|
1273
|
+
tx = parseBytes(parts[3]);
|
|
1274
|
+
} else if (parts.length === 3) {
|
|
1275
|
+
rx = parseBytes(parts[1]);
|
|
1276
|
+
tx = parseBytes(parts[2]);
|
|
1277
|
+
}
|
|
1278
|
+
if (state === 'Established') connections++;
|
|
1279
|
+
|
|
1280
|
+
// Parse endpoints (e.g. "tcp4 172.16.0.2:49844<->17.242.13.5:5223")
|
|
1281
|
+
const spaceIdx = nameCol.indexOf(' ');
|
|
1282
|
+
const protocol = spaceIdx > 0 ? nameCol.substring(0, spaceIdx) : '';
|
|
1283
|
+
const endpoints = spaceIdx > 0 ? nameCol.substring(spaceIdx + 1) : nameCol;
|
|
1284
|
+
const arrowIdx = endpoints.indexOf('<->');
|
|
1285
|
+
let local = '';
|
|
1286
|
+
let remote = '';
|
|
1287
|
+
if (arrowIdx > 0) {
|
|
1288
|
+
local = endpoints.substring(0, arrowIdx);
|
|
1289
|
+
remote = endpoints.substring(arrowIdx + 3);
|
|
1290
|
+
|
|
1291
|
+
// Attempt DNS resolution on remote IP
|
|
1292
|
+
remote = resolveIp(remote);
|
|
1293
|
+
} else {
|
|
1294
|
+
local = endpoints;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Ignore purely local communication if you want, but for now include all
|
|
1298
|
+
currentProc.connections!.push({ protocol, local, remote, state, rxBytes: rx, txBytes: tx });
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (currentProc && currentProc.connections && currentProc.connections.length > 0) {
|
|
1303
|
+
allProcesses.push(currentProc);
|
|
1304
|
+
}
|
|
1305
|
+
} catch {
|
|
1306
|
+
allProcesses = [];
|
|
1307
|
+
}
|
|
1151
1308
|
|
|
1152
1309
|
// Sort processes by total bandwidth (tx + rx) to get top processes
|
|
1153
1310
|
const topProcesses = [...allProcesses].sort((a, b) => (b.rxBytes + b.txBytes) - (a.rxBytes + a.txBytes));
|
|
@@ -1176,7 +1333,9 @@ export async function collectPackets(): Promise<PacketData | null> {
|
|
|
1176
1333
|
}
|
|
1177
1334
|
|
|
1178
1335
|
export async function collectTasks(limit = 12): Promise<TaskData[]> {
|
|
1179
|
-
|
|
1336
|
+
if (process.platform === 'linux') return collectLinuxTasks(limit);
|
|
1337
|
+
if (process.platform === 'win32') return collectWindowsTasks(limit);
|
|
1338
|
+
try {
|
|
1180
1339
|
const isMac = process.platform === 'darwin';
|
|
1181
1340
|
const sortArg = isMac ? '-r' : '--sort=-pcpu';
|
|
1182
1341
|
const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
|
|
@@ -1202,6 +1361,8 @@ export interface DisplayInfo {
|
|
|
1202
1361
|
}
|
|
1203
1362
|
|
|
1204
1363
|
export async function getDisplayInfo(): Promise<DisplayInfo[]> {
|
|
1364
|
+
if (process.platform === 'linux') return getLinuxDisplayInfo();
|
|
1365
|
+
if (process.platform === 'win32') return getWindowsDisplayInfo();
|
|
1205
1366
|
try {
|
|
1206
1367
|
const raw = await run('system_profiler SPDisplaysDataType 2>&1', '');
|
|
1207
1368
|
const displays: DisplayInfo[] = [];
|
|
@@ -1246,6 +1407,9 @@ export interface TimeMachineStatus {
|
|
|
1246
1407
|
}
|
|
1247
1408
|
|
|
1248
1409
|
export async function getTimeMachineStatus(): Promise<TimeMachineStatus> {
|
|
1410
|
+
if (process.platform !== 'darwin') {
|
|
1411
|
+
return { configured: false, running: false };
|
|
1412
|
+
}
|
|
1249
1413
|
try {
|
|
1250
1414
|
const statusRaw = await run('tmutil status 2>&1', '');
|
|
1251
1415
|
if (statusRaw.includes('No Time Machine destination') || statusRaw.includes('not configured')) {
|