pyre-cli 9.5.1 → 9.5.2

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/src/live/state.ts CHANGED
@@ -23,6 +23,8 @@ export const MENU_OPTIONS = ['Resume Dashboard', 'Settings (Customizer)', 'Readm
23
23
 
24
24
  const config = readConfig();
25
25
 
26
+ const isDockerEnv = fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv');
27
+
26
28
  const state = {
27
29
  intervalHandle: null as NodeJS.Timeout | null,
28
30
  uiIntervalHandle: null as NodeJS.Timeout | null,
@@ -37,19 +39,21 @@ const state = {
37
39
 
38
40
  exportDir: config.exportDir,
39
41
  currentTheme: config.theme as ThemeName,
40
- visiblePanels: {
41
- cpu: config.visiblePanels.cpu,
42
- mem: config.visiblePanels.mem,
43
- gpu: config.visiblePanels.gpu,
44
- power: config.visiblePanels.power,
45
- battery: config.visiblePanels.battery,
46
- thermal: config.visiblePanels.thermal,
47
- network: config.visiblePanels.network,
48
- packets: config.visiblePanels.packets,
49
- tasks: config.visiblePanels.tasks,
50
- disk: config.visiblePanels.disk,
51
- process: config.visiblePanels.process,
52
- } as VisibleItems,
42
+ visiblePanels: {
43
+ cpu: config.visiblePanels.cpu,
44
+ mem: config.visiblePanels.mem,
45
+ gpu: config.visiblePanels.gpu,
46
+ power: config.visiblePanels.power,
47
+ battery: config.visiblePanels.battery,
48
+ thermal: config.visiblePanels.thermal,
49
+ network: config.visiblePanels.network,
50
+ packets: config.visiblePanels.packets,
51
+ tasks: config.visiblePanels.tasks,
52
+ disk: config.visiblePanels.disk,
53
+ process: config.visiblePanels.process,
54
+ containers: config.visiblePanels.containers,
55
+ blender: config.visiblePanels.blender,
56
+ } as VisibleItems,
53
57
  panelLayout: config.panelLayout || ['mem', 'disk', 'net'],
54
58
  logging: false,
55
59
  logStream: null as fs.WriteStream | null,
@@ -66,34 +70,36 @@ const state = {
66
70
  processScrollOffset: 0,
67
71
  trackedPid: null as number | null,
68
72
  inspectingProcess: null as any | null,
69
- inputMode: null as InputMode,
73
+ inputMode: (isDockerEnv && !config.dockerModeConfirmed) ? 'docker-alert' as InputMode : null as InputMode,
70
74
  inputBuffer: '',
71
75
  targetPid: '',
72
76
  selectedSignal: 'SIGTERM' as typeof SIGNAL_OPTIONS[number],
73
77
  treeView: config.treeView,
74
78
  SIGNAL_OPTIONS,
75
79
  customizerIndex: 0,
76
- CUSTOMIZER_OPTIONS: [
77
- 'Theme',
78
- 'Graph Mode',
79
- 'Splash Screen',
80
- 'Splash Color',
81
- 'Splash Animation',
82
- 'Notifications',
83
- 'Temperature Unit',
84
- 'Toggle CPU',
85
- 'Toggle Memory',
86
- 'Toggle GPU',
87
- 'Toggle Power',
88
- 'Toggle Battery',
89
- 'Toggle Thermal',
90
- 'Toggle Network',
91
- 'Toggle Packets',
92
- 'Toggle Tasks',
93
- 'Toggle Disk',
94
- 'Toggle Processes',
95
- 'Toggle Tree View',
96
- ],
80
+ CUSTOMIZER_OPTIONS: [
81
+ 'Theme',
82
+ 'Graph Mode',
83
+ 'Splash Screen',
84
+ 'Splash Color',
85
+ 'Splash Animation',
86
+ 'Notifications',
87
+ 'Temperature Unit',
88
+ 'Toggle CPU',
89
+ 'Toggle Memory',
90
+ 'Toggle GPU',
91
+ 'Toggle Power',
92
+ 'Toggle Battery',
93
+ 'Toggle Thermal',
94
+ 'Toggle Network',
95
+ 'Toggle Conns',
96
+ 'Toggle Tasks',
97
+ 'Toggle Disk',
98
+ 'Toggle Processes',
99
+ 'Toggle Containers',
100
+ 'Toggle Blender',
101
+ 'Toggle Tree View',
102
+ ],
97
103
  CPU_ALERT_PCT: config.cpuAlertPct,
98
104
  TEMP_ALERT_C: config.tempAlertC,
99
105
  watchdogProcess: config.watchdogProcess || '',
@@ -103,23 +109,25 @@ const state = {
103
109
  webhookUrl: config.webhookUrl || '',
104
110
  alertCmd: config.alertCmd || '',
105
111
  alerted: false,
106
- activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies',
112
+ activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies' | 'containers' | 'blender',
107
113
  mouseEnabled: config.mouseEnabled,
108
- PANEL_TABS: [
109
- { id: 'cpu', label: 'CPU', key: '1' },
110
- { id: 'mem', label: 'Memory', key: '2' },
111
- { id: 'gpu', label: 'GPU', key: '3' },
112
- { id: 'power', label: 'Power', key: '4' },
113
- { id: 'battery', label: 'Battery', key: '5' },
114
- { id: 'thermal', label: 'Thermal', key: '6' },
115
- { id: 'network', label: 'Network', key: '7' },
116
- { id: 'packets', label: 'Packets', key: '8' },
117
- { id: 'tasks', label: 'Tasks', key: '9' },
118
- { id: 'disk', label: 'Disk', key: '0' },
119
- { id: 'process', label: 'Process', key: 'P' },
120
- { id: 'p2p', label: 'P2P', key: 'R' },
121
- { id: 'anomalies', label: 'Anomalies', key: 'A' },
122
- ],
114
+ PANEL_TABS: [
115
+ { id: 'cpu', label: 'CPU', key: '1' },
116
+ { id: 'mem', label: 'Memory', key: '2' },
117
+ { id: 'gpu', label: 'GPU', key: '3' },
118
+ { id: 'power', label: 'Power', key: '4' },
119
+ { id: 'battery', label: 'Battery', key: '5' },
120
+ { id: 'thermal', label: 'Thermal', key: '6' },
121
+ { id: 'network', label: 'Network', key: '7' },
122
+ { id: 'packets', label: 'Conns', key: '8' },
123
+ { id: 'tasks', label: 'Tasks', key: '9' },
124
+ { id: 'disk', label: 'Disk', key: '0' },
125
+ { id: 'process', label: 'Process', key: 'P' },
126
+ { id: 'containers', label: 'Containers', key: 'C' },
127
+ { id: 'p2p', label: 'P2P', key: 'R' },
128
+ { id: 'anomalies', label: 'Anomalies', key: 'A' },
129
+ { id: 'blender', label: 'Blender', key: 'B' },
130
+ ],
123
131
  p2pServer: null as import('../p2p/server.js').P2PServer | null,
124
132
  p2pClient: null as import('../p2p/client.js').P2PClient | null,
125
133
  p2pServerRunning: false,
@@ -134,6 +142,9 @@ const state = {
134
142
  splashAnimation: config.splashAnimation as SplashAnimation,
135
143
  menuSelectionIndex: 0,
136
144
  readmeScrollOffset: 0,
145
+ isDocker: isDockerEnv,
146
+ dockerModeConfirmed: config.dockerModeConfirmed || false,
147
+ dockerSelectionIndex: 0,
137
148
  };
138
149
 
139
150
  function setStatus(msg: string, ms = 3000) {
@@ -153,10 +164,12 @@ function getToggleKey(opt: string): keyof VisibleItems | null {
153
164
  'Toggle Battery': 'battery',
154
165
  'Toggle Thermal': 'thermal',
155
166
  'Toggle Network': 'network',
156
- 'Toggle Packets': 'packets',
167
+ 'Toggle Conns': 'packets',
157
168
  'Toggle Tasks': 'tasks',
158
169
  'Toggle Disk': 'disk',
159
170
  'Toggle Processes': 'process',
171
+ 'Toggle Containers': 'containers',
172
+ 'Toggle Blender': 'blender',
160
173
  'Toggle Tree View': 'tree',
161
174
  };
162
175
  return map[opt] ?? null;
package/src/live/types.ts CHANGED
@@ -20,13 +20,13 @@ export interface LiveOptions {
20
20
 
21
21
  export type ExportFormat = 'json' | 'csv' | 'tsv' | 'html' | 'md';
22
22
 
23
- type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p' | 'menu' | 'readme' | 'credits';
23
+ type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p' | 'menu' | 'readme' | 'credits' | 'docker-alert';
24
24
 
25
25
  type SortMode = 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
26
26
 
27
27
  type GraphMode = 'spark' | 'bar';
28
28
 
29
- type ActivePanel = 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p';
29
+ type ActivePanel = 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'containers' | 'anomalies' | 'blender';
30
30
 
31
31
  export type { InputMode, SortMode, GraphMode, ActivePanel, ThemeName };
32
32
 
@@ -0,0 +1,144 @@
1
+ import { run } from './run.js';
2
+ import type { BlenderRenderData } from './types.js';
3
+
4
+ const RENDER_LOG_DIR = '/tmp/blender_render_logs';
5
+
6
+ function blendFileFromArgs(args: string[]): string {
7
+ const nonFlag = args.filter(a => !a.startsWith('-'));
8
+ for (const a of nonFlag) {
9
+ if (a.endsWith('.blend') || a.includes('.blend')) {
10
+ return a.split('/').pop() || a;
11
+ }
12
+ }
13
+ return 'unknown.blend';
14
+ }
15
+
16
+ function engineFromArgs(args: string[]): string {
17
+ const idx = args.indexOf('-E');
18
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
19
+ const flag = args.find(a => a.startsWith('-E='));
20
+ if (flag) return flag.slice(3);
21
+ return 'unknown';
22
+ }
23
+
24
+ function outputPathFromArgs(args: string[]): string {
25
+ const oIdx = args.indexOf('-o');
26
+ if (oIdx !== -1 && args[oIdx + 1]) {
27
+ let p = args[oIdx + 1];
28
+ if (p.endsWith('/')) p = p.slice(0, -1);
29
+ return p.split('/').slice(0, -1).join('/') || '/tmp';
30
+ }
31
+ const fFlag = args.find(a => a.startsWith('-o='));
32
+ if (fFlag) {
33
+ const p = fFlag.slice(3);
34
+ return p.endsWith('/') ? p.slice(0, -1).split('/').slice(0, -1).join('/') || '/tmp' : p.split('/').slice(0, -1).join('/') || '/tmp';
35
+ }
36
+ return '/tmp';
37
+ }
38
+
39
+ function frameRangeFromArgs(args: string[]): { start: number; end: number } {
40
+ const fIdx = args.indexOf('-f');
41
+ if (fIdx !== -1 && args[fIdx + 1]) {
42
+ const raw = args[fIdx + 1];
43
+ if (raw.includes('..')) {
44
+ const [s, e] = raw.split('..').map(Number);
45
+ if (!isNaN(s) && !isNaN(e)) return { start: s, end: e };
46
+ }
47
+ const n = parseInt(raw, 10);
48
+ if (!isNaN(n)) return { start: n, end: n };
49
+ }
50
+ const sFlag = args.find(a => a.startsWith('-s='));
51
+ const eFlag = args.find(a => a.startsWith('-e='));
52
+ const start = sFlag ? parseInt(sFlag.slice(3), 10) : 1;
53
+ const end = eFlag ? parseInt(eFlag.slice(3), 10) : 250;
54
+ return { start: isNaN(start) ? 1 : start, end: isNaN(end) ? 250 : end };
55
+ }
56
+
57
+ async function parseRenderLog(blendName: string): Promise<{ framesRendered: number; totalFrames: number; startTime: number; samples?: number } | null> {
58
+ const baseName = blendName.replace(/\.blend$/i, '');
59
+ try {
60
+ const entries = (await run(`ls -t "${RENDER_LOG_DIR}" 2>/dev/null | grep "^${baseName}_" | head -5`, '')).trim().split('\n').filter(Boolean);
61
+ for (const entry of entries) {
62
+ const logPath = `${RENDER_LOG_DIR}/${entry}`;
63
+ try {
64
+ const content = await run(`cat "${logPath}" 2>/dev/null`, '', 500);
65
+ if (!content) continue;
66
+ const lines = content.split('\n');
67
+ const rendered = lines.filter(l => /Fra:(\d+)/i.test(l)).length;
68
+ const totalMatch = content.match(/Fra:\s*\d+\s*\/\s*(\d+)/);
69
+ const totalFrames = totalMatch ? parseInt(totalMatch[1], 10) : 0;
70
+ const startMatch = content.match(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/m);
71
+ const startTime = startMatch ? new Date(startMatch[1]).getTime() : Date.now() - 60000;
72
+ const sampleMatch = content.match(/Sample\s*(\d+)/i);
73
+ if (rendered > 0) {
74
+ return { framesRendered: rendered, totalFrames: totalFrames || rendered, startTime, samples: sampleMatch ? parseInt(sampleMatch[1], 10) : undefined };
75
+ }
76
+ } catch {
77
+ // try next log file
78
+ }
79
+ }
80
+ } catch {
81
+ // ignore
82
+ }
83
+ return null;
84
+ }
85
+
86
+ export async function collectBlenderRenders(): Promise<BlenderRenderData[]> {
87
+ const renders: BlenderRenderData[] = [];
88
+ try {
89
+ const psRaw = await run("ps -eo pid,pcpu,pmem,state,command 2>/dev/null | grep -iE '[b]lender'", '', 2000);
90
+ if (!psRaw) return renders;
91
+
92
+ const lines = psRaw.split('\n');
93
+ for (const line of lines) {
94
+ const parts = line.trim().match(/\s*(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(.+)/);
95
+ if (!parts) continue;
96
+
97
+ const pid = parseInt(parts[1], 10);
98
+ const cpuPct = parseFloat(parts[2]);
99
+ const command = parts[5];
100
+ const args = command.split(/\s+/);
101
+
102
+ const isRender = /-b|--background|-f\s/.test(command) || /render/i.test(command);
103
+ if (!isRender && !command.includes('blender')) continue;
104
+
105
+ const blendFile = blendFileFromArgs(args);
106
+ const engine = engineFromArgs(args);
107
+ const outputPath = outputPathFromArgs(args);
108
+ const { start: frameStart, end: frameEnd } = frameRangeFromArgs(args);
109
+ const totalFrames = Math.max(1, frameEnd - frameStart + 1);
110
+
111
+ const logData = await parseRenderLog(blendFile);
112
+ const startTime = logData?.startTime ?? Date.now() - 60000;
113
+ const elapsedSec = Math.max(0, Math.round((Date.now() - startTime) / 1000));
114
+ let currentFrame = logData?.framesRendered ?? 0;
115
+ if (currentFrame === 0 && cpuPct > 0) currentFrame = 1;
116
+
117
+ let status: BlenderRenderData['status'] = 'unknown';
118
+ if (cpuPct > 0 && currentFrame > 0 && currentFrame < totalFrames) status = 'rendering';
119
+ else if (cpuPct > 0 && currentFrame >= totalFrames) status = 'finalizing';
120
+ else if (currentFrame === 0 && cpuPct > 0) status = 'loading';
121
+ else if (cpuPct === 0) status = 'idle';
122
+
123
+ const completionPercent = totalFrames > 0 ? Math.min(100, Math.round((currentFrame / totalFrames) * 100)) : 0;
124
+
125
+ renders.push({
126
+ pid,
127
+ blendFile,
128
+ renderEngine: engine,
129
+ frameStart,
130
+ frameEnd,
131
+ currentFrame,
132
+ totalFrames,
133
+ elapsedSec,
134
+ completionPercent,
135
+ status,
136
+ outputPath,
137
+ sampleCount: logData?.samples,
138
+ });
139
+ }
140
+ } catch {
141
+ // ignore
142
+ }
143
+ return renders;
144
+ }