pyre-cli 4.17.0 → 5.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 +197 -255
- package/dist/index.js +958 -81
- package/package.json +1 -1
- package/src/anomalies.ts +102 -0
- package/src/formatters/index.ts +3 -3
- package/src/formatters/output.ts +341 -8
- package/src/formatters/render.ts +53 -16
- package/src/formatters/types.ts +27 -16
- package/src/history.ts +57 -13
- package/src/index.ts +204 -11
- package/src/live/export.ts +64 -7
- package/src/live/input.ts +78 -34
- package/src/live/render.ts +114 -66
- package/src/live/state.ts +76 -70
- package/src/live/types.ts +4 -1
- package/src/monitors/collectors.ts +23 -1
- package/src/monitors/types.ts +1 -0
- package/src/state/config.ts +130 -0
package/src/live/render.ts
CHANGED
|
@@ -6,11 +6,28 @@
|
|
|
6
6
|
* alert checker. All state variables are imported from
|
|
7
7
|
* `state.ts`.
|
|
8
8
|
*/
|
|
9
|
+
import { execFile } from 'node:child_process';
|
|
9
10
|
import chalk from 'chalk';
|
|
10
11
|
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel } from '../formatters/index.js';
|
|
12
|
+
import { detectAnomalies } from '../anomalies.js';
|
|
11
13
|
import { state, setStatus, SIGNAL_OPTIONS, getToggleKey } from './state.js';
|
|
12
14
|
import type { StatsData } from '../monitors/index.js';
|
|
13
15
|
|
|
16
|
+
function formatTempForUnit(c: number): string {
|
|
17
|
+
if (state.tempUnit === 'f') {
|
|
18
|
+
const f = c * (9 / 5) + 32;
|
|
19
|
+
return `${f.toFixed(1)}°F`;
|
|
20
|
+
}
|
|
21
|
+
return `${c.toFixed(1)}°C`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function applyTempUnit(text: string): string {
|
|
25
|
+
if (state.tempUnit === 'f') {
|
|
26
|
+
return text.replace(/([\d.]+)°C \/ ([\d.]+)°F/g, '$2°F');
|
|
27
|
+
}
|
|
28
|
+
return text.replace(/([\d.]+)°C \/ ([\d.]+)°F/g, '$1°C');
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
// --- render caching -----------------------------------------------------------
|
|
15
32
|
|
|
16
33
|
let _cachedTable = '';
|
|
@@ -103,28 +120,33 @@ function renderCustomizerOverlay(): string {
|
|
|
103
120
|
chalk.bgCyan.black.bold(' UI CUSTOMIZER (Use ↑/↓ or J/K to navigate, Enter/Space to toggle, Esc to exit) '),
|
|
104
121
|
];
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
|
|
124
|
+
const isSelected = idx === state.customizerIndex;
|
|
125
|
+
const prefix = isSelected ? chalk.yellow('▶ ') : ' ';
|
|
126
|
+
|
|
127
|
+
if (opt === 'Theme') {
|
|
128
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.currentTheme)} [${themesList.join(', ')}]`);
|
|
129
|
+
} else if (opt === 'Graph Mode') {
|
|
130
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.graphMode)} [spark, bar]`);
|
|
131
|
+
} else if (opt === 'Splash Screen') {
|
|
132
|
+
const status = state.splashEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
|
|
133
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
134
|
+
} else if (opt === 'Splash Color') {
|
|
135
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
|
|
136
|
+
} else if (opt === 'Splash Animation') {
|
|
137
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
|
|
138
|
+
} else if (opt === 'Notifications') {
|
|
139
|
+
const status = state.notificationsEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
|
|
140
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
141
|
+
} else if (opt === 'Temperature Unit') {
|
|
142
|
+
lines.push(`${prefix}${opt}: ${chalk.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
|
|
143
|
+
} else {
|
|
144
|
+
const toggleKey = getToggleKey(opt);
|
|
145
|
+
const isVisible = toggleKey ? (toggleKey === 'tree' ? state.treeView : state.visiblePanels[toggleKey] !== false) : true;
|
|
146
|
+
const status = isVisible ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
|
|
147
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
128
150
|
|
|
129
151
|
return lines.map(l => ` ${l}`).join('\n');
|
|
130
152
|
}
|
|
@@ -170,26 +192,31 @@ function render() {
|
|
|
170
192
|
const lines: string[] = [];
|
|
171
193
|
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
172
194
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
const params = tableCacheParams();
|
|
196
|
+
const anomalies = detectAnomalies(state.lastData, state.history);
|
|
197
|
+
const anomalyCacheKey = anomalies.map(a => `${a.metric}:${a.zScore.toFixed(2)}`).join(',');
|
|
198
|
+
const cacheParams = params + '|' + anomalyCacheKey;
|
|
199
|
+
const tableStr = _tableDataDirty || state.lastData !== _cachedTableData || cacheParams !== _cachedTableParams
|
|
200
|
+
? (() => {
|
|
201
|
+
const out = formatTable(state.lastData, {
|
|
202
|
+
width: state.termWidth,
|
|
203
|
+
sortBy: state.sortMode,
|
|
204
|
+
filter: state.processFilter || undefined,
|
|
205
|
+
processLimit: processRowBudget(),
|
|
206
|
+
theme: state.currentTheme,
|
|
207
|
+
visible: state.visiblePanels,
|
|
208
|
+
treeView: state.treeView,
|
|
209
|
+
activePanel: state.activePanel,
|
|
210
|
+
anomalies,
|
|
211
|
+
tempUnit: state.tempUnit,
|
|
212
|
+
});
|
|
213
|
+
_cachedTableData = state.lastData;
|
|
214
|
+
_cachedTableParams = cacheParams;
|
|
215
|
+
_cachedTable = applyTempUnit(out);
|
|
216
|
+
_tableDataDirty = false;
|
|
217
|
+
return _cachedTable;
|
|
218
|
+
})()
|
|
219
|
+
: _cachedTable;
|
|
193
220
|
|
|
194
221
|
const bodyLines: string[] = [];
|
|
195
222
|
if (state.activePanel === 'p2p') {
|
|
@@ -206,8 +233,8 @@ function render() {
|
|
|
206
233
|
const out = formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode);
|
|
207
234
|
_cachedGraphsVersion = state.history.version;
|
|
208
235
|
_cachedGraphsParams = graphsParams;
|
|
209
|
-
_cachedGraphs = out;
|
|
210
|
-
return
|
|
236
|
+
_cachedGraphs = applyTempUnit(out);
|
|
237
|
+
return _cachedGraphs;
|
|
211
238
|
})()
|
|
212
239
|
: _cachedGraphs;
|
|
213
240
|
|
|
@@ -254,16 +281,17 @@ function render() {
|
|
|
254
281
|
|
|
255
282
|
function footerLine(): string {
|
|
256
283
|
const row1: [string, string][] = [
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
284
|
+
['q', 'quit'],
|
|
285
|
+
['c', 'customize UI'],
|
|
286
|
+
['←/→', 'tab'],
|
|
287
|
+
['1-9', 'panels'],
|
|
288
|
+
['g', state.showGraphs ? 'hide graphs' : 'show graphs'],
|
|
289
|
+
['b', `graph:${state.graphMode}`],
|
|
290
|
+
['t', state.treeView ? 'flat' : 'tree'],
|
|
291
|
+
['d', state.detailed ? 'basic' : 'detailed'],
|
|
292
|
+
['T', `temp:${state.tempUnit}`],
|
|
293
|
+
['p', state.paused ? 'resume' : 'pause'],
|
|
294
|
+
];
|
|
267
295
|
const row2: [string, string][] = [
|
|
268
296
|
['s', `sort:${state.sortMode}`],
|
|
269
297
|
['/', 'filter'],
|
|
@@ -286,20 +314,40 @@ function footerLine(): string {
|
|
|
286
314
|
if (state.p2pServerRunning) badges.push(chalk.green.bold(`P2P ${state.p2pBind || '0.0.0.0'}:${state.p2pPort}`));
|
|
287
315
|
const badgeStr = badges.join(' ');
|
|
288
316
|
|
|
289
|
-
|
|
290
|
-
|
|
317
|
+
return badgeStr ? `${str1}\n${str2} ${badgeStr}` : `${str1}\n${str2}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function sendNotification(title: string, message: string) {
|
|
321
|
+
if (!state.notificationsEnabled) return;
|
|
322
|
+
execFile('osascript', ['-e', `display notification "${message}" with title "${title}"`], (err) => {
|
|
323
|
+
if (err) {
|
|
324
|
+
// Silently ignore notification failures (e.g., terminal not in focus, no notification permission)
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
291
328
|
|
|
292
329
|
function checkAlerts(data: StatsData) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
330
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
331
|
+
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || (temp !== null && temp >= state.TEMP_ALERT_C);
|
|
332
|
+
const anomalies = detectAnomalies(data, state.history);
|
|
333
|
+
const hasCriticalAnomaly = anomalies.some(a => a.severity === 'critical');
|
|
334
|
+
const hasWarningAnomaly = anomalies.some(a => a.severity === 'warning');
|
|
335
|
+
const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
|
|
336
|
+
|
|
337
|
+
if ((hot || anomalyTriggered) && !state.alerted) {
|
|
338
|
+
state.alerted = true;
|
|
339
|
+
process.stdout.write('\x07');
|
|
340
|
+
const reasons: string[] = [];
|
|
341
|
+
if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
|
|
342
|
+
if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
|
|
343
|
+
for (const a of anomalies) {
|
|
344
|
+
reasons.push(`${a.metric} anomaly (σ=${a.zScore.toFixed(1)})`);
|
|
345
|
+
}
|
|
346
|
+
setStatus(chalk.red(`⚠ Alert: ${reasons.join('; ')}`), 5000);
|
|
347
|
+
sendNotification('pyre Alert', reasons.join('; '));
|
|
348
|
+
} else if (!hot && !anomalyTriggered) {
|
|
349
|
+
state.alerted = false;
|
|
350
|
+
}
|
|
302
351
|
}
|
|
303
|
-
}
|
|
304
352
|
|
|
305
353
|
export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget, invalidateTableCache, invalidateFrame };
|
package/src/live/state.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { History } from '../history.js';
|
|
|
11
11
|
import type { StatsData } from '../monitors/index.js';
|
|
12
12
|
import type { ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
|
|
13
13
|
import type { ThemeName, VisibleItems } from '../formatters/types.js';
|
|
14
|
+
import { readConfig, type PyreConfig } from '../state/config.js';
|
|
14
15
|
|
|
15
16
|
export const SIGNAL_OPTIONS = ['SIGTERM', 'SIGKILL', 'SIGINT', 'SIGHUP', 'SIGSTOP', 'SIGCONT'] as const;
|
|
16
17
|
|
|
@@ -18,54 +19,61 @@ import { type SplashColorScheme, type SplashAnimation } from '../splash.js';
|
|
|
18
19
|
|
|
19
20
|
export { type SplashColorScheme, type SplashAnimation };
|
|
20
21
|
|
|
22
|
+
const config = readConfig();
|
|
23
|
+
|
|
21
24
|
const state = {
|
|
22
25
|
intervalHandle: null as NodeJS.Timeout | null,
|
|
23
26
|
uiIntervalHandle: null as NodeJS.Timeout | null,
|
|
24
27
|
running: false,
|
|
25
28
|
paused: false,
|
|
26
|
-
detailed:
|
|
27
|
-
interval:
|
|
28
|
-
showGraphs:
|
|
29
|
-
graphMode:
|
|
29
|
+
detailed: config.detailed,
|
|
30
|
+
interval: config.interval,
|
|
31
|
+
showGraphs: config.showGraphs,
|
|
32
|
+
graphMode: config.graphMode as GraphMode,
|
|
30
33
|
exportFormat: 'json' as ExportFormat,
|
|
34
|
+
tempUnit: 'c' as 'c' | 'f',
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
customizerIndex: 0,
|
|
36
|
+
exportDir: config.exportDir,
|
|
37
|
+
currentTheme: config.theme as ThemeName,
|
|
38
|
+
visiblePanels: {
|
|
39
|
+
cpu: config.visiblePanels.cpu,
|
|
40
|
+
mem: config.visiblePanels.mem,
|
|
41
|
+
gpu: config.visiblePanels.gpu,
|
|
42
|
+
power: config.visiblePanels.power,
|
|
43
|
+
battery: config.visiblePanels.battery,
|
|
44
|
+
thermal: config.visiblePanels.thermal,
|
|
45
|
+
network: config.visiblePanels.network,
|
|
46
|
+
packets: config.visiblePanels.packets,
|
|
47
|
+
tasks: config.visiblePanels.tasks,
|
|
48
|
+
disk: config.visiblePanels.disk,
|
|
49
|
+
process: config.visiblePanels.process,
|
|
50
|
+
} as VisibleItems,
|
|
51
|
+
logging: false,
|
|
52
|
+
logStream: null as fs.WriteStream | null,
|
|
53
|
+
statusMessage: '',
|
|
54
|
+
statusTimer: null as NodeJS.Timeout | null,
|
|
55
|
+
lastData: null as StatsData | null,
|
|
56
|
+
history: new History(40),
|
|
57
|
+
keypressHandler: null as ((str: string, key: readline.Key) => void) | null,
|
|
58
|
+
termWidth: process.stdout.columns || 80,
|
|
59
|
+
termHeight: process.stdout.rows || 24,
|
|
60
|
+
sortMode: config.sortMode as SortMode,
|
|
61
|
+
processFilter: '',
|
|
62
|
+
inputMode: null as InputMode,
|
|
63
|
+
inputBuffer: '',
|
|
64
|
+
targetPid: '',
|
|
65
|
+
selectedSignal: 'SIGTERM' as typeof SIGNAL_OPTIONS[number],
|
|
66
|
+
treeView: config.treeView,
|
|
67
|
+
SIGNAL_OPTIONS,
|
|
68
|
+
customizerIndex: 0,
|
|
66
69
|
CUSTOMIZER_OPTIONS: [
|
|
67
70
|
'Theme',
|
|
68
71
|
'Graph Mode',
|
|
72
|
+
'Splash Screen',
|
|
73
|
+
'Splash Color',
|
|
74
|
+
'Splash Animation',
|
|
75
|
+
'Notifications',
|
|
76
|
+
'Temperature Unit',
|
|
69
77
|
'Toggle CPU',
|
|
70
78
|
'Toggle Memory',
|
|
71
79
|
'Toggle GPU',
|
|
@@ -78,39 +86,37 @@ const state = {
|
|
|
78
86
|
'Toggle Disk',
|
|
79
87
|
'Toggle Processes',
|
|
80
88
|
'Toggle Tree View',
|
|
81
|
-
'Splash Screen',
|
|
82
|
-
'Splash Color',
|
|
83
|
-
'Splash Animation',
|
|
84
89
|
],
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
90
|
+
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
91
|
+
TEMP_ALERT_C: config.tempAlertC,
|
|
92
|
+
notificationsEnabled: config.notificationsEnabled,
|
|
93
|
+
alerted: false,
|
|
94
|
+
activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p',
|
|
95
|
+
mouseEnabled: config.mouseEnabled,
|
|
96
|
+
PANEL_TABS: [
|
|
97
|
+
{ id: 'cpu', label: 'CPU', key: '1' },
|
|
98
|
+
{ id: 'mem', label: 'Memory', key: '2' },
|
|
99
|
+
{ id: 'gpu', label: 'GPU', key: '3' },
|
|
100
|
+
{ id: 'power', label: 'Power', key: '4' },
|
|
101
|
+
{ id: 'battery', label: 'Battery', key: '5' },
|
|
102
|
+
{ id: 'thermal', label: 'Thermal', key: '6' },
|
|
103
|
+
{ id: 'network', label: 'Network', key: '7' },
|
|
104
|
+
{ id: 'packets', label: 'Packets', key: '8' },
|
|
105
|
+
{ id: 'tasks', label: 'Tasks', key: '9' },
|
|
106
|
+
{ id: 'disk', label: 'Disk', key: '0' },
|
|
107
|
+
{ id: 'process', label: 'Process', key: 'P' },
|
|
108
|
+
{ id: 'p2p', label: 'P2P', key: 'R' },
|
|
109
|
+
] as const,
|
|
110
|
+
p2pEvents: [] as { type: string; detail: string; ts: number }[],
|
|
111
|
+
p2pBind: '',
|
|
112
|
+
p2pServer: null as { stop: () => void; peerEventHistory: { type: string; detail: string; ts: number }[] } | null,
|
|
113
|
+
p2pServerRunning: false,
|
|
114
|
+
p2pPort: config.p2pPort,
|
|
115
|
+
p2pPassword: config.p2pPassword,
|
|
116
|
+
splashEnabled: config.splashEnabled,
|
|
117
|
+
splashColorScheme: config.splashColorScheme as SplashColorScheme,
|
|
118
|
+
splashAnimation: config.splashAnimation as SplashAnimation,
|
|
119
|
+
};
|
|
114
120
|
|
|
115
121
|
function setStatus(msg: string, ms = 3000) {
|
|
116
122
|
state.statusMessage = msg;
|
package/src/live/types.ts
CHANGED
|
@@ -11,9 +11,12 @@ export interface LiveOptions {
|
|
|
11
11
|
exportDir?: string;
|
|
12
12
|
autoLog?: boolean;
|
|
13
13
|
theme?: ThemeName;
|
|
14
|
+
alertCpu?: number;
|
|
15
|
+
alertTemp?: number;
|
|
16
|
+
tempUnit?: 'c' | 'f';
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export type ExportFormat = 'json' | 'csv' | 'tsv';
|
|
19
|
+
export type ExportFormat = 'json' | 'csv' | 'tsv' | 'html' | 'md';
|
|
17
20
|
|
|
18
21
|
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p';
|
|
19
22
|
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
* returning their best-effort values.
|
|
13
13
|
*/
|
|
14
14
|
import chalk from 'chalk';
|
|
15
|
+
import os from 'node:os';
|
|
15
16
|
import { run } from './run.js';
|
|
16
17
|
import { getSmcMetrics, parseSuffix } from './smc.js';
|
|
17
18
|
import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess } from './types.js';
|
|
18
19
|
|
|
19
20
|
const SP_TTL_MS = 10_000;
|
|
20
21
|
const NETSTAT_TTL_MS = 1000;
|
|
22
|
+
let prevCpuTimes: { total: number; idle: number }[] | null = null;
|
|
21
23
|
const ROUTE_TTL_MS = 5_000;
|
|
22
24
|
const SYSCTL_TTL_MS = 60_000;
|
|
23
25
|
|
|
@@ -338,7 +340,27 @@ export async function collectCpu(): Promise<CpuData> {
|
|
|
338
340
|
// ignore
|
|
339
341
|
}
|
|
340
342
|
|
|
341
|
-
|
|
343
|
+
let coreUsage: number[] = [];
|
|
344
|
+
try {
|
|
345
|
+
const cpus = os.cpus();
|
|
346
|
+
const current = cpus.map(cpu => ({
|
|
347
|
+
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
348
|
+
idle: cpu.times.idle,
|
|
349
|
+
}));
|
|
350
|
+
if (prevCpuTimes) {
|
|
351
|
+
coreUsage = prevCpuTimes.map((prev, i) => {
|
|
352
|
+
const totalDelta = current[i].total - prev.total;
|
|
353
|
+
const idleDelta = current[i].idle - prev.idle;
|
|
354
|
+
if (totalDelta === 0) return 0;
|
|
355
|
+
return Math.round(((totalDelta - idleDelta) / totalDelta) * 100);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
prevCpuTimes = current;
|
|
359
|
+
} catch {
|
|
360
|
+
// ignore
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature, coreUsage };
|
|
342
364
|
}
|
|
343
365
|
|
|
344
366
|
export async function collectMemory(): Promise<MemoryData> {
|
package/src/monitors/types.ts
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre config file module.
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes `~/.config/pyre/config.json` to persist
|
|
5
|
+
* user preferences across sessions.
|
|
6
|
+
*
|
|
7
|
+
* The config file stores theme, interval, panel visibility,
|
|
8
|
+
* sort mode, tree view, graph mode, alert thresholds, P2P
|
|
9
|
+
* defaults, export directory, and splash preferences.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'pyre');
|
|
17
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
18
|
+
|
|
19
|
+
export interface PyreConfig {
|
|
20
|
+
theme?: string;
|
|
21
|
+
interval?: number;
|
|
22
|
+
exportDir?: string;
|
|
23
|
+
detailed?: boolean;
|
|
24
|
+
sortMode?: string;
|
|
25
|
+
treeView?: boolean;
|
|
26
|
+
graphMode?: string;
|
|
27
|
+
showGraphs?: boolean;
|
|
28
|
+
autoLog?: boolean;
|
|
29
|
+
mouseEnabled?: boolean;
|
|
30
|
+
cpuAlertPct?: number;
|
|
31
|
+
tempAlertC?: number;
|
|
32
|
+
notificationsEnabled?: boolean;
|
|
33
|
+
p2pPort?: number;
|
|
34
|
+
p2pPassword?: string;
|
|
35
|
+
splashEnabled?: boolean;
|
|
36
|
+
splashColorScheme?: string;
|
|
37
|
+
splashAnimation?: string;
|
|
38
|
+
visiblePanels?: {
|
|
39
|
+
cpu?: boolean;
|
|
40
|
+
mem?: boolean;
|
|
41
|
+
gpu?: boolean;
|
|
42
|
+
power?: boolean;
|
|
43
|
+
battery?: boolean;
|
|
44
|
+
thermal?: boolean;
|
|
45
|
+
network?: boolean;
|
|
46
|
+
packets?: boolean;
|
|
47
|
+
tasks?: boolean;
|
|
48
|
+
disk?: boolean;
|
|
49
|
+
process?: boolean;
|
|
50
|
+
tree?: boolean;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
55
|
+
theme: 'default',
|
|
56
|
+
interval: 2,
|
|
57
|
+
exportDir: './pyre-exports',
|
|
58
|
+
detailed: false,
|
|
59
|
+
sortMode: 'cpu',
|
|
60
|
+
treeView: false,
|
|
61
|
+
graphMode: 'spark',
|
|
62
|
+
showGraphs: true,
|
|
63
|
+
autoLog: false,
|
|
64
|
+
mouseEnabled: true,
|
|
65
|
+
cpuAlertPct: 90,
|
|
66
|
+
tempAlertC: 95,
|
|
67
|
+
notificationsEnabled: true,
|
|
68
|
+
p2pPort: 9876,
|
|
69
|
+
p2pPassword: 'pyre',
|
|
70
|
+
splashEnabled: true,
|
|
71
|
+
splashColorScheme: 'fire',
|
|
72
|
+
splashAnimation: 'classic',
|
|
73
|
+
visiblePanels: {
|
|
74
|
+
cpu: true,
|
|
75
|
+
mem: true,
|
|
76
|
+
gpu: true,
|
|
77
|
+
power: true,
|
|
78
|
+
battery: true,
|
|
79
|
+
thermal: true,
|
|
80
|
+
network: true,
|
|
81
|
+
packets: true,
|
|
82
|
+
tasks: true,
|
|
83
|
+
disk: true,
|
|
84
|
+
process: true,
|
|
85
|
+
tree: false,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function readConfig(): Required<PyreConfig> {
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
92
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
93
|
+
const parsed = JSON.parse(raw) as Partial<PyreConfig>;
|
|
94
|
+
return deepMerge(DEFAULT_CONFIG, parsed);
|
|
95
|
+
} catch {
|
|
96
|
+
return { ...DEFAULT_CONFIG };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function writeConfig(config: Partial<PyreConfig>): void {
|
|
101
|
+
try {
|
|
102
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
103
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
const merged = deepMerge(readConfig(), config);
|
|
106
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n');
|
|
107
|
+
} catch {
|
|
108
|
+
// ignore write errors
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function deepMerge(target: any, source: any): any {
|
|
113
|
+
const result = { ...target };
|
|
114
|
+
for (const key of Object.keys(source)) {
|
|
115
|
+
if (source[key] !== undefined && source[key] !== null) {
|
|
116
|
+
if (typeof source[key] === 'object' && !Array.isArray(source[key]) && typeof target[key] === 'object' && !Array.isArray(target[key])) {
|
|
117
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
118
|
+
} else {
|
|
119
|
+
result[key] = source[key];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getConfigPath(): string {
|
|
127
|
+
return CONFIG_FILE;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { CONFIG_FILE };
|