twd-relay 0.3.0 → 1.1.0

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/dist/cli.js CHANGED
@@ -1,259 +1,311 @@
1
1
  #!/usr/bin/env node
2
2
  import { createServer } from "http";
3
- import WebSocket$1, { WebSocketServer, WebSocket } from "ws";
4
- const DEFAULT_PATH = "/__twd/ws";
3
+ import WebSocket, { WebSocket as WebSocket$1, WebSocketServer } from "ws";
4
+ //#region src/relay/createTwdRelay.ts
5
+ var DEFAULT_PATH = "/__twd/ws";
5
6
  function createTwdRelay(server, options) {
6
- const path = options?.path ?? DEFAULT_PATH;
7
- const onError = options?.onError;
8
- const wss = new WebSocketServer({ noServer: true });
9
- let browser = null;
10
- const clients = /* @__PURE__ */ new Set();
11
- let runInProgress = false;
12
- function sendError(ws, code, message) {
13
- const msg = { type: "error", code, message };
14
- if (ws.readyState === WebSocket.OPEN) {
15
- ws.send(JSON.stringify(msg));
16
- }
17
- }
18
- function broadcastToClients(data) {
19
- for (const client of clients) {
20
- if (client.readyState === WebSocket.OPEN) {
21
- client.send(data);
22
- }
23
- }
24
- }
25
- function sendConnectedStatus(ws) {
26
- const msg = { type: "connected", browser: browser !== null && browser.readyState === WebSocket.OPEN };
27
- if (ws.readyState === WebSocket.OPEN) {
28
- ws.send(JSON.stringify(msg));
29
- }
30
- }
31
- function notifyBrowserDisconnected() {
32
- broadcastToClients(JSON.stringify({ type: "connected", browser: false }));
33
- }
34
- function handleBrowserMessage(data) {
35
- let parsed;
36
- try {
37
- parsed = JSON.parse(data);
38
- } catch {
39
- return;
40
- }
41
- if (parsed.type === "run:complete") {
42
- runInProgress = false;
43
- }
44
- broadcastToClients(data);
45
- }
46
- function handleClientMessage(ws, data) {
47
- let parsed;
48
- try {
49
- parsed = JSON.parse(data);
50
- } catch {
51
- sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
52
- return;
53
- }
54
- if (!parsed.type) {
55
- sendError(ws, "INVALID_MESSAGE", 'Missing "type" field');
56
- return;
57
- }
58
- if (parsed.type === "run") {
59
- if (!browser || browser.readyState !== WebSocket.OPEN) {
60
- sendError(ws, "NO_BROWSER", "No browser connected");
61
- return;
62
- }
63
- if (runInProgress) {
64
- sendError(ws, "RUN_IN_PROGRESS", "A test run is already in progress");
65
- return;
66
- }
67
- runInProgress = true;
68
- browser.send(data);
69
- return;
70
- }
71
- if (parsed.type === "status") {
72
- if (!browser || browser.readyState !== WebSocket.OPEN) {
73
- sendError(ws, "NO_BROWSER", "No browser connected");
74
- return;
75
- }
76
- browser.send(data);
77
- return;
78
- }
79
- sendError(ws, "UNKNOWN_COMMAND", `Unknown command: ${parsed.type}`);
80
- }
81
- function handleConnection(ws) {
82
- let identified = false;
83
- const identifyHandler = (raw) => {
84
- const data = typeof raw === "string" ? raw : raw.toString();
85
- if (!identified) {
86
- let parsed;
87
- try {
88
- parsed = JSON.parse(data);
89
- } catch {
90
- sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
91
- return;
92
- }
93
- if (parsed.type !== "hello" || parsed.role !== "browser" && parsed.role !== "client") {
94
- sendError(ws, "INVALID_MESSAGE", 'First message must be a hello with role "browser" or "client"');
95
- return;
96
- }
97
- identified = true;
98
- if (parsed.role === "browser") {
99
- if (browser && browser.readyState === WebSocket.OPEN) {
100
- browser.close(1e3, "Replaced by new browser");
101
- }
102
- browser = ws;
103
- runInProgress = false;
104
- ws.on("close", () => {
105
- if (browser === ws) {
106
- browser = null;
107
- runInProgress = false;
108
- notifyBrowserDisconnected();
109
- }
110
- });
111
- broadcastToClients(JSON.stringify({ type: "connected", browser: true }));
112
- ws.on("message", (raw2) => {
113
- const msg = typeof raw2 === "string" ? raw2 : raw2.toString();
114
- handleBrowserMessage(msg);
115
- });
116
- } else {
117
- clients.add(ws);
118
- ws.on("close", () => {
119
- clients.delete(ws);
120
- });
121
- sendConnectedStatus(ws);
122
- ws.on("message", (raw2) => {
123
- const msg = typeof raw2 === "string" ? raw2 : raw2.toString();
124
- handleClientMessage(ws, msg);
125
- });
126
- }
127
- ws.removeListener("message", identifyHandler);
128
- }
129
- };
130
- ws.on("message", identifyHandler);
131
- ws.on("error", (err) => {
132
- if (onError) onError(err);
133
- });
134
- }
135
- const upgradeHandler = (request, socket, head) => {
136
- const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
137
- if (url.pathname === path) {
138
- wss.handleUpgrade(request, socket, head, (ws) => {
139
- handleConnection(ws);
140
- });
141
- }
142
- };
143
- server.on("upgrade", upgradeHandler);
144
- wss.on("error", (err) => {
145
- if (onError) onError(err);
146
- });
147
- return {
148
- close() {
149
- server.removeListener("upgrade", upgradeHandler);
150
- for (const client of clients) {
151
- client.close(1e3, "Relay shutting down");
152
- }
153
- clients.clear();
154
- if (browser && browser.readyState === WebSocket.OPEN) {
155
- browser.close(1e3, "Relay shutting down");
156
- }
157
- browser = null;
158
- runInProgress = false;
159
- wss.close();
160
- },
161
- get browserConnected() {
162
- return browser !== null && browser.readyState === WebSocket.OPEN;
163
- },
164
- get clientCount() {
165
- return clients.size;
166
- }
167
- };
7
+ const path = options?.path ?? DEFAULT_PATH;
8
+ const onError = options?.onError;
9
+ const wss = new WebSocketServer({ noServer: true });
10
+ let browser = null;
11
+ const clients = /* @__PURE__ */ new Set();
12
+ let runInProgress = false;
13
+ let lastHeartbeat = null;
14
+ let heartbeatCheckTimer = null;
15
+ const HEARTBEAT_TIMEOUT_MS = 12e4;
16
+ const HEARTBEAT_CHECK_INTERVAL_MS = 1e4;
17
+ function sendError(ws, code, message) {
18
+ const msg = {
19
+ type: "error",
20
+ code,
21
+ message
22
+ };
23
+ if (ws.readyState === WebSocket$1.OPEN) ws.send(JSON.stringify(msg));
24
+ }
25
+ function broadcastToClients(data) {
26
+ for (const client of clients) if (client.readyState === WebSocket$1.OPEN) client.send(data);
27
+ }
28
+ function sendConnectedStatus(ws) {
29
+ const msg = {
30
+ type: "connected",
31
+ browser: browser !== null && browser.readyState === WebSocket$1.OPEN
32
+ };
33
+ if (ws.readyState === WebSocket$1.OPEN) ws.send(JSON.stringify(msg));
34
+ }
35
+ function notifyBrowserDisconnected() {
36
+ broadcastToClients(JSON.stringify({
37
+ type: "connected",
38
+ browser: false
39
+ }));
40
+ }
41
+ function startHeartbeatTracking() {
42
+ lastHeartbeat = Date.now();
43
+ heartbeatCheckTimer = setInterval(() => {
44
+ if (runInProgress && lastHeartbeat !== null) {
45
+ if (Date.now() - lastHeartbeat > HEARTBEAT_TIMEOUT_MS) {
46
+ runInProgress = false;
47
+ stopHeartbeatTracking();
48
+ broadcastToClients(JSON.stringify({
49
+ type: "run:abandoned",
50
+ reason: "heartbeat_timeout"
51
+ }));
52
+ }
53
+ }
54
+ }, HEARTBEAT_CHECK_INTERVAL_MS);
55
+ }
56
+ function stopHeartbeatTracking() {
57
+ lastHeartbeat = null;
58
+ if (heartbeatCheckTimer !== null) {
59
+ clearInterval(heartbeatCheckTimer);
60
+ heartbeatCheckTimer = null;
61
+ }
62
+ }
63
+ function handleBrowserMessage(data) {
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(data);
67
+ } catch {
68
+ return;
69
+ }
70
+ if (parsed.type === "heartbeat") {
71
+ lastHeartbeat = Date.now();
72
+ return;
73
+ }
74
+ if (parsed.type === "run:complete") {
75
+ runInProgress = false;
76
+ stopHeartbeatTracking();
77
+ }
78
+ broadcastToClients(data);
79
+ }
80
+ function handleClientMessage(ws, data) {
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(data);
84
+ } catch {
85
+ sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
86
+ return;
87
+ }
88
+ if (!parsed.type) {
89
+ sendError(ws, "INVALID_MESSAGE", "Missing \"type\" field");
90
+ return;
91
+ }
92
+ if (parsed.type === "run") {
93
+ if (!browser || browser.readyState !== WebSocket$1.OPEN) {
94
+ sendError(ws, "NO_BROWSER", "No browser connected");
95
+ return;
96
+ }
97
+ if (runInProgress) {
98
+ sendError(ws, "RUN_IN_PROGRESS", "A test run is already in progress. If the previous run appears stuck, the browser tab may be backgrounded and throttled — foreground the TWD tab (identified by the \"[TWD …]\" title prefix) or reload it. The relay also auto-clears the lock after 120s of heartbeat silence.");
99
+ return;
100
+ }
101
+ runInProgress = true;
102
+ startHeartbeatTracking();
103
+ browser.send(data);
104
+ return;
105
+ }
106
+ if (parsed.type === "status") {
107
+ if (!browser || browser.readyState !== WebSocket$1.OPEN) {
108
+ sendError(ws, "NO_BROWSER", "No browser connected");
109
+ return;
110
+ }
111
+ browser.send(data);
112
+ return;
113
+ }
114
+ sendError(ws, "UNKNOWN_COMMAND", `Unknown command: ${parsed.type}`);
115
+ }
116
+ function handleConnection(ws) {
117
+ let identified = false;
118
+ const identifyHandler = (raw) => {
119
+ const data = typeof raw === "string" ? raw : raw.toString();
120
+ if (!identified) {
121
+ let parsed;
122
+ try {
123
+ parsed = JSON.parse(data);
124
+ } catch {
125
+ sendError(ws, "INVALID_MESSAGE", "Invalid JSON");
126
+ return;
127
+ }
128
+ if (parsed.type !== "hello" || parsed.role !== "browser" && parsed.role !== "client") {
129
+ sendError(ws, "INVALID_MESSAGE", "First message must be a hello with role \"browser\" or \"client\"");
130
+ return;
131
+ }
132
+ identified = true;
133
+ if (parsed.role === "browser") {
134
+ if (browser && browser.readyState === WebSocket$1.OPEN) browser.close(1e3, "Replaced by new browser");
135
+ browser = ws;
136
+ runInProgress = false;
137
+ stopHeartbeatTracking();
138
+ ws.on("close", () => {
139
+ if (browser === ws) {
140
+ browser = null;
141
+ runInProgress = false;
142
+ stopHeartbeatTracking();
143
+ notifyBrowserDisconnected();
144
+ }
145
+ });
146
+ broadcastToClients(JSON.stringify({
147
+ type: "connected",
148
+ browser: true
149
+ }));
150
+ ws.on("message", (raw) => {
151
+ handleBrowserMessage(typeof raw === "string" ? raw : raw.toString());
152
+ });
153
+ } else {
154
+ clients.add(ws);
155
+ ws.on("close", () => {
156
+ clients.delete(ws);
157
+ });
158
+ sendConnectedStatus(ws);
159
+ ws.on("message", (raw) => {
160
+ handleClientMessage(ws, typeof raw === "string" ? raw : raw.toString());
161
+ });
162
+ }
163
+ ws.removeListener("message", identifyHandler);
164
+ }
165
+ };
166
+ ws.on("message", identifyHandler);
167
+ ws.on("error", (err) => {
168
+ if (onError) onError(err);
169
+ });
170
+ }
171
+ const upgradeHandler = (request, socket, head) => {
172
+ if (new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`).pathname === path) wss.handleUpgrade(request, socket, head, (ws) => {
173
+ handleConnection(ws);
174
+ });
175
+ };
176
+ server.on("upgrade", upgradeHandler);
177
+ wss.on("error", (err) => {
178
+ if (onError) onError(err);
179
+ });
180
+ return {
181
+ close() {
182
+ server.removeListener("upgrade", upgradeHandler);
183
+ stopHeartbeatTracking();
184
+ for (const client of clients) client.close(1e3, "Relay shutting down");
185
+ clients.clear();
186
+ if (browser && browser.readyState === WebSocket$1.OPEN) browser.close(1e3, "Relay shutting down");
187
+ browser = null;
188
+ runInProgress = false;
189
+ wss.close();
190
+ },
191
+ get browserConnected() {
192
+ return browser !== null && browser.readyState === WebSocket$1.OPEN;
193
+ },
194
+ get clientCount() {
195
+ return clients.size;
196
+ }
197
+ };
168
198
  }
199
+ //#endregion
200
+ //#region src/cli/run.ts
169
201
  function run(options) {
170
- const { port, timeout, path, host } = options;
171
- const url = `ws://${host}:${port}${path}`;
172
- console.log(`Connecting to ${url}...`);
173
- const ws = new WebSocket$1(url);
174
- let runSent = false;
175
- let runComplete = false;
176
- let failed = false;
177
- const timer = setTimeout(() => {
178
- console.error(`
179
- Timeout: no run:complete received within ${timeout / 1e3}s`);
180
- ws.close();
181
- process.exit(1);
182
- }, timeout);
183
- ws.on("open", () => {
184
- ws.send(JSON.stringify({ type: "hello", role: "client" }));
185
- });
186
- ws.on("message", (data) => {
187
- const msg = JSON.parse(data.toString());
188
- switch (msg.type) {
189
- case "connected":
190
- if (msg.browser && !runSent) {
191
- runSent = true;
192
- console.log("Browser connected, triggering test run...\n");
193
- ws.send(JSON.stringify({ type: "run", scope: "all" }));
194
- } else if (!msg.browser) {
195
- console.log("Waiting for browser to connect...");
196
- }
197
- break;
198
- case "run:start":
199
- console.log(`Running ${msg.testCount} test(s)...
200
- `);
201
- break;
202
- case "test:start":
203
- console.log(` RUN: ${msg.suite} > ${msg.name}`);
204
- break;
205
- case "test:pass":
206
- console.log(` PASS: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
207
- break;
208
- case "test:fail":
209
- failed = true;
210
- console.log(` FAIL: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
211
- if (msg.error) {
212
- console.log(` Error: ${msg.error}`);
213
- }
214
- break;
215
- case "test:skip":
216
- console.log(` SKIP: ${msg.suite} > ${msg.name}`);
217
- break;
218
- case "run:complete": {
219
- const duration = (msg.duration / 1e3).toFixed(1);
220
- console.log(`
221
- --- Run complete ---`);
222
- console.log(`Passed: ${msg.passed} | Failed: ${msg.failed} | Skipped: ${msg.skipped}`);
223
- console.log(`Duration: ${duration}s`);
224
- runComplete = true;
225
- clearTimeout(timer);
226
- ws.close();
227
- process.exit(failed || msg.failed > 0 ? 1 : 0);
228
- break;
229
- }
230
- case "error":
231
- console.error(`Error [${msg.code}]: ${msg.message}`);
232
- break;
233
- }
234
- });
235
- ws.on("close", () => {
236
- clearTimeout(timer);
237
- if (!runComplete) {
238
- console.error("Connection closed before run completed");
239
- process.exit(1);
240
- }
241
- });
242
- ws.on("error", (err) => {
243
- clearTimeout(timer);
244
- console.error(`Connection error: ${err.message}`);
245
- process.exit(1);
246
- });
202
+ const { port, timeout, path, host, testNames, maxTestDurationMs } = options;
203
+ const url = `ws://${host}:${port}${path}`;
204
+ console.log(`Connecting to ${url}...`);
205
+ const ws = new WebSocket(url);
206
+ let runSent = false;
207
+ let runComplete = false;
208
+ let failed = false;
209
+ const timer = setTimeout(() => {
210
+ console.error(`\nTimeout: no run:complete received within ${timeout / 1e3}s`);
211
+ ws.close();
212
+ process.exit(1);
213
+ }, timeout);
214
+ ws.on("open", () => {
215
+ ws.send(JSON.stringify({
216
+ type: "hello",
217
+ role: "client"
218
+ }));
219
+ });
220
+ ws.on("message", (data) => {
221
+ const msg = JSON.parse(data.toString());
222
+ switch (msg.type) {
223
+ case "connected":
224
+ if (msg.browser && !runSent) {
225
+ runSent = true;
226
+ console.log("Browser connected, triggering test run...\n");
227
+ const runMsg = {
228
+ type: "run",
229
+ scope: "all"
230
+ };
231
+ if (testNames?.length) runMsg.testNames = testNames;
232
+ if (maxTestDurationMs !== void 0) runMsg.maxTestDurationMs = maxTestDurationMs;
233
+ ws.send(JSON.stringify(runMsg));
234
+ } else if (!msg.browser) console.log("Waiting for browser to connect...");
235
+ break;
236
+ case "run:start":
237
+ console.log(`Running ${msg.testCount} test(s)...\n`);
238
+ break;
239
+ case "test:start":
240
+ console.log(` RUN: ${msg.suite} > ${msg.name}`);
241
+ break;
242
+ case "test:pass":
243
+ console.log(` PASS: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
244
+ break;
245
+ case "test:fail":
246
+ failed = true;
247
+ console.log(` FAIL: ${msg.suite} > ${msg.name} (${msg.duration}ms)`);
248
+ if (msg.error) console.log(` Error: ${msg.error}`);
249
+ break;
250
+ case "test:skip":
251
+ console.log(` SKIP: ${msg.suite} > ${msg.name}`);
252
+ break;
253
+ case "run:complete": {
254
+ const duration = (msg.duration / 1e3).toFixed(1);
255
+ console.log(`\n--- Run complete ---`);
256
+ console.log(`Passed: ${msg.passed} | Failed: ${msg.failed} | Skipped: ${msg.skipped}`);
257
+ console.log(`Duration: ${duration}s`);
258
+ runComplete = true;
259
+ clearTimeout(timer);
260
+ ws.close();
261
+ process.exit(failed || msg.failed > 0 ? 1 : 0);
262
+ break;
263
+ }
264
+ case "run:aborted": {
265
+ failed = true;
266
+ const seconds = typeof msg.durationMs === "number" ? (msg.durationMs / 1e3).toFixed(1) : "?";
267
+ console.error(`\nRun aborted: test "${msg.testName ?? "?"}" ran for ${seconds}s — threshold exceeded.\nThe TWD browser tab is likely backgrounded and throttled by the browser.\nForeground the TWD tab (identified by the "[TWD …]" title prefix) and keep it active, then retry.\nFor unattended runs, prefer \`twd-cli\` which drives a headless browser with no tab throttling.`);
268
+ break;
269
+ }
270
+ case "run:abandoned":
271
+ console.error("\nRun abandoned — browser tab appears frozen. Refresh the browser tab and retry.");
272
+ clearTimeout(timer);
273
+ ws.close();
274
+ process.exit(1);
275
+ break;
276
+ case "error":
277
+ console.error(`Error [${msg.code}]: ${msg.message}`);
278
+ break;
279
+ }
280
+ });
281
+ ws.on("close", () => {
282
+ clearTimeout(timer);
283
+ if (!runComplete) {
284
+ console.error("Connection closed before run completed");
285
+ process.exit(1);
286
+ }
287
+ });
288
+ ws.on("error", (err) => {
289
+ clearTimeout(timer);
290
+ console.error(`Connection error: ${err.message}`);
291
+ process.exit(1);
292
+ });
247
293
  }
248
- const args = process.argv.slice(2);
249
- const subcommand = args.find((a) => !a.startsWith("--"));
294
+ //#endregion
295
+ //#region src/cli/standalone.ts
296
+ var args = process.argv.slice(2);
297
+ var subcommand = args.find((a) => !a.startsWith("--"));
250
298
  function parseFlag(name) {
251
- const idx = args.indexOf(name);
252
- if (idx !== -1 && args[idx + 1]) return args[idx + 1];
253
- return void 0;
299
+ const idx = args.indexOf(name);
300
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
301
+ }
302
+ function parseFlagAll(name) {
303
+ const values = [];
304
+ for (let i = 0; i < args.length; i++) if (args[i] === name && args[i + 1] && !args[i + 1].startsWith("--")) values.push(args[i + 1]);
305
+ return values;
254
306
  }
255
307
  function printHelp() {
256
- console.log(`Usage: twd-relay [command] [options]
308
+ console.log(`Usage: twd-relay [command] [options]
257
309
 
258
310
  Commands:
259
311
  (none), serve Start the standalone relay server (default)
@@ -264,10 +316,14 @@ Options for serve:
264
316
  --path <path> WebSocket path (default: /__twd/ws)
265
317
 
266
318
  Options for run:
267
- --port <port> Relay port to connect to (default: 5173)
268
- --host <host> Relay host to connect to (default: localhost)
269
- --path <path> WebSocket path (default: /__twd/ws)
270
- --timeout <ms> Timeout in ms (default: 180000)
319
+ --port <port> Relay port to connect to (default: 5173)
320
+ --host <host> Relay host to connect to (default: localhost)
321
+ --path <path> WebSocket path (default: /__twd/ws)
322
+ --timeout <ms> Timeout in ms (default: 180000)
323
+ --test <name> Filter tests by name substring (repeatable)
324
+ --max-test-duration <ms> Abort if any single test exceeds this many
325
+ ms (default from browser client, typically
326
+ 5000; 0 disables)
271
327
 
272
328
  Examples:
273
329
  twd-relay # start relay on port 9876
@@ -275,61 +331,83 @@ Examples:
275
331
  twd-relay run # trigger run via Vite dev server on 5173
276
332
  twd-relay run --port 9876 # trigger run on custom port
277
333
  twd-relay run --host 192.168.1.10 --path /app/__twd/ws
278
- twd-relay run --timeout 30000 # custom timeout`);
334
+ twd-relay run --timeout 30000 # custom timeout
335
+ twd-relay run --test "login" # run tests matching "login"
336
+ twd-relay run --test "login" --test "signup" # run multiple
337
+ twd-relay run --max-test-duration 30000 # raise abort threshold to 30s
338
+ twd-relay run --max-test-duration 0 # disable abort detection`);
279
339
  }
280
340
  if (args.includes("--help") || args.includes("-h")) {
281
- printHelp();
282
- process.exit(0);
341
+ printHelp();
342
+ process.exit(0);
283
343
  }
284
344
  if (subcommand === "run") {
285
- const portStr = parseFlag("--port");
286
- const timeoutStr = parseFlag("--timeout");
287
- const pathFlag = parseFlag("--path") ?? "/__twd/ws";
288
- const hostFlag = parseFlag("--host") ?? "localhost";
289
- const port = portStr ? parseInt(portStr, 10) : 5173;
290
- if (isNaN(port)) {
291
- console.error("Invalid port number:", portStr);
292
- process.exit(1);
293
- }
294
- const timeout = timeoutStr ? parseInt(timeoutStr, 10) : 18e4;
295
- if (isNaN(timeout)) {
296
- console.error("Invalid timeout value:", timeoutStr);
297
- process.exit(1);
298
- }
299
- run({ port, timeout, path: pathFlag, host: hostFlag });
345
+ const portStr = parseFlag("--port");
346
+ const timeoutStr = parseFlag("--timeout");
347
+ const pathFlag = parseFlag("--path") ?? "/__twd/ws";
348
+ const hostFlag = parseFlag("--host") ?? "localhost";
349
+ const port = portStr ? parseInt(portStr, 10) : 5173;
350
+ if (isNaN(port)) {
351
+ console.error("Invalid port number:", portStr);
352
+ process.exit(1);
353
+ }
354
+ const timeout = timeoutStr ? parseInt(timeoutStr, 10) : 18e4;
355
+ if (isNaN(timeout)) {
356
+ console.error("Invalid timeout value:", timeoutStr);
357
+ process.exit(1);
358
+ }
359
+ const testNames = parseFlagAll("--test");
360
+ const maxDurationStr = parseFlag("--max-test-duration");
361
+ let maxTestDurationMs;
362
+ if (maxDurationStr !== void 0) {
363
+ maxTestDurationMs = parseInt(maxDurationStr, 10);
364
+ if (isNaN(maxTestDurationMs)) {
365
+ console.error("Invalid --max-test-duration value:", maxDurationStr);
366
+ process.exit(1);
367
+ }
368
+ }
369
+ run({
370
+ port,
371
+ timeout,
372
+ path: pathFlag,
373
+ host: hostFlag,
374
+ testNames: testNames.length > 0 ? testNames : void 0,
375
+ maxTestDurationMs
376
+ });
300
377
  } else if (!subcommand || subcommand === "serve") {
301
- let shutdown = function() {
302
- console.log("\nShutting down...");
303
- relay.close();
304
- server.close(() => {
305
- process.exit(0);
306
- });
307
- };
308
- const portStr = parseFlag("--port");
309
- const pathFlag = parseFlag("--path") ?? "/__twd/ws";
310
- let port = 9876;
311
- if (portStr) {
312
- port = parseInt(portStr, 10);
313
- if (isNaN(port)) {
314
- console.error("Invalid port number:", portStr);
315
- process.exit(1);
316
- }
317
- }
318
- const server = createServer();
319
- const relay = createTwdRelay(server, {
320
- path: pathFlag,
321
- onError(err) {
322
- console.error("[twd-relay] Error:", err.message);
323
- }
324
- });
325
- server.listen(port, () => {
326
- console.log(`TWD Relay running on ws://localhost:${port}${pathFlag}`);
327
- console.log("Waiting for connections...");
328
- });
329
- process.on("SIGINT", shutdown);
330
- process.on("SIGTERM", shutdown);
378
+ const portStr = parseFlag("--port");
379
+ const pathFlag = parseFlag("--path") ?? "/__twd/ws";
380
+ let port = 9876;
381
+ if (portStr) {
382
+ port = parseInt(portStr, 10);
383
+ if (isNaN(port)) {
384
+ console.error("Invalid port number:", portStr);
385
+ process.exit(1);
386
+ }
387
+ }
388
+ const server = createServer();
389
+ const relay = createTwdRelay(server, {
390
+ path: pathFlag,
391
+ onError(err) {
392
+ console.error("[twd-relay] Error:", err.message);
393
+ }
394
+ });
395
+ server.listen(port, () => {
396
+ console.log(`TWD Relay running on ws://localhost:${port}${pathFlag}`);
397
+ console.log("Waiting for connections...");
398
+ });
399
+ function shutdown() {
400
+ console.log("\nShutting down...");
401
+ relay.close();
402
+ server.close(() => {
403
+ process.exit(0);
404
+ });
405
+ }
406
+ process.on("SIGINT", shutdown);
407
+ process.on("SIGTERM", shutdown);
331
408
  } else {
332
- console.error(`Unknown command: ${subcommand}`);
333
- printHelp();
334
- process.exit(1);
409
+ console.error(`Unknown command: ${subcommand}`);
410
+ printHelp();
411
+ process.exit(1);
335
412
  }
413
+ //#endregion