pyre-cli 9.5.1 → 10.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.
Files changed (94) hide show
  1. package/README.md +84 -14
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/blender-FGH3ASEQ.js +7 -0
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
  7. package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
  8. package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
  9. package/dist/chunk-GHRDW646.js +2506 -0
  10. package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
  11. package/dist/chunk-M73HCQD2.js +95 -0
  12. package/dist/chunk-MZO5QKUR.js +32 -0
  13. package/dist/chunk-VIACN2RU.js +133 -0
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-4ZLF4HTS.js → config-GYGJXU2Q.js} +9 -1
  19. package/dist/doctor-GYHFTNZQ.js +387 -0
  20. package/dist/explainCmd-MF6PIMLW.js +222 -0
  21. package/dist/extensions-XJGHG7X3.js +134 -0
  22. package/dist/index.js +841 -206
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -3
  26. package/dist/profileProcCmd-N4H6MCBT.js +107 -0
  27. package/dist/prometheus-5BL5PVLC.js +6 -0
  28. package/dist/prometheusCmd-PL64BLN5.js +93 -0
  29. package/dist/replayCmd-Z3PYXY7V.js +164 -0
  30. package/dist/run-HOGQDDTA.js +8 -0
  31. package/dist/serveCmd-2H5VLF3P.js +153 -0
  32. package/dist/service-Q74BBILO.js +181 -0
  33. package/dist/smartCmd-ODWKSFNG.js +166 -0
  34. package/dist/syncCmd-A2W3QB3S.js +126 -0
  35. package/dist/topoCmd-DU2HHZNH.js +148 -0
  36. package/dist/watchdogCmd-UF53JPZB.js +161 -0
  37. package/dist/{wizard-GVIAHIVV.js → wizard-TD7BGX6O.js} +1 -1
  38. package/install.ps1 +29 -0
  39. package/install.sh +10 -9
  40. package/package.json +7 -3
  41. package/src/ai/models.ts +138 -0
  42. package/src/alertChannels.ts +119 -0
  43. package/src/anomalies.ts +114 -60
  44. package/src/brewHealth.ts +157 -40
  45. package/src/buildCmd.ts +48 -0
  46. package/src/checkCmd.ts +5 -5
  47. package/src/completions.ts +35 -0
  48. package/src/doctor.ts +235 -7
  49. package/src/easy_features.test.ts +40 -0
  50. package/src/explainCmd.ts +261 -0
  51. package/src/extensions.ts +71 -7
  52. package/src/formatters/index.ts +1 -1
  53. package/src/formatters/output.ts +100 -13
  54. package/src/formatters/render.ts +274 -38
  55. package/src/formatters/themes.ts +8 -0
  56. package/src/formatters/types.ts +6 -1
  57. package/src/index.ts +298 -56
  58. package/src/live/input.ts +122 -4
  59. package/src/live/render.ts +98 -0
  60. package/src/live/state.ts +70 -52
  61. package/src/live/types.ts +2 -2
  62. package/src/monitors/batteryPredictor.ts +143 -0
  63. package/src/monitors/blender.ts +144 -0
  64. package/src/monitors/buildTracker.ts +121 -0
  65. package/src/monitors/collectors.ts +558 -147
  66. package/src/monitors/dns_cache.ts +48 -0
  67. package/src/monitors/index.ts +3 -2
  68. package/src/monitors/ioreport.py +72 -0
  69. package/src/monitors/platform/linux.ts +756 -0
  70. package/src/monitors/platform/windows.ts +519 -0
  71. package/src/monitors/run.ts +27 -3
  72. package/src/monitors/smc.ts +21 -1
  73. package/src/monitors/types.ts +112 -14
  74. package/src/netUsageCmd.ts +121 -0
  75. package/src/platform.test.ts +99 -0
  76. package/src/plugins/index.ts +143 -0
  77. package/src/profileProcCmd.ts +133 -0
  78. package/src/prometheusCmd.ts +107 -0
  79. package/src/replayCmd.ts +186 -0
  80. package/src/serveCmd.ts +180 -0
  81. package/src/service.ts +125 -7
  82. package/src/smartCmd.ts +205 -0
  83. package/src/state/config.ts +64 -17
  84. package/src/syncCmd.ts +166 -0
  85. package/src/topoCmd.ts +176 -0
  86. package/src/watchdogCmd.ts +198 -0
  87. package/src/xbar.ts +22 -11
  88. package/dist/brewHealth-F2BXF5M6.js +0 -74
  89. package/dist/chunk-FH5WDJBF.js +0 -15
  90. package/dist/chunk-HGRBBPYX.js +0 -937
  91. package/dist/doctor-L6FZYA7F.js +0 -184
  92. package/dist/extensions-DVA5YLGH.js +0 -74
  93. package/dist/service-WTS3NBQU.js +0 -73
  94. /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
