everything-dev 0.2.1 → 0.3.1

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,471 +1,475 @@
1
+ import { Effect, Logger, LogLevel } from "effect";
1
2
  import { Box, render, Text, useApp, useInput } from "ink";
2
3
  import { useEffect, useState } from "react";
3
- import { Effect, Logger, LogLevel } from "effect";
4
+ import { getConfig, getProjectRoot } from "../config";
4
5
  import {
5
- ResourceMonitor,
6
- PlatformLive,
7
- createSnapshotWithPlatform,
8
- diffSnapshots,
9
- type Snapshot,
10
- type SnapshotDiff,
11
- type MonitorConfig,
6
+ createSnapshotWithPlatform,
7
+ diffSnapshots,
8
+ type MonitorConfig,
9
+ PlatformLive,
10
+ ResourceMonitor,
11
+ type Snapshot,
12
+ type SnapshotDiff,
12
13
  } from "../lib/resource-monitor";
13
- import { getAccount, getConfigPath } from "../config";
14
- import { colors, divider, gradients, icons, frames } from "../utils/theme";
14
+ import { colors, divider, frames, gradients, icons } from "../utils/theme";
15
15
 
16
16
  type Phase = "baseline" | "running" | "stopped";
17
17
 
18
18
  interface MonitorViewProps {
19
- baseline: Snapshot | null;
20
- current: Snapshot | null;
21
- diff: SnapshotDiff | null;
22
- phase: Phase;
23
- refreshing: boolean;
24
- onRefresh: () => void;
25
- onSnapshot: () => void;
26
- onExport: () => void;
27
- onExit: () => void;
19
+ baseline: Snapshot | null;
20
+ current: Snapshot | null;
21
+ diff: SnapshotDiff | null;
22
+ phase: Phase;
23
+ refreshing: boolean;
24
+ onRefresh: () => void;
25
+ onSnapshot: () => void;
26
+ onExport: () => void;
27
+ onExit: () => void;
28
28
  }
29
29
 
30
30
  function formatBytes(bytes: number): string {
31
- if (bytes < 1024) return `${bytes} B`;
32
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
33
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
31
+ if (bytes < 1024) return `${bytes} B`;
32
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
33
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
34
34
  }
35
35
 
36
36
  function PortRow({
37
- port,
38
- info,
37
+ port,
38
+ info,
39
39
  }: {
40
- port: number;
41
- info: { pid: number | null; command: string | null; state: string };
40
+ port: number;
41
+ info: { pid: number | null; command: string | null; state: string };
42
42
  }) {
43
- const isFree = info.state === "FREE";
44
- const icon = isFree ? "○" : "●";
45
- const statusColor = isFree ? "gray" : "#00ff41";
46
-
47
- return (
48
- <Box>
49
- <Text color={statusColor}> {icon} </Text>
50
- <Text color="#00ffff">:{port.toString().padEnd(5)}</Text>
51
- {isFree ? (
52
- <Text color="gray">free</Text>
53
- ) : (
54
- <Text>
55
- <Text color="#ff00ff">{info.pid}</Text>
56
- <Text color="gray"> {info.command}</Text>
57
- </Text>
58
- )}
59
- </Box>
60
- );
43
+ const isFree = info.state === "FREE";
44
+ const icon = isFree ? "○" : "●";
45
+ const statusColor = isFree ? "gray" : "#00ff41";
46
+
47
+ return (
48
+ <Box>
49
+ <Text color={statusColor}> {icon} </Text>
50
+ <Text color="#00ffff">:{port.toString().padEnd(5)}</Text>
51
+ {isFree ? (
52
+ <Text color="gray">free</Text>
53
+ ) : (
54
+ <Text>
55
+ <Text color="#ff00ff">{info.pid}</Text>
56
+ <Text color="gray"> {info.command}</Text>
57
+ </Text>
58
+ )}
59
+ </Box>
60
+ );
61
61
  }
62
62
 
