doomain 0.1.18 → 0.1.20
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 +65 -2
- package/dist/commands/auth/clerk.js +1 -1
- package/dist/commands/auth/vercel.js +10 -4
- package/dist/commands/clerk/domains/add.js +9 -3
- package/dist/commands/dns/point.d.ts +21 -0
- package/dist/commands/dns/point.js +123 -0
- package/dist/commands/domains/list.js +9 -3
- package/dist/commands/link.js +9 -3
- package/dist/commands/providers/connect.js +15 -4
- package/dist/commands/providers/disconnect.js +16 -4
- package/dist/commands/providers/list.js +1 -1
- package/dist/commands/wizard.js +50 -14
- package/dist/index.d.ts +1 -1
- package/dist/lib/clerk.js +5 -1
- package/dist/lib/command-schema.js +154 -34
- package/dist/lib/domain-provider.js +10 -3
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/flags.js +9 -3
- package/dist/lib/link-domain.js +11 -4
- package/dist/lib/point-domain.d.ts +50 -0
- package/dist/lib/point-domain.js +212 -0
- package/dist/lib/providers/cloudflare/index.js +13 -5
- package/dist/lib/providers/core/config.js +2 -1
- package/dist/lib/providers/core/planner.js +25 -23
- package/dist/lib/providers/hostinger/index.js +30 -1
- package/dist/lib/providers/namecheap/index.js +11 -2
- package/dist/lib/providers/registry.js +6 -1
- package/dist/lib/providers/status.js +3 -1
- package/dist/lib/validate.js +9 -2
- package/dist/lib/vercel.js +5 -2
- package/oclif.manifest.json +108 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Doomain
|
|
2
2
|
|
|
3
|
-
Doomain links Vercel projects
|
|
3
|
+
Doomain points DNS records and links Vercel projects or first-time Clerk production instances to custom domains from your terminal.
|
|
4
4
|
|
|
5
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
|
|
|
@@ -12,6 +12,7 @@ Use the interactive wizard when working by hand. Use explicit commands with `--j
|
|
|
12
12
|
- Script-friendly commands with one JSON object on stdout.
|
|
13
13
|
- Vercel project detection from `.vercel/project.json`.
|
|
14
14
|
- DNS provider inference by longest matching configured zone.
|
|
15
|
+
- Generic DNS pointing for VPS, load balancer, and canonical-hostname targets.
|
|
15
16
|
- Dry-run plans before writing changes.
|
|
16
17
|
- Safety checks before replacing DNS records that point elsewhere.
|
|
17
18
|
- DNS propagation and Vercel verification wait loop.
|
|
@@ -87,6 +88,12 @@ Apply it:
|
|
|
87
88
|
doomain link app.example.com --project my-vercel-project --json
|
|
88
89
|
```
|
|
89
90
|
|
|
91
|
+
Point a domain at non-Vercel infrastructure such as a VPS:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
doomain dns point app.example.com --target 203.0.113.10 --json
|
|
95
|
+
```
|
|
96
|
+
|
|
90
97
|
Set up a Clerk application's first production instance and primary domain:
|
|
91
98
|
|
|
92
99
|
```bash
|
|
@@ -154,7 +161,7 @@ doomain providers connect cloudflare \
|
|
|
154
161
|
--credential accountId=your_cloudflare_account_id
|
|
155
162
|
```
|
|
156
163
|
|
|
157
|
-
Cloudflare
|
|
164
|
+
Cloudflare `A`, `AAAA`, and `CNAME` records created by Doomain are set to `proxied: false` so targets remain directly verifiable.
|
|
158
165
|
|
|
159
166
|
### Hostinger
|
|
160
167
|
|
|
@@ -221,6 +228,39 @@ Use `-p` as shorthand for `--project`:
|
|
|
221
228
|
doomain link app.example.com -p my-app
|
|
222
229
|
```
|
|
223
230
|
|
|
231
|
+
## Pointing DNS At A VPS Or Hostname
|
|
232
|
+
|
|
233
|
+
Use `dns point` when Doomain should only reconcile DNS, without adding the domain to Vercel or Clerk. It infers the configured provider account and longest matching zone in the same way as `link`.
|
|
234
|
+
|
|
235
|
+
Point an apex or subdomain at an IPv4 or IPv6 address:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
doomain dns point example.com --target 203.0.113.10
|
|
239
|
+
doomain dns point app.example.com --target 2001:db8::10
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Point a subdomain at a canonical hostname:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
doomain dns point app.example.com --target origin.example.net
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The record type is inferred as `A`, `AAAA`, or `CNAME`. Use `--type` to require a specific type, `--ttl` to change the default 300-second TTL, and `--provider`/`--account` to override provider inference.
|
|
249
|
+
|
|
250
|
+
Canonical hostname targets use CNAME records. Doomain rejects an apex CNAME when the selected provider does not advertise apex-CNAME support; use an IPv4 or IPv6 target for that apex instead.
|
|
251
|
+
|
|
252
|
+
Preview without writing:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
doomain dns point app.example.com --target 203.0.113.10 --dry-run --json
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The preview resolves the provider, account, zone, record name, type, and value without reading or writing current DNS records. Conflicts are checked when a real write is attempted.
|
|
259
|
+
|
|
260
|
+
By default, a successful write waits up to 300 seconds for public DNS. Use `--no-wait` to return immediately or `--timeout <seconds>` to change the limit. Skipping the wait, a dry run, or reaching the timeout returns `propagated: false`; a propagation timeout does not turn a successful provider write into an error.
|
|
261
|
+
|
|
262
|
+
Existing exact records are skipped. Conflicting records fail with `DNS_TARGET_CONFLICT` in JSON/non-interactive mode. Use `--force` only after approving replacement of the existing target.
|
|
263
|
+
|
|
224
264
|
## Clerk Production Domains
|
|
225
265
|
|
|
226
266
|
`doomain clerk domains add` mirrors Clerk CLI's initial production deployment API. It creates a production instance by cloning the application's development instance, sets the requested primary domain, writes every CNAME returned by Clerk, and optionally waits for Clerk's DNS, SSL, and email DNS status.
|
|
@@ -335,6 +375,7 @@ Useful agent-safe commands:
|
|
|
335
375
|
|
|
336
376
|
```bash
|
|
337
377
|
doomain link app.example.com --project my-app --json
|
|
378
|
+
doomain dns point app.example.com --target 203.0.113.10 --json
|
|
338
379
|
doomain providers list --json
|
|
339
380
|
doomain providers status --no-verify --json
|
|
340
381
|
doomain domains find hacktheandes.com --json
|
|
@@ -413,6 +454,28 @@ doomain auth vercel --token vercel_token
|
|
|
413
454
|
doomain auth vercel --token vercel_token --team-id team_123 --json
|
|
414
455
|
```
|
|
415
456
|
|
|
457
|
+
### `doomain dns point <domain>`
|
|
458
|
+
|
|
459
|
+
Points an apex or subdomain at an IPv4 or IPv6 address, or a subdomain at a canonical hostname, using the matching configured DNS provider.
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
doomain dns point example.com --target 203.0.113.10
|
|
463
|
+
doomain dns point app.example.com --target origin.example.net --no-wait --json
|
|
464
|
+
doomain dns point app.example.com --target 203.0.113.10 --provider spaceship --account work --force --json
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Common flags:
|
|
468
|
+
|
|
469
|
+
- `--target <value>`: required IPv4, IPv6, or hostname target.
|
|
470
|
+
- `--type <A|AAAA|CNAME>`: require a record type instead of inferring it.
|
|
471
|
+
- `--provider <id>`, `--account <alias>`: select a configured provider account instead of inferring one.
|
|
472
|
+
- `--ttl <seconds>`: DNS TTL. Default is `300`.
|
|
473
|
+
- `--dry-run`: preview without writing.
|
|
474
|
+
- `--force`: overwrite conflicting DNS records.
|
|
475
|
+
- `--wait`, `--no-wait`: wait for public DNS propagation. Default is `--wait`.
|
|
476
|
+
- `--timeout <seconds>`: propagation wait timeout. Default is `300`.
|
|
477
|
+
- `--json`: output one JSON object.
|
|
478
|
+
|
|
416
479
|
### `doomain auth logout vercel`
|
|
417
480
|
|
|
418
481
|
Removes saved Vercel credentials from the local config file.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Command, Flags } from '@oclif/core';
|
|
2
1
|
import * as p from '@clack/prompts';
|
|
2
|
+
import { Command, Flags } from '@oclif/core';
|
|
3
3
|
import { createClerkPlatformClient } from '../../lib/clerk.js';
|
|
4
4
|
import { getConfigPath, maskSecret, updateConfig } from '../../lib/config.js';
|
|
5
5
|
import { jsonFlag } from '../../lib/flags.js';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Command, Flags } from '@oclif/core';
|
|
2
1
|
import * as p from '@clack/prompts';
|
|
2
|
+
import { Command, Flags } from '@oclif/core';
|
|
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';
|
|
7
6
|
import { createVercelClient } from '../../lib/vercel.js';
|
|
7
|
+
import { listGlobalVercelTokens } from '../../lib/vercel-auth.js';
|
|
8
8
|
const PERSONAL_ACCOUNT = '__personal__';
|
|
9
9
|
const NEW_TOKEN = '__new_token__';
|
|
10
10
|
function assertValue(value, message) {
|
|
@@ -24,7 +24,11 @@ async function promptVercelToken(globalTokens) {
|
|
|
24
24
|
const selected = await p.select({
|
|
25
25
|
message: 'Vercel token',
|
|
26
26
|
options: [
|
|
27
|
-
...globalTokens.map((token, index) => ({
|
|
27
|
+
...globalTokens.map((token, index) => ({
|
|
28
|
+
label: globalTokenLabel(token),
|
|
29
|
+
value: String(index),
|
|
30
|
+
hint: maskSecret(token.token),
|
|
31
|
+
})),
|
|
28
32
|
{ label: 'Enter a new token', value: NEW_TOKEN },
|
|
29
33
|
],
|
|
30
34
|
});
|
|
@@ -46,7 +50,9 @@ export default class AuthVercel extends Command {
|
|
|
46
50
|
static description = 'Save Vercel credentials locally.';
|
|
47
51
|
static flags = {
|
|
48
52
|
json: jsonFlag,
|
|
49
|
-
'team-id': Flags.string({
|
|
53
|
+
'team-id': Flags.string({
|
|
54
|
+
description: 'Vercel team id. Interactive mode can fetch and select this from your token.',
|
|
55
|
+
}),
|
|
50
56
|
token: Flags.string({ description: 'Vercel API token.' }),
|
|
51
57
|
};
|
|
52
58
|
async run() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Args, Command, Flags } from '@oclif/core';
|
|
2
1
|
import * as p from '@clack/prompts';
|
|
2
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
3
3
|
import { addClerkProductionDomain } from '../../../lib/clerk-domain.js';
|
|
4
4
|
import { accountFlag, jsonFlag, providerFlag } from '../../../lib/flags.js';
|
|
5
5
|
import { createOutput, outputError } from '../../../lib/output.js';
|
|
@@ -33,12 +33,18 @@ export default class ClerkDomainsAdd extends Command {
|
|
|
33
33
|
static flags = {
|
|
34
34
|
account: accountFlag,
|
|
35
35
|
app: Flags.string({ description: 'Clerk application id. Defaults to CLERK_APPLICATION_ID or saved Clerk config.' }),
|
|
36
|
-
'dry-run': Flags.boolean({
|
|
36
|
+
'dry-run': Flags.boolean({
|
|
37
|
+
description: 'Check the Clerk application and DNS zone without creating the production instance.',
|
|
38
|
+
}),
|
|
37
39
|
force: Flags.boolean({ description: 'Overwrite DNS records that conflict with Clerk requirements.' }),
|
|
38
40
|
json: jsonFlag,
|
|
39
41
|
provider: providerFlag,
|
|
40
42
|
timeout: Flags.integer({ default: 300, description: 'Wait timeout in seconds.' }),
|
|
41
|
-
wait: Flags.boolean({
|
|
43
|
+
wait: Flags.boolean({
|
|
44
|
+
allowNo: true,
|
|
45
|
+
default: true,
|
|
46
|
+
description: 'Wait for Clerk DNS, SSL, and email DNS verification.',
|
|
47
|
+
}),
|
|
42
48
|
};
|
|
43
49
|
async run() {
|
|
44
50
|
const { args, flags } = await this.parse(ClerkDomainsAdd);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class DnsPoint extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
domain: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
target: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
ttl: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
};
|
|
20
|
+
run(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
3
|
+
import { accountFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
|
|
4
|
+
import { createOutput, outputError } from '../../lib/output.js';
|
|
5
|
+
import { pointDomain } from '../../lib/point-domain.js';
|
|
6
|
+
function preview(result) {
|
|
7
|
+
return [
|
|
8
|
+
`Domain: ${result.domain}`,
|
|
9
|
+
`DNS provider: ${result.provider}/${result.account} (${result.zoneDomain})`,
|
|
10
|
+
`Record: ${result.record.type} ${result.record.name} -> ${result.record.value}`,
|
|
11
|
+
].join('\n');
|
|
12
|
+
}
|
|
13
|
+
function conflictNote(warning) {
|
|
14
|
+
return [
|
|
15
|
+
`${warning.domain} already points somewhere else.`,
|
|
16
|
+
'',
|
|
17
|
+
'Existing:',
|
|
18
|
+
...warning.conflicts.map((conflict) => `- ${conflict.existing.type} ${conflict.existing.name} -> ${conflict.existing.value}`),
|
|
19
|
+
'',
|
|
20
|
+
'Desired:',
|
|
21
|
+
...warning.desired.map((record) => `- ${record.type} ${record.name} -> ${record.value}`),
|
|
22
|
+
].join('\n');
|
|
23
|
+
}
|
|
24
|
+
function successMessages(result, waited) {
|
|
25
|
+
if (result.propagated) {
|
|
26
|
+
return {
|
|
27
|
+
outro: result.updated
|
|
28
|
+
? `${result.domain} now points to ${result.record.value}.`
|
|
29
|
+
: `${result.domain} already points to ${result.record.value}.`,
|
|
30
|
+
spinner: 'DNS record is live',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (!waited) {
|
|
34
|
+
return {
|
|
35
|
+
outro: result.updated
|
|
36
|
+
? `${result.domain} was updated; propagation was not checked.`
|
|
37
|
+
: `${result.domain} was already configured; propagation was not checked.`,
|
|
38
|
+
spinner: result.updated ? 'DNS record saved; propagation not checked' : 'DNS record already configured',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
outro: result.updated
|
|
43
|
+
? `${result.domain} was updated; propagation is still pending.`
|
|
44
|
+
: `${result.domain} was already configured; public DNS does not match yet.`,
|
|
45
|
+
spinner: result.updated ? 'DNS record saved' : 'DNS record already configured',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export default class DnsPoint extends Command {
|
|
49
|
+
static description = 'Point a DNS name at an IP address or canonical hostname.';
|
|
50
|
+
static examples = [
|
|
51
|
+
'<%= config.bin %> <%= command.id %> app.example.com --target 203.0.113.10 --json',
|
|
52
|
+
'<%= config.bin %> <%= command.id %> app.example.com --target origin.example.net --dry-run --json',
|
|
53
|
+
'<%= config.bin %> <%= command.id %> example.com --target 203.0.113.10 --provider spaceship --account work --force --json',
|
|
54
|
+
];
|
|
55
|
+
static args = {
|
|
56
|
+
domain: Args.string({ description: 'Fully qualified domain to point.', required: true }),
|
|
57
|
+
};
|
|
58
|
+
static flags = {
|
|
59
|
+
account: accountFlag,
|
|
60
|
+
'dry-run': Flags.boolean({ description: 'Preview the DNS record without writing it.' }),
|
|
61
|
+
force: Flags.boolean({ description: 'Overwrite conflicting DNS records.' }),
|
|
62
|
+
json: jsonFlag,
|
|
63
|
+
provider: providerFlag,
|
|
64
|
+
target: Flags.string({ description: 'IPv4, IPv6, or hostname target.', required: true }),
|
|
65
|
+
timeout: Flags.integer({ default: 300, description: 'DNS propagation wait timeout in seconds.' }),
|
|
66
|
+
ttl: Flags.integer({ default: 300, description: 'DNS record TTL in seconds.' }),
|
|
67
|
+
type: Flags.string({
|
|
68
|
+
description: 'Record type. Inferred from the target when omitted.',
|
|
69
|
+
options: ['A', 'AAAA', 'CNAME'],
|
|
70
|
+
}),
|
|
71
|
+
wait: Flags.boolean({ allowNo: true, default: true, description: 'Wait for public DNS propagation.' }),
|
|
72
|
+
};
|
|
73
|
+
async run() {
|
|
74
|
+
const { args, flags } = await this.parse(DnsPoint);
|
|
75
|
+
const out = createOutput({ json: flags.json });
|
|
76
|
+
let spinner;
|
|
77
|
+
try {
|
|
78
|
+
if (!flags['dry-run']) {
|
|
79
|
+
out.intro('Doomain');
|
|
80
|
+
spinner = out.spinner();
|
|
81
|
+
spinner.start(`Pointing ${args.domain}`);
|
|
82
|
+
}
|
|
83
|
+
const result = await pointDomain({
|
|
84
|
+
account: flags.account,
|
|
85
|
+
confirmDnsOverride: out.json
|
|
86
|
+
? undefined
|
|
87
|
+
: async (warning) => {
|
|
88
|
+
spinner?.stop('Existing DNS target found');
|
|
89
|
+
p.note(conflictNote(warning), 'DNS already points elsewhere');
|
|
90
|
+
const confirmed = await p.confirm({ message: `Override DNS for ${warning.domain}?`, initialValue: false });
|
|
91
|
+
if (confirmed === true)
|
|
92
|
+
spinner?.start(`Pointing ${warning.domain}`);
|
|
93
|
+
return confirmed === true;
|
|
94
|
+
},
|
|
95
|
+
domain: args.domain,
|
|
96
|
+
dryRun: flags['dry-run'],
|
|
97
|
+
force: flags.force,
|
|
98
|
+
progress: out.json ? undefined : (message) => spinner?.message(message),
|
|
99
|
+
provider: flags.provider,
|
|
100
|
+
recordType: flags.type,
|
|
101
|
+
target: flags.target,
|
|
102
|
+
timeoutSeconds: flags.timeout,
|
|
103
|
+
ttl: flags.ttl,
|
|
104
|
+
wait: flags.wait,
|
|
105
|
+
});
|
|
106
|
+
if (flags['dry-run']) {
|
|
107
|
+
out.result(result);
|
|
108
|
+
if (!out.json)
|
|
109
|
+
p.note(preview(result), 'Dry run');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const messages = successMessages(result, flags.wait);
|
|
113
|
+
spinner?.stop(messages.spinner);
|
|
114
|
+
out.result(result);
|
|
115
|
+
out.outro(messages.outro);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
spinner?.error('DNS update failed');
|
|
119
|
+
outputError(out.json, error, 'DNS_POINT_FAILED');
|
|
120
|
+
this.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -33,7 +33,9 @@ export default class DomainsList extends Command {
|
|
|
33
33
|
const accounts = account
|
|
34
34
|
? [{ account, isDefaultAccount: isDefaultProviderAccount(account), providerId: definition.id }]
|
|
35
35
|
: listConfiguredProviderAccounts(config, definition);
|
|
36
|
-
const selectedAccounts = accounts.length > 0
|
|
36
|
+
const selectedAccounts = accounts.length > 0
|
|
37
|
+
? accounts
|
|
38
|
+
: [{ account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: definition.id }];
|
|
37
39
|
const results = [];
|
|
38
40
|
for (const selectedAccount of selectedAccounts) {
|
|
39
41
|
const provider = await createProvider(definition.id, { account: selectedAccount.account });
|
|
@@ -47,14 +49,18 @@ export default class DomainsList extends Command {
|
|
|
47
49
|
records,
|
|
48
50
|
zone,
|
|
49
51
|
});
|
|
50
|
-
const accountLabel = selectedAccount.isDefaultAccount
|
|
52
|
+
const accountLabel = selectedAccount.isDefaultAccount
|
|
53
|
+
? provider.id
|
|
54
|
+
: `${provider.id}/${selectedAccount.account}`;
|
|
51
55
|
out.info(`${zone.name} (${records.length} records) via ${accountLabel}`);
|
|
52
56
|
for (const record of records)
|
|
53
57
|
out.info(` ${record.type} ${record.name} -> ${record.value}`);
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
out.result({
|
|
57
|
-
...(selectedAccounts.length === 1
|
|
61
|
+
...(selectedAccounts.length === 1
|
|
62
|
+
? { account: selectedAccounts[0].account, isDefaultAccount: selectedAccounts[0].isDefaultAccount }
|
|
63
|
+
: {}),
|
|
58
64
|
provider: definition.id,
|
|
59
65
|
zones: results,
|
|
60
66
|
});
|
package/dist/commands/link.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Args, Command, Flags } from '@oclif/core';
|
|
2
1
|
import * as p from '@clack/prompts';
|
|
2
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
3
3
|
import { accountFlag, apexFlag, domainFlag, jsonFlag, projectFlag, providerFlag, subdomainFlag } from '../lib/flags.js';
|
|
4
4
|
import { linkDomain } from '../lib/link-domain.js';
|
|
5
5
|
import { createOutput, outputError } from '../lib/output.js';
|
|
@@ -56,13 +56,19 @@ export default class Link extends Command {
|
|
|
56
56
|
apex: apexFlag,
|
|
57
57
|
domain: domainFlag,
|
|
58
58
|
'dry-run': Flags.boolean({ description: 'Preview changes without writing to Vercel or DNS.' }),
|
|
59
|
-
force: Flags.boolean({
|
|
59
|
+
force: Flags.boolean({
|
|
60
|
+
description: 'Move existing Vercel project domains and overwrite conflicting DNS records.',
|
|
61
|
+
}),
|
|
60
62
|
json: jsonFlag,
|
|
61
63
|
project: projectFlag,
|
|
62
64
|
provider: providerFlag,
|
|
63
65
|
subdomain: subdomainFlag,
|
|
64
66
|
timeout: Flags.integer({ default: 300, description: 'Wait timeout in seconds.' }),
|
|
65
|
-
wait: Flags.boolean({
|
|
67
|
+
wait: Flags.boolean({
|
|
68
|
+
allowNo: true,
|
|
69
|
+
default: true,
|
|
70
|
+
description: 'Wait for DNS propagation and Vercel verification.',
|
|
71
|
+
}),
|
|
66
72
|
};
|
|
67
73
|
async run() {
|
|
68
74
|
const { args, flags } = await this.parse(Link);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Args, Command, Flags } from '@oclif/core';
|
|
2
1
|
import * as p from '@clack/prompts';
|
|
2
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
3
3
|
import { getConfigPath, loadConfig, maskSecret, updateConfig } from '../../lib/config.js';
|
|
4
4
|
import { accountFlag, jsonFlag } from '../../lib/flags.js';
|
|
5
5
|
import { createOutput, outputError } from '../../lib/output.js';
|
|
@@ -68,7 +68,11 @@ function validateProviderAccount(value) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
async function promptProviderAccount() {
|
|
71
|
-
const value = await p.text({
|
|
71
|
+
const value = await p.text({
|
|
72
|
+
message: 'Profile name',
|
|
73
|
+
placeholder: DEFAULT_PROVIDER_ACCOUNT,
|
|
74
|
+
validate: validateProviderAccount,
|
|
75
|
+
});
|
|
72
76
|
if (p.isCancel(value)) {
|
|
73
77
|
p.cancel('Cancelled');
|
|
74
78
|
return null;
|
|
@@ -136,7 +140,11 @@ export default class ProvidersConnect extends Command {
|
|
|
136
140
|
const definition = args.provider ? getProviderDefinition(args.provider) : await promptProvider();
|
|
137
141
|
if (!definition)
|
|
138
142
|
return;
|
|
139
|
-
const account = flags.account
|
|
143
|
+
const account = flags.account
|
|
144
|
+
? normalizeProviderAccount(flags.account)
|
|
145
|
+
: out.json
|
|
146
|
+
? DEFAULT_PROVIDER_ACCOUNT
|
|
147
|
+
: await promptProviderAccount();
|
|
140
148
|
if (!account)
|
|
141
149
|
return;
|
|
142
150
|
const isDefaultAccount = isDefaultProviderAccount(account);
|
|
@@ -152,7 +160,10 @@ export default class ProvidersConnect extends Command {
|
|
|
152
160
|
if (!out.json)
|
|
153
161
|
showSetupGuide(definition, detectedPublicIp);
|
|
154
162
|
for (const credential of definition.credentials) {
|
|
155
|
-
const value = process.env[credential.env] ||
|
|
163
|
+
const value = process.env[credential.env] ||
|
|
164
|
+
passedCredentials[credential.key] ||
|
|
165
|
+
legacyFlagValue(flags, credential) ||
|
|
166
|
+
undefined;
|
|
156
167
|
if (value) {
|
|
157
168
|
credentials[credential.key] = value;
|
|
158
169
|
continue;
|
|
@@ -38,7 +38,9 @@ export default class ProvidersDisconnect extends Command {
|
|
|
38
38
|
else if (provider) {
|
|
39
39
|
const nextProvider = { ...provider };
|
|
40
40
|
if (isDefaultProviderAccount(account)) {
|
|
41
|
-
removed =
|
|
41
|
+
removed =
|
|
42
|
+
nextProvider.credentials !== undefined ||
|
|
43
|
+
(definition.id === 'spaceship' && ('apiKey' in nextProvider || 'apiSecret' in nextProvider));
|
|
42
44
|
delete nextProvider.credentials;
|
|
43
45
|
if (definition.id === 'spaceship') {
|
|
44
46
|
delete nextProvider.apiKey;
|
|
@@ -52,7 +54,9 @@ export default class ProvidersDisconnect extends Command {
|
|
|
52
54
|
delete accounts[account];
|
|
53
55
|
nextProvider.accounts = Object.keys(accounts).length > 0 ? accounts : undefined;
|
|
54
56
|
}
|
|
55
|
-
if (nextProvider.credentials ||
|
|
57
|
+
if (nextProvider.credentials ||
|
|
58
|
+
nextProvider.settings ||
|
|
59
|
+
(nextProvider.accounts && Object.keys(nextProvider.accounts).length > 0)) {
|
|
56
60
|
providers[definition.id] = nextProvider;
|
|
57
61
|
}
|
|
58
62
|
else {
|
|
@@ -69,10 +73,18 @@ export default class ProvidersDisconnect extends Command {
|
|
|
69
73
|
};
|
|
70
74
|
});
|
|
71
75
|
const overrides = envOverrides(definition);
|
|
72
|
-
out.result({
|
|
76
|
+
out.result({
|
|
77
|
+
account,
|
|
78
|
+
configPath: getConfigPath(),
|
|
79
|
+
environmentOverrides: overrides,
|
|
80
|
+
provider: definition.id,
|
|
81
|
+
removed,
|
|
82
|
+
});
|
|
73
83
|
if (overrides.length > 0)
|
|
74
84
|
out.warn(`${definition.displayName} environment credentials are still set: ${overrides.join(', ')}.`);
|
|
75
|
-
out.success(removed
|
|
85
|
+
out.success(removed
|
|
86
|
+
? `${definition.displayName} credentials removed from ${getConfigPath()}.`
|
|
87
|
+
: `${definition.displayName} was not connected.`);
|
|
76
88
|
}
|
|
77
89
|
catch (error) {
|
|
78
90
|
outputError(out.json, error, 'PROVIDER_NOT_FOUND');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import { jsonFlag } from '../../lib/flags.js';
|
|
3
|
-
import { listProviderDefinitions } from '../../lib/providers/registry.js';
|
|
4
3
|
import { createOutput } from '../../lib/output.js';
|
|
4
|
+
import { listProviderDefinitions } from '../../lib/providers/registry.js';
|
|
5
5
|
export default class ProvidersList extends Command {
|
|
6
6
|
static description = 'List supported DNS providers.';
|
|
7
7
|
static flags = {
|
package/dist/commands/wizard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
1
|
import * as p from '@clack/prompts';
|
|
2
|
+
import { Command } from '@oclif/core';
|
|
3
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';
|
|
@@ -8,8 +8,8 @@ import { detectLocalVercelProject } from '../lib/local-vercel.js';
|
|
|
8
8
|
import { createOutput, outputError } from '../lib/output.js';
|
|
9
9
|
import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, providerAccountHasCredentials, withProviderAccountCredentials, } from '../lib/providers/core/config.js';
|
|
10
10
|
import { createProvider, getProviderDefinition, listProviderDefinitions } from '../lib/providers/registry.js';
|
|
11
|
-
import { listGlobalVercelTokens } from '../lib/vercel-auth.js';
|
|
12
11
|
import { createVercelClient } from '../lib/vercel.js';
|
|
12
|
+
import { listGlobalVercelTokens } from '../lib/vercel-auth.js';
|
|
13
13
|
const PERSONAL_ACCOUNT = '__personal__';
|
|
14
14
|
const NEW_TOKEN = '__new_token__';
|
|
15
15
|
function cancelIfNeeded(value) {
|
|
@@ -22,7 +22,11 @@ function cancelIfNeeded(value) {
|
|
|
22
22
|
async function promptRequired(message, opts = {}) {
|
|
23
23
|
const value = opts.password
|
|
24
24
|
? await p.password({ message })
|
|
25
|
-
: await p.text({
|
|
25
|
+
: await p.text({
|
|
26
|
+
message,
|
|
27
|
+
placeholder: opts.placeholder,
|
|
28
|
+
validate: (input) => (input?.trim() ? undefined : 'Required'),
|
|
29
|
+
});
|
|
26
30
|
const resolved = cancelIfNeeded(value);
|
|
27
31
|
return typeof resolved === 'string' ? resolved.trim() : null;
|
|
28
32
|
}
|
|
@@ -36,7 +40,11 @@ function validateProviderAccount(value) {
|
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
async function promptProviderAccount() {
|
|
39
|
-
const value = await p.text({
|
|
43
|
+
const value = await p.text({
|
|
44
|
+
message: 'Profile name',
|
|
45
|
+
placeholder: DEFAULT_PROVIDER_ACCOUNT,
|
|
46
|
+
validate: validateProviderAccount,
|
|
47
|
+
});
|
|
40
48
|
const resolved = cancelIfNeeded(value);
|
|
41
49
|
return typeof resolved === 'string' ? normalizeProviderAccount(resolved) : null;
|
|
42
50
|
}
|
|
@@ -145,7 +153,11 @@ async function promptVercelToken(globalTokens) {
|
|
|
145
153
|
const selected = await p.select({
|
|
146
154
|
message: 'Vercel token',
|
|
147
155
|
options: [
|
|
148
|
-
...globalTokens.map((token, index) => ({
|
|
156
|
+
...globalTokens.map((token, index) => ({
|
|
157
|
+
label: globalTokenLabel(token),
|
|
158
|
+
value: String(index),
|
|
159
|
+
hint: maskSecret(token.token),
|
|
160
|
+
})),
|
|
149
161
|
{ label: 'Enter a new token', value: NEW_TOKEN },
|
|
150
162
|
],
|
|
151
163
|
});
|
|
@@ -256,7 +268,9 @@ export default class Wizard extends Command {
|
|
|
256
268
|
}
|
|
257
269
|
}
|
|
258
270
|
const selectedTeam = teams.find((team) => team.id === vercelTeamId);
|
|
259
|
-
const teamDisplay = vercelTeamId
|
|
271
|
+
const teamDisplay = vercelTeamId
|
|
272
|
+
? teamLabel(selectedTeam ?? { id: vercelTeamId, name: null, role: null, slug: vercelTeamId })
|
|
273
|
+
: 'Personal account';
|
|
260
274
|
p.log.success(`Vercel account ready: ${teamDisplay}`);
|
|
261
275
|
const projectSpinner = p.spinner();
|
|
262
276
|
activeSpinner = projectSpinner;
|
|
@@ -268,7 +282,9 @@ export default class Wizard extends Command {
|
|
|
268
282
|
throw new DoomainError('PROJECT_NOT_FOUND', `No Vercel projects found in ${teamDisplay}.`);
|
|
269
283
|
}
|
|
270
284
|
const localProjectMatchesTeam = localProject && (vercelTeamId ? localProject.orgId === vercelTeamId : !localProject.orgId);
|
|
271
|
-
const initialProject = localProjectMatchesTeam && projects.some((item) => item.id === localProject.projectId)
|
|
285
|
+
const initialProject = localProjectMatchesTeam && projects.some((item) => item.id === localProject.projectId)
|
|
286
|
+
? localProject.projectId
|
|
287
|
+
: undefined;
|
|
272
288
|
const selected = await p.autocomplete({
|
|
273
289
|
message: 'Select Vercel project',
|
|
274
290
|
placeholder: 'Type to filter projects...',
|
|
@@ -303,7 +319,11 @@ export default class Wizard extends Command {
|
|
|
303
319
|
if (!overwrite)
|
|
304
320
|
return;
|
|
305
321
|
}
|
|
306
|
-
const providerAccount = {
|
|
322
|
+
const providerAccount = {
|
|
323
|
+
account,
|
|
324
|
+
isDefaultAccount: isDefaultProviderAccount(account),
|
|
325
|
+
providerId: selectedDefinition.id,
|
|
326
|
+
};
|
|
307
327
|
const credentials = await promptProviderCredentials(selectedDefinition);
|
|
308
328
|
if (!credentials)
|
|
309
329
|
return;
|
|
@@ -334,7 +354,9 @@ export default class Wizard extends Command {
|
|
|
334
354
|
domainOptions.push(...(await listProviderDomainOptions(definition, account)));
|
|
335
355
|
}
|
|
336
356
|
catch (error) {
|
|
337
|
-
const label = account.isDefaultAccount
|
|
357
|
+
const label = account.isDefaultAccount
|
|
358
|
+
? definition.displayName
|
|
359
|
+
: `${definition.displayName}/${account.account}`;
|
|
338
360
|
providerFailures.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
339
361
|
}
|
|
340
362
|
}
|
|
@@ -354,14 +376,18 @@ export default class Wizard extends Command {
|
|
|
354
376
|
p.log.success(`DNS ready: ${providerNames} (${domainOptions.length} domains)`);
|
|
355
377
|
let selectedDomain = domainOptions[0];
|
|
356
378
|
if (domainOptions.length > 1) {
|
|
357
|
-
const initialValue = domainOptions.find((option) => option.providerId === defaultProvider && option.domain === defaultDomain)
|
|
358
|
-
domainOptions.find((option) => option.domain === defaultDomain)?.id;
|
|
379
|
+
const initialValue = domainOptions.find((option) => option.providerId === defaultProvider && option.domain === defaultDomain)
|
|
380
|
+
?.id ?? domainOptions.find((option) => option.domain === defaultDomain)?.id;
|
|
359
381
|
const selectedDomainId = await p.autocomplete({
|
|
360
382
|
message: 'Select domain',
|
|
361
383
|
placeholder: 'Type to filter domains...',
|
|
362
384
|
maxItems: 10,
|
|
363
385
|
initialValue,
|
|
364
|
-
options: domainOptions.map((option) => ({
|
|
386
|
+
options: domainOptions.map((option) => ({
|
|
387
|
+
label: option.domain,
|
|
388
|
+
value: option.id,
|
|
389
|
+
hint: providerAccountLabel(option),
|
|
390
|
+
})),
|
|
365
391
|
});
|
|
366
392
|
const selectedId = cancelIfNeeded(selectedDomainId);
|
|
367
393
|
if (selectedId === null)
|
|
@@ -392,8 +418,18 @@ export default class Wizard extends Command {
|
|
|
392
418
|
return;
|
|
393
419
|
}
|
|
394
420
|
const fullDomain = apex ? domain : `${subdomain}.${domain}`;
|
|
395
|
-
const preview = await createLinkPlan({
|
|
396
|
-
|
|
421
|
+
const preview = await createLinkPlan({
|
|
422
|
+
account: selectedDomain.account,
|
|
423
|
+
provider: selectedDomain.providerId,
|
|
424
|
+
domain,
|
|
425
|
+
subdomain,
|
|
426
|
+
apex,
|
|
427
|
+
project,
|
|
428
|
+
});
|
|
429
|
+
p.note([
|
|
430
|
+
`Vercel: add ${preview.domain} to ${projectDisplay}`,
|
|
431
|
+
...preview.records.map((record) => recordPreview(record, providerAccountLabel(selectedDomain))),
|
|
432
|
+
].join('\n'), 'Preview');
|
|
397
433
|
const confirmed = await p.confirm({
|
|
398
434
|
message: `Link ${fullDomain} via ${selectedDomain.providerName} to Vercel project ${projectDisplay}?`,
|
|
399
435
|
initialValue: true,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { run } from '@oclif/core';
|
|
2
|
-
export {
|
|
2
|
+
export { type DomainProviderResult, type FindDomainProviderInput, findDomainProvider, type ProviderSearchWarning, } from './lib/domain-provider.js';
|
package/dist/lib/clerk.js
CHANGED
|
@@ -17,7 +17,11 @@ export async function resolveClerkPlatformConfig(appId) {
|
|
|
17
17
|
return { appId: resolvedAppId, platformApiKey };
|
|
18
18
|
}
|
|
19
19
|
function apiErrorMessage(status, body) {
|
|
20
|
-
return body?.errors?.[0]?.long_message ??
|
|
20
|
+
return (body?.errors?.[0]?.long_message ??
|
|
21
|
+
body?.errors?.[0]?.message ??
|
|
22
|
+
body?.error?.message ??
|
|
23
|
+
body?.message ??
|
|
24
|
+
`Clerk API error (${status}).`);
|
|
21
25
|
}
|
|
22
26
|
function apiErrorCode(body) {
|
|
23
27
|
return body?.errors?.[0]?.code ?? body?.error?.code ?? body?.code;
|