@@ -13,9 +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
- import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData } from './types.js';
21
+ import { resolveIp } from './dns_cache.js';
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';
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';
19
54
 
20
55
  const SP_TTL_MS = 10_000;
21
56
  const NETSTAT_TTL_MS = 1000;
@@ -30,6 +65,33 @@ let netstatIbCache: { raw: string; ts: number } | null = null;
30
65
  let routeCache: { iface: string; ip: string; ts: number } | null = null;
31
66
  const sysctlCache = new Map<string, { value: string; ts: number }>();
32
67
 
68
+ let prevNetSample: { rxBytes: number; txBytes: number; rxPackets: number; txPackets: number; ts: number } | null = null;
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
+
33
95
  async function cachedSysctl(key: string, fallback: string): Promise<string> {
34
96
  const now = Date.now();
35
97
  const hit = sysctlCache.get(key);
@@ -77,7 +139,7 @@ const MIN_ESTIMATE_WINDOW_MS = 30 * 1000;
77
139
 
78
140
  let prevBattery: { samples: { level: number; ts: number }[] } = { samples: [] };
79
141
 
80
- function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmpty?: string; dischargeRatePerHour?: number; powerWatts?: number } {
142
+ function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmpty?: string; dischargeRatePerHour?: number } {
81
143
  const now = Date.now();
82
144
 
83
145
  if (state === 'charged' || state === 'charging') {
@@ -113,21 +175,18 @@ function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmp
113
175
  const m = Math.round((hoursToEmpty - h) * 60);
114
176
  const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : 'calculating';
115
177
 
116
- // Rough wattage from discharge rate: %/hour * (typical battery capacity
117
- // in Wh) / 100. This is only used as a last-resort estimate — collectPower()
118
- // now prefers real ioreg-derived wattage when available (see below).
119
- const ASSUMED_CAPACITY_WH = 55;
120
- const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
121
-
122
- return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
178
+ return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10 };
123
179
  }
124
180
 
181
+ import { collectBuilds } from './buildTracker.js';
182
+ import { computeSmartBatteryPrediction } from './batteryPredictor.js';
183
+
125
184
  /**
126
185
  * Gather every metric category in parallel and return a
127
186
  * complete {@link StatsData} snapshot.
128
187
  */
