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/README.md +73 -3
- package/dist/browser.cjs.js +1 -1
- package/dist/browser.d.ts +7 -0
- package/dist/browser.es.js +279 -108
- package/dist/cli.js +380 -302
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +24 -3
- package/dist/index.es.js +1 -1
- package/dist/relay-CK82tv0w.cjs +1 -0
- package/dist/relay-NDt-wlXz.js +148 -0
- package/dist/vite.cjs.js +1 -1
- package/dist/vite.es.js +2 -2
- package/package.json +7 -5
- package/dist/createTwdRelay-22etCW_8.js +0 -123
- package/dist/createTwdRelay-VRmjHboP.cjs +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,259 +1,311 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from "http";
|
|
3
|
-
import WebSocket
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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>
|
|
268
|
-
--host <host>
|
|
269
|
-
--path <path>
|
|
270
|
-
--timeout <ms>
|
|
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
|
-
|
|
282
|
-
|
|
341
|
+
printHelp();
|
|
342
|
+
process.exit(0);
|
|
283
343
|
}
|
|
284
344
|
if (subcommand === "run") {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
409
|
+
console.error(`Unknown command: ${subcommand}`);
|
|
410
|
+
printHelp();
|
|
411
|
+
process.exit(1);
|
|
335
412
|
}
|
|
413
|
+
//#endregion
|