everything-dev 0.1.3 → 0.1.5

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/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "everything-dev",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
8
- "./types": "./src/types.ts"
8
+ "./types": "./src/types.ts",
9
+ "./ui": "./src/ui/index.ts",
10
+ "./ui/types": "./src/ui/types.ts",
11
+ "./ui/runtime": "./src/ui/runtime.ts",
12
+ "./ui/head": "./src/ui/head.ts"
9
13
  },
10
14
  "files": [
11
15
  "src"
@@ -18,14 +22,14 @@
18
22
  },
19
23
  "scripts": {
20
24
  "bos": "bun run src/cli.ts",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
21
27
  "typecheck": "tsc --noEmit"
22
28
  },
23
29
  "dependencies": {
24
30
  "@clack/prompts": "^1.0.0",
25
31
  "@effect/platform": "^0.94.2",
26
32
  "@effect/platform-bun": "^0.87.1",
27
- "effect": "3.19.15",
28
- "zod": "4.3.5",
29
33
  "@hono/node-server": "^1.19.9",
30
34
  "@inquirer/prompts": "^8.2.0",
31
35
  "@libsql/client": "^0.17.0",
@@ -36,6 +40,7 @@
36
40
  "chalk": "^5.6.2",
37
41
  "commander": "^14.0.2",
38
42
  "degit": "^2.8.4",
43
+ "effect": "3.19.15",
39
44
  "every-plugin": "0.9.0",
40
45
  "execa": "^9.6.1",
41
46
  "gradient-string": "^3.0.0",
@@ -45,13 +50,30 @@
45
50
  "nova-sdk-js": "^1.0.3",
46
51
  "open": "^11.0.0",
47
52
  "react": "18.3.1",
48
- "react-dom": "18.3.1"
53
+ "react-dom": "18.3.1",
54
+ "zod": "4.3.5"
49
55
  },
50
56
  "devDependencies": {
51
- "@types/bun": "latest",
57
+ "@tanstack/react-query": "^5.0.0",
58
+ "@tanstack/react-router": "^1.0.0",
59
+ "@types/bun": "^1.3.8",
60
+ "@types/gradient-string": "^1.1.6",
52
61
  "@types/react": "^18.3.0",
53
62
  "@types/react-dom": "^18.3.0",
54
- "@types/gradient-string": "^1.1.6",
55
- "typescript": "^5.9.3"
63
+ "playwright": "^1.58.1",
64
+ "typescript": "^5.9.3",
65
+ "vitest": "^4.0.18"
66
+ },
67
+ "peerDependencies": {
68
+ "@tanstack/react-query": ">=5.0.0",
69
+ "@tanstack/react-router": ">=1.0.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@tanstack/react-query": {
73
+ "optional": true
74
+ },
75
+ "@tanstack/react-router": {
76
+ "optional": true
77
+ }
56
78
  }
57
79
  }
package/src/cli.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { spinner } from "@clack/prompts";
3
3
  import { program } from "commander";
4
4
  import { createPluginRuntime } from "every-plugin";
5
- import { getConfigDir, getConfigPath, getPackages, getTitle, loadConfig, type BosConfig } from "./config";
5
+ import { type BosConfig, getConfigDir, getConfigPath, getPackages, getTitle, loadConfig } from "./config";
6
6
  import BosPlugin from "./plugin";
7
7
  import { printBanner } from "./utils/banner";
8
8
  import { colors, frames, gradients, icons } from "./utils/theme";
@@ -854,6 +854,29 @@ Zephyr Configuration:
854
854
  console.log();
855
855
  });
856
856
 
857
+ program
858
+ .command("monitor")
859
+ .description("Monitor system resources (ports, processes, memory)")
860
+ .option("--json", "Output as JSON")
861
+ .option("-w, --watch", "Watch mode with live updates")
862
+ .option("-p, --ports <ports>", "Ports to monitor (comma-separated)")
863
+ .action(async (options) => {
864
+ const result = await client.monitor({
865
+ json: options.json || false,
866
+ watch: options.watch || false,
867
+ ports: options.ports ? options.ports.split(",").map(Number) : undefined,
868
+ });
869
+
870
+ if (result.status === "error") {
871
+ console.error(colors.error(`${icons.err} ${result.error}`));
872
+ process.exit(1);
873
+ }
874
+
875
+ if (result.status === "snapshot" && options.json) {
876
+ console.log(JSON.stringify(result.snapshot, null, 2));
877
+ }
878
+ });
879
+
857
880
  program
