pyre-cli 6.1.0 → 7.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 +28 -13
- package/dist/index.js +481 -186
- package/package.json +1 -1
- package/src/anomalies.ts +1 -0
- package/src/fleet.ts +31 -0
- package/src/formatters/output.ts +107 -10
- package/src/formatters/render.ts +111 -63
- package/src/formatters/types.ts +6 -1
- package/src/index.ts +73 -47
- package/src/live/input.ts +2 -2
- package/src/live/render.ts +30 -2
- package/src/live/state.ts +15 -8
- package/src/state/config.ts +6 -0
- package/src/xbar.ts +53 -0
package/src/live/input.ts
CHANGED
|
@@ -42,10 +42,10 @@ import { writeConfig } from '../state/config.js';
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function isUpKey(key: readline.Key, str?: string): boolean {
|
|
45
|
-
return key.name === 'up' ||
|
|
45
|
+
return key.name === 'up' || key.sequence === '\x1b[A' || key.sequence === '\x1bOA';
|
|
46
46
|
}
|
|
47
47
|
function isDownKey(key: readline.Key, str?: string): boolean {
|
|
48
|
-
return key.name === 'down' ||
|
|
48
|
+
return key.name === 'down' || key.sequence === '\x1b[B' || key.sequence === '\x1bOB';
|
|
49
49
|
}
|
|
50
50
|
function isEnterKey(key: readline.Key, str?: string): boolean {
|
|
51
51
|
return key.name === 'return' || key.name === 'enter' || key.sequence === '\r' || key.sequence === '\n' || str === '\r' || str === '\n';
|
package/src/live/render.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* alert checker. All state variables are imported from
|
|
7
7
|
* `state.ts`.
|
|
8
8
|
*/
|
|
9
|
-
import { execFile } from 'node:child_process';
|
|
9
|
+
import { execFile, exec } from 'node:child_process';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel, fitVisible } from '../formatters/index.js';
|
|
12
12
|
import { detectAnomalies } from '../anomalies.js';
|
|
@@ -54,6 +54,7 @@ function tableCacheParams(): string {
|
|
|
54
54
|
state.treeView ? '1' : '0',
|
|
55
55
|
state.history.version,
|
|
56
56
|
JSON.stringify(state.visiblePanels),
|
|
57
|
+
JSON.stringify(state.panelLayout),
|
|
57
58
|
].join('|');
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -215,10 +216,12 @@ function render() {
|
|
|
215
216
|
visible: state.visiblePanels,
|
|
216
217
|
treeView: state.treeView,
|
|
217
218
|
activePanel: state.activePanel,
|
|
218
|
-
anomalies,
|
|
219
|
+
anomalies: anomalies,
|
|
220
|
+
anomalyHistory: state.anomalyHistory,
|
|
219
221
|
tempUnit: state.tempUnit,
|
|
220
222
|
history: state.history,
|
|
221
223
|
graphMode: state.graphMode,
|
|
224
|
+
panelLayout: state.panelLayout,
|
|
222
225
|
});
|
|
223
226
|
_cachedTableData = state.lastData;
|
|
224
227
|
_cachedTableParams = cacheParams;
|
|
@@ -353,6 +356,19 @@ function checkAlerts(data: StatsData) {
|
|
|
353
356
|
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
354
357
|
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || (temp !== null && temp >= state.TEMP_ALERT_C);
|
|
355
358
|
const anomalies = detectAnomalies(data, state.history);
|
|
359
|
+
|
|
360
|
+
// Add timestamp to anomalies and push to history
|
|
361
|
+
for (const a of anomalies) {
|
|
362
|
+
const timestamped = { ...a, timestamp: new Date() };
|
|
363
|
+
// Only add if not already recently reported (same metric and similar zScore)
|
|
364
|
+
const recent = state.anomalyHistory.slice(-5);
|
|
365
|
+
const isDuplicate = recent.some(r => r.metric === a.metric && Math.abs(r.zScore - a.zScore) < 0.5);
|
|
366
|
+
if (!isDuplicate) {
|
|
367
|
+
state.anomalyHistory.push(timestamped as any);
|
|
368
|
+
if (state.anomalyHistory.length > 50) state.anomalyHistory.shift();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
356
372
|
const hasCriticalAnomaly = anomalies.some(a => a.severity === 'critical');
|
|
357
373
|
const hasWarningAnomaly = anomalies.some(a => a.severity === 'warning');
|
|
358
374
|
// Process watchdog check
|
|
@@ -381,6 +397,18 @@ function checkAlerts(data: StatsData) {
|
|
|
381
397
|
}
|
|
382
398
|
setStatus(chalk.red(`⚠ Alert: ${reasons.join('; ')}`), 5000);
|
|
383
399
|
sendNotification('pyre Alert', reasons.join('; '));
|
|
400
|
+
|
|
401
|
+
const alertMsg = reasons.join('; ');
|
|
402
|
+
if (state.webhookUrl) {
|
|
403
|
+
fetch(state.webhookUrl, {
|
|
404
|
+
method: 'POST',
|
|
405
|
+
headers: { 'Content-Type': 'application/json' },
|
|
406
|
+
body: JSON.stringify({ alert: alertMsg, timestamp: new Date().toISOString() })
|
|
407
|
+
}).catch(() => {});
|
|
408
|
+
}
|
|
409
|
+
if (state.alertCmd) {
|
|
410
|
+
exec(state.alertCmd, { env: { ...process.env, PYRE_ALERT: alertMsg } }, () => {});
|
|
411
|
+
}
|
|
384
412
|
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
385
413
|
state.alerted = false;
|
|
386
414
|
}
|
package/src/live/state.ts
CHANGED
|
@@ -48,6 +48,7 @@ const state = {
|
|
|
48
48
|
disk: config.visiblePanels.disk,
|
|
49
49
|
process: config.visiblePanels.process,
|
|
50
50
|
} as VisibleItems,
|
|
51
|
+
panelLayout: config.panelLayout || ['mem', 'disk', 'net'],
|
|
51
52
|
logging: false,
|
|
52
53
|
logStream: null as fs.WriteStream | null,
|
|
53
54
|
statusMessage: '',
|
|
@@ -93,8 +94,10 @@ const state = {
|
|
|
93
94
|
watchdogCpu: config.watchdogCpu || 80,
|
|
94
95
|
watchdogMem: config.watchdogMem || 80,
|
|
95
96
|
notificationsEnabled: config.notificationsEnabled,
|
|
97
|
+
webhookUrl: config.webhookUrl || '',
|
|
98
|
+
alertCmd: config.alertCmd || '',
|
|
96
99
|
alerted: false,
|
|
97
|
-
activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p',
|
|
100
|
+
activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies',
|
|
98
101
|
mouseEnabled: config.mouseEnabled,
|
|
99
102
|
PANEL_TABS: [
|
|
100
103
|
{ id: 'cpu', label: 'CPU', key: '1' },
|
|
@@ -109,13 +112,17 @@ const state = {
|
|
|
109
112
|
{ id: 'disk', label: 'Disk', key: '0' },
|
|
110
113
|
{ id: 'process', label: 'Process', key: 'P' },
|
|
111
114
|
{ id: 'p2p', label: 'P2P', key: 'R' },
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
{ id: 'anomalies', label: 'Anomalies', key: 'A' },
|
|
116
|
+
],
|
|
117
|
+
p2pServer: null as import('../p2p/server.js').P2PServer | null,
|
|
118
|
+
p2pClient: null as import('../p2p/client.js').P2PClient | null,
|
|
119
|
+
p2pServerRunning: false,
|
|
120
|
+
p2pClientConnected: false,
|
|
121
|
+
p2pEvents: [] as any[],
|
|
122
|
+
p2pBind: '',
|
|
123
|
+
p2pPassword: config.p2pPassword,
|
|
124
|
+
p2pPort: config.p2pPort,
|
|
125
|
+
anomalyHistory: [] as import('../anomalies.js').AnomalyAlert[],
|
|
119
126
|
splashEnabled: config.splashEnabled,
|
|
120
127
|
splashColorScheme: config.splashColorScheme as SplashColorScheme,
|
|
121
128
|
splashAnimation: config.splashAnimation as SplashAnimation,
|
package/src/state/config.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface PyreConfig {
|
|
|
37
37
|
watchdogProcess?: string;
|
|
38
38
|
watchdogCpu?: number;
|
|
39
39
|
watchdogMem?: number;
|
|
40
|
+
webhookUrl?: string;
|
|
41
|
+
alertCmd?: string;
|
|
40
42
|
visiblePanels?: {
|
|
41
43
|
cpu?: boolean;
|
|
42
44
|
mem?: boolean;
|
|
@@ -51,6 +53,7 @@ export interface PyreConfig {
|
|
|
51
53
|
process?: boolean;
|
|
52
54
|
tree?: boolean;
|
|
53
55
|
};
|
|
56
|
+
panelLayout?: string[];
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
@@ -75,6 +78,8 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
75
78
|
splashEnabled: true,
|
|
76
79
|
splashColorScheme: 'fire',
|
|
77
80
|
splashAnimation: 'classic',
|
|
81
|
+
webhookUrl: '',
|
|
82
|
+
alertCmd: '',
|
|
78
83
|
visiblePanels: {
|
|
79
84
|
cpu: true,
|
|
80
85
|
mem: true,
|
|
@@ -89,6 +94,7 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
89
94
|
process: true,
|
|
90
95
|
tree: false,
|
|
91
96
|
},
|
|
97
|
+
panelLayout: ['mem', 'disk', 'net'],
|
|
92
98
|
};
|
|
93
99
|
|
|
94
100
|
export function readConfig(): Required<PyreConfig> {
|
package/src/xbar.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export function generateXbarPlugin() {
|
|
7
|
+
const pyrePath = process.argv[1];
|
|
8
|
+
const nodePath = process.execPath;
|
|
9
|
+
|
|
10
|
+
const script = `#!/bin/bash
|
|
11
|
+
# <xbar.title>Pyre Monitor</xbar.title>
|
|
12
|
+
# <xbar.version>v1.0</xbar.version>
|
|
13
|
+
# <xbar.author>somalip</xbar.author>
|
|
14
|
+
# <xbar.author.github>somalip</xbar.author.github>
|
|
15
|
+
# <xbar.desc>Mac system monitoring powered by pyre.</xbar.desc>
|
|
16
|
+
|
|
17
|
+
# Run pyre to get a JSON snapshot
|
|
18
|
+
# We use node directly to avoid env issues in xbar
|
|
19
|
+
DATA=$("${nodePath}" "${pyrePath}" --once --json)
|
|
20
|
+
|
|
21
|
+
if [ -z "$DATA" ]; then
|
|
22
|
+
echo "🔥 err"
|
|
23
|
+
echo "---"
|
|
24
|
+
echo "Could not fetch data"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
CPU=$(echo "$DATA" | grep -o '"usage": *[0-9.]*' | head -1 | awk '{print $2}')
|
|
29
|
+
MEM=$(echo "$DATA" | grep -o '"usagePercent": *[0-9.]*' | head -1 | awk '{print $2}')
|
|
30
|
+
TEMP=$(echo "$DATA" | grep -o '"temperature": *[0-9.]*' | head -1 | awk '{print $2}')
|
|
31
|
+
|
|
32
|
+
# Format to 1 decimal place
|
|
33
|
+
CPU=$(printf "%.1f" "$CPU")
|
|
34
|
+
MEM=$(printf "%.1f" "$MEM")
|
|
35
|
+
TEMP=$(printf "%.1f" "$TEMP")
|
|
36
|
+
|
|
37
|
+
echo "🔥 C:\${CPU}% M:\${MEM}% T:\${TEMP}°C | font=Menlo"
|
|
38
|
+
echo "---"
|
|
39
|
+
echo "Open Dashboard | bash='${nodePath}' param1='${pyrePath}' param2='live' terminal=true"
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const pluginDir = path.join(os.homedir(), 'Library', 'Application Support', 'xbar', 'plugins');
|
|
43
|
+
const filename = 'pyre-monitor.2s.sh';
|
|
44
|
+
const outPath = path.join(process.cwd(), filename);
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync(outPath, script);
|
|
47
|
+
fs.chmodSync(outPath, '755');
|
|
48
|
+
|
|
49
|
+
console.log(chalk.green(`Generated xbar plugin script at ${outPath}`));
|
|
50
|
+
console.log(`To install, move it to your xbar plugins directory:`);
|
|
51
|
+
console.log(chalk.cyan(` mv ${filename} "${pluginDir}/"`));
|
|
52
|
+
console.log(`Then refresh xbar.`);
|
|
53
|
+
}
|