everything-dev 0.1.3 → 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,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 };
@@ -0,0 +1,115 @@
1
+ import type { Snapshot } from "../resource-monitor";
2
+
3
+ export interface SessionConfig {
4
+ ports: number[];
5
+ snapshotIntervalMs: number;
6
+ headless: boolean;
7
+ baseUrl: string;
8
+ timeout: number;
9
+ outputPath?: string;
10
+ devMode?: "local" | "remote";
11
+ }
12
+
13
+ export interface BrowserMetrics {
14
+ jsHeapUsedSize: number;
15
+ jsHeapTotalSize: number;
16
+ documents: number;
17
+ frames: number;
18
+ jsEventListeners: number;
19
+ nodes: number;
20
+ layoutCount: number;
21
+ recalcStyleCount: number;
22
+ scriptDuration: number;
23
+ taskDuration: number;
24
+ }
25
+
26
+ export type SessionEventType =
27
+ | "baseline"
28
+ | "interval"
29
+ | "pageload"
30
+ | "navigation"
31
+ | "click"
32
+ | "popup_open"
33
+ | "popup_close"
34
+ | "auth_start"
35
+ | "auth_complete"
36
+ | "auth_failed"
37
+ | "error"
38
+ | "custom";
39
+
40
+ export interface SessionEvent {
41
+ id: string;
42
+ timestamp: number;
43
+ type: SessionEventType;
44
+ label: string;
45
+ snapshot: Snapshot;
46
+ browserMetrics?: BrowserMetrics;
47
+ url?: string;
48
+ error?: string;
49
+ metadata?: Record<string, unknown>;
50
+ }
51
+
52
+ export interface SessionSummary {
53
+ totalMemoryDeltaMb: number;
54
+ peakMemoryMb: number;
55
+ averageMemoryMb: number;
56
+ processesSpawned: number;
57
+ processesKilled: number;
58
+ orphanedProcesses: number;
59
+ portsUsed: number[];
60
+ portsLeaked: number;
61
+ hasLeaks: boolean;
62
+ eventCount: number;
63
+ duration: number;
64
+ browserMetricsSummary?: {
65
+ peakJsHeapMb: number;
66
+ averageJsHeapMb: number;
67
+ totalLayoutCount: number;
68
+ totalScriptDuration: number;
69
+ };
70
+ }
71
+
72
+ export interface SessionReport {
73
+ sessionId: string;
74
+ config: SessionConfig;
75
+ startTime: number;
76
+ endTime: number;
77
+ events: SessionEvent[];
78
+ summary: SessionSummary;
79
+ platform: NodeJS.Platform;
80
+ nodeVersion: string;
81
+ }
82
+
83
+ export interface ServerHandle {
84
+ pid: number;
85
+ port: number;
86
+ name: string;
87
+ kill: () => Promise<void>;
88
+ waitForExit: (timeoutMs?: number) => Promise<number | null>;
89
+ }
90
+
91
+ export interface ServerOrchestrator {
92
+ handles: ServerHandle[];
93
+ ports: number[];
94
+ shutdown: () => Promise<void>;
95
+ waitForReady: () => Promise<boolean>;
96
+ }
97
+
98
+ export type SessionFlow = (context: FlowContext) => Promise<void>;
99
+
100
+ export interface FlowContext {
101
+ page: unknown;
102
+ context: unknown;
103
+ recordEvent: (type: SessionEventType, label: string, metadata?: Record<string, unknown>) => Promise<void>;
104
+ headless: boolean;
105
+ baseUrl: string;
106
+ }
107
+
108
+ export const DEFAULT_SESSION_CONFIG: SessionConfig = {
109
+ ports: [3000, 3002, 3014],
110
+ snapshotIntervalMs: 2000,
111
+ headless: true,
112
+ baseUrl: "http://localhost:3000",
113
+ timeout: 120000,
114
+ devMode: "remote",
115
+ };
package/src/plugin.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createPlugin } from "every-plugin";
2
2
  import { Effect } from "every-plugin/effect";
3
3
  import { z } from "every-plugin/zod";
4
- import { Graph, calculateRequiredDeposit } from "near-social-js";
4
+ import { calculateRequiredDeposit, Graph } from "near-social-js";
5
5
 
