uplink-cli 0.1.38 → 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 (54) hide show
  1. package/AGENTS.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +46 -47
  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 +3 -0
  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 +3 -3
  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/launchDomainking.ts +64 -0
  48. package/docs/AGENTS.md +113 -147
  49. package/docs/MENU_STRUCTURE.md +56 -288
  50. package/docs/README.md +6 -6
  51. package/package.json +18 -35
  52. package/scripts/tunnel/client-improved.js +127 -38
  53. package/scripts/tunnel/client.js +118 -0
  54. package/assets/cli-screenshot.png +0 -0
@@ -16,8 +16,10 @@ type Deps = {
16
16
  scanCommonPorts: () => Promise<number[]>;
17
17
  findTunnelClients: () => Array<{ pid: number; port: number; token: string }>;
18
18
  createAndStartTunnel: (port: number) => Promise<string>;
19
- killTunnelClient: (pid: number) => boolean;
20
- killAllTunnelClients: (clients: Array<{ pid: number; port: number; token: string }>) => number;
19
+ stopTunnelClients: (
20
+ clients: Array<{ pid: number; port: number; token: string }>,
21
+ opts?: { connectedGhosts?: boolean }
22
+ ) => Promise<{ killed: number; deleted: number }>;
21
23
  colorDim: (text: string) => string;
22
24
  colorRed: (text: string) => string;
23
25
  };
@@ -33,8 +35,7 @@ export function buildManageTunnelsMenu(deps: Deps): MenuChoice {
33
35
  scanCommonPorts,
34
36
  findTunnelClients,
35
37
  createAndStartTunnel,
36
- killTunnelClient,
37
- killAllTunnelClients,
38
+ stopTunnelClients,
38
39
  colorDim,
39
40
  colorRed,
40
41
  } = deps;