63
63
  function ProcessRow({
64
- proc,
64
+ proc,
65
65
  }: {
66
- proc: { pid: number; command: string; rss: number; children: number[] };
66
+ proc: { pid: number; command: string; rss: number; children: number[] };
67
67
  }) {
68
- const childCount = proc.children.length;
69
- const childText = childCount > 0 ? ` [${childCount}]` : "";
70
-
71
- return (
72
- <Box>
73
- <Text color="#00ffff"> {proc.pid.toString().padEnd(7)}</Text>
74
- <Text>{proc.command.slice(0, 20).padEnd(20)}</Text>
75
- <Text color="#ff00ff">{formatBytes(proc.rss).padStart(10)}</Text>
76
- <Text color="gray">{childText}</Text>
77
- </Box>
78
- );
68
+ const childCount = proc.children.length;
69
+ const childText = childCount > 0 ? ` [${childCount}]` : "";
70
+
71
+ return (
72
+ <Box>
73
+ <Text color="#00ffff"> {proc.pid.toString().padEnd(7)}</Text>
74
+ <Text>{proc.command.slice(0, 20).padEnd(20)}</Text>
75
+ <Text color="#ff00ff">{formatBytes(proc.rss).padStart(10)}</Text>
76
+ <Text color="gray">{childText}</Text>
77
+ </Box>
78
+ );
79
79
  }
80
80
 
81
81
  function SnapshotSection({
82
- title,
83
- snapshot,
82
+ title,
83
+ snapshot,
84
84
  }: {
85
- title: string;
86
- snapshot: Snapshot | null;
85
+ title: string;
86
+ snapshot: Snapshot | null;
87
87
  }) {
88
- if (!snapshot) {
89
- return (
90
- <Box flexDirection="column" marginBottom={1}>
91
- <Text color="#00ffff"> {title}</Text>
92
- <Text color="gray"> (waiting for snapshot...)</Text>
93
- </Box>
94
- );
95
- }
96
-
97
- const ports = Object.entries(snapshot.ports);
98
- const boundPorts = ports.filter(([, info]) => info.state !== "FREE").length;
99
- const totalRss = formatBytes(snapshot.memory.processRss);
100
-
101
- return (
102
- <Box flexDirection="column" marginBottom={1}>
103
- <Text color="#00ffff"> {title}</Text>
104
- <Text color="gray">{divider(50)}</Text>
105
-
106
- <Text color="gray">
107
- {" "}
108
- PORTS ({boundPorts}/{ports.length} bound)
109
- </Text>
110
- {ports.map(([port, info]) => (
111
- <PortRow key={port} port={parseInt(port, 10)} info={info} />
112
- ))}
113
-
114
- {snapshot.processes.length > 0 && (
115
- <>
116
- <Box marginTop={1}>
117
- <Text color="gray">
118
- {" "}
119
- PROCESSES ({snapshot.processes.length})
120
- </Text>
121
- </Box>
122
- {snapshot.processes.slice(0, 8).map((proc) => (
123
- <ProcessRow key={proc.pid} proc={proc} />
124
- ))}
125
- {snapshot.processes.length > 8 && (
126
- <Text color="gray">
127
- {" "}
128
- ... and {snapshot.processes.length - 8} more
129
- </Text>
130
- )}
131
- </>
132
- )}
133
-
134
- <Box marginTop={1}>
135
- <Text color="gray"> Memory: {totalRss}</Text>
136
- </Box>
137
- </Box>
138
- );
88
+ if (!snapshot) {
89
+ return (
90
+ <Box flexDirection="column" marginBottom={1}>
91
+ <Text color="#00ffff"> {title}</Text>
92
+ <Text color="gray"> (waiting for snapshot...)</Text>
93
+ </Box>
94
+ );
95
+ }
96
+
97
+ const ports = Object.entries(snapshot.ports);
98
+ const boundPorts = ports.filter(([, info]) => info.state !== "FREE").length;
99
+ const totalRss = formatBytes(snapshot.memory.processRss);
100
+
101
+ return (
102
+ <Box flexDirection="column" marginBottom={1}>
103
+ <Text color="#00ffff"> {title}</Text>
104
+ <Text color="gray">{divider(50)}</Text>
105
+
106
+ <Text color="gray">
107
+ {" "}
108
+ PORTS ({boundPorts}/{ports.length} bound)
109
+ </Text>
110
+ {ports.map(([port, info]) => (
111
+ <PortRow key={port} port={parseInt(port, 10)} info={info} />
112
+ ))}
113
+
114
+ {snapshot.processes.length > 0 && (
115
+ <>
116
+ <Box marginTop={1}>
117
+ <Text color="gray"> PROCESSES ({snapshot.processes.length})</Text>
118
+ </Box>
119
+ {snapshot.processes.slice(0, 8).map((proc) => (
120
+ <ProcessRow key={proc.pid} proc={proc} />
121
+ ))}
122
+ {snapshot.processes.length > 8 && (
123
+ <Text color="gray">
124
+ {" "}
125
+ ... and {snapshot.processes.length - 8} more
126
+ </Text>
127
+ )}
128
+ </>
129
+ )}
130
+
131
+ <Box marginTop={1}>
132
+ <Text color="gray"> Memory: {totalRss}</Text>
133
+ </Box>
134
+ </Box>
135
+ );
139
136
  }
