uplink-cli 0.1.26 → 0.1.28
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/cli/src/index.ts +2 -4
- package/cli/src/subcommands/menu/tunnels.ts +15 -2
- package/cli/src/subcommands/menu.ts +359 -96
- package/docs/AGENTS.md +139 -0
- package/docs/MENU_STRUCTURE.md +292 -0
- package/docs/OPEN_SOURCE_CLI.md +71 -0
- package/docs/PRODUCT.md +314 -0
- package/docs/guides/MANUAL.md +513 -0
- package/docs/guides/QUICKSTART.md +280 -0
- package/docs/guides/README.md +22 -0
- package/docs/guides/TESTING.md +408 -0
- package/docs/guides/TUNNEL_SETUP.md +134 -0
- package/docs/guides/USAGE.md +247 -0
- package/package.json +3 -2
- package/scripts/tunnel/client-improved.js +14 -2
package/cli/src/index.ts
CHANGED
|
@@ -7,11 +7,9 @@ import { menuCommand } from "./subcommands/menu";
|
|
|
7
7
|
import { tunnelCommand } from "./subcommands/tunnel";
|
|
8
8
|
import { signupCommand } from "./subcommands/signup";
|
|
9
9
|
import { readFileSync } from "fs";
|
|
10
|
-
import { join
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
10
|
+
import { join } from "path";
|
|
12
11
|
|
|
13
|
-
// Get version from package.json
|
|
14
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
// Get version from package.json (CommonJS build: __dirname available)
|
|
15
13
|
const pkgPath = join(__dirname, "../../package.json");
|
|
16
14
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
17
15
|
|
|
@@ -19,6 +19,7 @@ export async function createAndStartTunnel(port: number): Promise<string> {
|
|
|
19
19
|
const result = await apiRequest("POST", "/v1/tunnels", { port });
|
|
20
20
|
const url = result.url || "(no url)";
|
|
21
21
|
const token = result.token || "(no token)";
|
|
22
|
+
const alias = result.alias || null;
|
|
22
23
|
const ctrl = process.env.TUNNEL_CTRL || "tunnel.uplink.spot:7071";
|
|
23
24
|
|
|
24
25
|
const path = require("path");
|
|
@@ -40,16 +41,28 @@ export async function createAndStartTunnel(port: number): Promise<string> {
|
|
|
40
41
|
/* ignore */
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
const lines = [
|
|
44
45
|
`✓ Tunnel created and client started`,
|
|
45
46
|
``,
|
|
46
47
|
`→ Public URL ${url}`,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
if (alias) {
|
|
51
|
+
// Use aliasUrl from backend if available, otherwise construct it
|
|
52
|
+
const aliasUrl = result.aliasUrl || `https://${alias}.uplink.spot`;
|
|
53
|
+
lines.push(`→ Alias ${alias}`);
|
|
54
|
+
lines.push(`→ Alias URL ${aliasUrl}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lines.push(
|
|
47
58
|
`→ Token ${token}`,
|
|
48
59
|
`→ Local port ${port}`,
|
|
49
60
|
``,
|
|
50
61
|
`Tunnel client running in background.`,
|
|
51
62
|
`Use "Stop Tunnel" to disconnect.`,
|
|
52
|
-
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return lines.join("\n");
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
export function findTunnelClients(): Array<{ pid: number; port: number; token: string }> {
|
|
@@ -785,19 +785,31 @@ export const menuCommand = new Command("menu")
|
|
|
785
785
|
label: "View Tunnel Stats",
|
|
786
786
|
action: async () => {
|
|
787
787
|
try {
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
if (
|
|
788
|
+
// Show stats for running tunnels only
|
|
789
|
+
const runningClients = findTunnelClients();
|
|
790
|
+
if (runningClients.length === 0) {
|
|
791
791
|
restoreRawMode();
|
|
792
|
-
return "No tunnels
|
|
792
|
+
return "No active tunnels. Start a tunnel first.";
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Get alias info
|
|
796
|
+
let aliasMap: Record<number, string> = {};
|
|
797
|
+
try {
|
|
798
|
+
const aliasResult = await apiRequest("GET", "/v1/tunnels/aliases");
|
|
799
|
+
const aliases = aliasResult.aliases || [];
|
|
800
|
+
for (const a of aliases) {
|
|
801
|
+
aliasMap[a.targetPort || a.target_port] = a.alias;
|
|
802
|
+
}
|
|
803
|
+
} catch {
|
|
804
|
+
// Continue without alias info
|
|
793
805
|
}
|
|
794
806
|
|
|
795
|
-
const options: SelectOption[] =
|
|
796
|
-
const token = truncate(
|
|
797
|
-
const alias =
|
|
807
|
+
const options: SelectOption[] = runningClients.map((c) => {
|
|
808
|
+
const token = truncate(c.token, 12);
|
|
809
|
+
const alias = aliasMap[c.port] ? `${aliasMap[c.port]}.uplink.spot` : `port ${c.port}`;
|
|
798
810
|
return {
|
|
799
811
|
label: `${token} ${alias}`,
|
|
800
|
-
value:
|
|
812
|
+
value: c.token,
|
|
801
813
|
};
|
|
802
814
|
});
|
|
803
815
|
|
|
@@ -807,7 +819,16 @@ export const menuCommand = new Command("menu")
|
|
|
807
819
|
return "";
|
|
808
820
|
}
|
|
809
821
|
|
|
810
|
-
|
|
822
|
+
// Find the tunnel by token to get its ID
|
|
823
|
+
const result = await apiRequest("GET", "/v1/tunnels");
|
|
824
|
+
const tunnels = result.tunnels || [];
|
|
825
|
+
const tunnel = tunnels.find((t: any) => t.token === choice.value);
|
|
826
|
+
|
|
827
|
+
if (!tunnel) {
|
|
828
|
+
return "Tunnel not found in backend. It may have been cleaned up.";
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const stats = await apiRequest("GET", `/v1/tunnels/${tunnel.id}/stats`) as any;
|
|
811
832
|
const connected = stats.connected ? "yes" : "no";
|
|
812
833
|
const alias = stats.alias || null;
|
|
813
834
|
|
|
@@ -823,7 +844,7 @@ export const menuCommand = new Command("menu")
|
|
|
823
844
|
|
|
824
845
|
const totals = stats.totals || {};
|
|
825
846
|
const current = stats.currentRun || {};
|
|
826
|
-
const permanentUrl = `https://${alias}.
|
|
847
|
+
const permanentUrl = `https://${alias}.uplink.spot`;
|
|
827
848
|
return [
|
|
828
849
|
`Permanent URL: ${permanentUrl}`,
|
|
829
850
|
`Connected: ${connected}`,
|
|
@@ -845,96 +866,50 @@ export const menuCommand = new Command("menu")
|
|
|
845
866
|
},
|
|
846
867
|
},
|
|
847
868
|
{
|
|
848
|
-
label: "
|
|
869
|
+
label: "Active Tunnels",
|
|
849
870
|
action: async () => {
|
|
871
|
+
// Get tunnels from API with connection status
|
|
872
|
+
let connectedTunnels: Array<{ token: string; targetPort: number; url: string; alias?: string; aliasUrl?: string }> = [];
|
|
850
873
|
try {
|
|
851
|
-
const
|
|
852
|
-
const tunnels =
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
874
|
+
const tunnelsResult = await apiRequest("GET", "/v1/tunnels");
|
|
875
|
+
const tunnels = tunnelsResult.tunnels || [];
|
|
876
|
+
// Only show tunnels that are actually connected to the relay
|
|
877
|
+
connectedTunnels = tunnels.filter((t: any) => t.connected === true);
|
|
878
|
+
} catch (err: any) {
|
|
879
|
+
// If API fails, fall back to local process check
|
|
880
|
+
const runningClients = findTunnelClients();
|
|
881
|
+
if (runningClients.length === 0) {
|
|
882
|
+
return "No active tunnels. Use 'Start' to create one.";
|
|
856
883
|
}
|
|
857
|
-
|
|
858
|
-
const
|
|
859
|
-
const token = truncate(
|
|
860
|
-
const
|
|
861
|
-
return {
|
|
862
|
-
label: `${token} ${alias}`,
|
|
863
|
-
value: t.id,
|
|
864
|
-
};
|
|
884
|
+
// Show warning that we can't verify connection status
|
|
885
|
+
const lines = runningClients.map((c) => {
|
|
886
|
+
const token = truncate(c.token, 12);
|
|
887
|
+
const port = String(c.port).padEnd(5);
|
|
888
|
+
return `${token.padEnd(14)} ${port} (status unknown)`;
|
|
865
889
|
});
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
try { process.stdin.setRawMode(false); } catch { /* ignore */ }
|
|
874
|
-
const aliasName = await promptLine("Enter alias name (e.g. my-app): ");
|
|
875
|
-
restoreRawMode();
|
|
876
|
-
|
|
877
|
-
if (!aliasName.trim()) {
|
|
878
|
-
return "No alias provided.";
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
try {
|
|
882
|
-
const aliasResult = await apiRequest("POST", `/v1/tunnels/${choice.value}/alias`, {
|
|
883
|
-
alias: aliasName.trim(),
|
|
884
|
-
});
|
|
885
|
-
const permanentUrl = `https://${aliasResult.alias}.x.uplink.spot`;
|
|
886
|
-
return [
|
|
887
|
-
"✓ Permanent URL created",
|
|
888
|
-
"",
|
|
889
|
-
`→ Alias ${aliasResult.alias}`,
|
|
890
|
-
`→ URL ${permanentUrl}`,
|
|
891
|
-
"",
|
|
892
|
-
"Your tunnel will now be accessible at this permanent URL.",
|
|
893
|
-
].join("\n");
|
|
894
|
-
} catch (err: any) {
|
|
895
|
-
const msg = err?.message || String(err);
|
|
896
|
-
if (msg.includes("ALIAS_NOT_ENABLED")) {
|
|
897
|
-
return [
|
|
898
|
-
"❌ Permanent URLs are a premium feature",
|
|
899
|
-
"",
|
|
900
|
-
"Contact us on Discord at uplink.spot to upgrade your account.",
|
|
901
|
-
].join("\n");
|
|
902
|
-
}
|
|
903
|
-
if (msg.includes("ALIAS_LIMIT_REACHED")) {
|
|
904
|
-
return [
|
|
905
|
-
"❌ URL limit reached",
|
|
906
|
-
"",
|
|
907
|
-
"You've reached your URL limit. Contact us to increase it.",
|
|
908
|
-
].join("\n");
|
|
909
|
-
}
|
|
910
|
-
if (msg.includes("ALIAS_TAKEN") || msg.includes("already in use")) {
|
|
911
|
-
return `❌ Alias "${aliasName.trim()}" is already in use. Try a different name.`;
|
|
912
|
-
}
|
|
913
|
-
throw err;
|
|
914
|
-
}
|
|
915
|
-
} catch (err: any) {
|
|
916
|
-
restoreRawMode();
|
|
917
|
-
throw err;
|
|
890
|
+
return [
|
|
891
|
+
"⚠ Could not verify connection status from relay",
|
|
892
|
+
"",
|
|
893
|
+
"Token Port Status",
|
|
894
|
+
"-".repeat(40),
|
|
895
|
+
...lines,
|
|
896
|
+
].join("\n");
|
|
918
897
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
label: "My Tunnels",
|
|
923
|
-
action: async () => {
|
|
924
|
-
const runningClients = findTunnelClients();
|
|
925
|
-
const result = await apiRequest("GET", "/v1/tunnels");
|
|
926
|
-
const tunnels = result.tunnels || result?.items || [];
|
|
927
|
-
if (!tunnels || tunnels.length === 0) {
|
|
928
|
-
return "No tunnels found.";
|
|
898
|
+
|
|
899
|
+
if (connectedTunnels.length === 0) {
|
|
900
|
+
return "No active tunnels connected to relay. Use 'Start' to create one.";
|
|
929
901
|
}
|
|
930
902
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const
|
|
937
|
-
|
|
903
|
+
// Match with local processes to get port info
|
|
904
|
+
const runningClients = findTunnelClients();
|
|
905
|
+
const tokenToClient = new Map(runningClients.map(c => [c.token, c]));
|
|
906
|
+
|
|
907
|
+
const lines = connectedTunnels.map((tunnel) => {
|
|
908
|
+
const token = truncate(tunnel.token, 12);
|
|
909
|
+
const client = tokenToClient.get(tunnel.token);
|
|
910
|
+
const port = client ? String(client.port).padEnd(5) : String(tunnel.targetPort).padEnd(5);
|
|
911
|
+
const alias = tunnel.aliasUrl || (tunnel.alias ? `https://${tunnel.alias}.uplink.spot` : "-");
|
|
912
|
+
return `${token.padEnd(14)} ${port} connected ${alias}`;
|
|
938
913
|
});
|
|
939
914
|
|
|
940
915
|
return [
|
|
@@ -947,6 +922,280 @@ export const menuCommand = new Command("menu")
|
|
|
947
922
|
],
|
|
948
923
|
});
|
|
949
924
|
|
|
925
|
+
// Manage Aliases (Premium feature)
|
|
926
|
+
mainMenu.push({
|
|
927
|
+
label: "Manage Aliases",
|
|
928
|
+
subMenu: [
|
|
929
|
+
{
|
|
930
|
+
label: "My Aliases",
|
|
931
|
+
action: async () => {
|
|
932
|
+
try {
|
|
933
|
+
const result = await apiRequest("GET", "/v1/tunnels/aliases");
|
|
934
|
+
const aliases = result.aliases || [];
|
|
935
|
+
|
|
936
|
+
if (aliases.length === 0) {
|
|
937
|
+
return [
|
|
938
|
+
"No aliases configured.",
|
|
939
|
+
"",
|
|
940
|
+
"Use 'Create Alias' to set up a permanent URL for a port.",
|
|
941
|
+
].join("\n");
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const runningClients = findTunnelClients();
|
|
945
|
+
const runningPorts = new Set(runningClients.map(c => c.port));
|
|
946
|
+
|
|
947
|
+
const lines = aliases.map((a: any) => {
|
|
948
|
+
const alias = a.alias.padEnd(15);
|
|
949
|
+
const port = String(a.targetPort || a.target_port).padEnd(6);
|
|
950
|
+
const status = runningPorts.has(a.targetPort || a.target_port) ? "active" : "inactive";
|
|
951
|
+
return `${alias} ${port} ${status}`;
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
return [
|
|
955
|
+
"Alias Port Status",
|
|
956
|
+
"-".repeat(40),
|
|
957
|
+
...lines,
|
|
958
|
+
"",
|
|
959
|
+
"Active = tunnel running on that port",
|
|
960
|
+
].join("\n");
|
|
961
|
+
} catch (err: any) {
|
|
962
|
+
const msg = err?.message || String(err);
|
|
963
|
+
if (msg.includes("ALIAS_NOT_ENABLED") || msg.includes("403")) {
|
|
964
|
+
return [
|
|
965
|
+
"❌ Aliases are a premium feature",
|
|
966
|
+
"",
|
|
967
|
+
"Contact us on Discord at uplink.spot to upgrade.",
|
|
968
|
+
].join("\n");
|
|
969
|
+
}
|
|
970
|
+
throw err;
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
label: "Create Alias",
|
|
976
|
+
action: async () => {
|
|
977
|
+
let aliasName = "";
|
|
978
|
+
let port = 0;
|
|
979
|
+
try {
|
|
980
|
+
// Step 1: Select port - show running tunnels + custom option
|
|
981
|
+
const runningClients = findTunnelClients();
|
|
982
|
+
const portOptions: SelectOption[] = [];
|
|
983
|
+
|
|
984
|
+
// Add running tunnel ports
|
|
985
|
+
for (const client of runningClients) {
|
|
986
|
+
portOptions.push({
|
|
987
|
+
label: `Port ${client.port} (tunnel running)`,
|
|
988
|
+
value: client.port,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Add custom option
|
|
993
|
+
portOptions.push({ label: "Enter custom port", value: "custom" });
|
|
994
|
+
|
|
995
|
+
const portChoice = await inlineSelect("Select port to create alias for", portOptions, true);
|
|
996
|
+
if (portChoice === null) {
|
|
997
|
+
restoreRawMode();
|
|
998
|
+
return "";
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (portChoice.value === "custom") {
|
|
1002
|
+
try { process.stdin.setRawMode(false); } catch { /* ignore */ }
|
|
1003
|
+
const portStr = await promptLine("Enter port number (e.g. 3000): ");
|
|
1004
|
+
port = Number(portStr);
|
|
1005
|
+
if (!port || port < 1 || port > 65535) {
|
|
1006
|
+
restoreRawMode();
|
|
1007
|
+
return "Invalid port number.";
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
port = portChoice.value as number;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Step 2: Enter alias name
|
|
1014
|
+
try { process.stdin.setRawMode(false); } catch { /* ignore */ }
|
|
1015
|
+
aliasName = await promptLine("Enter alias name (e.g. my-app): ");
|
|
1016
|
+
restoreRawMode();
|
|
1017
|
+
|
|
1018
|
+
if (!aliasName.trim()) {
|
|
1019
|
+
return "No alias provided.";
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const result = await apiRequest("POST", "/v1/tunnels/aliases", {
|
|
1023
|
+
alias: aliasName.trim(),
|
|
1024
|
+
port,
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
const tunnelRunning = runningClients.some(c => c.port === port);
|
|
1028
|
+
const statusMsg = tunnelRunning
|
|
1029
|
+
? "Alias is now active!"
|
|
1030
|
+
: "Start a tunnel on this port to make it accessible.";
|
|
1031
|
+
|
|
1032
|
+
return [
|
|
1033
|
+
"✓ Alias created",
|
|
1034
|
+
"",
|
|
1035
|
+
`→ Alias ${result.alias}`,
|
|
1036
|
+
`→ Port ${result.targetPort}`,
|
|
1037
|
+
`→ URL ${result.url}`,
|
|
1038
|
+
"",
|
|
1039
|
+
statusMsg,
|
|
1040
|
+
].join("\n");
|
|
1041
|
+
} catch (err: any) {
|
|
1042
|
+
restoreRawMode();
|
|
1043
|
+
const msg = err?.message || String(err);
|
|
1044
|
+
if (msg.includes("ALIAS_NOT_ENABLED")) {
|
|
1045
|
+
return [
|
|
1046
|
+
"❌ Aliases are a premium feature",
|
|
1047
|
+
"",
|
|
1048
|
+
"Contact us on Discord at uplink.spot to upgrade.",
|
|
1049
|
+
].join("\n");
|
|
1050
|
+
}
|
|
1051
|
+
if (msg.includes("ALIAS_LIMIT_REACHED")) {
|
|
1052
|
+
return [
|
|
1053
|
+
"❌ Alias limit reached",
|
|
1054
|
+
"",
|
|
1055
|
+
"You've reached your alias limit. Contact us to increase it.",
|
|
1056
|
+
].join("\n");
|
|
1057
|
+
}
|
|
1058
|
+
if (msg.includes("ALIAS_TAKEN")) {
|
|
1059
|
+
return `❌ Alias "${aliasName.trim()}" is already taken. Try a different name.`;
|
|
1060
|
+
}
|
|
1061
|
+
if (msg.includes("PORT_HAS_ALIAS")) {
|
|
1062
|
+
return `❌ Port ${port} already has an alias. Use 'Reassign Alias' to change it.`;
|
|
1063
|
+
}
|
|
1064
|
+
throw err;
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
label: "Reassign Alias",
|
|
1070
|
+
action: async () => {
|
|
1071
|
+
try {
|
|
1072
|
+
const result = await apiRequest("GET", "/v1/tunnels/aliases");
|
|
1073
|
+
const aliases = result.aliases || [];
|
|
1074
|
+
|
|
1075
|
+
if (aliases.length === 0) {
|
|
1076
|
+
restoreRawMode();
|
|
1077
|
+
return "No aliases to reassign. Create one first.";
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Step 1: Select which alias to reassign
|
|
1081
|
+
const aliasOptions: SelectOption[] = aliases.map((a: any) => ({
|
|
1082
|
+
label: `${a.alias} → port ${a.targetPort || a.target_port}`,
|
|
1083
|
+
value: a.alias,
|
|
1084
|
+
}));
|
|
1085
|
+
|
|
1086
|
+
const aliasChoice = await inlineSelect("Select alias to reassign", aliasOptions, true);
|
|
1087
|
+
if (aliasChoice === null) {
|
|
1088
|
+
restoreRawMode();
|
|
1089
|
+
return "";
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const selectedAlias = aliases.find((a: any) => a.alias === aliasChoice.value);
|
|
1093
|
+
const currentPort = selectedAlias?.targetPort || selectedAlias?.target_port;
|
|
1094
|
+
|
|
1095
|
+
// Step 2: Show available ports (running tunnels + custom option)
|
|
1096
|
+
const runningClients = findTunnelClients();
|
|
1097
|
+
const portOptions: SelectOption[] = [];
|
|
1098
|
+
|
|
1099
|
+
// Add running tunnel ports (excluding current port)
|
|
1100
|
+
for (const client of runningClients) {
|
|
1101
|
+
if (client.port !== currentPort) {
|
|
1102
|
+
portOptions.push({
|
|
1103
|
+
label: `Port ${client.port} (tunnel running)`,
|
|
1104
|
+
value: client.port,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Add current port indicator if tunnel is running
|
|
1110
|
+
const currentRunning = runningClients.find(c => c.port === currentPort);
|
|
1111
|
+
if (currentRunning) {
|
|
1112
|
+
portOptions.unshift({
|
|
1113
|
+
label: `Port ${currentPort} (current, tunnel running)`,
|
|
1114
|
+
value: `current-${currentPort}`,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Add custom option
|
|
1119
|
+
portOptions.push({ label: "Enter custom port", value: "custom" });
|
|
1120
|
+
|
|
1121
|
+
const portChoice = await inlineSelect("Select new port for alias", portOptions, true);
|
|
1122
|
+
if (portChoice === null) {
|
|
1123
|
+
restoreRawMode();
|
|
1124
|
+
return "";
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
let port: number;
|
|
1128
|
+
if (portChoice.value === "custom") {
|
|
1129
|
+
try { process.stdin.setRawMode(false); } catch { /* ignore */ }
|
|
1130
|
+
const portStr = await promptLine("Enter new port number: ");
|
|
1131
|
+
restoreRawMode();
|
|
1132
|
+
port = Number(portStr);
|
|
1133
|
+
if (!port || port < 1 || port > 65535) {
|
|
1134
|
+
return "Invalid port number.";
|
|
1135
|
+
}
|
|
1136
|
+
} else if (typeof portChoice.value === "string" && portChoice.value.startsWith("current-")) {
|
|
1137
|
+
restoreRawMode();
|
|
1138
|
+
return "Alias is already assigned to this port.";
|
|
1139
|
+
} else {
|
|
1140
|
+
port = portChoice.value as number;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
restoreRawMode();
|
|
1144
|
+
|
|
1145
|
+
const updateResult = await apiRequest("PUT", `/v1/tunnels/aliases/${aliasChoice.value}`, { port });
|
|
1146
|
+
|
|
1147
|
+
return [
|
|
1148
|
+
"✓ Alias reassigned",
|
|
1149
|
+
"",
|
|
1150
|
+
`→ Alias ${updateResult.alias}`,
|
|
1151
|
+
`→ Port ${updateResult.targetPort}`,
|
|
1152
|
+
`→ URL ${updateResult.url}`,
|
|
1153
|
+
].join("\n");
|
|
1154
|
+
} catch (err: any) {
|
|
1155
|
+
restoreRawMode();
|
|
1156
|
+
const msg = err?.message || String(err);
|
|
1157
|
+
if (msg.includes("PORT_HAS_ALIAS")) {
|
|
1158
|
+
return "❌ That port already has an alias assigned.";
|
|
1159
|
+
}
|
|
1160
|
+
throw err;
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
label: "Delete Alias",
|
|
1166
|
+
action: async () => {
|
|
1167
|
+
try {
|
|
1168
|
+
const result = await apiRequest("GET", "/v1/tunnels/aliases");
|
|
1169
|
+
const aliases = result.aliases || [];
|
|
1170
|
+
|
|
1171
|
+
if (aliases.length === 0) {
|
|
1172
|
+
restoreRawMode();
|
|
1173
|
+
return "No aliases to delete.";
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const options: SelectOption[] = aliases.map((a: any) => ({
|
|
1177
|
+
label: `${a.alias} → port ${a.targetPort || a.target_port}`,
|
|
1178
|
+
value: a.alias,
|
|
1179
|
+
}));
|
|
1180
|
+
|
|
1181
|
+
const choice = await inlineSelect("Select alias to delete", options, true);
|
|
1182
|
+
if (choice === null) {
|
|
1183
|
+
restoreRawMode();
|
|
1184
|
+
return "";
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
await apiRequest("DELETE", `/v1/tunnels/aliases/${choice.value}`);
|
|
1188
|
+
|
|
1189
|
+
return `✓ Alias "${choice.value}" deleted.`;
|
|
1190
|
+
} catch (err: any) {
|
|
1191
|
+
restoreRawMode();
|
|
1192
|
+
throw err;
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
},
|
|
1196
|
+
],
|
|
1197
|
+
});
|
|
1198
|
+
|
|
950
1199
|
// Admin-only: Usage section
|
|
951
1200
|
if (isAdmin) {
|
|
952
1201
|
mainMenu.push({
|
|
@@ -1128,7 +1377,8 @@ export const menuCommand = new Command("menu")
|
|
|
1128
1377
|
if (clients.length === 0) {
|
|
1129
1378
|
cachedActiveTunnels = "";
|
|
1130
1379
|
} else {
|
|
1131
|
-
|
|
1380
|
+
// Default domain should be the current production domain; allow override via env.
|
|
1381
|
+
const domain = process.env.TUNNEL_DOMAIN || "x.uplink.spot";
|
|
1132
1382
|
const scheme = (process.env.TUNNEL_URL_SCHEME || "https").toLowerCase();
|
|
1133
1383
|
|
|
1134
1384
|
const tunnelLines = clients.map((client, idx) => {
|
|
@@ -1374,6 +1624,7 @@ async function createAndStartTunnel(port: number): Promise<string> {
|
|
|
1374
1624
|
const result = await apiRequest("POST", "/v1/tunnels", { port });
|
|
1375
1625
|
const url = result.url || "(no url)";
|
|
1376
1626
|
const token = result.token || "(no token)";
|
|
1627
|
+
const alias = result.alias || null;
|
|
1377
1628
|
const ctrl = process.env.TUNNEL_CTRL || "tunnel.uplink.spot:7071";
|
|
1378
1629
|
|
|
1379
1630
|
// Start tunnel client in background
|
|
@@ -1397,16 +1648,28 @@ async function createAndStartTunnel(port: number): Promise<string> {
|
|
|
1397
1648
|
/* ignore */
|
|
1398
1649
|
}
|
|
1399
1650
|
|
|
1400
|
-
|
|
1651
|
+
const lines = [
|
|
1401
1652
|
`✓ Tunnel created and client started`,
|
|
1402
1653
|
``,
|
|
1403
1654
|
`→ Public URL ${url}`,
|
|
1655
|
+
];
|
|
1656
|
+
|
|
1657
|
+
if (alias) {
|
|
1658
|
+
// Use aliasUrl from backend if available, otherwise construct it
|
|
1659
|
+
const aliasUrl = result.aliasUrl || `https://${alias}.uplink.spot`;
|
|
1660
|
+
lines.push(`→ Alias ${alias}`);
|
|
1661
|
+
lines.push(`→ Alias URL ${aliasUrl}`);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
lines.push(
|
|
1404
1665
|
`→ Token ${token}`,
|
|
1405
1666
|
`→ Local port ${port}`,
|
|
1406
1667
|
``,
|
|
1407
1668
|
`Tunnel client running in background.`,
|
|
1408
1669
|
`Use "Stop Tunnel" to disconnect.`,
|
|
1409
|
-
|
|
1670
|
+
);
|
|
1671
|
+
|
|
1672
|
+
return lines.join("\n");
|
|
1410
1673
|
}
|
|
1411
1674
|
|
|
1412
1675
|
function findTunnelClients(): Array<{ pid: number; port: number; token: string }> {
|