858
881
  .command("kill")
859
882
  .description("Kill all tracked BOS processes")
@@ -1115,6 +1138,91 @@ Zephyr Configuration:
1115
1138
  console.log();
1116
1139
  });
1117
1140
 
1141
+ program
1142
+ .command("session")
1143
+ .description("Record a performance analysis session with Playwright")
1144
+ .option("--headless", "Run browser in headless mode (default: true)", true)
1145
+ .option("--no-headless", "Run browser with UI visible")
1146
+ .option("-t, --timeout <ms>", "Session timeout in milliseconds", "120000")
1147
+ .option("-o, --output <path>", "Output report path", "./session-report.json")
1148
+ .option("-f, --format <format>", "Report format: json | html", "json")
1149
+ .option("--flow <flow>", "Flow to run: login | navigation | custom", "login")
1150
+ .option("--routes <routes>", "Routes for navigation flow (comma-separated)")
1151
+ .option("--interval <ms>", "Snapshot interval in milliseconds", "2000")
1152
+ .action(async (options) => {
1153
+ console.log();
1154
+ console.log(colors.cyan(frames.top(52)));
1155
+ console.log(` ${icons.scan} ${gradients.cyber("SESSION RECORDER")}`);
1156
+ console.log(colors.cyan(frames.bottom(52)));
1157
+ console.log();
1158
+ console.log(` ${colors.dim("Flow:")} ${colors.white(options.flow)}`);
1159
+ console.log(` ${colors.dim("Headless:")} ${colors.white(String(options.headless))}`);
1160
+ console.log(` ${colors.dim("Output:")} ${colors.white(options.output)}`);
1161
+ console.log(` ${colors.dim("Timeout:")} ${colors.white(options.timeout)}ms`);
1162
+ console.log();
1163
+
1164
+ const s = spinner();
1165
+ s.start("Starting session recording...");
1166
+
1167
+ const result = await client.session({
1168
+ headless: options.headless,
1169
+ timeout: parseInt(options.timeout, 10),
1170
+ output: options.output,
1171
+ format: options.format as "json" | "html",
1172
+ flow: options.flow as "login" | "navigation" | "custom",
1173
+ routes: options.routes ? options.routes.split(",") : undefined,
1174
+ snapshotInterval: parseInt(options.interval, 10),
1175
+ });
1176
+
1177
+ if (result.status === "error") {
1178
+ s.stop(colors.error(`${icons.err} Session failed: ${result.error}`));
1179
+ process.exit(1);
1180
+ }
1181
+
1182
+ if (result.status === "timeout") {
1183
+ s.stop(colors.error(`${icons.err} Session timed out`));
1184
+ process.exit(1);
1185
+ }
1186
+
1187
+ s.stop(colors.green(`${icons.ok} Session completed`));
1188
+
1189
+ console.log();
1190
+ console.log(colors.cyan(frames.top(52)));
1191
+ console.log(` ${icons.ok} ${gradients.cyber("SESSION SUMMARY")}`);
1192
+ console.log(colors.cyan(frames.bottom(52)));
1193
+ console.log();
1194
+
1195
+ if (result.summary) {
1196
+ console.log(` ${colors.dim("Session ID:")} ${colors.white(result.sessionId || "")}`);
1197
+ console.log(` ${colors.dim("Duration:")} ${colors.white(`${(result.summary.duration / 1000).toFixed(1)}s`)}`);
1198
+ console.log(` ${colors.dim("Events:")} ${colors.white(String(result.summary.eventCount))}`);
1199
+ console.log();
1200
+ console.log(` ${colors.dim("Peak Memory:")} ${colors.cyan(`${result.summary.peakMemoryMb.toFixed(1)} MB`)}`);
1201
+ console.log(` ${colors.dim("Avg Memory:")} ${colors.cyan(`${result.summary.averageMemoryMb.toFixed(1)} MB`)}`);
1202
+ console.log(` ${colors.dim("Delta:")} ${result.summary.totalMemoryDeltaMb >= 0 ? colors.cyan("+") : ""}${colors.cyan(`${result.summary.totalMemoryDeltaMb.toFixed(1)} MB`)}`);
1203
+ console.log();
1204
+ console.log(` ${colors.dim("Processes:")} ${colors.white(`${result.summary.processesSpawned} spawned, ${result.summary.processesKilled} killed`)}`);
1205
+ console.log(` ${colors.dim("Ports:")} ${colors.white(result.summary.portsUsed.join(", "))}`);
1206
+ console.log();
1207
+
1208
+ if (result.status === "leaks_detected") {
1209
+ console.log(colors.error(` ${icons.err} RESOURCE LEAKS DETECTED`));
1210
+ if (result.summary.orphanedProcesses > 0) {
1211
+ console.log(colors.error(` - ${result.summary.orphanedProcesses} orphaned process(es)`));
1212
+ }
1213
+ if (result.summary.portsLeaked > 0) {
1214
+ console.log(colors.error(` - ${result.summary.portsLeaked} port(s) still bound`));
1215
+ }
1216
+ } else {
1217
+ console.log(colors.green(` ${icons.ok} No resource leaks detected`));
1218
+ }
1219
+ }
1220
+
1221
+ console.log();
1222
+ console.log(` ${colors.dim("Report:")} ${colors.cyan(result.reportPath || options.output)}`);
1223
+ console.log();
1224
+ });
1225
+
1118
1226
  program.parse();
