uplink-cli 0.1.38 → 0.2.0

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 (67) hide show
  1. package/AGENTS.md +177 -0
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE +21 -0
  4. package/README.md +72 -52
  5. package/cli/src/index.ts +16 -3
  6. package/cli/src/registrars/cloudflare.ts +148 -0
  7. package/cli/src/registrars/dreamhost.ts +129 -0
  8. package/cli/src/registrars/godaddy.ts +105 -0
  9. package/cli/src/registrars/hostinger.ts +106 -0
  10. package/cli/src/registrars/http.ts +18 -0
  11. package/cli/src/registrars/index.ts +32 -0
  12. package/cli/src/registrars/namecheap.ts +163 -0
  13. package/cli/src/registrars/secret.ts +66 -0
  14. package/cli/src/registrars/store.ts +55 -0
  15. package/cli/src/registrars/types.ts +42 -0
  16. package/cli/src/subcommands/admin.ts +17 -30
  17. package/cli/src/subcommands/db.ts +63 -57
  18. package/cli/src/subcommands/dev.ts +23 -25
  19. package/cli/src/subcommands/domains.ts +295 -0
  20. package/cli/src/subcommands/host-domains.ts +148 -0
  21. package/cli/src/subcommands/host.ts +3 -0
  22. package/cli/src/subcommands/login.ts +85 -0
  23. package/cli/src/subcommands/menu/colors.ts +1 -1
  24. package/cli/src/subcommands/menu/effects/tunnel-clients.ts +87 -14
  25. package/cli/src/subcommands/menu/inline-tree-select.ts +6 -5
  26. package/cli/src/subcommands/menu/io.ts +27 -5
  27. package/cli/src/subcommands/menu/menus/domain-check.ts +34 -0
  28. package/cli/src/subcommands/menu/menus/domains.ts +197 -0
  29. package/cli/src/subcommands/menu/menus/hosting.ts +14 -46
  30. package/cli/src/subcommands/menu/menus/index.ts +1 -0
  31. package/cli/src/subcommands/menu/menus/tunnels.ts +25 -67
  32. package/cli/src/subcommands/menu/render.ts +2 -2
  33. package/cli/src/subcommands/menu/requests.ts +9 -2
  34. package/cli/src/subcommands/menu/tests.ts +1 -1
  35. package/cli/src/subcommands/menu/tunnels.ts +10 -99
  36. package/cli/src/subcommands/menu/types.ts +8 -0
  37. package/cli/src/subcommands/menu.ts +32 -524
  38. package/cli/src/subcommands/signup.ts +2 -2
  39. package/cli/src/subcommands/system.ts +58 -36
  40. package/cli/src/subcommands/tunnel.ts +126 -33
  41. package/cli/src/templates/index.ts +3 -3
  42. package/cli/src/tui/App.tsx +202 -0
  43. package/cli/src/tui/AppInspector.tsx +114 -0
  44. package/cli/src/tui/HomeStatus.tsx +92 -0
  45. package/cli/src/tui/brand.tsx +20 -0
  46. package/cli/src/tui/format.ts +22 -0
  47. package/cli/src/tui/index.mts +6 -0
  48. package/cli/src/tui/liveTree.ts +40 -0
  49. package/cli/src/tui/package.json +3 -0
  50. package/cli/src/tui/runMenu.tsx +57 -0
  51. package/cli/src/tui/session.mts +267 -0
  52. package/cli/src/tui/snapshot.ts +175 -0
  53. package/cli/src/utils/api-base.ts +11 -0
  54. package/cli/src/utils/credentials.ts +58 -0
  55. package/cli/src/utils/domain-availability.ts +56 -0
  56. package/cli/src/utils/guest-access.ts +38 -0
  57. package/cli/src/utils/launchDomainking.ts +64 -0
  58. package/cli/src/utils/login-flow.ts +57 -0
  59. package/docs/AGENTS.md +130 -148
  60. package/docs/HOSTING.md +55 -0
  61. package/docs/MENU_STRUCTURE.md +60 -288
  62. package/docs/PRODUCT.md +64 -0
  63. package/docs/README.md +11 -7
  64. package/package.json +22 -36
  65. package/scripts/tunnel/client-improved.js +127 -38
  66. package/scripts/tunnel/client.js +118 -0
  67. package/assets/cli-screenshot.png +0 -0