@@ -43,7 +44,7 @@ export function buildManageTunnelsMenu(deps: Deps): MenuChoice {
43
44
  label: "Share",
44
45
  subMenu: [
45
46
  {
46
- label: "Start (Auto)",
47
+ label: "Start tunnel",
47
48
  action: async () => {
48
49
  try {
49
50
  // Scan for active ports
@@ -142,65 +143,25 @@ export function buildManageTunnelsMenu(deps: Deps): MenuChoice {
142
143
  },
143
144
  },
144
145
  {
145
- label: "Start (Manual)",
146
+ label: "Stop tunnel",
146
147
  action: async () => {
147
- const answer = await promptLine('Local port to expose (default 3000, or "back"): ');
148
- if (isBackInput(answer)) {
149
- restoreRawMode();
150
- return "";
151
- }
152
- const port = Number(answer) || 3000;
153
148
  try {
154
- const result = await apiRequest("POST", "/v1/tunnels", { port });
155
- try {
156
- process.stdin.setRawMode(true);
157
- process.stdin.resume();
158
- } catch {
159
- /* ignore */
160
- }
161
- const url = result.url || "(no url)";
162
- const token = result.token || "(no token)";
163
- const httpFallback = typeof url === "string" && url.startsWith("https://") ? url.replace(/^https:\/\//, "http://") : "";
164
- return [
165
- `Created tunnel: ${url}`,
166
- httpFallback && url !== httpFallback ? `HTTP fallback: ${httpFallback}` : "",
167
- `Token: ${token}`,
168
- "",
169
- "To start the tunnel client, run:",
170
- ` node scripts/tunnel/client-improved.js --token ${token} --port ${port} --ctrl ${process.env.TUNNEL_CTRL || "tunnel.uplink.spot:7071"}`,
171
- ]
172
- .filter(Boolean)
173
- .join("\n");
174
- } catch (err: any) {
175
- try {
176
- process.stdin.setRawMode(true);
177
- process.stdin.resume();
178
- } catch {
179
- /* ignore */
180
- }
181
- throw err;
182
- }
183
- },
184
- },
185
- {
186
- label: "Stop Tunnel",
187
- action: async () => {
188
- try {
189
- // Find running tunnel client processes
190
149
  const processes = findTunnelClients();
191
150
 
192
151
  if (processes.length === 0) {
152
+ const ghost = await stopTunnelClients([], { connectedGhosts: true });
193
153
  restoreRawMode();
154
+ if (ghost.deleted > 0) {
155
+ return `✓ Removed ${ghost.deleted} relay-connected tunnel${ghost.deleted !== 1 ? "s" : ""} with no local client`;
156
+ }
194
157
  return "No running tunnel clients found.";
195
158
  }
196
159
 
197
- // Build options from running tunnels
198
160
  const options: SelectOption[] = processes.map((p) => ({
199
161
  label: `Port ${p.port} ${colorDim(`(${truncate(p.token, 8)})`)}`,
200
162
  value: p.pid,
201
163
  }));
202
164
 
203
- // Add "Stop all" option if more than one tunnel
204
165
  if (processes.length > 1) {
205
166
  options.push({ label: colorRed("Stop all tunnels"), value: "all" });
206
167
  }
@@ -208,28 +169,25 @@ export function buildManageTunnelsMenu(deps: Deps): MenuChoice {
208
169
  const result = await inlineSelect("Select tunnel to stop", options, true);
209
170
 
210
171
  if (result === null) {
211
- // User selected Back
212
172
  restoreRawMode();
213
- return ""; // Return empty to go back without message
173
+ return "";
214
174
  }
215
175
 
216
- let killed = 0;
217
- if (result.value === "all") {
218
- // Kill all
219
- killed = killAllTunnelClients(processes);
220
- } else {
221
- // Kill specific client
222
- const pid = result.value as number;
223
- const ok = killTunnelClient(pid);
224
- if (!ok) {
225
- restoreRawMode();
226
- throw new Error(`Failed to kill process ${pid}`);
227
- }
228
- killed = 1;
229
- }
176
+ const selected =
177
+ result.value === "all"
178
+ ? processes
179
+ : processes.filter((p) => {
180
+ const pid = result.value as number;
181
+ const chosen = processes.find((c) => c.pid === pid);
182
+ return p.pid === pid || (chosen && p.token === chosen.token);
183
+ });
230
184
 
185
+ const { killed, deleted } = await stopTunnelClients(selected);
231
186
  restoreRawMode();
232
- return `✓ Stopped ${killed} tunnel client${killed !== 1 ? "s" : ""}`;
187
+ if (killed === 0 && deleted === 0) {
188
+ throw new Error("Failed to stop tunnel");
189
+ }
190
+ return `✓ Stopped ${killed} local client${killed !== 1 ? "s" : ""}, removed ${deleted} tunnel record${deleted !== 1 ? "s" : ""}`;
233
191
  } catch (err: any) {
234
192
  restoreRawMode();
235
193
  throw err;
@@ -1,5 +1,5 @@
1
1
  import { clearScreen } from "./io";
2
- import { colorBold, colorCyan, colorDim, colorGreen, colorRed } from "./colors";
2
+ import { colorBold, colorDim, colorGreen, colorRed } from "./colors";
3
3
  import { DEFAULT_MENU_MESSAGE, type MenuChoice } from "./types";
4
4
 
5
5
  export type RenderArgs = {
@@ -107,7 +107,7 @@ export function renderMenu(args: RenderArgs) {
107
107
  const styledLine = line
108
108
  .replace(/^✓/, colorGreen("✓"))
109
109
  .replace(/^✗/, colorRed("✗"))
110
- .replace(/^→/, colorCyan("→"));
110
+ .replace(/^→/, colorDim("→"));
111
111
  console.log(styledLine);
112
112
  });
113
113
  }
@@ -1,6 +1,6 @@
1
1
  import fetch from "node-fetch";
2
2
  import { spawn } from "child_process";
3
- import { resolveProjectRoot } from "../../../utils/project-root";
3
+ import { resolveProjectRoot } from "../../utils/project-root";
4
4
  import { getResolvedApiBase, getResolvedApiToken } from "../../utils/api-base";
5
5
 
6
6
  export function runSmoke(script: "smoke:tunnel" | "smoke:db" | "smoke:all" | "test:comprehensive") {
@@ -1,99 +1,10 @@
1
- import { spawn, execSync } from "child_process";
2
- import { join } from "path";
3
- import { apiRequest } from "../../http";
4
- import { resolveProjectRoot } from "../../../utils/project-root";
5
-
6
- export async function createAndStartTunnel(port: number): Promise<string> {
7
- // Check if tunnel already running on this port
8
- const existing = findTunnelClients().filter(c => c.port === port);
9
- if (existing.length > 0) {
10
- return [
11
- `⚠ Tunnel already running on port ${port}`,
12
- ``,
13
- `→ PID: ${existing[0].pid}`,
14
- `→ Token: ${existing[0].token.substring(0, 8)}...`,
15
- ``,
16
- `Use "Stop Tunnel" first to disconnect the existing tunnel.`,
17
- ].join("\n");
18
- }
19
-
20
- const result = await apiRequest("POST", "/v1/tunnels", { port });
21
- const url = result.url || "(no url)";
22
- const token = result.token || "(no token)";
23
- const alias = result.alias || null;
24
- const ctrl = process.env.TUNNEL_CTRL || "tunnel.uplink.spot:7071";
25
-
26
- const path = require("path");
27
- const projectRoot = resolveProjectRoot(__dirname);
28
- const clientPath = path.join(projectRoot, "scripts/tunnel/client-improved.js");
29
- const clientProcess = spawn("node", [clientPath, "--token", token, "--port", String(port), "--ctrl", ctrl], {
30
- stdio: "ignore",
31
- detached: true,
32
- cwd: projectRoot,
33
- });
34
- clientProcess.unref();
35
-
36
- await new Promise((resolve) => setTimeout(resolve, 2000));
37
-
38
- try {
39
- process.stdin.setRawMode(true);
40
- process.stdin.resume();
41
- } catch {
42
- /* ignore */
43
- }
44
-
45
- const lines = [
46
- `✓ Tunnel created and client started`,
47
- ``,
48
- `→ Public URL ${url}`,
49
- ];
50
-
51
- if (alias) {
52
- // Use aliasUrl from backend if available, otherwise construct it
53
- const aliasUrl = result.aliasUrl || `https://${alias}.uplink.spot`;
54
- lines.push(`→ Alias ${alias}`);
55
- lines.push(`→ Alias URL ${aliasUrl}`);
56
- }
57
-
58
- lines.push(
59
- `→ Token ${token}`,
60
- `→ Local port ${port}`,
61
- ``,
62
- `Tunnel client running in background.`,
63
- `Use "Stop Tunnel" to disconnect.`,
64
- );
65
-
66
- return lines.join("\n");
67
- }
68
-
69
- export function findTunnelClients(): Array<{ pid: number; port: number; token: string }> {
70
- try {
71
- const user = process.env.USER || "";
72
- const psCmd = user ? `ps -u ${user} -o pid=,command=` : "ps -eo pid=,command=";
73
- const output = execSync(psCmd, { encoding: "utf-8" });
74
- const lines = output
75
- .trim()
76
- .split("\n")
77
- .filter((line) => line.includes("scripts/tunnel/client-improved.js"));
78
-
79
- const clients: Array<{ pid: number; port: number; token: string }> = [];
80
-
81
- for (const line of lines) {
82
- const pidMatch = line.match(/^\s*(\d+)/);
83
- const tokenMatch = line.match(/--token\s+(\S+)/);
84
- const portMatch = line.match(/--port\s+(\d+)/);
85
-
86
- if (pidMatch && tokenMatch && portMatch) {
87
- clients.push({
88
- pid: parseInt(pidMatch[1], 10),
89
- port: parseInt(portMatch[1], 10),
90
- token: tokenMatch[1],
91
- });
92
- }
93
- }
94
-
95
- return clients;
96
- } catch {
97
- return [];
98
- }
99
- }
1
+ /** @deprecated Prefer `./effects/tunnel-clients` kept as a thin re-export. */
2
+ export {
3
+ createAndStartTunnel,
4
+ findTunnelClients,
5
+ killAllTunnelClients,
6
+ killTunnelClient,
7
+ startTunnelClient,
8
+ stopTunnelClients,
9
+ resolveTunnelClientPath,
10
+ } from "./effects/tunnel-clients";
@@ -1,7 +1,15 @@
1
+ export type MenuInspect = {
2
+ kind: "app";
3
+ id: string;
4
+ url?: string;
5
+ createdAt?: string;
6
+ };
7
+
1
8
  export type MenuChoice = {
2
9
  label: string;
3
10
  action?: () => Promise<string>;
4
11
  subMenu?: MenuChoice[];
12
+ inspect?: MenuInspect;
5
13
  };
6
14
 
7
15
  export const DEFAULT_MENU_MESSAGE = "Use ↑/↓ and Enter. ← to go back. Ctrl+C to quit.";