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.
Files changed (54) hide show
  1. package/AGENTS.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +46 -47
  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 +3 -0
  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 +3 -3
  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/launchDomainking.ts +64 -0
  48. package/docs/AGENTS.md +113 -147
  49. package/docs/MENU_STRUCTURE.md +56 -288
  50. package/docs/README.md +6 -6
  51. package/package.json +18 -35
  52. package/scripts/tunnel/client-improved.js +127 -38
  53. package/scripts/tunnel/client.js +118 -0
  54. 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
- 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
 
@@ -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 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)
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
- # List tunnels
57
- echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel list --json
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
- # Set alias (if enabled on account)
60
- echo "$AGENTCLOUD_TOKEN" | uplink --token-stdin tunnel alias-set --id tun_xxx --alias myapp --json
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; 2 usage; 10 auth missing/invalid; 20 network; 30 server/unknown
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 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)
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
- - “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`.
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
- - Menu reference: `docs/MENU_STRUCTURE.md`
95
- - Agent guide: `docs/AGENTS.md`
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("Agent-friendly cloud CLI")
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
- console.error(err);
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
+ };