package/AGENTS.md ADDED
@@ -0,0 +1,177 @@
1
+ # Agent Integration Guide
2
+
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
+ Until **0.2.0** is on npm: `npm install -g github:firstprinciplecode/uplink#v0.2.0`
7
+ Package name: `uplink-cli` · Binary: `uplink`
8
+
9
+ ## Auth
10
+
11
+ - `uplink tunnel create` automatically creates guest access when no token exists. Guest access includes **1 active tunnel**, expiring after **24 hours**.
12
+ - Use `AGENTCLOUD_TOKEN` (bearer). Prefer stdin over argv:
13
+ ```bash
14
+ echo "$TOKEN" | uplink --token-stdin …
15
+ ```
16
+ - Humans: `uplink login --email you@example.com` then `--code 123456`. This upgrades current guest access, preserves its tunnel, and unlocks persistent features. Credentials are saved to `~/.uplink/credentials` (chmod 600).
17
+ - API base: `--api-base https://api.uplink.spot` or `AGENTCLOUD_API_BASE`.
18
+
19
+ ## Explicit guest token (optional)
20
+
21
+ ```bash
22
+ uplink signup --json
23
+ uplink signup --label "cursor-agent" --expires-days 30 --json
24
+ ```
25
+
26
+ Save `token` from the JSON — it is shown only once. Then:
27
+
28
+ ```bash
29
+ export AGENTCLOUD_TOKEN='…'
30
+ ```
31
+
32
+ Explicit signup creates guest access. A later email login upgrades that guest account when the current token is available.
33
+
34
+ ## Machine-mode contract
35
+
36
+ | Rule | Detail |
37
+ |------|--------|
38
+ | `--json` | stdout = JSON only; logs/errors go to stderr |
39
+ | Exit `0` | success |
40
+ | Exit `2` | usage / bad args |
41
+ | Exit `10` | auth missing/invalid |
42
+ | Exit `20` | network |
43
+ | Exit `30` | server / unknown |
44
+
45
+ Guest accounts can share one local port and use public domain search. Hosting, databases, aliases, and custom domains require a verified email account. Gate error: `ACCOUNT_VERIFICATION_REQUIRED`.
46
+
47
+ Premium aliases may return `ALIAS_NOT_ENABLED` / `ALIAS_LIMIT_REACHED`.
48
+
49
+ Free hosting (new accounts): **1 app**, **100 MB** of live artifacts, **no custom domains**, app **sleeps after 30 minutes idle**. Errors: `HOST_APP_LIMIT_REACHED`, `HOST_STORAGE_LIMIT_REACHED`, `HOST_DOMAIN_NOT_ENABLED`. Check quota: the `hosting` object on `GET /v1/me`.
50
+
51
+ ## Tunnels (share localhost)
52
+
53
+ `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.
54
+
55
+ ```bash
56
+ # Create + start client (optional alias if enabled)
57
+ echo "$TOKEN" | uplink --token-stdin \
58
+ tunnel create --port 3000 --alias myapp --json
59
+
60
+ # List (includes connected status)
61
+ echo "$TOKEN" | uplink --token-stdin tunnel list --json
62
+
63
+ # Alias on an existing tunnel
64
+ echo "$TOKEN" | uplink --token-stdin tunnel alias-set --id tun_xxx --alias myapp --json
65
+ echo "$TOKEN" | uplink --token-stdin tunnel alias-delete --id tun_xxx --json
66
+
67
+ # Stats / stop
68
+ echo "$TOKEN" | uplink --token-stdin tunnel stats --id tun_xxx --json
69
+ echo "$TOKEN" | uplink --token-stdin tunnel stop --id tun_xxx --json
70
+ echo "$TOKEN" | uplink --token-stdin tunnel stop --all --json
71
+ ```
72
+
73
+ JSON create shape (representative):
74
+
75
+ ```json
76
+ {
77
+ "tunnel": { "id": "tun_…", "url": "https://abc.x.uplink.spot", "token": "…", "status": "…" },
78
+ "alias": "myapp",
79
+ "aliasError": null,
80
+ "url": "https://myapp.uplink.spot",
81
+ "client": { "pid": 12345, "started": true }
82
+ }
83
+ ```
84
+
85
+ `connected` on `tunnel list` means the local client is attached to the relay.
86
+
87
+ ## Hosting
88
+
89
+ Use `--json`. For prompts, pass `--yes`.
90
+
91
+ ```bash
92
+ echo "$TOKEN" | uplink --token-stdin host setup \
93
+ --path /path/to/app --name myapp --env-file /path/to/.env \
94
+ --wait-timeout 900 --wait-interval 5 --yes --json
95
+
96
+ echo "$TOKEN" | uplink --token-stdin host deploy \
97
+ --path /path/to/app --name myapp --wait --json
98
+
99
+ echo "$TOKEN" | uplink --token-stdin host analyze --path /path/to/app --json
100
+ echo "$TOKEN" | uplink --token-stdin host preflight --path /path/to/app --json
101
+ echo "$TOKEN" | uplink --token-stdin host list --json
102
+ echo "$TOKEN" | uplink --token-stdin host status --id app_xxx --json
103
+ echo "$TOKEN" | uplink --token-stdin host logs --id app_xxx --json
104
+ echo "$TOKEN" | uplink --token-stdin host delete --id app_xxx --yes --json
105
+ ```
106
+
107
+ Notes:
108
+ - Next.js server hosting expects `output: "standalone"`. Vite/CRA → static `dist`/`build`.
109
+ - Prefer a `.uplinkignore` (`node_modules`, `.next`, `dist`, `*.log`, local `.db`, …).
110
+ - `host logs` may return `NOT_READY` until a deployment is running.
111
+ - `host delete` requires `--yes` (or typing `DELETE` interactively).
112
+
113
+ ## Custom domains
114
+
115
+ Registrar inventory is CLI. Attach/verify is under `host domains`.
116
+ The bare `uplink domains` search TUI is **optional** and not bundled with npm — use the JSON commands below.
117
+
118
+ ```bash
119
+ uplink domains providers connect godaddy --token-env GODADDY_PAT --json
120
+ uplink domains providers connect cloudflare --token-env CF_API_TOKEN --json
121
+ uplink domains providers connect hostinger --token-env HOSTINGER_API_TOKEN --json
122
+ uplink domains providers connect dreamhost --token-env DREAMHOST_API_KEY --json
123
+ uplink domains providers connect namecheap --token-env NAMECHEAP_API_KEY --user-env NAMECHEAP_API_USER --json
124
+ uplink domains providers list --json
125
+ uplink domains providers disconnect godaddy --json
126
+
127
+ uplink domains list --json
128
+ uplink domains check example.com --json
129
+
130
+ echo "$TOKEN" | uplink --token-stdin host domains add --id app_xxx --hostname example.com --json
131
+ echo "$TOKEN" | uplink --token-stdin host domains verify --id app_xxx --hostname example.com --json
132
+ echo "$TOKEN" | uplink --token-stdin host domains list --id app_xxx --json
133
+ echo "$TOKEN" | uplink --token-stdin host domains remove --id app_xxx --hostname example.com --json
134
+ ```
135
+
136
+ `domains check` works without a registrar: it falls back to public DNS/RDAP and returns `provider: "public"` with no price. Do not treat RDAP “available” as buyable unless `domains check` says `buyable: true`. Purchase is not wired yet.
137
+
138
+ ## Databases (optional)
139
+
140
+ ```bash
141
+ echo "$TOKEN" | uplink --token-stdin db create --name mydb --project myproj --json
142
+ echo "$TOKEN" | uplink --token-stdin db list --json
143
+ echo "$TOKEN" | uplink --token-stdin db info --id db_xxx --json
144
+ echo "$TOKEN" | uplink --token-stdin db delete --id db_xxx --yes --json
145
+ ```
146
+
147
+ ## Failure modes agents should expect
148
+
149
+ | Symptom | Likely cause |
150
+ |---------|----------------|
151
+ | URL 502 / not connected | Local process on `--port` not running, or client died — re-run `tunnel create` or check `tunnel list` |
152
+ | Auth errors | Missing/invalid `AGENTCLOUD_TOKEN`; use `--token-stdin` |
153
+ | `ALIAS_NOT_ENABLED` | Account does not have permanent aliases |
154
+ | Domain search TUI missing | Expected on npm — use `domains list` / `check` / `host domains *` |
155
+ | `HOST_APP_LIMIT_REACHED` | Free plan is 1 hosted app — delete one or the account needs hosting granted |
156
+ | `HOST_STORAGE_LIMIT_REACHED` | Upload exceeds the 100 MB free hosting budget |
157
+ | `HOST_DOMAIN_NOT_ENABLED` | Custom domains are paid — `*.host.uplink.spot` still works |
158
+ | First request after idle is slow | Free apps sleep after 30 minutes; the router wakes them |
159
+ | Hosting stuck `queued` | Edge builder/runner issue — check `host status` / `host logs` |
160
+
161
+ ## Interactive menu
162
+
163
+ Humans: `uplink` or `uplink menu` (Share · Hosting · Domains).
164
+ Agents should prefer the non-interactive commands above.
165
+
166
+ ## URLs
167
+
168
+ - Ephemeral tunnels: `https://<token>.x.uplink.spot`
169
+ - Aliases: `https://<alias>.uplink.spot`
170
+
171
+ ## More
172
+
173
+ - Menu map: `docs/MENU_STRUCTURE.md`
174
+ - Hosting: `docs/HOSTING.md`
175
+ - Product: `docs/PRODUCT.md`
176
+ - Website: https://uplink.spot
177
+ - npm: https://www.npmjs.com/package/uplink-cli (0.2.0 pending; use the GitHub tag until then)
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-08-28
4
+
5
+ Tagged on GitHub. npm publish is pending org auth (`firstprinciplellc`). Until then install from the tag (see README).
6
+
7
+ - **Guest access** — `tunnel create` and the interactive menu mint a guest token when none exists (1 active tunnel, 24-hour expiry). Stored in `~/.uplink/credentials`.
8
+ - **Email login** — `uplink login --email` / `--code` upgrades that guest account, keeps the tunnel, and unlocks hosting, databases, and registrar inventory.
9
+ - **Public domain check** — `uplink domains check` works with no registrar (DNS + RDAP). Connect a registrar for price.
10
+ - **DreamHost** — inventory adapter; DNS-scoped keys and multiple `--token-env` names are supported.
11
+ - **GoDaddy** — stop falling back to the v1 API on rate-limit / auth errors.
12
+
13
+ ## 0.1.39 — 2026-08-27
14
+
15
+ Current version on npm. Agent-ready CLI: `--json`, `--token-stdin`, tunnels, hosting, domains.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 First Principle Code
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,81 +1,98 @@
1
1
  # Uplink CLI
