metwatch 0.1.0 → 0.2.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.
@@ -1,293 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Process Table Widget
3
- //
4
- // Dual-mode scrollable process table:
5
- // [a] All — all system processes sorted by CPU descending
6
- // [f] Watch — only processes matching config.watchedProcesses
7
- //
8
- // Keyboard bindings:
9
- // a / f → switch view mode
10
- // ↑ / k → move selection up
11
- // ↓ / j → move selection down
12
- // K (shift) → kill selected process (with confirmation)
13
- // r → restart selected managed process
14
- // s → stop selected managed process
15
- // c / m → sort by CPU / Memory
16
- //
17
- // Phase 2 columns: PID · NAME · CPU% · MEM · MEM% · USER · THRD · STATUS
18
- // ---------------------------------------------------------------------------
19
-
20
- import blessed from 'blessed';
21
- import type { BlessedScreen, BlessedElement } from 'blessed';
22
- import { bus } from '../../core/event-bus.ts';
23
- import { getProcesses } from '../../core/state-manager.ts';
24
- import type { ProcessInfo, ProcessList, ProcessViewMode, ProcessSortKey } from '../../types/process.types.ts';
25
- import type { ResolvedConfig } from '../../types/config.types.ts';
26
- import type { ManagedProcess } from '../../types/managed-process.types.ts';
27
- import { formatBytes, formatPercent, formatUptime, truncate } from '../../utils/formatters.ts';
28
-
29
- interface ProcessTableWidgetOptions {
30
- screen: BlessedScreen;
31
- config: ResolvedConfig;
32
- getManagedById: (id: string) => ManagedProcess | undefined;
33
- managedNames: Set<string>;
34
- top: number | string;
35
- left: number | string;
36
- width: number | string;
37
- height: number | string;
38
- }
39
-
40
- interface ProcessTableWidgetHandle {
41
- box: BlessedElement;
42
- destroy: () => void;
43
- }
44
-
45
- // Columns: PID · NAME · CPU% · MEM · MEM% · USER · THRD · STATUS
46
- const HEADERS = ['PID', 'NAME', 'CPU%', 'MEM', 'MEM%', 'USER', 'THR', 'STATUS'];
47
- const COL_WIDTHS = [7, 22, 7, 10, 6, 10, 5, 11 ];
48
-
49
- const STATUS_BADGE: Record<string, string> = {
50
- running: '{green-fg}●{/green-fg}',
51
- restarting: '{yellow-fg}↻{/yellow-fg}',
52
- crashed: '{red-fg}✕{/red-fg}',
53
- stopped: '{gray-fg}■{/gray-fg}',
54
- };
55
-
56
- function uptime(p: ProcessInfo): string {
57
- if (!p.startedAt) return '—';
58
- return formatUptime(Math.floor((Date.now() - p.startedAt) / 1000));
59
- }
60
-
61
- function buildRow(p: ProcessInfo, managed: ManagedProcess | undefined): string[] {
62
- const badge = managed ? (STATUS_BADGE[managed.status] ?? '') + ' ' : ' ';
63
- const name = badge + truncate(p.name, 19);
64
- const status = managed
65
- ? `${managed.status}${managed.restarts > 0 ? ` ×${managed.restarts}` : ''}`
66
- : p.status;
67
-
68
- return [
69
- String(p.pid),
70
- name,
71
- formatPercent(p.cpu),
72
- formatBytes(p.memory),
73
- formatPercent(p.memoryPercent),
74
- truncate(p.user || '—', 10),
75
- String(p.threads || 1),
76
- status,
77
- ];
78
- }
79
-
80
- function colorRow(cells: string[], p: ProcessInfo, isManaged: boolean): string {
81
- const padded = cells.map((cell, i) => {
82
- if (i === 1) return cell; // NAME has blessed tags — don't naive-pad
83
- return cell.padEnd(COL_WIDTHS[i] ?? 10);
84
- });
85
- padded[1] = (padded[1] ?? '').padEnd(COL_WIDTHS[1] ?? 22);
86
- const line = padded.join(' ');
87
- if (isManaged) return `{cyan-fg}${line}{/cyan-fg}`;
88
- if (p.cpu >= 50) return `{red-fg}${line}{/red-fg}`;
89
- if (p.cpu >= 20) return `{yellow-fg}${line}{/yellow-fg}`;
90
- return line;
91
- }
92
-
93
- export function createProcessTableWidget(options: ProcessTableWidgetOptions): ProcessTableWidgetHandle {
94
- const { screen, config, getManagedById, managedNames, top, left, width, height } = options;
95
-
96
- let viewMode: ProcessViewMode = 'all';
97
- let sortKey: ProcessSortKey = 'cpu';
98
- let selectedIndex = 0;
99
- let currentList: ProcessList = [];
100
-
101
- const hasManagedKeys = managedNames.size > 0;
102
- const LABEL_DEFAULT = hasManagedKeys
103
- ? ' Processes [a/f=view c/m=sort ↑↓=nav K=kill r=restart s=stop] '
104
- : ' Processes [a/f=view c/m=sort ↑↓=nav K=kill] ';
105
-
106
- // ── Container ──────────────────────────────────────────────────────────────
107
-
108
- const container = blessed.box({
109
- top, left, width, height,
110
- label: LABEL_DEFAULT,
111
- tags: true,
112
- border: { type: 'line' },
113
- style: {
114
- border: { fg: 'yellow' },
115
- label: { fg: 'yellow', bold: true },
116
- },
117
- });
118
-
119
- const modeBar = blessed.box({
120
- parent: container,
121
- top: 0, left: 0,
122
- width: '100%', height: 1,
123
- tags: true,
124
- content: buildModeBar(),
125
- });
126
-
127
- const listBox = blessed.box({
128
- parent: container,
129
- top: 1, left: 0,
130
- width: '100%', height: '100%-3',
131
- tags: true,
132
- scrollable: true,
133
- alwaysScroll: true,
134
- keys: true,
135
- vi: false,
136
- mouse: true,
137
- scrollbar: { ch: '│', style: { fg: 'yellow' } } as never,
138
- style: { scrollbar: { fg: 'yellow' } },
139
- });
140
-
141
- const question = blessed.question({
142
- parent: screen as unknown as BlessedElement,
143
- top: 'center', left: 'center',
144
- width: 56, height: 7,
145
- tags: true,
146
- border: { type: 'line' },
147
- style: { border: { fg: 'red' }, label: { fg: 'red' } },
148
- label: ' Confirm Kill ',
149
- hidden: true,
150
- });
151
-
152
- screen.append(container);
153
-
154
- // ── Helpers ────────────────────────────────────────────────────────────────
155
-
156
- function buildModeBar(): string {
157
- const allTag = viewMode === 'all'
158
- ? '{black-fg}{cyan-bg} ALL {/} '
159
- : '{gray-fg} ALL {/} ';
160
- const watchedTag = viewMode === 'watched'
161
- ? '{black-fg}{cyan-bg} WATCH {/}'
162
- : '{gray-fg} WATCH {/}';
163
- const sortTag = ` {gray-fg}sort:${sortKey}{/gray-fg}`;
164
- const extraHint = hasManagedKeys
165
- ? ' {gray-fg}[r]=restart [s]=stop managed{/gray-fg}'
166
- : '';
167
- return ` ${allTag}${watchedTag}${sortTag}${extraHint}`;
168
- }
169
-
170
- function sortList(list: ProcessList): ProcessList {
171
- return [...list].sort((a, b) => {
172
- switch (sortKey) {
173
- case 'cpu': return b.cpu - a.cpu;
174
- case 'memory': return b.memory - a.memory;
175
- case 'name': return a.name.localeCompare(b.name);
176
- case 'pid': return a.pid - b.pid;
177
- }
178
- });
179
- }
180
-
181
- function filterList(list: ProcessList): ProcessList {
182
- if (viewMode === 'all') return sortList(list);
183
- const patterns = config.watchedProcesses.map(w => w.name.toLowerCase());
184
- const managed = [...managedNames].map(n => n.toLowerCase());
185
- return sortList(list.filter(p => {
186
- const name = p.name.toLowerCase();
187
- return patterns.some(pat => name.includes(pat))
188
- || managed.some(m => name.includes(m));
189
- }));
190
- }
191
-
192
- function render(list: ProcessList): void {
193
- currentList = filterList(list);
194
- selectedIndex = Math.min(selectedIndex, Math.max(0, currentList.length - 1));
195
-
196
- modeBar.setContent(buildModeBar());
197
-
198
- const headerCells = HEADERS.map((h, i) => h.padEnd(COL_WIDTHS[i] ?? 10));
199
- const headerLine = `{bold}{cyan-fg} ${headerCells.join(' ')}{/cyan-fg}{/bold}`;
200
-
201
- const rows = currentList.map((p, i) => {
202
- const managed = getManagedById(p.name);
203
- const isManaged = managedNames.has(p.name);
204
- const cells = buildRow(p, managed);
205
- const line = colorRow(cells, p, isManaged);
206
- const pre = i === selectedIndex ? '{inverse}' : '';
207
- const post = i === selectedIndex ? '{/inverse}' : '';
208
- return ` ${pre}${line}${post}`;
209
- });
210
-
211
- const content = currentList.length === 0
212
- ? `${headerLine}\n\n{gray-fg} No processes found.{/gray-fg}`
213
- : [headerLine, ...rows].join('\n');
214
-
215
- listBox.setContent(content);
216
- screen.render();
217
- }
218
-
219
- function flashLabel(msg: string, ms = 2500): void {
220
- container.setLabel(` ${msg} `);
221
- screen.render();
222
- setTimeout(() => { container.setLabel(LABEL_DEFAULT); screen.render(); }, ms);
223
- }
224
-
225
- function scrollTo(idx: number): void {
226
- selectedIndex = Math.max(0, Math.min(idx, currentList.length - 1));
227
- render(getProcesses());
228
- }
229
-
230
- // ── Keybindings ────────────────────────────────────────────────────────────
231
-
232
- screen.key(['a'], () => { viewMode = 'all'; render(getProcesses()); });
233
- screen.key(['f'], () => { viewMode = 'watched'; render(getProcesses()); });
234
- screen.key(['c'], () => { sortKey = 'cpu'; render(getProcesses()); });
235
- screen.key(['m'], () => { sortKey = 'memory'; render(getProcesses()); });
236
-
237
- screen.key(['up', 'k'], () => scrollTo(selectedIndex - 1));
238
- screen.key(['down', 'j'], () => scrollTo(selectedIndex + 1));
239
-
240
- screen.key(['K'], () => {
241
- const proc = currentList[selectedIndex];
242
- if (!proc) return;
243
- question.ask(
244
- `Kill process "${proc.name}" (PID ${proc.pid})?`,
245
- (_err, confirmed) => {
246
- if (confirmed) bus.emit('process:kill:requested', { pid: proc.pid });
247
- screen.render();
248
- }
249
- );
250
- screen.render();
251
- });
252
-
253
- screen.key(['r'], () => {
254
- const proc = currentList[selectedIndex];
255
- if (!proc || !managedNames.has(proc.name)) return;
256
- bus.emit('managed:restart:requested', { id: proc.name });
257
- flashLabel(`{yellow-fg}Restarting ${proc.name}…{/yellow-fg}`);
258
- });
259
-
260
- screen.key(['s'], () => {
261
- const proc = currentList[selectedIndex];
262
- if (!proc || !managedNames.has(proc.name)) return;
263
- bus.emit('managed:stop:requested', { id: proc.name });
264
- flashLabel(`{gray-fg}Stopping ${proc.name}…{/gray-fg}`);
265
- });
266
-
267
- // ── Bus subscriptions ──────────────────────────────────────────────────────
268
-
269
- const unsubProcesses = bus.on('processes:updated', render);
270
- const unsubManagedStarted = bus.on('managed:started', () => render(getProcesses()));
271
- const unsubManagedStopped = bus.on('managed:stopped', () => render(getProcesses()));
272
- const unsubManagedCrashed = bus.on('managed:crashed', () => render(getProcesses()));
273
- const unsubManagedRestart = bus.on('managed:restarted', () => render(getProcesses()));
274
- const unsubKillResult = bus.on('process:kill:result', ({ pid, success, error }) => {
275
- flashLabel(success
276
- ? `{green-fg}Process ${pid} terminated.{/green-fg}`
277
- : `{red-fg}Kill failed for ${pid}: ${error ?? 'unknown'}{/red-fg}`);
278
- });
279
-
280
- render(getProcesses());
281
-
282
- function destroy(): void {
283
- unsubProcesses();
284
- unsubManagedStarted();
285
- unsubManagedStopped();
286
- unsubManagedCrashed();
287
- unsubManagedRestart();
288
- unsubKillResult();
289
- container.destroy();
290
- }
291
-
292
- return { box: container, destroy };
293
- }
@@ -1,119 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Runtime Widget
3
- //
4
- // Displays Node/Bun runtime metrics for managed processes collected via
5
- // the inspector protocol (runtime-manager.ts).
6
- //
7
- // Shows per-process panels with:
8
- // Heap: [bar] used / total RSS: x MB External: x KB
9
- // Event Loop Lag: x ms Handles: x Requests: x
10
- // GC: count totalMs lastPauseMs
11
- // Uptime: x
12
- //
13
- // Toggle key: R (handled in layout.ts)
14
- // ---------------------------------------------------------------------------
15
-
16
- import blessed from 'blessed';
17
- import type { BlessedScreen, BlessedElement } from 'blessed';
18
- import { bus } from '../../core/event-bus.ts';
19
- import { getAllRuntimeMetrics } from '../../core/state-manager.ts';
20
- import type { RuntimeMetrics } from '../../types/metrics.types.ts';
21
- import { formatBytes, formatUptime, bar, colorPercent } from '../../utils/formatters.ts';
22
-
23
- interface RuntimeWidgetOptions {
24
- screen: BlessedScreen;
25
- top: number | string;
26
- left: number | string;
27
- width: number | string;
28
- height: number | string;
29
- }
30
-
31
- interface RuntimeWidgetHandle {
32
- box: BlessedElement;
33
- destroy: () => void;
34
- }
35
-
36
- function lagColor(ms: number): string {
37
- if (ms >= 100) return `{red-fg}${ms.toFixed(1)}ms{/red-fg}`;
38
- if (ms >= 20) return `{yellow-fg}${ms.toFixed(1)}ms{/yellow-fg}`;
39
- return `{green-fg}${ms.toFixed(1)}ms{/green-fg}`;
40
- }
41
-
42
- function renderProcess(m: RuntimeMetrics): string[] {
43
- const heapPct = m.heapTotal > 0 ? (m.heapUsed / m.heapTotal) * 100 : 0;
44
- const heapBar = bar(heapPct, 16);
45
- const heapBarC = heapPct >= 85
46
- ? `{red-fg}${heapBar}{/red-fg}`
47
- : heapPct >= 65
48
- ? `{yellow-fg}${heapBar}{/yellow-fg}`
49
- : `{green-fg}${heapBar}{/green-fg}`;
50
-
51
- const lines: string[] = [
52
- ` {bold}{cyan-fg}◈ ${m.managedId}{/cyan-fg}{/bold} pid:${m.pid} uptime:${formatUptime(m.uptime)}`,
53
- ` Heap [${heapBarC}] ${formatBytes(m.heapUsed)} / ${formatBytes(m.heapTotal)} ${colorPercent(heapPct)}`,
54
- ` RSS: ${formatBytes(m.rss)} External: ${formatBytes(m.external)} ArrayBuf: ${formatBytes(m.arrayBuffers)}`,
55
- ` EventLoop Lag: ${lagColor(m.eventLoopLag)} Handles: ${m.activeHandles} Requests: ${m.activeRequests}`,
56
- ` GC: ${m.gc.count} events Total: ${m.gc.totalPauseMs.toFixed(0)}ms` +
57
- (m.gc.lastPauseMs !== null ? ` Last: ${m.gc.lastPauseMs.toFixed(1)}ms` : ''),
58
- ];
59
- return lines;
60
- }
61
-
62
- export function createRuntimeWidget(options: RuntimeWidgetOptions): RuntimeWidgetHandle {
63
- const { screen, top, left, width, height } = options;
64
-
65
- const box = blessed.box({
66
- top,
67
- left,
68
- width,
69
- height,
70
- label: ' Runtime [R=toggle] ',
71
- tags: true,
72
- border: { type: 'line' },
73
- scrollable: true,
74
- alwaysScroll: true,
75
- keys: false,
76
- mouse: true,
77
- style: {
78
- border: { fg: 'cyan' },
79
- label: { fg: 'cyan', bold: true },
80
- },
81
- padding: { top: 0, left: 1, right: 1, bottom: 0 },
82
- });
83
-
84
- screen.append(box);
85
-
86
- function render(all: RuntimeMetrics[]): void {
87
- if (all.length === 0) {
88
- box.setContent(
89
- '{gray-fg} No runtime metrics yet.\n' +
90
- ' Launch a managed process with `mw start <file>` to see Node/Bun internals.{/gray-fg}'
91
- );
92
- screen.render();
93
- return;
94
- }
95
-
96
- const lines: string[] = [];
97
- for (let i = 0; i < all.length; i++) {
98
- if (i > 0) lines.push('');
99
- lines.push(...renderProcess(all[i]!));
100
- }
101
-
102
- box.setContent(lines.join('\n'));
103
- screen.render();
104
- }
105
-
106
- // Initial render from state
107
- render(getAllRuntimeMetrics());
108
-
109
- const unsub = bus.on('metrics:runtime:updated', () => {
110
- render(getAllRuntimeMetrics());
111
- });
112
-
113
- function destroy(): void {
114
- unsub();
115
- box.destroy();
116
- }
117
-
118
- return { box, destroy };
119
- }