everything-dev 0.1.2 → 0.1.4
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 +11 -7
- package/src/cli.ts +109 -1
- package/src/components/monitor-view.tsx +471 -0
- package/src/contract.ts +94 -0
- package/src/lib/nova.ts +11 -11
- package/src/lib/orchestrator.ts +13 -2
- package/src/lib/process.ts +50 -5
- package/src/lib/resource-monitor/assertions.ts +234 -0
- package/src/lib/resource-monitor/command.ts +283 -0
- package/src/lib/resource-monitor/diff.ts +143 -0
- package/src/lib/resource-monitor/errors.ts +127 -0
- package/src/lib/resource-monitor/index.ts +305 -0
- package/src/lib/resource-monitor/platform/darwin.ts +293 -0
- package/src/lib/resource-monitor/platform/index.ts +35 -0
- package/src/lib/resource-monitor/platform/linux.ts +332 -0
- package/src/lib/resource-monitor/platform/windows.ts +298 -0
- package/src/lib/resource-monitor/snapshot.ts +204 -0
- package/src/lib/resource-monitor/types.ts +74 -0
- package/src/lib/session-recorder/errors.ts +102 -0
- package/src/lib/session-recorder/flows/login.ts +210 -0
- package/src/lib/session-recorder/index.ts +361 -0
- package/src/lib/session-recorder/playwright.ts +257 -0
- package/src/lib/session-recorder/report.ts +353 -0
- package/src/lib/session-recorder/server.ts +267 -0
- package/src/lib/session-recorder/types.ts +115 -0
- package/src/plugin.ts +161 -17
- package/src/types.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "everything-dev",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"bos": "bun run src/cli.ts",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
21
23
|
"typecheck": "tsc --noEmit"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
24
26
|
"@clack/prompts": "^1.0.0",
|
|
25
27
|
"@effect/platform": "^0.94.2",
|
|
26
28
|
"@effect/platform-bun": "^0.87.1",
|
|
27
|
-
"effect": "3.19.15",
|
|
28
|
-
"zod": "4.3.5",
|
|
29
29
|
"@hono/node-server": "^1.19.9",
|
|
30
30
|
"@inquirer/prompts": "^8.2.0",
|
|
31
31
|
"@libsql/client": "^0.17.0",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.2",
|
|
38
38
|
"degit": "^2.8.4",
|
|
39
|
+
"effect": "3.19.15",
|
|
39
40
|
"every-plugin": "0.9.0",
|
|
40
41
|
"execa": "^9.6.1",
|
|
41
42
|
"gradient-string": "^3.0.0",
|
|
@@ -45,13 +46,16 @@
|
|
|
45
46
|
"nova-sdk-js": "^1.0.3",
|
|
46
47
|
"open": "^11.0.0",
|
|
47
48
|
"react": "18.3.1",
|
|
48
|
-
"react-dom": "18.3.1"
|
|
49
|
+
"react-dom": "18.3.1",
|
|
50
|
+
"zod": "4.3.5"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
|
-
"@types/bun": "
|
|
53
|
+
"@types/bun": "^1.3.8",
|
|
54
|
+
"@types/gradient-string": "^1.1.6",
|
|
52
55
|
"@types/react": "^18.3.0",
|
|
53
56
|
"@types/react-dom": "^18.3.0",
|
|
54
|
-
"
|
|
55
|
-
"typescript": "^5.9.3"
|
|
57
|
+
"playwright": "^1.58.1",
|
|
58
|
+
"typescript": "^5.9.3",
|
|
59
|
+
"vitest": "^4.0.18"
|
|
56
60
|
}
|
|
57
61
|
}
|
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
|
|
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
|
+
}
|