129
188
  export async function collectAll(opts: { detailed?: boolean; processLimit?: number } = {}): Promise<StatsData> {
130
- const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
189
+ const [cpu, gpu, memory, disk, rawBattery, thermal, network, processes, system, power, packets, tasks, containers, blenderRenders, activeBuilds] = await Promise.all([
131
190
  collectCpu(),
132
191
  collectGpu(opts.detailed),
133
192
  collectMemory(),
@@ -140,8 +199,16 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
140
199
  collectPower(),
141
200
  collectPackets(),
142
201
  collectTasks(),
202
+ collectContainers(),
203
+ collectBlenderRenders(),
204
+ collectBuilds(),
143
205
  ]);
144
206
 
207
+ const battery = rawBattery ? {
208
+ ...rawBattery,
209
+ smartPrediction: computeSmartBatteryPrediction(rawBattery, power) || undefined,
210
+ } : null;
211
+
145
212
  return {
146
213
  header: {
147
214
  title: chalk.hex('#ff5500').bold('PYRE'),
@@ -161,66 +228,100 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
161
228
  timestamp: new Date().toISOString(),
162
229
  packets,
163
230
  tasks,
231
+ containers,
232
+ blenderRenders,
233
+ activeBuilds,
164
234
  };
165
235
  }
166
236
 
237
+ export async function collectContainers(): Promise<ContainerData[]> {
238
+ try {
239
+ const raw = (await run('docker stats --no-stream --format "{{.ID}}|{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}" 2>/dev/null', '')).trim();
240
+ if (!raw) return [];
241
+
242
+ return raw.split('\n').map(line => {
243
+ const parts = line.split('|');
244
+ return {
245
+ id: parts[0],
246
+ name: parts[1],
247
+ cpuPercent: parseFloat(parts[2].replace('%', '')) || 0,
248
+ memUsage: parts[3],
249
+ memPercent: parseFloat(parts[4].replace('%', '')) || 0,
250
+ netIO: parts[5],
251
+ blockIO: parts[6],
252
+ pids: parseInt(parts[7], 10) || 0
253
+ };
254
+ });
255
+ } catch {
256
+ return [];
257
+ }
258
+ }
259
+
167
260
  /**
168
261
  * Read power-draw metrics (CPU watts, GPU watts, combined)
169
262
  * from the SMC via powermetrics. Returns `null` when no
170
263
  * power data is available.
171
264
  */
172
265
  export async function collectPower(): Promise<PowerData | null> {
173
- try {
174
- const { power } = await getSmcMetrics();
175
- if (power.cpu !== undefined || power.gpu !== undefined || power.combined !== undefined) {
176
- return { cpuWatts: power.cpu, gpuWatts: power.gpu, aneWatts: 0, combinedWatts: power.combined };
177
- }
178
- } catch {
179
- // ignore
180
- }
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
+ }
181
276
 
182
- // `pmset -g batt` never actually prints a "mW" figure on any macOS
183
- // version — that regex could never match, so this fallback was dead
184
- // code and collectPower() silently returned null whenever powermetrics
185
- // wasn't available (which is most machines without passwordless sudo
186
- // configured). ioreg's AppleSmartBattery entry exposes instantaneous
187
- // amperage (mA, signed: negative while discharging) and voltage (mV)
188
- // without any privileges, so we can derive real wattage from that.
189
- try {
190
- const ioreg = (await run('ioreg -rn AppleSmartBattery 2>/dev/null')).trim();
191
- const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
192
- const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
193
- if (ampMatch && voltMatch) {
194
- // Two representations show up in the wild: Intel Macs often print a
195
- // literal minus sign ("-1200") — no unwrap needed. Apple Silicon
196
- // instead wraps a negative reading into an *unsigned 64-bit* field
197
- // (e.g. real -1200mA prints as 18446744073709550416). The previous
198
- // 32-bit unwrap (`- 0x100000000`) didn't touch that at all, and
199
- // `parseInt` can't even represent a number that large precisely, so
200
- // it silently mangled into a huge garbage value → absurd wattage.
201
- // Parse as BigInt and only unwrap when there's no explicit sign.
202
- let ampsBig = BigInt(ampMatch[1]);
203
- if (!ampMatch[1].startsWith('-')) {
204
- const UINT64_MAX_PLUS_1 = 1n << 64n;
205
- const INT64_MAX = (1n << 63n) - 1n;
206
- if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
207
- }
208
- const amps = Number(ampsBig);
209
- const volts = parseInt(voltMatch[1], 10) / 1000;
210
- const watts = Math.abs((amps / 1000) * volts);
211
- // Sanity clamp — a real Mac never draws anywhere near this much;
212
- // treat an out-of-range reading as "no usable data" rather than
213
- // display nonsense.
214
- if (Number.isFinite(watts) && watts < 500) {
215
- return { combinedWatts: Math.round(watts * 100) / 100 };
216
- }
217
- }
218
- } catch {
219
- // ignore
220
- }
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
+ }
221
297
 
