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.
Files changed (56) hide show
  1. package/AGENTS.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +45 -44
  4. package/cli/src/index.ts +5 -3
  5. package/cli/src/registrars/cloudflare.ts +148 -0
  6. package/cli/src/registrars/godaddy.ts +99 -0
  7. package/cli/src/registrars/hostinger.ts +106 -0
  8. package/cli/src/registrars/http.ts +18 -0
  9. package/cli/src/registrars/index.ts +30 -0
  10. package/cli/src/registrars/namecheap.ts +163 -0
  11. package/cli/src/registrars/secret.ts +66 -0
  12. package/cli/src/registrars/store.ts +55 -0
  13. package/cli/src/registrars/types.ts +40 -0
  14. package/cli/src/subcommands/admin.ts +17 -30
  15. package/cli/src/subcommands/db.ts +63 -57
  16. package/cli/src/subcommands/dev.ts +23 -25
  17. package/cli/src/subcommands/domains.ts +268 -0
  18. package/cli/src/subcommands/host-domains.ts +148 -0
  19. package/cli/src/subcommands/host.ts +106 -32
  20. package/cli/src/subcommands/menu/colors.ts +1 -1
  21. package/cli/src/subcommands/menu/effects/tunnel-clients.ts +87 -14
  22. package/cli/src/subcommands/menu/inline-tree-select.ts +6 -5
  23. package/cli/src/subcommands/menu/io.ts +27 -5
  24. package/cli/src/subcommands/menu/menus/domains.ts +199 -0
  25. package/cli/src/subcommands/menu/menus/hosting.ts +14 -46
  26. package/cli/src/subcommands/menu/menus/index.ts +1 -0
  27. package/cli/src/subcommands/menu/menus/tunnels.ts +25 -67
  28. package/cli/src/subcommands/menu/render.ts +2 -2
  29. package/cli/src/subcommands/menu/tests.ts +1 -1
  30. package/cli/src/subcommands/menu/tunnels.ts +10 -99
  31. package/cli/src/subcommands/menu/types.ts +8 -0
  32. package/cli/src/subcommands/menu.ts +32 -524
  33. package/cli/src/subcommands/system.ts +58 -36
  34. package/cli/src/subcommands/tunnel.ts +124 -33
  35. package/cli/src/templates/index.ts +122 -0
  36. package/cli/src/tui/App.tsx +197 -0
  37. package/cli/src/tui/AppInspector.tsx +114 -0
  38. package/cli/src/tui/HomeStatus.tsx +59 -0
  39. package/cli/src/tui/brand.tsx +20 -0
  40. package/cli/src/tui/format.ts +22 -0
  41. package/cli/src/tui/index.mts +6 -0
  42. package/cli/src/tui/liveTree.ts +40 -0
  43. package/cli/src/tui/package.json +3 -0
  44. package/cli/src/tui/runMenu.tsx +57 -0
  45. package/cli/src/tui/session.mts +382 -0
  46. package/cli/src/tui/snapshot.ts +146 -0
  47. package/cli/src/utils/analyze.ts +27 -43
  48. package/cli/src/utils/framework-output.ts +171 -0
  49. package/cli/src/utils/launchDomainking.ts +64 -0
  50. package/docs/AGENTS.md +113 -146
  51. package/docs/MENU_STRUCTURE.md +56 -288
  52. package/docs/README.md +6 -6
  53. package/package.json +18 -35
  54. package/scripts/tunnel/client-improved.js +127 -38
  55. package/scripts/tunnel/client.js +118 -0
  56. 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
- const chunks = [];
292
- let totalSize = 0;
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
- chunks.push(d);
296
- totalSize += d.length;
297
-
298
- // Check response size
299
- if (totalSize > MAX_BODY_SIZE) {
300
- req.destroy();
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
- const body = Buffer.concat(chunks);
309
- const resMsg = {
310
- type: "response",
311
- id: msg.id,
312
- status: resp.statusCode,
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.end();
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(0);
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