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 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, dirname } from "path";
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
- return [
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
- ].join("\n");
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
- const result = await apiRequest("GET", "/v1/tunnels");
789
- const tunnels = result.tunnels || result?.items || [];
790
- if (!tunnels || tunnels.length === 0) {
788
+ // Show stats for running tunnels only
789
+ const runningClients = findTunnelClients();
790
+ if (runningClients.length === 0) {
791
791
  restoreRawMode();
792
- return "No tunnels found.";
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[] = tunnels.map((t: any) => {
796
- const token = truncate(t.token || t.id, 12);
797
- const alias = t.alias ? `${t.alias}.x.uplink.spot` : "(no permanent URL)";
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: t.id,
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
- const stats = await apiRequest("GET", `/v1/tunnels/${choice.value}/stats`) as any;
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}.x.uplink.spot`;
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: "Create Permanent URL",
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 result = await apiRequest("GET", "/v1/tunnels");
852
- const tunnels = result.tunnels || result?.items || [];
853
- if (!tunnels || tunnels.length === 0) {
854
- restoreRawMode();
855
- return "No tunnels found. Create a tunnel first.";
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 options: SelectOption[] = tunnels.map((t: any) => {
859
- const token = truncate(t.token || t.id, 12);
860
- const alias = t.alias ? `${t.alias}.x.uplink.spot` : "(no permanent URL)";
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
- const choice = await inlineSelect("Select tunnel to set permanent URL", options, true);
868
- if (choice === null) {
869
- restoreRawMode();
870
- return "";
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
- const lines = tunnels.map((t: any) => {
932
- const token = truncate(t.token || "", 12);
933
- const port = String(t.target_port ?? t.targetPort ?? "-").padEnd(5);
934
- const connectedLocal = runningClients.some((c) => c.token === (t.token || ""));
935
- const status = connectedLocal ? "connected" : "unknown";
936
- const alias = t.alias ? `${t.alias}.x.uplink.spot` : "-";
937
- return `${token.padEnd(14)} ${port} ${status.padEnd(11)} ${alias}`;
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
- const domain = process.env.TUNNEL_DOMAIN || "t.uplink.spot";
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
- return [
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
- ].join("\n");
1670
+ );
1671
+
1672
+ return lines.join("\n");
1410
1673
  }
1411
1674
 
1412
1675
  function findTunnelClients(): Array<{ pid: number; port: number; token: string }> {