6
- import { createProcessRegistry } from "./lib/process-registry";
6
+ import { runMonitorCli } from "./components/monitor-view";
7
7
  import {
8
8
  type AppConfig,
9
9
  type BosConfig as BosConfigType,
@@ -18,7 +18,7 @@ import {
18
18
  loadConfig,
19
19
  type RemoteConfig,
20
20
  resolvePackageModes,
21
- type SourceMode,
21
+ type SourceMode,
22
22
  setConfig
23
23
  } from "./config";
24
24
  import { bosContract } from "./contract";
@@ -37,6 +37,19 @@ import {
37
37
  verifyNovaCredentials
38
38
  } from "./lib/nova";
39
39
  import { type AppOrchestrator, startApp } from "./lib/orchestrator";
40
+ import { createProcessRegistry } from "./lib/process-registry";
41
+ import {
42
+ createSnapshotWithPlatform,
43
+ formatSnapshotSummary,
44
+ runWithInfo
45
+ } from "./lib/resource-monitor";
46
+ import {
47
+ formatReportSummary,
48
+ navigateTo,
49
+ runLoginFlow,
50
+ runNavigationFlow,
51
+ SessionRecorder,
52
+ } from "./lib/session-recorder";
40
53
  import { syncFiles } from "./lib/sync";
41
54
  import { run } from "./utils/run";
42
55
  import { colors, icons } from "./utils/theme";
@@ -76,8 +89,8 @@ function getSocialContract(network: "mainnet" | "testnet"): string {
76
89
  }
77
90
 
78
91
  function getSocialExplorerUrl(network: "mainnet" | "testnet", path: string): string {
79
- const baseUrl = network === "testnet"
80
- ? "https://test.near.social"
92
+ const baseUrl = network === "testnet"
93
+ ? "https://test.near.social"
81
94
  : "https://near.social";
82
95
  return `${baseUrl}/${path}`;
83
96
  }
@@ -359,7 +372,7 @@ export default createPlugin({
359
372
 
360
373
  serve: builder.serve.handler(async ({ input }) => {
361
374
  const port = input.port;
362
-
375
+
363
376
  return {
364
377
  status: "serving" as const,
365
378
  url: `http://localhost:${port}`,
@@ -481,7 +494,7 @@ export default createPlugin({
481
494
  };
482
495
  const argsBase64 = Buffer.from(JSON.stringify(socialArgs)).toString("base64");
483
496
 
484
- const graph = new Graph({
497
+ const graph = new Graph({
485
498
  network,
486
499
  contractId: socialContract,
487
500
  });
@@ -1361,11 +1374,11 @@ export default createPlugin({
1361
1374
 
1362
1375
  const mergeAppConfig = (localApp: Record<string, unknown>, remoteApp: Record<string, unknown>): Record<string, unknown> => {
1363
1376
  const merged: Record<string, unknown> = {};
1364
-
1377
+
1365
1378
  for (const key of Object.keys(remoteApp)) {
1366
1379
  const local = localApp[key] as Record<string, unknown> | undefined;
1367
1380
  const remote = remoteApp[key] as Record<string, unknown>;
1368
-
1381
+
1369
1382
  if (!local) {
1370
1383
  merged[key] = remote;
1371
1384
  continue;
@@ -1384,7 +1397,7 @@ export default createPlugin({
1384
1397
  },
1385
1398
  };
1386
1399
  }
1387
-
1400
+
1388
1401
  return merged;
1389
1402
  };
1390
1403
 
@@ -1608,7 +1621,7 @@ export default createPlugin({
1608
1621
  const port = input.port || (input.target === "development" ? 4000 : 3000);
1609
1622
 
1610
1623
  const args = ["run"];
1611
-
1624
+
1612
1625
  if (input.detach) {
1613
1626
  args.push("-d");
1614
1627
  }
@@ -1630,8 +1643,8 @@ export default createPlugin({
1630
1643
  args.push("-e", `BOS_ACCOUNT=${bosConfig.account}`);
1631
1644
  const gateway = bosConfig.gateway as { production?: string } | string | undefined;
1632
1645
  if (gateway) {
1633
- const domain = typeof gateway === "string"
1634
- ? gateway
1646
+ const domain = typeof gateway === "string"
1647
+ ? gateway
1635
1648
  : gateway.production?.replace(/^https?:\/\//, "") || "";
1636
1649
  if (domain) {
1637
1650
  args.push("-e", `GATEWAY_DOMAIN=${domain}`);
@@ -1688,14 +1701,14 @@ export default createPlugin({
1688
1701
  stopped.push(input.containerId!);
1689
1702
  } else if (input.all) {
1690
1703
  const imageName = bosConfig?.account?.replace(/\./g, "-") || "bos-app";
1691
-
1704
+
1692
1705
  const psResult = yield* Effect.tryPromise({
1693
1706
  try: () => execa("docker", ["ps", "-q", "--filter", `ancestor=${imageName}`]),
1694
1707
  catch: () => new Error("Failed to list containers"),
1695
1708
  });
1696
1709
 
1697
1710
  const containerIds = psResult.stdout.trim().split("\n").filter(Boolean);
1698
-
1711
+
1699
1712
  for (const id of containerIds) {
1700
1713
  yield* Effect.tryPromise({
1701
1714
  try: () => execa("docker", ["stop", id]),
@@ -1721,6 +1734,132 @@ export default createPlugin({
1721
1734
  };
1722
1735
  }
1723
1736
  }),
1737
+
1738
+ monitor: builder.monitor.handler(async ({ input }) => {
1739
+ try {
1740
+ if (input.json) {
1741
+ const snapshot = await runWithInfo(
1742
+ createSnapshotWithPlatform(input.ports ? { ports: input.ports } : undefined)
1743
+ );
1744
+ return {
1745
+ status: "snapshot" as const,
1746
+ snapshot: snapshot as any,
1747
+ };
1748
+ }
1749
+
1750
+ if (input.watch) {
1751
+ runMonitorCli({ ports: input.ports, json: false });
1752
+ return {
1753
+ status: "watching" as const,
1754
+ };
1755
+ }
1756
+
1757
+ const snapshot = await runWithInfo(
1758
+ createSnapshotWithPlatform(input.ports ? { ports: input.ports } : undefined)
1759
+ );
1760
+ console.log(formatSnapshotSummary(snapshot));
1761
+
1762
+ return {
1763
+ status: "snapshot" as const,
1764
+ snapshot: snapshot as any,
1765
+ };
1766
+ } catch (error) {
1767
+ return {
1768
+ status: "error" as const,
1769
+ error: error instanceof Error ? error.message : "Unknown error",
1770
+ };
1771
+ }
1772
+ }),
1773
+
1774
+ session: builder.session.handler(async ({ input }) => {
1775
+ const sessionEffect = Effect.gen(function* () {
1776
+ const recorder = yield* SessionRecorder.create({
1777
+ ports: [3000],
1778
+ snapshotIntervalMs: input.snapshotInterval,
1779
+ headless: input.headless,
1780
+ baseUrl: "http://localhost:3000",
1781
+ timeout: input.timeout,
1782
+ });
1783
+
1784
+ try {
1785
+ yield* recorder.startServers("start");
1786
+
1787
+ yield* recorder.startRecording();
1788
+
1789
+ const browser = yield* recorder.launchBrowser();
1790
+
1791
+ if (input.flow === "login") {
1792
+ yield* runLoginFlow(browser, {
1793
+ recordEvent: (type, label, metadata) =>
1794
+ recorder.recordEvent(type, label, metadata).pipe(
1795
+ Effect.asVoid,
1796
+ Effect.catchAll(() => Effect.void)
1797
+ ),
1798
+ }, {
1799
+ baseUrl: "http://localhost:3000",
1800
+ headless: input.headless,
1801
+ stubWallet: input.headless,
1802
+ timeout: 30000,
1803
+ });
1804
+ } else if (input.flow === "navigation" && input.routes) {
1805
+ yield* runNavigationFlow(
1806
+ browser,
1807
+ {
1808
+ recordEvent: (type, label, metadata) =>
1809
+ recorder.recordEvent(type, label, metadata).pipe(
1810
+ Effect.asVoid,
1811
+ Effect.catchAll(() => Effect.void)
1812
+ ),
1813
+ },
1814
+ input.routes,
1815
+ "http://localhost:3000"
1816
+ );
1817
+ } else {
1818
+ yield* navigateTo(browser.page, "http://localhost:3000");
1819
+ yield* Effect.sleep("5 seconds");
1820
+ }
1821
+
1822
+ yield* recorder.cleanup();
1823
+
1824
+ const report = yield* recorder.stopRecording();
1825
+
1826
+ yield* recorder.exportReport(input.output, input.format);
1827
+
1828
+ console.log(formatReportSummary(report));
1829
+
1830
+ return {
1831
+ status: report.summary.hasLeaks ? "leaks_detected" as const : "completed" as const,
1832
+ sessionId: recorder.getSessionId(),
1833
+ reportPath: input.output,
1834
+ summary: {
1835
+ totalMemoryDeltaMb: report.summary.totalMemoryDeltaMb,
1836
+ peakMemoryMb: report.summary.peakMemoryMb,
1837
+ averageMemoryMb: report.summary.averageMemoryMb,
1838
+ processesSpawned: report.summary.processesSpawned,
1839
+ processesKilled: report.summary.processesKilled,
1840
+ orphanedProcesses: report.summary.orphanedProcesses,
1841
+ portsUsed: report.summary.portsUsed,
1842
+ portsLeaked: report.summary.portsLeaked,
1843
+ hasLeaks: report.summary.hasLeaks,
1844
+ eventCount: report.summary.eventCount,
1845
+ duration: report.summary.duration,
1846
+ },
1847
+ };
1848
+ } catch (error) {
1849
+ yield* recorder.cleanup();
1850
+ throw error;
1851
+ }
1852
+ });
1853
+
1854
+ try {
1855
+ return await Effect.runPromise(sessionEffect);
1856
+ } catch (error) {
1857
+ return {
1858
+ status: "error" as const,
1859
+ error: error instanceof Error ? error.message : "Unknown error",
1860
+ };
1861
+ }
1862
+ }),
1724
1863
  }),
1725
1864
  });
1726
1865