uplink-cli 0.1.38 → 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.
- package/AGENTS.md +161 -0
- package/LICENSE +21 -0
- package/README.md +46 -47
- package/cli/src/index.ts +5 -3
- package/cli/src/registrars/cloudflare.ts +148 -0
- package/cli/src/registrars/godaddy.ts +99 -0
- package/cli/src/registrars/hostinger.ts +106 -0
- package/cli/src/registrars/http.ts +18 -0
- package/cli/src/registrars/index.ts +30 -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 +40 -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 +268 -0
- package/cli/src/subcommands/host-domains.ts +148 -0
- package/cli/src/subcommands/host.ts +3 -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/domains.ts +199 -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/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/system.ts +58 -36
- package/cli/src/subcommands/tunnel.ts +124 -33
- package/cli/src/templates/index.ts +3 -3
- package/cli/src/tui/App.tsx +197 -0
- package/cli/src/tui/AppInspector.tsx +114 -0
- package/cli/src/tui/HomeStatus.tsx +59 -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 +382 -0
- package/cli/src/tui/snapshot.ts +146 -0
- package/cli/src/utils/launchDomainking.ts +64 -0
- package/docs/AGENTS.md +113 -147
- package/docs/MENU_STRUCTURE.md +56 -288
- package/docs/README.md +6 -6
- package/package.json +18 -35
- 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,161 @@
|
|
|
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
|
+
Package name: `uplink-cli` · Binary: `uplink`
|
|
7
|
+
|
|
8
|
+
## Auth
|
|
9
|
+
|
|
10
|
+
- Use `AGENTCLOUD_TOKEN` (bearer). Prefer stdin over argv:
|
|
11
|
+
```bash
|
|
12
|
+
echo "$TOKEN" | uplink --token-stdin …
|
|
13
|
+
```
|
|
14
|
+
- API base: `--api-base https://api.uplink.spot` or `AGENTCLOUD_API_BASE`.
|
|
15
|
+
|
|
16
|
+
## Signup (no auth required)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uplink signup --json
|
|
20
|
+
uplink signup --label "cursor-agent" --expires-days 30 --json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Save `token` from the JSON — it is shown only once. Then:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export AGENTCLOUD_TOKEN='…'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Machine-mode contract
|
|
30
|
+
|
|
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
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Create + start client (optional alias if enabled)
|
|
48
|
+
echo "$TOKEN" | uplink --token-stdin \
|
|
49
|
+
tunnel create --port 3000 --alias myapp --json
|
|
50
|
+
|
|
51
|
+
# List (includes connected status)
|
|
52
|
+
echo "$TOKEN" | uplink --token-stdin tunnel list --json
|
|
53
|
+
|
|
54
|
+
# Alias on an existing tunnel
|
|
55
|
+
echo "$TOKEN" | uplink --token-stdin tunnel alias-set --id tun_xxx --alias myapp --json
|
|
56
|
+
echo "$TOKEN" | uplink --token-stdin tunnel alias-delete --id tun_xxx --json
|
|
57
|
+
|
|
58
|
+
# Stats / stop
|
|
59
|
+
echo "$TOKEN" | uplink --token-stdin tunnel stats --id tun_xxx --json
|
|
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
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
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`.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
echo "$TOKEN" | uplink --token-stdin host setup \
|
|
84
|
+
--path /path/to/app --name myapp --env-file /path/to/.env \
|
|
85
|
+
--wait-timeout 900 --wait-interval 5 --yes --json
|
|
86
|
+
|
|
87
|
+
echo "$TOKEN" | uplink --token-stdin host deploy \
|
|
88
|
+
--path /path/to/app --name myapp --wait --json
|
|
89
|
+
|
|
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
|
|
92
|
+
echo "$TOKEN" | uplink --token-stdin host list --json
|
|
93
|
+
echo "$TOKEN" | uplink --token-stdin host status --id app_xxx --json
|
|
94
|
+
echo "$TOKEN" | uplink --token-stdin host logs --id app_xxx --json
|
|
95
|
+
echo "$TOKEN" | uplink --token-stdin host delete --id app_xxx --yes --json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
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).
|
|
103
|
+
|
|
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
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Do not treat RDAP “available” as buyable unless `domains check` says `buyable: true`. Purchase is not wired yet.
|
|
127
|
+
|
|
128
|
+
## Databases (optional)
|
|
129
|
+
|
|
130
|
+
```bash
|
|
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
|
|
135
|
+
```
|
|
136
|
+
|
|
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
|
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,20 +1,15 @@
|
|
|
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
|
|
|
@@ -32,50 +27,54 @@ uplink signup --json # creates account + token
|
|
|
32
27
|
export AGENTCLOUD_TOKEN=your-token-here # save securely
|
|
33
28
|
```
|
|
34
29
|
|
|
30
|
+
## Quick start (agents)
|
|
31
|
+
```bash
|
|
32
|
+
# Create tunnel + start local client (URL works immediately)
|
|
33
|
+
echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel create --port 3000 --json
|
|
34
|
+
|
|
35
|
+
# List / stop
|
|
36
|
+
echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel list --json
|
|
37
|
+
echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel stop --id tun_xxx --json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Full contract: **[AGENTS.md](./AGENTS.md)** (also at `docs/AGENTS.md`).
|
|
41
|
+
|
|
35
42
|
## Quick start (interactive)
|
|
36
43
|
```bash
|
|
37
44
|
uplink # open menu
|
|
38
45
|
```
|
|
39
|
-
- Start
|
|
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)
|
|
52
|
-
```bash
|
|
53
|
-
# Create tunnel (any port: 3000, 8080, 5173, etc.)
|
|
54
|
-
echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel create --port 3000 --json
|
|
46
|
+
- **Share** → Start tunnel → pick port → public URL
|
|
47
|
+
- **Hosting** → Setup / Deploy / List / Delete
|
|
48
|
+
- **Domains** → connect registrar, attach hostname to a hosted app
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
## Hosting (non-interactive)
|
|
51
|
+
```bash
|
|
52
|
+
echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin host setup \
|
|
53
|
+
--path . --name myapp --yes --json
|
|
54
|
+
```
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
## Domains (non-interactive)
|
|
57
|
+
```bash
|
|
58
|
+
uplink domains providers connect godaddy --token-env GODADDY_PAT --json
|
|
59
|
+
uplink domains list --json
|
|
60
|
+
echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin \
|
|
61
|
+
host domains add --id app_xxx --hostname example.com --json
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
## Agent essentials
|
|
64
65
|
- **`--json`**: stdout = JSON only; stderr = logs/errors
|
|
65
66
|
- **`--token-stdin`**: read token once from stdin (avoid argv leaks)
|
|
66
67
|
- **`--api-base`**: override API host if needed
|
|
67
|
-
- **Exit codes**: 0 ok
|
|
68
|
-
See `docs/AGENTS.md` for the full contract.
|
|
68
|
+
- **Exit codes**: 0 ok · 2 usage · 10 auth · 20 network · 30 server/unknown
|
|
69
69
|
|
|
70
70
|
## Key commands
|
|
71
71
|
- `uplink menu` — interactive UI
|
|
72
|
-
- `uplink
|
|
73
|
-
- `uplink tunnel
|
|
74
|
-
- `uplink tunnel alias-set
|
|
75
|
-
- `uplink
|
|
76
|
-
- `uplink
|
|
77
|
-
- `uplink
|
|
78
|
-
- `uplink signup --json` — create user + token (no auth)
|
|
72
|
+
- `uplink signup --json`
|
|
73
|
+
- `uplink tunnel create --port <p> [--alias <a>] [--json]` — starts client
|
|
74
|
+
- `uplink tunnel list|stats|stop|alias-set|alias-delete --json`
|
|
75
|
+
- `uplink host setup|deploy|list|status|logs|delete …`
|
|
76
|
+
- `uplink host domains add|verify|list|remove …`
|
|
77
|
+
- `uplink domains list|check|providers …`
|
|
79
78
|
|
|
80
79
|
## Environment
|
|
81
80
|
```bash
|
|
@@ -86,14 +85,14 @@ export TUNNEL_DOMAIN=x.uplink.spot
|
|
|
86
85
|
```
|
|
87
86
|
|
|
88
87
|
## Troubleshooting
|
|
89
|
-
-
|
|
90
|
-
- Auth errors — verify `AGENTCLOUD_TOKEN
|
|
91
|
-
- Relay errors —
|
|
88
|
+
- URL not live — ensure something is listening on the port and the client started (`tunnel list` → `connected`)
|
|
89
|
+
- Auth errors — verify `AGENTCLOUD_TOKEN`; prefer `--token-stdin`
|
|
90
|
+
- Relay errors — `TUNNEL_CTRL=tunnel.uplink.spot:7071`
|
|
91
|
+
- Domain search TUI — not bundled on npm; use `domains list` / `check` / `host domains *`
|
|
92
92
|
|
|
93
93
|
## Docs
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- Open source CLI scope vs backend: `docs/OPEN_SOURCE_CLI.md`
|
|
94
|
+
- Agents: [AGENTS.md](./AGENTS.md)
|
|
95
|
+
- Menu: [docs/MENU_STRUCTURE.md](./docs/MENU_STRUCTURE.md)
|
|
97
96
|
|
|
98
97
|
## License
|
|
99
|
-
MIT
|
|
98
|
+
MIT — see [LICENSE](./LICENSE)
|
package/cli/src/index.ts
CHANGED
|
@@ -8,9 +8,11 @@ import { tunnelCommand } from "./subcommands/tunnel";
|
|
|
8
8
|
import { signupCommand } from "./subcommands/signup";
|
|
9
9
|
import { systemCommand } from "./subcommands/system";
|
|
10
10
|
import { hostCommand } from "./subcommands/host";
|
|
11
|
+
import { domainsCommand } from "./subcommands/domains";
|
|
11
12
|
import { readFileSync } from "fs";
|
|
12
13
|
import { join } from "path";
|
|
13
14
|
import { ensureApiBase, parseTokenEnv } from "./utils/api-base";
|
|
15
|
+
import { handleError } from "./utils/machine";
|
|
14
16
|
|
|
15
17
|
// Get version from package.json (CommonJS build: __dirname available)
|
|
16
18
|
const pkgPath = join(__dirname, "../../package.json");
|
|
@@ -20,7 +22,7 @@ const program = new Command();
|
|
|
20
22
|
|
|
21
23
|
program
|
|
22
24
|
.name("uplink")
|
|
23
|
-
.description("
|
|
25
|
+
.description("Software for agents — share localhost, host apps, attach domains")
|
|
24
26
|
.version(pkg.version)
|
|
25
27
|
.option("--api-base <url>", "Override API base URL (default env AGENTCLOUD_API_BASE)")
|
|
26
28
|
.option("--token-stdin", "Read AGENTCLOUD_TOKEN from stdin once");
|
|
@@ -33,6 +35,7 @@ program.addCommand(signupCommand);
|
|
|
33
35
|
program.addCommand(systemCommand);
|
|
34
36
|
program.addCommand(menuCommand);
|
|
35
37
|
program.addCommand(hostCommand);
|
|
38
|
+
program.addCommand(domainsCommand);
|
|
36
39
|
|
|
37
40
|
// Global pre-action hook to apply shared options
|
|
38
41
|
let cachedTokenStdin: string | null = null;
|
|
@@ -83,7 +86,6 @@ if (process.argv.length === 2) {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
program.parseAsync(process.argv).catch((err) => {
|
|
86
|
-
|
|
87
|
-
process.exit(1);
|
|
89
|
+
handleError(err);
|
|
88
90
|
});
|
|
89
91
|
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { httpError, parseJson } from "./http";
|
|
2
|
+
import type { DomainQuote, InventoryDomain, RegistrarAdapter, RegistrarCredentials } from "./types";
|
|
3
|
+
|
|
4
|
+
const BASE = "https://api.godaddy.com";
|
|
5
|
+
|
|
6
|
+
async function godaddyFetch(
|
|
7
|
+
creds: RegistrarCredentials,
|
|
8
|
+
path: string,
|
|
9
|
+
init: RequestInit = {}
|
|
10
|
+
): Promise<Response> {
|
|
11
|
+
if (!creds.token) throw new Error("GoDaddy token is missing");
|
|
12
|
+
return fetch(`${BASE}${path}`, {
|
|
13
|
+
...init,
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${creds.token}`,
|
|
16
|
+
Accept: "application/json",
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
...(init.headers || {}),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function centsToUsd(value: number): number {
|
|
24
|
+
return value / 100;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function microToUsd(value: number): number {
|
|
28
|
+
return value / 1_000_000;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const godaddyAdapter: RegistrarAdapter = {
|
|
32
|
+
id: "godaddy",
|
|
33
|
+
label: "GoDaddy",
|
|
34
|
+
connectHelp: "Personal Access Token with domains.domain:read. --token-env GODADDY_PAT",
|
|
35
|
+
async verify(creds) {
|
|
36
|
+
const res = await godaddyFetch(creds, "/v1/domains?limit=1");
|
|
37
|
+
if (!res.ok) throw httpError(res, await res.text());
|
|
38
|
+
return creds;
|
|
39
|
+
},
|
|
40
|
+
async listDomains(creds) {
|
|
41
|
+
const out: InventoryDomain[] = [];
|
|
42
|
+
let marker: string | undefined;
|
|
43
|
+
for (;;) {
|
|
44
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
45
|
+
if (marker) params.set("marker", marker);
|
|
46
|
+
const res = await godaddyFetch(creds, `/v1/domains?${params}`);
|
|
47
|
+
if (!res.ok) throw httpError(res, await res.text());
|
|
48
|
+
const batch = await parseJson<Array<{ domain?: string; expires?: string }>>(res);
|
|
49
|
+
if (!Array.isArray(batch) || batch.length === 0) break;
|
|
50
|
+
for (const item of batch) {
|
|
51
|
+
if (!item.domain) continue;
|
|
52
|
+
out.push({
|
|
53
|
+
domain: item.domain.toLowerCase(),
|
|
54
|
+
provider: "godaddy",
|
|
55
|
+
status: "owned",
|
|
56
|
+
expiresAt: item.expires,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (batch.length < 100) break;
|
|
60
|
+
marker = batch[batch.length - 1]?.domain;
|
|
61
|
+
if (!marker) break;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
},
|
|
65
|
+
async check(creds, domain) {
|
|
66
|
+
const v3 = await godaddyFetch(
|
|
67
|
+
creds,
|
|
68
|
+
`/v3/domains/check-availability?domain=${encodeURIComponent(domain)}`
|
|
69
|
+
);
|
|
70
|
+
if (v3.ok) {
|
|
71
|
+
const data = await parseJson<{
|
|
72
|
+
available?: boolean;
|
|
73
|
+
prices?: Array<{ period?: number; price?: { value?: number } }>;
|
|
74
|
+
}>(v3);
|
|
75
|
+
const year1 = data.prices?.find((p) => p.period === 1) ?? data.prices?.[0];
|
|
76
|
+
const priceUsd = year1?.price?.value != null ? centsToUsd(year1.price.value) : undefined;
|
|
77
|
+
if (data.available) {
|
|
78
|
+
return { domain, provider: "godaddy", status: "available", buyable: true, priceUsd };
|
|
79
|
+
}
|
|
80
|
+
return { domain, provider: "godaddy", status: "taken", buyable: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const v1 = await godaddyFetch(
|
|
84
|
+
creds,
|
|
85
|
+
`/v1/domains/available?domain=${encodeURIComponent(domain)}`
|
|
86
|
+
);
|
|
87
|
+
if (!v1.ok) throw httpError(v1, await v1.text());
|
|
88
|
+
const data = await parseJson<{
|
|
89
|
+
available?: boolean;
|
|
90
|
+
price?: number;
|
|
91
|
+
currency?: string;
|
|
92
|
+
}>(v1);
|
|
93
|
+
const priceUsd = data.price != null ? microToUsd(data.price) : undefined;
|
|
94
|
+
if (data.available) {
|
|
95
|
+
return { domain, provider: "godaddy", status: "available", buyable: true, priceUsd };
|
|
96
|
+
}
|
|
97
|
+
return { domain, provider: "godaddy", status: "taken", buyable: false };
|
|
98
|
+
},
|
|
99
|
+
};
|