140
137
 
141
138
  function DiffSection({ diff }: { diff: SnapshotDiff | null }) {
142
- if (!diff) return null;
143
-
144
- const hasLeaks =
145
- diff.orphanedProcesses.length > 0 || diff.stillBoundPorts.length > 0;
146
- const memDelta = diff.memoryDeltaBytes;
147
- const memSign = memDelta >= 0 ? "+" : "";
148
-
149
- return (
150
- <Box flexDirection="column" marginBottom={1}>
151
- <Text color={hasLeaks ? "#ff3366" : "#00ff41"}>
152
- {hasLeaks ? ` ${icons.err} LEAKS DETECTED` : ` ${icons.ok} CLEAN`}
153
- </Text>
154
-
155
- {diff.stillBoundPorts.length > 0 && (
156
- <>
157
- <Text color="#ff3366"> Still Bound:</Text>
158
- {diff.stillBoundPorts.map((port) => (
159
- <Text key={port.port} color="#ff3366">
160
- {" "}
161
- :{port.port} ← PID {port.pid}
162
- </Text>
163
- ))}
164
- </>
165
- )}
166
-
167
- {diff.orphanedProcesses.length > 0 && (
168
- <>
169
- <Text color="#ff3366"> Orphaned Processes:</Text>
170
- {diff.orphanedProcesses.map((proc) => (
171
- <Text key={proc.pid} color="#ff3366">
172
- {" "}
173
- {proc.pid} {proc.command}
174
- </Text>
175
- ))}
176
- </>
177
- )}
178
-
179
- {diff.freedPorts.length > 0 && (
180
- <Text color="#00ff41"> Freed: {diff.freedPorts.join(", ")}</Text>
181
- )}
182
-
183
- <Text color={memDelta > 50 * 1024 * 1024 ? "#ff3366" : "gray"}>
184
- Memory Delta: {memSign}
185
- {formatBytes(memDelta)}
186
- </Text>
187
- </Box>
188
- );
139
+ if (!diff) return null;
140
+
141
+ const hasLeaks =
142
+ diff.orphanedProcesses.length > 0 || diff.stillBoundPorts.length > 0;
143
+ const memDelta = diff.memoryDeltaBytes;
144
+ const memSign = memDelta >= 0 ? "+" : "";
145
+
146
+ return (
147
+ <Box flexDirection="column" marginBottom={1}>
148
+ <Text color={hasLeaks ? "#ff3366" : "#00ff41"}>
149
+ {hasLeaks ? ` ${icons.err} LEAKS DETECTED` : ` ${icons.ok} CLEAN`}
150
+ </Text>
151
+
152
+ {diff.stillBoundPorts.length > 0 && (
153
+ <>
154
+ <Text color="#ff3366"> Still Bound:</Text>
155
+ {diff.stillBoundPorts.map((port) => (
156
+ <Text key={port.port} color="#ff3366">
157
+ {" "}
158
+ :{port.port} ← PID {port.pid}
159
+ </Text>
160
+ ))}
161
+ </>
162
+ )}
163
+
164
+ {diff.orphanedProcesses.length > 0 && (
165
+ <>
166
+ <Text color="#ff3366"> Orphaned Processes:</Text>
167
+ {diff.orphanedProcesses.map((proc) => (
168
+ <Text key={proc.pid} color="#ff3366">
169
+ {" "}
170
+ {proc.pid} {proc.command}
171
+ </Text>
172
+ ))}
173
+ </>
174
+ )}
175
+
176
+ {diff.freedPorts.length > 0 && (
177
+ <Text color="#00ff41"> Freed: {diff.freedPorts.join(", ")}</Text>
178
+ )}
179
+
180
+ <Text color={memDelta > 50 * 1024 * 1024 ? "#ff3366" : "gray"}>
181
+ Memory Delta: {memSign}
182
+ {formatBytes(memDelta)}
183
+ </Text>
184
+ </Box>
185
+ );
189
186
  }
