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.
- package/AGENTS.md +161 -0
- package/LICENSE +21 -0
- package/README.md +46 -47
- 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 +3 -0
- 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 +3 -3
- 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/launchDomainking.ts +64 -0
- package/docs/AGENTS.md +113 -147
- 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
|
@@ -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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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 "";
|
|
173
|
+
return "";
|
|
214
174
|
}
|
|
215
175
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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,
|
|
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(/^→/,
|
|
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 "
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.";
|