pyre-cli 9.4.0 → 9.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checkCmd-EWFNLBWE.js +74 -0
- package/dist/{chunk-QOLZ7T32.js → chunk-HGRBBPYX.js} +65 -3
- package/dist/chunk-MOAEGMFP.js +1042 -0
- package/dist/chunk-SG26Y3WF.js +0 -0
- package/dist/{collectors-VACLPHRP.js → collectors-WD6VKH6M.js} +1 -1
- package/dist/index.js +343 -1271
- package/dist/pipeCmd-7THXEGDH.js +39 -0
- package/dist/prometheus-LFYHF6VO.js +56 -0
- package/dist/service-WTS3NBQU.js +73 -0
- package/dist/stressCmd-MFXMFXYX.js +51 -0
- package/dist/wizard-GVIAHIVV.js +55 -0
- package/package.json +1 -1
- package/src/checkCmd.ts +83 -0
- package/src/fleet.ts +17 -16
- package/src/formatters/render.ts +46 -9
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +77 -7
- package/src/monitors/collectors.ts +50 -3
- package/src/monitors/smc.ts +27 -0
- package/src/monitors/types.ts +10 -0
- package/src/pipeCmd.ts +42 -0
- package/src/prometheus.ts +74 -0
- package/src/service.ts +77 -0
- package/src/stressCmd.ts +59 -0
- package/src/wizard.ts +61 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import "./chunk-SG26Y3WF.js";
|
|
2
|
+
import {
|
|
3
|
+
collectAll
|
|
4
|
+
} from "./chunk-HGRBBPYX.js";
|
|
5
|
+
import "./chunk-FH5WDJBF.js";
|
|
6
|
+
|
|
7
|
+
// src/pipeCmd.ts
|
|
8
|
+
async function runPipeCommand(opts = {}) {
|
|
9
|
+
const intervalMs = opts.intervalMs ?? 1e3;
|
|
10
|
+
const maxSamples = opts.samples ?? 0;
|
|
11
|
+
let count = 0;
|
|
12
|
+
const emitSample = async () => {
|
|
13
|
+
try {
|
|
14
|
+
const data = await collectAll({ detailed: opts.detailed });
|
|
15
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
16
|
+
count++;
|
|
17
|
+
if (maxSamples > 0 && count >= maxSamples) {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
process.stderr.write(`Error collecting stats: ${err.message}
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
await emitSample();
|
|
26
|
+
if (maxSamples > 0 && count >= maxSamples) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const timer = setInterval(async () => {
|
|
30
|
+
await emitSample();
|
|
31
|
+
}, intervalMs);
|
|
32
|
+
process.once("SIGINT", () => {
|
|
33
|
+
clearInterval(timer);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
runPipeCommand
|
|
39
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/prometheus.ts
|
|
2
|
+
function formatPrometheusMetrics(data) {
|
|
3
|
+
const lines = [];
|
|
4
|
+
const addGauge = (name, help, val, labels = {}) => {
|
|
5
|
+
if (val === void 0 || Number.isNaN(val)) return;
|
|
6
|
+
lines.push(`# HELP ${name} ${help}`);
|
|
7
|
+
lines.push(`# TYPE ${name} gauge`);
|
|
8
|
+
const labelStr = Object.entries(labels).map(([k, v]) => `${k}="${v}"`).join(",");
|
|
9
|
+
const labelSuffix = labelStr ? `{${labelStr}}` : "";
|
|
10
|
+
lines.push(`${name}${labelSuffix} ${val}`);
|
|
11
|
+
};
|
|
12
|
+
addGauge("pyre_cpu_usage_percent", "CPU usage percentage", data.cpu.usage);
|
|
13
|
+
addGauge("pyre_cpu_frequency_mhz", "CPU frequency in MHz", data.cpu.frequency);
|
|
14
|
+
addGauge("pyre_cpu_cores_physical", "Physical CPU core count", data.cpu.physicalCores);
|
|
15
|
+
addGauge("pyre_cpu_cores_logical", "Logical CPU core count", data.cpu.cores);
|
|
16
|
+
if (data.cpu.loadAvg && data.cpu.loadAvg.length >= 3) {
|
|
17
|
+
addGauge("pyre_cpu_load_1m", "1-minute load average", data.cpu.loadAvg[0]);
|
|
18
|
+
addGauge("pyre_cpu_load_5m", "5-minute load average", data.cpu.loadAvg[1]);
|
|
19
|
+
addGauge("pyre_cpu_load_15m", "15-minute load average", data.cpu.loadAvg[2]);
|
|
20
|
+
}
|
|
21
|
+
if (data.cpu.temperature) {
|
|
22
|
+
addGauge("pyre_cpu_temperature_celsius", "CPU die temperature in Celsius", data.cpu.temperature);
|
|
23
|
+
}
|
|
24
|
+
addGauge("pyre_memory_total_bytes", "Total RAM in bytes", data.memory.total);
|
|
25
|
+
addGauge("pyre_memory_used_bytes", "Used RAM in bytes", data.memory.used);
|
|
26
|
+
addGauge("pyre_memory_free_bytes", "Free RAM in bytes", data.memory.free);
|
|
27
|
+
addGauge("pyre_memory_usage_percent", "Memory usage percentage", data.memory.usagePercent);
|
|
28
|
+
if (data.memory.swapTotal) {
|
|
29
|
+
addGauge("pyre_memory_swap_total_bytes", "Total swap memory in bytes", data.memory.swapTotal);
|
|
30
|
+
addGauge("pyre_memory_swap_used_bytes", "Used swap memory in bytes", data.memory.swapUsed);
|
|
31
|
+
}
|
|
32
|
+
if (data.gpu) {
|
|
33
|
+
addGauge("pyre_gpu_utilization_percent", "GPU utilization percentage", data.gpu.utilization, { model: data.gpu.model });
|
|
34
|
+
addGauge("pyre_gpu_memory_bytes", "GPU memory size in bytes", data.gpu.memory, { model: data.gpu.model });
|
|
35
|
+
if (data.gpu.temperature) {
|
|
36
|
+
addGauge("pyre_gpu_temperature_celsius", "GPU temperature in Celsius", data.gpu.temperature, { model: data.gpu.model });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (data.power) {
|
|
40
|
+
if (data.power.cpuWatts) addGauge("pyre_power_cpu_watts", "CPU power draw in Watts", data.power.cpuWatts);
|
|
41
|
+
if (data.power.gpuWatts) addGauge("pyre_power_gpu_watts", "GPU power draw in Watts", data.power.gpuWatts);
|
|
42
|
+
if (data.power.combinedWatts) addGauge("pyre_power_system_watts", "Combined system power draw in Watts", data.power.combinedWatts);
|
|
43
|
+
}
|
|
44
|
+
if (data.battery) {
|
|
45
|
+
addGauge("pyre_battery_level_percent", "Battery level percentage", data.battery.level);
|
|
46
|
+
if (data.battery.cycles) addGauge("pyre_battery_cycle_count", "Battery cycle count", data.battery.cycles);
|
|
47
|
+
if (data.battery.maxCapacityPercent) addGauge("pyre_battery_max_capacity_percent", "Battery health maximum capacity percentage", data.battery.maxCapacityPercent);
|
|
48
|
+
}
|
|
49
|
+
addGauge("pyre_thermal_pressure_level", "Thermal pressure level (0=Nominal, 1=Fair, 2=Serious, 3=Critical)", data.thermal.pressureLevel);
|
|
50
|
+
addGauge("pyre_network_rx_bytes_total", "Total network received bytes", data.network.rxBytes);
|
|
51
|
+
addGauge("pyre_network_tx_bytes_total", "Total network transmitted bytes", data.network.txBytes);
|
|
52
|
+
return lines.join("\n") + "\n";
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
formatPrometheusMetrics
|
|
56
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/service.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
var PLIST_LABEL = "com.pyre.web";
|
|
8
|
+
var PLIST_PATH = path.join(os.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
9
|
+
function installLaunchdAgent(port = 3e3) {
|
|
10
|
+
const nodePath = process.execPath;
|
|
11
|
+
const scriptPath = path.resolve(process.argv[1]);
|
|
12
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
13
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
14
|
+
<plist version="1.0">
|
|
15
|
+
<dict>
|
|
16
|
+
<key>Label</key>
|
|
17
|
+
<string>${PLIST_LABEL}</string>
|
|
18
|
+
<key>ProgramArguments</key>
|
|
19
|
+
<array>
|
|
20
|
+
<string>${nodePath}</string>
|
|
21
|
+
<string>${scriptPath}</string>
|
|
22
|
+
<string>web</string>
|
|
23
|
+
<string>--port</string>
|
|
24
|
+
<string>${port}</string>
|
|
25
|
+
</array>
|
|
26
|
+
<key>RunAtLoad</key>
|
|
27
|
+
<true/>
|
|
28
|
+
<key>KeepAlive</key>
|
|
29
|
+
<true/>
|
|
30
|
+
<key>StandardOutPath</key>
|
|
31
|
+
<string>${path.join(os.homedir(), ".config", "pyre", "web.log")}</string>
|
|
32
|
+
<key>StandardErrorPath</key>
|
|
33
|
+
<string>${path.join(os.homedir(), ".config", "pyre", "web.err")}</string>
|
|
34
|
+
</dict>
|
|
35
|
+
</plist>
|
|
36
|
+
`;
|
|
37
|
+
try {
|
|
38
|
+
const launchAgentsDir = path.dirname(PLIST_PATH);
|
|
39
|
+
if (!fs.existsSync(launchAgentsDir)) {
|
|
40
|
+
fs.mkdirSync(launchAgentsDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(PLIST_PATH, plistContent);
|
|
43
|
+
try {
|
|
44
|
+
execSync(`launchctl unload ${PLIST_PATH} 2>/dev/null`);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
execSync(`launchctl load ${PLIST_PATH}`);
|
|
48
|
+
console.log(chalk.green(`\u2714 Installed and started launchd agent at ${PLIST_PATH}`));
|
|
49
|
+
console.log(chalk.dim(` Service label: ${PLIST_LABEL}`));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.log(chalk.red(`\u2716 Failed to install launchd agent: ${err.message}`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function uninstallLaunchdAgent() {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(PLIST_PATH)) {
|
|
57
|
+
try {
|
|
58
|
+
execSync(`launchctl unload ${PLIST_PATH} 2>/dev/null`);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
fs.unlinkSync(PLIST_PATH);
|
|
62
|
+
console.log(chalk.green(`\u2714 Uninstalled launchd agent ${PLIST_PATH}`));
|
|
63
|
+
} else {
|
|
64
|
+
console.log(chalk.yellow(` No launchd agent found at ${PLIST_PATH}`));
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.log(chalk.red(`\u2716 Failed to uninstall launchd agent: ${err.message}`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
installLaunchdAgent,
|
|
72
|
+
uninstallLaunchdAgent
|
|
73
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/stressCmd.ts
|
|
2
|
+
import { Worker, isMainThread, workerData } from "worker_threads";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
if (!isMainThread) {
|
|
6
|
+
const endTime = workerData?.endTime;
|
|
7
|
+
while (true) {
|
|
8
|
+
if (endTime && Date.now() >= endTime) {
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
Math.hypot(Math.random(), Math.random());
|
|
12
|
+
}
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
async function runStressCommand(opts = {}) {
|
|
16
|
+
const durationSec = opts.durationSec ?? 30;
|
|
17
|
+
const numCores = opts.cores ?? os.cpus().length;
|
|
18
|
+
const endTime = Date.now() + durationSec * 1e3;
|
|
19
|
+
console.log(chalk.bold(`
|
|
20
|
+
pyre stress \u2014 Synthetic Load Generator`));
|
|
21
|
+
console.log(chalk.dim(` Spawning ${numCores} worker threads for ${durationSec} seconds...`));
|
|
22
|
+
console.log(chalk.dim(` Press Ctrl+C to stop early.
|
|
23
|
+
`));
|
|
24
|
+
const workers = [];
|
|
25
|
+
for (let i = 0; i < numCores; i++) {
|
|
26
|
+
const worker = new Worker(new URL(import.meta.url), {
|
|
27
|
+
workerData: { endTime }
|
|
28
|
+
});
|
|
29
|
+
workers.push(worker);
|
|
30
|
+
}
|
|
31
|
+
const cleanup = () => {
|
|
32
|
+
for (const w of workers) {
|
|
33
|
+
w.terminate();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
process.once("SIGINT", () => {
|
|
37
|
+
cleanup();
|
|
38
|
+
console.log(chalk.yellow("\n Stress test cancelled by user."));
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
cleanup();
|
|
44
|
+
console.log(chalk.green(` \u2714 Synthetic load test completed after ${durationSec}s.`));
|
|
45
|
+
resolve();
|
|
46
|
+
}, durationSec * 1e3);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
runStressCommand
|
|
51
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_FILE,
|
|
3
|
+
writeConfig
|
|
4
|
+
} from "./chunk-RVGKEJXT.js";
|
|
5
|
+
|
|
6
|
+
// src/wizard.ts
|
|
7
|
+
import readline from "readline";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
async function runSetupWizardIfNeeded(skipWizardFlag) {
|
|
11
|
+
if (skipWizardFlag || process.env.PYRE_NO_WIZARD === "1" || !process.stdout.isTTY) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.log(chalk.bold("\n \u{1F525} Welcome to pyre! First-run Setup Wizard\n"));
|
|
18
|
+
console.log(chalk.dim(" This quick 3-question setup configures your defaults.\n"));
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout
|
|
22
|
+
});
|
|
23
|
+
const ask = (query) => {
|
|
24
|
+
return new Promise((resolve) => rl.question(query, resolve));
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
console.log(chalk.cyan(" 1. Pick a default color theme:"));
|
|
28
|
+
console.log(chalk.dim(" (1) default (2) dracula (3) cyberpunk (4) monochrome (5) nord (6) gruvbox"));
|
|
29
|
+
const themeAns = (await ask(chalk.yellow(" Choice [1-6] (default: 1): "))).trim();
|
|
30
|
+
let theme = "default";
|
|
31
|
+
if (themeAns === "2") theme = "dracula";
|
|
32
|
+
else if (themeAns === "3") theme = "cyberpunk";
|
|
33
|
+
else if (themeAns === "4") theme = "monochrome";
|
|
34
|
+
else if (themeAns === "5") theme = "nord";
|
|
35
|
+
else if (themeAns === "6") theme = "gruvbox";
|
|
36
|
+
console.log(chalk.cyan("\n 2. Enable desktop notifications on system thermal/CPU alerts?"));
|
|
37
|
+
const notifAns = (await ask(chalk.yellow(" Enable notifications? [Y/n]: "))).trim().toLowerCase();
|
|
38
|
+
const notificationsEnabled = notifAns !== "n";
|
|
39
|
+
console.log(chalk.cyan("\n 3. Sudo / Powermetrics setup:"));
|
|
40
|
+
console.log(chalk.dim(" pyre reads basic metrics sudolessly. For detailed per-core wattages,"));
|
|
41
|
+
console.log(chalk.dim(" you can optionally grant passwordless sudo for powermetrics later."));
|
|
42
|
+
await ask(chalk.yellow(" Press Enter to finish setup... "));
|
|
43
|
+
writeConfig({
|
|
44
|
+
theme,
|
|
45
|
+
notificationsEnabled
|
|
46
|
+
});
|
|
47
|
+
console.log(chalk.green("\n \u2714 Preferences saved to ~/.config/pyre/config.json\n"));
|
|
48
|
+
} catch {
|
|
49
|
+
} finally {
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
runSetupWizardIfNeeded
|
|
55
|
+
};
|
package/package.json
CHANGED
package/src/checkCmd.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { collectAll } from './monitors/index.js';
|
|
3
|
+
import { formatBytes } from './formatters/render.js';
|
|
4
|
+
|
|
5
|
+
export interface CheckOptions {
|
|
6
|
+
exitCode?: boolean;
|
|
7
|
+
cpuAlertPct?: number;
|
|
8
|
+
tempAlertC?: number;
|
|
9
|
+
detailed?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runCheckCommand(opts: CheckOptions = {}): Promise<void> {
|
|
13
|
+
const data = await collectAll({ detailed: opts.detailed });
|
|
14
|
+
|
|
15
|
+
const cpuUsage = data.cpu.usage;
|
|
16
|
+
const tempC = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
|
|
17
|
+
const memUsage = data.memory.usagePercent;
|
|
18
|
+
const disk = data.disk.mainVolume;
|
|
19
|
+
|
|
20
|
+
const sentences: string[] = [];
|
|
21
|
+
let isWarning = false;
|
|
22
|
+
|
|
23
|
+
// CPU & Temp sentence
|
|
24
|
+
const tempStr = tempC !== undefined ? `${Math.round(tempC)}°C` : null;
|
|
25
|
+
const cpuAlertThreshold = opts.cpuAlertPct ?? 90;
|
|
26
|
+
const tempAlertThreshold = opts.tempAlertC ?? 95;
|
|
27
|
+
|
|
28
|
+
if (cpuUsage >= cpuAlertThreshold || (tempC !== undefined && tempC >= tempAlertThreshold)) {
|
|
29
|
+
isWarning = true;
|
|
30
|
+
if (tempStr) {
|
|
31
|
+
sentences.push(`CPU running hot at ${tempStr}, ${Math.round(cpuUsage)}% used.`);
|
|
32
|
+
} else {
|
|
33
|
+
sentences.push(`CPU load is high at ${Math.round(cpuUsage)}% used.`);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
if (tempStr) {
|
|
37
|
+
sentences.push(`CPU running cool at ${tempStr}, ${Math.round(cpuUsage)}% used.`);
|
|
38
|
+
} else {
|
|
39
|
+
sentences.push(`CPU running normally at ${Math.round(cpuUsage)}% used.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Memory & Battery sentence
|
|
44
|
+
if (data.battery) {
|
|
45
|
+
if (data.battery.friendlySummary) {
|
|
46
|
+
if (data.battery.friendlySummary.toLowerCase().includes('service') || data.battery.friendlySummary.toLowerCase().includes('reduced')) {
|
|
47
|
+
isWarning = true;
|
|
48
|
+
}
|
|
49
|
+
sentences.push(data.battery.friendlySummary);
|
|
50
|
+
} else {
|
|
51
|
+
let healthStr = 'health good';
|
|
52
|
+
if (data.battery.health.toLowerCase().includes('service') || data.battery.health.toLowerCase().includes('check')) {
|
|
53
|
+
healthStr = 'service recommended';
|
|
54
|
+
isWarning = true;
|
|
55
|
+
}
|
|
56
|
+
sentences.push(`Battery at ${data.battery.level}%, ${healthStr}.`);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
sentences.push(`Memory at ${Math.round(memUsage)}% used.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Disk sentence
|
|
63
|
+
if (disk) {
|
|
64
|
+
const freeBytes = disk.free;
|
|
65
|
+
const freeStr = formatBytes(freeBytes);
|
|
66
|
+
sentences.push(`${freeStr} free on ${disk.mount}.`);
|
|
67
|
+
if (disk.usePercent >= 90) {
|
|
68
|
+
isWarning = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const summaryText = sentences.join(' ');
|
|
73
|
+
|
|
74
|
+
if (isWarning) {
|
|
75
|
+
console.log(chalk.yellow(`⚠ ${summaryText}`));
|
|
76
|
+
} else {
|
|
77
|
+
console.log(chalk.green(`✔ ${summaryText}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (opts.exitCode && isWarning) {
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/fleet.ts
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { formatTable } from './formatters/render.js';
|
|
4
5
|
import type { StatsData } from './monitors/types.js';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
console.log(chalk.cyan(`Fetching data from ${hosts.length} hosts...`));
|
|
8
|
-
const results: { host: string; data?: StatsData; error?: string }[] = [];
|
|
7
|
+
const execAsync = promisify(exec);
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
export async function runFleetCommand(hosts: string[]): Promise<void> {
|
|
10
|
+
console.log(chalk.cyan(`Fetching concurrent data from ${hosts.length} fleet host(s)...`));
|
|
11
|
+
|
|
12
|
+
const promises = hosts.map(async (host) => {
|
|
11
13
|
try {
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
results.push({ host, data });
|
|
14
|
+
const { stdout } = await execAsync(`ssh -o ConnectTimeout=5 ${host} "pyre --once --json"`);
|
|
15
|
+
const data = JSON.parse(stdout) as StatsData;
|
|
16
|
+
return { host, data };
|
|
16
17
|
} catch (e: any) {
|
|
17
|
-
|
|
18
|
+
return { host, error: e.message || String(e) };
|
|
18
19
|
}
|
|
19
|
-
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const results = await Promise.all(promises);
|
|
20
23
|
|
|
21
|
-
// Very basic grid layout for fleet
|
|
22
24
|
for (const result of results) {
|
|
23
|
-
console.log(chalk.bold.blue(`\n=== ${result.host} ===\n`));
|
|
25
|
+
console.log(chalk.bold.blue(`\n=== Host: ${result.host} ===\n`));
|
|
24
26
|
if (result.error) {
|
|
25
|
-
console.log(chalk.red(`Error connecting to host: ${result.error}`));
|
|
27
|
+
console.log(chalk.red(` ✖ Error connecting to host: ${result.error}`));
|
|
26
28
|
} else if (result.data) {
|
|
27
|
-
|
|
28
|
-
console.log(formatTable(result.data, { width: process.stdout.columns || 80, activePanel: 'grid' }));
|
|
29
|
+
console.log(formatTable(result.data, { width: process.stdout.columns || 80, minimal: true }));
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}
|
package/src/formatters/render.ts
CHANGED
|
@@ -184,17 +184,25 @@ function gaugeRow(label: string, percent: number, contentWidth: number): string
|
|
|
184
184
|
return `${chalk.dim(label.padEnd(labelW))}${color(str)}${chalk.dim('─'.repeat(empty))} ${pctText}`;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
export function statusSymbol(level: 'ok' | 'warn' | 'crit'): string {
|
|
188
|
+
if (level === 'crit') return '✕ ';
|
|
189
|
+
if (level === 'warn') return '⚠ ';
|
|
190
|
+
return '✓ ';
|
|
191
|
+
}
|
|
192
|
+
|
|
187
193
|
export function thermalColor(pressureLevel: number) {
|
|
188
|
-
|
|
189
|
-
if (pressureLevel
|
|
190
|
-
if (pressureLevel ===
|
|
191
|
-
return chalk.
|
|
194
|
+
const symbol = pressureLevel >= 3 ? '✕ ' : pressureLevel >= 1 ? '⚠ ' : '✓ ';
|
|
195
|
+
if (pressureLevel >= 3) return (s: string) => chalk.red(symbol + s);
|
|
196
|
+
if (pressureLevel === 2) return (s: string) => chalk.yellow(symbol + s);
|
|
197
|
+
if (pressureLevel === 1) return (s: string) => chalk.yellowBright(symbol + s);
|
|
198
|
+
return (s: string) => chalk.green(symbol + s);
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
export function capacityColor(percent: number) {
|
|
195
|
-
|
|
196
|
-
if (percent <
|
|
197
|
-
return chalk.
|
|
202
|
+
const symbol = percent < 80 ? '✕ ' : percent < 90 ? '⚠ ' : '✓ ';
|
|
203
|
+
if (percent < 80) return (s: string) => chalk.red(symbol + s);
|
|
204
|
+
if (percent < 90) return (s: string) => chalk.yellow(symbol + s);
|
|
205
|
+
return (s: string) => chalk.green(symbol + s);
|
|
198
206
|
}
|
|
199
207
|
|
|
200
208
|
export function formatBytes(bytes: number): string {
|
|
@@ -232,6 +240,11 @@ function cpuCard(data: StatsData, contentWidth: number): string[] {
|
|
|
232
240
|
gaugeRow('Usage', data.cpu.usage, contentWidth),
|
|
233
241
|
statRow('Load', data.cpu.loadAvg.map(l => l.toFixed(2)).join(' ')),
|
|
234
242
|
];
|
|
243
|
+
if (data.cpu.clusters && data.cpu.clusters.length) {
|
|
244
|
+
for (const cl of data.cpu.clusters) {
|
|
245
|
+
lines.push(statRow(cl.name, `${cl.frequencyMhz} MHz · eff ${cl.effectiveUsagePercent}% · act ${cl.activeResidencyPercent}%`));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
235
248
|
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
236
249
|
const slotW = 2;
|
|
237
250
|
const maxCores = Math.max(1, Math.floor((contentWidth - 10) / (slotW + 1)));
|
|
@@ -275,6 +288,7 @@ function gpuCard(data: StatsData, contentWidth: number): string[] | null {
|
|
|
275
288
|
if (data.power) {
|
|
276
289
|
if (data.power.cpuWatts !== undefined) lines.push(statRow('CPU', `${data.power.cpuWatts.toFixed(2)} W`));
|
|
277
290
|
if (data.power.gpuWatts !== undefined) lines.push(statRow('GPU', `${data.power.gpuWatts.toFixed(2)} W`));
|
|
291
|
+
lines.push(statRow('ANE', `${(data.power.aneWatts ?? 0).toFixed(2)} W`));
|
|
278
292
|
if (data.power.combinedWatts !== undefined) lines.push(statRow('Total', `${data.power.combinedWatts.toFixed(2)} W`));
|
|
279
293
|
}
|
|
280
294
|
if (data.battery?.powerWatts !== undefined && data.battery.powerWatts > 0) {
|
|
@@ -769,9 +783,15 @@ function btopCpuBox(data: StatsData, width: number, theme: ThemeColors, opts: Ta
|
|
|
769
783
|
const rightW = contentWidth - leftW - 2;
|
|
770
784
|
|
|
771
785
|
const leftLines: string[] = [];
|
|
772
|
-
leftLines.push(fitVisible(theme.cpu('CPU Usage History'), leftW));
|
|
773
|
-
|
|
774
786
|
const cpuHistory = opts.history?.cpuUsage || [];
|
|
787
|
+
let historyLabel = 'CPU Usage History';
|
|
788
|
+
if (cpuHistory.length > 0) {
|
|
789
|
+
const avg = Math.round(cpuHistory.reduce((a, b) => a + b, 0) / cpuHistory.length);
|
|
790
|
+
const max = Math.round(Math.max(...cpuHistory));
|
|
791
|
+
historyLabel += ` (avg ${avg}% · max ${max}%)`;
|
|
792
|
+
}
|
|
793
|
+
leftLines.push(fitVisible(theme.cpu(historyLabel), leftW));
|
|
794
|
+
|
|
775
795
|
if (cpuHistory.length > 1) {
|
|
776
796
|
const graphRows = multiRowBrailleGraph(cpuHistory, 3, { min: 0, max: 100 });
|
|
777
797
|
for (const gr of graphRows) {
|
|
@@ -1005,6 +1025,23 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
|
1005
1025
|
out.push(tabBar(activePanel, width, theme));
|
|
1006
1026
|
out.push('');
|
|
1007
1027
|
|
|
1028
|
+
if (opts.minimal) {
|
|
1029
|
+
const contentWidth = width - 4;
|
|
1030
|
+
const lines: string[] = [];
|
|
1031
|
+
lines.push(gaugeRow('CPU', data.cpu.usage, contentWidth));
|
|
1032
|
+
lines.push(gaugeRow('Memory', data.memory.usagePercent, contentWidth));
|
|
1033
|
+
if (data.gpu) {
|
|
1034
|
+
lines.push(gaugeRow('GPU', data.gpu.utilization, contentWidth));
|
|
1035
|
+
}
|
|
1036
|
+
const tempC = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
|
|
1037
|
+
if (tempC !== undefined) {
|
|
1038
|
+
const tempStr = formatTempUnit(tempC, opts.tempUnit);
|
|
1039
|
+
lines.push(statRow('Temp', thermalColor(data.thermal.pressureLevel)(tempStr)));
|
|
1040
|
+
}
|
|
1041
|
+
out.push(...panel('macmon mode', lines, width, theme.cpu, theme.border));
|
|
1042
|
+
return out.join('\n');
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1008
1045
|
if (activePanel !== 'grid') {
|
|
1009
1046
|
const lines = activeDetailLines(data, activePanel, width, opts);
|
|
1010
1047
|
if (lines) {
|
package/src/formatters/types.ts
CHANGED
|
@@ -86,4 +86,6 @@ export interface TableOptions {
|
|
|
86
86
|
trackedPid?: number | null;
|
|
87
87
|
/** Currently inspected process details for modal overlay. */
|
|
88
88
|
inspectProcess?: any;
|
|
89
|
+
/** Minimal mode: renders only CPU, GPU, Memory, and Temperature as large glanceable gauges. */
|
|
90
|
+
minimal?: boolean;
|
|
89
91
|
}
|