190
187
 
191
188
  function MonitorView({
192
- baseline,
193
- current,
194
- diff,
195
- phase,
196
- refreshing,
197
- onRefresh,
198
- onSnapshot,
199
- onExport,
200
- onExit,
189
+ baseline,
190
+ current,
191
+ diff,
192
+ phase,
193
+ refreshing,
194
+ onRefresh,
195
+ onSnapshot,
196
+ onExport,
197
+ onExit,
201
198
  }: MonitorViewProps) {
202
- const { exit } = useApp();
203
-
204
- useInput((input, key) => {
205
- if (input === "q" || (key.ctrl && input === "c")) {
206
- onExit();
207
- exit();
208
- }
209
- if (input === "r") onRefresh();
210
- if (input === "s") onSnapshot();
211
- if (input === "e") onExport();
212
- });
213
-
214
- let account = "unknown";
215
- let configPath = "";
216
- try {
217
- account = getAccount();
218
- configPath = getConfigPath();
219
- } catch {
220
- // No config
221
- }
222
-
223
- const phaseLabel =
224
- phase === "baseline" ? "BASELINE" : phase === "running" ? "RUNNING" : "STOPPED";
225
- const phaseColor =
226
- phase === "baseline" ? "gray" : phase === "running" ? "#00ffff" : "#ff00ff";
227
-
228
- return (
229
- <Box flexDirection="column">
230
- <Box marginBottom={0}>
231
- <Text color="#00ffff">{frames.top(56)}</Text>
232
- </Box>
233
- <Box>
234
- <Text>
235
- {" "}
236
- {icons.scan} {gradients.cyber("BOS RESOURCE MONITOR")}
237
- </Text>
238
- </Box>
239
- <Box marginBottom={1}>
240
- <Text color="#00ffff">{frames.bottom(56)}</Text>
241
- </Box>
242
-
243
- <Box marginBottom={1}>
244
- <Text color="gray"> Account: </Text>
245
- <Text color="#00ffff">{account}</Text>
246
- </Box>
247
- {configPath && (
248
- <Box marginBottom={1}>
249
- <Text color="gray"> Config: </Text>
250
- <Text color="gray">{configPath}</Text>
251
- </Box>
252
- )}
253
-
254
- <Box marginBottom={1}>
255
- <Text color="gray"> Phase: </Text>
256
- <Text color={phaseColor}>{phaseLabel}</Text>
257
- {refreshing && <Text color="gray"> (refreshing...)</Text>}
258
- </Box>
259
-
260
- <Text>{colors.dim(divider(56))}</Text>
261
-
262
- {phase === "baseline" && (
263
- <SnapshotSection title="📊 BASELINE" snapshot={baseline} />
264
- )}
265
-
266
- {phase === "running" && (
267
- <>
268
- <SnapshotSection title="📊 BASELINE" snapshot={baseline} />
269
- <SnapshotSection title="🔄 CURRENT" snapshot={current} />
270
- </>
271
- )}
272
-
273
- {phase === "stopped" && (
274
- <>
275
- <SnapshotSection title="🔄 AFTER STOP" snapshot={current} />
276
- <DiffSection diff={diff} />
277
- </>
278
- )}
279
-
280
- <Text>{colors.dim(divider(56))}</Text>
281
- <Box marginTop={1}>
282
- <Text color="gray"> [r] refresh [s] snapshot [e] export [q] quit</Text>
283
- </Box>
284
- </Box>
285
- );
199
+ const { exit } = useApp();
200
+
201
+ useInput((input, key) => {
202
+ if (input === "q" || (key.ctrl && input === "c")) {
203
+ onExit();
204
+ exit();
205
+ }
206
+ if (input === "r") onRefresh();
207
+ if (input === "s") onSnapshot();
208
+ if (input === "e") onExport();
209
+ });
210
+
211
+ let account = "unknown";
212
+ let configPath = "";
213
+ try {
214
+ const config = getConfig();
215
+ if (config) {
216
+ account = config.account;
217
+ configPath = `${getProjectRoot()}/bos.config.json`;
218
+ }
219
+ } catch {
220
+ // No config
221
+ }
222
+
223
+ const phaseLabel =
224
+ phase === "baseline"
225
+ ? "BASELINE"
226
+ : phase === "running"
227
+ ? "RUNNING"
228
+ : "STOPPED";
229
+ const phaseColor =
230
+ phase === "baseline" ? "gray" : phase === "running" ? "#00ffff" : "#ff00ff";
231
+
232
+ return (
233
+ <Box flexDirection="column">
234
+ <Box marginBottom={0}>
235
+ <Text color="#00ffff">{frames.top(56)}</Text>
236
+ </Box>
237
+ <Box>
238
+ <Text>
239
+ {" "}
240
+ {icons.scan} {gradients.cyber("BOS RESOURCE MONITOR")}
241
+ </Text>
242
+ </Box>
243
+ <Box marginBottom={1}>
244
+ <Text color="#00ffff">{frames.bottom(56)}</Text>
245
+ </Box>
246
+
247
+ <Box marginBottom={1}>
248
+ <Text color="gray"> Account: </Text>
249
+ <Text color="#00ffff">{account}</Text>
250
+ </Box>
251
+ {configPath && (
252
+ <Box marginBottom={1}>
253
+ <Text color="gray"> Config: </Text>
254
+ <Text color="gray">{configPath}</Text>
255
+ </Box>
256
+ )}
257
+
258
+ <Box marginBottom={1}>
259
+ <Text color="gray"> Phase: </Text>
260
+ <Text color={phaseColor}>{phaseLabel}</Text>
261
+ {refreshing && <Text color="gray"> (refreshing...)</Text>}
262
+ </Box>
263
+
264
+ <Text>{colors.dim(divider(56))}</Text>
265
+
266
+ {phase === "baseline" && (
267
+ <SnapshotSection title="📊 BASELINE" snapshot={baseline} />
268
+ )}
269
+
270
+ {phase === "running" && (
271
+ <>
272
+ <SnapshotSection title="📊 BASELINE" snapshot={baseline} />
273
+ <SnapshotSection title="🔄 CURRENT" snapshot={current} />
274
+ </>
275
+ )}
276
+
277
+ {phase === "stopped" && (
278
+ <>
279
+ <SnapshotSection title="🔄 AFTER STOP" snapshot={current} />
280
+ <DiffSection diff={diff} />
281
+ </>
282
+ )}
283
+
284
+ <Text>{colors.dim(divider(56))}</Text>
285
+ <Box marginTop={1}>
286
+ <Text color="gray"> [r] refresh [s] snapshot [e] export [q] quit</Text>
287
+ </Box>
288
+ </Box>
289
+ );
286
290
  }
