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.
- 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/orchestrator.ts +13 -2
- 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 +154 -15
|
@@ -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 {
|
|
4
|
+
import { calculateRequiredDeposit, Graph } from "near-social-js";
|
|
5
5
|
|
|
6
|
-
import {
|
|
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
|
|