pyre-cli 6.0.0 → 6.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 +1 -1
- package/dist/index.js +416 -157
- package/package.json +1 -1
- package/src/completions.ts +118 -0
- package/src/diffCmd.ts +36 -0
- package/src/doctor.ts +123 -0
- package/src/formatters/render.ts +12 -3
- package/src/formatters/themes.ts +47 -12
- package/src/historyCmd.ts +71 -0
- package/src/index.ts +90 -5
- package/src/live/input.ts +8 -0
- package/src/live/render.ts +82 -55
- package/src/live/state.ts +3 -0
- package/src/monitors/collectors.ts +41 -2
- package/src/monitors/types.ts +2 -0
- package/src/state/config.ts +6 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import fs8 from "fs";
|
|
5
|
+
import os4 from "os";
|
|
6
|
+
import path5 from "path";
|
|
7
7
|
import readline2 from "readline";
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
-
import
|
|
9
|
+
import chalk11 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/monitors/collectors.ts
|
|
12
12
|
import chalk from "chalk";
|
|
@@ -472,9 +472,41 @@ async function collectMemory() {
|
|
|
472
472
|
usagePercent
|
|
473
473
|
};
|
|
474
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
|
+
}
|
|
475
504
|
async function collectDisk() {
|
|
476
|
-
const
|
|
477
|
-
|
|
505
|
+
const [rawDf, ioRates] = await Promise.all([
|
|
506
|
+
run("df -h"),
|
|
507
|
+
getDiskIoRates()
|
|
508
|
+
]);
|
|
509
|
+
const lines = rawDf.trim().split("\n").slice(1);
|
|
478
510
|
return lines.map((line) => {
|
|
479
511
|
const parts = line.split(/\s+/);
|
|
480
512
|
return {
|
|
@@ -483,7 +515,9 @@ async function collectDisk() {
|
|
|
483
515
|
used: parts[2],
|
|
484
516
|
available: parts[3],
|
|
485
517
|
capacity: parts[4],
|
|
486
|
-
mountpoint: parts[5] || parts[6] || "/"
|
|
518
|
+
mountpoint: parts[5] || parts[6] || "/",
|
|
519
|
+
readBytesSec: ioRates.readBytesSec,
|
|
520
|
+
writeBytesSec: ioRates.writeBytesSec
|
|
487
521
|
};
|
|
488
522
|
}).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
|
|
489
523
|
}
|
|
@@ -791,7 +825,10 @@ function multiRowSparkline(values, height, opts2 = {}) {
|
|
|
791
825
|
|
|
792
826
|
// src/formatters/themes.ts
|
|
793
827
|
import chalk2 from "chalk";
|
|
794
|
-
|
|
828
|
+
import fs from "fs";
|
|
829
|
+
import os2 from "os";
|
|
830
|
+
import path from "path";
|
|
831
|
+
var BUILTIN_THEMES = {
|
|
795
832
|
default: {
|
|
796
833
|
border: chalk2.hex("#4b5563"),
|
|
797
834
|
cpu: chalk2.hex("#22d3ee"),
|
|
@@ -871,6 +908,42 @@ var THEMES = {
|
|
|
871
908
|
process: chalk2.hex("#ebdbb2")
|
|
872
909
|
}
|
|
873
910
|
};
|
|
911
|
+
function loadCustomThemes() {
|
|
912
|
+
const themes = { ...BUILTIN_THEMES };
|
|
913
|
+
const themesDir = path.join(os2.homedir(), ".config", "pyre", "themes");
|
|
914
|
+
try {
|
|
915
|
+
if (fs.existsSync(themesDir)) {
|
|
916
|
+
const files = fs.readdirSync(themesDir);
|
|
917
|
+
for (const file of files) {
|
|
918
|
+
if (file.endsWith(".json")) {
|
|
919
|
+
const name = path.basename(file, ".json");
|
|
920
|
+
try {
|
|
921
|
+
const raw = fs.readFileSync(path.join(themesDir, file), "utf-8");
|
|
922
|
+
const data = JSON.parse(raw);
|
|
923
|
+
const buildColor = (val) => val ? val.startsWith("#") ? chalk2.hex(val) : chalk2[val] || chalk2.white : chalk2.white;
|
|
924
|
+
themes[name] = {
|
|
925
|
+
border: buildColor(data.border),
|
|
926
|
+
cpu: buildColor(data.cpu),
|
|
927
|
+
mem: buildColor(data.mem),
|
|
928
|
+
gpu: buildColor(data.gpu),
|
|
929
|
+
power: buildColor(data.power),
|
|
930
|
+
battery: buildColor(data.battery),
|
|
931
|
+
thermal: buildColor(data.thermal),
|
|
932
|
+
network: buildColor(data.network),
|
|
933
|
+
disk: buildColor(data.disk),
|
|
934
|
+
graphs: buildColor(data.graphs),
|
|
935
|
+
process: buildColor(data.process)
|
|
936
|
+
};
|
|
937
|
+
} catch {
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
return themes;
|
|
945
|
+
}
|
|
946
|
+
var THEMES = loadCustomThemes();
|
|
874
947
|
|
|
875
948
|
// src/formatters/render.ts
|
|
876
949
|
function visLen(s) {
|
|
@@ -1405,7 +1478,7 @@ function btopCpuBox(data, width, theme, opts2) {
|
|
|
1405
1478
|
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
1406
1479
|
const coreColW = Math.floor((rightW - 1) / 2);
|
|
1407
1480
|
const half = Math.ceil(data.cpu.coreUsage.length / 2);
|
|
1408
|
-
for (let i = 0; i <
|
|
1481
|
+
for (let i = 0; i < half; i++) {
|
|
1409
1482
|
const u1 = data.cpu.coreUsage[i];
|
|
1410
1483
|
const u2 = data.cpu.coreUsage[i + half];
|
|
1411
1484
|
const c1BarW = Math.max(3, coreColW - 8);
|
|
@@ -1447,9 +1520,18 @@ function btopLeftColumn(data, width, theme, opts2) {
|
|
|
1447
1520
|
for (const d of data.disk.slice(0, 2)) {
|
|
1448
1521
|
const capNum = parseInt(d.capacity, 10) || 0;
|
|
1449
1522
|
const mountShort = truncatePlain(d.mountpoint, 8);
|
|
1450
|
-
|
|
1523
|
+
let ioStr = "";
|
|
1524
|
+
if (d.readBytesSec !== void 0 || d.writeBytesSec !== void 0) {
|
|
1525
|
+
const rStr = formatBytes(d.readBytesSec || 0);
|
|
1526
|
+
const wStr = formatBytes(d.writeBytesSec || 0);
|
|
1527
|
+
ioStr = ` R:${rStr}/s W:${wStr}/s`;
|
|
1528
|
+
}
|
|
1529
|
+
diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18 - ioStr.length))}] ${d.capacity}${ioStr}`);
|
|
1451
1530
|
}
|
|
1452
|
-
|
|
1531
|
+
const totalRead = data.disk.reduce((acc, d) => acc + (d.readBytesSec || 0), 0);
|
|
1532
|
+
const totalWrite = data.disk.reduce((acc, d) => acc + (d.writeBytesSec || 0), 0);
|
|
1533
|
+
const diskHeaderRight = `R:${formatBytes(totalRead)}/s W:${formatBytes(totalWrite)}/s`;
|
|
1534
|
+
out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length, diskHeaderRight));
|
|
1453
1535
|
}
|
|
1454
1536
|
const netLines = [];
|
|
1455
1537
|
const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
|
|
@@ -2152,11 +2234,11 @@ var History = class _History {
|
|
|
2152
2234
|
};
|
|
2153
2235
|
|
|
2154
2236
|
// src/state/config.ts
|
|
2155
|
-
import
|
|
2156
|
-
import
|
|
2157
|
-
import
|
|
2158
|
-
var CONFIG_DIR =
|
|
2159
|
-
var CONFIG_FILE =
|
|
2237
|
+
import fs2 from "fs";
|
|
2238
|
+
import os3 from "os";
|
|
2239
|
+
import path2 from "path";
|
|
2240
|
+
var CONFIG_DIR = path2.join(os3.homedir(), ".config", "pyre");
|
|
2241
|
+
var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
|
|
2160
2242
|
var DEFAULT_CONFIG = {
|
|
2161
2243
|
theme: "default",
|
|
2162
2244
|
interval: 2,
|
|
@@ -2170,6 +2252,9 @@ var DEFAULT_CONFIG = {
|
|
|
2170
2252
|
mouseEnabled: true,
|
|
2171
2253
|
cpuAlertPct: 90,
|
|
2172
2254
|
tempAlertC: 95,
|
|
2255
|
+
watchdogProcess: "",
|
|
2256
|
+
watchdogCpu: 80,
|
|
2257
|
+
watchdogMem: 80,
|
|
2173
2258
|
notificationsEnabled: true,
|
|
2174
2259
|
p2pPort: 9876,
|
|
2175
2260
|
p2pPassword: "pyre",
|
|
@@ -2193,8 +2278,8 @@ var DEFAULT_CONFIG = {
|
|
|
2193
2278
|
};
|
|
2194
2279
|
function readConfig() {
|
|
2195
2280
|
try {
|
|
2196
|
-
if (!
|
|
2197
|
-
const raw =
|
|
2281
|
+
if (!fs2.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
2282
|
+
const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
|
|
2198
2283
|
const parsed = JSON.parse(raw);
|
|
2199
2284
|
return deepMerge(DEFAULT_CONFIG, parsed);
|
|
2200
2285
|
} catch {
|
|
@@ -2203,11 +2288,11 @@ function readConfig() {
|
|
|
2203
2288
|
}
|
|
2204
2289
|
function writeConfig(config3) {
|
|
2205
2290
|
try {
|
|
2206
|
-
if (!
|
|
2207
|
-
|
|
2291
|
+
if (!fs2.existsSync(CONFIG_DIR)) {
|
|
2292
|
+
fs2.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
2208
2293
|
}
|
|
2209
2294
|
const merged = deepMerge(readConfig(), config3);
|
|
2210
|
-
|
|
2295
|
+
fs2.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
2211
2296
|
} catch {
|
|
2212
2297
|
}
|
|
2213
2298
|
}
|
|
@@ -2295,6 +2380,9 @@ var state = {
|
|
|
2295
2380
|
],
|
|
2296
2381
|
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
2297
2382
|
TEMP_ALERT_C: config.tempAlertC,
|
|
2383
|
+
watchdogProcess: config.watchdogProcess || "",
|
|
2384
|
+
watchdogCpu: config.watchdogCpu || 80,
|
|
2385
|
+
watchdogMem: config.watchdogMem || 80,
|
|
2298
2386
|
notificationsEnabled: config.notificationsEnabled,
|
|
2299
2387
|
alerted: false,
|
|
2300
2388
|
activePanel: "grid",
|
|
@@ -2349,10 +2437,10 @@ function getToggleKey(opt) {
|
|
|
2349
2437
|
}
|
|
2350
2438
|
|
|
2351
2439
|
// src/live/export.ts
|
|
2352
|
-
import
|
|
2353
|
-
import
|
|
2440
|
+
import fs3 from "fs";
|
|
2441
|
+
import path3 from "path";
|
|
2354
2442
|
function ensureDir(dir) {
|
|
2355
|
-
if (!
|
|
2443
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2356
2444
|
}
|
|
2357
2445
|
function timestampForFile() {
|
|
2358
2446
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -2365,7 +2453,7 @@ function exportSnapshot() {
|
|
|
2365
2453
|
try {
|
|
2366
2454
|
ensureDir(state.exportDir);
|
|
2367
2455
|
const ext = state.exportFormat === "json" ? "json" : state.exportFormat === "csv" ? "csv" : state.exportFormat === "tsv" ? "tsv" : state.exportFormat === "html" ? "html" : "md";
|
|
2368
|
-
const file =
|
|
2456
|
+
const file = path3.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
|
|
2369
2457
|
let content;
|
|
2370
2458
|
switch (state.exportFormat) {
|
|
2371
2459
|
case "json":
|
|
@@ -2384,7 +2472,7 @@ function exportSnapshot() {
|
|
|
2384
2472
|
content = formatMarkdown(state.lastData);
|
|
2385
2473
|
break;
|
|
2386
2474
|
}
|
|
2387
|
-
|
|
2475
|
+
fs3.writeFileSync(file, content);
|
|
2388
2476
|
setStatus(`Exported snapshot -> ${file}`);
|
|
2389
2477
|
} catch (err) {
|
|
2390
2478
|
setStatus(`Export failed: ${err.message}`);
|
|
@@ -2394,8 +2482,8 @@ function startLogging() {
|
|
|
2394
2482
|
try {
|
|
2395
2483
|
ensureDir(state.exportDir);
|
|
2396
2484
|
rotateLogs();
|
|
2397
|
-
const file =
|
|
2398
|
-
state.logStream =
|
|
2485
|
+
const file = path3.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
|
|
2486
|
+
state.logStream = fs3.createWriteStream(file, { flags: "a" });
|
|
2399
2487
|
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");
|
|
2400
2488
|
state.logging = true;
|
|
2401
2489
|
setStatus(`Logging every tick -> ${file}`);
|
|
@@ -2409,37 +2497,37 @@ var MAX_LOG_BYTES = 50 * 1024 * 1024;
|
|
|
2409
2497
|
function rotateLogs() {
|
|
2410
2498
|
try {
|
|
2411
2499
|
const dir = state.exportDir;
|
|
2412
|
-
if (!
|
|
2413
|
-
const files =
|
|
2500
|
+
if (!fs3.existsSync(dir)) return;
|
|
2501
|
+
const files = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path3.join(dir, f), mtime: fs3.statSync(path3.join(dir, f)).mtimeMs, size: fs3.statSync(path3.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
|
|
2414
2502
|
const now = Date.now();
|
|
2415
2503
|
for (const f of files) {
|
|
2416
2504
|
if (now - f.mtime > MAX_LOG_AGE_MS) {
|
|
2417
2505
|
try {
|
|
2418
|
-
|
|
2506
|
+
fs3.unlinkSync(f.path);
|
|
2419
2507
|
} catch {
|
|
2420
2508
|
}
|
|
2421
2509
|
}
|
|
2422
2510
|
}
|
|
2423
|
-
const remaining =
|
|
2511
|
+
const remaining = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
|
|
2424
2512
|
if (remaining.length > MAX_LOG_COUNT) {
|
|
2425
2513
|
const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
|
|
2426
2514
|
for (const f of toDelete) {
|
|
2427
2515
|
try {
|
|
2428
|
-
|
|
2516
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
2429
2517
|
} catch {
|
|
2430
2518
|
}
|
|
2431
2519
|
}
|
|
2432
2520
|
}
|
|
2433
2521
|
let totalSize = 0;
|
|
2434
2522
|
for (const f of remaining) {
|
|
2435
|
-
totalSize +=
|
|
2523
|
+
totalSize += fs3.statSync(path3.join(dir, f)).size;
|
|
2436
2524
|
}
|
|
2437
2525
|
if (totalSize > MAX_LOG_BYTES) {
|
|
2438
2526
|
const sorted = remaining.sort();
|
|
2439
2527
|
for (const f of sorted) {
|
|
2440
|
-
const sz =
|
|
2528
|
+
const sz = fs3.statSync(path3.join(dir, f)).size;
|
|
2441
2529
|
try {
|
|
2442
|
-
|
|
2530
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
2443
2531
|
} catch {
|
|
2444
2532
|
}
|
|
2445
2533
|
totalSize -= sz;
|
|
@@ -2634,13 +2722,15 @@ function p2pPanelLines() {
|
|
|
2634
2722
|
const title = `P2P \xB7 ${state.p2pServerRunning ? "ON" : "OFF"} \xB7 ${events.length} events`;
|
|
2635
2723
|
return { title, body };
|
|
2636
2724
|
}
|
|
2637
|
-
function renderCustomizerOverlay() {
|
|
2725
|
+
function renderCustomizerOverlay(scrollOffset, maxVisibleItems) {
|
|
2638
2726
|
const themesList = Object.keys(THEMES);
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2727
|
+
const headerLine = chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ");
|
|
2728
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
2729
|
+
const end = Math.min(scrollOffset + maxVisibleItems, totalItems);
|
|
2730
|
+
const lines = [headerLine];
|
|
2731
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
2732
|
+
const opt = state.CUSTOMIZER_OPTIONS[i];
|
|
2733
|
+
const isSelected = i === state.customizerIndex;
|
|
2644
2734
|
const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
|
|
2645
2735
|
if (opt === "Theme") {
|
|
2646
2736
|
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
|
|
@@ -2664,7 +2754,7 @@ function renderCustomizerOverlay() {
|
|
|
2664
2754
|
const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
|
|
2665
2755
|
lines.push(`${prefix}${opt}: ${status}`);
|
|
2666
2756
|
}
|
|
2667
|
-
}
|
|
2757
|
+
}
|
|
2668
2758
|
return lines.map((l) => ` ${l}`).join("\n");
|
|
2669
2759
|
}
|
|
2670
2760
|
var _prevFrameLines = [];
|
|
@@ -2752,9 +2842,18 @@ function render() {
|
|
|
2752
2842
|
const footerLine22 = footerLine()[1] || "";
|
|
2753
2843
|
const footerLine3 = footerLine()[2] || " ";
|
|
2754
2844
|
const footerLines = [footerLine1, footerLine22, footerLine3];
|
|
2845
|
+
const footerHeight = 3;
|
|
2846
|
+
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
2755
2847
|
if (state.inputMode === "customizer") {
|
|
2756
|
-
bodyLines.
|
|
2757
|
-
|
|
2848
|
+
const preOverlayCount = bodyLines.length;
|
|
2849
|
+
const availableForOverlay = targetBodyHeight - preOverlayCount;
|
|
2850
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
2851
|
+
const headerLines = 1;
|
|
2852
|
+
const blankLines = availableForOverlay >= 2 ? 1 : 0;
|
|
2853
|
+
const maxVisibleItems = Math.max(0, availableForOverlay - headerLines - blankLines);
|
|
2854
|
+
const scrollOffset = totalItems > maxVisibleItems ? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems)) : 0;
|
|
2855
|
+
if (blankLines > 0) bodyLines.push("");
|
|
2856
|
+
bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split("\n"));
|
|
2758
2857
|
} else if (state.inputMode === "filter") {
|
|
2759
2858
|
bodyLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
2760
2859
|
} else if (state.inputMode === "kill") {
|
|
@@ -2770,8 +2869,6 @@ function render() {
|
|
|
2770
2869
|
} else if (state.statusMessage) {
|
|
2771
2870
|
bodyLines.push(` ${state.statusMessage}`);
|
|
2772
2871
|
}
|
|
2773
|
-
const footerHeight = 3;
|
|
2774
|
-
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
2775
2872
|
if (bodyLines.length > targetBodyHeight) {
|
|
2776
2873
|
bodyLines.length = targetBodyHeight;
|
|
2777
2874
|
}
|
|
@@ -2831,19 +2928,30 @@ function checkAlerts(data) {
|
|
|
2831
2928
|
const anomalies = detectAnomalies(data, state.history);
|
|
2832
2929
|
const hasCriticalAnomaly = anomalies.some((a) => a.severity === "critical");
|
|
2833
2930
|
const hasWarningAnomaly = anomalies.some((a) => a.severity === "warning");
|
|
2931
|
+
let watchdogTriggered = false;
|
|
2932
|
+
if (state.watchdogProcess) {
|
|
2933
|
+
const matches = data.processes.filter((p) => p.command.toLowerCase().includes(state.watchdogProcess.toLowerCase()));
|
|
2934
|
+
for (const p of matches) {
|
|
2935
|
+
if (p.cpu >= state.watchdogCpu || p.mem >= state.watchdogMem) {
|
|
2936
|
+
watchdogTriggered = true;
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2834
2941
|
const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
|
|
2835
|
-
if ((hot || anomalyTriggered) && !state.alerted) {
|
|
2942
|
+
if ((hot || anomalyTriggered || watchdogTriggered) && !state.alerted) {
|
|
2836
2943
|
state.alerted = true;
|
|
2837
2944
|
process.stdout.write("\x07");
|
|
2838
2945
|
const reasons = [];
|
|
2839
2946
|
if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
|
|
2840
2947
|
if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
|
|
2948
|
+
if (watchdogTriggered) reasons.push(`Watchdog process "${state.watchdogProcess}" crossed threshold`);
|
|
2841
2949
|
for (const a of anomalies) {
|
|
2842
2950
|
reasons.push(`${a.metric} anomaly (\u03C3=${a.zScore.toFixed(1)})`);
|
|
2843
2951
|
}
|
|
2844
2952
|
setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
|
|
2845
2953
|
sendNotification("pyre Alert", reasons.join("; "));
|
|
2846
|
-
} else if (!hot && !anomalyTriggered) {
|
|
2954
|
+
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
2847
2955
|
state.alerted = false;
|
|
2848
2956
|
}
|
|
2849
2957
|
}
|
|
@@ -2851,7 +2959,7 @@ function checkAlerts(data) {
|
|
|
2851
2959
|
// src/p2p/server.ts
|
|
2852
2960
|
import net from "net";
|
|
2853
2961
|
import tls from "tls";
|
|
2854
|
-
import
|
|
2962
|
+
import fs4 from "fs";
|
|
2855
2963
|
import crypto2 from "crypto";
|
|
2856
2964
|
import chalk6 from "chalk";
|
|
2857
2965
|
|
|
@@ -2996,9 +3104,9 @@ var P2PServer = class {
|
|
|
2996
3104
|
async start() {
|
|
2997
3105
|
if (this.options.auditLog) {
|
|
2998
3106
|
try {
|
|
2999
|
-
|
|
3107
|
+
fs4.mkdirSync(this.options.auditLog, { recursive: true });
|
|
3000
3108
|
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
3001
|
-
this.auditStream =
|
|
3109
|
+
this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
|
|
3002
3110
|
} catch {
|
|
3003
3111
|
this.log(chalk6.yellow("Warning: could not open audit log"));
|
|
3004
3112
|
}
|
|
@@ -3007,11 +3115,11 @@ var P2PServer = class {
|
|
|
3007
3115
|
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
3008
3116
|
if (useTLS) {
|
|
3009
3117
|
const tlsOptions = {
|
|
3010
|
-
key:
|
|
3011
|
-
cert:
|
|
3118
|
+
key: fs4.readFileSync(this.options.tlsKey),
|
|
3119
|
+
cert: fs4.readFileSync(this.options.tlsCert)
|
|
3012
3120
|
};
|
|
3013
3121
|
if (this.options.tlsCA) {
|
|
3014
|
-
tlsOptions.ca =
|
|
3122
|
+
tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
|
|
3015
3123
|
}
|
|
3016
3124
|
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
3017
3125
|
} else {
|
|
@@ -3279,7 +3387,7 @@ async function startP2PServer(options) {
|
|
|
3279
3387
|
// src/p2p/client.ts
|
|
3280
3388
|
import net2 from "net";
|
|
3281
3389
|
import tls2 from "tls";
|
|
3282
|
-
import
|
|
3390
|
+
import fs5 from "fs";
|
|
3283
3391
|
import crypto3 from "crypto";
|
|
3284
3392
|
import chalk7 from "chalk";
|
|
3285
3393
|
var P2PClient = class {
|
|
@@ -3307,9 +3415,9 @@ var P2PClient = class {
|
|
|
3307
3415
|
async connect() {
|
|
3308
3416
|
if (this.options.auditLog) {
|
|
3309
3417
|
try {
|
|
3310
|
-
|
|
3418
|
+
fs5.mkdirSync(this.options.auditLog, { recursive: true });
|
|
3311
3419
|
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
3312
|
-
this.auditStream =
|
|
3420
|
+
this.auditStream = fs5.createWriteStream(logFile, { flags: "a" });
|
|
3313
3421
|
} catch {
|
|
3314
3422
|
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
3315
3423
|
}
|
|
@@ -3324,7 +3432,7 @@ var P2PClient = class {
|
|
|
3324
3432
|
rejectUnauthorized: !this.options.tlsInsecure
|
|
3325
3433
|
};
|
|
3326
3434
|
if (this.options.tlsCA) {
|
|
3327
|
-
tlsOptions.ca =
|
|
3435
|
+
tlsOptions.ca = fs5.readFileSync(this.options.tlsCA);
|
|
3328
3436
|
}
|
|
3329
3437
|
this.socket = tls2.connect(tlsOptions, () => {
|
|
3330
3438
|
this.running = true;
|
|
@@ -3678,11 +3786,19 @@ function handleInputModeKey(str, key) {
|
|
|
3678
3786
|
}
|
|
3679
3787
|
if (key.name === "backspace") {
|
|
3680
3788
|
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
3789
|
+
if (state.inputMode === "filter") {
|
|
3790
|
+
state.processFilter = state.inputBuffer;
|
|
3791
|
+
invalidateTableCache();
|
|
3792
|
+
}
|
|
3681
3793
|
render();
|
|
3682
3794
|
return;
|
|
3683
3795
|
}
|
|
3684
3796
|
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
3685
3797
|
state.inputBuffer += str;
|
|
3798
|
+
if (state.inputMode === "filter") {
|
|
3799
|
+
state.processFilter = state.inputBuffer;
|
|
3800
|
+
invalidateTableCache();
|
|
3801
|
+
}
|
|
3686
3802
|
render();
|
|
3687
3803
|
}
|
|
3688
3804
|
}
|
|
@@ -4439,11 +4555,106 @@ async function showSplash(config3) {
|
|
|
4439
4555
|
});
|
|
4440
4556
|
}
|
|
4441
4557
|
|
|
4558
|
+
// src/historyCmd.ts
|
|
4559
|
+
import fs6 from "fs";
|
|
4560
|
+
import path4 from "path";
|
|
4561
|
+
import chalk9 from "chalk";
|
|
4562
|
+
function runHistoryCommand(opts2 = {}) {
|
|
4563
|
+
const days = opts2.days || 7;
|
|
4564
|
+
const dir = opts2.dir || "./pyre-exports";
|
|
4565
|
+
console.log(chalk9.bold(`
|
|
4566
|
+
pyre history \u2014 Past ${days} Days Trend
|
|
4567
|
+
`));
|
|
4568
|
+
if (!fs6.existsSync(dir)) {
|
|
4569
|
+
console.log(chalk9.yellow(` No logs directory found at ${dir}`));
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
const files = fs6.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
|
|
4573
|
+
if (files.length === 0) {
|
|
4574
|
+
console.log(chalk9.yellow(` No CSV logs found in ${dir}`));
|
|
4575
|
+
return;
|
|
4576
|
+
}
|
|
4577
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
4578
|
+
const cpuPoints = [];
|
|
4579
|
+
const memPoints = [];
|
|
4580
|
+
const tempPoints = [];
|
|
4581
|
+
for (const file of files.sort()) {
|
|
4582
|
+
const fullPath = path4.join(dir, file);
|
|
4583
|
+
try {
|
|
4584
|
+
const content = fs6.readFileSync(fullPath, "utf-8");
|
|
4585
|
+
const lines = content.split("\n").slice(1);
|
|
4586
|
+
for (const line of lines) {
|
|
4587
|
+
if (!line.trim()) continue;
|
|
4588
|
+
const parts = line.split(",");
|
|
4589
|
+
if (parts.length >= 4) {
|
|
4590
|
+
const tsStr = parts[0];
|
|
4591
|
+
const ts = new Date(tsStr).getTime();
|
|
4592
|
+
if (!isNaN(ts) && ts >= cutoff) {
|
|
4593
|
+
const cpu = parseFloat(parts[1]);
|
|
4594
|
+
const mem = parseFloat(parts[2]);
|
|
4595
|
+
const temp = parseFloat(parts[3]);
|
|
4596
|
+
if (!isNaN(cpu)) cpuPoints.push(cpu);
|
|
4597
|
+
if (!isNaN(mem)) memPoints.push(mem);
|
|
4598
|
+
if (!isNaN(temp)) tempPoints.push(temp);
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
} catch {
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
if (cpuPoints.length === 0) {
|
|
4606
|
+
console.log(chalk9.yellow(` No historical entries recorded within the last ${days} days.`));
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
console.log(` Data Points: ${cpuPoints.length} logged snapshots`);
|
|
4610
|
+
console.log(` CPU Usage: ${sparkline(cpuPoints, { min: 0, max: 100 })} (${(cpuPoints.reduce((a, b) => a + b, 0) / cpuPoints.length).toFixed(1)}% avg)`);
|
|
4611
|
+
console.log(` Memory: ${sparkline(memPoints, { min: 0, max: 100 })} (${(memPoints.reduce((a, b) => a + b, 0) / memPoints.length).toFixed(1)}% avg)`);
|
|
4612
|
+
if (tempPoints.length) {
|
|
4613
|
+
console.log(` Temperature: ${sparkline(tempPoints)} (${(tempPoints.reduce((a, b) => a + b, 0) / tempPoints.length).toFixed(1)}\xB0C avg)`);
|
|
4614
|
+
}
|
|
4615
|
+
console.log();
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
// src/diffCmd.ts
|
|
4619
|
+
import fs7 from "fs";
|
|
4620
|
+
import chalk10 from "chalk";
|
|
4621
|
+
function runDiffCommand(file1, file2) {
|
|
4622
|
+
console.log(chalk10.bold(`
|
|
4623
|
+
pyre diff \u2014 Snapshot Comparison
|
|
4624
|
+
`));
|
|
4625
|
+
console.log(chalk10.dim(` Snapshot A: ${file1}`));
|
|
4626
|
+
console.log(chalk10.dim(` Snapshot B: ${file2}
|
|
4627
|
+
`));
|
|
4628
|
+
try {
|
|
4629
|
+
const dataA = JSON.parse(fs7.readFileSync(file1, "utf-8"));
|
|
4630
|
+
const dataB = JSON.parse(fs7.readFileSync(file2, "utf-8"));
|
|
4631
|
+
const diffNum = (label, valA, valB, unit = "") => {
|
|
4632
|
+
const delta = valB - valA;
|
|
4633
|
+
const sign = delta > 0 ? "+" : "";
|
|
4634
|
+
const color = delta > 0 ? chalk10.red : delta < 0 ? chalk10.green : chalk10.dim;
|
|
4635
|
+
console.log(` ${label.padEnd(20)} ${valA.toFixed(1)}${unit} \u2794 ${valB.toFixed(1)}${unit} (${color(sign + delta.toFixed(1) + unit)})`);
|
|
4636
|
+
};
|
|
4637
|
+
diffNum("CPU Usage", dataA.cpu.usage, dataB.cpu.usage, "%");
|
|
4638
|
+
diffNum("Memory Usage", dataA.memory.usagePercent, dataB.memory.usagePercent, "%");
|
|
4639
|
+
if (dataA.cpu.temperature && dataB.cpu.temperature) {
|
|
4640
|
+
diffNum("CPU Temp", dataA.cpu.temperature, dataB.cpu.temperature, "\xB0C");
|
|
4641
|
+
}
|
|
4642
|
+
if (dataA.gpu && dataB.gpu) {
|
|
4643
|
+
diffNum("GPU Util", dataA.gpu.utilization, dataB.gpu.utilization, "%");
|
|
4644
|
+
}
|
|
4645
|
+
diffNum("Network RX Packets", dataA.network.rxPackets, dataB.network.rxPackets);
|
|
4646
|
+
diffNum("Network TX Packets", dataA.network.txPackets, dataB.network.txPackets);
|
|
4647
|
+
console.log();
|
|
4648
|
+
} catch (err) {
|
|
4649
|
+
console.log(chalk10.red(`Failed to compare snapshots: ${err.message}`));
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4442
4653
|
// src/index.ts
|
|
4443
4654
|
var config2 = readConfig();
|
|
4444
|
-
var pkg = JSON.parse(
|
|
4655
|
+
var pkg = JSON.parse(fs8.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
4445
4656
|
var program = new Command();
|
|
4446
|
-
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("--port <port>", "Port number for web server mode", "3000");
|
|
4657
|
+
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("--port <port>", "Port number for web server mode", "3000").option("--web-auth", "Enable Basic Authentication for web server").option("--web-user <user>", "Username for web server authentication", "admin").option("--web-pass <pass>", "Password for web server authentication", "pyre");
|
|
4447
4658
|
program.parse(process.argv);
|
|
4448
4659
|
var opts = program.opts();
|
|
4449
4660
|
function isExportMode() {
|
|
@@ -4456,7 +4667,7 @@ function sanitizeHost2(host) {
|
|
|
4456
4667
|
return host.trim().replace(/%$/, "");
|
|
4457
4668
|
}
|
|
4458
4669
|
function detectLocalIP() {
|
|
4459
|
-
const ifaces =
|
|
4670
|
+
const ifaces = os4.networkInterfaces();
|
|
4460
4671
|
for (const [, addrs] of Object.entries(ifaces)) {
|
|
4461
4672
|
if (!addrs) continue;
|
|
4462
4673
|
for (const addr of addrs) {
|
|
@@ -4470,34 +4681,46 @@ function detectLocalIP() {
|
|
|
4470
4681
|
async function runServerCommand() {
|
|
4471
4682
|
const ip = detectLocalIP();
|
|
4472
4683
|
if (!ip) {
|
|
4473
|
-
console.log(
|
|
4684
|
+
console.log(chalk11.red("Could not detect a non-loopback IP address. Connect to a network first."));
|
|
4474
4685
|
process.exit(1);
|
|
4475
4686
|
}
|
|
4476
4687
|
const port = 9876;
|
|
4477
4688
|
const password = "mysecret";
|
|
4478
|
-
console.log(
|
|
4689
|
+
console.log(chalk11.bold(`
|
|
4479
4690
|
pyre server`));
|
|
4480
|
-
console.log(
|
|
4481
|
-
console.log(
|
|
4482
|
-
console.log(
|
|
4691
|
+
console.log(chalk11.dim(` Detected IP: ${ip}`));
|
|
4692
|
+
console.log(chalk11.dim(` Port: ${port}`));
|
|
4693
|
+
console.log(chalk11.dim(` Password: ${password}`));
|
|
4483
4694
|
console.log();
|
|
4484
|
-
console.log(
|
|
4485
|
-
console.log(
|
|
4695
|
+
console.log(chalk11.bold("Run this on THIS machine (server):"));
|
|
4696
|
+
console.log(chalk11.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
|
|
4486
4697
|
console.log();
|
|
4487
|
-
console.log(
|
|
4488
|
-
console.log(
|
|
4698
|
+
console.log(chalk11.bold("Run this on the OTHER machine (client):"));
|
|
4699
|
+
console.log(chalk11.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
|
|
4489
4700
|
console.log();
|
|
4490
4701
|
}
|
|
4491
4702
|
async function main() {
|
|
4492
4703
|
const cmd = program.args[0];
|
|
4493
|
-
if (cmd === "
|
|
4494
|
-
|
|
4704
|
+
if (cmd === "history") {
|
|
4705
|
+
const daysIdx = program.args.indexOf("--days");
|
|
4706
|
+
const days = daysIdx !== -1 ? parseInt(program.args[daysIdx + 1], 10) || 7 : 7;
|
|
4707
|
+
runHistoryCommand({ days });
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
if (cmd === "diff") {
|
|
4711
|
+
const file1 = program.args[1];
|
|
4712
|
+
const file2 = program.args[2];
|
|
4713
|
+
if (!file1 || !file2) {
|
|
4714
|
+
console.log(chalk11.red("Usage: pyre diff <snapshot1.json> <snapshot2.json>"));
|
|
4715
|
+
process.exit(1);
|
|
4716
|
+
}
|
|
4717
|
+
runDiffCommand(file1, file2);
|
|
4495
4718
|
return;
|
|
4496
4719
|
}
|
|
4497
4720
|
if (cmd === "ssh") {
|
|
4498
4721
|
const host = program.args[1];
|
|
4499
4722
|
if (!host) {
|
|
4500
|
-
console.log(
|
|
4723
|
+
console.log(chalk11.red("Usage: pyre ssh <host>"));
|
|
4501
4724
|
process.exit(1);
|
|
4502
4725
|
}
|
|
4503
4726
|
await runSshCommand(host);
|
|
@@ -4510,7 +4733,7 @@ async function main() {
|
|
|
4510
4733
|
if (cmd === "bench") {
|
|
4511
4734
|
const benchCmd = program.args.slice(1).join(" ");
|
|
4512
4735
|
if (!benchCmd) {
|
|
4513
|
-
console.log(
|
|
4736
|
+
console.log(chalk11.red("Usage: pyre bench <command>"));
|
|
4514
4737
|
process.exit(1);
|
|
4515
4738
|
}
|
|
4516
4739
|
await runBenchCommand(benchCmd);
|
|
@@ -4522,8 +4745,8 @@ async function main() {
|
|
|
4522
4745
|
const cfg = readConfig();
|
|
4523
4746
|
console.log(JSON.stringify(cfg, null, 2));
|
|
4524
4747
|
} else if (sub === "reset") {
|
|
4525
|
-
|
|
4526
|
-
console.log(
|
|
4748
|
+
fs8.rmSync(CONFIG_FILE, { force: true });
|
|
4749
|
+
console.log(chalk11.green("Config reset to defaults"));
|
|
4527
4750
|
} else {
|
|
4528
4751
|
console.log("Usage: pyre config <show|reset>");
|
|
4529
4752
|
}
|
|
@@ -4571,7 +4794,7 @@ async function main() {
|
|
|
4571
4794
|
await runP2PConnect();
|
|
4572
4795
|
return;
|
|
4573
4796
|
} else {
|
|
4574
|
-
console.log(
|
|
4797
|
+
console.log(chalk11.red("Usage: pyre p2p <server|connect> [options]"));
|
|
4575
4798
|
process.exit(1);
|
|
4576
4799
|
}
|
|
4577
4800
|
}
|
|
@@ -4587,13 +4810,13 @@ async function main() {
|
|
|
4587
4810
|
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config2.treeView, visible: { packets: opts.packets ? true : void 0 } });
|
|
4588
4811
|
console.log(output);
|
|
4589
4812
|
if (opts.out) {
|
|
4590
|
-
|
|
4591
|
-
console.log(
|
|
4813
|
+
fs8.writeFileSync(opts.out, stripAnsi(output));
|
|
4814
|
+
console.log(chalk11.dim(`
|
|
4592
4815
|
Saved to ${opts.out}`));
|
|
4593
4816
|
}
|
|
4594
4817
|
}
|
|
4595
4818
|
main().catch((err) => {
|
|
4596
|
-
console.error(
|
|
4819
|
+
console.error(chalk11.red(`Error: ${err.message}`));
|
|
4597
4820
|
process.exit(1);
|
|
4598
4821
|
});
|
|
4599
4822
|
async function runP2PServer() {
|
|
@@ -4609,15 +4832,15 @@ async function runP2PServer() {
|
|
|
4609
4832
|
const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4610
4833
|
const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4611
4834
|
if (!password) {
|
|
4612
|
-
console.log(
|
|
4835
|
+
console.log(chalk11.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
|
|
4613
4836
|
process.exit(1);
|
|
4614
4837
|
}
|
|
4615
4838
|
if (tlsCert && !tlsKey) {
|
|
4616
|
-
console.log(
|
|
4839
|
+
console.log(chalk11.red("Error: --p2p-key is required when --p2p-cert is specified"));
|
|
4617
4840
|
process.exit(1);
|
|
4618
4841
|
}
|
|
4619
4842
|
if (tlsKey && !tlsCert) {
|
|
4620
|
-
console.log(
|
|
4843
|
+
console.log(chalk11.red("Error: --p2p-cert is required when --p2p-key is specified"));
|
|
4621
4844
|
process.exit(1);
|
|
4622
4845
|
}
|
|
4623
4846
|
const server = new P2PServer({
|
|
@@ -4637,18 +4860,18 @@ async function runP2PServer() {
|
|
|
4637
4860
|
hmacKey: opts.p2pHmacKey
|
|
4638
4861
|
});
|
|
4639
4862
|
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
4640
|
-
console.log(
|
|
4863
|
+
console.log(chalk11.bold(`
|
|
4641
4864
|
pyre P2P Server`));
|
|
4642
|
-
console.log(
|
|
4643
|
-
console.log(
|
|
4644
|
-
console.log(
|
|
4645
|
-
console.log(
|
|
4646
|
-
console.log(
|
|
4647
|
-
console.log(
|
|
4648
|
-
console.log(
|
|
4649
|
-
if (allowList.length > 0) console.log(
|
|
4650
|
-
if (denyList.length > 0) console.log(
|
|
4651
|
-
console.log(
|
|
4865
|
+
console.log(chalk11.dim(` Host: ${host}`));
|
|
4866
|
+
console.log(chalk11.dim(` Port: ${port}`));
|
|
4867
|
+
console.log(chalk11.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
|
|
4868
|
+
console.log(chalk11.dim(` Password: ${password}`));
|
|
4869
|
+
console.log(chalk11.dim(` Interval: ${interval}s`));
|
|
4870
|
+
console.log(chalk11.dim(` Detailed: ${detailed ? "on" : "off"}`));
|
|
4871
|
+
console.log(chalk11.dim(` Rate limit: ${rateLimit} attempts/min`));
|
|
4872
|
+
if (allowList.length > 0) console.log(chalk11.dim(` Allowed IPs: ${allowList.join(", ")}`));
|
|
4873
|
+
if (denyList.length > 0) console.log(chalk11.dim(` Denied IPs: ${denyList.join(", ")}`));
|
|
4874
|
+
console.log(chalk11.dim(` Waiting for peers...
|
|
4652
4875
|
`));
|
|
4653
4876
|
await server.start();
|
|
4654
4877
|
const rl = readline2.createInterface({ input: process.stdin });
|
|
@@ -4660,7 +4883,7 @@ async function runP2PServer() {
|
|
|
4660
4883
|
process.exit(0);
|
|
4661
4884
|
} else if (trimmed === "status") {
|
|
4662
4885
|
const info = server.info;
|
|
4663
|
-
console.log(
|
|
4886
|
+
console.log(chalk11.dim(` Peers connected: ${info.peersConnected}`));
|
|
4664
4887
|
}
|
|
4665
4888
|
});
|
|
4666
4889
|
process.once("SIGINT", () => {
|
|
@@ -4693,7 +4916,7 @@ async function runP2PConnect() {
|
|
|
4693
4916
|
const tlsCA = opts.p2pCA;
|
|
4694
4917
|
const tlsInsecure = !!opts.p2pInsecure;
|
|
4695
4918
|
if (!password) {
|
|
4696
|
-
console.log(
|
|
4919
|
+
console.log(chalk11.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
|
|
4697
4920
|
process.exit(1);
|
|
4698
4921
|
}
|
|
4699
4922
|
const client = new P2PClient({
|
|
@@ -4707,12 +4930,12 @@ async function runP2PConnect() {
|
|
|
4707
4930
|
hmacKey: opts.p2pHmacKey
|
|
4708
4931
|
});
|
|
4709
4932
|
client.setOnStatus((msg) => {
|
|
4710
|
-
process.stdout.write(`\r${
|
|
4933
|
+
process.stdout.write(`\r${chalk11.dim(msg)}${" ".repeat(40)}\r`);
|
|
4711
4934
|
});
|
|
4712
4935
|
client.setOnData((data) => {
|
|
4713
4936
|
const lines = [];
|
|
4714
|
-
lines.push(
|
|
4715
|
-
lines.push(
|
|
4937
|
+
lines.push(chalk11.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
|
|
4938
|
+
lines.push(chalk11.dim(` ${data.timestamp}`));
|
|
4716
4939
|
lines.push("");
|
|
4717
4940
|
lines.push(...formatTable(data, {
|
|
4718
4941
|
width: process.stdout.columns || 80,
|
|
@@ -4729,11 +4952,11 @@ async function runP2PConnect() {
|
|
|
4729
4952
|
writeFrame2(lines);
|
|
4730
4953
|
});
|
|
4731
4954
|
const protocolLabel = useTLS ? "TLS" : "TCP";
|
|
4732
|
-
console.log(
|
|
4955
|
+
console.log(chalk11.bold(`
|
|
4733
4956
|
pyre P2P Client`));
|
|
4734
|
-
console.log(
|
|
4735
|
-
console.log(
|
|
4736
|
-
console.log(
|
|
4957
|
+
console.log(chalk11.dim(` Protocol: ${protocolLabel}`));
|
|
4958
|
+
console.log(chalk11.dim(` Connecting to ${host}:${port}...`));
|
|
4959
|
+
console.log(chalk11.dim(` Press Ctrl+C to disconnect
|
|
4737
4960
|
`));
|
|
4738
4961
|
process.stdout.write("\x1B[?25l");
|
|
4739
4962
|
try {
|
|
@@ -4741,23 +4964,23 @@ async function runP2PConnect() {
|
|
|
4741
4964
|
} catch (err) {
|
|
4742
4965
|
const code = err.code;
|
|
4743
4966
|
const msg = err.message || String(err);
|
|
4744
|
-
console.log(
|
|
4967
|
+
console.log(chalk11.red(`Connection failed: ${msg}`));
|
|
4745
4968
|
if (code === "ECONNREFUSED") {
|
|
4746
|
-
console.log(
|
|
4747
|
-
console.log(
|
|
4748
|
-
console.log(
|
|
4749
|
-
console.log(
|
|
4969
|
+
console.log(chalk11.yellow(" Hints:"));
|
|
4970
|
+
console.log(chalk11.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
|
|
4971
|
+
console.log(chalk11.dim(" 2. Check the server IP with: hostname -I"));
|
|
4972
|
+
console.log(chalk11.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
|
|
4750
4973
|
} else if (code === "ETIMEDOUT") {
|
|
4751
|
-
console.log(
|
|
4752
|
-
console.log(
|
|
4753
|
-
console.log(
|
|
4754
|
-
console.log(
|
|
4974
|
+
console.log(chalk11.yellow(" Hints:"));
|
|
4975
|
+
console.log(chalk11.dim(" 1. Verify the server IP is correct"));
|
|
4976
|
+
console.log(chalk11.dim(" 2. Check that both machines are on the same network/subnet"));
|
|
4977
|
+
console.log(chalk11.dim(" 3. Open the port on the server firewall"));
|
|
4755
4978
|
} else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
|
|
4756
|
-
console.log(
|
|
4757
|
-
console.log(
|
|
4758
|
-
console.log(
|
|
4759
|
-
console.log(
|
|
4760
|
-
console.log(
|
|
4979
|
+
console.log(chalk11.yellow(" Hints:"));
|
|
4980
|
+
console.log(chalk11.dim(" 1. The server at " + host + " is not responding"));
|
|
4981
|
+
console.log(chalk11.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
|
|
4982
|
+
console.log(chalk11.dim(" 3. Check connectivity: ping " + host));
|
|
4983
|
+
console.log(chalk11.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
|
|
4761
4984
|
}
|
|
4762
4985
|
process.stdout.write("\x1B[?25h");
|
|
4763
4986
|
process.exit(1);
|
|
@@ -4769,41 +4992,21 @@ async function runP2PConnect() {
|
|
|
4769
4992
|
process.exit(0);
|
|
4770
4993
|
});
|
|
4771
4994
|
}
|
|
4772
|
-
async function runInfoCommand() {
|
|
4773
|
-
const data = await collectAll({ detailed: true });
|
|
4774
|
-
const lines = [];
|
|
4775
|
-
lines.push(chalk9.bold(" pyre info"));
|
|
4776
|
-
lines.push("");
|
|
4777
|
-
lines.push(` Hostname: ${data.header.hostname}`);
|
|
4778
|
-
lines.push(` OS: ${data.header.os}`);
|
|
4779
|
-
lines.push(` Uptime: ${data.header.uptime}`);
|
|
4780
|
-
lines.push(` CPU: ${data.cpu.brand}`);
|
|
4781
|
-
lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
|
|
4782
|
-
lines.push(` Frequency: ${data.cpu.frequency} MHz`);
|
|
4783
|
-
lines.push(` Memory: ${formatBytes(data.memory.total)}`);
|
|
4784
|
-
if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
|
|
4785
|
-
if (data.battery) {
|
|
4786
|
-
lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
|
|
4787
|
-
lines.push(` Health: ${data.battery.health}`);
|
|
4788
|
-
}
|
|
4789
|
-
lines.push(` Thermal: ${data.thermal.state}`);
|
|
4790
|
-
console.log(lines.join("\n"));
|
|
4791
|
-
}
|
|
4792
4995
|
async function runSshCommand(host) {
|
|
4793
4996
|
const password = opts.p2pPassword || "mysecret";
|
|
4794
4997
|
const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
|
|
4795
|
-
console.log(
|
|
4998
|
+
console.log(chalk11.bold(`
|
|
4796
4999
|
pyre ssh \u2014 ${host}`));
|
|
4797
|
-
console.log(
|
|
5000
|
+
console.log(chalk11.dim(` Running: ${cmd}
|
|
4798
5001
|
`));
|
|
4799
5002
|
const { execFile: execFile2 } = await import("child_process");
|
|
4800
5003
|
const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
|
|
4801
5004
|
if (err) {
|
|
4802
|
-
console.log(
|
|
4803
|
-
console.log(
|
|
5005
|
+
console.log(chalk11.red(` SSH failed: ${err.message}`));
|
|
5006
|
+
console.log(chalk11.dim(" Ensure SSH keys are set up or the host is reachable."));
|
|
4804
5007
|
process.exit(1);
|
|
4805
5008
|
}
|
|
4806
|
-
if (stderr) console.log(
|
|
5009
|
+
if (stderr) console.log(chalk11.dim(stderr));
|
|
4807
5010
|
try {
|
|
4808
5011
|
const remote = JSON.parse(stdout);
|
|
4809
5012
|
console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
|
|
@@ -4821,14 +5024,66 @@ async function runSshCommand(host) {
|
|
|
4821
5024
|
async function runWebCommand() {
|
|
4822
5025
|
const http = await import("http");
|
|
4823
5026
|
const port = parseInt(opts.port || process.env.PORT || "3000", 10) || 3e3;
|
|
5027
|
+
const authEnabled = !!opts.webAuth;
|
|
5028
|
+
const authUser = opts.webUser || "admin";
|
|
5029
|
+
const authPass = opts.webPass || "pyre";
|
|
4824
5030
|
const server = http.createServer(async (req, res) => {
|
|
5031
|
+
if (authEnabled) {
|
|
5032
|
+
const authHeader = req.headers.authorization;
|
|
5033
|
+
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
5034
|
+
res.writeHead(401, {
|
|
5035
|
+
"WWW-Authenticate": 'Basic realm="pyre Web Dashboard"',
|
|
5036
|
+
"Content-Type": "text/plain"
|
|
5037
|
+
});
|
|
5038
|
+
res.end("401 Unauthorized");
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
const credentials = Buffer.from(authHeader.split(" ")[1], "base64").toString("utf-8");
|
|
5042
|
+
const [user, pass] = credentials.split(":");
|
|
5043
|
+
if (user !== authUser || pass !== authPass) {
|
|
5044
|
+
res.writeHead(401, {
|
|
5045
|
+
"WWW-Authenticate": 'Basic realm="pyre Web Dashboard"',
|
|
5046
|
+
"Content-Type": "text/plain"
|
|
5047
|
+
});
|
|
5048
|
+
res.end("401 Unauthorized");
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
4825
5052
|
const url = req.url || "/";
|
|
5053
|
+
if (url === "/health" || url === "/api/health") {
|
|
5054
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5055
|
+
res.end(JSON.stringify({ status: "ok", uptime: process.uptime(), timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
5058
|
+
if (url === "/api/stream") {
|
|
5059
|
+
res.writeHead(200, {
|
|
5060
|
+
"Content-Type": "text/event-stream",
|
|
5061
|
+
"Cache-Control": "no-cache",
|
|
5062
|
+
"Connection": "keep-alive",
|
|
5063
|
+
"Access-Control-Allow-Origin": "*"
|
|
5064
|
+
});
|
|
5065
|
+
const sendStats = async () => {
|
|
5066
|
+
try {
|
|
5067
|
+
const data = await collectAll({ detailed: true });
|
|
5068
|
+
res.write(`data: ${JSON.stringify(data)}
|
|
5069
|
+
|
|
5070
|
+
`);
|
|
5071
|
+
} catch {
|
|
5072
|
+
}
|
|
5073
|
+
};
|
|
5074
|
+
sendStats();
|
|
5075
|
+
const intervalId = setInterval(sendStats, 2e3);
|
|
5076
|
+
req.on("close", () => {
|
|
5077
|
+
clearInterval(intervalId);
|
|
5078
|
+
});
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
4826
5081
|
if (url === "/" || url === "/index.html") {
|
|
4827
5082
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4828
|
-
const indexPath =
|
|
5083
|
+
const indexPath = path5.join(process.cwd(), "index.html");
|
|
4829
5084
|
let htmlContent = "";
|
|
4830
|
-
if (
|
|
4831
|
-
htmlContent =
|
|
5085
|
+
if (fs8.existsSync(indexPath)) {
|
|
5086
|
+
htmlContent = fs8.readFileSync(indexPath, "utf-8");
|
|
4832
5087
|
} else {
|
|
4833
5088
|
const data = await collectAll({ detailed: true });
|
|
4834
5089
|
htmlContent = formatHtml(data);
|
|
@@ -4848,11 +5103,15 @@ async function runWebCommand() {
|
|
|
4848
5103
|
}
|
|
4849
5104
|
});
|
|
4850
5105
|
server.listen(port, "0.0.0.0", () => {
|
|
4851
|
-
console.log(
|
|
5106
|
+
console.log(chalk11.bold(`
|
|
4852
5107
|
pyre web server running on port ${port}`));
|
|
4853
|
-
console.log(
|
|
4854
|
-
console.log(
|
|
4855
|
-
console.log(
|
|
5108
|
+
console.log(chalk11.dim(` Dashboard: http://0.0.0.0:${port}/`));
|
|
5109
|
+
console.log(chalk11.dim(` API: http://0.0.0.0:${port}/api`));
|
|
5110
|
+
console.log(chalk11.dim(` SSE Stream: http://0.0.0.0:${port}/api/stream`));
|
|
5111
|
+
if (authEnabled) {
|
|
5112
|
+
console.log(chalk11.yellow(` Authentication: Enabled (${authUser}:${authPass})`));
|
|
5113
|
+
}
|
|
5114
|
+
console.log(chalk11.dim(" Press Ctrl+C to stop\n"));
|
|
4856
5115
|
});
|
|
4857
5116
|
process.once("SIGINT", () => {
|
|
4858
5117
|
server.close();
|
|
@@ -4862,14 +5121,14 @@ async function runWebCommand() {
|
|
|
4862
5121
|
async function runBenchCommand(benchCmd) {
|
|
4863
5122
|
const { spawn } = await import("child_process");
|
|
4864
5123
|
const logDir = opts.exportDir || "./pyre-exports";
|
|
4865
|
-
const file =
|
|
4866
|
-
if (!
|
|
4867
|
-
const stream =
|
|
5124
|
+
const file = path5.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
|
|
5125
|
+
if (!fs8.existsSync(logDir)) fs8.mkdirSync(logDir, { recursive: true });
|
|
5126
|
+
const stream = fs8.createWriteStream(file, { flags: "a" });
|
|
4868
5127
|
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");
|
|
4869
|
-
console.log(
|
|
5128
|
+
console.log(chalk11.bold(`
|
|
4870
5129
|
pyre bench`));
|
|
4871
|
-
console.log(
|
|
4872
|
-
console.log(
|
|
5130
|
+
console.log(chalk11.dim(` Command: ${benchCmd}`));
|
|
5131
|
+
console.log(chalk11.dim(` Log: ${file}
|
|
4873
5132
|
`));
|
|
4874
5133
|
const child = spawn("sh", ["-c", benchCmd]);
|
|
4875
5134
|
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
@@ -4890,7 +5149,7 @@ async function runBenchCommand(benchCmd) {
|
|
|
4890
5149
|
child.on("close", () => {
|
|
4891
5150
|
clearInterval(handle);
|
|
4892
5151
|
stream.end();
|
|
4893
|
-
console.log(
|
|
5152
|
+
console.log(chalk11.green(`
|
|
4894
5153
|
Bench complete. Log saved to ${file}`));
|
|
4895
5154
|
process.exit(0);
|
|
4896
5155
|
});
|