pyre-cli 1.3.0 → 1.4.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 +209 -16
- package/dist/index.js +735 -332
- package/package.json +5 -2
- package/parity.md +542 -0
- package/src/formatters/index.ts +17 -0
- package/src/formatters/output.ts +137 -0
- package/src/formatters/render.ts +468 -0
- package/src/formatters/themes.ts +115 -0
- package/src/formatters/types.ts +58 -0
- package/src/history.ts +115 -0
- package/src/index.ts +81 -0
- package/src/live/export.ts +73 -0
- package/src/live/index.ts +16 -0
- package/src/live/input.ts +311 -0
- package/src/live/render.ts +142 -0
- package/src/live/state.ts +83 -0
- package/src/live/types.ts +23 -0
- package/src/monitors/collectors.ts +484 -0
- package/src/monitors/index.ts +14 -0
- package/src/monitors/run.ts +18 -0
- package/src/monitors/smc.ts +107 -0
- package/src/monitors/types.ts +107 -0
- package/src/pyre_img +0 -0
- package/src/sparkline.ts +34 -0
- package/src/splash.ts +102 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live dashboard rendering.
|
|
3
|
+
*
|
|
4
|
+
* Contains the main `render` loop, the footer key-bindings
|
|
5
|
+
* line, the UI customizer overlay, and the thermal/CPU
|
|
6
|
+
* alert checker. All state variables are imported from
|
|
7
|
+
* `state.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName } from '../formatters/index.js';
|
|
11
|
+
import { state, setStatus, SIGNAL_OPTIONS } from './state.js';
|
|
12
|
+
import type { StatsData } from '../monitors/index.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How many process rows we can realistically fit given
|
|
16
|
+
* current terminal height. Computed from the number of
|
|
17
|
+
* card columns and the reserved space for the header,
|
|
18
|
+
* cards, graphs, and footer.
|
|
19
|
+
*/
|
|
20
|
+
function processRowBudget(): number {
|
|
21
|
+
const columns = gridColumns(state.termWidth);
|
|
22
|
+
const cardRows = Math.ceil(6 / columns);
|
|
23
|
+
const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
|
|
24
|
+
return Math.max(3, Math.min(40, state.termHeight - reserved));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderCustomizerOverlay(): string {
|
|
28
|
+
const themesList = Object.keys(THEMES) as ThemeName[];
|
|
29
|
+
const lines = [
|
|
30
|
+
chalk.bgCyan.black.bold(' UI CUSTOMIZER (Use ↑/↓ or J/K to navigate, Enter/Space to toggle, Esc to exit) '),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
|
|
34
|
+
const isSelected = idx === state.customizerIndex;
|
|
35
|
+
const prefix = isSelected ? chalk.yellow('▶ ') : ' ';
|
|
36
|
+
|
|
37
|
+
if (opt === 'Theme') {
|
|
38
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.currentTheme)} [${themesList.join(', ')}]`);
|
|
39
|
+
} else if (opt === 'Graph Mode') {
|
|
40
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.graphMode)} [spark, bar]`);
|
|
41
|
+
} else if (opt === 'Toggle Tree View') {
|
|
42
|
+
|
|
43
|
+
const status = state.treeView ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
|
|
44
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
45
|
+
} else {
|
|
46
|
+
const key = opt.replace('Toggle ', '').toLowerCase();
|
|
47
|
+
const isVisible = state.visiblePanels[key as keyof typeof state.visiblePanels] !== false;
|
|
48
|
+
const status = isVisible ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
|
|
49
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return lines.map(l => ` ${l}`).join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function render() {
|
|
57
|
+
if (!state.lastData) return;
|
|
58
|
+
const lines: string[] = [];
|
|
59
|
+
lines.push(
|
|
60
|
+
formatTable(state.lastData, {
|
|
61
|
+
width: state.termWidth,
|
|
62
|
+
sortBy: state.sortMode,
|
|
63
|
+
filter: state.processFilter || undefined,
|
|
64
|
+
processLimit: processRowBudget(),
|
|
65
|
+
theme: state.currentTheme,
|
|
66
|
+
visible: state.visiblePanels,
|
|
67
|
+
treeView: state.treeView,
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (state.showGraphs) {
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(footerLine());
|
|
78
|
+
|
|
79
|
+
if (state.inputMode === 'customizer') {
|
|
80
|
+
lines.push(renderCustomizerOverlay());
|
|
81
|
+
} else if (state.inputMode === 'filter') {
|
|
82
|
+
lines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
83
|
+
} else if (state.inputMode === 'kill') {
|
|
84
|
+
lines.push(chalk.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
|
|
85
|
+
} else if (state.inputMode === 'signal') {
|
|
86
|
+
const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
|
|
87
|
+
const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk.yellow(s) : chalk.dim(s)).join(' ');
|
|
88
|
+
lines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
|
|
89
|
+
lines.push(chalk.dim(` ${sigList}`));
|
|
90
|
+
lines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
|
|
91
|
+
} else if (state.statusMessage) {
|
|
92
|
+
lines.push(` ${state.statusMessage}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
96
|
+
process.stdout.write(lines.join('\n'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function footerLine(): string {
|
|
100
|
+
const keys: [string, string][] = [
|
|
101
|
+
['q', 'quit'],
|
|
102
|
+
['c', 'customize UI'],
|
|
103
|
+
['p', state.paused ? 'resume' : 'pause'],
|
|
104
|
+
['g', state.showGraphs ? 'hide graphs' : 'show graphs'],
|
|
105
|
+
['b', `graph:${state.graphMode}`],
|
|
106
|
+
['d', state.detailed ? 'basic' : 'detailed'],
|
|
107
|
+
|
|
108
|
+
['s', `sort:${state.sortMode}`],
|
|
109
|
+
['/', 'filter'],
|
|
110
|
+
['k', 'kill'],
|
|
111
|
+
['S', 'signal'],
|
|
112
|
+
['t', state.treeView ? 'flat' : 'tree'],
|
|
113
|
+
['e', 'export'],
|
|
114
|
+
['l', state.logging ? 'stop log' : 'log'],
|
|
115
|
+
['f', state.exportFormat],
|
|
116
|
+
['+/-', `${state.interval}s`],
|
|
117
|
+
];
|
|
118
|
+
const keyStr = keys.map(([k, label]) => `${chalk.hex('#50fa7b').bold(k)} ${chalk.dim(label)}`).join(chalk.dim(' | '));
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
const badges: string[] = [];
|
|
122
|
+
if (state.paused) badges.push(chalk.yellow.bold('⏸ PAUSED'));
|
|
123
|
+
if (state.logging) badges.push(chalk.red.bold('● REC'));
|
|
124
|
+
const badgeStr = badges.join(' ');
|
|
125
|
+
|
|
126
|
+
return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkAlerts(data: StatsData) {
|
|
130
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
131
|
+
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || (temp !== null && temp >= state.TEMP_ALERT_C);
|
|
132
|
+
if (hot && !state.alerted) {
|
|
133
|
+
state.alerted = true;
|
|
134
|
+
process.stdout.write('\x07');
|
|
135
|
+
const reason = data.cpu.usage >= state.CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}°C`;
|
|
136
|
+
setStatus(chalk.red(`⚠ Alert: ${reason}`), 5000);
|
|
137
|
+
} else if (!hot) {
|
|
138
|
+
state.alerted = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live dashboard session state.
|
|
3
|
+
*
|
|
4
|
+
* All mutable state for the interactive live mode lives
|
|
5
|
+
* in this module so that render, input, and export
|
|
6
|
+
* sub-modules can share it without circular dependencies.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import readline from 'node:readline';
|
|
10
|
+
import { History } from '../history.js';
|
|
11
|
+
import type { StatsData } from '../monitors/index.js';
|
|
12
|
+
import type { ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
|
|
13
|
+
import type { ThemeName, VisibleItems } from '../formatters/types.js';
|
|
14
|
+
|
|
15
|
+
export const SIGNAL_OPTIONS = ['SIGTERM', 'SIGKILL', 'SIGINT', 'SIGHUP', 'SIGSTOP', 'SIGCONT'] as const;
|
|
16
|
+
|
|
17
|
+
const state = {
|
|
18
|
+
intervalHandle: null as NodeJS.Timeout | null,
|
|
19
|
+
running: false,
|
|
20
|
+
paused: false,
|
|
21
|
+
detailed: false,
|
|
22
|
+
interval: 2,
|
|
23
|
+
showGraphs: true,
|
|
24
|
+
graphMode: 'spark' as GraphMode,
|
|
25
|
+
exportFormat: 'json' as ExportFormat,
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
exportDir: './pyre-exports',
|
|
29
|
+
currentTheme: 'default' as ThemeName,
|
|
30
|
+
visiblePanels: {
|
|
31
|
+
cpu: true,
|
|
32
|
+
mem: true,
|
|
33
|
+
power: true,
|
|
34
|
+
battery: true,
|
|
35
|
+
thermal: true,
|
|
36
|
+
network: true,
|
|
37
|
+
disk: true,
|
|
38
|
+
process: true,
|
|
39
|
+
} as VisibleItems,
|
|
40
|
+
logging: false,
|
|
41
|
+
logStream: null as fs.WriteStream | null,
|
|
42
|
+
statusMessage: '',
|
|
43
|
+
statusTimer: null as NodeJS.Timeout | null,
|
|
44
|
+
lastData: null as StatsData | null,
|
|
45
|
+
history: new History(40),
|
|
46
|
+
keypressHandler: null as ((str: string, key: readline.Key) => void) | null,
|
|
47
|
+
termWidth: process.stdout.columns || 80,
|
|
48
|
+
termHeight: process.stdout.rows || 24,
|
|
49
|
+
sortMode: 'cpu' as SortMode,
|
|
50
|
+
processFilter: '',
|
|
51
|
+
inputMode: null as InputMode,
|
|
52
|
+
inputBuffer: '',
|
|
53
|
+
treeView: false,
|
|
54
|
+
SIGNAL_OPTIONS,
|
|
55
|
+
customizerIndex: 0,
|
|
56
|
+
CUSTOMIZER_OPTIONS: [
|
|
57
|
+
'Theme',
|
|
58
|
+
'Graph Mode',
|
|
59
|
+
'Toggle CPU',
|
|
60
|
+
|
|
61
|
+
'Toggle Memory',
|
|
62
|
+
'Toggle Power',
|
|
63
|
+
'Toggle Battery',
|
|
64
|
+
'Toggle Thermal',
|
|
65
|
+
'Toggle Network',
|
|
66
|
+
'Toggle Disk',
|
|
67
|
+
'Toggle Processes',
|
|
68
|
+
'Toggle Tree View',
|
|
69
|
+
],
|
|
70
|
+
CPU_ALERT_PCT: 90,
|
|
71
|
+
TEMP_ALERT_C: 95,
|
|
72
|
+
alerted: false,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function setStatus(msg: string, ms = 3000) {
|
|
76
|
+
state.statusMessage = msg;
|
|
77
|
+
if (state.statusTimer) clearTimeout(state.statusTimer);
|
|
78
|
+
state.statusTimer = setTimeout(() => {
|
|
79
|
+
state.statusMessage = '';
|
|
80
|
+
}, ms);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { state, setStatus };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live dashboard mode types.
|
|
3
|
+
*
|
|
4
|
+
* Defines the options passed to {@link startLive}, the
|
|
5
|
+
* export format enum, and the input-mode union used
|
|
6
|
+
* throughout the interactive session.
|
|
7
|
+
*/
|
|
8
|
+
export interface LiveOptions {
|
|
9
|
+
interval: number;
|
|
10
|
+
detailed?: boolean;
|
|
11
|
+
exportDir?: string;
|
|
12
|
+
autoLog?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ExportFormat = 'json' | 'csv' | 'tsv';
|
|
16
|
+
|
|
17
|
+
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer';
|
|
18
|
+
|
|
19
|
+
type SortMode = 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
|
|
20
|
+
|
|
21
|
+
type GraphMode = 'spark' | 'bar';
|
|
22
|
+
|
|
23
|
+
export type { InputMode, SortMode, GraphMode };
|