287
291
 
288
292
  export interface MonitorViewHandle {
289
- setPhase: (phase: Phase) => void;
290
- setBaseline: (snapshot: Snapshot) => void;
291
- setCurrent: (snapshot: Snapshot) => void;
292
- setDiff: (diff: SnapshotDiff) => void;
293
- unmount: () => void;
293
+ setPhase: (phase: Phase) => void;
294
+ setBaseline: (snapshot: Snapshot) => void;
295
+ setCurrent: (snapshot: Snapshot) => void;
296
+ setDiff: (diff: SnapshotDiff) => void;
297
+ unmount: () => void;
294
298
  }
295
299
 
296
300
  export interface MonitorViewOptions {
297
- ports?: number[];
298
- onExit?: () => void;
299
- onExport?: (data: unknown) => void;
301
+ ports?: number[];
302
+ onExit?: () => void;
303
+ onExport?: (data: unknown) => void;
300
304
  }
301
305
 
302
306
  const runEffect = <A,>(effect: Effect.Effect<A, unknown, never>): Promise<A> =>
303
- effect.pipe(Logger.withMinimumLogLevel(LogLevel.Info), Effect.runPromise);
307
+ effect.pipe(Logger.withMinimumLogLevel(LogLevel.Info), Effect.runPromise);
304
308
 