222
- return null;
223
- }
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
+ }
224
325
 
225
326
  /**
226
327
  * Gather GPU information from system_profiler and powermetrics.
@@ -232,6 +333,8 @@ export async function collectPower(): Promise<PowerData | null> {
232
333
  * Returns null when GPU hardware is unavailable.
233
334
  */
234
335
  export async function collectGpu(detailed?: boolean): Promise<GpuData | null> {
336
+ if (process.platform === 'linux') return collectLinuxGpu();
337
+ if (process.platform === 'win32') return collectWindowsGpu();
235
338
  let spRaw = '';
236
339
  const now = Date.now();
237
340
  if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
@@ -370,7 +473,9 @@ export async function collectPower(): Promise<PowerData | null> {
370
473
  return { model, memory, utilization, temperature, processes: gpuProcesses };
371
474
  }
372
475
 
373
- 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();
374
479
  const hostname = (await run('hostname', 'Mac')).trim();
375
480
  const osRaw = (await run('sw_vers -productVersion', 'Unknown')).trim();
376
481
  const uptimeRaw = (await run('uptime')).trim();
@@ -383,6 +488,17 @@ async function collectSystem(): Promise<{ hostname: string; os: string; uptime:
383
488
  }
384
489
 
385
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
+ }
386
502
  const brand = (await cachedSysctl('machdep.cpu.brand_string', 'Unknown CPU')).trim();
387
503
  const cores = parseInt(await cachedSysctl('hw.ncpu', '1')) || 1;
388
504
  const physicalCores = parseInt(await cachedSysctl('hw.physicalcpu', String(cores))) || cores;
@@ -502,6 +618,8 @@ export async function collectCpu(): Promise<CpuData> {
502
618
  }
503
619
 
504
620
  export async function collectMemory(): Promise<MemoryData> {
621
+ if (process.platform === 'linux') return collectLinuxMemory();
622
+ if (process.platform === 'win32') return collectWindowsMemory();
505
623
  const totalBytes = parseInt(await cachedSysctl('hw.memsize', '0')) || 0;
506
624
  const pageSize = parseInt(await cachedSysctl('hw.pagesize', '4096')) || 4096;
507
625
 
@@ -576,7 +694,7 @@ export async function collectMemory(): Promise<MemoryData> {
576
694
  };
577
695
  }
578
696
 
579
- let prevDiskIoSample: { ts: number } | null = null;
697
+ let prevDiskIoSample: { totalBytes: number; ts: number } | null = null;
580
698
  let cachedDiskIo: { readBytesSec: number; writeBytesSec: number; ts: number } | null = null;
581
699
 
582
700
  async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec: number }> {
@@ -585,22 +703,23 @@ async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec:
585
703
  return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
586
704
  }
587
705
  try {
588
- const raw = (await run('iostat -d -c 2 1 2', '', 3000)).trim();
589
- const blocks = raw.split(/\n\s*\n/).filter(Boolean);
590
- const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
591
- if (targetBlock) {
592
- const lines = targetBlock.trim().split('\n');
593
- if (lines.length >= 2) {
594
- const dataLine = lines[lines.length - 1].trim();
595
- const parts = dataLine.split(/\s+/);
596
- if (parts.length >= 3) {
597
- const mbSec = parseFloat(parts[2]);
598
- if (!isNaN(mbSec)) {
599
- const bytesSec = Math.round(mbSec * 1024 * 1024);
600
- // Splitting combined throughput evenly/estimating if read/write distinction unavailable directly from summary iostat
601
- cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
602
- 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);
603
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 };
604
723
  }
605
724
  }
606
725
  }
