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.
- package/AGENTS.md +177 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +72 -52
- package/cli/src/index.ts +16 -3
- package/cli/src/registrars/cloudflare.ts +148 -0
- package/cli/src/registrars/dreamhost.ts +129 -0
- package/cli/src/registrars/godaddy.ts +105 -0
- package/cli/src/registrars/hostinger.ts +106 -0
- package/cli/src/registrars/http.ts +18 -0
- package/cli/src/registrars/index.ts +32 -0
- package/cli/src/registrars/namecheap.ts +163 -0
- package/cli/src/registrars/secret.ts +66 -0
- package/cli/src/registrars/store.ts +55 -0
- package/cli/src/registrars/types.ts +42 -0
- package/cli/src/subcommands/admin.ts +17 -30
- package/cli/src/subcommands/db.ts +63 -57
- package/cli/src/subcommands/dev.ts +23 -25
- package/cli/src/subcommands/domains.ts +295 -0
- package/cli/src/subcommands/host-domains.ts +148 -0
- package/cli/src/subcommands/host.ts +3 -0
- package/cli/src/subcommands/login.ts +85 -0
- package/cli/src/subcommands/menu/colors.ts +1 -1
- package/cli/src/subcommands/menu/effects/tunnel-clients.ts +87 -14
- package/cli/src/subcommands/menu/inline-tree-select.ts +6 -5
- package/cli/src/subcommands/menu/io.ts +27 -5
- package/cli/src/subcommands/menu/menus/domain-check.ts +34 -0
- package/cli/src/subcommands/menu/menus/domains.ts +197 -0
- package/cli/src/subcommands/menu/menus/hosting.ts +14 -46
- package/cli/src/subcommands/menu/menus/index.ts +1 -0
- package/cli/src/subcommands/menu/menus/tunnels.ts +25 -67
- package/cli/src/subcommands/menu/render.ts +2 -2
- package/cli/src/subcommands/menu/requests.ts +9 -2
- package/cli/src/subcommands/menu/tests.ts +1 -1
- package/cli/src/subcommands/menu/tunnels.ts +10 -99
- package/cli/src/subcommands/menu/types.ts +8 -0
- package/cli/src/subcommands/menu.ts +32 -524
- package/cli/src/subcommands/signup.ts +2 -2
- package/cli/src/subcommands/system.ts +58 -36
- package/cli/src/subcommands/tunnel.ts +126 -33
- package/cli/src/templates/index.ts +3 -3
- package/cli/src/tui/App.tsx +202 -0
- package/cli/src/tui/AppInspector.tsx +114 -0
- package/cli/src/tui/HomeStatus.tsx +92 -0
- package/cli/src/tui/brand.tsx +20 -0
- package/cli/src/tui/format.ts +22 -0
- package/cli/src/tui/index.mts +6 -0
- package/cli/src/tui/liveTree.ts +40 -0
- package/cli/src/tui/package.json +3 -0
- package/cli/src/tui/runMenu.tsx +57 -0
- package/cli/src/tui/session.mts +267 -0
- package/cli/src/tui/snapshot.ts +175 -0
- package/cli/src/utils/api-base.ts +11 -0
- package/cli/src/utils/credentials.ts +58 -0
- package/cli/src/utils/domain-availability.ts +56 -0
- package/cli/src/utils/guest-access.ts +38 -0
- package/cli/src/utils/launchDomainking.ts +64 -0
- package/cli/src/utils/login-flow.ts +57 -0
- package/docs/AGENTS.md +130 -148
- package/docs/HOSTING.md +55 -0
- package/docs/MENU_STRUCTURE.md +60 -288
- package/docs/PRODUCT.md +64 -0
- package/docs/README.md +11 -7
- package/package.json +22 -36
- package/scripts/tunnel/client-improved.js +127 -38
- package/scripts/tunnel/client.js +118 -0
- 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
|
-
|
|
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
|

|
|
6
6
|
|
|
7
7
|
## Key features
|
|
8
|
-
- **
|
|
9
|
-
- **Agent-first**:
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
+
Guest access includes one active tunnel for 24 hours and public domain search.
|
|
38
|
+
|
|
39
|
+
## Unlock persistent features
|
|
36
40
|
```bash
|
|
37
|
-
uplink
|
|
41
|
+
uplink login --email you@example.com
|
|
42
|
+
uplink login --email you@example.com --code 123456 --json
|
|
38
43
|
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
#
|
|
54
|
-
|
|
49
|
+
# Creates guest access automatically when needed
|
|
50
|
+
uplink tunnel create --port 3000 --json
|
|
55
51
|
|
|
56
|
-
# List
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
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
|
|
73
|
-
- `uplink
|
|
74
|
-
- `uplink tunnel
|
|
75
|
-
- `uplink tunnel alias-delete --
|
|
76
|
-
- `uplink
|
|
77
|
-
- `uplink
|
|
78
|
-
- `uplink
|
|
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
|
-
-
|
|
90
|
-
- Auth errors — verify `AGENTCLOUD_TOKEN`
|
|
91
|
-
- Relay errors —
|
|
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
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
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("
|
|
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
|
-
|
|
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
|
+
};
|