305
309
  const runSnapshotEffect = (config?: MonitorConfig): Promise<Snapshot> =>
306
- createSnapshotWithPlatform(config).pipe(
307
- Effect.provide(PlatformLive),
308
- Logger.withMinimumLogLevel(LogLevel.Info),
309
- Effect.runPromise
310
- );
310
+ createSnapshotWithPlatform(config).pipe(
311
+ Effect.provide(PlatformLive),
312
+ Logger.withMinimumLogLevel(LogLevel.Info),
313
+ Effect.runPromise,
314
+ );
311
315
 
312
316
  export function renderMonitorView(
313
- options: MonitorViewOptions = {}
317
+ options: MonitorViewOptions = {},
314
318
  ): MonitorViewHandle {
315
- let phase: Phase = "baseline";
316
- let baseline: Snapshot | null = null;
317
- let current: Snapshot | null = null;
318
- let diff: SnapshotDiff | null = null;
319
- let refreshing = false;
320
- let rerender: (() => void) | null = null;
321
- let monitor: ResourceMonitor | null = null;
322
- const config: MonitorConfig | undefined = options.ports
323
- ? { ports: options.ports }
324
- : undefined;
325
-
326
- const initMonitor = async () => {
327
- monitor = await runEffect(ResourceMonitor.createWithPlatform(config));
328
- };
329
-
330
- initMonitor();
331
-
332
- const setPhase = (p: Phase) => {
333
- phase = p;
334
- rerender?.();
335
- };
336
-
337
- const setBaseline = (snap: Snapshot) => {
338
- baseline = snap;
339
- rerender?.();
340
- };
341
-
342
- const setCurrent = (snap: Snapshot) => {
343
- current = snap;
344
- if (baseline && phase === "stopped") {
345
- diff = diffSnapshots(baseline, snap);
346
- }
347
- rerender?.();
348
- };
349
-
350
- const setDiff = (d: SnapshotDiff) => {
351
- diff = d;
352
- rerender?.();
353
- };
354
-
355
- const handleRefresh = async () => {
356
- if (!monitor) return;
357
- refreshing = true;
358
- rerender?.();
359
-
360
- const snap = await runEffect(monitor.snapshotWithPlatform());
361
- if (phase === "baseline") {
362
- baseline = snap;
363
- } else {
364
- current = snap;
365
- if (baseline && phase === "stopped") {
366
- diff = diffSnapshots(baseline, snap);
367
- }
368
- }
369
-
370
- refreshing = false;
371
- rerender?.();
372
- };
373
-
374
- const handleSnapshot = async () => {
375
- if (!monitor) return;
376
- const snap = await runEffect(monitor.snapshotWithPlatform());
377
-
378
- if (!baseline) {
379
- baseline = snap;
380
- phase = "running";
381
- } else if (phase === "running") {
382
- current = snap;
383
- } else {
384
- current = snap;
385
- diff = diffSnapshots(baseline, snap);
386
- }
387
-
388
- rerender?.();
389
- };
390
-
391
- const handleExport = async () => {
392
- if (!monitor) return;
393
- const exportPath = `.bos/monitor-export-${Date.now()}.json`;
394
- await runEffect(monitor.export(exportPath));
395
- options.onExport?.({ path: exportPath });
396
- };
397
-
398
- const handleExit = () => {
399
- options.onExit?.();
400
- };
401
-
402
- function MonitorViewWrapper() {
403
- const [, forceUpdate] = useState(0);
404
-
405
- useEffect(() => {
406
- rerender = () => forceUpdate((n) => n + 1);
407
-
408
- handleRefresh();
409
-
410
- return () => {
411
- rerender = null;
412
- };
413
- }, []);
414
-
415
- return (
416
- <MonitorView
417
- baseline={baseline}
418
- current={current}
419
- diff={diff}
420
- phase={phase}
421
- refreshing={refreshing}
422
- onRefresh={handleRefresh}
423
- onSnapshot={handleSnapshot}
424
- onExport={handleExport}
425
- onExit={handleExit}
426
- />
427
- );
428
- }
429
-
430
- const { unmount } = render(<MonitorViewWrapper />);
431
-
432
- return { setPhase, setBaseline, setCurrent, setDiff, unmount };
319
+ let phase: Phase = "baseline";
320
+ let baseline: Snapshot | null = null;
321
+ let current: Snapshot | null = null;
322
+ let diff: SnapshotDiff | null = null;
323
+ let refreshing = false;
324
+ let rerender: (() => void) | null = null;
325
+ let monitor: ResourceMonitor | null = null;
326
+ const config: MonitorConfig | undefined = options.ports
327
+ ? { ports: options.ports }
328
+ : undefined;
329
+
330
+ const initMonitor = async () => {
331
+ monitor = await runEffect(ResourceMonitor.createWithPlatform(config));
332
+ };
333
+
334
+ initMonitor();
335
+
336
+ const setPhase = (p: Phase) => {
337
+ phase = p;
338
+ rerender?.();
339
+ };
340
+
341
+ const setBaseline = (snap: Snapshot) => {
342
+ baseline = snap;
343
+ rerender?.();
344
+ };
345
+
346
+ const setCurrent = (snap: Snapshot) => {
347
+ current = snap;
348
+ if (baseline && phase === "stopped") {
349
+ diff = diffSnapshots(baseline, snap);
350
+ }
351
+ rerender?.();
352
+ };
353
+
354
+ const setDiff = (d: SnapshotDiff) => {
355
+ diff = d;
356
+ rerender?.();
357
+ };
358
+
359
+ const handleRefresh = async () => {
360
+ if (!monitor) return;
361
+ refreshing = true;
362
+ rerender?.();
363
+
364
+ const snap = await runEffect(monitor.snapshotWithPlatform());
365
+ if (phase === "baseline") {
366
+ baseline = snap;
367
+ } else {
368
+ current = snap;
369
+ if (baseline && phase === "stopped") {
370
+ diff = diffSnapshots(baseline, snap);
371
+ }
372
+ }
373
+
374
+ refreshing = false;
375
+ rerender?.();
376
+ };
377
+
378
+ const handleSnapshot = async () => {
379
+ if (!monitor) return;
380
+ const snap = await runEffect(monitor.snapshotWithPlatform());
381
+
382
+ if (!baseline) {
383
+ baseline = snap;
384
+ phase = "running";
385
+ } else if (phase === "running") {
386
+ current = snap;
387
+ } else {
388
+ current = snap;
389
+ diff = diffSnapshots(baseline, snap);
390
+ }
391
+
392
+ rerender?.();
393
+ };
394
+
395
+ const handleExport = async () => {
396
+ if (!monitor) return;
397
+ const exportPath = `.bos/monitor-export-${Date.now()}.json`;
398
+ await runEffect(monitor.export(exportPath));
399
+ options.onExport?.({ path: exportPath });
400
+ };
401
+
402
+ const handleExit = () => {
403
+ options.onExit?.();
404
+ };
405
+
406
+ function MonitorViewWrapper() {
407
+ const [, forceUpdate] = useState(0);
408
+
409
+ useEffect(() => {
410
+ rerender = () => forceUpdate((n) => n + 1);
411
+
412
+ handleRefresh();
413
+
414
+ return () => {
415
+ rerender = null;
416
+ };
417
+ }, []);
418
+
419
+ return (
420
+ <MonitorView
421
+ baseline={baseline}
422
+ current={current}
423
+ diff={diff}
424
+ phase={phase}
425
+ refreshing={refreshing}
426
+ onRefresh={handleRefresh}
427
+ onSnapshot={handleSnapshot}
428
+ onExport={handleExport}
429
+ onExit={handleExit}
430
+ />
431
+ );
432
+ }
433
+
434
+ const { unmount } = render(<MonitorViewWrapper />);
435
+
436
+ return { setPhase, setBaseline, setCurrent, setDiff, unmount };
433
437
  }
