uplink-cli 0.1.37 → 0.1.39
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/AGENTS.md +161 -0
- package/LICENSE +21 -0
- package/README.md +45 -44
- package/cli/src/index.ts +5 -3
- package/cli/src/registrars/cloudflare.ts +148 -0
- package/cli/src/registrars/godaddy.ts +99 -0
- package/cli/src/registrars/hostinger.ts +106 -0
- package/cli/src/registrars/http.ts +18 -0
- package/cli/src/registrars/index.ts +30 -0
- package/cli/src/registrars/namecheap.ts +163 -0
- package/cli/src/registrars/secret.ts +66 -0
- package/cli/src/registrars/store.ts +55 -0
- package/cli/src/registrars/types.ts +40 -0
- package/cli/src/subcommands/admin.ts +17 -30
- package/cli/src/subcommands/db.ts +63 -57
- package/cli/src/subcommands/dev.ts +23 -25
- package/cli/src/subcommands/domains.ts +268 -0
- package/cli/src/subcommands/host-domains.ts +148 -0
- package/cli/src/subcommands/host.ts +106 -32
- package/cli/src/subcommands/menu/colors.ts +1 -1
- package/cli/src/subcommands/menu/effects/tunnel-clients.ts +87 -14
- package/cli/src/subcommands/menu/inline-tree-select.ts +6 -5
- package/cli/src/subcommands/menu/io.ts +27 -5
- package/cli/src/subcommands/menu/menus/domains.ts +199 -0
- package/cli/src/subcommands/menu/menus/hosting.ts +14 -46
- package/cli/src/subcommands/menu/menus/index.ts +1 -0
- package/cli/src/subcommands/menu/menus/tunnels.ts +25 -67
- package/cli/src/subcommands/menu/render.ts +2 -2
- package/cli/src/subcommands/menu/tests.ts +1 -1
- package/cli/src/subcommands/menu/tunnels.ts +10 -99
- package/cli/src/subcommands/menu/types.ts +8 -0
- package/cli/src/subcommands/menu.ts +32 -524
- package/cli/src/subcommands/system.ts +58 -36
- package/cli/src/subcommands/tunnel.ts +124 -33
- package/cli/src/templates/index.ts +122 -0
- package/cli/src/tui/App.tsx +197 -0
- package/cli/src/tui/AppInspector.tsx +114 -0
- package/cli/src/tui/HomeStatus.tsx +59 -0
- package/cli/src/tui/brand.tsx +20 -0
- package/cli/src/tui/format.ts +22 -0
- package/cli/src/tui/index.mts +6 -0
- package/cli/src/tui/liveTree.ts +40 -0
- package/cli/src/tui/package.json +3 -0
- package/cli/src/tui/runMenu.tsx +57 -0
- package/cli/src/tui/session.mts +382 -0
- package/cli/src/tui/snapshot.ts +146 -0
- package/cli/src/utils/analyze.ts +27 -43
- package/cli/src/utils/framework-output.ts +171 -0
- package/cli/src/utils/launchDomainking.ts +64 -0
- package/docs/AGENTS.md +113 -146
- package/docs/MENU_STRUCTURE.md +56 -288
- package/docs/README.md +6 -6
- package/package.json +18 -35
- package/scripts/tunnel/client-improved.js +127 -38
- package/scripts/tunnel/client.js +118 -0
- package/assets/cli-screenshot.png +0 -0
|
@@ -55,10 +55,13 @@ function optionalRead(path) {
|
|
|
55
55
|
|
|
56
56
|
// State
|
|
57
57
|
let socket = null;
|
|
58
|
+
// wsId -> net.Socket — active WebSocket (upgrade) passthrough sessions to the local service
|
|
59
|
+
const wsSessions = new Map();
|
|
58
60
|
let reconnectDelay = INITIAL_RECONNECT_DELAY;
|
|
59
61
|
let reconnectTimer = null;
|
|
60
62
|
let isConnected = false;
|
|
61
63
|
let isRegistered = false;
|
|
64
|
+
let shuttingDown = false;
|
|
62
65
|
let healthCheckTimer = null;
|
|
63
66
|
let lastHealthTimeoutLog = 0;
|
|
64
67
|
let stats = {
|
|
@@ -138,6 +141,7 @@ function checkLocalService() {
|
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
function connect() {
|
|
144
|
+
if (shuttingDown) return;
|
|
141
145
|
if (socket && !socket.destroyed) {
|
|
142
146
|
socket.destroy();
|
|
143
147
|
}
|
|
@@ -197,12 +201,27 @@ function connect() {
|
|
|
197
201
|
const msg = JSON.parse(line);
|
|
198
202
|
if (msg.type === "request") {
|
|
199
203
|
handleRequest(msg);
|
|
204
|
+
} else if (msg.type === "ws-open") {
|
|
205
|
+
handleWsOpen(msg);
|
|
206
|
+
} else if (msg.type === "ws-data") {
|
|
207
|
+
const local = wsSessions.get(msg.id);
|
|
208
|
+
if (local && !local.destroyed) {
|
|
209
|
+
local.write(Buffer.from(msg.data || "", "base64"));
|
|
210
|
+
}
|
|
211
|
+
} else if (msg.type === "ws-close") {
|
|
212
|
+
const local = wsSessions.get(msg.id);
|
|
213
|
+
wsSessions.delete(msg.id);
|
|
214
|
+
if (local && !local.destroyed) local.end();
|
|
200
215
|
} else if (msg.type === "registered") {
|
|
201
216
|
isRegistered = true;
|
|
202
217
|
log("success", `Registered with relay (token: ${token.substring(0, 8)}...)`);
|
|
203
218
|
stats.reconnects = 0; // Reset reconnect count on successful registration
|
|
204
219
|
} else if (msg.type === "error") {
|
|
205
220
|
log("error", "Relay error:", msg.message || "Unknown error");
|
|
221
|
+
if (/invalid token/i.test(String(msg.message || ""))) {
|
|
222
|
+
log("error", "Token rejected by relay; exiting");
|
|
223
|
+
shutdown(1);
|
|
224
|
+
}
|
|
206
225
|
}
|
|
207
226
|
} catch (err) {
|
|
208
227
|
log("error", "Parse error:", err.message, `(message length: ${line.length})`);
|
|
@@ -213,15 +232,18 @@ function connect() {
|
|
|
213
232
|
socket.on("error", (err) => {
|
|
214
233
|
isConnected = false;
|
|
215
234
|
isRegistered = false;
|
|
235
|
+
closeAllWsSessions();
|
|
216
236
|
logError(err, "Connection error");
|
|
217
|
-
scheduleReconnect();
|
|
237
|
+
if (!shuttingDown) scheduleReconnect();
|
|
218
238
|
});
|
|
219
239
|
|
|
220
240
|
socket.on("close", () => {
|
|
221
241
|
const wasRegistered = isRegistered;
|
|
222
242
|
isConnected = false;
|
|
223
243
|
isRegistered = false;
|
|
224
|
-
|
|
244
|
+
closeAllWsSessions();
|
|
245
|
+
|
|
246
|
+
if (shuttingDown) return;
|
|
225
247
|
if (wasRegistered) {
|
|
226
248
|
log("warn", "Connection closed. Attempting to reconnect...");
|
|
227
249
|
scheduleReconnect();
|
|
@@ -232,6 +254,7 @@ function connect() {
|
|
|
232
254
|
}
|
|
233
255
|
|
|
234
256
|
function scheduleReconnect() {
|
|
257
|
+
if (shuttingDown) return;
|
|
235
258
|
if (reconnectTimer) {
|
|
236
259
|
clearTimeout(reconnectTimer);
|
|
237
260
|
}
|
|
@@ -286,39 +309,45 @@ function handleRequest(msg) {
|
|
|
286
309
|
headers: cleanHeaders,
|
|
287
310
|
timeout: REQUEST_TIMEOUT,
|
|
288
311
|
};
|
|
312
|
+
log("info", `Local ${options.method} ${options.path} host=${cleanHeaders.host || cleanHeaders.Host || "-"}`);
|
|
289
313
|
|
|
290
314
|
const req = http.request(options, (resp) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
315
|
+
// Stream the response over the control channel instead of buffering it.
|
|
316
|
+
// This removes the response size cap (needed e.g. for large dev-mode JS
|
|
317
|
+
// chunks) and keeps memory usage flat regardless of response size.
|
|
318
|
+
let aborted = false;
|
|
319
|
+
const send = (obj) => {
|
|
320
|
+
if (!socket || socket.destroyed || !isRegistered) {
|
|
321
|
+
aborted = true;
|
|
322
|
+
resp.destroy();
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
return socket.write(JSON.stringify(obj) + "\n");
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
send({
|
|
329
|
+
type: "response-head",
|
|
330
|
+
id: msg.id,
|
|
331
|
+
status: resp.statusCode,
|
|
332
|
+
headers: resp.headers,
|
|
333
|
+
});
|
|
334
|
+
|
|
294
335
|
resp.on("data", (d) => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
log("error", `Response too large: ${totalSize} bytes (max: ${MAX_BODY_SIZE})`);
|
|
302
|
-
sendErrorResponse(msg.id, 413, "Response entity too large");
|
|
303
|
-
return;
|
|
336
|
+
if (aborted) return;
|
|
337
|
+
const ok = send({ type: "response-chunk", id: msg.id, data: d.toString("base64") });
|
|
338
|
+
// Backpressure: pause the local response while the control socket drains
|
|
339
|
+
if (ok === false && socket && !socket.destroyed) {
|
|
340
|
+
resp.pause();
|
|
341
|
+
socket.once("drain", () => resp.resume());
|
|
304
342
|
}
|
|
305
343
|
});
|
|
306
|
-
|
|
344
|
+
|
|
307
345
|
resp.on("end", () => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
headers: resp.headers,
|
|
314
|
-
body: body.length ? body.toString("base64") : "",
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
if (socket && !socket.destroyed && isRegistered) {
|
|
318
|
-
socket.write(JSON.stringify(resMsg) + "\n");
|
|
319
|
-
} else {
|
|
320
|
-
log("warn", "Cannot send response: not connected");
|
|
321
|
-
}
|
|
346
|
+
if (!aborted) send({ type: "response-end", id: msg.id });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
resp.on("error", () => {
|
|
350
|
+
if (!aborted) send({ type: "response-end", id: msg.id });
|
|
322
351
|
});
|
|
323
352
|
});
|
|
324
353
|
|
|
@@ -353,6 +382,56 @@ function handleRequest(msg) {
|
|
|
353
382
|
req.end();
|
|
354
383
|
}
|
|
355
384
|
|
|
385
|
+
// WebSocket / HTTP Upgrade passthrough: open a raw TCP connection to the
|
|
386
|
+
// local service, replay the original handshake bytes, then stream data in
|
|
387
|
+
// both directions over the control channel until either side closes.
|
|
388
|
+
function handleWsOpen(msg) {
|
|
389
|
+
if (!msg.id || !msg.head) return;
|
|
390
|
+
stats.requests++;
|
|
391
|
+
|
|
392
|
+
const local = net.createConnection({ host: "127.0.0.1", port }, () => {
|
|
393
|
+
local.write(Buffer.from(msg.head, "base64"));
|
|
394
|
+
});
|
|
395
|
+
local.setNoDelay(true);
|
|
396
|
+
wsSessions.set(msg.id, local);
|
|
397
|
+
|
|
398
|
+
local.on("data", (data) => {
|
|
399
|
+
if (!socket || socket.destroyed || !isRegistered) return;
|
|
400
|
+
try {
|
|
401
|
+
socket.write(
|
|
402
|
+
JSON.stringify({ type: "ws-data", id: msg.id, data: data.toString("base64") }) + "\n"
|
|
403
|
+
);
|
|
404
|
+
} catch {
|
|
405
|
+
local.destroy();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const closeSession = () => {
|
|
410
|
+
if (!wsSessions.has(msg.id)) return;
|
|
411
|
+
wsSessions.delete(msg.id);
|
|
412
|
+
if (socket && !socket.destroyed && isRegistered) {
|
|
413
|
+
try {
|
|
414
|
+
socket.write(JSON.stringify({ type: "ws-close", id: msg.id }) + "\n");
|
|
415
|
+
} catch {
|
|
416
|
+
/* control channel gone */
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
local.on("close", closeSession);
|
|
421
|
+
local.on("error", (err) => {
|
|
422
|
+
stats.errors++;
|
|
423
|
+
logError(err, "Local ws connection error");
|
|
424
|
+
closeSession();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function closeAllWsSessions() {
|
|
429
|
+
for (const [, local] of wsSessions.entries()) {
|
|
430
|
+
if (!local.destroyed) local.destroy();
|
|
431
|
+
}
|
|
432
|
+
wsSessions.clear();
|
|
433
|
+
}
|
|
434
|
+
|
|
356
435
|
function sendErrorResponse(id, status, message) {
|
|
357
436
|
if (!socket || socket.destroyed || !isRegistered) {
|
|
358
437
|
return;
|
|
@@ -374,26 +453,36 @@ function sendErrorResponse(id, status, message) {
|
|
|
374
453
|
}
|
|
375
454
|
|
|
376
455
|
// Graceful shutdown
|
|
377
|
-
function shutdown() {
|
|
456
|
+
function shutdown(exitCode = 0) {
|
|
457
|
+
if (shuttingDown) {
|
|
458
|
+
process.exit(exitCode);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
shuttingDown = true;
|
|
378
462
|
log("info", "Shutting down...");
|
|
379
|
-
|
|
463
|
+
|
|
380
464
|
if (reconnectTimer) {
|
|
381
465
|
clearTimeout(reconnectTimer);
|
|
466
|
+
reconnectTimer = null;
|
|
382
467
|
}
|
|
383
|
-
|
|
468
|
+
|
|
384
469
|
if (healthCheckTimer) {
|
|
385
470
|
clearInterval(healthCheckTimer);
|
|
471
|
+
healthCheckTimer = null;
|
|
386
472
|
}
|
|
387
|
-
|
|
473
|
+
|
|
474
|
+
closeAllWsSessions();
|
|
475
|
+
|
|
388
476
|
if (socket && !socket.destroyed) {
|
|
389
|
-
socket.
|
|
477
|
+
socket.removeAllListeners("close");
|
|
478
|
+
socket.removeAllListeners("error");
|
|
479
|
+
socket.destroy();
|
|
390
480
|
}
|
|
391
|
-
|
|
392
|
-
// Print stats
|
|
481
|
+
|
|
393
482
|
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
|
394
483
|
log("info", `Stats: ${stats.requests} requests, ${stats.errors} errors, ${stats.reconnects} reconnects, ${uptime}s uptime`);
|
|
395
|
-
|
|
396
|
-
process.exit(
|
|
484
|
+
|
|
485
|
+
process.exit(exitCode);
|
|
397
486
|
}
|
|
398
487
|
|
|
399
488
|
// Start connection
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal tunnel client for local testing.
|
|
4
|
+
* Connects to control server, receives HTTP requests, proxies to localhost:port.
|
|
5
|
+
* Usage: node scripts/tunnel/client.js --token <token> --port 3000 --ctrl 127.0.0.1:7071
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const net = require("net");
|
|
9
|
+
const http = require("http");
|
|
10
|
+
|
|
11
|
+
function parseArgs() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const out = {};
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const a = args[i];
|
|
16
|
+
if (a === "--token") out.token = args[++i];
|
|
17
|
+
else if (a === "--port") out.port = Number(args[++i]);
|
|
18
|
+
else if (a === "--ctrl") out.ctrl = args[++i];
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { token, port, ctrl } = parseArgs();
|
|
24
|
+
if (!token || !port || !ctrl) {
|
|
25
|
+
console.error("Usage: node scripts/tunnel/client.js --token <token> --port <port> --ctrl <host:port>");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const [CTRL_HOST, CTRL_PORT] = ctrl.split(":");
|
|
30
|
+
|
|
31
|
+
function log(...args) {
|
|
32
|
+
console.log(new Date().toISOString(), ...args);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const socket = net.createConnection({ host: CTRL_HOST, port: Number(CTRL_PORT) }, () => {
|
|
36
|
+
log("connected to relay ctrl");
|
|
37
|
+
socket.write(
|
|
38
|
+
JSON.stringify({ type: "register", token, targetPort: port }) + "\n"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let buf = "";
|
|
43
|
+
socket.on("data", (chunk) => {
|
|
44
|
+
buf += chunk.toString("utf8");
|
|
45
|
+
let idx;
|
|
46
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
47
|
+
const line = buf.slice(0, idx);
|
|
48
|
+
buf = buf.slice(idx + 1);
|
|
49
|
+
if (!line.trim()) continue;
|
|
50
|
+
try {
|
|
51
|
+
const msg = JSON.parse(line);
|
|
52
|
+
if (msg.type === "request") {
|
|
53
|
+
handleRequest(msg);
|
|
54
|
+
} else if (msg.type === "registered") {
|
|
55
|
+
log("registered with relay");
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log("client parse error", err.message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
socket.on("error", (err) => {
|
|
64
|
+
log("ctrl connection error", err.message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
socket.on("close", () => {
|
|
69
|
+
log("ctrl connection closed");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function handleRequest(msg) {
|
|
74
|
+
const options = {
|
|
75
|
+
hostname: "127.0.0.1",
|
|
76
|
+
port,
|
|
77
|
+
path: msg.path || "/",
|
|
78
|
+
method: msg.method || "GET",
|
|
79
|
+
headers: msg.headers || {},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const req = http.request(options, (resp) => {
|
|
83
|
+
const chunks = [];
|
|
84
|
+
resp.on("data", (d) => chunks.push(d));
|
|
85
|
+
resp.on("end", () => {
|
|
86
|
+
const body = Buffer.concat(chunks);
|
|
87
|
+
const resMsg = {
|
|
88
|
+
type: "response",
|
|
89
|
+
id: msg.id,
|
|
90
|
+
status: resp.statusCode,
|
|
91
|
+
headers: resp.headers,
|
|
92
|
+
body: body.length ? body.toString("base64") : "",
|
|
93
|
+
};
|
|
94
|
+
socket.write(JSON.stringify(resMsg) + "\n");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
req.on("error", (err) => {
|
|
99
|
+
const resMsg = {
|
|
100
|
+
type: "response",
|
|
101
|
+
id: msg.id,
|
|
102
|
+
status: 502,
|
|
103
|
+
headers: { "x-tunnel-error": err.message },
|
|
104
|
+
body: "",
|
|
105
|
+
};
|
|
106
|
+
socket.write(JSON.stringify(resMsg) + "\n");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (msg.body) {
|
|
110
|
+
req.write(Buffer.from(msg.body, "base64"));
|
|
111
|
+
}
|
|
112
|
+
req.end();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
Binary file
|