pyre-cli 6.0.0 → 7.0.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 +778 -224
- package/package.json +1 -1
- package/src/anomalies.ts +1 -0
- package/src/completions.ts +118 -0
- package/src/diffCmd.ts +36 -0
- package/src/doctor.ts +123 -0
- package/src/fleet.ts +31 -0
- package/src/formatters/output.ts +107 -10
- package/src/formatters/render.ts +113 -56
- package/src/formatters/themes.ts +47 -12
- package/src/formatters/types.ts +6 -1
- package/src/historyCmd.ts +71 -0
- package/src/index.ts +126 -15
- package/src/live/input.ts +8 -0
- package/src/live/render.ts +112 -57
- package/src/live/state.ts +18 -8
- package/src/monitors/collectors.ts +41 -2
- package/src/monitors/types.ts +2 -0
- package/src/state/config.ts +12 -1
- package/src/xbar.ts +53 -0
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
|
|
|
@@ -107,42 +108,45 @@ function p2pPanelLines(): { title: string; body: string[] } {
|
|
|
107
108
|
return { title, body };
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
function renderCustomizerOverlay(): string {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
111
|
+
function renderCustomizerOverlay(scrollOffset: number, maxVisibleItems: number): string {
|
|
112
|
+
const themesList = Object.keys(THEMES) as ThemeName[];
|
|
113
|
+
const headerLine = chalk.bgCyan.black.bold(' UI CUSTOMIZER (Use ↑/↓ or J/K to navigate, Enter/Space to toggle, Esc to exit) ');
|
|
114
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
115
|
+
const end = Math.min(scrollOffset + maxVisibleItems, totalItems);
|
|
116
|
+
|
|
117
|
+
const lines = [headerLine];
|
|
118
|
+
|
|
119
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
120
|
+
const opt = state.CUSTOMIZER_OPTIONS[i];
|
|
121
|
+
const isSelected = i === state.customizerIndex;
|
|
122
|
+
const prefix = isSelected ? chalk.yellow('▶ ') : ' ';
|
|
123
|
+
|
|
124
|
+
if (opt === 'Theme') {
|
|
125
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.currentTheme)} [${themesList.join(', ')}]`);
|
|
126
|
+
} else if (opt === 'Graph Mode') {
|
|
127
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.graphMode)} [spark, bar]`);
|
|
128
|
+
} else if (opt === 'Splash Screen') {
|
|
129
|
+
const status = state.splashEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
|
|
130
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
131
|
+
} else if (opt === 'Splash Color') {
|
|
132
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
|
|
133
|
+
} else if (opt === 'Splash Animation') {
|
|
134
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
|
|
135
|
+
} else if (opt === 'Notifications') {
|
|
136
|
+
const status = state.notificationsEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
|
|
137
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
138
|
+
} else if (opt === 'Temperature Unit') {
|
|
139
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
|
|
140
|
+
} else {
|
|
141
|
+
const toggleKey = getToggleKey(opt);
|
|
142
|
+
const isVisible = toggleKey ? (toggleKey === 'tree' ? state.treeView : state.visiblePanels[toggleKey] !== false) : true;
|
|
143
|
+
const status = isVisible ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
|
|
144
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
143
147
|
|
|
144
148
|
return lines.map(l => ` ${l}`).join('\n');
|
|
145
|
-
|
|
149
|
+
}
|
|
146
150
|
|
|
147
151
|
// --- frame diffing -------------------------------------------------------
|
|
148
152
|
// Instead of erasing the whole screen (`\x1b[2J`) and repainting everything
|
|
@@ -212,10 +216,12 @@ function render() {
|
|
|
212
216
|
visible: state.visiblePanels,
|
|
213
217
|
treeView: state.treeView,
|
|
214
218
|
activePanel: state.activePanel,
|
|
215
|
-
anomalies,
|
|
219
|
+
anomalies: anomalies,
|
|
220
|
+
anomalyHistory: state.anomalyHistory,
|
|
216
221
|
tempUnit: state.tempUnit,
|
|
217
222
|
history: state.history,
|
|
218
223
|
graphMode: state.graphMode,
|
|
224
|
+
panelLayout: state.panelLayout,
|
|
219
225
|
});
|
|
220
226
|
_cachedTableData = state.lastData;
|
|
221
227
|
_cachedTableParams = cacheParams;
|
|
@@ -255,27 +261,38 @@ function render() {
|
|
|
255
261
|
const footerLine3 = footerLine()[2] || ' ';
|
|
256
262
|
const footerLines = [footerLine1, footerLine2, footerLine3];
|
|
257
263
|
|
|
258
|
-
if (state.inputMode === 'customizer') {
|
|
259
|
-
bodyLines.push('');
|
|
260
|
-
bodyLines.push(...renderCustomizerOverlay().split('\n'));
|
|
261
|
-
} else if (state.inputMode === 'filter') {
|
|
262
|
-
bodyLines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
263
|
-
} else if (state.inputMode === 'kill') {
|
|
264
|
-
bodyLines.push(chalk.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
|
|
265
|
-
} else if (state.inputMode === 'signal') {
|
|
266
|
-
const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
|
|
267
|
-
const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk.yellow(s) : chalk.dim(s)).join(' ');
|
|
268
|
-
bodyLines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
|
|
269
|
-
bodyLines.push(chalk.dim(` ${sigList}`));
|
|
270
|
-
bodyLines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
|
|
271
|
-
} else if (state.inputMode === 'p2p') {
|
|
272
|
-
bodyLines.push(chalk.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
|
|
273
|
-
} else if (state.statusMessage) {
|
|
274
|
-
bodyLines.push(` ${state.statusMessage}`);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
264
|
const footerHeight = 3;
|
|
278
265
|
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
266
|
+
|
|
267
|
+
if (state.inputMode === 'customizer') {
|
|
268
|
+
const preOverlayCount = bodyLines.length;
|
|
269
|
+
const availableForOverlay = targetBodyHeight - preOverlayCount;
|
|
270
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
271
|
+
const headerLines = 1;
|
|
272
|
+
const blankLines = availableForOverlay >= 2 ? 1 : 0;
|
|
273
|
+
const maxVisibleItems = Math.max(0, availableForOverlay - headerLines - blankLines);
|
|
274
|
+
const scrollOffset = totalItems > maxVisibleItems
|
|
275
|
+
? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems))
|
|
276
|
+
: 0;
|
|
277
|
+
|
|
278
|
+
if (blankLines > 0) bodyLines.push('');
|
|
279
|
+
bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split('\n'));
|
|
280
|
+
} else if (state.inputMode === 'filter') {
|
|
281
|
+
bodyLines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
282
|
+
} else if (state.inputMode === 'kill') {
|
|
283
|
+
bodyLines.push(chalk.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
|
|
284
|
+
} else if (state.inputMode === 'signal') {
|
|
285
|
+
const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
|
|
286
|
+
const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk.yellow(s) : chalk.dim(s)).join(' ');
|
|
287
|
+
bodyLines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
|
|
288
|
+
bodyLines.push(chalk.dim(` ${sigList}`));
|
|
289
|
+
bodyLines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
|
|
290
|
+
} else if (state.inputMode === 'p2p') {
|
|
291
|
+
bodyLines.push(chalk.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
|
|
292
|
+
} else if (state.statusMessage) {
|
|
293
|
+
bodyLines.push(` ${state.statusMessage}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
279
296
|
if (bodyLines.length > targetBodyHeight) {
|
|
280
297
|
bodyLines.length = targetBodyHeight;
|
|
281
298
|
}
|
|
@@ -339,22 +356,60 @@ function checkAlerts(data: StatsData) {
|
|
|
339
356
|
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
340
357
|
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || (temp !== null && temp >= state.TEMP_ALERT_C);
|
|
341
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
|
+
|
|
342
372
|
const hasCriticalAnomaly = anomalies.some(a => a.severity === 'critical');
|
|
343
373
|
const hasWarningAnomaly = anomalies.some(a => a.severity === 'warning');
|
|
374
|
+
// Process watchdog check
|
|
375
|
+
let watchdogTriggered = false;
|
|
376
|
+
if (state.watchdogProcess) {
|
|
377
|
+
const matches = data.processes.filter(p => p.command.toLowerCase().includes(state.watchdogProcess.toLowerCase()));
|
|
378
|
+
for (const p of matches) {
|
|
379
|
+
if (p.cpu >= state.watchdogCpu || p.mem >= state.watchdogMem) {
|
|
380
|
+
watchdogTriggered = true;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
344
386
|
const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
|
|
345
387
|
|
|
346
|
-
if ((hot || anomalyTriggered) && !state.alerted) {
|
|
388
|
+
if ((hot || anomalyTriggered || watchdogTriggered) && !state.alerted) {
|
|
347
389
|
state.alerted = true;
|
|
348
390
|
process.stdout.write('\x07');
|
|
349
391
|
const reasons: string[] = [];
|
|
350
392
|
if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
|
|
351
393
|
if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
|
|
394
|
+
if (watchdogTriggered) reasons.push(`Watchdog process "${state.watchdogProcess}" crossed threshold`);
|
|
352
395
|
for (const a of anomalies) {
|
|
353
396
|
reasons.push(`${a.metric} anomaly (σ=${a.zScore.toFixed(1)})`);
|
|
354
397
|
}
|
|
355
398
|
setStatus(chalk.red(`⚠ Alert: ${reasons.join('; ')}`), 5000);
|
|
356
399
|
sendNotification('pyre Alert', reasons.join('; '));
|
|
357
|
-
|
|
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
|
+
}
|
|
412
|
+
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
358
413
|
state.alerted = false;
|
|
359
414
|
}
|
|
360
415
|
}
|
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: '',
|
|
@@ -89,9 +90,14 @@ const state = {
|
|
|
89
90
|
],
|
|
90
91
|
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
91
92
|
TEMP_ALERT_C: config.tempAlertC,
|
|
93
|
+
watchdogProcess: config.watchdogProcess || '',
|
|
94
|
+
watchdogCpu: config.watchdogCpu || 80,
|
|
95
|
+
watchdogMem: config.watchdogMem || 80,
|
|
92
96
|
notificationsEnabled: config.notificationsEnabled,
|
|
97
|
+
webhookUrl: config.webhookUrl || '',
|
|
98
|
+
alertCmd: config.alertCmd || '',
|
|
93
99
|
alerted: false,
|
|
94
|
-
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',
|
|
95
101
|
mouseEnabled: config.mouseEnabled,
|
|
96
102
|
PANEL_TABS: [
|
|
97
103
|
{ id: 'cpu', label: 'CPU', key: '1' },
|
|
@@ -106,13 +112,17 @@ const state = {
|
|
|
106
112
|
{ id: 'disk', label: 'Disk', key: '0' },
|
|
107
113
|
{ id: 'process', label: 'Process', key: 'P' },
|
|
108
114
|
{ id: 'p2p', label: 'P2P', key: 'R' },
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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[],
|
|
116
126
|
splashEnabled: config.splashEnabled,
|
|
117
127
|
splashColorScheme: config.splashColorScheme as SplashColorScheme,
|
|
118
128
|
splashAnimation: config.splashAnimation as SplashAnimation,
|
|
@@ -504,9 +504,46 @@ export async function collectMemory(): Promise<MemoryData> {
|
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
let prevDiskIoSample: { ts: number } | null = null;
|
|
508
|
+
let cachedDiskIo: { readBytesSec: number; writeBytesSec: number; ts: number } | null = null;
|
|
509
|
+
|
|
510
|
+
async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec: number }> {
|
|
511
|
+
const now = Date.now();
|
|
512
|
+
if (cachedDiskIo && now - cachedDiskIo.ts < 1000) {
|
|
513
|
+
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const raw = (await run('iostat -d -c 2 1 2', '')).trim();
|
|
517
|
+
const blocks = raw.split(/\n\s*\n/).filter(Boolean);
|
|
518
|
+
const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
|
|
519
|
+
if (targetBlock) {
|
|
520
|
+
const lines = targetBlock.trim().split('\n');
|
|
521
|
+
if (lines.length >= 2) {
|
|
522
|
+
const dataLine = lines[lines.length - 1].trim();
|
|
523
|
+
const parts = dataLine.split(/\s+/);
|
|
524
|
+
if (parts.length >= 3) {
|
|
525
|
+
const mbSec = parseFloat(parts[2]);
|
|
526
|
+
if (!isNaN(mbSec)) {
|
|
527
|
+
const bytesSec = Math.round(mbSec * 1024 * 1024);
|
|
528
|
+
// Splitting combined throughput evenly/estimating if read/write distinction unavailable directly from summary iostat
|
|
529
|
+
cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
530
|
+
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
} catch {
|
|
536
|
+
// ignore
|
|
537
|
+
}
|
|
538
|
+
return { readBytesSec: 0, writeBytesSec: 0 };
|
|
539
|
+
}
|
|
540
|
+
|
|
507
541
|
export async function collectDisk(): Promise<DiskData[]> {
|
|
508
|
-
const
|
|
509
|
-
|
|
542
|
+
const [rawDf, ioRates] = await Promise.all([
|
|
543
|
+
run('df -h'),
|
|
544
|
+
getDiskIoRates(),
|
|
545
|
+
]);
|
|
546
|
+
const lines = rawDf.trim().split('\n').slice(1);
|
|
510
547
|
return lines
|
|
511
548
|
.map(line => {
|
|
512
549
|
const parts = line.split(/\s+/);
|
|
@@ -517,6 +554,8 @@ export async function collectDisk(): Promise<DiskData[]> {
|
|
|
517
554
|
available: parts[3],
|
|
518
555
|
capacity: parts[4],
|
|
519
556
|
mountpoint: parts[5] || parts[6] || '/',
|
|
557
|
+
readBytesSec: ioRates.readBytesSec,
|
|
558
|
+
writeBytesSec: ioRates.writeBytesSec,
|
|
520
559
|
};
|
|
521
560
|
})
|
|
522
561
|
.filter(d => d.filesystem.includes('/') || d.size.includes('G') || d.size.includes('T'));
|
package/src/monitors/types.ts
CHANGED
package/src/state/config.ts
CHANGED
|
@@ -34,7 +34,11 @@ export interface PyreConfig {
|
|
|
34
34
|
p2pPassword?: string;
|
|
35
35
|
splashEnabled?: boolean;
|
|
36
36
|
splashColorScheme?: string;
|
|
37
|
-
|
|
37
|
+
watchdogProcess?: string;
|
|
38
|
+
watchdogCpu?: number;
|
|
39
|
+
watchdogMem?: number;
|
|
40
|
+
webhookUrl?: string;
|
|
41
|
+
alertCmd?: string;
|
|
38
42
|
visiblePanels?: {
|
|
39
43
|
cpu?: boolean;
|
|
40
44
|
mem?: boolean;
|
|
@@ -49,6 +53,7 @@ export interface PyreConfig {
|
|
|
49
53
|
process?: boolean;
|
|
50
54
|
tree?: boolean;
|
|
51
55
|
};
|
|
56
|
+
panelLayout?: string[];
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
@@ -64,12 +69,17 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
64
69
|
mouseEnabled: true,
|
|
65
70
|
cpuAlertPct: 90,
|
|
66
71
|
tempAlertC: 95,
|
|
72
|
+
watchdogProcess: '',
|
|
73
|
+
watchdogCpu: 80,
|
|
74
|
+
watchdogMem: 80,
|
|
67
75
|
notificationsEnabled: true,
|
|
68
76
|
p2pPort: 9876,
|
|
69
77
|
p2pPassword: 'pyre',
|
|
70
78
|
splashEnabled: true,
|
|
71
79
|
splashColorScheme: 'fire',
|
|
72
80
|
splashAnimation: 'classic',
|
|
81
|
+
webhookUrl: '',
|
|
82
|
+
alertCmd: '',
|
|
73
83
|
visiblePanels: {
|
|
74
84
|
cpu: true,
|
|
75
85
|
mem: true,
|
|
@@ -84,6 +94,7 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
84
94
|
process: true,
|
|
85
95
|
tree: false,
|
|
86
96
|
},
|
|
97
|
+
panelLayout: ['mem', 'disk', 'net'],
|
|
87
98
|
};
|
|
88
99
|
|
|
89
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
|
+
}
|