@@ -611,6 +730,8 @@ async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec:
611
730
  }
612
731
 
613
732
  export async function collectDisk(): Promise<DiskData[]> {
733
+ if (process.platform === 'linux') return collectLinuxDisk();
734
+ if (process.platform === 'win32') return collectWindowsDisk();
614
735
  const [rawDf, ioRates] = await Promise.all([
615
736
  run('df -h'),
616
737
  getDiskIoRates(),
@@ -634,6 +755,8 @@ export async function collectDisk(): Promise<DiskData[]> {
634
755
  }
635
756
 
636
757
  export async function collectBattery(): Promise<BatteryData | null> {
758
+ if (process.platform === 'linux') return collectLinuxBattery();
759
+ if (process.platform === 'win32') return collectWindowsBattery();
637
760
  try {
638
761
  const raw = (await run('pmset -g batt')).trim();
639
762
  if (!raw.includes('Battery Power') && !raw.includes('AC Power')) return null;
@@ -703,6 +826,31 @@ export async function collectBattery(): Promise<BatteryData | null> {
703
826
 
704
827
  const estimate = estimateBatteryLife(level, state);
705
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
+
706
854
  return {
707
855
  level,
708
856
  state,
@@ -713,6 +861,7 @@ export async function collectBattery(): Promise<BatteryData | null> {
713
861
  condition,
714
862
  maxCapacityPercent,
715
863
  friendlySummary,
864
+ powerWatts,
716
865
  ...estimate,
717
866
  };
718
867
  } catch {
@@ -729,6 +878,8 @@ function pressureFromState(state: string): number {
729
878
  }
730
879
 
731
880
  export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
881
+ if (process.platform === 'linux') return collectLinuxThermal();
882
+ if (process.platform === 'win32') return collectWindowsThermal();
732
883
  try {
733
884
  const therm = (await run('pmset -g therm', '')).trim();
734
885
 
@@ -797,11 +948,21 @@ export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
797
948
  detail = `${state} (estimated from ${cpuDieTemp}°C)`;
798
949
  }
799
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
+
800
960
  return {
801
961
  state,
802
962
  detail,
803
963
  pressureLevel: pressureFromState(state),
804
964
  temperatures: Object.keys(temperatures).length ? temperatures : undefined,
965
+ fans: fans.length > 0 ? fans : undefined,
805
966
  };
806
967
  } catch {
807
968
  return { state: 'Unknown', pressureLevel: 0, error: 'Thermal info unavailable on this system' };
@@ -809,6 +970,8 @@ export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
809
970
  }
810
971
 
811
972
  export async function collectNetwork(): Promise<NetworkData> {
973
+ if (process.platform === 'linux') return collectLinuxNetwork();
974
+ if (process.platform === 'win32') return collectWindowsNetwork();
812
975
  try {
813
976
  const { iface, ip } = await cachedRouteIface();
814
977
  const netStat = await cachedNetstatIb();
@@ -829,13 +992,172 @@ export async function collectNetwork(): Promise<NetworkData> {
829
992
  }
830
993
  }
831
994
 
832
- return { interface: iface, ip, rxBytes, txBytes, rxPackets, txPackets };
995
+ const now = Date.now();
996
+ let rxRate = 0;
997
+ let txRate = 0;
998
+ if (prevNetSample && prevNetSample.ts > 0) {
999
+ const dt = Math.max((now - prevNetSample.ts) / 1000, 0.001);
1000
+ rxRate = Math.max(0, (rxBytes - prevNetSample.rxBytes) / dt);
1001
+ txRate = Math.max(0, (txBytes - prevNetSample.txBytes) / dt);
1002
+ }
1003
+ prevNetSample = { rxBytes, txBytes, rxPackets, txPackets, ts: now };
1004
+
1005
+ const protocolStats = await getProtocolStats();
1006
+ const connectionStates = await getConnectionStates();
1007
+ const listeningPorts = await getListeningPorts();
1008
+ const establishedConnections = connectionStates.established;
1009
+ const topRemoteHosts = await getTopRemoteHosts();
1010
+
1011
+ return {
1012
+ interface: iface,
1013
+ ip,
1014
+ rxBytes,
1015
+ txBytes,
1016
+ rxPackets,
1017
+ txPackets,
1018
+ rxRate,
1019
+ txRate,
1020
+ connections: establishedConnections,
1021
+ protocols: protocolStats,
1022
+ connectionStates,
1023
+ listeningPorts,
1024
+ establishedConnections,
1025
+ topRemoteHosts,
1026
+ };
833
1027
  } catch {
834
- return { interface: 'unknown', ip: '0.0.0.0', rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0 };
1028
+ return {
1029
+ interface: 'unknown',
1030
+ ip: '0.0.0.0',
1031
+ rxBytes: 0,
1032
+ txBytes: 0,
1033
+ rxPackets: 0,
1034
+ txPackets: 0,
1035
+ rxRate: 0,
1036
+ txRate: 0,
1037
+ connections: 0,
1038
+ protocols: { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } },
1039
+ connectionStates: { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 },
1040
+ listeningPorts: [],
1041
+ establishedConnections: 0,
1042
+ topRemoteHosts: [],
1043
+ };
1044
+ }
1045
+ }
1046
+
1047
+ async function getProtocolStats(): Promise<ProtocolStats> {
1048
+ try {
1049
+ const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $1}' | sort | uniq -c", '');
1050
+ const tcpConnections = (await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | wc -l", '0')).trim();
1051
+ const udpConnections = (await run("netstat -n -p udp 2>/dev/null | tail -n +3 | wc -l", '0')).trim();
1052
+
1053
+ let tcpRxBytes = 0;
1054
+ let tcpTxBytes = 0;
1055
+ let udpRxBytes = 0;
1056
+ let udpTxBytes = 0;
1057
+
1058
+ try {
1059
+ const netstat = await cachedNetstatIb();
1060
+ for (const line of netstat.split('\n')) {
1061
+ const parts = line.split(/\s+/);
1062
+ if (parts.length >= 9 && parts[0] && !parts[0].startsWith('Name')) {
1063
+ const iface = parts[0];
1064
+ if (iface.includes('lo')) continue;
1065
+ const rx = parseInt(parts[5]) || 0;
1066
+ const tx = parseInt(parts[7]) || 0;
1067
+ tcpRxBytes += rx;
1068
+ tcpTxBytes += tx;
1069
+ }
1070
+ }
1071
+ } catch {
1072
+ // ignore
1073
+ }
1074
+
1075
+ return {
1076
+ tcp: {
1077
+ connections: parseInt(tcpConnections) || 0,
1078
+ rxBytes: tcpRxBytes,
1079
+ txBytes: tcpTxBytes,
1080
+ },
1081
+ udp: {
1082
+ connections: parseInt(udpConnections) || 0,
1083
+ rxBytes: udpRxBytes,
1084
+ txBytes: udpTxBytes,
1085
+ },
1086
+ };
1087
+ } catch {
1088
+ return { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } };
1089
+ }
1090
+ }
1091
+
1092
+ async function getConnectionStates(): Promise<ConnectionStateStats> {
1093
+ try {
1094
+ const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $6}' | sort | uniq -c", '');
1095
+ const lines = raw.split('\n').filter(Boolean);
1096
+ const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
1097
+
1098
+ for (const line of lines) {
1099
+ const match = line.match(/(\d+)\s+(\S+)/);
1100
+ if (!match) continue;
1101
+ const count = parseInt(match[1]);
1102
+ const state = match[2].toUpperCase();
1103
+
1104
+ if (state.includes('ESTABLISHED')) stats.established += count;
1105
+ else if (state.includes('LISTEN')) stats.listening += count;
1106
+ else if (state.includes('TIME_WAIT')) stats.timeWait += count;
1107
+ else if (state.includes('CLOSE_WAIT')) stats.closeWait += count;
1108
+ else stats.other += count;
1109
+ }
1110
+
1111
+ return stats;
1112
+ } catch {
1113
+ return { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
1114
+ }
1115
+ }
1116
+
1117
+ async function getListeningPorts(): Promise<number[]> {
1118
+ try {
1119
+ const raw = await run("netstat -n -p tcp 2>/dev/null | grep LISTEN | awk '{print $4}' | rev | cut -d. -f1 | rev", '');
1120
+ const ports = raw.split('\n')
1121
+ .map(p => parseInt(p, 10))
1122
+ .filter(p => !isNaN(p) && p > 0);
1123
+ return [...new Set(ports)].sort((a, b) => a - b);
1124
+ } catch {
1125
+ return [];
1126
+ }
1127
+ }
1128
+
1129
+ async function getTopRemoteHosts(): Promise<RemoteHostInfo[]> {
1130
+ try {
1131
+ const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | grep ESTABLISHED | awk '{print $5}' | rev | cut -d. -f1 | rev", '');
1132
+ const ports = raw.split('\n').filter(Boolean);
1133
+ const hostMap = new Map<string, { count: number; bytes: number }>();
1134
+
1135
+ for (const port of ports) {
1136
+ const host = port.split(':')[0];
1137
+ if (!host || host === '127.0.0.1' || host === '::1') continue;
1138
+ const existing = hostMap.get(host) || { count: 0, bytes: 0 };
1139
+ hostMap.set(host, { count: existing.count + 1, bytes: existing.bytes });
1140
+ }
1141
+
1142
+ const result: RemoteHostInfo[] = [];
1143
+ for (const [host, info] of hostMap.entries()) {
1144
+ result.push({
1145
+ host,
1146
+ ip: host,
1147
+ bytesTransferred: info.bytes,
1148
+ connectionCount: info.count,
1149
+ });
1150
+ }
1151
+
1152
+ return result.sort((a, b) => b.connectionCount - a.connectionCount).slice(0, 10);
1153
+ } catch {
1154
+ return [];
835
1155
  }
836
1156
  }
837
1157
 
838
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);
839
1161
  try {
840
1162
  const isMac = process.platform === 'darwin';
841
1163
  const sortArg = isMac ? '-r' : '--sort=-pcpu';
@@ -857,79 +1179,163 @@ export async function collectProcesses(limit?: number): Promise<ProcessData[]> {
857
1179
  }
858
1180
  }
859
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
+
860
1195
  export async function collectPackets(): Promise<PacketData | null> {
861
- try {
862
- const netStat = await cachedNetstatIb();
863
- const lines = netStat.split('\n').slice(1);
864
- let totalRxPackets = 0;
865
- let totalTxPackets = 0;
866
- let totalRxBytes = 0;
867
- let totalTxBytes = 0;
868
- const ifaceStats: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[] = [];
869
-
870
- for (const line of lines) {
871
- const parts = line.split(/\s+/);
872
- if (parts.length >= 11 && parts[0] && !parts[0].startsWith('Name')) {
873
- const iface = parts[0];
874
- const rxPackets = parseInt(parts[4]) || 0;
875
- const rxBytes = parseInt(parts[5]) || 0;
876
- const txPackets = parseInt(parts[6]) || 0;
877
- const txBytes = parseInt(parts[7]) || 0;
878
- totalRxPackets += rxPackets;
879
- totalTxPackets += txPackets;
880
- totalRxBytes += rxBytes;
881
- totalTxBytes += txBytes;
882
- if (iface && !iface.includes('lo')) {
883
- ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
884
- }
885
- }
886
- }
887
-
888
- let connections = 0;
889
- try {
890
- const tcpConns = (await run('netstat -an | grep ESTABLISHED | wc -l', '0', 2000)).trim();
891
- connections = parseInt(tcpConns) || 0;
892
- } catch {
893
- connections = 0;
894
- }
895
-
896
- let topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[] = [];
897
- let allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[] = [];
898
- try {
899
- const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '', 2000)).trim();
900
- const lsofLines = lsof.split('\n');
901
- topProcesses = lsofLines.map(line => {
902
- const parts = line.trim().split(/\s+/);
903
- if (parts.length >= 3) {
904
- return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
905
- }
906
- return { pid: 0, command: '', rxBytes: 0, txBytes: 0 };
907
- }).filter(p => p.pid > 0);
908
-
909
- allProcesses = topProcesses;
910
- } catch {
911
- topProcesses = [];
912
- allProcesses = [];
913
- }
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 }[] = [];
914
1206
 
915
- return {
916
- totalPackets: totalRxPackets + totalTxPackets,
917
- rxPackets: totalRxPackets,
918
- txPackets: totalTxPackets,
919
- rxRate: 0,
920
- txRate: 0,
921
- connections,
922
- topProcesses,
923
- interfaces: ifaceStats,
924
- allProcesses,
925
- };
926
- } catch {
927
- return null;
928
- }
929
- }
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
+ }
1308
+
1309
+ // Sort processes by total bandwidth (tx + rx) to get top processes
1310
+ const topProcesses = [...allProcesses].sort((a, b) => (b.rxBytes + b.txBytes) - (a.rxBytes + a.txBytes));
1311
+
1312
+ const protocolStats = await getProtocolStats();
1313
+ const connectionStates = await getConnectionStates();
1314
+ const listeningPorts = await getListeningPorts();
1315
+
1316
+ return {
1317
+ totalPackets: totalRxPackets + totalTxPackets,
1318
+ rxPackets: totalRxPackets,
1319
+ txPackets: totalTxPackets,
1320
+ rxRate: 0,
1321
+ txRate: 0,
1322
+ connections,
1323
+ topProcesses,
1324
+ interfaces: ifaceStats,
1325
+ allProcesses,
1326
+ protocolStats,
1327
+ connectionStates,
1328
+ listeningPorts,
1329
+ };
1330
+ } catch {
1331
+ return null;
1332
+ }
1333
+ }
930
1334
 
