uplink-cli 0.1.37 → 0.1.39

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.
Files changed (56) hide show
  1. package/AGENTS.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +45 -44
  4. package/cli/src/index.ts +5 -3
  5. package/cli/src/registrars/cloudflare.ts +148 -0
  6. package/cli/src/registrars/godaddy.ts +99 -0
  7. package/cli/src/registrars/hostinger.ts +106 -0
  8. package/cli/src/registrars/http.ts +18 -0
  9. package/cli/src/registrars/index.ts +30 -0
  10. package/cli/src/registrars/namecheap.ts +163 -0
  11. package/cli/src/registrars/secret.ts +66 -0
  12. package/cli/src/registrars/store.ts +55 -0
  13. package/cli/src/registrars/types.ts +40 -0
  14. package/cli/src/subcommands/admin.ts +17 -30
  15. package/cli/src/subcommands/db.ts +63 -57
  16. package/cli/src/subcommands/dev.ts +23 -25
  17. package/cli/src/subcommands/domains.ts +268 -0
  18. package/cli/src/subcommands/host-domains.ts +148 -0
  19. package/cli/src/subcommands/host.ts +106 -32
  20. package/cli/src/subcommands/menu/colors.ts +1 -1
  21. package/cli/src/subcommands/menu/effects/tunnel-clients.ts +87 -14
  22. package/cli/src/subcommands/menu/inline-tree-select.ts +6 -5
  23. package/cli/src/subcommands/menu/io.ts +27 -5
  24. package/cli/src/subcommands/menu/menus/domains.ts +199 -0
  25. package/cli/src/subcommands/menu/menus/hosting.ts +14 -46
  26. package/cli/src/subcommands/menu/menus/index.ts +1 -0
  27. package/cli/src/subcommands/menu/menus/tunnels.ts +25 -67
  28. package/cli/src/subcommands/menu/render.ts +2 -2
  29. package/cli/src/subcommands/menu/tests.ts +1 -1
  30. package/cli/src/subcommands/menu/tunnels.ts +10 -99
  31. package/cli/src/subcommands/menu/types.ts +8 -0
  32. package/cli/src/subcommands/menu.ts +32 -524
  33. package/cli/src/subcommands/system.ts +58 -36
  34. package/cli/src/subcommands/tunnel.ts +124 -33
  35. package/cli/src/templates/index.ts +122 -0
  36. package/cli/src/tui/App.tsx +197 -0
  37. package/cli/src/tui/AppInspector.tsx +114 -0
  38. package/cli/src/tui/HomeStatus.tsx +59 -0
  39. package/cli/src/tui/brand.tsx +20 -0
  40. package/cli/src/tui/format.ts +22 -0
  41. package/cli/src/tui/index.mts +6 -0
  42. package/cli/src/tui/liveTree.ts +40 -0
  43. package/cli/src/tui/package.json +3 -0
  44. package/cli/src/tui/runMenu.tsx +57 -0
  45. package/cli/src/tui/session.mts +382 -0
  46. package/cli/src/tui/snapshot.ts +146 -0
  47. package/cli/src/utils/analyze.ts +27 -43
  48. package/cli/src/utils/framework-output.ts +171 -0
  49. package/cli/src/utils/launchDomainking.ts +64 -0
  50. package/docs/AGENTS.md +113 -146
  51. package/docs/MENU_STRUCTURE.md +56 -288
  52. package/docs/README.md +6 -6
  53. package/package.json +18 -35
  54. package/scripts/tunnel/client-improved.js +127 -38
  55. package/scripts/tunnel/client.js +118 -0
  56. package/assets/cli-screenshot.png +0 -0
