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/src/live/render.ts
CHANGED
|
@@ -107,42 +107,45 @@ function p2pPanelLines(): { title: string; body: string[] } {
|
|
|
107
107
|
return { title, body };
|
|
108
108
|
}
|
|
109
109
|
|
|
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
|
-
|
|
110
|
+
function renderCustomizerOverlay(scrollOffset: number, maxVisibleItems: number): string {
|
|
111
|
+
const themesList = Object.keys(THEMES) as ThemeName[];
|
|
112
|
+
const headerLine = chalk.bgCyan.black.bold(' UI CUSTOMIZER (Use ↑/↓ or J/K to navigate, Enter/Space to toggle, Esc to exit) ');
|
|
113
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
114
|
+
const end = Math.min(scrollOffset + maxVisibleItems, totalItems);
|
|
115
|
+
|
|
116
|
+
const lines = [headerLine];
|
|
117
|
+
|
|
118
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
119
|
+
const opt = state.CUSTOMIZER_OPTIONS[i];
|
|
120
|
+
const isSelected = i === state.customizerIndex;
|
|
121
|
+
const prefix = isSelected ? chalk.yellow('▶ ') : ' ';
|
|
122
|
+
|
|
123
|
+
if (opt === 'Theme') {
|
|
124
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.currentTheme)} [${themesList.join(', ')}]`);
|
|
125
|
+
} else if (opt === 'Graph Mode') {
|
|
126
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.graphMode)} [spark, bar]`);
|
|
127
|
+
} else if (opt === 'Splash Screen') {
|
|
128
|
+
const status = state.splashEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
|
|
129
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
130
|
+
} else if (opt === 'Splash Color') {
|
|
131
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
|
|
132
|
+
} else if (opt === 'Splash Animation') {
|
|
133
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
|
|
134
|
+
} else if (opt === 'Notifications') {
|
|
135
|
+
const status = state.notificationsEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
|
|
136
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
137
|
+
} else if (opt === 'Temperature Unit') {
|
|
138
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
|
|
139
|
+
} else {
|
|
140
|
+
const toggleKey = getToggleKey(opt);
|
|
141
|
+
const isVisible = toggleKey ? (toggleKey === 'tree' ? state.treeView : state.visiblePanels[toggleKey] !== false) : true;
|
|
142
|
+
const status = isVisible ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
|
|
143
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
143
146
|
|
|
144
147
|
return lines.map(l => ` ${l}`).join('\n');
|
|
145
|
-
|
|
148
|
+
}
|
|
146
149
|
|
|
147
150
|
// --- frame diffing -------------------------------------------------------
|
|
148
151
|
// Instead of erasing the whole screen (`\x1b[2J`) and repainting everything
|
|
@@ -255,27 +258,38 @@ function render() {
|
|
|
255
258
|
const footerLine3 = footerLine()[2] || ' ';
|
|
256
259
|
const footerLines = [footerLine1, footerLine2, footerLine3];
|
|
257
260
|
|
|
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
261
|
const footerHeight = 3;
|
|
278
262
|
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
263
|
+
|
|
264
|
+
if (state.inputMode === 'customizer') {
|
|
265
|
+
const preOverlayCount = bodyLines.length;
|
|
266
|
+
const availableForOverlay = targetBodyHeight - preOverlayCount;
|
|
267
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
268
|
+
const headerLines = 1;
|
|
269
|
+
const blankLines = availableForOverlay >= 2 ? 1 : 0;
|
|
270
|
+
const maxVisibleItems = Math.max(0, availableForOverlay - headerLines - blankLines);
|
|
271
|
+
const scrollOffset = totalItems > maxVisibleItems
|
|
272
|
+
? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems))
|
|
273
|
+
: 0;
|
|
274
|
+
|
|
275
|
+
if (blankLines > 0) bodyLines.push('');
|
|
276
|
+
bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split('\n'));
|
|
277
|
+
} else if (state.inputMode === 'filter') {
|
|
278
|
+
bodyLines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
279
|
+
} else if (state.inputMode === 'kill') {
|
|
280
|
+
bodyLines.push(chalk.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
|
|
281
|
+
} else if (state.inputMode === 'signal') {
|
|
282
|
+
const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
|
|
283
|
+
const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk.yellow(s) : chalk.dim(s)).join(' ');
|
|
284
|
+
bodyLines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
|
|
285
|
+
bodyLines.push(chalk.dim(` ${sigList}`));
|
|
286
|
+
bodyLines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
|
|
287
|
+
} else if (state.inputMode === 'p2p') {
|
|
288
|
+
bodyLines.push(chalk.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
|
|
289
|
+
} else if (state.statusMessage) {
|
|
290
|
+
bodyLines.push(` ${state.statusMessage}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
279
293
|
if (bodyLines.length > targetBodyHeight) {
|
|
280
294
|
bodyLines.length = targetBodyHeight;
|
|
281
295
|
}
|
|
@@ -341,20 +355,33 @@ function checkAlerts(data: StatsData) {
|
|
|
341
355
|
const anomalies = detectAnomalies(data, state.history);
|
|
342
356
|
const hasCriticalAnomaly = anomalies.some(a => a.severity === 'critical');
|
|
343
357
|
const hasWarningAnomaly = anomalies.some(a => a.severity === 'warning');
|
|
358
|
+
// Process watchdog check
|
|
359
|
+
let watchdogTriggered = false;
|
|
360
|
+
if (state.watchdogProcess) {
|
|
361
|
+
const matches = data.processes.filter(p => p.command.toLowerCase().includes(state.watchdogProcess.toLowerCase()));
|
|
362
|
+
for (const p of matches) {
|
|
363
|
+
if (p.cpu >= state.watchdogCpu || p.mem >= state.watchdogMem) {
|
|
364
|
+
watchdogTriggered = true;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
344
370
|
const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
|
|
345
371
|
|
|
346
|
-
if ((hot || anomalyTriggered) && !state.alerted) {
|
|
372
|
+
if ((hot || anomalyTriggered || watchdogTriggered) && !state.alerted) {
|
|
347
373
|
state.alerted = true;
|
|
348
374
|
process.stdout.write('\x07');
|
|
349
375
|
const reasons: string[] = [];
|
|
350
376
|
if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
|
|
351
377
|
if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
|
|
378
|
+
if (watchdogTriggered) reasons.push(`Watchdog process "${state.watchdogProcess}" crossed threshold`);
|
|
352
379
|
for (const a of anomalies) {
|
|
353
380
|
reasons.push(`${a.metric} anomaly (σ=${a.zScore.toFixed(1)})`);
|
|
354
381
|
}
|
|
355
382
|
setStatus(chalk.red(`⚠ Alert: ${reasons.join('; ')}`), 5000);
|
|
356
383
|
sendNotification('pyre Alert', reasons.join('; '));
|
|
357
|
-
} else if (!hot && !anomalyTriggered) {
|
|
384
|
+
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
358
385
|
state.alerted = false;
|
|
359
386
|
}
|
|
360
387
|
}
|
package/src/live/state.ts
CHANGED
|
@@ -89,6 +89,9 @@ const state = {
|
|
|
89
89
|
],
|
|
90
90
|
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
91
91
|
TEMP_ALERT_C: config.tempAlertC,
|
|
92
|
+
watchdogProcess: config.watchdogProcess || '',
|
|
93
|
+
watchdogCpu: config.watchdogCpu || 80,
|
|
94
|
+
watchdogMem: config.watchdogMem || 80,
|
|
92
95
|
notificationsEnabled: config.notificationsEnabled,
|
|
93
96
|
alerted: false,
|
|
94
97
|
activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p',
|
|
@@ -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,9 @@ 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;
|
|
38
40
|
visiblePanels?: {
|
|
39
41
|
cpu?: boolean;
|
|
40
42
|
mem?: boolean;
|
|
@@ -64,6 +66,9 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
64
66
|
mouseEnabled: true,
|
|
65
67
|
cpuAlertPct: 90,
|
|
66
68
|
tempAlertC: 95,
|
|
69
|
+
watchdogProcess: '',
|
|
70
|
+
watchdogCpu: 80,
|
|
71
|
+
watchdogMem: 80,
|
|
67
72
|
notificationsEnabled: true,
|
|
68
73
|
p2pPort: 9876,
|
|
69
74
|
p2pPassword: 'pyre',
|