434
438
 
435
439
  export async function runMonitorCli(
436
- options: { ports?: number[]; json?: boolean } = {}
440
+ options: { ports?: number[]; json?: boolean } = {},
437
441
  ) {
438
- const config: MonitorConfig | undefined = options.ports
439
- ? { ports: options.ports }
440
- : undefined;
441
-
442
- if (options.json) {
443
- const snapshot = await runSnapshotEffect(config);
444
- console.log(JSON.stringify(snapshot, null, 2));
445
- return;
446
- }
447
-
448
- const monitor = await runEffect(ResourceMonitor.createWithPlatform(config));
449
-
450
- const view = renderMonitorView({
451
- ports: options.ports,
452
- onExit: () => process.exit(0),
453
- onExport: (data) =>
454
- console.log("Exported to:", (data as { path: string }).path),
455
- });
456
-
457
- const baseline = await runEffect(monitor.setBaselineWithPlatform());
458
- view.setBaseline(baseline);
459
- view.setPhase("baseline");
460
-
461
- const interval = setInterval(async () => {
462
- const snap = await runEffect(monitor.snapshotWithPlatform());
463
- view.setCurrent(snap);
464
- }, 2000);
465
-
466
- process.on("SIGINT", () => {
467
- clearInterval(interval);
468
- view.unmount();
469
- process.exit(0);
470
- });
442
+ const config: MonitorConfig | undefined = options.ports
443
+ ? { ports: options.ports }
444
+ : undefined;
445
+
446
+ if (options.json) {
447
+ const snapshot = await runSnapshotEffect(config);
448
+ console.log(JSON.stringify(snapshot, null, 2));
449
+ return;
450
+ }
451
+
452
+ const monitor = await runEffect(ResourceMonitor.createWithPlatform(config));
453
+
454
+ const view = renderMonitorView({
455
+ ports: options.ports,
456
+ onExit: () => process.exit(0),
457
+ onExport: (data) =>
458
+ console.log("Exported to:", (data as { path: string }).path),
459
+ });
460
+
461
+ const baseline = await runEffect(monitor.setBaselineWithPlatform());
462
+ view.setBaseline(baseline);
463
+ view.setPhase("baseline");
464
+
465
+ const interval = setInterval(async () => {
466
+ const snap = await runEffect(monitor.snapshotWithPlatform());
467
+ view.setCurrent(snap);
468
+ }, 2000);
469
+
470
+ process.on("SIGINT", () => {
471
+ clearInterval(interval);
472
+ view.unmount();
473
+ process.exit(0);
474
+ });
471
475
  }