doomain 0.1.1 → 0.1.2
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/README.md +405 -114
- package/dist/commands/auth/vercel.js +39 -8
- package/dist/commands/wizard.js +33 -4
- package/dist/lib/vercel-auth.d.ts +13 -0
- package/dist/lib/vercel-auth.js +54 -0
- package/dist/lib/vercel.js +3 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Doomain
|
|
2
2
|
|
|
3
|
-
Doomain links a Vercel project to a domain from
|
|
3
|
+
Doomain links a Vercel project to a custom domain from your terminal.
|
|
4
4
|
|
|
5
|
-
It handles the
|
|
5
|
+
It handles the boring parts of custom-domain setup: selecting the Vercel project, finding the right DNS zone, adding the domain to Vercel, writing the Vercel DNS records, waiting for public DNS propagation, and asking Vercel to verify the domain.
|
|
6
6
|
|
|
7
|
-
Use the interactive wizard when working by hand. Use `--json`
|
|
7
|
+
Use the interactive wizard when working by hand. Use explicit commands with `--json` for scripts, CI, or agents.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- Interactive domain-linking wizard
|
|
12
|
-
-
|
|
11
|
+
- Interactive Vercel domain-linking wizard.
|
|
12
|
+
- Script-friendly commands with one JSON object on stdout.
|
|
13
13
|
- Vercel project detection from `.vercel/project.json`.
|
|
14
|
-
- DNS provider inference by longest matching zone.
|
|
15
|
-
- Dry
|
|
16
|
-
- DNS propagation and Vercel verification
|
|
17
|
-
-
|
|
14
|
+
- DNS provider inference by longest matching configured zone.
|
|
15
|
+
- Dry-run plans before writing changes.
|
|
16
|
+
- DNS propagation and Vercel verification wait loop.
|
|
17
|
+
- DNS provider support for Spaceship, Namecheap, and Cloudflare.
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
@@ -32,34 +32,122 @@ Run the wizard:
|
|
|
32
32
|
doomain
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The wizard
|
|
35
|
+
The wizard will:
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
1. Ask for a Vercel token if one is not already configured.
|
|
38
|
+
2. Let you choose a Vercel personal account or team.
|
|
39
|
+
3. Detect and preselect a local Vercel project when `.vercel/project.json` exists.
|
|
40
|
+
4. Connect a DNS provider if none is configured.
|
|
41
|
+
5. List domains from configured DNS providers.
|
|
42
|
+
6. Preview the Vercel and DNS changes.
|
|
43
|
+
7. Apply the changes and request Vercel verification.
|
|
44
|
+
|
|
45
|
+
If you already know the target project and domain, run the link command directly:
|
|
38
46
|
|
|
39
47
|
```bash
|
|
40
|
-
doomain link app.example.com --project my-
|
|
48
|
+
doomain link app.example.com --project my-vercel-project
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Scripted Setup
|
|
52
|
+
|
|
53
|
+
For CI, shell scripts, and agents, use explicit commands and `--json`.
|
|
54
|
+
|
|
55
|
+
First save Vercel credentials:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
doomain auth vercel \
|
|
59
|
+
--token "$VERCEL_TOKEN" \
|
|
60
|
+
--team-id "$VERCEL_TEAM_ID" \
|
|
61
|
+
--json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`--team-id` is optional for personal-account usage.
|
|
65
|
+
|
|
66
|
+
Then connect one DNS provider:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
doomain providers connect cloudflare \
|
|
70
|
+
--credential apiToken="$CLOUDFLARE_API_TOKEN" \
|
|
71
|
+
--credential accountId="$CLOUDFLARE_ACCOUNT_ID" \
|
|
72
|
+
--json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Preview the domain link:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
doomain link app.example.com --project my-vercel-project --dry-run --json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Apply it:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
doomain link app.example.com --project my-vercel-project --json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Provider Setup
|
|
88
|
+
|
|
89
|
+
Doomain stores local credentials in `~/.doomain/config.json` with `0600` file permissions. Environment variables override saved config values.
|
|
90
|
+
|
|
91
|
+
| Provider | Provider ID | Required credentials | Environment variables | Notes |
|
|
92
|
+
| --- | --- | --- | --- | --- |
|
|
93
|
+
| Spaceship | `spaceship` | `apiKey`, `apiSecret` | `SPACESHIP_API_KEY`, `SPACESHIP_API_SECRET` | API key needs domain read access and DNS record read/write access. |
|
|
94
|
+
| Namecheap | `namecheap` | `apiUser`, `apiKey`, `clientIp` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY`, `NAMECHEAP_CLIENT_IP` | API access must be enabled and `clientIp` must be your whitelisted public IPv4. Optional: `username`, `sandbox`. |
|
|
95
|
+
| Cloudflare | `cloudflare` | `apiToken`, `accountId` | `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` | API token needs `Zone:Read` and `DNS:Edit`. Vercel records are written as DNS-only records, not proxied. |
|
|
96
|
+
|
|
97
|
+
### Spaceship
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
doomain providers connect spaceship \
|
|
101
|
+
--credential apiKey=spaceship_key \
|
|
102
|
+
--credential apiSecret=spaceship_secret
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Compatibility aliases are also available:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
doomain providers connect spaceship --api-key spaceship_key --api-secret spaceship_secret
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Namecheap
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
doomain providers connect namecheap \
|
|
115
|
+
--credential apiUser=your_namecheap_user \
|
|
116
|
+
--credential apiKey=your_api_key \
|
|
117
|
+
--credential clientIp=your_whitelisted_ipv4
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
For Namecheap sandbox testing:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
doomain providers connect namecheap \
|
|
124
|
+
--credential apiUser=your_sandbox_user \
|
|
125
|
+
--credential apiKey=your_sandbox_key \
|
|
126
|
+
--credential clientIp=your_whitelisted_ipv4 \
|
|
127
|
+
--credential sandbox=true
|
|
41
128
|
```
|
|
42
129
|
|
|
43
|
-
|
|
130
|
+
`NAMECHEAP_USERNAME` is optional. If omitted, Doomain uses `apiUser` as the Namecheap username.
|
|
131
|
+
|
|
132
|
+
### Cloudflare
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
doomain providers connect cloudflare \
|
|
136
|
+
--credential apiToken=your_cloudflare_api_token \
|
|
137
|
+
--credential accountId=your_cloudflare_account_id
|
|
138
|
+
```
|
|
44
139
|
|
|
45
|
-
|
|
46
|
-
2. Resolve the target domain from an argument, `--domain`, `--subdomain`, `--apex`, config, or environment variables.
|
|
47
|
-
3. Choose the DNS provider from `--provider` or infer it from configured DNS zones.
|
|
48
|
-
4. Add the domain to the Vercel project.
|
|
49
|
-
5. Read Vercel's recommended target and verification records.
|
|
50
|
-
6. Plan DNS changes against the current provider records.
|
|
51
|
-
7. Apply DNS changes unless `--dry-run` is set.
|
|
52
|
-
8. Wait for public DNS propagation and Vercel verification unless `--no-wait` is set.
|
|
140
|
+
Cloudflare records created for Vercel `A`, `AAAA`, and `CNAME` targets are set to `proxied: false` so Vercel can validate the domain.
|
|
53
141
|
|
|
54
142
|
## Linking Domains
|
|
55
143
|
|
|
56
|
-
|
|
144
|
+
You can pass the full target domain as a positional argument:
|
|
57
145
|
|
|
58
146
|
```bash
|
|
59
147
|
doomain link app.example.com --project my-app
|
|
60
148
|
```
|
|
61
149
|
|
|
62
|
-
|
|
150
|
+
Or pass a base domain and subdomain:
|
|
63
151
|
|
|
64
152
|
```bash
|
|
65
153
|
doomain link --domain example.com --subdomain app --project my-app
|
|
@@ -71,223 +159,426 @@ Link the apex/root domain:
|
|
|
71
159
|
doomain link --domain example.com --apex --project my-app
|
|
72
160
|
```
|
|
73
161
|
|
|
74
|
-
|
|
162
|
+
An exact zone match without `--subdomain` is also treated as apex:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
doomain link example.com --project my-app
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Preview without writing to Vercel or DNS:
|
|
75
169
|
|
|
76
170
|
```bash
|
|
77
|
-
doomain link
|
|
171
|
+
doomain link app.example.com --project my-app --dry-run --json
|
|
78
172
|
```
|
|
79
173
|
|
|
80
|
-
Skip the verification wait:
|
|
174
|
+
Skip the DNS/Vercel verification wait:
|
|
81
175
|
|
|
82
176
|
```bash
|
|
83
177
|
doomain link app.example.com --project my-app --no-wait
|
|
84
178
|
```
|
|
85
179
|
|
|
86
|
-
|
|
180
|
+
Change the wait timeout, in seconds:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
doomain link app.example.com --project my-app --timeout 600
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Use a specific DNS provider instead of provider inference:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
doomain link app.example.com --project my-app --provider cloudflare
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Use `-p` as shorthand for `--project`:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
doomain link app.example.com -p my-app
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## What Gets Created
|
|
199
|
+
|
|
200
|
+
For apex/root domains, Doomain creates Vercel's apex `A` record:
|
|
201
|
+
|
|
202
|
+
```text
|
|
203
|
+
A @ 76.76.21.21
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
For subdomains, Doomain creates a `CNAME` to Vercel's recommended target. If Vercel does not return a special target, it uses:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
CNAME app cname.vercel-dns.com
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
During real linking, Doomain also reads Vercel's domain response and adds any required TXT verification records, for example:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
TXT _vercel vc-domain-verify=app.example.com,...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Project Detection
|
|
219
|
+
|
|
220
|
+
`--project` accepts a Vercel project id or name. If you omit it, Doomain searches upward from the current directory for `.vercel/project.json` and uses its `projectId`.
|
|
221
|
+
|
|
222
|
+
The interactive wizard also uses `.vercel/project.json`, but only preselects the local project when the detected `orgId` matches the selected Vercel account or team.
|
|
223
|
+
|
|
224
|
+
## Provider And Zone Inference
|
|
225
|
+
|
|
226
|
+
When `--provider` is omitted, Doomain lists zones from every configured provider and chooses the longest zone that matches the target domain.
|
|
227
|
+
|
|
228
|
+
Example: for `api.dev.example.com`, a provider zone named `dev.example.com` wins over `example.com`.
|
|
229
|
+
|
|
230
|
+
If two providers have the same best matching zone, Doomain stops with `PROVIDER_ZONE_AMBIGUOUS`. Re-run with `--provider` to choose one:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
doomain link app.example.com --project my-app --provider cloudflare
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Safety, Dry Runs, And Conflicts
|
|
237
|
+
|
|
238
|
+
Use `--dry-run` before applying changes:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
doomain link app.example.com --project my-app --dry-run --json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Dry runs do not write to Vercel or DNS. They resolve the target project, provider, zone, and base Vercel DNS record. They do not add the domain to Vercel, fetch Vercel TXT verification records, or inspect current DNS records for conflicts.
|
|
245
|
+
|
|
246
|
+
DNS conflict rules:
|
|
247
|
+
|
|
248
|
+
- Existing exact records are skipped.
|
|
249
|
+
- TXT records can coexist at the same name.
|
|
250
|
+
- Same-name, same-type conflicts require `--force`.
|
|
251
|
+
- CNAME slot conflicts require `--force` because a CNAME cannot share a name with most other record types.
|
|
252
|
+
|
|
253
|
+
Use `--force` only when you intend to replace conflicting DNS records or move an existing Vercel alias:
|
|
87
254
|
|
|
88
255
|
```bash
|
|
89
256
|
doomain link app.example.com --project my-app --force
|
|
90
257
|
```
|
|
91
258
|
|
|
259
|
+
`--force` can remove an existing Vercel alias from another project and add it to the target project.
|
|
260
|
+
|
|
261
|
+
Namecheap note: Namecheap's API writes DNS through `setHosts`, which replaces the full host list. Doomain reads all existing records first, applies planned changes in memory, preserves unrelated records, then submits the complete final record set.
|
|
262
|
+
|
|
92
263
|
## JSON And Agent Usage
|
|
93
264
|
|
|
94
|
-
`--json`
|
|
265
|
+
Use `--json` for automation. JSON mode never prompts and writes exactly one JSON object to stdout.
|
|
266
|
+
|
|
267
|
+
Successful commands use this shape:
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{"ok":true,"data":{}}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Failed commands use this shape:
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{"ok":false,"error":{"code":"MISSING_ARGUMENT","message":"Domain is required."}}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
JSON mode is also enabled automatically when stdout is not a TTY, which makes piped commands script-safe.
|
|
280
|
+
|
|
281
|
+
Use explicit commands for agents. The bare `doomain` command is interactive, and `doomain --json` returns an error that points agents to `doomain link --json`.
|
|
282
|
+
|
|
283
|
+
Useful agent-safe commands:
|
|
95
284
|
|
|
96
285
|
```bash
|
|
97
286
|
doomain link app.example.com --project my-app --json
|
|
98
|
-
doomain link --domain app.example.com --project my-app --json
|
|
99
|
-
doomain link --domain example.com --subdomain app --project my-app --json
|
|
100
|
-
doomain projects list --json
|
|
101
287
|
doomain providers list --json
|
|
102
288
|
doomain providers status --no-verify --json
|
|
289
|
+
doomain domains list --provider cloudflare --domain example.com --json
|
|
290
|
+
doomain projects list --search my-app --json
|
|
103
291
|
doomain schema --json
|
|
292
|
+
doomain schema link --json
|
|
104
293
|
```
|
|
105
294
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Use the schema command to inspect machine-readable command metadata:
|
|
295
|
+
The schema command prints machine-readable metadata for the documented command contracts:
|
|
109
296
|
|
|
110
297
|
```bash
|
|
111
|
-
doomain schema
|
|
112
|
-
doomain schema
|
|
298
|
+
doomain schema --json
|
|
299
|
+
doomain schema "providers connect" --json
|
|
113
300
|
```
|
|
114
301
|
|
|
115
|
-
##
|
|
302
|
+
## Command Reference
|
|
116
303
|
|
|
117
|
-
|
|
304
|
+
Run `doomain help <command>` for oclif-generated help.
|
|
305
|
+
|
|
306
|
+
### `doomain`
|
|
307
|
+
|
|
308
|
+
Starts the interactive wizard.
|
|
118
309
|
|
|
119
310
|
```bash
|
|
120
|
-
|
|
311
|
+
doomain
|
|
121
312
|
```
|
|
122
313
|
|
|
123
|
-
|
|
314
|
+
### `doomain link [domain]`
|
|
315
|
+
|
|
316
|
+
Links a Vercel project to a domain and creates DNS records.
|
|
124
317
|
|
|
125
|
-
|
|
318
|
+
Common flags:
|
|
319
|
+
|
|
320
|
+
- `--domain <domain>`: target domain or base zone.
|
|
321
|
+
- `--subdomain <name>`: subdomain under `--domain`.
|
|
322
|
+
- `--apex`: use the root/apex domain.
|
|
323
|
+
- `-p, --project <project>`: Vercel project id or name.
|
|
324
|
+
- `--provider <id>`: DNS provider id.
|
|
325
|
+
- `--dry-run`: preview without writing.
|
|
326
|
+
- `--force`: overwrite DNS conflicts and allow Vercel alias moves.
|
|
327
|
+
- `--wait`, `--no-wait`: wait for DNS and Vercel verification. Default is `--wait`.
|
|
328
|
+
- `--timeout <seconds>`: wait timeout. Default is `300`.
|
|
329
|
+
- `--json`: output one JSON object.
|
|
330
|
+
|
|
331
|
+
Examples:
|
|
126
332
|
|
|
127
333
|
```bash
|
|
128
|
-
doomain
|
|
334
|
+
doomain link app.example.com --project my-app
|
|
335
|
+
doomain link --domain example.com --subdomain app --project my-app
|
|
336
|
+
doomain link --domain example.com --apex --project my-app
|
|
337
|
+
doomain link app.example.com --project my-app --dry-run --json
|
|
129
338
|
```
|
|
130
339
|
|
|
131
|
-
|
|
340
|
+
### `doomain auth vercel`
|
|
132
341
|
|
|
133
|
-
|
|
342
|
+
Saves Vercel credentials locally.
|
|
134
343
|
|
|
135
344
|
```bash
|
|
136
|
-
doomain
|
|
137
|
-
|
|
138
|
-
--credential apiSecret=spaceship_secret
|
|
345
|
+
doomain auth vercel --token vercel_token
|
|
346
|
+
doomain auth vercel --token vercel_token --team-id team_123 --json
|
|
139
347
|
```
|
|
140
348
|
|
|
141
|
-
|
|
349
|
+
### `doomain auth logout vercel`
|
|
350
|
+
|
|
351
|
+
Removes saved Vercel credentials from the local config file.
|
|
142
352
|
|
|
143
353
|
```bash
|
|
144
|
-
doomain
|
|
354
|
+
doomain auth logout vercel
|
|
355
|
+
doomain auth logout vercel --json
|
|
145
356
|
```
|
|
146
357
|
|
|
147
|
-
|
|
358
|
+
If `VERCEL_TOKEN` or `VERCEL_TEAM_ID` are still set, they continue to override local config.
|
|
148
359
|
|
|
149
|
-
###
|
|
360
|
+
### `doomain providers list`
|
|
361
|
+
|
|
362
|
+
Lists supported DNS providers.
|
|
150
363
|
|
|
151
364
|
```bash
|
|
152
|
-
doomain providers
|
|
153
|
-
|
|
154
|
-
--credential apiKey=your_api_key \
|
|
155
|
-
--credential clientIp=your_whitelisted_ipv4
|
|
365
|
+
doomain providers list
|
|
366
|
+
doomain providers list --json
|
|
156
367
|
```
|
|
157
368
|
|
|
158
|
-
|
|
369
|
+
### `doomain providers connect [provider]`
|
|
370
|
+
|
|
371
|
+
Saves DNS provider credentials locally.
|
|
159
372
|
|
|
160
373
|
```bash
|
|
161
|
-
doomain providers connect
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
--credential clientIp=your_whitelisted_ipv4 \
|
|
165
|
-
--credential sandbox=true
|
|
374
|
+
doomain providers connect cloudflare -c apiToken=token -c accountId=account_id
|
|
375
|
+
doomain providers connect namecheap -c apiUser=user -c apiKey=key -c clientIp=127.0.0.1 --json
|
|
376
|
+
doomain providers connect spaceship --api-key key --api-secret secret
|
|
166
377
|
```
|
|
167
378
|
|
|
168
|
-
|
|
379
|
+
Common flags:
|
|
169
380
|
|
|
170
|
-
|
|
381
|
+
- `-c, --credential key=value`: provider credential. Can be repeated.
|
|
382
|
+
- `--api-key <key>`: Spaceship compatibility alias for `apiKey`.
|
|
383
|
+
- `--api-secret <secret>`: Spaceship compatibility alias for `apiSecret`.
|
|
384
|
+
- `--no-verify`: save credentials without calling the provider API first.
|
|
385
|
+
- `--json`: output one JSON object.
|
|
386
|
+
|
|
387
|
+
### `doomain providers add [provider]`
|
|
388
|
+
|
|
389
|
+
Alias for `providers connect`.
|
|
171
390
|
|
|
172
391
|
```bash
|
|
173
|
-
doomain providers
|
|
174
|
-
--credential apiToken=your_cloudflare_api_token \
|
|
175
|
-
--credential accountId=your_cloudflare_account_id
|
|
392
|
+
doomain providers add cloudflare
|
|
176
393
|
```
|
|
177
394
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
## Provider Management
|
|
395
|
+
### `doomain providers status`
|
|
181
396
|
|
|
182
|
-
|
|
397
|
+
Shows configured provider health.
|
|
183
398
|
|
|
184
399
|
```bash
|
|
185
|
-
doomain providers
|
|
400
|
+
doomain providers status
|
|
401
|
+
doomain providers status --no-verify --json
|
|
186
402
|
```
|
|
187
403
|
|
|
188
|
-
|
|
404
|
+
### `doomain providers verify <provider>`
|
|
405
|
+
|
|
406
|
+
Verifies saved provider credentials.
|
|
189
407
|
|
|
190
408
|
```bash
|
|
191
|
-
doomain providers
|
|
409
|
+
doomain providers verify cloudflare
|
|
410
|
+
doomain providers verify namecheap --json
|
|
192
411
|
```
|
|
193
412
|
|
|
194
|
-
|
|
413
|
+
### `doomain providers disconnect <provider>`
|
|
414
|
+
|
|
415
|
+
Removes saved DNS provider credentials locally. `providers logout` is an alias.
|
|
195
416
|
|
|
196
417
|
```bash
|
|
197
|
-
doomain providers
|
|
198
|
-
doomain providers
|
|
418
|
+
doomain providers disconnect cloudflare
|
|
419
|
+
doomain providers logout namecheap --json
|
|
199
420
|
```
|
|
200
421
|
|
|
201
|
-
|
|
422
|
+
Environment variables for that provider still override local config after disconnect.
|
|
423
|
+
|
|
424
|
+
### `doomain domains list`
|
|
425
|
+
|
|
426
|
+
Lists DNS zones and records.
|
|
202
427
|
|
|
203
428
|
```bash
|
|
204
|
-
doomain
|
|
205
|
-
doomain
|
|
429
|
+
doomain domains list --provider cloudflare
|
|
430
|
+
doomain domains list --provider cloudflare --domain example.com --json
|
|
206
431
|
```
|
|
207
432
|
|
|
208
|
-
|
|
433
|
+
If `--provider` is omitted, this command uses `DOOMAIN_PROVIDER`, then the saved default provider, then `spaceship`.
|
|
434
|
+
|
|
435
|
+
### `doomain projects list`
|
|
436
|
+
|
|
437
|
+
Lists Vercel projects for the configured Vercel account or team.
|
|
209
438
|
|
|
210
439
|
```bash
|
|
211
|
-
doomain
|
|
440
|
+
doomain projects list
|
|
441
|
+
doomain projects list --search my-app --json
|
|
212
442
|
```
|
|
213
443
|
|
|
214
|
-
|
|
444
|
+
### `doomain verify`
|
|
215
445
|
|
|
216
|
-
|
|
446
|
+
Asks Vercel to verify a project domain without changing DNS.
|
|
217
447
|
|
|
218
448
|
```bash
|
|
219
|
-
doomain
|
|
220
|
-
doomain
|
|
449
|
+
doomain verify --domain example.com --subdomain app --project my-app
|
|
450
|
+
doomain verify --domain example.com --apex --project my-app --json
|
|
221
451
|
```
|
|
222
452
|
|
|
223
|
-
|
|
453
|
+
For `verify`, pass a base domain plus `--subdomain`, or pass a base domain plus `--apex`.
|
|
454
|
+
|
|
455
|
+
### `doomain schema [command]`
|
|
456
|
+
|
|
457
|
+
Prints machine-readable command metadata.
|
|
224
458
|
|
|
225
459
|
```bash
|
|
226
|
-
doomain
|
|
227
|
-
doomain
|
|
460
|
+
doomain schema --json
|
|
461
|
+
doomain schema link --json
|
|
462
|
+
doomain schema "providers connect" --json
|
|
228
463
|
```
|
|
229
464
|
|
|
230
465
|
## Environment Variables
|
|
231
466
|
|
|
467
|
+
Vercel:
|
|
468
|
+
|
|
232
469
|
```bash
|
|
233
470
|
VERCEL_TOKEN
|
|
234
471
|
VERCEL_TEAM_ID
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Spaceship:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
235
477
|
SPACESHIP_API_KEY
|
|
236
478
|
SPACESHIP_API_SECRET
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Namecheap:
|
|
482
|
+
|
|
483
|
+
```bash
|
|
237
484
|
NAMECHEAP_API_USER
|
|
238
485
|
NAMECHEAP_API_KEY
|
|
239
486
|
NAMECHEAP_USERNAME
|
|
240
487
|
NAMECHEAP_CLIENT_IP
|
|
241
488
|
NAMECHEAP_SANDBOX
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Cloudflare:
|
|
492
|
+
|
|
493
|
+
```bash
|
|
242
494
|
CLOUDFLARE_API_TOKEN
|
|
243
495
|
CLOUDFLARE_ACCOUNT_ID
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Doomain defaults and config:
|
|
499
|
+
|
|
500
|
+
```bash
|
|
244
501
|
DOOMAIN_DOMAIN
|
|
245
502
|
DOOMAIN_PROVIDER
|
|
503
|
+
DOOMAIN_CONFIG_DIR
|
|
246
504
|
DOOMAIN_CONFIG_FILE
|
|
505
|
+
DOOMAIN_DEBUG
|
|
247
506
|
```
|
|
248
507
|
|
|
249
|
-
|
|
508
|
+
Notes:
|
|
250
509
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
doomain
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
doomain projects list # list Vercel projects
|
|
264
|
-
doomain verify # ask Vercel to verify a domain
|
|
265
|
-
doomain schema # print command schemas for agents
|
|
266
|
-
```
|
|
510
|
+
- `DOOMAIN_DOMAIN` is used by the interactive wizard and internal link planning, but the `doomain link` command currently still requires a positional domain or `--domain` before it calls the linker.
|
|
511
|
+
- `DOOMAIN_PROVIDER` is used by the interactive wizard and `domains list`. For `link`, pass `--provider` when you want to force a specific provider.
|
|
512
|
+
- Set `DOOMAIN_DEBUG=1` to enable provider debug mode where supported.
|
|
513
|
+
- Use `DOOMAIN_CONFIG_FILE` in tests or scripts when you want isolated credentials.
|
|
514
|
+
|
|
515
|
+
## Troubleshooting
|
|
516
|
+
|
|
517
|
+
`Missing Vercel token`
|
|
518
|
+
|
|
519
|
+
Run `doomain auth vercel` or set `VERCEL_TOKEN`.
|
|
520
|
+
|
|
521
|
+
`No DNS provider is configured`
|
|
267
522
|
|
|
268
|
-
Run `doomain
|
|
523
|
+
Run `doomain providers connect <provider>` or set the provider's required environment variables.
|
|
269
524
|
|
|
270
|
-
|
|
525
|
+
`PROVIDER_ZONE_NOT_FOUND`
|
|
271
526
|
|
|
272
|
-
|
|
527
|
+
The selected provider does not have a DNS zone matching the target domain. Check `doomain domains list --provider <provider>` or pass the correct `--provider`.
|
|
273
528
|
|
|
274
|
-
|
|
529
|
+
`PROVIDER_ZONE_AMBIGUOUS`
|
|
275
530
|
|
|
276
|
-
|
|
531
|
+
More than one configured provider has the same best matching zone. Re-run with `--provider cloudflare`, `--provider namecheap`, or `--provider spaceship`.
|
|
532
|
+
|
|
533
|
+
Namecheap authentication or permission errors
|
|
534
|
+
|
|
535
|
+
Make sure Namecheap API access is enabled and your current public IPv4 is whitelisted in Namecheap API Access settings.
|
|
536
|
+
|
|
537
|
+
Cloudflare permission errors
|
|
538
|
+
|
|
539
|
+
Make sure the API token has `Zone:Read` and `DNS:Edit` permissions for the account that owns the zones.
|
|
540
|
+
|
|
541
|
+
DNS propagation timeout
|
|
542
|
+
|
|
543
|
+
The DNS records may have been saved even if Vercel verification timed out. Check the domain in Vercel, inspect records with `doomain domains list`, or re-run verification with `doomain verify`.
|
|
544
|
+
|
|
545
|
+
Domain already assigned to another Vercel project
|
|
546
|
+
|
|
547
|
+
If you intend to move it, re-run `doomain link` with `--force`. This can remove the alias from the previous Vercel project.
|
|
548
|
+
|
|
549
|
+
SSL certificate is not ready yet
|
|
550
|
+
|
|
551
|
+
Vercel may need a few extra minutes to provision SSL after the domain verifies.
|
|
277
552
|
|
|
278
553
|
## Development
|
|
279
554
|
|
|
555
|
+
This repository is a TypeScript ESM oclif CLI package.
|
|
556
|
+
|
|
280
557
|
```bash
|
|
281
|
-
bun install
|
|
558
|
+
bun install --frozen-lockfile
|
|
282
559
|
bun run build
|
|
283
560
|
bun run test
|
|
284
561
|
```
|
|
285
562
|
|
|
286
|
-
Useful
|
|
563
|
+
Useful commands:
|
|
287
564
|
|
|
288
565
|
```bash
|
|
289
566
|
bun run lint
|
|
290
|
-
bun run
|
|
567
|
+
bun run check
|
|
568
|
+
bun run format
|
|
569
|
+
bunx mocha --forbid-only "test/path/to-file.test.ts"
|
|
570
|
+
./bin/dev.js link app.example.com --project my-app --dry-run
|
|
291
571
|
```
|
|
292
572
|
|
|
293
|
-
|
|
573
|
+
Notes for contributors:
|
|
574
|
+
|
|
575
|
+
- Source commands live in `src/commands/**`.
|
|
576
|
+
- Shared logic lives in `src/lib/**`.
|
|
577
|
+
- Build output goes to `dist/`; do not edit `dist` directly.
|
|
578
|
+
- `examples/**` is excluded from this package's Biome surface.
|
|
579
|
+
- Public command contract metadata lives in `src/lib/command-schema.ts`.
|
|
580
|
+
- `prepack` runs `oclif manifest && oclif readme`, which may update generated README command docs.
|
|
581
|
+
|
|
582
|
+
## License
|
|
583
|
+
|
|
584
|
+
MIT
|
|
@@ -3,8 +3,10 @@ import * as p from '@clack/prompts';
|
|
|
3
3
|
import { getConfigPath, maskSecret, updateConfig } from '../../lib/config.js';
|
|
4
4
|
import { jsonFlag } from '../../lib/flags.js';
|
|
5
5
|
import { createOutput, outputError } from '../../lib/output.js';
|
|
6
|
+
import { listGlobalVercelTokens } from '../../lib/vercel-auth.js';
|
|
6
7
|
import { createVercelClient } from '../../lib/vercel.js';
|
|
7
8
|
const PERSONAL_ACCOUNT = '__personal__';
|
|
9
|
+
const NEW_TOKEN = '__new_token__';
|
|
8
10
|
function assertValue(value, message) {
|
|
9
11
|
if (typeof value === 'string' && value.trim())
|
|
10
12
|
return value.trim();
|
|
@@ -14,6 +16,32 @@ function teamLabel(team) {
|
|
|
14
16
|
const name = team.name ?? team.slug;
|
|
15
17
|
return `${name} (${team.id})`;
|
|
16
18
|
}
|
|
19
|
+
function globalTokenLabel(token) {
|
|
20
|
+
return token.source === 'environment' ? `Use ${token.label}` : `Use ${token.label} token`;
|
|
21
|
+
}
|
|
22
|
+
async function promptVercelToken(globalTokens) {
|
|
23
|
+
if (globalTokens.length > 0) {
|
|
24
|
+
const selected = await p.select({
|
|
25
|
+
message: 'Vercel token',
|
|
26
|
+
options: [
|
|
27
|
+
...globalTokens.map((token, index) => ({ label: globalTokenLabel(token), value: String(index), hint: maskSecret(token.token) })),
|
|
28
|
+
{ label: 'Enter a new token', value: NEW_TOKEN },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
if (p.isCancel(selected)) {
|
|
32
|
+
p.cancel('Cancelled');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (selected !== NEW_TOKEN)
|
|
36
|
+
return globalTokens[Number(selected)]?.token ?? null;
|
|
37
|
+
}
|
|
38
|
+
const value = await p.password({ message: 'Vercel token' });
|
|
39
|
+
if (p.isCancel(value)) {
|
|
40
|
+
p.cancel('Cancelled');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
17
45
|
export default class AuthVercel extends Command {
|
|
18
46
|
static description = 'Save Vercel credentials locally.';
|
|
19
47
|
static flags = {
|
|
@@ -26,16 +54,19 @@ export default class AuthVercel extends Command {
|
|
|
26
54
|
const out = createOutput({ json: flags.json });
|
|
27
55
|
try {
|
|
28
56
|
let token = flags.token;
|
|
29
|
-
let teamId = flags['team-id'];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
57
|
+
let teamId = flags['team-id'] || process.env.VERCEL_TEAM_ID?.trim() || undefined;
|
|
58
|
+
const globalTokens = token ? [] : await listGlobalVercelTokens();
|
|
59
|
+
if (!token) {
|
|
60
|
+
if (out.json) {
|
|
61
|
+
token = globalTokens[0]?.token;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
token = (await promptVercelToken(globalTokens)) ?? undefined;
|
|
65
|
+
if (!token)
|
|
66
|
+
return;
|
|
35
67
|
}
|
|
36
|
-
token = value;
|
|
37
68
|
}
|
|
38
|
-
token = assertValue(token, 'Missing Vercel token. Pass --token or
|
|
69
|
+
token = assertValue(token, 'Missing Vercel token. Pass --token, set VERCEL_TOKEN, or sign in with Vercel CLI.');
|
|
39
70
|
if (!out.json && teamId === undefined) {
|
|
40
71
|
const spinner = p.spinner();
|
|
41
72
|
spinner.start('Loading Vercel teams');
|
package/dist/commands/wizard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
|
-
import { loadConfig, updateConfig } from '../lib/config.js';
|
|
3
|
+
import { loadConfig, maskSecret, updateConfig } from '../lib/config.js';
|
|
4
4
|
import { DoomainError } from '../lib/errors.js';
|
|
5
5
|
import { jsonFlag } from '../lib/flags.js';
|
|
6
6
|
import { createLinkPlan, linkDomain } from '../lib/link-domain.js';
|
|
@@ -8,8 +8,11 @@ import { detectLocalVercelProject } from '../lib/local-vercel.js';
|
|
|
8
8
|
import { createOutput, outputError } from '../lib/output.js';
|
|
9
9
|
import { createProvider, getProviderDefinition, listProviderDefinitions } from '../lib/providers/registry.js';
|
|
10
10
|
import { isProviderConfigured } from '../lib/providers/status.js';
|
|
11
|
+
import { listGlobalVercelTokens } from '../lib/vercel-auth.js';
|
|
11
12
|
import { createVercelClient } from '../lib/vercel.js';
|
|
12
13
|
const PERSONAL_ACCOUNT = '__personal__';
|
|
14
|
+
const NEW_TOKEN = '__new_token__';
|
|
15
|
+
const SAVED_TOKEN = '__saved_token__';
|
|
13
16
|
function cancelIfNeeded(value) {
|
|
14
17
|
if (p.isCancel(value)) {
|
|
15
18
|
p.cancel('Cancelled');
|
|
@@ -105,6 +108,31 @@ function teamLabel(team) {
|
|
|
105
108
|
const name = team.name ?? team.slug;
|
|
106
109
|
return `${name} (${team.id})`;
|
|
107
110
|
}
|
|
111
|
+
function globalTokenLabel(token) {
|
|
112
|
+
return token.source === 'environment' ? `Use ${token.label}` : `Use ${token.label} token`;
|
|
113
|
+
}
|
|
114
|
+
async function promptVercelToken(globalTokens, savedToken) {
|
|
115
|
+
if (globalTokens.length > 0) {
|
|
116
|
+
const selected = await p.select({
|
|
117
|
+
message: 'Vercel token',
|
|
118
|
+
options: [
|
|
119
|
+
...globalTokens.map((token, index) => ({ label: globalTokenLabel(token), value: String(index), hint: maskSecret(token.token) })),
|
|
120
|
+
...(savedToken && !globalTokens.some((token) => token.token === savedToken)
|
|
121
|
+
? [{ label: 'Use saved Doomain token', value: SAVED_TOKEN, hint: maskSecret(savedToken) }]
|
|
122
|
+
: []),
|
|
123
|
+
{ label: 'Enter a new token', value: NEW_TOKEN },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
const resolved = cancelIfNeeded(selected);
|
|
127
|
+
if (resolved === null)
|
|
128
|
+
return null;
|
|
129
|
+
if (resolved === SAVED_TOKEN)
|
|
130
|
+
return savedToken ?? null;
|
|
131
|
+
if (resolved !== NEW_TOKEN)
|
|
132
|
+
return globalTokens[Number(resolved)]?.token ?? null;
|
|
133
|
+
}
|
|
134
|
+
return promptRequired('Vercel token', { password: true });
|
|
135
|
+
}
|
|
108
136
|
function recordPreview(record, providerName) {
|
|
109
137
|
const name = record.name === '@' ? 'root' : record.name;
|
|
110
138
|
return `DNS: ${record.type} ${name} -> ${record.value} in ${providerName}`;
|
|
@@ -128,12 +156,13 @@ export default class Wizard extends Command {
|
|
|
128
156
|
const config = await loadConfig();
|
|
129
157
|
const providerDefinitions = listProviderDefinitions();
|
|
130
158
|
const localProject = detectLocalVercelProject();
|
|
131
|
-
|
|
159
|
+
const globalVercelTokens = await listGlobalVercelTokens();
|
|
160
|
+
let vercelToken = config.vercel?.token;
|
|
132
161
|
let vercelTeamId = process.env.VERCEL_TEAM_ID || localProject?.orgId || config.vercel?.teamId;
|
|
133
162
|
const defaultProvider = process.env.DOOMAIN_PROVIDER || config.defaults?.provider;
|
|
134
163
|
const defaultDomain = process.env.DOOMAIN_DOMAIN || config.defaults?.domain;
|
|
135
|
-
if (!vercelToken) {
|
|
136
|
-
vercelToken = (await
|
|
164
|
+
if (globalVercelTokens.length > 0 || !vercelToken) {
|
|
165
|
+
vercelToken = (await promptVercelToken(globalVercelTokens, vercelToken)) ?? undefined;
|
|
137
166
|
if (!vercelToken)
|
|
138
167
|
return;
|
|
139
168
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type GlobalVercelTokenSource = 'environment' | 'vercel-cli';
|
|
2
|
+
export interface GlobalVercelToken {
|
|
3
|
+
authFile?: string;
|
|
4
|
+
label: string;
|
|
5
|
+
source: GlobalVercelTokenSource;
|
|
6
|
+
token: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function getVercelCliAuthFiles(env?: NodeJS.ProcessEnv): string[];
|
|
9
|
+
export declare function listGlobalVercelTokens(opts?: {
|
|
10
|
+
authFile?: string;
|
|
11
|
+
authFiles?: string[];
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
}): Promise<GlobalVercelToken[]>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { delimiter, join } from 'node:path';
|
|
5
|
+
function xdgDataHome(env) {
|
|
6
|
+
if (env.XDG_DATA_HOME)
|
|
7
|
+
return env.XDG_DATA_HOME;
|
|
8
|
+
if (process.platform === 'darwin')
|
|
9
|
+
return join(homedir(), 'Library', 'Application Support');
|
|
10
|
+
if (process.platform === 'win32')
|
|
11
|
+
return join(env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'xdg.data');
|
|
12
|
+
return join(homedir(), '.local', 'share');
|
|
13
|
+
}
|
|
14
|
+
function xdgDataDirs(appName, env) {
|
|
15
|
+
const dirs = [join(xdgDataHome(env), appName)];
|
|
16
|
+
if (env.XDG_DATA_DIRS)
|
|
17
|
+
dirs.push(...env.XDG_DATA_DIRS.split(delimiter).map((dir) => join(dir, appName)));
|
|
18
|
+
return dirs;
|
|
19
|
+
}
|
|
20
|
+
export function getVercelCliAuthFiles(env = process.env) {
|
|
21
|
+
return [
|
|
22
|
+
...xdgDataDirs('com.vercel.cli', env),
|
|
23
|
+
join(homedir(), '.now'),
|
|
24
|
+
...xdgDataDirs('now', env),
|
|
25
|
+
join(homedir(), '.vercel'),
|
|
26
|
+
].map((dir) => join(dir, 'auth.json'));
|
|
27
|
+
}
|
|
28
|
+
async function readVercelCliToken(authFile) {
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(await readFile(authFile, 'utf8'));
|
|
31
|
+
return typeof data.token === 'string' && data.token.trim() ? data.token.trim() : undefined;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function listGlobalVercelTokens(opts = {}) {
|
|
38
|
+
const env = opts.env ?? process.env;
|
|
39
|
+
const tokens = [];
|
|
40
|
+
const envToken = env.VERCEL_TOKEN?.trim();
|
|
41
|
+
if (envToken) {
|
|
42
|
+
tokens.push({ label: 'VERCEL_TOKEN', source: 'environment', token: envToken });
|
|
43
|
+
}
|
|
44
|
+
const authFiles = opts.authFile ? [opts.authFile] : (opts.authFiles ?? getVercelCliAuthFiles(env));
|
|
45
|
+
for (const authFile of authFiles) {
|
|
46
|
+
if (!existsSync(authFile))
|
|
47
|
+
continue;
|
|
48
|
+
const cliToken = await readVercelCliToken(authFile);
|
|
49
|
+
if (cliToken && !tokens.some((item) => item.token === cliToken)) {
|
|
50
|
+
tokens.push({ authFile, label: 'Vercel CLI', source: 'vercel-cli', token: cliToken });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return tokens;
|
|
54
|
+
}
|
package/dist/lib/vercel.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { loadConfig } from './config.js';
|
|
2
2
|
import { DoomainError } from './errors.js';
|
|
3
|
+
import { listGlobalVercelTokens } from './vercel-auth.js';
|
|
3
4
|
const VERCEL_API_URL = 'https://api.vercel.com';
|
|
4
5
|
export const VERCEL_APEX_A_RECORD = '76.76.21.21';
|
|
5
6
|
export const VERCEL_CNAME_RECORD = 'cname.vercel-dns.com';
|
|
6
7
|
export async function resolveVercelConfig() {
|
|
7
8
|
const config = await loadConfig();
|
|
8
|
-
const token = process.env.VERCEL_TOKEN || config.vercel?.token;
|
|
9
|
+
const token = process.env.VERCEL_TOKEN || config.vercel?.token || (await listGlobalVercelTokens())[0]?.token;
|
|
9
10
|
const teamId = process.env.VERCEL_TEAM_ID || config.vercel?.teamId;
|
|
10
11
|
if (!token) {
|
|
11
|
-
throw new DoomainError('MISSING_CREDENTIALS', 'Missing Vercel token. Run `doomain auth vercel
|
|
12
|
+
throw new DoomainError('MISSING_CREDENTIALS', 'Missing Vercel token. Run `doomain auth vercel`, set VERCEL_TOKEN, or sign in with Vercel CLI.');
|
|
12
13
|
}
|
|
13
14
|
return { token, teamId };
|
|
14
15
|
}
|
package/oclif.manifest.json
CHANGED