2
2
 
3
- Share your local app with others for demos, testing, review, and quick feedback without deploying.
3
+ **Software for agents** share localhost, host apps, and attach domains from the terminal. Built for Cursor, Claude Code, Codex, Windsurf, and humans who live in a shell.
4
4
 
5
5
  ![Uplink CLI](./assets/cli-screenshot.png)
6
6
 
7
7
  ## Key features
8
- - **Expose any local port**: Turn `localhost:<port>` into a public HTTPS URL like `https://abc123.x.uplink.spot`
9
- - **Agent-first**: Works well with Cursor, Claude Code, Codex, Windsurf (and other agentic tools)
10
- - **Terminal-native**: Start/stop tunnels and manage URLs from an interactive menu
11
- - **No browser required**: Create an account + token from the CLI (`uplink signup`), then automate everything
12
- - **Open source CLI**: Inspect, extend, and contribute
13
-
14
- ## Why use Uplink
15
- - **Fastest way to share localhost**: Great for “can you look at this?” moments
16
- - **Works great with agents**: machine-readable `--json`, stable exit codes, and stdin token support
17
- - **Share links + optional permanent URLs**: Permanent URLs are available if enabled on your account
8
+ - **Share any local port**: `localhost:<port>` public HTTPS (`https://abc123.x.uplink.spot`)
9
+ - **Agent-first**: `--json`, stable exit codes, `--token-stdin` (no browser required)
10
+ - **Hosting**: deploy Next.js / Vite / static apps to Uplink
11
+ - **Domains**: list registrar inventory and attach custom hostnames to hosted apps
12
+ - **Interactive menu**: `uplink` for humans; CLI subcommands for agents
18
13
 