931
1335
  export async function collectTasks(limit = 12): Promise<TaskData[]> {
932
- try {
1336
+ if (process.platform === 'linux') return collectLinuxTasks(limit);
1337
+ if (process.platform === 'win32') return collectWindowsTasks(limit);
1338
+ try {
933
1339
  const isMac = process.platform === 'darwin';
934
1340
  const sortArg = isMac ? '-r' : '--sort=-pcpu';
935
1341
  const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
@@ -955,6 +1361,8 @@ export interface DisplayInfo {
955
1361
  }
956
1362
 
957
1363
  export async function getDisplayInfo(): Promise<DisplayInfo[]> {
1364
+ if (process.platform === 'linux') return getLinuxDisplayInfo();
1365
+ if (process.platform === 'win32') return getWindowsDisplayInfo();
958
1366
  try {
959
1367
  const raw = await run('system_profiler SPDisplaysDataType 2>&1', '');
960
1368
  const displays: DisplayInfo[] = [];
@@ -999,6 +1407,9 @@ export interface TimeMachineStatus {
999
1407
  }
1000
1408
 
1001
1409
  export async function getTimeMachineStatus(): Promise<TimeMachineStatus> {
1410
+ if (process.platform !== 'darwin') {
1411
+ return { configured: false, running: false };
1412
+ }
1002
1413
  try {
1003
1414
  const statusRaw = await run('tmutil status 2>&1', '');
1004
1415
  if (statusRaw.includes('No Time Machine destination') || statusRaw.includes('not configured')) {