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.
@@ -0,0 +1,353 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { Effect } from "effect";
3
+ import { diffSnapshots, hasLeaks } from "../resource-monitor";
4
+ import { ExportFailed } from "./errors";
5
+ import type {
6
+ SessionConfig,
7
+ SessionEvent,
8
+ SessionReport,
9
+ SessionSummary,
10
+ } from "./types";
11
+
12
+ export const generateSummary = (
13
+ events: SessionEvent[],
14
+ config: SessionConfig
15
+ ): SessionSummary => {
16
+ if (events.length === 0) {
17
+ return {
18
+ totalMemoryDeltaMb: 0,
19
+ peakMemoryMb: 0,
20
+ averageMemoryMb: 0,
21
+ processesSpawned: 0,
22
+ processesKilled: 0,
23
+ orphanedProcesses: 0,
24
+ portsUsed: config.ports,
25
+ portsLeaked: 0,
26
+ hasLeaks: false,
27
+ eventCount: 0,
28
+ duration: 0,
29
+ };
30
+ }
31
+
32
+ const baselineEvent = events.find((e) => e.type === "baseline");
33
+ const lastEvent = events[events.length - 1];
34
+
35
+ const memoryValues = events
36
+ .map((e) => e.snapshot.memory.processRss / 1024 / 1024)
37
+ .filter((v) => v > 0);
38
+
39
+ const peakMemoryMb = Math.max(...memoryValues, 0);
40
+ const averageMemoryMb = memoryValues.length > 0
41
+ ? memoryValues.reduce((a, b) => a + b, 0) / memoryValues.length
42
+ : 0;
43
+
44
+ const baselineMemory = baselineEvent?.snapshot.memory.processRss ?? 0;
45
+ const finalMemory = lastEvent.snapshot.memory.processRss;
46
+ const totalMemoryDeltaMb = (finalMemory - baselineMemory) / 1024 / 1024;
47
+
48
+ const allProcessPids = new Set<number>();
49
+ const finalProcessPids = new Set<number>();
50
+
51
+ for (const event of events) {
52
+ for (const proc of event.snapshot.processes) {
53
+ allProcessPids.add(proc.pid);
54
+ }
55
+ }
56
+
57
+ for (const proc of lastEvent.snapshot.processes) {
58
+ finalProcessPids.add(proc.pid);
59
+ }
60
+
61
+ const processesSpawned = allProcessPids.size;
62
+ const processesKilled = allProcessPids.size - finalProcessPids.size;
63
+
64
+ let orphanedProcesses = 0;
65
+ let portsLeaked = 0;
66
+ let leaksDetected = false;
67
+
68
+ if (baselineEvent) {
69
+ const diff = diffSnapshots(baselineEvent.snapshot, lastEvent.snapshot);
70
+ orphanedProcesses = diff.orphanedProcesses.length;
71
+ portsLeaked = diff.stillBoundPorts.length;
72
+ leaksDetected = hasLeaks(diff);
73
+ }
74
+
75
+ const browserMetricsEvents = events.filter((e) => e.browserMetrics);
76
+ let browserMetricsSummary: SessionSummary["browserMetricsSummary"];
77
+
78
+ if (browserMetricsEvents.length > 0) {
79
+ const jsHeapValues = browserMetricsEvents.map(
80
+ (e) => e.browserMetrics!.jsHeapUsedSize / 1024 / 1024
81
+ );
82
+ const layoutCounts = browserMetricsEvents.map(
83
+ (e) => e.browserMetrics!.layoutCount
84
+ );
85
+ const scriptDurations = browserMetricsEvents.map(
86
+ (e) => e.browserMetrics!.scriptDuration
87
+ );
88
+
89
+ browserMetricsSummary = {
90
+ peakJsHeapMb: Math.max(...jsHeapValues),
91
+ averageJsHeapMb: jsHeapValues.reduce((a, b) => a + b, 0) / jsHeapValues.length,
92
+ totalLayoutCount: Math.max(...layoutCounts),
93
+ totalScriptDuration: scriptDurations.reduce((a, b) => a + b, 0),
94
+ };
95
+ }
96
+
97
+ const duration = lastEvent.timestamp - (baselineEvent?.timestamp ?? events[0].timestamp);
98
+
99
+ return {
100
+ totalMemoryDeltaMb,
101
+ peakMemoryMb,
102
+ averageMemoryMb,
103
+ processesSpawned,
104
+ processesKilled,
105
+ orphanedProcesses,
106
+ portsUsed: config.ports,
107
+ portsLeaked,
108
+ hasLeaks: leaksDetected,
109
+ eventCount: events.length,
110
+ duration,
111
+ browserMetricsSummary,
112
+ };
113
+ };
114
+
115
+ export const generateReport = (
116
+ sessionId: string,
117
+ config: SessionConfig,
118
+ events: SessionEvent[],
119
+ startTime: number,
120
+ endTime: number
121
+ ): SessionReport => {
122
+ const summary = generateSummary(events, config);
123
+
124
+ return {
125
+ sessionId,
126
+ config,
127
+ startTime,
128
+ endTime,
129
+ events,
130
+ summary,
131
+ platform: process.platform,
132
+ nodeVersion: process.version,
133
+ };
134
+ };
135
+
136
+ export const exportJSON = (
137
+ report: SessionReport,
138
+ filepath: string
139
+ ): Effect.Effect<void, ExportFailed> =>
140
+ Effect.gen(function* () {
141
+ yield* Effect.logInfo(`Exporting session report to ${filepath}`);
142
+
143
+ yield* Effect.tryPromise({
144
+ try: () => writeFile(filepath, JSON.stringify(report, null, 2)),
145
+ catch: (e) => new ExportFailed({
146
+ path: filepath,
147
+ reason: String(e),
148
+ }),
149
+ });
150
+
151
+ yield* Effect.logInfo(`Report exported: ${report.events.length} events, ${report.summary.duration}ms duration`);
152
+ });
153
+
154
+ export const formatReportSummary = (report: SessionReport): string => {
155
+ const lines: string[] = [];
156
+ const { summary } = report;
157
+
158
+ lines.push("═".repeat(60));
159
+ lines.push(" SESSION REPORT SUMMARY");
160
+ lines.push("═".repeat(60));
161
+ lines.push("");
162
+ lines.push(` Session ID: ${report.sessionId}`);
163
+ lines.push(` Duration: ${(summary.duration / 1000).toFixed(1)}s`);
164
+ lines.push(` Events: ${summary.eventCount}`);
165
+ lines.push(` Platform: ${report.platform}`);
166
+ lines.push("");
167
+ lines.push("─".repeat(60));
168
+ lines.push(" MEMORY");
169
+ lines.push("─".repeat(60));
170
+ lines.push(` Peak: ${summary.peakMemoryMb.toFixed(1)} MB`);
171
+ lines.push(` Average: ${summary.averageMemoryMb.toFixed(1)} MB`);
172
+ lines.push(` Delta: ${summary.totalMemoryDeltaMb >= 0 ? "+" : ""}${summary.totalMemoryDeltaMb.toFixed(1)} MB`);
173
+ lines.push("");
174
+ lines.push("─".repeat(60));
175
+ lines.push(" PROCESSES");
176
+ lines.push("─".repeat(60));
177
+ lines.push(` Spawned: ${summary.processesSpawned}`);
178
+ lines.push(` Killed: ${summary.processesKilled}`);
179
+ lines.push(` Orphaned: ${summary.orphanedProcesses}`);
180
+ lines.push("");
181
+ lines.push("─".repeat(60));
182
+ lines.push(" PORTS");
183
+ lines.push("─".repeat(60));
184
+ lines.push(` Monitored: ${summary.portsUsed.join(", ")}`);
185
+ lines.push(` Leaked: ${summary.portsLeaked}`);
186
+ lines.push("");
187
+
188
+ if (summary.browserMetricsSummary) {
189
+ lines.push("─".repeat(60));
190
+ lines.push(" BROWSER METRICS");
191
+ lines.push("─".repeat(60));
192
+ lines.push(` Peak JS Heap: ${summary.browserMetricsSummary.peakJsHeapMb.toFixed(1)} MB`);
193
+ lines.push(` Avg JS Heap: ${summary.browserMetricsSummary.averageJsHeapMb.toFixed(1)} MB`);
194
+ lines.push(` Layout Count: ${summary.browserMetricsSummary.totalLayoutCount}`);
195
+ lines.push(` Script Time: ${summary.browserMetricsSummary.totalScriptDuration.toFixed(2)}s`);
196
+ lines.push("");
197
+ }
198
+
199
+ lines.push("═".repeat(60));
200
+ if (summary.hasLeaks) {
201
+ lines.push(" ❌ RESOURCE LEAKS DETECTED");
202
+ if (summary.orphanedProcesses > 0) {
203
+ lines.push(` - ${summary.orphanedProcesses} orphaned process(es)`);
204
+ }
205
+ if (summary.portsLeaked > 0) {
206
+ lines.push(` - ${summary.portsLeaked} port(s) still bound`);
207
+ }
208
+ } else {
209
+ lines.push(" ✅ NO RESOURCE LEAKS");
210
+ }
211
+ lines.push("═".repeat(60));
212
+
213
+ return lines.join("\n");
214
+ };
215
+
216
+ export const formatEventTimeline = (events: SessionEvent[]): string => {
217
+ const lines: string[] = [];
218
+
219
+ lines.push("EVENT TIMELINE");
220
+ lines.push("─".repeat(80));
221
+
222
+ const baseTime = events[0]?.timestamp ?? 0;
223
+
224
+ for (const event of events) {
225
+ const elapsed = ((event.timestamp - baseTime) / 1000).toFixed(2).padStart(8);
226
+ const type = event.type.padEnd(12);
227
+ const memory = (event.snapshot.memory.processRss / 1024 / 1024).toFixed(1).padStart(6);
228
+
229
+ lines.push(` ${elapsed}s │ ${type} │ ${memory}MB │ ${event.label}`);
230
+ }
231
+
232
+ lines.push("─".repeat(80));
233
+
234
+ return lines.join("\n");
235
+ };
236
+
237
+ export const generateHTMLReport = (report: SessionReport): string => {
238
+ const { summary, events } = report;
239
+ const baseTime = events[0]?.timestamp ?? 0;
240
+
241
+ const eventRows = events.map((e) => {
242
+ const elapsed = ((e.timestamp - baseTime) / 1000).toFixed(2);
243
+ const memory = (e.snapshot.memory.processRss / 1024 / 1024).toFixed(1);
244
+ return `<tr>
245
+ <td>${elapsed}s</td>
246
+ <td><span class="event-type event-${e.type}">${e.type}</span></td>
247
+ <td>${memory} MB</td>
248
+ <td>${e.label}</td>
249
+ </tr>`;
250
+ }).join("\n");
251
+
252
+ return `<!DOCTYPE html>
253
+ <html lang="en">
254
+ <head>
255
+ <meta charset="UTF-8">
256
+ <title>Session Report - ${report.sessionId}</title>
257
+ <style>
258
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; background: #f5f5f5; }
259
+ .container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
260
+ h1 { color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }
261
+ h2 { color: #555; margin-top: 30px; }
262
+ .summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 20px 0; }
263
+ .summary-card { background: #f8f9fa; padding: 20px; border-radius: 8px; text-align: center; }
264
+ .summary-card .value { font-size: 2em; font-weight: bold; color: #007bff; }
265
+ .summary-card .label { color: #666; font-size: 0.9em; }
266
+ .status { padding: 15px; border-radius: 8px; margin: 20px 0; }
267
+ .status.success { background: #d4edda; color: #155724; }
268
+ .status.error { background: #f8d7da; color: #721c24; }
269
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; }
270
+ th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
271
+ th { background: #f8f9fa; }
272
+ .event-type { padding: 4px 8px; border-radius: 4px; font-size: 0.85em; }
273
+ .event-baseline { background: #007bff; color: white; }
274
+ .event-interval { background: #6c757d; color: white; }
275
+ .event-pageload { background: #28a745; color: white; }
276
+ .event-click { background: #ffc107; color: #333; }
277
+ .event-popup_open, .event-popup_close { background: #17a2b8; color: white; }
278
+ .event-auth_start, .event-auth_complete { background: #6f42c1; color: white; }
279
+ .event-custom { background: #fd7e14; color: white; }
280
+ .event-error { background: #dc3545; color: white; }
281
+ </style>
282
+ </head>
283
+ <body>
284
+ <div class="container">
285
+ <h1>Session Report</h1>
286
+ <p><strong>Session ID:</strong> ${report.sessionId}</p>
287
+ <p><strong>Duration:</strong> ${(summary.duration / 1000).toFixed(1)} seconds</p>
288
+ <p><strong>Platform:</strong> ${report.platform} (Node ${report.nodeVersion})</p>
289
+
290
+ <div class="summary-grid">
291
+ <div class="summary-card">
292
+ <div class="value">${summary.eventCount}</div>
293
+ <div class="label">Events</div>
294
+ </div>
295
+ <div class="summary-card">
296
+ <div class="value">${summary.peakMemoryMb.toFixed(1)} MB</div>
297
+ <div class="label">Peak Memory</div>
298
+ </div>
299
+ <div class="summary-card">
300
+ <div class="value">${summary.processesSpawned}</div>
301
+ <div class="label">Processes</div>
302
+ </div>
303
+ <div class="summary-card">
304
+ <div class="value">${summary.portsUsed.length}</div>
305
+ <div class="label">Ports Monitored</div>
306
+ </div>
307
+ </div>
308
+
309
+ <div class="status ${summary.hasLeaks ? 'error' : 'success'}">
310
+ ${summary.hasLeaks
311
+ ? `❌ Resource leaks detected: ${summary.orphanedProcesses} orphaned processes, ${summary.portsLeaked} ports leaked`
312
+ : '✅ No resource leaks detected'
313
+ }
314
+ </div>
315
+
316
+ <h2>Event Timeline</h2>
317
+ <table>
318
+ <thead>
319
+ <tr>
320
+ <th>Time</th>
321
+ <th>Event</th>
322
+ <th>Memory</th>
323
+ <th>Label</th>
324
+ </tr>
325
+ </thead>
326
+ <tbody>
327
+ ${eventRows}
328
+ </tbody>
329
+ </table>
330
+ </div>
331
+ </body>
332
+ </html>`;
333
+ };
334
+
335
+ export const exportHTMLReport = (
336
+ report: SessionReport,
337
+ filepath: string
338
+ ): Effect.Effect<void, ExportFailed> =>
339
+ Effect.gen(function* () {
340
+ yield* Effect.logInfo(`Exporting HTML report to ${filepath}`);
341
+
342
+ const html = generateHTMLReport(report);
343
+
344
+ yield* Effect.tryPromise({
345
+ try: () => writeFile(filepath, html),
346
+ catch: (e) => new ExportFailed({
347
+ path: filepath,
348
+ reason: String(e),
349
+ }),
350
+ });
351
+
352
+ yield* Effect.logInfo("HTML report exported");
353
+ });
@@ -0,0 +1,267 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Effect } from "effect";
5
+ import {
6
+ createSnapshotWithPlatform,
7
+ runSilent,
8
+ } from "../resource-monitor";
9
+ import { ServerNotReady, ServerStartFailed } from "./errors";
10
+ import type { ServerHandle, ServerOrchestrator } from "./types";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const CLI_DIR = resolve(__dirname, "../../..");
14
+
15
+ const sleep = (ms: number): Promise<void> =>
16
+ new Promise((resolve) => setTimeout(resolve, ms));
17
+
18
+ interface SpawnOptions {
19
+ port?: number;
20
+ account?: string;
21
+ domain?: string;
22
+ interactive?: boolean;
23
+ }
24
+
25
+ const createServerHandle = (
26
+ proc: ReturnType<typeof spawn>,
27
+ name: string,
28
+ port: number
29
+ ): ServerHandle => {
30
+ proc.stdout?.on("data", () => {});
31
+ proc.stderr?.on("data", () => {});
32
+
33
+ let exitHandled = false;
34
+ let exitCode: number | null = null;
35
+ const exitPromise = new Promise<number | null>((resolve) => {
36
+ (proc as unknown as NodeJS.EventEmitter).on("exit", (code: number | null) => {
37
+ exitHandled = true;
38
+ exitCode = code;
39
+ resolve(code);
40
+ });
41
+ });
42
+
43
+ return {
44
+ pid: proc.pid!,
45
+ port,
46
+ name,
47
+ kill: async () => {
48
+ proc.kill("SIGTERM");
49
+ const killPromise = new Promise<void>((res) => {
50
+ const timeout = setTimeout(() => {
51
+ proc.kill("SIGKILL");
52
+ res();
53
+ }, 5000);
54
+ if (exitHandled) {
55
+ clearTimeout(timeout);
56
+ res();
57
+ } else {
58
+ exitPromise.then(() => {
59
+ clearTimeout(timeout);
60
+ res();
61
+ });
62
+ }
63
+ });
64
+ await killPromise;
65
+ },
66
+ waitForExit: (timeoutMs = 10000): Promise<number | null> =>
67
+ new Promise((res) => {
68
+ const timeout = setTimeout(() => res(null), timeoutMs);
69
+ if (exitHandled) {
70
+ clearTimeout(timeout);
71
+ res(exitCode);
72
+ } else {
73
+ exitPromise.then((code) => {
74
+ clearTimeout(timeout);
75
+ res(code);
76
+ });
77
+ }
78
+ }),
79
+ };
80
+ };
81
+
82
+ const spawnBosStart = (options: SpawnOptions = {}): ServerHandle => {
83
+ const args = [
84
+ "run",
85
+ "src/cli.ts",
86
+ "start",
87
+ "--account", options.account || "every.near",
88
+ "--domain", options.domain || "everything.dev",
89
+ ];
90
+
91
+ if (!options.interactive) {
92
+ args.push("--no-interactive");
93
+ }
94
+
95
+ if (options.port) {
96
+ args.push("--port", String(options.port));
97
+ }
98
+
99
+ const proc = spawn("bun", args, {
100
+ cwd: CLI_DIR,
101
+ stdio: ["ignore", "pipe", "pipe"],
102
+ detached: false,
103
+ env: { ...process.env, NODE_ENV: "production" },
104
+ });
105
+
106
+ return createServerHandle(proc, "bos-start", options.port || 3000);
107
+ };
108
+
109
+ const spawnBosDev = (options: SpawnOptions = {}): ServerHandle => {
110
+ const args = ["run", "src/cli.ts", "dev"];
111
+
112
+ if (!options.interactive) {
113
+ args.push("--no-interactive");
114
+ }
115
+
116
+ if (options.port) {
117
+ args.push("--port", String(options.port));
118
+ }
119
+
120
+ const proc = spawn("bun", args, {
121
+ cwd: CLI_DIR,
122
+ stdio: ["ignore", "pipe", "pipe"],
123
+ detached: false,
124
+ env: { ...process.env, NODE_ENV: "development" },
125
+ });
126
+
127
+ return createServerHandle(proc, "bos-dev", options.port || 3000);
128
+ };
129
+
130
+ const waitForPortBound = async (
131
+ port: number,
132
+ timeoutMs = 60000
133
+ ): Promise<boolean> => {
134
+ const start = Date.now();
135
+
136
+ while (Date.now() - start < timeoutMs) {
137
+ try {
138
+ const snapshot = await runSilent(
139
+ createSnapshotWithPlatform({ ports: [port] })
140
+ );
141
+ if (snapshot.ports[port]?.state === "LISTEN") {
142
+ return true;
143
+ }
144
+ } catch {
145
+ // ignore errors during polling
146
+ }
147
+ await sleep(500);
148
+ }
149
+
150
+ return false;
151
+ };
152
+
153
+ const waitForPortFree = async (
154
+ port: number,
155
+ timeoutMs = 15000
156
+ ): Promise<boolean> => {
157
+ const start = Date.now();
158
+
159
+ while (Date.now() - start < timeoutMs) {
160
+ try {
161
+ const snapshot = await runSilent(
162
+ createSnapshotWithPlatform({ ports: [port] })
163
+ );
164
+ if (snapshot.ports[port]?.state === "FREE") {
165
+ return true;
166
+ }
167
+ } catch {
168
+ // ignore errors during polling
169
+ }
170
+ await sleep(200);
171
+ }
172
+
173
+ return false;
174
+ };
175
+
176
+ export const startServers = (
177
+ mode: "start" | "dev" = "start",
178
+ options: SpawnOptions = {}
179
+ ): Effect.Effect<ServerOrchestrator, ServerStartFailed | ServerNotReady> =>
180
+ Effect.gen(function* () {
181
+ const port = options.port || 3000;
182
+
183
+ yield* Effect.logInfo(`Starting BOS in ${mode} mode on port ${port}`);
184
+
185
+ const handle = mode === "dev"
186
+ ? spawnBosDev(options)
187
+ : spawnBosStart(options);
188
+
189
+ const ready = yield* Effect.tryPromise({
190
+ try: () => waitForPortBound(port, 90000),
191
+ catch: (e) => new ServerStartFailed({
192
+ server: handle.name,
193
+ port,
194
+ reason: String(e),
195
+ }),
196
+ });
197
+
198
+ if (!ready) {
199
+ yield* Effect.promise(() => handle.kill());
200
+ return yield* Effect.fail(
201
+ new ServerNotReady({
202
+ servers: [handle.name],
203
+ timeoutMs: 90000,
204
+ })
205
+ );
206
+ }
207
+
208
+ yield* Effect.logInfo(`Server ready on port ${port}`);
209
+
210
+ const orchestrator: ServerOrchestrator = {
211
+ handles: [handle],
212
+ ports: [port],
213
+ shutdown: async () => {
214
+ console.log("Shutting down servers");
215
+ await handle.kill();
216
+ await waitForPortFree(port, 15000);
217
+ console.log("Servers stopped");
218
+ },
219
+ waitForReady: async () => {
220
+ return waitForPortBound(port, 30000);
221
+ },
222
+ };
223
+
224
+ return orchestrator;
225
+ });
226
+
227
+ export const shutdownServers = (
228
+ orchestrator: ServerOrchestrator
229
+ ): Effect.Effect<void> =>
230
+ Effect.gen(function* () {
231
+ yield* Effect.logInfo(`Shutting down ${orchestrator.handles.length} server(s)`);
232
+
233
+ for (const handle of orchestrator.handles) {
234
+ yield* Effect.logDebug(`Killing ${handle.name} (PID ${handle.pid})`);
235
+ yield* Effect.promise(() => handle.kill());
236
+ }
237
+
238
+ for (const port of orchestrator.ports) {
239
+ yield* Effect.logDebug(`Waiting for port ${port} to be free`);
240
+ const freed = yield* Effect.promise(() => waitForPortFree(port, 15000));
241
+ if (!freed) {
242
+ yield* Effect.logWarning(`Port ${port} still bound after shutdown`);
243
+ }
244
+ }
245
+
246
+ yield* Effect.logInfo("All servers stopped");
247
+ });
248
+
249
+ export const checkPortsAvailable = (
250
+ ports: number[]
251
+ ): Effect.Effect<boolean> =>
252
+ Effect.gen(function* () {
253
+ const snapshot = yield* Effect.promise(() =>
254
+ runSilent(createSnapshotWithPlatform({ ports }))
255
+ );
256
+
257
+ for (const port of ports) {
258
+ if (snapshot.ports[port]?.state !== "FREE") {
259
+ yield* Effect.logWarning(`Port ${port} is already in use`);
260
+ return false;
261
+ }
262
+ }
263
+
264
+ return true;
265
+ });
266
+
267
+ export { waitForPortBound, waitForPortFree };