19
14
  Learn more at [uplink.spot](https://uplink.spot)
20
15
 
21
16
  ## Install
17
+
18
+ **npm (current published: 0.1.39)** — guest login and DreamHost land in **0.2.0**, tagged on GitHub, npm publish waiting on org auth.
19
+
22
20
  ```bash
21
+ # Until 0.2.0 is on npm:
22
+ npm install -g github:firstprinciplecode/uplink#v0.2.0
23
+
24
+ # After publish:
23
25
  npm install -g uplink-cli
24
- # or
25
26
  npx uplink-cli --help
26
27
  ```
27
28
 
28
- ## Authenticate (no browser)
29
+ Changelog: [CHANGELOG.md](./CHANGELOG.md). Product overview: [docs/PRODUCT.md](./docs/PRODUCT.md). Hosting: [docs/HOSTING.md](./docs/HOSTING.md).
30
+
31
+ ## Start without signup
29
32
  ```bash
30
- uplink signup --json # creates account + token
31
- # or interactive: uplink (Get Started)
32
- export AGENTCLOUD_TOKEN=your-token-here # save securely
33
+ # Guest token is created and saved automatically
34
+ uplink tunnel create --port 3000 --json
33
35
  ```
34
36
 
35
- ## Quick start (interactive)
37
+ Guest access includes one active tunnel for 24 hours and public domain search.
38
+
39
+ ## Unlock persistent features
36
40
  ```bash
37
- uplink # open menu
41
+ uplink login --email you@example.com
42
+ uplink login --email you@example.com --code 123456 --json
38
43
  ```
39
- - Start Tunnel → pick or enter port → get URL (e.g., https://abc123.x.uplink.spot)
40
- - My Tunnels see status and permanent URL if set
41
- - Create Permanent URL → pick tunnel → enter alias (if premium enabled)
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
- - Next.js default: server hosting expects `output: "standalone"`
49
- - Vite / CRA: static build is supported (dist/build served as static)
50
-
51
- ## Quick start (non-interactive)
44
+
45
+ Email verification preserves the guest tunnel and unlocks hosting, databases, and registrar features. Aliases and custom domains still depend on the account plan.
46
+
47
+ ## Quick start (agents)
52
48
  ```bash
53
- # Create tunnel (any port: 3000, 8080, 5173, etc.)
54
- echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel create --port 3000 --json
49
+ # Creates guest access automatically when needed
50
+ uplink tunnel create --port 3000 --json
55
51
 
56
- # List tunnels
52
+ # List / stop
57
53
  echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel list --json
54
+ echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel stop --id tun_xxx --json
55
+ ```
58
56
 
59
- # Set alias (if enabled on account)
60
- echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel alias-set --id tun_xxx --alias myapp --json
57
+ Full contract: **[AGENTS.md](./AGENTS.md)** (also at `docs/AGENTS.md`).
58
+
59
+ ## Quick start (interactive)
60
+ ```bash
61
+ uplink # open menu
62
+ ```
63
+ - **Share** → Start tunnel → pick port → public URL
64
+ - **Hosting** → Setup / Deploy / List / Delete
65
+ - **Domains** → connect registrar, attach hostname to a hosted app
66
+
67
+ ## Hosting (non-interactive)
68
+ ```bash
69
+ echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin host setup \
70
+ --path . --name myapp --yes --json
71
+ ```
72
+
73
+ ## Domains (non-interactive)
74
+ ```bash
75
+ uplink domains providers connect godaddy --token-env GODADDY_PAT --json
76
+ uplink domains list --json
77
+ echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin \
78
+ host domains add --id app_xxx --hostname example.com --json
61
79
  ```
62
80
 
63
81
  ## Agent essentials
64
82
  - **`--json`**: stdout = JSON only; stderr = logs/errors
65
83
  - **`--token-stdin`**: read token once from stdin (avoid argv leaks)
66
84
  - **`--api-base`**: override API host if needed
67
- - **Exit codes**: 0 ok; 2 usage; 10 auth missing/invalid; 20 network; 30 server/unknown
68
- See `docs/AGENTS.md` for the full contract.
85
+ - **Exit codes**: 0 ok · 2 usage · 10 auth · 20 network · 30 server/unknown
69
86
 
70
87
  ## Key commands
71
88
  - `uplink menu` — interactive UI
72
- - `uplink tunnel create --port <p> [--alias <a>] [--json]`
73
- - `uplink tunnel list --json`
74
- - `uplink tunnel alias-set --id <id> --alias <a> --json`
75
- - `uplink tunnel alias-delete --id <id> --json`
76
- - `uplink tunnel stats --id <id> --json`
77
- - `uplink tunnel stop --id <id> --json`
78
- - `uplink signup --json` — create user + token (no auth)
89
+ - `uplink login --email <email> [--code <code>] [--json]`
90
+ - `uplink signup --json`
91
+ - `uplink tunnel create --port <p> [--alias <a>] [--json]` — starts client
92
+ - `uplink tunnel list|stats|stop|alias-set|alias-delete --json`
93
+ - `uplink host setup|deploy|list|status|logs|delete …`
94
+ - `uplink host domains add|verify|list|remove …`
95
+ - `uplink domains list|check|providers …`
79
96
 
80
97
  ## Environment
81
98
  ```bash
@@ -86,14 +103,17 @@ export TUNNEL_DOMAIN=x.uplink.spot
86
103
  ```
87
104
 
88
105
  ## Troubleshooting
89
- - “No running tunnel clients found” make sure the tunnel client is still running; restart `uplink` and start a tunnel.
90
- - Auth errors — verify `AGENTCLOUD_TOKEN` is set/exported; use `--token-stdin`.
91
- - Relay errors — ensure `TUNNEL_CTRL=tunnel.uplink.spot:7071`.
106
+ - URL not live ensure something is listening on the port and the client started (`tunnel list` `connected`)
107
+ - Auth errors — verify `AGENTCLOUD_TOKEN` or `~/.uplink/credentials`; prefer `--token-stdin` for agents
108
+ - Relay errors — `TUNNEL_CTRL=tunnel.uplink.spot:7071`
109
+ - Domain search TUI — not bundled on npm; use `domains list` / `check` / `host domains *`
92
110
 
93
111
  ## Docs
94
- - Menu reference: `docs/MENU_STRUCTURE.md`
95
- - Agent guide: `docs/AGENTS.md`
96
- - Open source CLI scope vs backend: `docs/OPEN_SOURCE_CLI.md`
112
+ - Agents: [AGENTS.md](./AGENTS.md)
113
+ - Product: [docs/PRODUCT.md](./docs/PRODUCT.md)
114
+ - Hosting: [docs/HOSTING.md](./docs/HOSTING.md)
115
+ - Menu: [docs/MENU_STRUCTURE.md](./docs/MENU_STRUCTURE.md)
116
+ - Changelog: [CHANGELOG.md](./CHANGELOG.md)
97
117
 
98
118
  ## License
99
- MIT
119
+ MIT — see [LICENSE](./LICENSE)
package/cli/src/index.ts CHANGED
@@ -5,12 +5,16 @@ import { devCommand } from "./subcommands/dev";
5
5
  import { adminCommand } from "./subcommands/admin";
6
6
  import { menuCommand } from "./subcommands/menu";
7
7
  import { tunnelCommand } from "./subcommands/tunnel";
8
+ import { loginCommand } from "./subcommands/login";
8
9
  import { signupCommand } from "./subcommands/signup";
9
10
  import { systemCommand } from "./subcommands/system";
10
11
  import { hostCommand } from "./subcommands/host";
12
+ import { domainsCommand } from "./subcommands/domains";
11
13
  import { readFileSync } from "fs";
12
14
  import { join } from "path";
13
15
  import { ensureApiBase, parseTokenEnv } from "./utils/api-base";
16
+ import { readStoredCredentials } from "./utils/credentials";
17
+ import { handleError } from "./utils/machine";
14
18
 
15
19
  // Get version from package.json (CommonJS build: __dirname available)
16
20
  const pkgPath = join(__dirname, "../../package.json");
@@ -20,7 +24,7 @@ const program = new Command();
20
24
 
21
25
  program
22
26
  .name("uplink")
23
- .description("Agent-friendly cloud CLI")
27
+ .description("Software for agents — share localhost, host apps, attach domains")
24
28
  .version(pkg.version)
25
29
  .option("--api-base <url>", "Override API base URL (default env AGENTCLOUD_API_BASE)")
26
30
  .option("--token-stdin", "Read AGENTCLOUD_TOKEN from stdin once");
@@ -30,9 +34,11 @@ program.addCommand(devCommand);
30
34
  program.addCommand(adminCommand);
31
35
  program.addCommand(tunnelCommand);
32
36
  program.addCommand(signupCommand);
37
+ program.addCommand(loginCommand);
33
38
  program.addCommand(systemCommand);
34
39
  program.addCommand(menuCommand);
35
40
  program.addCommand(hostCommand);
41
+ program.addCommand(domainsCommand);
36
42
 
37
43
  // Global pre-action hook to apply shared options
38
44
  let cachedTokenStdin: string | null = null;
@@ -68,6 +74,14 @@ program.hook("preAction", async (thisCommand) => {
68
74
  if (!process.env.AGENTCLOUD_API_BASE && parsed.apiBase) {
69
75
  process.env.AGENTCLOUD_API_BASE = parsed.apiBase;
70
76
  }
77
+ } else {
78
+ const stored = readStoredCredentials();
79
+ if (stored?.token && !process.env.AGENTCLOUD_TOKEN) {
80
+ process.env.AGENTCLOUD_TOKEN = stored.token;
81
+ if (!process.env.AGENTCLOUD_API_BASE && stored.apiBase) {
82
+ process.env.AGENTCLOUD_API_BASE = stored.apiBase;
83
+ }
84
+ }
71
85
  }
72
86
 
73
87
  const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !opts.json);
@@ -83,7 +97,6 @@ if (process.argv.length === 2) {
83
97
  }
84
98
 
85
99
  program.parseAsync(process.argv).catch((err) => {
86
- console.error(err);
87
- process.exit(1);
100
+ handleError(err);
88
101
  });
89
102
 
@@ -0,0 +1,148 @@
1
+ import { httpError } from "./http";
2
+ import type { DomainQuote, InventoryDomain, RegistrarAdapter, RegistrarCredentials } from "./types";
3
+
4
+ const BASE = "https://api.cloudflare.com/client/v4";
5
+
6
+ type CfEnvelope<T> = {
7
+ success?: boolean;
8
+ errors?: Array<{ message?: string }>;
9
+ result?: T;
10
+ result_info?: { cursor?: string };
11
+ };
12
+
13
+ async function cfFetch(
14
+ creds: RegistrarCredentials,
15
+ path: string,
16
+ init: RequestInit = {}
17
+ ): Promise<Response> {
18
+ if (!creds.token) throw new Error("Cloudflare token is missing");
19
+ return fetch(`${BASE}${path}`, {
20
+ ...init,
21
+ headers: {
22
+ Authorization: `Bearer ${creds.token}`,
23
+ Accept: "application/json",
24
+ "Content-Type": "application/json",
25
+ ...(init.headers || {}),
26
+ },
27
+ });
28
+ }
29
+
30
+ async function cfJson<T>(res: Response): Promise<CfEnvelope<T>> {
31
+ const text = await res.text();
32
+ let body: CfEnvelope<T>;
33
+ try {
34
+ body = text ? (JSON.parse(text) as CfEnvelope<T>) : {};
35
+ } catch {
36
+ throw httpError(res, text);
37
+ }
38
+ if (!res.ok || body.success === false) {
39
+ const message = body.errors?.map((e) => e.message).filter(Boolean).join("; ") || text;
40
+ throw httpError(res, message);
41
+ }
42
+ return body;
43
+ }
44
+
45
+ function pickUsd(value: unknown): number | undefined {
46
+ if (typeof value === "number" && Number.isFinite(value)) return value;
47
+ if (typeof value === "string") {
48
+ const n = Number(value);
49
+ return Number.isFinite(n) ? n : undefined;
50
+ }
51
+ if (value && typeof value === "object") {
52
+ const obj = value as { amount?: unknown; value?: unknown; usd?: unknown };
53
+ return pickUsd(obj.amount ?? obj.value ?? obj.usd);
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ export const cloudflareAdapter: RegistrarAdapter = {
59
+ id: "cloudflare",
60
+ label: "Cloudflare",
61
+ connectHelp: "API token with account read. --token-env CF_API_TOKEN (optional --account-env CF_ACCOUNT_ID)",
62
+ async verify(creds) {
63
+ const res = await cfFetch(creds, "/accounts?per_page=1");
64
+ const body = await cfJson<Array<{ id?: string }>>(res);
65
+ const accountId = creds.accountId || body.result?.[0]?.id;
66
+ if (!accountId) throw new Error("Cloudflare token has no accounts");
67
+ return { ...creds, accountId };
68
+ },
69
+ async listDomains(creds) {
70
+ const accountId = creds.accountId;
71
+ if (!accountId) throw new Error("Cloudflare account id is missing");
72
+ const out: InventoryDomain[] = [];
73
+
74
+ const registrations = await cfFetch(creds, `/accounts/${accountId}/registrar/registrations`);
75
+ if (registrations.ok) {
76
+ const body = await cfJson<Array<{ domain_name?: string; expires_at?: string }>>(registrations);
77
+ for (const item of body.result || []) {
78
+ if (!item.domain_name) continue;
79
+ out.push({
80
+ domain: item.domain_name.toLowerCase(),
81
+ provider: "cloudflare",
82
+ status: "owned",
83
+ expiresAt: item.expires_at,
84
+ });
85
+ }
86
+ }
87
+
88
+ if (out.length > 0) return out;
89
+
90
+ const zones = await cfFetch(creds, "/zones?per_page=50");
91
+ if (!zones.ok) {
92
+ if (!registrations.ok) throw httpError(registrations, await registrations.text());
93
+ return out;
94
+ }
95
+ const body = await cfJson<Array<{ name?: string }>>(zones);
96
+ for (const zone of body.result || []) {
97
+ if (!zone.name) continue;
98
+ out.push({ domain: zone.name.toLowerCase(), provider: "cloudflare", status: "owned" });
99
+ }
100
+ return out;
101
+ },
102
+ async check(creds, domain) {
103
+ const accountId = creds.accountId;
104
+ if (!accountId) throw new Error("Cloudflare account id is missing");
105
+ let res = await cfFetch(creds, `/accounts/${accountId}/registrar/domain-check`, {
106
+ method: "POST",
107
+ body: JSON.stringify({ domains: [domain] }),
108
+ });
109
+ if (res.status === 400) {
110
+ res = await cfFetch(creds, `/accounts/${accountId}/registrar/domain-check`, {
111
+ method: "POST",
112
+ body: JSON.stringify({ domain_names: [domain] }),
113
+ });
114
+ }
115
+ if (!res.ok) throw httpError(res, await res.text());
116
+ const body = await cfJson<unknown>(res);
117
+ const rows = Array.isArray(body.result) ? body.result : body.result ? [body.result] : [];
118
+ const row = (rows[0] || {}) as {
119
+ domain_name?: string;
120
+ registrable?: boolean;
121
+ available?: boolean;
122
+ tier?: string;
123
+ reason?: string;
124
+ fees?: { registration?: unknown };
125
+ prices?: { registration?: unknown };
126
+ price?: unknown;
127
+ };
128
+ const available = row.registrable === true || row.available === true;
129
+ const priceUsd = pickUsd(row.fees?.registration ?? row.prices?.registration ?? row.price);
130
+ if (available) {
131
+ return {
132
+ domain,
133
+ provider: "cloudflare",
134
+ status: "available",
135
+ buyable: row.tier !== "premium",
136
+ premium: row.tier === "premium",
137
+ priceUsd,
138
+ };
139
+ }
140
+ return {
141
+ domain,
142
+ provider: "cloudflare",
143
+ status: row.reason ? "not_for_sale" : "taken",
144
+ buyable: false,
145
+ error: row.reason,
146
+ };
147
+ },
148
+ };