pyre-cli 9.4.0 → 9.5.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.
@@ -0,0 +1,39 @@
1
+ import "./chunk-SG26Y3WF.js";
2
+ import {
3
+ collectAll
4
+ } from "./chunk-PV4MXTAJ.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyre-cli",
3
- "version": "9.4.0",
3
+ "version": "9.5.0",
4
4
  "description": "CLI Mac monitoring tool: temps, system stats, live dashboard, packet monitor, battery predictor, export",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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 { execSync } from 'node:child_process';
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
- export function runFleetCommand(hosts: string[]) {
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
- for (const host of hosts) {
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
- // Connect to each host via ssh and run pyre
13
- const out = execSync(`ssh ${host} "pyre --once --json"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
14
- const data = JSON.parse(out) as StatsData;
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
- results.push({ host, error: e.message });
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
- // Print a compact table for each host
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
  }
@@ -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
- if (pressureLevel >= 3) return chalk.red;
189
- if (pressureLevel === 2) return chalk.yellow;
190
- if (pressureLevel === 1) return chalk.yellowBright;
191
- return chalk.green;
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
- if (percent < 80) return chalk.red;
196
- if (percent < 90) return chalk.yellow;
197
- return chalk.green;
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) {
@@ -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
  }