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,204 @@
1
+ import { Effect } from "effect";
2
+ import { getConfigPath, getPortsFromConfig, loadConfig } from "../../config";
3
+ import { PlatformService, withPlatform } from "./platform";
4
+ import type { MonitorConfig, ProcessInfo, Snapshot } from "./types";
5
+
6
+ export const getPortsToMonitor = (
7
+ config?: MonitorConfig
8
+ ): Effect.Effect<number[]> =>
9
+ Effect.gen(function* () {
10
+ if (config?.ports && config.ports.length > 0) {
11
+ yield* Effect.logDebug(
12
+ `Using configured ports: ${config.ports.join(", ")}`
13
+ );
14
+ return config.ports;
15
+ }
16
+
17
+ return yield* Effect.try({
18
+ try: () => {
19
+ loadConfig(config?.configPath);
20
+ const portConfig = getPortsFromConfig();
21
+ const ports = [portConfig.host, portConfig.ui, portConfig.api].filter(
22
+ (p) => p > 0
23
+ );
24
+ return ports;
25
+ },
26
+ catch: () => new Error("Config not found"),
27
+ }).pipe(Effect.catchAll(() => Effect.succeed([3000, 3002, 3014])));
28
+ });
29
+
30
+ export const getConfigPathSafe = (
31
+ config?: MonitorConfig
32
+ ): Effect.Effect<string | null> =>
33
+ Effect.try({
34
+ try: () => {
35
+ loadConfig(config?.configPath);
36
+ return getConfigPath();
37
+ },
38
+ catch: () => new Error("Config not found"),
39
+ }).pipe(Effect.catchAll(() => Effect.succeed(null)));
40
+
41
+ export const createSnapshot = (
42
+ config?: MonitorConfig
43
+ ): Effect.Effect<Snapshot, never, PlatformService> =>
44
+ Effect.gen(function* () {
45
+ yield* Effect.logInfo("Creating system snapshot");
46
+
47
+ const platform = yield* PlatformService;
48
+ const ports = yield* getPortsToMonitor(config);
49
+ const configPath = yield* getConfigPathSafe(config);
50
+
51
+ yield* Effect.logDebug(`Monitoring ports: ${ports.join(", ")}`);
52
+
53
+ const portInfo = yield* platform.getPortInfo(ports);
54
+ const boundPorts = Object.values(portInfo).filter(
55
+ (p) => p.state !== "FREE"
56
+ );
57
+
58
+ yield* Effect.logInfo(`Found ${boundPorts.length} bound ports`);
59
+
60
+ const rootPids = boundPorts
61
+ .map((p) => p.pid)
62
+ .filter((pid): pid is number => pid !== null);
63
+
64
+ const processes =
65
+ rootPids.length > 0 ? yield* platform.getProcessTree(rootPids) : [];
66
+
67
+ yield* Effect.logInfo(`Tracked ${processes.length} processes in tree`);
68
+
69
+ const memory = yield* platform.getMemoryInfo();
70
+
71
+ const totalRss = processes.reduce((sum, p) => sum + p.rss, 0);
72
+ memory.processRss = totalRss;
73
+
74
+ yield* Effect.logDebug(
75
+ `Total process RSS: ${(totalRss / 1024 / 1024).toFixed(1)}MB`
76
+ );
77
+
78
+ const snapshot: Snapshot = {
79
+ timestamp: Date.now(),
80
+ configPath,
81
+ ports: portInfo,
82
+ processes,
83
+ memory,
84
+ platform: process.platform,
85
+ };
86
+
87
+ yield* Effect.logInfo(
88
+ `Snapshot created at ${new Date(snapshot.timestamp).toISOString()}`
89
+ );
90
+
91
+ return snapshot;
92
+ });
93
+
94
+ export const createSnapshotWithPlatform = (
95
+ config?: MonitorConfig
96
+ ): Effect.Effect<Snapshot> => withPlatform(createSnapshot(config));
97
+
98
+ export const findProcessesByPattern = (
99
+ patterns: string[]
100
+ ): Effect.Effect<ProcessInfo[], never, PlatformService> =>
101
+ Effect.gen(function* () {
102
+ yield* Effect.logDebug(
103
+ `Finding processes matching: ${patterns.join(", ")}`
104
+ );
105
+
106
+ const platform = yield* PlatformService;
107
+ const allProcesses = yield* platform.getAllProcesses();
108
+
109
+ const matched = allProcesses.filter((proc) =>
110
+ patterns.some((pattern) =>
111
+ proc.command.toLowerCase().includes(pattern.toLowerCase())
112
+ )
113
+ );
114
+
115
+ yield* Effect.logInfo(
116
+ `Found ${matched.length} processes matching patterns`
117
+ );
118
+
119
+ return matched;
120
+ });
121
+
122
+ export const findBosProcesses = (): Effect.Effect<
123
+ ProcessInfo[],
124
+ never,
125
+ PlatformService
126
+ > => {
127
+ const patterns = ["bun", "rspack", "rsbuild", "esbuild", "webpack", "node"];
128
+ return findProcessesByPattern(patterns);
129
+ };
130
+
131
+ export const isProcessAliveSync = (pid: number): boolean => {
132
+ try {
133
+ process.kill(pid, 0);
134
+ return true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ };
139
+
140
+ export const isProcessAlive = (pid: number): Effect.Effect<boolean> =>
141
+ Effect.sync(() => isProcessAliveSync(pid));
142
+
143
+ export const waitForProcessDeath = (
144
+ pid: number,
145
+ timeoutMs = 5000
146
+ ): Effect.Effect<boolean> =>
147
+ Effect.gen(function* () {
148
+ yield* Effect.logDebug(
149
+ `Waiting for PID ${pid} to die (timeout: ${timeoutMs}ms)`
150
+ );
151
+ const start = Date.now();
152
+
153
+ while (Date.now() - start < timeoutMs) {
154
+ const alive = yield* isProcessAlive(pid);
155
+ if (!alive) {
156
+ yield* Effect.logDebug(`PID ${pid} is dead`);
157
+ return true;
158
+ }
159
+ yield* Effect.sleep("100 millis");
160
+ }
161
+
162
+ const finalAlive = yield* isProcessAlive(pid);
163
+ if (finalAlive) {
164
+ yield* Effect.logWarning(`PID ${pid} still alive after ${timeoutMs}ms`);
165
+ }
166
+ return !finalAlive;
167
+ });
168
+
169
+ export const waitForPortFree = (
170
+ port: number,
171
+ timeoutMs = 5000
172
+ ): Effect.Effect<boolean, never, PlatformService> =>
173
+ Effect.gen(function* () {
174
+ yield* Effect.logDebug(
175
+ `Waiting for port :${port} to be free (timeout: ${timeoutMs}ms)`
176
+ );
177
+ const platform = yield* PlatformService;
178
+ const start = Date.now();
179
+
180
+ while (Date.now() - start < timeoutMs) {
181
+ const portInfo = yield* platform.getPortInfo([port]);
182
+ if (portInfo[port].state === "FREE") {
183
+ yield* Effect.logDebug(`Port :${port} is now free`);
184
+ return true;
185
+ }
186
+ yield* Effect.sleep("100 millis");
187
+ }
188
+
189
+ const finalPortInfo = yield* platform.getPortInfo([port]);
190
+ const isFree = finalPortInfo[port].state === "FREE";
191
+
192
+ if (!isFree) {
193
+ yield* Effect.logWarning(
194
+ `Port :${port} still bound after ${timeoutMs}ms`
195
+ );
196
+ }
197
+
198
+ return isFree;
199
+ });
200
+
201
+ export const waitForPortFreeWithPlatform = (
202
+ port: number,
203
+ timeoutMs = 5000
204
+ ): Effect.Effect<boolean> => withPlatform(waitForPortFree(port, timeoutMs));
@@ -0,0 +1,74 @@
1
+ import { Context, Effect } from "effect";
2
+
3
+ export interface PortInfo {
4
+ port: number;
5
+ pid: number | null;
6
+ command: string | null;
7
+ state: "LISTEN" | "ESTABLISHED" | "TIME_WAIT" | "FREE";
8
+ name?: string;
9
+ }
10
+
11
+ export interface ProcessInfo {
12
+ pid: number;
13
+ ppid: number;
14
+ command: string;
15
+ args: string[];
16
+ rss: number;
17
+ children: number[];
18
+ startTime?: number;
19
+ }
20
+
21
+ export interface MemoryInfo {
22
+ total: number;
23
+ used: number;
24
+ free: number;
25
+ processRss: number;
26
+ }
27
+
28
+ export interface Snapshot {
29
+ timestamp: number;
30
+ configPath: string | null;
31
+ ports: Record<number, PortInfo>;
32
+ processes: ProcessInfo[];
33
+ memory: MemoryInfo;
34
+ platform: NodeJS.Platform;
35
+ }
36
+
37
+ export interface SnapshotDiff {
38
+ from: Snapshot;
39
+ to: Snapshot;
40
+ orphanedProcesses: ProcessInfo[];
41
+ stillBoundPorts: PortInfo[];
42
+ freedPorts: number[];
43
+ memoryDeltaBytes: number;
44
+ newProcesses: ProcessInfo[];
45
+ killedProcesses: ProcessInfo[];
46
+ }
47
+
48
+ export interface MonitorConfig {
49
+ ports?: number[];
50
+ processPatterns?: string[];
51
+ refreshInterval?: number;
52
+ configPath?: string;
53
+ }
54
+
55
+ export interface PlatformOperations {
56
+ readonly getPortInfo: (
57
+ ports: number[]
58
+ ) => Effect.Effect<Record<number, PortInfo>, never>;
59
+
60
+ readonly getProcessTree: (
61
+ rootPids: number[]
62
+ ) => Effect.Effect<ProcessInfo[], never>;
63
+
64
+ readonly getMemoryInfo: () => Effect.Effect<MemoryInfo, never>;
65
+
66
+ readonly getAllProcesses: () => Effect.Effect<ProcessInfo[], never>;
67
+
68
+ readonly findChildProcesses: (pid: number) => Effect.Effect<number[], never>;
69
+ }
70
+
71
+ export class PlatformService extends Context.Tag("PlatformService")<
72
+ PlatformService,
73
+ PlatformOperations
74
+ >() {}
@@ -0,0 +1,102 @@
1
+ import { Data } from "effect";
2
+
3
+ export class SessionTimeout extends Data.TaggedError("SessionTimeout")<{
4
+ readonly timeoutMs: number;
5
+ readonly elapsedMs: number;
6
+ }> {
7
+ get message() {
8
+ return `Session timed out after ${this.elapsedMs}ms (limit: ${this.timeoutMs}ms)`;
9
+ }
10
+ }
11
+
12
+ export class BrowserLaunchFailed extends Data.TaggedError("BrowserLaunchFailed")<{
13
+ readonly reason: string;
14
+ readonly headless: boolean;
15
+ }> {
16
+ get message() {
17
+ return `Failed to launch browser (headless: ${this.headless}): ${this.reason}`;
18
+ }
19
+ }
20
+
21
+ export class ServerStartFailed extends Data.TaggedError("ServerStartFailed")<{
22
+ readonly server: string;
23
+ readonly port: number;
24
+ readonly reason: string;
25
+ }> {
26
+ get message() {
27
+ return `Failed to start ${this.server} on port ${this.port}: ${this.reason}`;
28
+ }
29
+ }
30
+
31
+ export class ServerNotReady extends Data.TaggedError("ServerNotReady")<{
32
+ readonly servers: string[];
33
+ readonly timeoutMs: number;
34
+ }> {
35
+ get message() {
36
+ return `Servers not ready after ${this.timeoutMs}ms: ${this.servers.join(", ")}`;
37
+ }
38
+ }
39
+
40
+ export class FlowExecutionFailed extends Data.TaggedError("FlowExecutionFailed")<{
41
+ readonly flowName: string;
42
+ readonly step: string;
43
+ readonly reason: string;
44
+ }> {
45
+ get message() {
46
+ return `Flow "${this.flowName}" failed at step "${this.step}": ${this.reason}`;
47
+ }
48
+ }
49
+
50
+ export class SnapshotFailed extends Data.TaggedError("SnapshotFailed")<{
51
+ readonly reason: string;
52
+ }> {
53
+ get message() {
54
+ return `Failed to capture snapshot: ${this.reason}`;
55
+ }
56
+ }
57
+
58
+ export class ExportFailed extends Data.TaggedError("ExportFailed")<{
59
+ readonly path: string;
60
+ readonly reason: string;
61
+ }> {
62
+ get message() {
63
+ return `Failed to export session to ${this.path}: ${this.reason}`;
64
+ }
65
+ }
66
+
67
+ export class BrowserMetricsFailed extends Data.TaggedError("BrowserMetricsFailed")<{
68
+ readonly reason: string;
69
+ }> {
70
+ get message() {
71
+ return `Failed to collect browser metrics: ${this.reason}`;
72
+ }
73
+ }
74
+
75
+ export class PopupNotDetected extends Data.TaggedError("PopupNotDetected")<{
76
+ readonly timeoutMs: number;
77
+ }> {
78
+ get message() {
79
+ return `Popup window not detected within ${this.timeoutMs}ms`;
80
+ }
81
+ }
82
+
83
+ export class AuthenticationFailed extends Data.TaggedError("AuthenticationFailed")<{
84
+ readonly step: string;
85
+ readonly reason: string;
86
+ }> {
87
+ get message() {
88
+ return `Authentication failed at "${this.step}": ${this.reason}`;
89
+ }
90
+ }
91
+
92
+ export type SessionRecorderError =
93
+ | SessionTimeout
94
+ | BrowserLaunchFailed
95
+ | ServerStartFailed
96
+ | ServerNotReady
97
+ | FlowExecutionFailed
98
+ | SnapshotFailed
99
+ | ExportFailed
100
+ | BrowserMetricsFailed
101
+ | PopupNotDetected
102
+ | AuthenticationFailed;
@@ -0,0 +1,210 @@
1
+ import { Effect } from "effect";
2
+ import { AuthenticationFailed, PopupNotDetected } from "../errors";
3
+ import {
4
+ type BrowserHandle,
5
+ clickElement,
6
+ getBrowserMetrics,
7
+ navigateTo,
8
+ sleep,
9
+ waitForPopup,
10
+ waitForSelector,
11
+ } from "../playwright";
12
+ import type { SessionEventType } from "../types";
13
+
14
+ interface FlowRecorder {
15
+ recordEvent: (
16
+ type: SessionEventType,
17
+ label: string,
18
+ metadata?: Record<string, unknown>
19
+ ) => Effect.Effect<void>;
20
+ }
21
+
22
+ interface LoginFlowOptions {
23
+ baseUrl: string;
24
+ headless: boolean;
25
+ stubWallet: boolean;
26
+ timeout: number;
27
+ }
28
+
29
+ const DEFAULT_LOGIN_OPTIONS: LoginFlowOptions = {
30
+ baseUrl: "http://localhost:3000",
31
+ headless: true,
32
+ stubWallet: true,
33
+ timeout: 30000,
34
+ };
35
+
36
+ export const runLoginFlow = (
37
+ browser: BrowserHandle,
38
+ recorder: FlowRecorder,
39
+ options: Partial<LoginFlowOptions> = {}
40
+ ): Effect.Effect<void, AuthenticationFailed | PopupNotDetected> =>
41
+ Effect.gen(function* () {
42
+ const opts = { ...DEFAULT_LOGIN_OPTIONS, ...options };
43
+ const { page, context } = browser;
44
+
45
+ yield* Effect.logInfo("Starting login flow");
46
+ yield* Effect.asVoid(recorder.recordEvent("custom", "login_flow_start"));
47
+
48
+ yield* navigateTo(page, `${opts.baseUrl}/login`);
49
+ yield* Effect.asVoid(recorder.recordEvent("pageload", "/login", { url: `${opts.baseUrl}/login` }));
50
+
51
+ yield* sleep(1000);
52
+
53
+ const connectButtonSelector = 'button:has-text("connect near wallet")';
54
+
55
+ yield* Effect.tryPromise({
56
+ try: () => page.waitForSelector(connectButtonSelector, { timeout: opts.timeout }),
57
+ catch: () => new AuthenticationFailed({
58
+ step: "find_connect_button",
59
+ reason: "Connect wallet button not found",
60
+ }),
61
+ });
62
+
63
+ yield* Effect.asVoid(recorder.recordEvent("auth_start", "wallet_connect_initiated"));
64
+
65
+ if (opts.headless && opts.stubWallet) {
66
+ yield* Effect.logInfo("Stubbing wallet connection in headless mode");
67
+ yield* Effect.asVoid(recorder.recordEvent("custom", "wallet_stubbed", { reason: "headless_mode" }));
68
+
69
+ yield* clickElement(page, connectButtonSelector);
70
+
71
+ yield* sleep(2000);
72
+
73
+ const hasSignInButton = yield* Effect.tryPromise({
74
+ try: async () => {
75
+ try {
76
+ await page.waitForSelector('button:has-text("sign in as")', { timeout: 5000 });
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ },
82
+ catch: () => false,
83
+ });
84
+
85
+ if (hasSignInButton) {
86
+ yield* Effect.asVoid(recorder.recordEvent("custom", "sign_in_button_appeared"));
87
+ } else {
88
+ yield* Effect.asVoid(recorder.recordEvent("custom", "wallet_popup_would_open", {
89
+ note: "In headed mode, NEAR wallet popup would appear here",
90
+ }));
91
+ }
92
+
93
+ yield* Effect.asVoid(recorder.recordEvent("auth_complete", "login_flow_completed_stubbed"));
94
+
95
+ } else {
96
+ yield* Effect.logInfo("Running full wallet flow (headed mode)");
97
+
98
+ yield* clickElement(page, connectButtonSelector);
99
+ yield* Effect.asVoid(recorder.recordEvent("click", "connect_wallet_button"));
100
+
101
+ const popupPromise = waitForPopup(context, opts.timeout);
102
+
103
+ const popup = yield* Effect.catchAll(popupPromise, () =>
104
+ Effect.fail(new PopupNotDetected({ timeoutMs: opts.timeout }))
105
+ );
106
+
107
+ yield* Effect.asVoid(recorder.recordEvent("popup_open", "near_wallet_popup", {
108
+ url: popup.url(),
109
+ }));
110
+
111
+ yield* Effect.logInfo("NEAR wallet popup opened - waiting for user interaction");
112
+ yield* Effect.asVoid(recorder.recordEvent("custom", "awaiting_user_approval"));
113
+
114
+ yield* sleep(5000);
115
+
116
+ const popupClosed = yield* Effect.tryPromise({
117
+ try: async () => {
118
+ try {
119
+ await popup.waitForSelector("body", { timeout: 1000 });
120
+ return false;
121
+ } catch {
122
+ return true;
123
+ }
124
+ },
125
+ catch: () => true,
126
+ });
127
+
128
+ if (popupClosed) {
129
+ yield* Effect.asVoid(recorder.recordEvent("popup_close", "near_wallet_popup"));
130
+ }
131
+
132
+ const signInButtonSelector = 'button:has-text("sign in as")';
133
+ const hasSignIn = yield* Effect.tryPromise({
134
+ try: async () => {
135
+ try {
136
+ await page.waitForSelector(signInButtonSelector, { timeout: 10000 });
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ },
142
+ catch: () => false,
143
+ });
144
+
145
+ if (hasSignIn) {
146
+ yield* Effect.asVoid(recorder.recordEvent("custom", "wallet_connected"));
147
+ yield* clickElement(page, signInButtonSelector);
148
+ yield* Effect.asVoid(recorder.recordEvent("click", "sign_in_button"));
149
+ }
150
+
151
+ yield* Effect.asVoid(recorder.recordEvent("auth_complete", "login_flow_completed"));
152
+ }
153
+
154
+ yield* Effect.logInfo("Login flow completed");
155
+ });
156
+
157
+ export const runNavigationFlow = (
158
+ browser: BrowserHandle,
159
+ recorder: FlowRecorder,
160
+ routes: string[],
161
+ baseUrl: string
162
+ ): Effect.Effect<void> =>
163
+ Effect.gen(function* () {
164
+ yield* Effect.logInfo(`Running navigation flow through ${routes.length} routes`);
165
+
166
+ for (const route of routes) {
167
+ const url = `${baseUrl}${route}`;
168
+ yield* navigateTo(browser.page, url);
169
+ yield* Effect.asVoid(recorder.recordEvent("navigation", route, { url }));
170
+
171
+ yield* sleep(1000);
172
+
173
+ yield* Effect.asVoid(recorder.recordEvent("interval", `visited_${route}`));
174
+ }
175
+
176
+ yield* Effect.logInfo("Navigation flow completed");
177
+ });
178
+
179
+ export const runClickFlow = (
180
+ browser: BrowserHandle,
181
+ recorder: FlowRecorder,
182
+ selectors: Array<{ selector: string; label: string }>
183
+ ): Effect.Effect<void> =>
184
+ Effect.gen(function* () {
185
+ yield* Effect.logInfo(`Running click flow with ${selectors.length} interactions`);
186
+
187
+ for (const { selector, label } of selectors) {
188
+ const exists = yield* Effect.tryPromise({
189
+ try: async () => {
190
+ try {
191
+ await browser.page.waitForSelector(selector, { timeout: 5000 });
192
+ return true;
193
+ } catch {
194
+ return false;
195
+ }
196
+ },
197
+ catch: () => false,
198
+ });
199
+
200
+ if (exists) {
201
+ yield* clickElement(browser.page, selector);
202
+ yield* Effect.asVoid(recorder.recordEvent("click", label, { selector }));
203
+ yield* sleep(500);
204
+ } else {
205
+ yield* Effect.logWarning(`Selector not found: ${selector}`);
206
+ }
207
+ }
208
+
209
+ yield* Effect.logInfo("Click flow completed");
210
+ });