@@ -0,0 +1,171 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import type { FrameworkInfo } from "./analyze";
4
+
5
+ export type FrameworkOutputMode = "standalone" | "export" | "static" | "unknown";
6
+
7
+ export type FrameworkOutputInfo = {
8
+ framework: "nextjs" | "vite" | "cra";
9
+ mode: FrameworkOutputMode;
10
+ distDir?: string;
11
+ configPath?: string;
12
+ };
13
+
14
+ export type FrameworkOutputCheck = {
15
+ framework: "nextjs" | "vite" | "cra";
16
+ mode: FrameworkOutputMode;
17
+ expectedPaths: string[];
18
+ summary: string;
19
+ guidance: string[];
20
+ };
21
+
22
+ function findNextConfigPath(dir: string): string | null {
23
+ const nextConfigPath = join(dir, "next.config.ts");
24
+ const nextConfigJsPath = join(dir, "next.config.js");
25
+ const nextConfigMjsPath = join(dir, "next.config.mjs");
26
+ if (existsSync(nextConfigPath)) return nextConfigPath;
27
+ if (existsSync(nextConfigJsPath)) return nextConfigJsPath;
28
+ if (existsSync(nextConfigMjsPath)) return nextConfigMjsPath;
29
+ return null;
30
+ }
31
+
32
+ function parseNextConfigContent(content: string): { mode: FrameworkOutputMode; distDir?: string } {
33
+ const outputMatches = [...content.matchAll(/output\s*:\s*["'](standalone|export)["']/g)];
34
+ const mode = outputMatches.length > 0 ? (outputMatches[outputMatches.length - 1][1] as FrameworkOutputMode) : "unknown";
35
+ const distMatches = [...content.matchAll(/distDir\s*:\s*["']([^"']+)["']/g)];
36
+ const distDir = distMatches.length > 0 ? distMatches[distMatches.length - 1][1] : undefined;
37
+ return { mode, distDir };
38
+ }
39
+
40
+ function detectNextOutput(dir: string): FrameworkOutputInfo {
41
+ const configPath = findNextConfigPath(dir);
42
+ if (!configPath) {
43
+ return { framework: "nextjs", mode: "unknown" };
44
+ }
45
+ const content = readFileSync(configPath, "utf8");
46
+ const parsed = parseNextConfigContent(content);
47
+ return {
48
+ framework: "nextjs",
49
+ mode: parsed.mode,
50
+ distDir: parsed.distDir,
51
+ configPath,
52
+ };
53
+ }
54
+
55
+ function findViteConfigPath(dir: string): string | null {
56
+ const viteTs = join(dir, "vite.config.ts");
57
+ const viteJs = join(dir, "vite.config.js");
58
+ const viteMjs = join(dir, "vite.config.mjs");
59
+ if (existsSync(viteTs)) return viteTs;
60
+ if (existsSync(viteJs)) return viteJs;
61
+ if (existsSync(viteMjs)) return viteMjs;
62
+ return null;
63
+ }
64
+
65
+ function parseViteConfigContent(content: string): { distDir?: string } {
66
+ const outDirMatches = [...content.matchAll(/outDir\s*:\s*["']([^"']+)["']/g)];
67
+ const distDir = outDirMatches.length > 0 ? outDirMatches[outDirMatches.length - 1][1] : undefined;
68
+ return { distDir };
69
+ }
70
+
71
+ function detectViteOutput(dir: string): FrameworkOutputInfo {
72
+ const configPath = findViteConfigPath(dir);
73
+ if (!configPath) {
74
+ return { framework: "vite", mode: "static" };
75
+ }
76
+ const content = readFileSync(configPath, "utf8");
77
+ const parsed = parseViteConfigContent(content);
78
+ return {
79
+ framework: "vite",
80
+ mode: "static",
81
+ distDir: parsed.distDir,
82
+ configPath,
83
+ };
84
+ }
85
+
86
+ function detectCraOutput(): FrameworkOutputInfo {
87
+ return { framework: "cra", mode: "static", distDir: "build" };
88
+ }
89
+
90
+ export function detectFrameworkOutput(
91
+ dir: string,
92
+ framework: FrameworkInfo | null
93
+ ): FrameworkOutputInfo | null {
94
+ if (!framework) return null;
95
+ if (framework.name === "nextjs") {
96
+ return detectNextOutput(dir);
97
+ }
98
+ if (framework.name === "vite") {
99
+ return detectViteOutput(dir);
100
+ }
101
+ if (framework.name === "cra") {
102
+ return detectCraOutput();
103
+ }
104
+ return null;
105
+ }
106
+
107
+ export function getFrameworkOutputCheck(info: FrameworkOutputInfo | null): FrameworkOutputCheck | null {
108
+ if (!info) return null;
109
+ if (info.framework === "nextjs") {
110
+ const distDir = info.distDir || ".next";
111
+ if (info.mode === "standalone") {
112
+ return {
113
+ framework: "nextjs",
114
+ mode: info.mode,
115
+ expectedPaths: [`${distDir}/standalone`, `${distDir}/static`],
116
+ summary: "Next.js standalone output detected.",
117
+ guidance: [],
118
+ };
119
+ }
120
+ if (info.mode === "export") {
121
+ return {
122
+ framework: "nextjs",
123
+ mode: info.mode,
124
+ expectedPaths: ["out"],
125
+ summary: "Next.js output is set to export.",
126
+ guidance: [
127
+ "Uplink hosting expects a server build by default.",
128
+ "Either set output: \"standalone\" in next.config.*",
129
+ "Or provide a Dockerfile that serves the static out/ directory.",
130
+ ],
131
+ };
132
+ }
133
+ return {
134
+ framework: "nextjs",
135
+ mode: "unknown",
136
+ expectedPaths: [`${distDir}/standalone`, `${distDir}/static`],
137
+ summary: "Next.js output mode not detected.",
138
+ guidance: [
139
+ "Uplink hosting expects output: \"standalone\" by default.",
140
+ "Add output: \"standalone\" to next.config.* for server hosting.",
141
+ ],
142
+ };
143
+ }
144
+ if (info.framework === "vite") {
145
+ const distDir = info.distDir || "dist";
146
+ return {
147
+ framework: "vite",
148
+ mode: "static",
149
+ expectedPaths: [distDir],
150
+ summary: "Vite static build detected.",
151
+ guidance: [
152
+ `Build output is expected in ${distDir}/.`,
153
+ "Provide a Dockerfile that serves the static files (e.g., nginx or a static file server).",
154
+ ],
155
+ };
156
+ }
157
+ if (info.framework === "cra") {
158
+ const distDir = info.distDir || "build";
159
+ return {
160
+ framework: "cra",
161
+ mode: "static",
162
+ expectedPaths: [distDir],
163
+ summary: "Create React App static build detected.",
164
+ guidance: [
165
+ `Build output is expected in ${distDir}/.`,
166
+ "Provide a Dockerfile that serves the static files (e.g., nginx or a static file server).",
167
+ ],
168
+ };
169
+ }
170
+ return null;
171
+ }
@@ -0,0 +1,64 @@
1
+ import { spawnSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+
6
+ function projectRoot(): string {
7
+ return join(__dirname, "../..");
8
+ }
9
+
10
+ function resolveTsx(): string {
11
+ const root = projectRoot();
12
+ try {
13
+ return require.resolve("tsx/dist/cli.cjs", { paths: [root] });
14
+ } catch {
15
+ try {
16
+ return require.resolve("tsx/cli", { paths: [root] });
17
+ } catch {
18
+ return "tsx";
19
+ }
20
+ }
21
+ }
22
+
23
+ /** Domainking stays its own package; we spawn it, we do not import it. */
24
+ export function resolveDomainkingEntry(): string | null {
25
+ if (process.env.DOMAINKING_ENTRY && existsSync(process.env.DOMAINKING_ENTRY)) {
26
+ return process.env.DOMAINKING_ENTRY;
27
+ }
28
+ const candidates = [
29
+ join(homedir(), "domainking", "src", "index.tsx"),
30
+ join(projectRoot(), "..", "domainking", "src", "index.tsx"),
31
+ join(projectRoot(), "..", "..", "domainking", "src", "index.tsx"),
32
+ ];
33
+ return candidates.find((path) => existsSync(path)) ?? null;
34
+ }
35
+
36
+ export function launchDomainking(): string {
37
+ const entry = resolveDomainkingEntry();
38
+ if (!entry) {
39
+ return [
40
+ "Domain search TUI is not bundled with uplink-cli.",
41
+ "",
42
+ "Agent-friendly commands (no TUI needed):",
43
+ " uplink domains list --json",
44
+ " uplink domains check example.com --json",
45
+ " uplink host domains add --id app_xxx --hostname example.com --json",
46
+ " uplink host domains verify --id app_xxx --hostname example.com --json",
47
+ "",
48
+ "Optional: set DOMAINKING_ENTRY to a Domainking src/index.tsx for the search UI.",
49
+ ].join("\n");
50
+ }
51
+
52
+ const result = spawnSync(resolveTsx(), [entry], {
53
+ stdio: "inherit",
54
+ cwd: join(entry, "..", ".."),
55
+ env: process.env,
56
+ });
57
+ if (result.error) {
58
+ throw result.error;
59
+ }
60
+ if (result.status && result.status !== 0) {
61
+ return "Domain search exited.";
62
+ }
63
+ return "Back from domain search. Attach a hostname with Domains › Attach to app.";
64
+ }
package/docs/AGENTS.md CHANGED
@@ -1,194 +1,161 @@
1
1
  # Agent Integration Guide
2
2
 
3
- For agents (Cursor/Claude/GPT/Windsurf) to use Uplink non-interactively.
3
+ For agents (Cursor, Claude Code, Codex, Windsurf, and similar) to use Uplink **non-interactively**.
4
+
5
+ Install: `npm install -g uplink-cli` or `npx uplink-cli …`
6
+ Package name: `uplink-cli` · Binary: `uplink`
4
7
 
5
8
  ## Auth
6
- - Use `AGENTCLOUD_TOKEN` (bearer). Avoid argv; prefer stdin:
9
+
10
+ - Use `AGENTCLOUD_TOKEN` (bearer). Prefer stdin over argv:
7
11
  ```bash
8
- echo "$TOKEN" | uplink --token-stdin ...
12
+ echo "$TOKEN" | uplink --token-stdin
9
13
  ```
10
- - API base override: `--api-base https://api.uplink.spot` (or `AGENTCLOUD_API_BASE`).
14
+ - API base: `--api-base https://api.uplink.spot` or `AGENTCLOUD_API_BASE`.
11
15
 
12
16
  ## Signup (no auth required)
17
+
13
18
  ```bash
14
19
  uplink signup --json
15
20
  uplink signup --label "cursor-agent" --expires-days 30 --json
16
21
  ```
17
- JSON example:
18
- ```json
19
- {
20
- "id": "tok_xxx",
21
- "token": "abc123...",
22
- "tokenPrefix": "abc123",
23
- "role": "user",
24
- "userId": "user_xxx",
25
- "label": "cursor-agent",
26
- "createdAt": "2025-01-01T00:00:00.000Z",
27
- "expiresAt": "2025-01-31T00:00:00.000Z",
28
- "message": "Token created successfully. Save this token securely..."
29
- }
22
+
23
+ Save `token` from the JSON — it is shown only once. Then:
24
+
25
+ ```bash
26
+ export AGENTCLOUD_TOKEN='…'
30
27
  ```
31
- Save `token`—shown only once.
32
28
 
33
29
  ## Machine-mode contract
34
- - `--json` → stdout = JSON only; stderr = logs/errors.
35
- - Exit codes: 0 ok; 2 usage; 10 auth missing/invalid; 20 network; 30 server/unknown.
36
- - Premium alias gating: alias commands may return `ALIAS_NOT_ENABLED` / `ALIAS_LIMIT_REACHED`.
37
30
 
38
- ## Core CLI flows (non-interactive)
31
+ | Rule | Detail |
32
+ |------|--------|
33
+ | `--json` | stdout = JSON only; logs/errors go to stderr |
34
+ | Exit `0` | success |
35
+ | Exit `2` | usage / bad args |
36
+ | Exit `10` | auth missing/invalid |
37
+ | Exit `20` | network |
38
+ | Exit `30` | server / unknown |
39
+
40
+ Premium aliases may return `ALIAS_NOT_ENABLED` / `ALIAS_LIMIT_REACHED`.
41
+
42
+ ## Tunnels (share localhost)
43
+
44
+ `tunnel create` **creates the API record and starts the local client** so the public URL works. Use `--api-only` only if you will start the client yourself.
45
+
39
46
  ```bash
40
- # Create tunnel (optional alias if enabled)
41
- echo "$TOKEN" | uplink --token-stdin --api-base https://api.uplink.spot \
47
+ # Create + start client (optional alias if enabled)
48
+ echo "$TOKEN" | uplink --token-stdin \
42
49
  tunnel create --port 3000 --alias myapp --json
43
50
 
44
- # List tunnels (includes connection status)
51
+ # List (includes connected status)
45
52
  echo "$TOKEN" | uplink --token-stdin tunnel list --json
46
53
 
47
- # Set alias on tunnel
54
+ # Alias on an existing tunnel
48
55
  echo "$TOKEN" | uplink --token-stdin tunnel alias-set --id tun_xxx --alias myapp --json
49
-
50
- # Delete alias from tunnel
51
56
  echo "$TOKEN" | uplink --token-stdin tunnel alias-delete --id tun_xxx --json
52
57
 
53
- # Stats
58
+ # Stats / stop
54
59
  echo "$TOKEN" | uplink --token-stdin tunnel stats --id tun_xxx --json
55
-
56
- # Stop/delete tunnel
57
60
  echo "$TOKEN" | uplink --token-stdin tunnel stop --id tun_xxx --json
61
+ echo "$TOKEN" | uplink --token-stdin tunnel stop --all --json
62
+ ```
63
+
64
+ JSON create shape (representative):
65
+
66
+ ```json
67
+ {
68
+ "tunnel": { "id": "tun_…", "url": "https://abc.x.uplink.spot", "token": "…", "status": "…" },
69
+ "alias": "myapp",
70
+ "aliasError": null,
71
+ "url": "https://myapp.uplink.spot",
72
+ "client": { "pid": 12345, "started": true }
73
+ }
58
74
  ```
59
75
 
60
- ## Hosting flows (non-interactive)
61
- Use `--json` for machine-mode output. For interactive prompts, pass `--yes`.
76
+ `connected` on `tunnel list` means the local client is attached to the relay.
77
+
78
+ ## Hosting
79
+
80
+ Use `--json`. For prompts, pass `--yes`.
62
81
 
63
82
  ```bash
64
- # Full setup (analyze + init + create + deploy)
65
83
  echo "$TOKEN" | uplink --token-stdin host setup \
66
- --path /path/to/app \
67
- --name myapp \
68
- --env-file /path/to/.env \
69
- --wait-timeout 900 \
70
- --wait-interval 5 \
71
- --yes \
72
- --json
73
-
74
- # Deploy only (Dockerfile-based app)
84
+ --path /path/to/app --name myapp --env-file /path/to/.env \
85
+ --wait-timeout 900 --wait-interval 5 --yes --json
86
+
75
87
  echo "$TOKEN" | uplink --token-stdin host deploy \
76
- --path /path/to/app \
77
- --name myapp \
78
- --env-file /path/to/.env \
79
- --wait \
80
- --wait-timeout 900 \
81
- --wait-interval 5 \
82
- --json
83
-
84
- # Analyze project
85
- echo "$TOKEN" | uplink --token-stdin host analyze --path /path/to/app --json
88
+ --path /path/to/app --name myapp --wait --json
86
89
 
87
- # List hosted apps
90
+ echo "$TOKEN" | uplink --token-stdin host analyze --path /path/to/app --json
91
+ echo "$TOKEN" | uplink --token-stdin host preflight --path /path/to/app --json
88
92
  echo "$TOKEN" | uplink --token-stdin host list --json
89
-
90
- # Status + logs
91
93
  echo "$TOKEN" | uplink --token-stdin host status --id app_xxx --json
92
94
  echo "$TOKEN" | uplink --token-stdin host logs --id app_xxx --json
93
-
94
- # Delete app
95
95
  echo "$TOKEN" | uplink --token-stdin host delete --id app_xxx --yes --json
96
96
  ```
97
97
 
98
98
  Notes:
99
- - `host logs` returns `NOT_READY` until a deployment is running.
100
- - If builds stay `queued`, check builder/runner services and build logs on the server.
101
- - For Prisma apps, ensure the Dockerfile copies `prisma/schema.prisma` before `npm ci` and runs `npx prisma generate` before `npm run build`.
102
- - Use a `.uplinkignore` file to keep tarballs small. Typical entries include `node_modules`, `.next`, `dist`, `build`, `coverage`, `.turbo`, `.cache`, `.vercel`, `*.log`, and any local SQLite `.db` files.
103
- - `host list` (non-JSON) prints two lines per app: `- name (app_id)` and the URL on the next line.
104
- - `host delete` requires typing `DELETE` unless `--yes` is provided.
105
-
106
- ## Interactive menu notes
107
- - All selection lists use arrow keys + Enter (no numeric entry required).
108
- - Hosted app lists display the app URL alongside name and ID.
109
-
110
- ### JSON shapes (representative)
111
- - Create: `{ "tunnel": { id, url?, token?, alias?, aliasUrl?, targetPort, status, connected?, createdAt }, "alias": "myapp"|null, "aliasError": "..."|null }`
112
- - List: `{ "tunnels": [ { id, url?, token?, alias?, aliasUrl?, targetPort, status, connected, createdAt } ], "count": n }`
113
- - Stats: alias tunnels include persisted totals + relay overlay; token-only tunnels show in-memory relay stats.
114
-
115
- **Note:** The `connected` field indicates whether the tunnel is actually connected to the relay server (verified via socket health check).
116
-
117
- ## HTTP API Reference
118
-
119
- Auth: `Authorization: Bearer <AGENTCLOUD_TOKEN>`
120
-
121
- ### Tunnels
122
- | Method | Endpoint | Description |
123
- |--------|----------|-------------|
124
- | `POST` | `/v1/tunnels` | Create tunnel (body: `{ port, alias? }`) |
125
- | `GET` | `/v1/tunnels` | List user's tunnels (includes `connected` status) |
126
- | `GET` | `/v1/tunnels/{id}` | Get tunnel details |
127
- | `GET` | `/v1/tunnels/{id}/stats` | Get tunnel statistics |
128
- | `DELETE` | `/v1/tunnels/{id}` | Delete tunnel |
129
- | `POST` | `/v1/tunnels/{id}/alias` | Set alias on tunnel (body: `{ alias }`) |
130
- | `DELETE` | `/v1/tunnels/{id}/alias` | Remove alias from tunnel |
131
-
132
- ### Port-Based Aliases (Premium)
133
- Aliases are now **port-based**: they persist across tunnel restarts and always point to the same port.
134
-
135
- | Method | Endpoint | Description |
136
- |--------|----------|-------------|
137
- | `GET` | `/v1/tunnels/aliases` | List all aliases for user |
138
- | `POST` | `/v1/tunnels/aliases` | Create alias for port (body: `{ alias, port }`) |
139
- | `PUT` | `/v1/tunnels/aliases/{alias}` | Reassign alias to different port (body: `{ port }`) |
140
- | `DELETE` | `/v1/tunnels/aliases/{alias}` | Delete alias |
141
-
142
- ### Example: Create port-based alias
143
- ```bash
144
- curl -X POST https://api.uplink.spot/v1/tunnels/aliases \
145
- -H "Authorization: Bearer $TOKEN" \
146
- -H "Content-Type: application/json" \
147
- -d '{"alias": "myapp", "port": 3000}'
148
- ```
99
+ - Next.js server hosting expects `output: "standalone"`. Vite/CRA static `dist`/`build`.
100
+ - Prefer a `.uplinkignore` (`node_modules`, `.next`, `dist`, `*.log`, local `.db`, …).
101
+ - `host logs` may return `NOT_READY` until a deployment is running.
102
+ - `host delete` requires `--yes` (or typing `DELETE` interactively).
149
103
 
150
- Response:
151
- ```json
152
- {
153
- "id": "alias_xxx",
154
- "alias": "myapp",
155
- "targetPort": 3000,
156
- "url": "https://myapp.uplink.spot",
157
- "createdAt": "2025-01-03T00:00:00.000Z"
158
- }
104
+ ## Custom domains
105
+
106
+ Registrar inventory is CLI. Attach/verify is under `host domains`.
107
+ The bare `uplink domains` search TUI is **optional** and not bundled with npm — use the JSON commands below.
108
+
109
+ ```bash
110
+ uplink domains providers connect godaddy --token-env GODADDY_PAT --json
111
+ uplink domains providers connect cloudflare --token-env CF_API_TOKEN --json
112
+ uplink domains providers connect hostinger --token-env HOSTINGER_API_TOKEN --json
113
+ uplink domains providers connect namecheap --token-env NAMECHEAP_API_KEY --user-env NAMECHEAP_API_USER --json
114
+ uplink domains providers list --json
115
+ uplink domains providers disconnect godaddy --json
116
+
117
+ uplink domains list --json
118
+ uplink domains check example.com --json
119
+
120
+ echo "$TOKEN" | uplink --token-stdin host domains add --id app_xxx --hostname example.com --json
121
+ echo "$TOKEN" | uplink --token-stdin host domains verify --id app_xxx --hostname example.com --json
122
+ echo "$TOKEN" | uplink --token-stdin host domains list --id app_xxx --json
123
+ echo "$TOKEN" | uplink --token-stdin host domains remove --id app_xxx --hostname example.com --json
159
124
  ```
160
125
 
161
- ## Domains
162
- - Public tunnels: `https://<token>.x.uplink.spot`
163
- - Permanent URLs (aliases): `https://<alias>.uplink.spot`
126
+ Do not treat RDAP “available” as buyable unless `domains check` says `buyable: true`. Purchase is not wired yet.
164
127
 
165
- ## Server Deployment (for self-hosted)
166
- Control plane + hosting runtime live in the private repo `uplink-hosting-runtime`.
128
+ ## Databases (optional)
167
129
 
168
- ### Deploy code changes
169
130
  ```bash
170
- # On server (via SSH)
171
- cd /opt/uplink-hosting-runtime
172
- git pull origin main
173
- systemctl restart backend-api
174
- systemctl restart tunnel-relay
175
- systemctl restart uplink-builder uplink-runner uplink-router # if hosting runtime changed
131
+ echo "$TOKEN" | uplink --token-stdin db create --name mydb --project myproj --json
132
+ echo "$TOKEN" | uplink --token-stdin db list --json
133
+ echo "$TOKEN" | uplink --token-stdin db info --id db_xxx --json
134
+ echo "$TOKEN" | uplink --token-stdin db delete --id db_xxx --yes --json
176
135
  ```
177
136
 
178
- ### Services
179
- | Service | Command | Description |
180
- |---------|---------|-------------|
181
- | `backend-api` | `systemctl status backend-api` | REST API server |
182
- | `tunnel-relay` | `systemctl status tunnel-relay` | WebSocket relay for tunnels |
183
- | `uplink-builder` | `systemctl status uplink-builder` | Builds Docker images |
184
- | `uplink-runner` | `systemctl status uplink-runner` | Runs containers |
185
- | `uplink-router` | `systemctl status uplink-router` | Routes host domains |
186
-
187
- ### Relay health check
188
- The relay exposes internal endpoints (protected by `RELAY_INTERNAL_SECRET`):
189
- - `/internal/connected-tokens` - List connected tunnel tokens (with socket health verification)
190
- - `/internal/traffic-stats` - Traffic statistics by token/alias
191
- - `/health` - Basic health check
192
-
193
- ## Optional JS helper
194
- Use `cli/src/agents/tunnels-client.ts` for the same retry/timeout behavior as the CLI.
137
+ ## Failure modes agents should expect
138
+
139
+ | Symptom | Likely cause |
140
+ |---------|----------------|
141
+ | URL 502 / not connected | Local process on `--port` not running, or client died — re-run `tunnel create` or check `tunnel list` |
142
+ | Auth errors | Missing/invalid `AGENTCLOUD_TOKEN`; use `--token-stdin` |
143
+ | `ALIAS_NOT_ENABLED` | Account does not have permanent aliases |
144
+ | Domain search TUI missing | Expected on npm — use `domains list` / `check` / `host domains *` |
145
+ | Hosting stuck `queued` | Edge builder/runner issue — check `host status` / `host logs` |
146
+
147
+ ## Interactive menu
148
+
149
+ Humans: `uplink` or `uplink menu` (Share · Hosting · Domains).
150
+ Agents should prefer the non-interactive commands above.
151
+
152
+ ## URLs
153
+
154
+ - Ephemeral tunnels: `https://<token>.x.uplink.spot`
155
+ - Aliases: `https://<alias>.uplink.spot`
156
+
157
+ ## More
158
+
159
+ - Menu map: `docs/MENU_STRUCTURE.md`
160
+ - Website: https://uplink.spot
161
+ - npm: https://www.npmjs.com/package/uplink-cli