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 CHANGED
@@ -1,20 +1,20 @@
1
1
  # Doomain
2
2
 
3
- Doomain links a Vercel project to a domain from the terminal.
3
+ Doomain links a Vercel project to a custom domain from your terminal.
4
4
 
5
- It handles the repetitive parts of shipping a custom domain: finding the right Vercel project, choosing a DNS zone, adding the domain to Vercel, creating the required DNS records, waiting for DNS propagation, and asking Vercel to verify the domain.
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` when calling Doomain from scripts, CI, or agents.
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 powered by Clack.
12
- - Non-interactive JSON output for automation and agent workflows.
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 runs that preview Vercel and DNS changes before writing.
16
- - DNS propagation and Vercel verification waiting.
17
- - Built-in Spaceship, Namecheap, and Cloudflare providers.
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 prompts for missing credentials, lets you select a Vercel account/team from the token, detects the current Vercel project when possible, lets you connect a DNS provider, lists available domains, previews the changes, and then applies them.
35
+ The wizard will:
36
36
 
37
- For automation, pass everything explicitly and add `--json`:
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-app --json
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
- ## How It Works
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
- 1. Resolve the Vercel account/team, then resolve the project from `--project`, `.vercel/project.json`, or the interactive project selector.
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
- Link a full domain:
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
- Link a subdomain from a base zone:
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
- Preview changes without writing to Vercel or DNS:
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 --provider spaceship --domain example.com --apex --project my-app --dry-run --json
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
- Overwrite conflicting DNS records:
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` prints a single JSON object to stdout and never prompts. Use it for scripts, CI, or agent tools.
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
- When `--provider` is omitted, Doomain searches configured DNS providers and selects the longest matching DNS zone for the target domain. If more than one provider has the same best match, Doomain asks you to pass `--provider` explicitly.
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 link --json
298
+ doomain schema --json
299
+ doomain schema "providers connect" --json
113
300
  ```
114
301
 
115
- ## Credentials
302
+ ## Command Reference
116
303
 
117
- Doomain stores local configuration at:
304
+ Run `doomain help <command>` for oclif-generated help.
305
+
306
+ ### `doomain`
307
+
308
+ Starts the interactive wizard.
118
309
 
119
310
  ```bash
120
- ~/.doomain/config.json
311
+ doomain
121
312
  ```
122
313
 
123
- The config file is written with `0600` permissions. Environment variables override values in the local config.
314
+ ### `doomain link [domain]`
315
+
316
+ Links a Vercel project to a domain and creates DNS records.
124
317
 
125
- ### Vercel
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 auth vercel --token vercel_token
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
- Interactive mode fetches Vercel teams from the token and lets you choose a team or your personal account. Use `--team-id` or `VERCEL_TEAM_ID` for non-interactive team-scoped usage.
340
+ ### `doomain auth vercel`
132
341
 
133
- ### Spaceship
342
+ Saves Vercel credentials locally.
134
343
 
135
344
  ```bash
136
- doomain providers connect spaceship \
137
- --credential apiKey=spaceship_key \
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
- Compatibility aliases are also available:
349
+ ### `doomain auth logout vercel`
350
+
351
+ Removes saved Vercel credentials from the local config file.
142
352
 
143
353
  ```bash
144
- doomain providers connect spaceship --api-key spaceship_key --api-secret spaceship_secret
354
+ doomain auth logout vercel
355
+ doomain auth logout vercel --json
145
356
  ```
146
357
 
147
- Spaceship API keys need domain read access and DNS record read/write access.
358
+ If `VERCEL_TOKEN` or `VERCEL_TEAM_ID` are still set, they continue to override local config.
148
359
 
149
- ### Namecheap
360
+ ### `doomain providers list`
361
+
362
+ Lists supported DNS providers.
150
363
 
151
364
  ```bash
152
- doomain providers connect namecheap \
153
- --credential apiUser=your_namecheap_user \
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
- For Namecheap sandbox testing:
369
+ ### `doomain providers connect [provider]`
370
+
371
+ Saves DNS provider credentials locally.
159
372
 
160
373
  ```bash
161
- doomain providers connect namecheap \
162
- --credential apiUser=your_sandbox_user \
163
- --credential apiKey=your_sandbox_key \
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
- Namecheap API access must be enabled, and `clientIp` must be whitelisted in Namecheap API Access settings.
379
+ Common flags:
169
380
 
