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