1119
1227
  }
1120
1228
 
@@ -0,0 +1,471 @@
1
+ import { Box, render, Text, useApp, useInput } from "ink";
2
+ import { useEffect, useState } from "react";
3
+ import { Effect, Logger, LogLevel } from "effect";
4
+ import {
5
+ ResourceMonitor,
6
+ PlatformLive,
7
+ createSnapshotWithPlatform,
8
+ diffSnapshots,
9
+ type Snapshot,
10
+ type SnapshotDiff,
11
+ type MonitorConfig,
12
+ } from "../lib/resource-monitor";
13
+ import { getAccount, getConfigPath } from "../config";
14
+ import { colors, divider, gradients, icons, frames } from "../utils/theme";
15
+
16
+ type Phase = "baseline" | "running" | "stopped";
17
+
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;
28
+ }
29
+
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`;
34
+ }
35
+
36
+ function PortRow({
37
+ port,
38
+ info,
39
+ }: {
40
+ port: number;
41
+ info: { pid: number | null; command: string | null; state: string };
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
+ );
61
+ }
62
+
63
+ function ProcessRow({
64
+ proc,
65
+ }: {
66
+ proc: { pid: number; command: string; rss: number; children: number[] };
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
+ );
79
+ }
80
+
81
+ function SnapshotSection({
82
+ title,
83
+ snapshot,
84
+ }: {
85
+ title: string;
86
+ snapshot: Snapshot | null;
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
+ );
139
+ }
140
+
141
+ 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
+ );
189
+ }
190
+
191
+ function MonitorView({
192
+ baseline,
193
+ current,
194
+ diff,
195
+ phase,
196
+ refreshing,
197
+ onRefresh,
198
+ onSnapshot,
199
+ onExport,
200
+ onExit,
201
+ }: 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
+ );
286
+ }
287
+
288
+ 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;
294
+ }
295
+
296
+ export interface MonitorViewOptions {
297
+ ports?: number[];
298
+ onExit?: () => void;
299
+ onExport?: (data: unknown) => void;
300
+ }
301
+
302
+ const runEffect = <A,>(effect: Effect.Effect<A, unknown, never>): Promise<A> =>
303
+ effect.pipe(Logger.withMinimumLogLevel(LogLevel.Info), Effect.runPromise);
304
+
305
+ const runSnapshotEffect = (config?: MonitorConfig): Promise<Snapshot> =>
306
+ createSnapshotWithPlatform(config).pipe(
307
+ Effect.provide(PlatformLive),
308
+ Logger.withMinimumLogLevel(LogLevel.Info),
309
+ Effect.runPromise
310
+ );
311
+
312
+ export function renderMonitorView(
313
+ options: MonitorViewOptions = {}
314
+ ): 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 };
433
+ }
434
+
435
+ export async function runMonitorCli(
436
+ options: { ports?: number[]; json?: boolean } = {}
437
+ ) {
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
+ });
471
+ }