170
- ### Cloudflare
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 connect cloudflare \
174
- --credential apiToken=your_cloudflare_api_token \
175
- --credential accountId=your_cloudflare_account_id
392
+ doomain providers add cloudflare
176
393
  ```
177
394
 
178
- Cloudflare API tokens need `Zone:Read` and `DNS:Edit` permissions for the account. Doomain creates Vercel records as DNS-only records, not proxied records.
179
-
180
- ## Provider Management
395
+ ### `doomain providers status`
181
396
 
182
- List supported providers:
397
+ Shows configured provider health.
183
398
 
184
399
  ```bash
185
- doomain providers list
400
+ doomain providers status
401
+ doomain providers status --no-verify --json
186
402
  ```
187
403
 
188
- Interactively add a provider:
404
+ ### `doomain providers verify <provider>`
405
+
406
+ Verifies saved provider credentials.
189
407
 
190
408
  ```bash
191
- doomain providers add
409
+ doomain providers verify cloudflare
410
+ doomain providers verify namecheap --json
192
411
  ```
193
412
 
194
- Check configured provider health:
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 status
198
- doomain providers status --no-verify --json
418
+ doomain providers disconnect cloudflare
419
+ doomain providers logout namecheap --json
199
420
  ```
200
421
 
201
- Disconnect a saved DNS provider:
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 providers disconnect namecheap
205
- doomain providers disconnect cloudflare --json
429
+ doomain domains list --provider cloudflare
430
+ doomain domains list --provider cloudflare --domain example.com --json
206
431
  ```
207
432
 
208
- Remove saved Vercel credentials:
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 auth logout vercel
440
+ doomain projects list
441
+ doomain projects list --search my-app --json
212
442
  ```
213
443
 
214
- Logout commands remove credentials from `~/.doomain/config.json`. If matching environment variables are still set, they continue to override local config.
444
+ ### `doomain verify`
215
445
 
216
- Verify one provider's saved credentials:
446
+ Asks Vercel to verify a project domain without changing DNS.
217
447
 
218
448
  ```bash
219
- doomain providers verify spaceship
220
- doomain providers verify namecheap --json
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
- List DNS zones and records:
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 domains list
227
- doomain domains list --provider cloudflare --domain example.com --json
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
- ## Commands
508
+ Notes:
250
509
 
251
- ```bash
252
- doomain # interactive wizard
253
- doomain link # link a Vercel project and domain
254
- doomain auth vercel # save Vercel credentials
255
- doomain auth logout vercel # remove saved Vercel credentials
256
- doomain providers list # list supported DNS providers
257
- doomain providers add # interactively add a DNS provider
258
- doomain providers connect # save provider credentials
259
- doomain providers disconnect # remove saved provider credentials
260
- doomain providers status # show configured provider health
261
- doomain providers verify # verify provider credentials
262
- doomain domains list # list DNS zones and records
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 help <command>` for command-specific flags and examples.
523
+ Run `doomain providers connect <provider>` or set the provider's required environment variables.
269
524
 
270
- ## Provider Notes
525
+ `PROVIDER_ZONE_NOT_FOUND`
271
526
 
272
- Spaceship, Namecheap, and Cloudflare are implemented through a shared DNS provider contract. Each provider declares its credentials and capabilities, then implements zone listing, record listing, change planning, and change application behind the same interface.
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
- Namecheap writes DNS through `setHosts`, which replaces the full host list. Doomain reads all existing records first, applies the planned change in memory, preserves unrelated records, then submits the complete final record set.
529
+ `PROVIDER_ZONE_AMBIGUOUS`
275
530
 
276
- Cloudflare supports proxied records generally, but Doomain writes Vercel `A`, `AAAA`, and `CNAME` records with `proxied: false` so Vercel can validate them correctly.
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 scripts:
563
+ Useful commands:
287
564
 
288
565
  ```bash
289
566
  bun run lint
290
- bun run prepack
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
- The package is an oclif CLI. Source lives in `src/commands` and `src/lib`; compiled output is written to `dist`.
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
- if (!out.json && !token) {
31
- const value = await p.password({ message: 'Vercel token' });
32
- if (p.isCancel(value)) {
33
- p.cancel('Cancelled');
34
- return;
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 set VERCEL_TOKEN.');
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');
@@ -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
- let vercelToken = process.env.VERCEL_TOKEN || config.vercel?.token;
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 promptRequired('Vercel token', { password: true })) ?? undefined;
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
+ }
@@ -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` or set VERCEL_TOKEN.');
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
  }
@@ -629,5 +629,5 @@
629
629
  ]
630
630
  }
631
631
  },
632
- "version": "0.1.1"
632
+ "version": "0.1.2"
633
633
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "doomain",
3
3
  "description": "Link your vercel project and domain in seconds",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {