uplink-cli 0.1.0-alpha.5 → 0.1.0-alpha.7
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/README.md +7 -5
- package/cli/src/http.ts +81 -17
- package/cli/src/subcommands/menu.ts +76 -90
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -10,10 +10,12 @@ Perfect for sharing work-in-progress, testing webhooks, or demoing to clients. A
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **Instant Public URLs** - Your `localhost:3000` becomes `https://xyz.
|
|
13
|
+
- **Instant Public URLs** - Your `localhost:3000` becomes `https://xyz.x.uplink.spot`
|
|
14
|
+
- **Permanent Aliases** - Claim memorable URLs like `https://myapp.uplink.spot`
|
|
14
15
|
- **Zero Browser Required** - Signup, auth, and tunnel management all in terminal
|
|
15
16
|
- **Agent-Friendly** - AI assistants can create tokens and start tunnels via API
|
|
16
17
|
- **Auto Port Detection** - Scans for running servers, select with arrow keys
|
|
18
|
+
- **Auto-Retry** - Built-in retry logic with exponential backoff for reliability
|
|
17
19
|
|
|
18
20
|
## Quick Start
|
|
19
21
|
|
|
@@ -59,7 +61,7 @@ Once authenticated, select **"Manage Tunnels"** → **"Start Tunnel"**:
|
|
|
59
61
|
- The CLI will scan for active servers on your local machine
|
|
60
62
|
- Use arrow keys to select a port, or choose "Enter custom port"
|
|
61
63
|
- Press "Back" if you want to cancel
|
|
62
|
-
- Your tunnel URL will be displayed (e.g., `https://abc123.
|
|
64
|
+
- Your tunnel URL will be displayed (e.g., `https://abc123.x.uplink.spot`)
|
|
63
65
|
|
|
64
66
|
**Keep the terminal running** - the tunnel client must stay active.
|
|
65
67
|
|
|
@@ -103,8 +105,8 @@ export AGENTCLOUD_TOKEN=your-token-here
|
|
|
103
105
|
# Tunnel control server (default: tunnel.uplink.spot:7071)
|
|
104
106
|
export TUNNEL_CTRL=tunnel.uplink.spot:7071
|
|
105
107
|
|
|
106
|
-
# Tunnel domain (default:
|
|
107
|
-
export TUNNEL_DOMAIN=
|
|
108
|
+
# Tunnel domain (default: x.uplink.spot)
|
|
109
|
+
export TUNNEL_DOMAIN=x.uplink.spot
|
|
108
110
|
```
|
|
109
111
|
|
|
110
112
|
## Requirements
|
|
@@ -119,7 +121,7 @@ export TUNNEL_DOMAIN=t.uplink.spot
|
|
|
119
121
|
1. **Create tunnel** - Request a tunnel from the API
|
|
120
122
|
2. **Get token** - Receive a unique token (e.g., `abc123`)
|
|
121
123
|
3. **Start client** - Run the tunnel client locally, connecting to the relay
|
|
122
|
-
4. **Access** - Your local server is accessible at `https://abc123.
|
|
124
|
+
4. **Access** - Your local server is accessible at `https://abc123.x.uplink.spot`
|
|
123
125
|
|
|
124
126
|
The tunnel client forwards HTTP requests from the public URL to your local server.
|
|
125
127
|
|
package/cli/src/http.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import fetch from "node-fetch";
|
|
1
|
+
import fetch, { AbortError } from "node-fetch";
|
|
2
|
+
|
|
3
|
+
// Configuration
|
|
4
|
+
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
5
|
+
const MAX_RETRIES = 3;
|
|
6
|
+
const INITIAL_RETRY_DELAY = 1000; // 1 second
|
|
2
7
|
|
|
3
8
|
function getApiBase(): string {
|
|
4
9
|
return process.env.AGENTCLOUD_API_BASE ?? "https://api.uplink.spot";
|
|
@@ -28,6 +33,30 @@ function getApiToken(apiBase: string): string | undefined {
|
|
|
28
33
|
);
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
// Check if error is retryable (network issues, 5xx errors)
|
|
37
|
+
function isRetryable(error: unknown, statusCode?: number): boolean {
|
|
38
|
+
// Network errors are retryable
|
|
39
|
+
if (error instanceof AbortError) return true;
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
const msg = error.message.toLowerCase();
|
|
42
|
+
if (msg.includes("econnrefused") || msg.includes("enotfound") ||
|
|
43
|
+
msg.includes("etimedout") || msg.includes("econnreset") ||
|
|
44
|
+
msg.includes("socket hang up")) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 5xx server errors are retryable
|
|
49
|
+
if (statusCode && statusCode >= 500 && statusCode < 600) return true;
|
|
50
|
+
// 429 Too Many Requests is retryable
|
|
51
|
+
if (statusCode === 429) return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Sleep helper
|
|
56
|
+
function sleep(ms: number): Promise<void> {
|
|
57
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
export async function apiRequest(
|
|
32
61
|
method: string,
|
|
33
62
|
path: string,
|
|
@@ -39,22 +68,57 @@ export async function apiRequest(
|
|
|
39
68
|
throw new Error("Missing AGENTCLOUD_TOKEN");
|
|
40
69
|
}
|
|
41
70
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
let lastError: Error | null = null;
|
|
72
|
+
|
|
73
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
74
|
+
// Create abort controller for timeout
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`${apiBase}${path}`, {
|
|
80
|
+
method,
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
Authorization: `Bearer ${apiToken}`,
|
|
84
|
+
},
|
|
85
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
58
88
|
|
|
89
|
+
clearTimeout(timeoutId);
|
|
59
90
|
|
|
91
|
+
const json = await response.json().catch(() => ({}));
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
// Check if this error is retryable
|
|
95
|
+
if (isRetryable(null, response.status) && attempt < MAX_RETRIES - 1) {
|
|
96
|
+
const delay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
|
|
97
|
+
await sleep(delay);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(JSON.stringify(json, null, 2));
|
|
101
|
+
}
|
|
60
102
|
|
|
103
|
+
return json;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
107
|
+
|
|
108
|
+
// Check if we should retry
|
|
109
|
+
if (isRetryable(error) && attempt < MAX_RETRIES - 1) {
|
|
110
|
+
const delay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
|
|
111
|
+
await sleep(delay);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Not retryable or out of retries
|
|
116
|
+
if (error instanceof AbortError) {
|
|
117
|
+
throw new Error(`Request timeout after ${REQUEST_TIMEOUT}ms`);
|
|
118
|
+
}
|
|
119
|
+
throw lastError;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw lastError || new Error("Request failed after retries");
|
|
124
|
+
}
|
|
@@ -79,18 +79,14 @@ 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
|
-
|
|
86
|
-
function colorMagenta(text: string) {
|
|
87
|
-
return `${c.magenta}${text}${c.reset}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
82
|
function colorWhite(text: string) {
|
|
91
83
|
return `${c.brightWhite}${text}${c.reset}`;
|
|
92
84
|
}
|
|
93
85
|
|
|
86
|
+
const TOKEN_DOMAIN = process.env.TUNNEL_DOMAIN || "x.uplink.spot";
|
|
87
|
+
const ALIAS_DOMAIN = process.env.ALIAS_DOMAIN || "uplink.spot";
|
|
88
|
+
const URL_SCHEME = (process.env.TUNNEL_URL_SCHEME || "https").toLowerCase();
|
|
89
|
+
|
|
94
90
|
// ASCII banner with color styling
|
|
95
91
|
const ASCII_UPLINK = colorWhite([
|
|
96
92
|
"██╗ ██╗██████╗ ██╗ ██╗███╗ ██╗██╗ ██╗",
|
|
@@ -273,7 +269,7 @@ export const menuCommand = new Command("menu")
|
|
|
273
269
|
// If authentication failed, show ONLY "Get Started" and "Exit"
|
|
274
270
|
if (authFailed) {
|
|
275
271
|
mainMenu.push({
|
|
276
|
-
label: "
|
|
272
|
+
label: "Get Started",
|
|
277
273
|
action: async () => {
|
|
278
274
|
restoreRawMode();
|
|
279
275
|
clearScreen();
|
|
@@ -301,21 +297,21 @@ export const menuCommand = new Command("menu")
|
|
|
301
297
|
});
|
|
302
298
|
if (!result) {
|
|
303
299
|
restoreRawMode();
|
|
304
|
-
return "
|
|
300
|
+
return "Error: No response from server.";
|
|
305
301
|
}
|
|
306
302
|
} catch (err: any) {
|
|
307
303
|
restoreRawMode();
|
|
308
304
|
const errorMsg = err?.message || String(err);
|
|
309
|
-
console.error("\
|
|
305
|
+
console.error("\nSignup error:", errorMsg);
|
|
310
306
|
if (errorMsg.includes("429") || errorMsg.includes("RATE_LIMIT")) {
|
|
311
|
-
return "
|
|
307
|
+
return "Too many signup attempts. Please try again later.";
|
|
312
308
|
}
|
|
313
|
-
return
|
|
309
|
+
return `Error creating account: ${errorMsg}`;
|
|
314
310
|
}
|
|
315
311
|
|
|
316
312
|
if (!result || !result.token) {
|
|
317
313
|
restoreRawMode();
|
|
318
|
-
return "
|
|
314
|
+
return "Error: Invalid response from server. Token not received.";
|
|
319
315
|
}
|
|
320
316
|
|
|
321
317
|
const token = result.token;
|
|
@@ -455,9 +451,9 @@ export const menuCommand = new Command("menu")
|
|
|
455
451
|
restoreRawMode();
|
|
456
452
|
const errorMsg = err?.message || String(err);
|
|
457
453
|
if (errorMsg.includes("429") || errorMsg.includes("RATE_LIMIT")) {
|
|
458
|
-
return "
|
|
454
|
+
return "Too many signup attempts. Please try again later.";
|
|
459
455
|
}
|
|
460
|
-
return
|
|
456
|
+
return `Error creating account: ${errorMsg}`;
|
|
461
457
|
}
|
|
462
458
|
},
|
|
463
459
|
});
|
|
@@ -527,6 +523,42 @@ export const menuCommand = new Command("menu")
|
|
|
527
523
|
return "test:comprehensive completed";
|
|
528
524
|
},
|
|
529
525
|
},
|
|
526
|
+
{
|
|
527
|
+
label: "View Connected Tunnels",
|
|
528
|
+
action: async () => {
|
|
529
|
+
try {
|
|
530
|
+
const data = await apiRequest("GET", "/v1/admin/relay-status") as {
|
|
531
|
+
connectedTunnels?: number;
|
|
532
|
+
tunnels?: Array<{ token: string; clientIp: string; targetPort: number; connectedAt: string; connectedFor: string }>;
|
|
533
|
+
timestamp?: string;
|
|
534
|
+
error?: string;
|
|
535
|
+
message?: string;
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
if (data.error) {
|
|
539
|
+
return `Error: ${data.error}${data.message ? ` - ${data.message}` : ""}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!data.tunnels || data.tunnels.length === 0) {
|
|
543
|
+
return "No tunnels currently connected to the relay.";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const lines = data.tunnels.map((t) =>
|
|
547
|
+
`${truncate(t.token, 12).padEnd(14)} ${t.clientIp.padEnd(16)} ${String(t.targetPort).padEnd(6)} ${t.connectedFor.padEnd(10)} ${truncate(t.connectedAt, 19)}`
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
return [
|
|
551
|
+
`Connected Tunnels: ${data.connectedTunnels}`,
|
|
552
|
+
"",
|
|
553
|
+
"Token Client IP Port Uptime Connected At",
|
|
554
|
+
"-".repeat(75),
|
|
555
|
+
...lines,
|
|
556
|
+
].join("\n");
|
|
557
|
+
} catch (err: any) {
|
|
558
|
+
return `Error: Failed to get relay status - ${err.message}`;
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
},
|
|
530
562
|
],
|
|
531
563
|
});
|
|
532
564
|
}
|
|
@@ -712,7 +744,7 @@ export const menuCommand = new Command("menu")
|
|
|
712
744
|
const userId = parsed?.error?.details?.user_id || "(check your token)";
|
|
713
745
|
return [
|
|
714
746
|
"",
|
|
715
|
-
colorYellow("
|
|
747
|
+
colorYellow("Permanent Aliases - Premium Feature"),
|
|
716
748
|
"",
|
|
717
749
|
"Permanent aliases give you stable URLs like:",
|
|
718
750
|
` ${colorGreen(`https://myapp.${ALIAS_DOMAIN}`)}`,
|
|
@@ -727,11 +759,11 @@ export const menuCommand = new Command("menu")
|
|
|
727
759
|
"",
|
|
728
760
|
].join("\n");
|
|
729
761
|
} catch {
|
|
730
|
-
return colorYellow("
|
|
762
|
+
return colorYellow("Aliases are a premium feature. Contact us at uplink.spot to upgrade.");
|
|
731
763
|
}
|
|
732
764
|
}
|
|
733
765
|
if (errMsg.includes("ALIAS_LIMIT_REACHED")) {
|
|
734
|
-
return colorYellow("
|
|
766
|
+
return colorYellow("You've reached your alias limit. Contact us to increase it.");
|
|
735
767
|
}
|
|
736
768
|
throw err; // Re-throw other errors
|
|
737
769
|
}
|
|
@@ -756,43 +788,6 @@ export const menuCommand = new Command("menu")
|
|
|
756
788
|
return "✓ Alias removed";
|
|
757
789
|
},
|
|
758
790
|
},
|
|
759
|
-
{
|
|
760
|
-
label: "View Connected (with IPs)",
|
|
761
|
-
action: async () => {
|
|
762
|
-
try {
|
|
763
|
-
// Use the API endpoint which proxies to the relay
|
|
764
|
-
const data = await apiRequest("GET", "/v1/admin/relay-status") as {
|
|
765
|
-
connectedTunnels?: number;
|
|
766
|
-
tunnels?: Array<{ token: string; clientIp: string; targetPort: number; connectedAt: string; connectedFor: string }>;
|
|
767
|
-
timestamp?: string;
|
|
768
|
-
error?: string;
|
|
769
|
-
message?: string;
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
if (data.error) {
|
|
773
|
-
return `❌ Relay error: ${data.error}${data.message ? ` - ${data.message}` : ""}`;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (!data.tunnels || data.tunnels.length === 0) {
|
|
777
|
-
return "No tunnels currently connected to the relay.";
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const lines = data.tunnels.map((t) =>
|
|
781
|
-
`${truncate(t.token, 12).padEnd(14)} ${t.clientIp.padEnd(16)} ${String(t.targetPort).padEnd(6)} ${t.connectedFor.padEnd(10)} ${truncate(t.connectedAt, 19)}`
|
|
782
|
-
);
|
|
783
|
-
|
|
784
|
-
return [
|
|
785
|
-
`Connected Tunnels: ${data.connectedTunnels}`,
|
|
786
|
-
"",
|
|
787
|
-
"Token Client IP Port Uptime Connected At",
|
|
788
|
-
"-".repeat(75),
|
|
789
|
-
...lines,
|
|
790
|
-
].join("\n");
|
|
791
|
-
} catch (err: any) {
|
|
792
|
-
return `❌ Failed to get relay status: ${err.message}`;
|
|
793
|
-
}
|
|
794
|
-
},
|
|
795
|
-
},
|
|
796
791
|
],
|
|
797
792
|
});
|
|
798
793
|
|
|
@@ -800,7 +795,7 @@ export const menuCommand = new Command("menu")
|
|
|
800
795
|
label: "Usage",
|
|
801
796
|
subMenu: [
|
|
802
797
|
{
|
|
803
|
-
label: isAdmin ? "List Tunnels
|
|
798
|
+
label: isAdmin ? "List All Tunnels" : "List My Tunnels",
|
|
804
799
|
action: async () => {
|
|
805
800
|
const runningClients = findTunnelClients();
|
|
806
801
|
const path = isAdmin ? "/v1/admin/tunnels?limit=20" : "/v1/tunnels";
|
|
@@ -844,7 +839,7 @@ export const menuCommand = new Command("menu")
|
|
|
844
839
|
},
|
|
845
840
|
},
|
|
846
841
|
{
|
|
847
|
-
label: isAdmin ? "List Databases
|
|
842
|
+
label: isAdmin ? "List All Databases" : "List My Databases",
|
|
848
843
|
action: async () => {
|
|
849
844
|
const path = isAdmin ? "/v1/admin/databases?limit=20" : "/v1/dbs";
|
|
850
845
|
const result = await apiRequest("GET", path);
|
|
@@ -875,7 +870,7 @@ export const menuCommand = new Command("menu")
|
|
|
875
870
|
// Admin-only: Manage Tokens
|
|
876
871
|
if (isAdmin) {
|
|
877
872
|
mainMenu.push({
|
|
878
|
-
label: "Manage Tokens
|
|
873
|
+
label: "Manage Tokens",
|
|
879
874
|
subMenu: [
|
|
880
875
|
{
|
|
881
876
|
label: "List Tokens",
|
|
@@ -1036,9 +1031,9 @@ export const menuCommand = new Command("menu")
|
|
|
1036
1031
|
],
|
|
1037
1032
|
});
|
|
1038
1033
|
|
|
1039
|
-
// Admin-only: Stop ALL Tunnel Clients
|
|
1034
|
+
// Admin-only: Stop ALL Tunnel Clients
|
|
1040
1035
|
mainMenu.push({
|
|
1041
|
-
label: "
|
|
1036
|
+
label: "Stop All Tunnel Clients",
|
|
1042
1037
|
action: async () => {
|
|
1043
1038
|
const clients = findTunnelClients();
|
|
1044
1039
|
if (clients.length === 0) {
|
|
@@ -1166,40 +1161,34 @@ export const menuCommand = new Command("menu")
|
|
|
1166
1161
|
const isSelected = idx === selected;
|
|
1167
1162
|
const branch = isLast ? "└─" : "├─";
|
|
1168
1163
|
|
|
1169
|
-
// Clean up labels - remove emojis for cleaner look
|
|
1170
|
-
let cleanLabel = choice.label
|
|
1171
|
-
.replace(/^🚀\s*/, "")
|
|
1172
|
-
.replace(/^⚠️\s*/, "")
|
|
1173
|
-
.replace(/^✅\s*/, "")
|
|
1174
|
-
.replace(/^❌\s*/, "");
|
|
1175
|
-
|
|
1176
1164
|
// Style based on selection and type
|
|
1177
1165
|
let label: string;
|
|
1178
1166
|
let branchColor: string;
|
|
1167
|
+
const labelLower = choice.label.toLowerCase();
|
|
1179
1168
|
|
|
1180
1169
|
if (isSelected) {
|
|
1181
1170
|
// Selected: cyan highlight
|
|
1182
1171
|
branchColor = colorCyan(branch);
|
|
1183
|
-
if (
|
|
1184
|
-
label = colorDim(
|
|
1185
|
-
} else if (
|
|
1186
|
-
label = colorRed(
|
|
1187
|
-
} else if (
|
|
1188
|
-
label = colorGreen(
|
|
1172
|
+
if (labelLower.includes("exit")) {
|
|
1173
|
+
label = colorDim(choice.label);
|
|
1174
|
+
} else if (labelLower.includes("stop all")) {
|
|
1175
|
+
label = colorRed(choice.label);
|
|
1176
|
+
} else if (labelLower.includes("get started")) {
|
|
1177
|
+
label = colorGreen(choice.label);
|
|
1189
1178
|
} else {
|
|
1190
|
-
label = colorCyan(
|
|
1179
|
+
label = colorCyan(choice.label);
|
|
1191
1180
|
}
|
|
1192
1181
|
} else {
|
|
1193
1182
|
// Not selected: white text
|
|
1194
1183
|
branchColor = colorWhite(branch);
|
|
1195
|
-
if (
|
|
1196
|
-
label = colorDim(
|
|
1197
|
-
} else if (
|
|
1198
|
-
label = colorRed(
|
|
1199
|
-
} else if (
|
|
1200
|
-
label = colorGreen(
|
|
1184
|
+
if (labelLower.includes("exit")) {
|
|
1185
|
+
label = colorDim(choice.label);
|
|
1186
|
+
} else if (labelLower.includes("stop all")) {
|
|
1187
|
+
label = colorRed(choice.label);
|
|
1188
|
+
} else if (labelLower.includes("get started")) {
|
|
1189
|
+
label = colorGreen(choice.label);
|
|
1201
1190
|
} else {
|
|
1202
|
-
label = colorWhite(
|
|
1191
|
+
label = colorWhite(choice.label);
|
|
1203
1192
|
}
|
|
1204
1193
|
}
|
|
1205
1194
|
|
|
@@ -1220,14 +1209,11 @@ export const menuCommand = new Command("menu")
|
|
|
1220
1209
|
const lines = message.split("\n");
|
|
1221
1210
|
lines.forEach((line) => {
|
|
1222
1211
|
// Color success/error indicators
|
|
1212
|
+
// Style success/error prefixes consistently
|
|
1223
1213
|
let styledLine = line
|
|
1224
|
-
.replace(
|
|
1225
|
-
.replace(
|
|
1226
|
-
.replace(
|
|
1227
|
-
.replace(/^🔑/, colorCyan("→"))
|
|
1228
|
-
.replace(/^🌐/, colorCyan("→"))
|
|
1229
|
-
.replace(/^📡/, colorCyan("→"))
|
|
1230
|
-
.replace(/^💡/, colorYellow("→"));
|
|
1214
|
+
.replace(/^✓\s*/, colorGreen("✓ "))
|
|
1215
|
+
.replace(/^→\s*/, colorCyan("→ "))
|
|
1216
|
+
.replace(/^Error:\s*/, colorRed("✗ "));
|
|
1231
1217
|
console.log(colorDim("│ ") + styledLine);
|
|
1232
1218
|
});
|
|
1233
1219
|
}
|
|
@@ -1424,7 +1410,7 @@ function runSmoke(script: "smoke:tunnel" | "smoke:db" | "smoke:all" | "test:comp
|
|
|
1424
1410
|
const env = {
|
|
1425
1411
|
...process.env,
|
|
1426
1412
|
AGENTCLOUD_API_BASE: process.env.AGENTCLOUD_API_BASE ?? "https://api.uplink.spot",
|
|
1427
|
-
AGENTCLOUD_TOKEN: process.env.AGENTCLOUD_TOKEN
|
|
1413
|
+
AGENTCLOUD_TOKEN: process.env.AGENTCLOUD_TOKEN,
|
|
1428
1414
|
};
|
|
1429
1415
|
|
|
1430
1416
|
// For test:comprehensive, run inline (no subprocess)
|
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uplink-cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
|
+
"description": "Expose localhost to the internet in seconds. Interactive terminal UI, permanent custom domains, zero config. A modern ngrok alternative.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tunnel",
|
|
7
7
|
"localhost",
|
|
8
8
|
"ngrok",
|
|
9
|
+
"ngrok-alternative",
|
|
9
10
|
"expose",
|
|
10
11
|
"cli",
|
|
11
12
|
"dev-tools",
|
|
12
13
|
"webhook",
|
|
13
14
|
"public-url",
|
|
14
|
-
"port-forwarding"
|
|
15
|
+
"port-forwarding",
|
|
16
|
+
"reverse-proxy",
|
|
17
|
+
"tunneling",
|
|
18
|
+
"local-development",
|
|
19
|
+
"https-tunnel",
|
|
20
|
+
"custom-domain"
|
|
15
21
|
],
|
|
16
22
|
"homepage": "https://github.com/firstprinciplecode/uplink#readme",
|
|
17
23
|
"repository": {
|