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 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 = getApiBase();
66
- const apiToken = getApiToken(apiBase);
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 = process.env.AGENTCLOUD_API_BASE || "https://api.uplink.spot";
32
+ const apiBase = getResolvedApiBase();
32
33
  let health = { status: "unknown" };
33
34
  try {
34
35
  const fetch = (await import("node-fetch")).default;