uplink-cli 0.1.0-alpha.3 → 0.1.0-alpha.5
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/http.ts +3 -3
- package/cli/src/subcommands/menu.ts +207 -28
- package/package.json +3 -1
package/cli/src/http.ts
CHANGED
|
@@ -18,13 +18,13 @@ function getApiToken(apiBase: string): string | undefined {
|
|
|
18
18
|
return process.env.AGENTCLOUD_TOKEN || undefined;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// Local dev
|
|
21
|
+
// Local dev:
|
|
22
22
|
// - Prefer AGENTCLOUD_TOKEN if set
|
|
23
|
-
// - Otherwise allow AGENTCLOUD_TOKEN_DEV
|
|
23
|
+
// - Otherwise allow AGENTCLOUD_TOKEN_DEV (no hardcoded default for security)
|
|
24
24
|
return (
|
|
25
25
|
process.env.AGENTCLOUD_TOKEN ||
|
|
26
26
|
process.env.AGENTCLOUD_TOKEN_DEV ||
|
|
27
|
-
|
|
27
|
+
undefined
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -79,12 +79,20 @@ function colorRed(text: string) {
|
|
|
79
79
|
return `${c.red}${text}${c.reset}`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
const TOKEN_DOMAIN = process.env.TUNNEL_DOMAIN || "x.uplink.spot";
|
|
83
|
+
const ALIAS_DOMAIN = process.env.ALIAS_DOMAIN || "uplink.spot";
|
|
84
|
+
const URL_SCHEME = (process.env.TUNNEL_URL_SCHEME || "https").toLowerCase();
|
|
85
|
+
|
|
82
86
|
function colorMagenta(text: string) {
|
|
83
87
|
return `${c.magenta}${text}${c.reset}`;
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
function colorWhite(text: string) {
|
|
91
|
+
return `${c.brightWhite}${text}${c.reset}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
// ASCII banner with color styling
|
|
87
|
-
const ASCII_UPLINK =
|
|
95
|
+
const ASCII_UPLINK = colorWhite([
|
|
88
96
|
"██╗ ██╗██████╗ ██╗ ██╗███╗ ██╗██╗ ██╗",
|
|
89
97
|
"██║ ██║██╔══██╗██║ ██║████╗ ██║██║ ██╔╝",
|
|
90
98
|
"██║ ██║██████╔╝██║ ██║██╔██╗ ██║█████╔╝ ",
|
|
@@ -141,6 +149,7 @@ async function inlineSelect(
|
|
|
141
149
|
let branchColor: string;
|
|
142
150
|
|
|
143
151
|
if (isSelected) {
|
|
152
|
+
// Selected: cyan highlight
|
|
144
153
|
branchColor = colorCyan(branch);
|
|
145
154
|
if (opt.label === "Back") {
|
|
146
155
|
label = colorDim(opt.label);
|
|
@@ -148,11 +157,12 @@ async function inlineSelect(
|
|
|
148
157
|
label = colorCyan(opt.label);
|
|
149
158
|
}
|
|
150
159
|
} else {
|
|
151
|
-
|
|
160
|
+
// Not selected: white
|
|
161
|
+
branchColor = colorWhite(branch);
|
|
152
162
|
if (opt.label === "Back") {
|
|
153
163
|
label = colorDim(opt.label);
|
|
154
164
|
} else {
|
|
155
|
-
label = opt.label;
|
|
165
|
+
label = colorWhite(opt.label);
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
|
|
@@ -167,8 +177,8 @@ async function inlineSelect(
|
|
|
167
177
|
allOptions.forEach((opt, idx) => {
|
|
168
178
|
const isLast = idx === allOptions.length - 1;
|
|
169
179
|
const branch = isLast ? "└─" : "├─";
|
|
170
|
-
const branchColor = idx === 0 ? colorCyan(branch) :
|
|
171
|
-
const label = idx === 0 ? colorCyan(opt.label) : (opt.label === "Back" ? colorDim(opt.label) : opt.label);
|
|
180
|
+
const branchColor = idx === 0 ? colorCyan(branch) : colorWhite(branch);
|
|
181
|
+
const label = idx === 0 ? colorCyan(opt.label) : (opt.label === "Back" ? colorDim(opt.label) : colorWhite(opt.label));
|
|
172
182
|
console.log(`${branchColor} ${label}`);
|
|
173
183
|
});
|
|
174
184
|
|
|
@@ -652,6 +662,100 @@ export const menuCommand = new Command("menu")
|
|
|
652
662
|
}
|
|
653
663
|
},
|
|
654
664
|
},
|
|
665
|
+
{
|
|
666
|
+
label: "Set Permanent Alias",
|
|
667
|
+
action: async () => {
|
|
668
|
+
const data = await apiRequest("GET", "/v1/tunnels");
|
|
669
|
+
const tunnels = data.tunnels || [];
|
|
670
|
+
if (!tunnels.length) return "No tunnels found.";
|
|
671
|
+
|
|
672
|
+
const options: SelectOption[] = tunnels.map((t: any) => {
|
|
673
|
+
const token = truncate(t.token || "", 10);
|
|
674
|
+
const alias = t.alias ? colorGreen(t.alias) : colorDim("none");
|
|
675
|
+
const port = t.target_port ?? t.targetPort ?? "-";
|
|
676
|
+
return {
|
|
677
|
+
label: `${token.padEnd(12)} port ${String(port).padEnd(5)} alias ${alias}`,
|
|
678
|
+
value: t.id,
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const choice = await inlineSelect("Select tunnel for alias", options, true);
|
|
683
|
+
if (choice === null) return "";
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
process.stdin.setRawMode(false);
|
|
687
|
+
} catch {
|
|
688
|
+
/* ignore */
|
|
689
|
+
}
|
|
690
|
+
const aliasInput = await promptLine("Enter alias (e.g. thomas): ");
|
|
691
|
+
restoreRawMode();
|
|
692
|
+
const alias = aliasInput.trim();
|
|
693
|
+
if (!alias) return "Alias not set (empty).";
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
const result = await apiRequest("POST", `/v1/tunnels/${choice.value}/alias`, {
|
|
697
|
+
alias,
|
|
698
|
+
});
|
|
699
|
+
const aliasUrl = result.aliasUrl || `${URL_SCHEME}://${alias}.${ALIAS_DOMAIN}`;
|
|
700
|
+
const tokenUrl = result.url || `${URL_SCHEME}://${result.token}.${TOKEN_DOMAIN}`;
|
|
701
|
+
return [
|
|
702
|
+
"✓ Alias updated",
|
|
703
|
+
`→ Alias URL ${aliasUrl}`,
|
|
704
|
+
`→ Token URL ${tokenUrl}`,
|
|
705
|
+
].join("\n");
|
|
706
|
+
} catch (err: any) {
|
|
707
|
+
const errMsg = err?.message || String(err);
|
|
708
|
+
// Check for premium feature errors
|
|
709
|
+
if (errMsg.includes("ALIAS_NOT_ENABLED")) {
|
|
710
|
+
try {
|
|
711
|
+
const parsed = JSON.parse(errMsg);
|
|
712
|
+
const userId = parsed?.error?.details?.user_id || "(check your token)";
|
|
713
|
+
return [
|
|
714
|
+
"",
|
|
715
|
+
colorYellow("🔒 Permanent Aliases - Premium Feature"),
|
|
716
|
+
"",
|
|
717
|
+
"Permanent aliases give you stable URLs like:",
|
|
718
|
+
` ${colorGreen(`https://myapp.${ALIAS_DOMAIN}`)}`,
|
|
719
|
+
"",
|
|
720
|
+
"Instead of regenerating tokens each time.",
|
|
721
|
+
"",
|
|
722
|
+
"To unlock this feature:",
|
|
723
|
+
` → Join our Discord: ${colorCyan("https://uplink.spot")}`,
|
|
724
|
+
` → Share your user ID: ${colorDim(userId)}`,
|
|
725
|
+
"",
|
|
726
|
+
"We'll enable it for your account!",
|
|
727
|
+
"",
|
|
728
|
+
].join("\n");
|
|
729
|
+
} catch {
|
|
730
|
+
return colorYellow("🔒 Aliases are a premium feature. Contact us at uplink.spot to upgrade.");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (errMsg.includes("ALIAS_LIMIT_REACHED")) {
|
|
734
|
+
return colorYellow("⚠️ You've reached your alias limit. Contact us to increase it.");
|
|
735
|
+
}
|
|
736
|
+
throw err; // Re-throw other errors
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
label: "Remove Alias",
|
|
742
|
+
action: async () => {
|
|
743
|
+
const data = await apiRequest("GET", "/v1/tunnels");
|
|
744
|
+
const tunnels = (data.tunnels || []).filter((t: any) => !!t.alias);
|
|
745
|
+
if (!tunnels.length) return "No tunnels with aliases.";
|
|
746
|
+
|
|
747
|
+
const options: SelectOption[] = tunnels.map((t: any) => ({
|
|
748
|
+
label: `${truncate(t.token || "", 10).padEnd(12)} alias ${colorGreen(t.alias)}`,
|
|
749
|
+
value: t.id,
|
|
750
|
+
}));
|
|
751
|
+
|
|
752
|
+
const choice = await inlineSelect("Select tunnel to remove alias", options, true);
|
|
753
|
+
if (choice === null) return "";
|
|
754
|
+
|
|
755
|
+
await apiRequest("DELETE", `/v1/tunnels/${choice.value}/alias`);
|
|
756
|
+
return "✓ Alias removed";
|
|
757
|
+
},
|
|
758
|
+
},
|
|
655
759
|
{
|
|
656
760
|
label: "View Connected (with IPs)",
|
|
657
761
|
action: async () => {
|
|
@@ -705,27 +809,38 @@ export const menuCommand = new Command("menu")
|
|
|
705
809
|
if (!tunnels || tunnels.length === 0) {
|
|
706
810
|
return "No tunnels found.";
|
|
707
811
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
812
|
+
const lines = tunnels.map((t: any) => {
|
|
813
|
+
const token = t.token || "";
|
|
814
|
+
const alias = t.alias || "-";
|
|
815
|
+
const tokenUrl = t.url || `${URL_SCHEME}://${token}.${TOKEN_DOMAIN}`;
|
|
816
|
+
const aliasUrl =
|
|
817
|
+
t.aliasUrl || (t.alias ? `${URL_SCHEME}://${t.alias}.${ALIAS_DOMAIN}` : "-");
|
|
818
|
+
const connectedFromApi = t.connected ?? false;
|
|
819
|
+
const connectedLocal = runningClients.some((c) => c.token === token);
|
|
820
|
+
const connectionStatus = isAdmin
|
|
821
|
+
? connectedFromApi
|
|
822
|
+
? "connected"
|
|
823
|
+
: "disconnected"
|
|
824
|
+
: connectedLocal
|
|
825
|
+
? "connected"
|
|
826
|
+
: "unknown";
|
|
827
|
+
|
|
828
|
+
return [
|
|
829
|
+
`${truncate(t.id, 12)} ${truncate(token, 10).padEnd(12)} ${String(
|
|
719
830
|
t.target_port ?? t.targetPort ?? "-"
|
|
720
831
|
).padEnd(5)} ${connectionStatus.padEnd(12)} ${truncate(
|
|
721
832
|
t.created_at ?? t.createdAt ?? "",
|
|
722
833
|
19
|
|
723
|
-
)}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
834
|
+
)}`,
|
|
835
|
+
` url: ${tokenUrl}`,
|
|
836
|
+
` alias: ${aliasUrl} (${alias})`,
|
|
837
|
+
].join("\n");
|
|
838
|
+
});
|
|
839
|
+
return [
|
|
840
|
+
"ID Token Port Connection Created",
|
|
841
|
+
"-".repeat(90),
|
|
842
|
+
...lines,
|
|
843
|
+
].join("\n\n");
|
|
729
844
|
},
|
|
730
845
|
},
|
|
731
846
|
{
|
|
@@ -853,6 +968,71 @@ export const menuCommand = new Command("menu")
|
|
|
853
968
|
}
|
|
854
969
|
},
|
|
855
970
|
},
|
|
971
|
+
{
|
|
972
|
+
label: "Grant Alias Access",
|
|
973
|
+
action: async () => {
|
|
974
|
+
try {
|
|
975
|
+
// Fetch available tokens to show users
|
|
976
|
+
const result = await apiRequest("GET", "/v1/admin/tokens");
|
|
977
|
+
const tokens = result.tokens || [];
|
|
978
|
+
|
|
979
|
+
if (tokens.length === 0) {
|
|
980
|
+
restoreRawMode();
|
|
981
|
+
return "No tokens found.";
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Build options from tokens (group by user_id)
|
|
985
|
+
const userMap = new Map<string, any>();
|
|
986
|
+
for (const t of tokens) {
|
|
987
|
+
const userId = t.user_id || t.userId;
|
|
988
|
+
if (userId && !userMap.has(userId)) {
|
|
989
|
+
userMap.set(userId, t);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const options: SelectOption[] = Array.from(userMap.entries()).map(([userId, t]) => ({
|
|
994
|
+
label: `${truncate(userId, 20)} ${colorDim(`${t.role || "user"} - ${t.label || "no label"}`)}`,
|
|
995
|
+
value: userId,
|
|
996
|
+
}));
|
|
997
|
+
|
|
998
|
+
const selected = await inlineSelect("Select user to grant alias access", options, true);
|
|
999
|
+
|
|
1000
|
+
if (selected === null) {
|
|
1001
|
+
restoreRawMode();
|
|
1002
|
+
return "";
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const userId = selected.value as string;
|
|
1006
|
+
|
|
1007
|
+
// Prompt for limit
|
|
1008
|
+
try {
|
|
1009
|
+
process.stdin.setRawMode(false);
|
|
1010
|
+
} catch {
|
|
1011
|
+
/* ignore */
|
|
1012
|
+
}
|
|
1013
|
+
const limitAnswer = await promptLine("Alias limit (1-10, or -1 for unlimited, default 1): ");
|
|
1014
|
+
restoreRawMode();
|
|
1015
|
+
|
|
1016
|
+
const aliasLimit = limitAnswer.trim() ? parseInt(limitAnswer.trim(), 10) : 1;
|
|
1017
|
+
if (isNaN(aliasLimit) || aliasLimit < -1 || aliasLimit > 100) {
|
|
1018
|
+
return "Invalid limit. Must be -1 (unlimited) or 0-100.";
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
await apiRequest("POST", "/v1/admin/grant-alias", { userId, aliasLimit });
|
|
1022
|
+
|
|
1023
|
+
const limitDesc = aliasLimit === -1 ? "unlimited" : String(aliasLimit);
|
|
1024
|
+
return [
|
|
1025
|
+
"✓ Alias access granted",
|
|
1026
|
+
"",
|
|
1027
|
+
`→ User ${userId}`,
|
|
1028
|
+
`→ Limit ${limitDesc} alias(es)`,
|
|
1029
|
+
].join("\n");
|
|
1030
|
+
} catch (err: any) {
|
|
1031
|
+
restoreRawMode();
|
|
1032
|
+
throw err;
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
856
1036
|
],
|
|
857
1037
|
});
|
|
858
1038
|
|
|
@@ -903,11 +1083,8 @@ export const menuCommand = new Command("menu")
|
|
|
903
1083
|
if (clients.length === 0) {
|
|
904
1084
|
cachedActiveTunnels = "";
|
|
905
1085
|
} else {
|
|
906
|
-
const domain = process.env.TUNNEL_DOMAIN || "t.uplink.spot";
|
|
907
|
-
const scheme = (process.env.TUNNEL_URL_SCHEME || "https").toLowerCase();
|
|
908
|
-
|
|
909
1086
|
const tunnelLines = clients.map((client, idx) => {
|
|
910
|
-
const url = `${
|
|
1087
|
+
const url = `${URL_SCHEME}://${client.token}.${TOKEN_DOMAIN}`;
|
|
911
1088
|
const isLast = idx === clients.length - 1;
|
|
912
1089
|
const branch = isLast ? "└─" : "├─";
|
|
913
1090
|
return colorDim(branch) + " " + colorGreen(url) + colorDim(" → ") + `localhost:${client.port}`;
|
|
@@ -1001,6 +1178,7 @@ export const menuCommand = new Command("menu")
|
|
|
1001
1178
|
let branchColor: string;
|
|
1002
1179
|
|
|
1003
1180
|
if (isSelected) {
|
|
1181
|
+
// Selected: cyan highlight
|
|
1004
1182
|
branchColor = colorCyan(branch);
|
|
1005
1183
|
if (cleanLabel.toLowerCase().includes("exit")) {
|
|
1006
1184
|
label = colorDim(cleanLabel);
|
|
@@ -1012,7 +1190,8 @@ export const menuCommand = new Command("menu")
|
|
|
1012
1190
|
label = colorCyan(cleanLabel);
|
|
1013
1191
|
}
|
|
1014
1192
|
} else {
|
|
1015
|
-
|
|
1193
|
+
// Not selected: white text
|
|
1194
|
+
branchColor = colorWhite(branch);
|
|
1016
1195
|
if (cleanLabel.toLowerCase().includes("exit")) {
|
|
1017
1196
|
label = colorDim(cleanLabel);
|
|
1018
1197
|
} else if (cleanLabel.toLowerCase().includes("stop all") || cleanLabel.toLowerCase().includes("kill")) {
|
|
@@ -1020,7 +1199,7 @@ export const menuCommand = new Command("menu")
|
|
|
1020
1199
|
} else if (cleanLabel.toLowerCase().includes("get started")) {
|
|
1021
1200
|
label = colorGreen(cleanLabel);
|
|
1022
1201
|
} else {
|
|
1023
|
-
label = cleanLabel;
|
|
1202
|
+
label = colorWhite(cleanLabel);
|
|
1024
1203
|
}
|
|
1025
1204
|
}
|
|
1026
1205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uplink-cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
4
4
|
"description": "Localhost to public URL in seconds. No signup forms, no browser - everything in your terminal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tunnel",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"better-sqlite3": "^11.10.0",
|
|
51
51
|
"body-parser": "^1.20.3",
|
|
52
52
|
"commander": "^12.1.0",
|
|
53
|
+
"compression": "^1.7.4",
|
|
53
54
|
"dotenv": "^16.6.1",
|
|
54
55
|
"express": "^4.19.2",
|
|
55
56
|
"express-rate-limit": "^8.2.1",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"zod": "^4.2.1"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
66
|
+
"@types/compression": "^1.7.5",
|
|
65
67
|
"@types/express": "^4.17.21",
|
|
66
68
|
"@types/express-rate-limit": "^5.1.3",
|
|
67
69
|
"@types/node": "^22.7.4",
|