pyre-cli 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1267 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs2 from "fs";
5
+ import { Command } from "commander";
6
+ import chalk4 from "chalk";
7
+
8
+ // src/monitors.ts
9
+ import chalk from "chalk";
10
+ async function run(cmd, fallback = "") {
11
+ try {
12
+ const { execSync } = await import("child_process");
13
+ return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }).trim();
14
+ } catch {
15
+ return fallback;
16
+ }
17
+ }
18
+ var smcCache = null;
19
+ async function getSmcMetrics() {
20
+ const now = Date.now();
21
+ if (smcCache && now - smcCache.ts < 1500) return smcCache.data;
22
+ const result = { temps: {}, power: {}, freq: {} };
23
+ try {
24
+ const pm = (await run(
25
+ "sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null",
26
+ ""
27
+ )).trim();
28
+ if (pm) {
29
+ const cpuTemp = pm.match(/CPU die temperature:\s*([\d.]+)/i);
30
+ if (cpuTemp) result.temps["cpu_die"] = parseFloat(cpuTemp[1]);
31
+ const gpuTemp = pm.match(/GPU die temperature:\s*([\d.]+)/i);
32
+ if (gpuTemp) result.temps["gpu_die"] = parseFloat(gpuTemp[1]);
33
+ const smcTemp = pm.match(/SMC die temperature:\s*([\d.]+)/i);
34
+ if (smcTemp) result.temps["smc_die"] = parseFloat(smcTemp[1]);
35
+ const cpuPower = pm.match(/CPU Power:\s*([\d.]+)\s*mW/i);
36
+ if (cpuPower) result.power.cpu = parseFloat(cpuPower[1]) / 1e3;
37
+ const gpuPower = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
38
+ if (gpuPower) result.power.gpu = parseFloat(gpuPower[1]) / 1e3;
39
+ const combinedPower = pm.match(/Combined Power \(CPU \+ GPU.*?\):\s*([\d.]+)\s*mW/i);
40
+ if (combinedPower) result.power.combined = parseFloat(combinedPower[1]) / 1e3;
41
+ const eFreq = pm.match(/E-Cluster HW active frequency:\s*([\d.]+)/i);
42
+ if (eFreq) result.freq["e_cluster"] = parseFloat(eFreq[1]);
43
+ const pFreq = pm.match(/P-Cluster HW active frequency:\s*([\d.]+)/i);
44
+ if (pFreq) result.freq["p_cluster"] = parseFloat(pFreq[1]);
45
+ const cpuFreq = pm.match(/CPU HW active frequency:\s*([\d.]+)/i);
46
+ if (cpuFreq) result.freq["cpu"] = parseFloat(cpuFreq[1]);
47
+ }
48
+ } catch {
49
+ }
50
+ smcCache = { data: result, ts: now };
51
+ return result;
52
+ }
53
+ function parseSuffix(s) {
54
+ return s.toUpperCase() === "K" ? 1024 : s.toUpperCase() === "M" ? 1024 * 1024 : s.toUpperCase() === "G" ? 1024 * 1024 * 1024 : 1;
55
+ }
56
+ async function collectAll(opts2 = {}) {
57
+ const [cpu, memory, disk, battery, thermal, network, processes, system, power] = await Promise.all([
58
+ collectCpu(),
59
+ collectMemory(),
60
+ collectDisk(),
61
+ collectBattery(),
62
+ collectThermal(opts2.detailed),
63
+ collectNetwork(),
64
+ collectProcesses(opts2.processLimit ?? 10),
65
+ collectSystem(),
66
+ collectPower()
67
+ ]);
68
+ return {
69
+ header: {
70
+ title: chalk.hex("#ff5500").bold("PYRE"),
71
+ hostname: system.hostname,
72
+ os: system.os,
73
+ uptime: system.uptime
74
+ },
75
+ cpu,
76
+ memory,
77
+ disk,
78
+ battery,
79
+ thermal,
80
+ network,
81
+ processes,
82
+ power,
83
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
84
+ };
85
+ }
86
+ async function collectPower() {
87
+ try {
88
+ const { power } = await getSmcMetrics();
89
+ if (power.cpu === void 0 && power.gpu === void 0 && power.combined === void 0) return null;
90
+ return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ async function collectSystem() {
96
+ const hostname = (await run("hostname", "Mac")).trim();
97
+ const osRaw = (await run("sw_vers -productVersion", "Unknown")).trim();
98
+ const uptimeRaw = (await run("uptime")).trim();
99
+ const uptimeMatch = uptimeRaw.match(/up\s+(.*?),\s+\d+\s+users/);
100
+ return {
101
+ hostname,
102
+ os: `macOS ${osRaw}`,
103
+ uptime: uptimeMatch ? uptimeMatch[1] : uptimeRaw
104
+ };
105
+ }
106
+ async function collectCpu() {
107
+ const brand = (await run("sysctl -n machdep.cpu.brand_string", "Unknown CPU")).trim();
108
+ const cores = parseInt((await run("sysctl -n hw.ncpu")).trim()) || 1;
109
+ const physicalCores = parseInt((await run("sysctl -n hw.physicalcpu")).trim()) || cores;
110
+ const frequencyHz = parseInt((await run("sysctl -n hw.cpufrequency")).trim());
111
+ let frequency = frequencyHz > 0 ? Math.round(frequencyHz / 1e6) : 0;
112
+ if (frequency === 0) {
113
+ try {
114
+ const { freq } = await getSmcMetrics();
115
+ const values = Object.values(freq);
116
+ if (values.length) frequency = Math.round(Math.max(...values));
117
+ } catch {
118
+ }
119
+ }
120
+ let usage = 0;
121
+ try {
122
+ const top = (await run("top -l 1 -n 0")).trim();
123
+ const cpuLine = top.match(/CPU usage:\s*[\d.]+%\s*user,\s*[\d.]+%\s*sys,\s*([\d.]+)%\s*idle/);
124
+ if (cpuLine) {
125
+ const idle = parseFloat(cpuLine[1]);
126
+ usage = Math.round(100 - idle);
127
+ }
128
+ } catch {
129
+ }
130
+ const loadAvgStr = (await run("sysctl -n vm.loadavg", "0.00 0.00 0.00")).trim();
131
+ const loadAvg = loadAvgStr.match(/\{ (.+?) \}/)?.[1].split(" ").map(Number) || [0, 0, 0];
132
+ let temperature;
133
+ try {
134
+ const { temps } = await getSmcMetrics();
135
+ if (temps.cpu_die !== void 0) temperature = temps.cpu_die;
136
+ } catch {
137
+ }
138
+ return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature };
139
+ }
140
+ async function collectMemory() {
141
+ const totalBytes = parseInt((await run("sysctl -n hw.memsize")).trim()) || 0;
142
+ const pageSize = parseInt((await run("sysctl -n hw.pagesize", "4096")).trim()) || 4096;
143
+ const vmStat = (await run("vm_stat")).trim();
144
+ const getPageCount = (label) => {
145
+ const m = vmStat.match(new RegExp(`Pages\\s+${label}:\\s+(\\d+)\\.`));
146
+ return m ? parseInt(m[1]) : 0;
147
+ };
148
+ const freePages = getPageCount("free");
149
+ const activePages = getPageCount("active");
150
+ const inactivePages = getPageCount("inactive");
151
+ const wiredPages = getPageCount("wired down");
152
+ const compressedPages = getPageCount("occupied by compressor");
153
+ const free = freePages * pageSize;
154
+ const used = (activePages + inactivePages + wiredPages + compressedPages) * pageSize;
155
+ const usagePercent = totalBytes > 0 ? Math.round(used / totalBytes * 100) : 0;
156
+ let swapTotal = 0;
157
+ let swapUsed = 0;
158
+ let swapFree = 0;
159
+ try {
160
+ const swapRaw = (await run("sysctl -n vm.swapusage")).trim();
161
+ const swapMatch = swapRaw.match(
162
+ /total\s*=\s*([\d.]+)([KMGkmg])\s+used\s*=\s*([\d.]+)([KMGkmg])\s+free\s*=\s*([\d.]+)([KMGkmg])/
163
+ );
164
+ if (swapMatch) {
165
+ swapTotal = parseFloat(swapMatch[1]) * parseSuffix(swapMatch[2]);
166
+ swapUsed = parseFloat(swapMatch[3]) * parseSuffix(swapMatch[4]);
167
+ swapFree = parseFloat(swapMatch[5]) * parseSuffix(swapMatch[6]);
168
+ }
169
+ } catch {
170
+ }
171
+ return {
172
+ total: totalBytes,
173
+ used,
174
+ free,
175
+ swapTotal,
176
+ swapUsed,
177
+ swapFree,
178
+ pageSize,
179
+ usagePercent
180
+ };
181
+ }
182
+ async function collectDisk() {
183
+ const raw = (await run("df -h")).trim();
184
+ const lines = raw.split("\n").slice(1);
185
+ return lines.map((line) => {
186
+ const parts = line.split(/\s+/);
187
+ return {
188
+ filesystem: parts[0],
189
+ size: parts[1],
190
+ used: parts[2],
191
+ available: parts[3],
192
+ capacity: parts[4],
193
+ mountpoint: parts[5] || parts[6] || "/"
194
+ };
195
+ }).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
196
+ }
197
+ async function collectBattery() {
198
+ try {
199
+ const raw = (await run("pmset -g batt")).trim();
200
+ if (!raw.includes("Battery Power") && !raw.includes("AC Power")) return null;
201
+ const levelMatch = raw.match(/(\d+)%/);
202
+ const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
203
+ const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i);
204
+ const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
205
+ const level = levelMatch ? parseInt(levelMatch[1]) : 0;
206
+ const state = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
207
+ const timeRemaining = timeMatch ? timeMatch[1] : powerPlugged ? "\u221E" : "calculating";
208
+ let cycles;
209
+ let condition;
210
+ let maxCapacityPercent;
211
+ try {
212
+ const battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null")).trim();
213
+ const cycleMatch = battInfo.match(/Cycle Count:\s+(\d+)/);
214
+ if (cycleMatch) cycles = parseInt(cycleMatch[1]);
215
+ const conditionMatch = battInfo.match(/Condition:\s+(.+)/);
216
+ if (conditionMatch) condition = conditionMatch[1].trim();
217
+ const maxCapMatch = battInfo.match(/Maximum Capacity:\s+(\d+)%/);
218
+ if (maxCapMatch) {
219
+ maxCapacityPercent = parseInt(maxCapMatch[1]);
220
+ } else {
221
+ const fullMatch = battInfo.match(/Full Charge Capacity[^\d]*(\d+)/i);
222
+ const designMatch = battInfo.match(/Design Capacity[^\d]*(\d+)/i);
223
+ if (fullMatch && designMatch) {
224
+ const full = parseInt(fullMatch[1]);
225
+ const design = parseInt(designMatch[1]);
226
+ if (design > 0) maxCapacityPercent = Math.round(full / design * 100);
227
+ }
228
+ }
229
+ } catch {
230
+ }
231
+ const healthParts = [];
232
+ if (condition) healthParts.push(condition);
233
+ if (maxCapacityPercent !== void 0) healthParts.push(`${maxCapacityPercent}% capacity`);
234
+ if (cycles !== void 0) healthParts.push(`${cycles} cycles`);
235
+ const health = healthParts.length ? healthParts.join(", ") : "unknown";
236
+ return {
237
+ level,
238
+ state,
239
+ timeRemaining,
240
+ health,
241
+ powerSource: powerPlugged ? "AC" : "Battery",
242
+ cycles,
243
+ condition,
244
+ maxCapacityPercent
245
+ };
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ function pressureFromState(state) {
251
+ const s = state.toLowerCase();
252
+ if (s.includes("critical")) return 3;
253
+ if (s.includes("serious") || s.includes("heavy")) return 2;
254
+ if (s.includes("fair") || s.includes("moderate")) return 1;
255
+ return 0;
256
+ }
257
+ async function collectThermal(detailed2) {
258
+ try {
259
+ const therm = (await run("pmset -g therm", "")).trim();
260
+ let state = "Unknown";
261
+ let detail = therm || "No pmset thermal data";
262
+ const stateMatch = therm.match(/Thermal state:\s*(.+)/i);
263
+ if (stateMatch) {
264
+ const raw = stateMatch[1].trim();
265
+ state = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
266
+ detail = state;
267
+ } else {
268
+ const limitMatch = therm.match(
269
+ /CPU_(?:Speed|Scheduler)_Limit\s*(?:=|:)\s*([\d.]+)/i
270
+ );
271
+ if (limitMatch) {
272
+ const limit = parseFloat(limitMatch[1]);
273
+ if (limit >= 100) state = "Nominal";
274
+ else if (limit >= 85) state = "Fair";
275
+ else if (limit >= 50) state = "Serious";
276
+ else state = "Critical";
277
+ detail = `${state} (CPU limit ${limit}%)`;
278
+ } else if (/no thermal warning level has been recorded/i.test(therm)) {
279
+ state = "Nominal";
280
+ detail = "Nominal (no thermal warning recorded)";
281
+ }
282
+ }
283
+ const temperatures = {};
284
+ let cpuDieTemp;
285
+ try {
286
+ const { temps } = await getSmcMetrics();
287
+ cpuDieTemp = temps.cpu_die;
288
+ if (detailed2) {
289
+ if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
290
+ if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
291
+ if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
292
+ if (!Object.keys(temps).length) {
293
+ detail += " (sensor data needs powermetrics privileges)";
294
+ }
295
+ }
296
+ } catch {
297
+ if (detailed2) detail += " (powermetrics unavailable)";
298
+ }
299
+ if (state === "Unknown" && cpuDieTemp !== void 0) {
300
+ if (cpuDieTemp >= 100) state = "Critical";
301
+ else if (cpuDieTemp >= 90) state = "Serious";
302
+ else if (cpuDieTemp >= 80) state = "Fair";
303
+ else state = "Nominal";
304
+ detail = `${state} (estimated from ${cpuDieTemp}\xB0C)`;
305
+ }
306
+ return {
307
+ state,
308
+ detail,
309
+ pressureLevel: pressureFromState(state),
310
+ temperatures: Object.keys(temperatures).length ? temperatures : void 0
311
+ };
312
+ } catch {
313
+ return { state: "Unknown", pressureLevel: 0, error: "Thermal info unavailable on this system" };
314
+ }
315
+ }
316
+ async function collectNetwork() {
317
+ try {
318
+ const primary = (await run("route -n get default")).trim();
319
+ const ifaceMatch = primary.match(/interface:\s+(\S+)/);
320
+ const iface = ifaceMatch?.[1] || "en0";
321
+ let ip = "0.0.0.0";
322
+ try {
323
+ const ifconfig = (await run(`ifconfig ${iface}`)).trim();
324
+ const ipMatch = ifconfig.match(/inet\s+([\d.]+)\s+netmask/);
325
+ if (ipMatch) ip = ipMatch[1];
326
+ } catch {
327
+ }
328
+ const netStat = (await run("netstat -ib")).trim();
329
+ let rxBytes = 0;
330
+ let txBytes = 0;
331
+ let rxPackets = 0;
332
+ let txPackets = 0;
333
+ for (const line of netStat.split("\n")) {
334
+ if (line.startsWith(iface) || line.startsWith(iface + ":")) {
335
+ const parts = line.split(/\s+/);
336
+ if (parts.length >= 9) {
337
+ rxPackets += parseInt(parts[4]) || 0;
338
+ rxBytes += parseInt(parts[5]) || 0;
339
+ txPackets += parseInt(parts[6]) || 0;
340
+ txBytes += parseInt(parts[7]) || 0;
341
+ }
342
+ }
343
+ }
344
+ return { interface: iface, ip, rxBytes, txBytes, rxPackets, txPackets };
345
+ } catch {
346
+ return { interface: "unknown", ip: "0.0.0.0", rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0 };
347
+ }
348
+ }
349
+ async function collectProcesses(limit = 10) {
350
+ try {
351
+ const raw = (await run(`ps -Ao pid,user,pcpu,pmem,comm -r | head -n ${limit + 1}`)).trim();
352
+ const lines = raw.split("\n").slice(1);
353
+ return lines.map((line) => {
354
+ const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(.+)/);
355
+ if (!parts) return null;
356
+ return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), command: parts[5] };
357
+ }).filter((p) => p !== null && p.pid > 0);
358
+ } catch {
359
+ return [];
360
+ }
361
+ }
362
+
363
+ // src/formatters.ts
364
+ import chalk2 from "chalk";
365
+
366
+ // src/sparkline.ts
367
+ var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
368
+ function sparkline(values, opts2 = {}) {
369
+ if (!values.length) return "";
370
+ const min = opts2.min ?? Math.min(...values);
371
+ const max = opts2.max ?? Math.max(...values);
372
+ const range = max - min || 1;
373
+ return values.map((v) => {
374
+ const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
375
+ const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
376
+ return BLOCKS[clamped];
377
+ }).join("");
378
+ }
379
+
380
+ // src/formatters.ts
381
+ var THEMES = {
382
+ default: {
383
+ border: chalk2.hex("#4b5563"),
384
+ cpu: chalk2.hex("#22d3ee"),
385
+ mem: chalk2.hex("#a78bfa"),
386
+ power: chalk2.hex("#fbbf24"),
387
+ battery: chalk2.hex("#34d399"),
388
+ thermal: chalk2.hex("#fb923c"),
389
+ network: chalk2.hex("#60a5fa"),
390
+ disk: chalk2.hex("#f472b6"),
391
+ graphs: chalk2.hex("#22d3ee"),
392
+ process: chalk2.hex("#e5e7eb")
393
+ },
394
+ dracula: {
395
+ border: chalk2.hex("#6272a4"),
396
+ cpu: chalk2.hex("#8be9fd"),
397
+ mem: chalk2.hex("#bd93f9"),
398
+ power: chalk2.hex("#f1fa8c"),
399
+ battery: chalk2.hex("#50fa7b"),
400
+ thermal: chalk2.hex("#ffb86c"),
401
+ network: chalk2.hex("#ff79c6"),
402
+ disk: chalk2.hex("#ff5555"),
403
+ graphs: chalk2.hex("#8be9fd"),
404
+ process: chalk2.hex("#f8f8f2")
405
+ },
406
+ cyberpunk: {
407
+ border: chalk2.hex("#ff0055"),
408
+ cpu: chalk2.hex("#00ffcc"),
409
+ mem: chalk2.hex("#ff00ff"),
410
+ power: chalk2.hex("#ffff00"),
411
+ battery: chalk2.hex("#00ff00"),
412
+ thermal: chalk2.hex("#ff6600"),
413
+ network: chalk2.hex("#00ffff"),
414
+ disk: chalk2.hex("#ff007f"),
415
+ graphs: chalk2.hex("#00ffcc"),
416
+ process: chalk2.hex("#ffffff")
417
+ },
418
+ monochrome: {
419
+ border: chalk2.gray,
420
+ cpu: chalk2.white.bold,
421
+ mem: chalk2.white.bold,
422
+ power: chalk2.white.bold,
423
+ battery: chalk2.white.bold,
424
+ thermal: chalk2.white.bold,
425
+ network: chalk2.white.bold,
426
+ disk: chalk2.white.bold,
427
+ graphs: chalk2.white.bold,
428
+ process: chalk2.white.bold
429
+ }
430
+ };
431
+ function clampWidth(width) {
432
+ if (!width || Number.isNaN(width)) return 80;
433
+ return Math.max(60, Math.min(width, 240));
434
+ }
435
+ function gridColumns(width) {
436
+ if (width >= 150) return 3;
437
+ if (width >= 96) return 2;
438
+ return 1;
439
+ }
440
+ function visLen(s) {
441
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
442
+ }
443
+ function padVisible(s, width) {
444
+ const len = visLen(s);
445
+ if (len >= width) return s;
446
+ return s + " ".repeat(width - len);
447
+ }
448
+ function truncatePlain(s, width) {
449
+ if (s.length <= width) return s;
450
+ return width <= 1 ? s.slice(0, width) : s.slice(0, width - 1) + "\u2026";
451
+ }
452
+ function panel(title, lines, width, accent, borderAccent, height) {
453
+ const h = height ?? lines.length;
454
+ const contentWidth = Math.max(1, width - 4);
455
+ const label = ` ${title} `;
456
+ const dashLen = Math.max(0, width - 3 - label.length);
457
+ const top = borderAccent("\u256D\u2500" + label + "\u2500".repeat(dashLen) + "\u256E");
458
+ const bottom = borderAccent("\u2570" + "\u2500".repeat(width - 2) + "\u256F");
459
+ const body = [];
460
+ for (let i = 0; i < h; i++) {
461
+ const line = lines[i] ?? "";
462
+ body.push(`${borderAccent("\u2502")} ${padVisible(line, contentWidth)} ${borderAccent("\u2502")}`);
463
+ }
464
+ return [top, ...body, bottom];
465
+ }
466
+ function hstack(blocks, gap = 2) {
467
+ if (!blocks.length) return [];
468
+ const height = Math.max(...blocks.map((b) => b.length));
469
+ const widths = blocks.map((b) => visLen(b[0] ?? ""));
470
+ const padded = blocks.map((b, i) => {
471
+ const filler = " ".repeat(widths[i]);
472
+ const arr = [...b];
473
+ while (arr.length < height) arr.push(filler);
474
+ return arr;
475
+ });
476
+ const out = [];
477
+ for (let row = 0; row < height; row++) {
478
+ out.push(padded.map((a) => a[row]).join(" ".repeat(gap)));
479
+ }
480
+ return out;
481
+ }
482
+ function statRow(label, value, width = 9) {
483
+ const w = Math.max(width, label.length + 1);
484
+ return `${chalk2.dim(label.padEnd(w))}${value}`;
485
+ }
486
+ function bar(percent, width = 20) {
487
+ const filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * width);
488
+ const empty = width - filled;
489
+ const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
490
+ const color = percent > 90 ? chalk2.red : percent > 70 ? chalk2.yellow : chalk2.green;
491
+ return color(barStr);
492
+ }
493
+ function gaugeRow(label, percent, contentWidth) {
494
+ const labelW = 9;
495
+ const pctText = `${Math.round(percent)}%`;
496
+ const barW = Math.max(6, contentWidth - labelW - pctText.length - 1);
497
+ return `${chalk2.dim(label.padEnd(labelW))}${bar(percent, barW)} ${pctText}`;
498
+ }
499
+ function thermalColor(pressureLevel) {
500
+ if (pressureLevel >= 3) return chalk2.red;
501
+ if (pressureLevel === 2) return chalk2.yellow;
502
+ if (pressureLevel === 1) return chalk2.yellowBright;
503
+ return chalk2.green;
504
+ }
505
+ function capacityColor(percent) {
506
+ if (percent < 80) return chalk2.red;
507
+ if (percent < 90) return chalk2.yellow;
508
+ return chalk2.green;
509
+ }
510
+ function formatBytes(bytes) {
511
+ if (bytes < 1024) return `${bytes} B`;
512
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
513
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
514
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
515
+ }
516
+ function celsiusToFahrenheit(c) {
517
+ return c * (9 / 5) + 32;
518
+ }
519
+ function formatTemp(c) {
520
+ return `${c.toFixed(1)}\xB0C / ${celsiusToFahrenheit(c).toFixed(1)}\xB0F`;
521
+ }
522
+ function cpuCard(data, contentWidth) {
523
+ const lines = [
524
+ statRow("Brand", truncatePlain(data.cpu.brand, Math.max(4, contentWidth - 8))),
525
+ statRow("Cores", `${data.cpu.physicalCores}/${data.cpu.cores} phys/log`),
526
+ statRow("Freq", `${data.cpu.frequency} MHz`),
527
+ gaugeRow("Usage", data.cpu.usage, contentWidth),
528
+ statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" \xB7 "))
529
+ ];
530
+ if (data.cpu.temperature) lines.push(statRow("Temp", formatTemp(data.cpu.temperature)));
531
+ return lines;
532
+ }
533
+ function memCard(data, contentWidth) {
534
+ return [
535
+ statRow("Total", formatBytes(data.memory.total)),
536
+ statRow("Used", formatBytes(data.memory.used)),
537
+ statRow("Free", formatBytes(data.memory.free)),
538
+ gaugeRow("Usage", data.memory.usagePercent, contentWidth),
539
+ statRow("Swap", `${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}`)
540
+ ];
541
+ }
542
+ function powerCard(data) {
543
+ if (!data.power) return null;
544
+ const lines = [];
545
+ if (data.power.cpuWatts !== void 0) lines.push(statRow("CPU", `${data.power.cpuWatts.toFixed(2)} W`));
546
+ if (data.power.gpuWatts !== void 0) lines.push(statRow("GPU", `${data.power.gpuWatts.toFixed(2)} W`));
547
+ if (data.power.combinedWatts !== void 0) lines.push(statRow("Total", `${data.power.combinedWatts.toFixed(2)} W`));
548
+ return lines.length ? lines : null;
549
+ }
550
+ function batteryCard(data, contentWidth) {
551
+ if (!data.battery) return null;
552
+ const valW = Math.max(6, contentWidth - 10);
553
+ const lines = [
554
+ statRow("Level", `${data.battery.level}% (${data.battery.state})`),
555
+ statRow("Time", data.battery.timeRemaining),
556
+ statRow("Health", truncatePlain(data.battery.health, valW))
557
+ ];
558
+ if (data.battery.maxCapacityPercent !== void 0) {
559
+ lines.push(statRow("Capacity", capacityColor(data.battery.maxCapacityPercent)(`${data.battery.maxCapacityPercent}%`)));
560
+ }
561
+ lines.push(statRow("Source", data.battery.powerSource));
562
+ return lines;
563
+ }
564
+ function thermalCard(data, contentWidth) {
565
+ const valW = Math.max(6, contentWidth - 10);
566
+ const lines = [statRow("State", thermalColor(data.thermal.pressureLevel)(truncatePlain(data.thermal.state, valW)))];
567
+ if (data.thermal.temperatures) {
568
+ for (const [k, v] of Object.entries(data.thermal.temperatures)) {
569
+ lines.push(statRow(k.replace("_", " "), v !== null && v !== void 0 ? formatTemp(v) : "N/A"));
570
+ }
571
+ }
572
+ if (data.thermal.error) lines.push(statRow("Error", truncatePlain(data.thermal.error, valW)));
573
+ return lines;
574
+ }
575
+ function networkCard(data, contentWidth) {
576
+ const valW = Math.max(6, contentWidth - 8);
577
+ return [
578
+ statRow("Iface", truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)),
579
+ statRow("RX", `${formatBytes(data.network.rxBytes)} \xB7 ${data.network.rxPackets} pkts`),
580
+ statRow("TX", `${formatBytes(data.network.txBytes)} \xB7 ${data.network.txPackets} pkts`)
581
+ ];
582
+ }
583
+ function header(data, width, badges = [], borderAccent = THEMES.default.border) {
584
+ const left = `${chalk2.hex("#ff5500").bold(" PYRE ")} ${chalk2.bold(data.header.hostname)} ${chalk2.dim(data.header.os)} ${chalk2.dim("up " + data.header.uptime)}`;
585
+ const right = badges.join(" ");
586
+ const gap = Math.max(1, width - visLen(left) - visLen(right));
587
+ const line = left + " ".repeat(gap) + right;
588
+ return [line, borderAccent("\u2500".repeat(width))];
589
+ }
590
+ function sortProcesses(processes, sortBy) {
591
+ const copy = [...processes];
592
+ if (sortBy === "pid") return copy.sort((a, b) => a.pid - b.pid);
593
+ if (sortBy === "mem") return copy.sort((a, b) => b.mem - a.mem);
594
+ return copy.sort((a, b) => b.cpu - a.cpu);
595
+ }
596
+ function pctColor(v) {
597
+ return v > 80 ? chalk2.red : v > 50 ? chalk2.yellow : chalk2.dim;
598
+ }
599
+ function processTableLines(processes, contentWidth, borderAccent = THEMES.default.border) {
600
+ const pidW = 7, userW = 10, cpuW = 7, memW = 7;
601
+ const cmdW = Math.max(10, contentWidth - pidW - userW - cpuW - memW - 4);
602
+ const head = chalk2.bold.dim("PID".padEnd(pidW)) + chalk2.bold.dim("USER".padEnd(userW)) + chalk2.bold.dim("CPU%".padEnd(cpuW)) + chalk2.bold.dim("MEM%".padEnd(memW)) + chalk2.bold.dim("COMMAND");
603
+ const rows = processes.map((p) => {
604
+ const cpuStr = pctColor(p.cpu)(`${p.cpu.toFixed(1)}`.padEnd(cpuW));
605
+ const memStr = pctColor(p.mem)(`${p.mem.toFixed(1)}`.padEnd(memW));
606
+ return String(p.pid).padEnd(pidW) + truncatePlain(p.user, userW - 1).padEnd(userW) + cpuStr + memStr + truncatePlain(p.command, cmdW);
607
+ });
608
+ return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
609
+ }
610
+ function diskTableLines(disks, contentWidth, borderAccent = THEMES.default.border) {
611
+ const fsW = 16, sizeW = 8, usedW = 8, availW = 8, capW = 7;
612
+ const mountW = Math.max(8, contentWidth - fsW - sizeW - usedW - availW - capW - 5);
613
+ const head = chalk2.bold.dim("FILESYSTEM".padEnd(fsW)) + chalk2.bold.dim("SIZE".padEnd(sizeW)) + chalk2.bold.dim("USED".padEnd(usedW)) + chalk2.bold.dim("AVAIL".padEnd(availW)) + chalk2.bold.dim("CAP".padEnd(capW)) + chalk2.bold.dim("MOUNT");
614
+ const rows = disks.slice(0, 8).map((d) => {
615
+ const capNum = parseInt(d.capacity, 10) || 0;
616
+ const capStr = pctColor(capNum)(d.capacity.padEnd(capW));
617
+ return truncatePlain(d.filesystem, fsW - 1).padEnd(fsW) + d.size.padEnd(sizeW) + d.used.padEnd(usedW) + d.available.padEnd(availW) + capStr + truncatePlain(d.mountpoint, mountW);
618
+ });
619
+ return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
620
+ }
621
+ function formatTable(data, opts2 = {}) {
622
+ const width = clampWidth(opts2.width);
623
+ const columns = gridColumns(width);
624
+ const gap = 2;
625
+ const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
626
+ const contentWidth = cardWidth - 4;
627
+ const themeName = opts2.theme || "default";
628
+ const theme = THEMES[themeName] || THEMES.default;
629
+ const vis = opts2.visible ?? {};
630
+ const out = [];
631
+ out.push(...header(data, width, [], theme.border));
632
+ out.push("");
633
+ const cards = [];
634
+ if (vis.cpu !== false) {
635
+ cards.push({ title: "CPU", accent: theme.cpu, lines: cpuCard(data, contentWidth) });
636
+ }
637
+ if (vis.mem !== false) {
638
+ cards.push({ title: "Memory", accent: theme.mem, lines: memCard(data, contentWidth) });
639
+ }
640
+ if (vis.power !== false) {
641
+ const power = powerCard(data);
642
+ if (power) cards.push({ title: "Power", accent: theme.power, lines: power });
643
+ }
644
+ if (vis.battery !== false) {
645
+ const battery = batteryCard(data, contentWidth);
646
+ if (battery) cards.push({ title: "Battery", accent: theme.battery, lines: battery });
647
+ }
648
+ if (vis.thermal !== false) {
649
+ cards.push({ title: "Thermal", accent: theme.thermal, lines: thermalCard(data, contentWidth) });
650
+ }
651
+ if (vis.network !== false) {
652
+ cards.push({ title: "Network", accent: theme.network, lines: networkCard(data, contentWidth) });
653
+ }
654
+ for (let i = 0; i < cards.length; i += columns) {
655
+ const row = cards.slice(i, i + columns);
656
+ const rowHeight = Math.max(...row.map((c) => c.lines.length));
657
+ const blocks = row.map((c) => panel(c.title, c.lines, cardWidth, c.accent, theme.border, rowHeight));
658
+ out.push(...hstack(blocks, gap));
659
+ out.push("");
660
+ }
661
+ if (vis.disk !== false && data.disk.length) {
662
+ const dw = width - 4;
663
+ out.push(...panel("Disk", diskTableLines(data.disk, dw, theme.border), width, theme.disk, theme.border));
664
+ out.push("");
665
+ }
666
+ if (vis.process !== false) {
667
+ const filteredProcesses = opts2.filter ? data.processes.filter((p) => p.command.toLowerCase().includes(opts2.filter.toLowerCase())) : data.processes;
668
+ const sortedProcesses = sortProcesses(filteredProcesses, opts2.sortBy ?? "cpu");
669
+ const limited = opts2.processLimit ? sortedProcesses.slice(0, opts2.processLimit) : sortedProcesses;
670
+ const procTitle = opts2.filter ? `Processes \xB7 filter "${opts2.filter}" \xB7 sort ${opts2.sortBy ?? "cpu"}` : `Processes \xB7 sort ${opts2.sortBy ?? "cpu"}`;
671
+ if (limited.length) {
672
+ out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
673
+ } else if (opts2.filter) {
674
+ out.push(...panel(procTitle, [chalk2.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
675
+ }
676
+ }
677
+ return out.join("\n");
678
+ }
679
+ function formatJson(data) {
680
+ return JSON.stringify(data, null, 2);
681
+ }
682
+ function formatCsv(data) {
683
+ const rows = [
684
+ ["property", "key", "value"],
685
+ ["cpu", "brand", data.cpu.brand],
686
+ ["cpu", "usage", String(data.cpu.usage)],
687
+ ["cpu", "loadAvg_1m", String(data.cpu.loadAvg[0])],
688
+ ["cpu", "loadAvg_5m", String(data.cpu.loadAvg[1])],
689
+ ["cpu", "loadAvg_15m", String(data.cpu.loadAvg[2])],
690
+ ["memory", "total", String(data.memory.total)],
691
+ ["memory", "used", String(data.memory.used)],
692
+ ["memory", "usagePercent", String(data.memory.usagePercent)],
693
+ ["memory", "swapUsed", String(data.memory.swapUsed)],
694
+ ["memory", "swapTotal", String(data.memory.swapTotal)],
695
+ ["thermal", "state", data.thermal.state],
696
+ ["thermal", "cpuTempC", data.thermal.temperatures?.cpu_die ? String(data.thermal.temperatures.cpu_die) : ""],
697
+ [
698
+ "thermal",
699
+ "cpuTempF",
700
+ data.thermal.temperatures?.cpu_die !== void 0 ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""
701
+ ],
702
+ ["network", "rxBytes", String(data.network.rxBytes)],
703
+ ["network", "txBytes", String(data.network.txBytes)],
704
+ ["battery", "level", data.battery ? String(data.battery.level) : ""],
705
+ ["battery", "powerSource", data.battery ? data.battery.powerSource : ""],
706
+ ["battery", "condition", data.battery?.condition ?? ""],
707
+ ["battery", "maxCapacityPercent", data.battery?.maxCapacityPercent !== void 0 ? String(data.battery.maxCapacityPercent) : ""],
708
+ ["battery", "cycles", data.battery?.cycles !== void 0 ? String(data.battery.cycles) : ""],
709
+ ["power", "cpuWatts", data.power?.cpuWatts !== void 0 ? String(data.power.cpuWatts) : ""],
710
+ ["power", "gpuWatts", data.power?.gpuWatts !== void 0 ? String(data.power.gpuWatts) : ""],
711
+ ["power", "combinedWatts", data.power?.combinedWatts !== void 0 ? String(data.power.combinedWatts) : ""],
712
+ ["system", "hostname", data.header.hostname],
713
+ ["system", "uptime", data.header.uptime],
714
+ ["system", "timestamp", data.timestamp]
715
+ ];
716
+ return rows.map((r) => r.map((item) => `"${String(item).replace(/"/g, '""')}"`).join(",")).join("\n");
717
+ }
718
+ function formatTsv(data) {
719
+ return formatCsv(data).split("\n").map((r) => r.split(",").join(" ")).join("\n");
720
+ }
721
+ function formatGraphs(history2, width = 80, themeName = "default") {
722
+ const theme = THEMES[themeName] || THEMES.default;
723
+ const contentWidth = width - 4;
724
+ const sampleCount = history2.cpuUsage.length;
725
+ const sparkWidth = Math.max(10, contentWidth - 28);
726
+ const lines = [];
727
+ lines.push(graphRow("CPU %", history2.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth));
728
+ lines.push(graphRow("Mem %", history2.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth));
729
+ lines.push(
730
+ graphRow("Temp", history2.temp, {}, formatTemp, sparkWidth, "no sensor access (needs sudo powermetrics)")
731
+ );
732
+ lines.push(graphRow("Net RX/s", history2.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth));
733
+ lines.push(graphRow("Net TX/s", history2.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth));
734
+ const title = `Graphs \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
735
+ return panel(title, lines, width, theme.graphs, theme.border).join("\n");
736
+ }
737
+ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...") {
738
+ if (!values.length) {
739
+ return `${chalk2.dim(label.padEnd(10))} ${chalk2.dim(emptyMessage)}`;
740
+ }
741
+ const visible = values.slice(-sparkWidth);
742
+ const spark = sparkline(visible, bounds);
743
+ const current = values[values.length - 1];
744
+ let color = chalk2.cyan;
745
+ if (bounds.min !== void 0 && bounds.max !== void 0) {
746
+ const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
747
+ color = pct > 90 ? chalk2.red : pct > 70 ? chalk2.yellow : chalk2.green;
748
+ }
749
+ return `${chalk2.dim(label.padEnd(10))}${color(spark)} ${chalk2.bold(fmt(current))}`;
750
+ }
751
+
752
+ // src/live.ts
753
+ import fs from "fs";
754
+ import path from "path";
755
+ import readline from "readline";
756
+ import chalk3 from "chalk";
757
+
758
+ // src/history.ts
759
+ var History = class {
760
+ maxLen;
761
+ cpuUsage = [];
762
+ memUsage = [];
763
+ temp = [];
764
+ netRxRate = [];
765
+ netTxRate = [];
766
+ lastRxBytes = 0;
767
+ lastTxBytes = 0;
768
+ lastTs = 0;
769
+ constructor(maxLen = 40) {
770
+ this.maxLen = maxLen;
771
+ }
772
+ push(sample) {
773
+ const now = Date.now();
774
+ let rxRate = 0;
775
+ let txRate = 0;
776
+ if (this.lastTs > 0) {
777
+ const dtSeconds = Math.max((now - this.lastTs) / 1e3, 1e-3);
778
+ rxRate = Math.max(0, (sample.rxBytes - this.lastRxBytes) / dtSeconds);
779
+ txRate = Math.max(0, (sample.txBytes - this.lastTxBytes) / dtSeconds);
780
+ }
781
+ this.lastRxBytes = sample.rxBytes;
782
+ this.lastTxBytes = sample.txBytes;
783
+ this.lastTs = now;
784
+ this.pushBounded(this.cpuUsage, sample.cpuUsage);
785
+ this.pushBounded(this.memUsage, sample.memUsage);
786
+ if (sample.temp !== null) this.pushBounded(this.temp, sample.temp);
787
+ this.pushBounded(this.netRxRate, rxRate);
788
+ this.pushBounded(this.netTxRate, txRate);
789
+ }
790
+ reset() {
791
+ this.cpuUsage = [];
792
+ this.memUsage = [];
793
+ this.temp = [];
794
+ this.netRxRate = [];
795
+ this.netTxRate = [];
796
+ this.lastRxBytes = 0;
797
+ this.lastTxBytes = 0;
798
+ this.lastTs = 0;
799
+ }
800
+ /** Grow or shrink the rolling window (e.g. on terminal resize), trimming from the oldest samples. */
801
+ setMaxLen(n) {
802
+ this.maxLen = Math.max(1, n);
803
+ for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate]) {
804
+ while (arr.length > this.maxLen) arr.shift();
805
+ }
806
+ }
807
+ pushBounded(arr, v) {
808
+ arr.push(v);
809
+ if (arr.length > this.maxLen) arr.shift();
810
+ }
811
+ };
812
+
813
+ // src/live.ts
814
+ var intervalHandle = null;
815
+ var running = false;
816
+ var paused = false;
817
+ var detailed = false;
818
+ var interval = 2;
819
+ var showGraphs = true;
820
+ var exportFormat = "json";
821
+ var exportDir = "./pyre-exports";
822
+ var currentTheme = "default";
823
+ var visiblePanels = {
824
+ cpu: true,
825
+ mem: true,
826
+ power: true,
827
+ battery: true,
828
+ thermal: true,
829
+ network: true,
830
+ disk: true,
831
+ process: true
832
+ };
833
+ var logging = false;
834
+ var logStream = null;
835
+ var statusMessage = "";
836
+ var statusTimer = null;
837
+ var lastData = null;
838
+ var history = new History(40);
839
+ var keypressHandler = null;
840
+ var termWidth = process.stdout.columns || 80;
841
+ var termHeight = process.stdout.rows || 24;
842
+ var sortMode = "cpu";
843
+ var processFilter = "";
844
+ var inputMode = null;
845
+ var inputBuffer = "";
846
+ var customizerIndex = 0;
847
+ var CUSTOMIZER_OPTIONS = [
848
+ "Theme",
849
+ "Toggle CPU",
850
+ "Toggle Memory",
851
+ "Toggle Power",
852
+ "Toggle Battery",
853
+ "Toggle Thermal",
854
+ "Toggle Network",
855
+ "Toggle Disk",
856
+ "Toggle Processes"
857
+ ];
858
+ var CPU_ALERT_PCT = 90;
859
+ var TEMP_ALERT_C = 95;
860
+ var alerted = false;
861
+ function setStatus(msg, ms = 3e3) {
862
+ statusMessage = msg;
863
+ if (statusTimer) clearTimeout(statusTimer);
864
+ statusTimer = setTimeout(() => {
865
+ statusMessage = "";
866
+ render();
867
+ }, ms);
868
+ }
869
+ function ensureDir(dir) {
870
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
871
+ }
872
+ function timestampForFile() {
873
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
874
+ }
875
+ function exportSnapshot() {
876
+ if (!lastData) {
877
+ setStatus("No data yet \u2014 try again in a moment");
878
+ return;
879
+ }
880
+ try {
881
+ ensureDir(exportDir);
882
+ const file = path.join(exportDir, `pyre-${timestampForFile()}.${exportFormat}`);
883
+ const content = exportFormat === "json" ? formatJson(lastData) : exportFormat === "csv" ? formatCsv(lastData) : formatTsv(lastData);
884
+ fs.writeFileSync(file, content);
885
+ setStatus(`Exported snapshot -> ${file}`);
886
+ } catch (err) {
887
+ setStatus(`Export failed: ${err.message}`);
888
+ }
889
+ }
890
+ function startLogging() {
891
+ try {
892
+ ensureDir(exportDir);
893
+ const file = path.join(exportDir, `pyre-log-${timestampForFile()}.csv`);
894
+ logStream = fs.createWriteStream(file, { flags: "a" });
895
+ logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,thermal_state\n");
896
+ logging = true;
897
+ setStatus(`Logging every tick -> ${file}`);
898
+ } catch (err) {
899
+ setStatus(`Logging failed to start: ${err.message}`);
900
+ }
901
+ }
902
+ function stopLogging() {
903
+ logging = false;
904
+ logStream?.end();
905
+ logStream = null;
906
+ setStatus("Logging stopped");
907
+ }
908
+ function toggleLogging() {
909
+ if (logging) stopLogging();
910
+ else startLogging();
911
+ }
912
+ function writeLogRow(data) {
913
+ if (!logging || !logStream) return;
914
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
915
+ logStream.write(
916
+ `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${data.thermal.state}
917
+ `
918
+ );
919
+ }
920
+ function processRowBudget() {
921
+ const columns = gridColumns(termWidth);
922
+ const cardRows = Math.ceil(6 / columns);
923
+ const reserved = 2 + cardRows * 8 + 11 + (showGraphs ? 9 : 0) + 4;
924
+ return Math.max(3, Math.min(40, termHeight - reserved));
925
+ }
926
+ function renderCustomizerOverlay() {
927
+ const themesList = Object.keys(THEMES);
928
+ const lines = [
929
+ chalk3.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ")
930
+ ];
931
+ CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
932
+ const isSelected = idx === customizerIndex;
933
+ const prefix = isSelected ? chalk3.yellow("\u25B6 ") : " ";
934
+ if (opt === "Theme") {
935
+ lines.push(`${prefix}${opt}: ${chalk3.bold.green(currentTheme)} [${themesList.join(", ")}]`);
936
+ } else {
937
+ const key = opt.replace("Toggle ", "").toLowerCase();
938
+ const isVisible = visiblePanels[key] !== false;
939
+ const status = isVisible ? chalk3.green("[VISIBLE]") : chalk3.red("[HIDDEN]");
940
+ lines.push(`${prefix}${opt}: ${status}`);
941
+ }
942
+ });
943
+ return lines.map((l) => ` ${l}`).join("\n");
944
+ }
945
+ function render() {
946
+ if (!lastData) return;
947
+ const lines = [];
948
+ lines.push(
949
+ formatTable(lastData, {
950
+ width: termWidth,
951
+ sortBy: sortMode,
952
+ filter: processFilter || void 0,
953
+ processLimit: processRowBudget(),
954
+ theme: currentTheme,
955
+ visible: visiblePanels
956
+ })
957
+ );
958
+ if (showGraphs) {
959
+ lines.push("");
960
+ lines.push(formatGraphs(history, termWidth, currentTheme));
961
+ }
962
+ lines.push("");
963
+ lines.push(footerLine());
964
+ if (inputMode === "customizer") {
965
+ lines.push(renderCustomizerOverlay());
966
+ } else if (inputMode === "filter") {
967
+ lines.push(chalk3.cyan(` Filter processes: ${inputBuffer}_`));
968
+ } else if (inputMode === "kill") {
969
+ lines.push(chalk3.cyan(` Kill PID: ${inputBuffer}_ (enter to confirm, esc to cancel)`));
970
+ } else if (statusMessage) {
971
+ lines.push(` ${statusMessage}`);
972
+ }
973
+ process.stdout.write("\x1B[2J\x1B[H");
974
+ process.stdout.write(lines.join("\n"));
975
+ }
976
+ function footerLine() {
977
+ const keys = [
978
+ ["q", "quit"],
979
+ ["c", "customize UI"],
980
+ ["p", paused ? "resume" : "pause"],
981
+ ["g", showGraphs ? "hide graphs" : "show graphs"],
982
+ ["d", detailed ? "basic" : "detailed"],
983
+ ["s", `sort:${sortMode}`],
984
+ ["/", "filter"],
985
+ ["k", "kill"],
986
+ ["e", "export"],
987
+ ["l", logging ? "stop log" : "log"],
988
+ ["f", exportFormat],
989
+ ["+/-", `${interval}s`]
990
+ ];
991
+ const keyStr = keys.map(([k, label]) => `${chalk3.cyan.bold(k)} ${chalk3.dim(label)}`).join(chalk3.dim(" \xB7 "));
992
+ const badges = [];
993
+ if (paused) badges.push(chalk3.yellow.bold("\u23F8 PAUSED"));
994
+ if (logging) badges.push(chalk3.red.bold("\u25CF REC"));
995
+ const badgeStr = badges.join(" ");
996
+ return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
997
+ }
998
+ function checkAlerts(data) {
999
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1000
+ const hot = data.cpu.usage >= CPU_ALERT_PCT || temp !== null && temp >= TEMP_ALERT_C;
1001
+ if (hot && !alerted) {
1002
+ alerted = true;
1003
+ process.stdout.write("\x07");
1004
+ const reason = data.cpu.usage >= CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}\xB0C`;
1005
+ setStatus(chalk3.red(`\u26A0 Alert: ${reason}`), 5e3);
1006
+ } else if (!hot) {
1007
+ alerted = false;
1008
+ }
1009
+ }
1010
+ async function tick() {
1011
+ if (paused) return;
1012
+ try {
1013
+ const data = await collectAll({ detailed, processLimit: processRowBudget() });
1014
+ lastData = data;
1015
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1016
+ history.push({
1017
+ cpuUsage: data.cpu.usage,
1018
+ memUsage: data.memory.usagePercent,
1019
+ temp,
1020
+ rxBytes: data.network.rxBytes,
1021
+ txBytes: data.network.txBytes
1022
+ });
1023
+ writeLogRow(data);
1024
+ checkAlerts(data);
1025
+ render();
1026
+ } catch {
1027
+ }
1028
+ }
1029
+ function killProcess(pidStr) {
1030
+ const pid = parseInt(pidStr, 10);
1031
+ if (!pid || pid <= 0) {
1032
+ setStatus(`Invalid PID: ${pidStr}`);
1033
+ return;
1034
+ }
1035
+ try {
1036
+ process.kill(pid, "SIGTERM");
1037
+ setStatus(`Sent SIGTERM to PID ${pid}`);
1038
+ } catch (err) {
1039
+ setStatus(`Failed to kill ${pid}: ${err.message}`);
1040
+ }
1041
+ }
1042
+ function handleInputModeKey(str, key) {
1043
+ if (key.name === "escape") {
1044
+ inputMode = null;
1045
+ inputBuffer = "";
1046
+ render();
1047
+ return;
1048
+ }
1049
+ if (inputMode === "customizer") {
1050
+ const themesList = Object.keys(THEMES);
1051
+ if (key.name === "up" || str === "k") {
1052
+ customizerIndex = (customizerIndex - 1 + CUSTOMIZER_OPTIONS.length) % CUSTOMIZER_OPTIONS.length;
1053
+ } else if (key.name === "down" || str === "j") {
1054
+ customizerIndex = (customizerIndex + 1) % CUSTOMIZER_OPTIONS.length;
1055
+ } else if (key.name === "return" || str === " ") {
1056
+ const selected = CUSTOMIZER_OPTIONS[customizerIndex];
1057
+ if (selected === "Theme") {
1058
+ const nextIdx = (themesList.indexOf(currentTheme) + 1) % themesList.length;
1059
+ currentTheme = themesList[nextIdx];
1060
+ } else {
1061
+ const itemKey = selected.replace("Toggle ", "").toLowerCase();
1062
+ visiblePanels[itemKey] = !visiblePanels[itemKey];
1063
+ }
1064
+ }
1065
+ render();
1066
+ return;
1067
+ }
1068
+ if (key.name === "return") {
1069
+ if (inputMode === "filter") {
1070
+ processFilter = inputBuffer.trim();
1071
+ setStatus(processFilter ? `Filtering: "${processFilter}"` : "Filter cleared");
1072
+ } else if (inputMode === "kill") {
1073
+ killProcess(inputBuffer.trim());
1074
+ }
1075
+ inputMode = null;
1076
+ inputBuffer = "";
1077
+ render();
1078
+ return;
1079
+ }
1080
+ if (key.name === "backspace") {
1081
+ inputBuffer = inputBuffer.slice(0, -1);
1082
+ render();
1083
+ return;
1084
+ }
1085
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
1086
+ inputBuffer += str;
1087
+ render();
1088
+ }
1089
+ }
1090
+ function onResize() {
1091
+ termWidth = process.stdout.columns || 80;
1092
+ termHeight = process.stdout.rows || 24;
1093
+ history.setMaxLen(Math.max(20, Math.min(200, termWidth - 30)));
1094
+ render();
1095
+ }
1096
+ function restartTicker() {
1097
+ if (intervalHandle) clearInterval(intervalHandle);
1098
+ intervalHandle = setInterval(tick, interval * 1e3);
1099
+ }
1100
+ async function startLive(opts2) {
1101
+ if (running) return;
1102
+ running = true;
1103
+ paused = false;
1104
+ detailed = !!opts2.detailed;
1105
+ interval = opts2.interval;
1106
+ if (opts2.exportDir) exportDir = opts2.exportDir;
1107
+ termWidth = process.stdout.columns || 80;
1108
+ termHeight = process.stdout.rows || 24;
1109
+ history.reset();
1110
+ history.setMaxLen(Math.max(20, Math.min(200, termWidth - 30)));
1111
+ process.stdout.write("\x1B[?1049h");
1112
+ process.stdout.write("\x1B[?25l");
1113
+ process.stdout.write("\x1B[2J\x1B[H");
1114
+ process.title = "pyre";
1115
+ await tick();
1116
+ restartTicker();
1117
+ if (opts2.autoLog) startLogging();
1118
+ readline.emitKeypressEvents(process.stdin);
1119
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
1120
+ process.stdin.resume();
1121
+ process.stdout.on("resize", onResize);
1122
+ keypressHandler = (str, key) => {
1123
+ if (!key) return;
1124
+ if (key.ctrl && key.name === "c") {
1125
+ stopLive();
1126
+ return;
1127
+ }
1128
+ if (inputMode) {
1129
+ handleInputModeKey(str, key);
1130
+ return;
1131
+ }
1132
+ switch (key.sequence) {
1133
+ case "+":
1134
+ interval += 1;
1135
+ restartTicker();
1136
+ setStatus(`Interval set to ${interval}s`);
1137
+ return;
1138
+ case "-":
1139
+ interval = Math.max(1, interval - 1);
1140
+ restartTicker();
1141
+ setStatus(`Interval set to ${interval}s`);
1142
+ return;
1143
+ case "/":
1144
+ inputMode = "filter";
1145
+ inputBuffer = processFilter;
1146
+ render();
1147
+ return;
1148
+ }
1149
+ switch (key.name) {
1150
+ case "q":
1151
+ stopLive();
1152
+ break;
1153
+ case "c":
1154
+ inputMode = "customizer";
1155
+ customizerIndex = 0;
1156
+ render();
1157
+ break;
1158
+ case "p":
1159
+ paused = !paused;
1160
+ setStatus(paused ? "Paused" : "Resumed");
1161
+ render();
1162
+ break;
1163
+ case "g":
1164
+ showGraphs = !showGraphs;
1165
+ render();
1166
+ break;
1167
+ case "d":
1168
+ detailed = !detailed;
1169
+ setStatus(`Detailed sensor mode: ${detailed ? "on" : "off"}`);
1170
+ break;
1171
+ case "s":
1172
+ sortMode = sortMode === "cpu" ? "mem" : sortMode === "mem" ? "pid" : "cpu";
1173
+ setStatus(`Sorting by ${sortMode}`);
1174
+ render();
1175
+ break;
1176
+ case "k":
1177
+ inputMode = "kill";
1178
+ inputBuffer = "";
1179
+ render();
1180
+ break;
1181
+ case "e":
1182
+ exportSnapshot();
1183
+ render();
1184
+ break;
1185
+ case "l":
1186
+ toggleLogging();
1187
+ render();
1188
+ break;
1189
+ case "f":
1190
+ exportFormat = exportFormat === "json" ? "csv" : exportFormat === "csv" ? "tsv" : "json";
1191
+ setStatus(`Export format: ${exportFormat}`);
1192
+ render();
1193
+ break;
1194
+ }
1195
+ };
1196
+ process.stdin.on("keypress", keypressHandler);
1197
+ process.once("SIGINT", () => stopLive());
1198
+ }
1199
+ function stopLive() {
1200
+ if (intervalHandle) {
1201
+ clearInterval(intervalHandle);
1202
+ intervalHandle = null;
1203
+ }
1204
+ if (statusTimer) {
1205
+ clearTimeout(statusTimer);
1206
+ statusTimer = null;
1207
+ }
1208
+ if (logStream) {
1209
+ logStream.end();
1210
+ logStream = null;
1211
+ }
1212
+ if (keypressHandler) {
1213
+ process.stdin.removeListener("keypress", keypressHandler);
1214
+ keypressHandler = null;
1215
+ }
1216
+ process.stdout.removeListener("resize", onResize);
1217
+ running = false;
1218
+ logging = false;
1219
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1220
+ process.stdin.pause();
1221
+ process.stdout.write("\x1B[?25h");
1222
+ process.stdout.write("\x1B[?1049l");
1223
+ process.stdout.write("\x1B[2J\x1B[H");
1224
+ process.exit(0);
1225
+ }
1226
+
1227
+ // src/index.ts
1228
+ var program = new Command();
1229
+ program.name("pyre").version("1.1.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts");
1230
+ program.parse(process.argv);
1231
+ var opts = program.opts();
1232
+ function isExportMode() {
1233
+ return opts.json || opts.csv || opts.tsv;
1234
+ }
1235
+ function stripAnsi(s) {
1236
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
1237
+ }
1238
+ async function main() {
1239
+ const requestedLive = program.args[0] === "live" || !isExportMode() && !opts.once;
1240
+ if (requestedLive) {
1241
+ const interval2 = parseFloat(opts.interval) || 2;
1242
+ await startLive({
1243
+ interval: interval2,
1244
+ detailed: opts.detailed,
1245
+ exportDir: opts.exportDir,
1246
+ autoLog: opts.log
1247
+ });
1248
+ return;
1249
+ }
1250
+ const data = await collectAll({ detailed: opts.detailed });
1251
+ let output;
1252
+ if (opts.json) output = formatJson(data);
1253
+ else if (opts.csv) output = formatCsv(data);
1254
+ else if (opts.tsv) output = formatTsv(data);
1255
+ else output = formatTable(data, { width: process.stdout.columns || 80 });
1256
+ console.log(output);
1257
+ if (opts.out) {
1258
+ fs2.writeFileSync(opts.out, stripAnsi(output));
1259
+ console.log(chalk4.dim(`
1260
+ Saved to ${opts.out}`));
1261
+ }
1262
+ }
1263
+ main().catch((err) => {
1264
+ console.error(chalk4.red(`Error: ${err.message}`));
1265
+ stopLive();
1266
+ process.exit(1);
1267
+ });