uplink-cli 0.1.35 → 0.1.36
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 +6 -0
- package/cli/bin/uplink.js +3 -1
- package/cli/src/http.ts +24 -31
- package/cli/src/index.ts +12 -0
- package/cli/src/subcommands/admin.ts +2 -1
- package/cli/src/subcommands/host.ts +1695 -43
- package/cli/src/subcommands/menu/effects/health.ts +2 -1
- package/cli/src/subcommands/menu/effects/smoke.ts +5 -2
- package/cli/src/subcommands/menu/effects/token-config.ts +23 -5
- package/cli/src/subcommands/menu/io.ts +21 -1
- package/cli/src/subcommands/menu/menus/aliases.ts +16 -3
- package/cli/src/subcommands/menu/menus/hosting.ts +269 -0
- package/cli/src/subcommands/menu/menus/index.ts +1 -0
- package/cli/src/subcommands/menu/menus/tokens.ts +21 -4
- package/cli/src/subcommands/menu/menus/tunnels.ts +17 -4
- package/cli/src/subcommands/menu/requests.ts +2 -1
- package/cli/src/subcommands/menu/tests.ts +5 -2
- package/cli/src/subcommands/menu.ts +120 -50
- package/cli/src/subcommands/signup.ts +5 -2
- package/cli/src/templates/index.ts +399 -0
- package/cli/src/utils/analyze.ts +689 -0
- package/cli/src/utils/api-base.ts +145 -0
- package/docs/AGENTS.md +59 -4
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -40,6 +40,12 @@ uplink # open menu
|
|
|
40
40
|
- My Tunnels → see status and permanent URL if set
|
|
41
41
|
- Create Permanent URL → pick tunnel → enter alias (if premium enabled)
|
|
42
42
|
|
|
43
|
+
### Hosting (interactive)
|
|
44
|
+
- Hosting → Setup Wizard → analyze + create + deploy
|
|
45
|
+
- Hosting → Deploy to Existing App → select app with arrow keys
|
|
46
|
+
- Hosting → List Hosted Apps → select app to view ID + URL
|
|
47
|
+
- Hosting → Delete Hosted App → select app, confirm options (type `DELETE` to proceed)
|
|
48
|
+
|
|
43
49
|
## Quick start (non-interactive)
|
|
44
50
|
```bash
|
|
45
51
|
# Create tunnel (any port: 3000, 8080, 5173, etc.)
|
package/cli/bin/uplink.js
CHANGED
|
@@ -12,6 +12,8 @@ const fs = require("fs");
|
|
|
12
12
|
const binDir = __dirname;
|
|
13
13
|
const projectRoot = path.join(binDir, "../..");
|
|
14
14
|
const cliPath = path.join(projectRoot, "cli/src/index.ts");
|
|
15
|
+
const invokeCwd = process.cwd();
|
|
16
|
+
const invokeBin = __filename;
|
|
15
17
|
|
|
16
18
|
// Get arguments
|
|
17
19
|
const args = process.argv.slice(2);
|
|
@@ -39,7 +41,7 @@ try {
|
|
|
39
41
|
const child = spawn(tsxPath, [cliPath, ...args], {
|
|
40
42
|
stdio: "inherit",
|
|
41
43
|
cwd: projectRoot,
|
|
42
|
-
env: process.env,
|
|
44
|
+
env: { ...process.env, UPLINK_CWD: invokeCwd, UPLINK_BIN: invokeBin },
|
|
43
45
|
shell: false,
|
|
44
46
|
});
|
|
45
47
|
|
package/cli/src/http.ts
CHANGED
|
@@ -1,37 +1,12 @@
|
|
|
1
1
|
import fetch, { AbortError } from "node-fetch";
|
|
2
|
+
import { getResolvedApiBase, getResolvedApiToken } from "./utils/api-base";
|
|
2
3
|
|
|
3
4
|
// Configuration
|
|
4
5
|
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
5
6
|
const MAX_RETRIES = 3;
|
|
7
|
+
const MAX_RETRIES_RATE_LIMIT = 5; // More retries for rate limits
|
|
6
8
|
const INITIAL_RETRY_DELAY = 1000; // 1 second
|
|
7
|
-
|
|
8
|
-
function getApiBase(): string {
|
|
9
|
-
return process.env.AGENTCLOUD_API_BASE ?? "https://api.uplink.spot";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isLocalApiBase(apiBase: string): boolean {
|
|
13
|
-
return (
|
|
14
|
-
apiBase.includes("://localhost") ||
|
|
15
|
-
apiBase.includes("://127.0.0.1") ||
|
|
16
|
-
apiBase.includes("://0.0.0.0")
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getApiToken(apiBase: string): string | undefined {
|
|
21
|
-
// Production (non-local) always requires an explicit token.
|
|
22
|
-
if (!isLocalApiBase(apiBase)) {
|
|
23
|
-
return process.env.AGENTCLOUD_TOKEN || undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Local dev:
|
|
27
|
-
// - Prefer AGENTCLOUD_TOKEN if set
|
|
28
|
-
// - Otherwise allow AGENTCLOUD_TOKEN_DEV (no hardcoded default for security)
|
|
29
|
-
return (
|
|
30
|
-
process.env.AGENTCLOUD_TOKEN ||
|
|
31
|
-
process.env.AGENTCLOUD_TOKEN_DEV ||
|
|
32
|
-
undefined
|
|
33
|
-
);
|
|
34
|
-
}
|
|
9
|
+
const INITIAL_RATE_LIMIT_DELAY = 5000; // 5 seconds for rate limit errors
|
|
35
10
|
|
|
36
11
|
// Check if error is retryable (network issues, 5xx errors)
|
|
37
12
|
function isRetryable(error: unknown, statusCode?: number): boolean {
|
|
@@ -62,8 +37,8 @@ export async function apiRequest(
|
|
|
62
37
|
path: string,
|
|
63
38
|
body?: unknown
|
|
64
39
|
): Promise<any> {
|
|
65
|
-
const apiBase =
|
|
66
|
-
const apiToken =
|
|
40
|
+
const apiBase = getResolvedApiBase();
|
|
41
|
+
const apiToken = getResolvedApiToken(apiBase);
|
|
67
42
|
if (!apiToken) {
|
|
68
43
|
throw new Error("Missing AGENTCLOUD_TOKEN");
|
|
69
44
|
}
|
|
@@ -91,6 +66,25 @@ export async function apiRequest(
|
|
|
91
66
|
const json = await response.json().catch(() => ({}));
|
|
92
67
|
|
|
93
68
|
if (!response.ok) {
|
|
69
|
+
// Special handling for rate limit errors (429)
|
|
70
|
+
if (response.status === 429) {
|
|
71
|
+
const maxRetries = MAX_RETRIES_RATE_LIMIT;
|
|
72
|
+
if (attempt < maxRetries - 1) {
|
|
73
|
+
// Check for Retry-After header
|
|
74
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
75
|
+
let delay: number;
|
|
76
|
+
if (retryAfter) {
|
|
77
|
+
const retryAfterSeconds = parseInt(retryAfter, 10);
|
|
78
|
+
delay = isNaN(retryAfterSeconds) ? INITIAL_RATE_LIMIT_DELAY * Math.pow(2, attempt) : retryAfterSeconds * 1000;
|
|
79
|
+
} else {
|
|
80
|
+
// Exponential backoff starting at 5 seconds for rate limits
|
|
81
|
+
delay = INITIAL_RATE_LIMIT_DELAY * Math.pow(2, attempt);
|
|
82
|
+
}
|
|
83
|
+
await sleep(delay);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
94
88
|
// Check if this error is retryable
|
|
95
89
|
if (isRetryable(null, response.status) && attempt < MAX_RETRIES - 1) {
|
|
96
90
|
const delay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
|
|
@@ -104,7 +98,6 @@ export async function apiRequest(
|
|
|
104
98
|
} catch (error) {
|
|
105
99
|
clearTimeout(timeoutId);
|
|
106
100
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
107
|
-
|
|
108
101
|
// Check if we should retry
|
|
109
102
|
if (isRetryable(error) && attempt < MAX_RETRIES - 1) {
|
|
110
103
|
const delay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
|
package/cli/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { systemCommand } from "./subcommands/system";
|
|
|
10
10
|
import { hostCommand } from "./subcommands/host";
|
|
11
11
|
import { readFileSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
|
+
import { ensureApiBase, parseTokenEnv } from "./utils/api-base";
|
|
13
14
|
|
|
14
15
|
// Get version from package.json (CommonJS build: __dirname available)
|
|
15
16
|
const pkgPath = join(__dirname, "../../package.json");
|
|
@@ -60,6 +61,17 @@ program.hook("preAction", async (thisCommand) => {
|
|
|
60
61
|
process.env.AGENTCLOUD_TOKEN = cachedTokenStdin;
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
|
|
65
|
+
const parsed = parseTokenEnv(process.env.AGENTCLOUD_TOKEN);
|
|
66
|
+
if (parsed.token) {
|
|
67
|
+
process.env.AGENTCLOUD_TOKEN = parsed.token;
|
|
68
|
+
if (!process.env.AGENTCLOUD_API_BASE && parsed.apiBase) {
|
|
69
|
+
process.env.AGENTCLOUD_API_BASE = parsed.apiBase;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !opts.json);
|
|
74
|
+
await ensureApiBase({ interactive: isInteractive });
|
|
63
75
|
});
|
|
64
76
|
|
|
65
77
|
// If no command provided and not a flag, default to menu
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { apiRequest } from "../http";
|
|
3
|
+
import { getResolvedApiBase } from "../utils/api-base";
|
|
3
4
|
|
|
4
5
|
export const adminCommand = new Command("admin")
|
|
5
6
|
.description("Admin commands for system management");
|
|
@@ -28,7 +29,7 @@ adminCommand
|
|
|
28
29
|
.action(async (opts) => {
|
|
29
30
|
try {
|
|
30
31
|
// Check health
|
|
31
|
-
const apiBase =
|
|
32
|
+
const apiBase = getResolvedApiBase();
|
|
32
33
|
let health = { status: "unknown" };
|
|
33
34
|
try {
|
|
34
35
|
const fetch = (await import("node-fetch")).default;
|