doomain 0.1.18 → 0.1.19
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/dns/point.d.ts +21 -0
- package/dist/commands/dns/point.js +123 -0
- package/dist/lib/command-schema.js +34 -1
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/point-domain.d.ts +50 -0
- package/dist/lib/point-domain.js +212 -0
- package/dist/lib/providers/core/planner.js +21 -23
- package/dist/lib/providers/hostinger/index.js +24 -1
- 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.
|
|
@@ -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 { jsonFlag, providerFlag, accountFlag } from '../../lib/flags.js';
|
|
4
|
+
import { outputError, createOutput } 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
|
+
}
|
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
import { listProviderStatuses } from './providers/status.js';
|
|
2
2
|
export const commandSchemas = [
|
|
3
|
+
{
|
|
4
|
+
name: 'dns point',
|
|
5
|
+
description: 'Point a DNS name at an IP address or canonical hostname.',
|
|
6
|
+
examples: [
|
|
7
|
+
'doomain dns point app.example.com --target 203.0.113.10 --json',
|
|
8
|
+
'doomain dns point app.example.com --target origin.example.net --dry-run --json',
|
|
9
|
+
'doomain dns point example.com --target 203.0.113.10 --provider spaceship --account work --force --json',
|
|
10
|
+
],
|
|
11
|
+
agentHint: 'Use this for non-Vercel infrastructure such as a VPS or load balancer. Run without --dry-run unless the user explicitly requests a preview; conflicts fail safely unless --force is explicit.',
|
|
12
|
+
agentInstructions: [
|
|
13
|
+
'When a user asks to point a domain at a VPS or hostname, run `doomain dns point <domain> --target <ip-or-hostname> --json`.',
|
|
14
|
+
'Pass --force only after the user has approved replacing an existing DNS target.',
|
|
15
|
+
],
|
|
16
|
+
agentQuickstart: {
|
|
17
|
+
doNotPreflight: true,
|
|
18
|
+
preferredFirstCommand: 'doomain dns point <domain> --target <ip-or-hostname> --json',
|
|
19
|
+
},
|
|
20
|
+
mutates: true,
|
|
21
|
+
safeForAgents: true,
|
|
22
|
+
flags: [
|
|
23
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
24
|
+
{ name: 'domain', type: 'string', description: 'Fully qualified apex or subdomain to point.', required: true },
|
|
25
|
+
{ name: 'target', type: 'string', description: 'IPv4, IPv6, or hostname target.', required: true },
|
|
26
|
+
{ name: 'type', type: 'string', description: 'A, AAAA, or CNAME. Inferred from the target when omitted.' },
|
|
27
|
+
{ name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
|
|
28
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias.' },
|
|
29
|
+
{ name: 'ttl', type: 'integer', description: 'DNS record TTL in seconds.', default: 300 },
|
|
30
|
+
{ name: 'dry-run', type: 'boolean', description: 'Preview without writing.' },
|
|
31
|
+
{ name: 'force', type: 'boolean', description: 'Overwrite conflicting DNS records.' },
|
|
32
|
+
{ name: 'wait', type: 'boolean', description: 'Wait for public DNS propagation. Use --no-wait to skip.', default: true },
|
|
33
|
+
{ name: 'timeout', type: 'integer', description: 'DNS propagation wait timeout in seconds.', default: 300 },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
3
36
|
{
|
|
4
37
|
name: 'link',
|
|
5
38
|
description: 'Link a Vercel project to a domain and create DNS records.',
|
|
@@ -266,7 +299,7 @@ async function configuredProviders() {
|
|
|
266
299
|
}));
|
|
267
300
|
}
|
|
268
301
|
function withProviderConnections(schema, providers) {
|
|
269
|
-
if (schema.name !== 'link' && schema.name !== 'clerk domains add')
|
|
302
|
+
if (schema.name !== 'link' && schema.name !== 'clerk domains add' && schema.name !== 'dns point')
|
|
270
303
|
return schema;
|
|
271
304
|
return { ...schema, configuredProviders: providers };
|
|
272
305
|
}
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_PROVIDER_DISCOVERY_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
|
|
1
|
+
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_POINT_FAILED' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_PROVIDER_DISCOVERY_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
|
|
2
2
|
export declare class DoomainError extends Error {
|
|
3
3
|
readonly code: DoomainErrorCode;
|
|
4
4
|
readonly details?: unknown;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ResolvedDnsTarget } from './domain-provider.js';
|
|
2
|
+
import { type DnsOverrideWarning } from './link-domain.js';
|
|
3
|
+
import type { DnsProvider, DnsRecordInput } from './providers/types.js';
|
|
4
|
+
export type PointRecordType = 'A' | 'AAAA' | 'CNAME';
|
|
5
|
+
export interface PointDomainInput {
|
|
6
|
+
account?: string;
|
|
7
|
+
domain: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
force?: boolean;
|
|
10
|
+
provider?: string;
|
|
11
|
+
recordType?: PointRecordType;
|
|
12
|
+
target: string;
|
|
13
|
+
timeoutSeconds?: number;
|
|
14
|
+
ttl?: number;
|
|
15
|
+
wait?: boolean;
|
|
16
|
+
confirmDnsOverride?: (warning: DnsOverrideWarning) => Promise<boolean>;
|
|
17
|
+
progress?: (message: string) => void;
|
|
18
|
+
}
|
|
19
|
+
export interface PointDomainResult {
|
|
20
|
+
account: string;
|
|
21
|
+
accountInferred: boolean;
|
|
22
|
+
domain: string;
|
|
23
|
+
dryRun: boolean;
|
|
24
|
+
isDefaultAccount: boolean;
|
|
25
|
+
provider: string;
|
|
26
|
+
providerInferred: boolean;
|
|
27
|
+
propagated: boolean;
|
|
28
|
+
record: DnsRecordInput;
|
|
29
|
+
skipped: DnsRecordInput[];
|
|
30
|
+
updated: boolean;
|
|
31
|
+
zoneDomain: string;
|
|
32
|
+
}
|
|
33
|
+
interface PointDomainDependencies {
|
|
34
|
+
createProvider: (provider: string, opts: {
|
|
35
|
+
account?: string;
|
|
36
|
+
}) => Promise<DnsProvider>;
|
|
37
|
+
resolve4?: (hostname: string) => Promise<string[]>;
|
|
38
|
+
resolve6?: (hostname: string) => Promise<string[]>;
|
|
39
|
+
resolveCname?: (hostname: string) => Promise<string[]>;
|
|
40
|
+
resolveTarget: (input: Pick<PointDomainInput, 'account' | 'domain' | 'provider'>) => Promise<ResolvedDnsTarget>;
|
|
41
|
+
}
|
|
42
|
+
export declare function createPointRecord(input: {
|
|
43
|
+
provider: string;
|
|
44
|
+
recordName: string;
|
|
45
|
+
recordType?: PointRecordType;
|
|
46
|
+
target: string;
|
|
47
|
+
ttl?: number;
|
|
48
|
+
}): DnsRecordInput;
|
|
49
|
+
export declare function pointDomain(input: PointDomainInput, dependencies?: PointDomainDependencies): Promise<PointDomainResult>;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { resolve4, resolve6, resolveCname } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { resolveProviderTarget } from './domain-provider.js';
|
|
4
|
+
import { DoomainError } from './errors.js';
|
|
5
|
+
import { withProviderRecordOptions } from './link-domain.js';
|
|
6
|
+
import { createProvider } from './providers/registry.js';
|
|
7
|
+
import { normalizeDomain } from './validate.js';
|
|
8
|
+
const defaultDependencies = {
|
|
9
|
+
createProvider,
|
|
10
|
+
resolveTarget: resolveProviderTarget,
|
|
11
|
+
};
|
|
12
|
+
function cleanTarget(value) {
|
|
13
|
+
return value.trim().replace(/\.$/, '');
|
|
14
|
+
}
|
|
15
|
+
function inferredRecordType(target) {
|
|
16
|
+
const version = isIP(target);
|
|
17
|
+
if (version === 4)
|
|
18
|
+
return 'A';
|
|
19
|
+
if (version === 6)
|
|
20
|
+
return 'AAAA';
|
|
21
|
+
return 'CNAME';
|
|
22
|
+
}
|
|
23
|
+
function validateTarget(recordType, target) {
|
|
24
|
+
const version = isIP(target);
|
|
25
|
+
if (recordType === 'A' && version !== 4)
|
|
26
|
+
throw new DoomainError('INVALID_INPUT', 'A records require an IPv4 target.');
|
|
27
|
+
if (recordType === 'AAAA' && version !== 6)
|
|
28
|
+
throw new DoomainError('INVALID_INPUT', 'AAAA records require an IPv6 target.');
|
|
29
|
+
if (recordType === 'CNAME' && version !== 0)
|
|
30
|
+
throw new DoomainError('INVALID_INPUT', 'CNAME records require a hostname target.');
|
|
31
|
+
}
|
|
32
|
+
export function createPointRecord(input) {
|
|
33
|
+
const target = cleanTarget(input.target);
|
|
34
|
+
if (!target)
|
|
35
|
+
throw new DoomainError('MISSING_ARGUMENT', 'A DNS target is required.');
|
|
36
|
+
const recordType = input.recordType ?? inferredRecordType(target);
|
|
37
|
+
validateTarget(recordType, target);
|
|
38
|
+
return withProviderRecordOptions(input.provider, {
|
|
39
|
+
name: input.recordName,
|
|
40
|
+
ttl: input.ttl ?? 300,
|
|
41
|
+
type: recordType,
|
|
42
|
+
value: recordType === 'CNAME' ? normalizeDomain(target) : target,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function recordFqdn(record, zoneDomain) {
|
|
46
|
+
return record.name === '@' ? zoneDomain : `${record.name}.${zoneDomain}`;
|
|
47
|
+
}
|
|
48
|
+
function cleanDnsValue(value) {
|
|
49
|
+
return value.toLowerCase().replace(/\.$/, '');
|
|
50
|
+
}
|
|
51
|
+
function normalizeIpv6(value) {
|
|
52
|
+
return new URL(`http://[${value}]`).hostname.slice(1, -1);
|
|
53
|
+
}
|
|
54
|
+
async function isPropagated(record, zoneDomain, dependencies) {
|
|
55
|
+
const fqdn = recordFqdn(record, zoneDomain);
|
|
56
|
+
try {
|
|
57
|
+
if (record.type === 'A')
|
|
58
|
+
return (await (dependencies.resolve4 ?? resolve4)(fqdn)).includes(record.value);
|
|
59
|
+
if (record.type === 'AAAA') {
|
|
60
|
+
const expected = normalizeIpv6(record.value);
|
|
61
|
+
return (await (dependencies.resolve6 ?? resolve6)(fqdn)).some((value) => normalizeIpv6(value) === expected);
|
|
62
|
+
}
|
|
63
|
+
if (record.type === 'CNAME') {
|
|
64
|
+
return (await (dependencies.resolveCname ?? resolveCname)(fqdn))
|
|
65
|
+
.map(cleanDnsValue)
|
|
66
|
+
.includes(cleanDnsValue(record.value));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
async function waitForPropagation(record, zoneDomain, timeoutSeconds, dependencies) {
|
|
75
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
76
|
+
while (true) {
|
|
77
|
+
if (await isPropagated(record, zoneDomain, dependencies))
|
|
78
|
+
return true;
|
|
79
|
+
const remaining = deadline - Date.now();
|
|
80
|
+
if (remaining <= 0)
|
|
81
|
+
return false;
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(5000, remaining)));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function conflictWarning(resolved, provider, record, conflicts) {
|
|
86
|
+
return {
|
|
87
|
+
account: resolved.account,
|
|
88
|
+
conflicts,
|
|
89
|
+
desired: [record],
|
|
90
|
+
domain: resolved.target.fullDomain,
|
|
91
|
+
provider: resolved.provider,
|
|
92
|
+
providerName: provider.name,
|
|
93
|
+
recordName: resolved.target.recordName,
|
|
94
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function dnsTargetConflictError(warning) {
|
|
98
|
+
const account = warning.account === 'default' ? '' : ` --account ${warning.account}`;
|
|
99
|
+
const target = warning.desired[0]?.value ?? '<ip-or-hostname>';
|
|
100
|
+
return new DoomainError('DNS_TARGET_CONFLICT', `${warning.domain} already has DNS records that point somewhere else. Re-run with --force to overwrite them.`, {
|
|
101
|
+
...warning,
|
|
102
|
+
recovery: 'Re-run with --force to overwrite conflicting DNS records, or confirm the DNS override in interactive mode.',
|
|
103
|
+
suggestedCommands: [
|
|
104
|
+
`doomain dns point ${warning.domain} --target ${target} --provider ${warning.provider}${account} --force --json`,
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function providerResolutionError(error, input) {
|
|
109
|
+
if (error.code !== 'CONFIG_NOT_FOUND' && error.code !== 'PROVIDER_ZONE_NOT_FOUND')
|
|
110
|
+
return error;
|
|
111
|
+
const details = error.details && typeof error.details === 'object' ? error.details : {};
|
|
112
|
+
const provider = input.provider ? ` --provider ${input.provider}` : '';
|
|
113
|
+
const account = input.account ? ` --account ${input.account}` : '';
|
|
114
|
+
const retry = `doomain dns point ${input.domain} --target ${input.target}${provider}${account} --json`;
|
|
115
|
+
return new DoomainError(error.code, error.message, {
|
|
116
|
+
...details,
|
|
117
|
+
recovery: `Connect or repair the DNS provider account that owns this domain, then retry \`${retry}\`.`,
|
|
118
|
+
suggestedCommands: ['doomain providers connect', retry],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function validateTtl(provider, ttl) {
|
|
122
|
+
if (ttl === undefined)
|
|
123
|
+
return;
|
|
124
|
+
const { maxTtl, minTtl } = provider.capabilities;
|
|
125
|
+
if (!Number.isInteger(ttl) || ttl <= 0) {
|
|
126
|
+
throw new DoomainError('INVALID_INPUT', 'DNS record TTL must be a positive integer.');
|
|
127
|
+
}
|
|
128
|
+
if (minTtl !== undefined && maxTtl !== undefined && (ttl < minTtl || ttl > maxTtl)) {
|
|
129
|
+
throw new DoomainError('INVALID_INPUT', `${provider.name} requires a TTL between ${minTtl} and ${maxTtl} seconds.`);
|
|
130
|
+
}
|
|
131
|
+
if (minTtl !== undefined && ttl < minTtl) {
|
|
132
|
+
throw new DoomainError('INVALID_INPUT', `${provider.name} requires a TTL of at least ${minTtl} seconds.`);
|
|
133
|
+
}
|
|
134
|
+
if (maxTtl !== undefined && ttl > maxTtl) {
|
|
135
|
+
throw new DoomainError('INVALID_INPUT', `${provider.name} requires a TTL no greater than ${maxTtl} seconds.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export async function pointDomain(input, dependencies = defaultDependencies) {
|
|
139
|
+
let resolved;
|
|
140
|
+
try {
|
|
141
|
+
resolved = await dependencies.resolveTarget(input);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof DoomainError)
|
|
145
|
+
throw providerResolutionError(error, input);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
const record = createPointRecord({
|
|
149
|
+
provider: resolved.provider,
|
|
150
|
+
recordName: resolved.target.recordName,
|
|
151
|
+
recordType: input.recordType,
|
|
152
|
+
target: input.target,
|
|
153
|
+
ttl: input.ttl,
|
|
154
|
+
});
|
|
155
|
+
const provider = await dependencies.createProvider(resolved.provider, { account: resolved.account });
|
|
156
|
+
validateTtl(provider, record.ttl);
|
|
157
|
+
if (!provider.capabilities.recordTypes.includes(record.type)) {
|
|
158
|
+
throw new DoomainError('PROVIDER_UNSUPPORTED_RECORD', `${provider.name} does not support ${record.type} records.`);
|
|
159
|
+
}
|
|
160
|
+
if (resolved.target.isApex && record.type === 'CNAME' && !provider.capabilities.supportsApexCname) {
|
|
161
|
+
throw new DoomainError('PROVIDER_UNSUPPORTED_RECORD', `${provider.name} does not support CNAME records at the zone apex. Use an A or AAAA target instead.`);
|
|
162
|
+
}
|
|
163
|
+
if (input.dryRun) {
|
|
164
|
+
return {
|
|
165
|
+
account: resolved.account,
|
|
166
|
+
accountInferred: resolved.accountInferred,
|
|
167
|
+
domain: resolved.target.fullDomain,
|
|
168
|
+
dryRun: true,
|
|
169
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
170
|
+
provider: resolved.provider,
|
|
171
|
+
providerInferred: resolved.providerInferred,
|
|
172
|
+
propagated: false,
|
|
173
|
+
record,
|
|
174
|
+
skipped: [],
|
|
175
|
+
updated: false,
|
|
176
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const zone = await provider.getZone(resolved.target.zoneDomain);
|
|
180
|
+
if (!zone)
|
|
181
|
+
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`);
|
|
182
|
+
input.progress?.(`Planning DNS change in ${provider.name}`);
|
|
183
|
+
let force = Boolean(input.force);
|
|
184
|
+
let plan = await provider.planChanges(zone, [record], { force });
|
|
185
|
+
if (!force && plan.conflicts.length > 0) {
|
|
186
|
+
const warning = conflictWarning(resolved, provider, record, plan.conflicts);
|
|
187
|
+
force = (await input.confirmDnsOverride?.(warning)) === true;
|
|
188
|
+
if (!force)
|
|
189
|
+
throw dnsTargetConflictError(warning);
|
|
190
|
+
plan = await provider.planChanges(zone, [record], { force: true });
|
|
191
|
+
}
|
|
192
|
+
input.progress?.(`Pointing ${resolved.target.fullDomain} to ${record.value}`);
|
|
193
|
+
const result = await provider.applyChanges(zone, plan, { force });
|
|
194
|
+
const shouldWait = input.wait ?? true;
|
|
195
|
+
const propagated = shouldWait
|
|
196
|
+
? await waitForPropagation(record, resolved.target.zoneDomain, input.timeoutSeconds ?? 300, dependencies)
|
|
197
|
+
: false;
|
|
198
|
+
return {
|
|
199
|
+
account: resolved.account,
|
|
200
|
+
accountInferred: resolved.accountInferred,
|
|
201
|
+
domain: resolved.target.fullDomain,
|
|
202
|
+
dryRun: false,
|
|
203
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
204
|
+
provider: resolved.provider,
|
|
205
|
+
providerInferred: resolved.providerInferred,
|
|
206
|
+
propagated,
|
|
207
|
+
record,
|
|
208
|
+
skipped: result.skipped,
|
|
209
|
+
updated: result.applied.length > 0,
|
|
210
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -5,6 +5,10 @@ function cleanDnsValue(value) {
|
|
|
5
5
|
function sameRecord(a, b) {
|
|
6
6
|
if (b.proxied !== undefined && a.proxied !== b.proxied)
|
|
7
7
|
return false;
|
|
8
|
+
if (b.priority !== undefined && a.priority !== b.priority)
|
|
9
|
+
return false;
|
|
10
|
+
if (b.ttl !== undefined && a.ttl !== b.ttl)
|
|
11
|
+
return false;
|
|
8
12
|
return a.type === b.type && a.name === b.name && cleanDnsValue(a.value) === cleanDnsValue(b.value);
|
|
9
13
|
}
|
|
10
14
|
function sameDnsValue(a, b) {
|
|
@@ -13,6 +17,11 @@ function sameDnsValue(a, b) {
|
|
|
13
17
|
function sameSlot(a, b) {
|
|
14
18
|
return a.type === b.type && a.name === b.name;
|
|
15
19
|
}
|
|
20
|
+
function recordOptionsDiffer(a, b) {
|
|
21
|
+
return ((b.proxied !== undefined && a.proxied !== b.proxied) ||
|
|
22
|
+
(b.priority !== undefined && a.priority !== b.priority) ||
|
|
23
|
+
(b.ttl !== undefined && a.ttl !== b.ttl));
|
|
24
|
+
}
|
|
16
25
|
function cnameSlotConflict(a, b) {
|
|
17
26
|
return a.name === b.name && (a.type === 'CNAME' || b.type === 'CNAME');
|
|
18
27
|
}
|
|
@@ -21,38 +30,27 @@ export function planDnsChanges(input) {
|
|
|
21
30
|
const conflicts = [];
|
|
22
31
|
for (const record of input.desired) {
|
|
23
32
|
const exact = input.existing.find((existing) => sameRecord(existing, record));
|
|
24
|
-
if (exact) {
|
|
25
|
-
changes.push({ action: 'skip', existing: exact, reason: 'already_exists', record });
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
33
|
const sameValue = input.existing.find((existing) => sameDnsValue(existing, record));
|
|
29
|
-
if (
|
|
30
|
-
changes.push({ action: '
|
|
34
|
+
if (record.type === 'TXT') {
|
|
35
|
+
changes.push(exact ? { action: 'skip', existing: exact, reason: 'already_exists', record } : { action: 'create', record });
|
|
31
36
|
continue;
|
|
32
37
|
}
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
const sameTyped = input.existing.filter((existing) => sameSlot(existing, record) && !sameDnsValue(existing, record));
|
|
39
|
+
const cnameConflicts = input.existing.filter((existing) => cnameSlotConflict(existing, record) && !sameSlot(existing, record));
|
|
40
|
+
if (!input.force && (sameTyped.length > 0 || cnameConflicts.length > 0)) {
|
|
41
|
+
conflicts.push(...sameTyped.map((existing) => ({ existing, reason: 'same_type_record_exists', record })), ...cnameConflicts.map((existing) => ({ existing, reason: 'cname_slot_conflict', record })));
|
|
35
42
|
continue;
|
|
36
43
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (input.force)
|
|
40
|
-
changes.push({ action: 'update', existing: sameTyped, record });
|
|
41
|
-
else
|
|
42
|
-
conflicts.push({ existing: sameTyped, reason: 'same_type_record_exists', record });
|
|
44
|
+
if (exact) {
|
|
45
|
+
changes.push(...sameTyped.map((existing) => ({ action: 'delete', existing, reason: 'same_type_record_exists' })), ...cnameConflicts.map((existing) => ({ action: 'delete', existing, reason: 'cname_slot_conflict' })), { action: 'skip', existing: exact, reason: 'already_exists', record });
|
|
43
46
|
continue;
|
|
44
47
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (input.force) {
|
|
48
|
-
changes.push({ action: 'delete', existing: cnameConflict, reason: 'cname_slot_conflict' }, { action: 'create', record });
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
conflicts.push({ existing: cnameConflict, reason: 'cname_slot_conflict', record });
|
|
52
|
-
}
|
|
48
|
+
if (sameValue && recordOptionsDiffer(sameValue, record)) {
|
|
49
|
+
changes.push(...sameTyped.map((existing) => ({ action: 'delete', existing, reason: 'same_type_record_exists' })), ...cnameConflicts.map((existing) => ({ action: 'delete', existing, reason: 'cname_slot_conflict' })), { action: 'update', existing: sameValue, record });
|
|
53
50
|
continue;
|
|
54
51
|
}
|
|
55
|
-
|
|
52
|
+
const [replace, ...remainingSameTyped] = sameTyped;
|
|
53
|
+
changes.push(...(replace ? [{ action: 'update', existing: replace, record }] : []), ...remainingSameTyped.map((existing) => ({ action: 'delete', existing, reason: 'same_type_record_exists' })), ...cnameConflicts.map((existing) => ({ action: 'delete', existing, reason: 'cname_slot_conflict' })), ...(replace ? [] : [{ action: 'create', record }]));
|
|
56
54
|
}
|
|
57
55
|
return { changes, conflicts, desired: input.desired, existing: input.existing, zone: input.zone };
|
|
58
56
|
}
|
|
@@ -67,6 +67,28 @@ function toHostingerRecord(record) {
|
|
|
67
67
|
function deleteFilter(record) {
|
|
68
68
|
return { name: record.name, type: record.type };
|
|
69
69
|
}
|
|
70
|
+
function sameRecordSet(record, desired) {
|
|
71
|
+
return record.name === desired.name && record.type === desired.type;
|
|
72
|
+
}
|
|
73
|
+
function collapseRecordSetWrites(plan) {
|
|
74
|
+
let changes = plan.changes;
|
|
75
|
+
for (const desired of plan.desired) {
|
|
76
|
+
const existing = plan.existing.filter((record) => sameRecordSet(record, desired));
|
|
77
|
+
if (existing.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
const rewritesRecordSet = changes.some((change) => (change.action === 'delete' && sameRecordSet(change.existing, desired)) ||
|
|
80
|
+
(change.action === 'update' && sameRecordSet(change.record, desired)));
|
|
81
|
+
if (!rewritesRecordSet)
|
|
82
|
+
continue;
|
|
83
|
+
changes = changes.filter((change) => {
|
|
84
|
+
if (change.action === 'delete')
|
|
85
|
+
return !sameRecordSet(change.existing, desired);
|
|
86
|
+
return !sameRecordSet(change.record, desired);
|
|
87
|
+
});
|
|
88
|
+
changes.push({ action: 'update', existing: existing[0], record: desired });
|
|
89
|
+
}
|
|
90
|
+
return { ...plan, changes };
|
|
91
|
+
}
|
|
70
92
|
export class HostingerProvider {
|
|
71
93
|
capabilities = capabilities;
|
|
72
94
|
id = 'hostinger';
|
|
@@ -107,7 +129,8 @@ export class HostingerProvider {
|
|
|
107
129
|
return records.flatMap((record) => toDnsRecords(record, zone));
|
|
108
130
|
}
|
|
109
131
|
async planChanges(zone, desired, opts = {}) {
|
|
110
|
-
|
|
132
|
+
const plan = planDnsChanges({ desired, existing: await this.listRecords(zone), force: opts.force, providerId: this.id, zone });
|
|
133
|
+
return collapseRecordSetWrites(plan);
|
|
111
134
|
}
|
|
112
135
|
async applyChanges(zone, plan) {
|
|
113
136
|
assertNoConflicts(this.id, plan);
|
package/oclif.manifest.json
CHANGED
|
@@ -312,6 +312,113 @@
|
|
|
312
312
|
"vercel.js"
|
|
313
313
|
]
|
|
314
314
|
},
|
|
315
|
+
"dns:point": {
|
|
316
|
+
"aliases": [],
|
|
317
|
+
"args": {
|
|
318
|
+
"domain": {
|
|
319
|
+
"description": "Fully qualified domain to point.",
|
|
320
|
+
"name": "domain",
|
|
321
|
+
"required": true
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
"description": "Point a DNS name at an IP address or canonical hostname.",
|
|
325
|
+
"examples": [
|
|
326
|
+
"<%= config.bin %> <%= command.id %> app.example.com --target 203.0.113.10 --json",
|
|
327
|
+
"<%= config.bin %> <%= command.id %> app.example.com --target origin.example.net --dry-run --json",
|
|
328
|
+
"<%= config.bin %> <%= command.id %> example.com --target 203.0.113.10 --provider spaceship --account work --force --json"
|
|
329
|
+
],
|
|
330
|
+
"flags": {
|
|
331
|
+
"account": {
|
|
332
|
+
"description": "DNS provider profile/account alias. Defaults to the provider default account.",
|
|
333
|
+
"name": "account",
|
|
334
|
+
"hasDynamicHelp": false,
|
|
335
|
+
"multiple": false,
|
|
336
|
+
"type": "option"
|
|
337
|
+
},
|
|
338
|
+
"dry-run": {
|
|
339
|
+
"description": "Preview the DNS record without writing it.",
|
|
340
|
+
"name": "dry-run",
|
|
341
|
+
"allowNo": false,
|
|
342
|
+
"type": "boolean"
|
|
343
|
+
},
|
|
344
|
+
"force": {
|
|
345
|
+
"description": "Overwrite conflicting DNS records.",
|
|
346
|
+
"name": "force",
|
|
347
|
+
"allowNo": false,
|
|
348
|
+
"type": "boolean"
|
|
349
|
+
},
|
|
350
|
+
"json": {
|
|
351
|
+
"description": "Output a single JSON object and never prompt.",
|
|
352
|
+
"name": "json",
|
|
353
|
+
"allowNo": false,
|
|
354
|
+
"type": "boolean"
|
|
355
|
+
},
|
|
356
|
+
"provider": {
|
|
357
|
+
"description": "DNS provider id. Inferred from the target domain when omitted.",
|
|
358
|
+
"name": "provider",
|
|
359
|
+
"hasDynamicHelp": false,
|
|
360
|
+
"multiple": false,
|
|
361
|
+
"type": "option"
|
|
362
|
+
},
|
|
363
|
+
"target": {
|
|
364
|
+
"description": "IPv4, IPv6, or hostname target.",
|
|
365
|
+
"name": "target",
|
|
366
|
+
"required": true,
|
|
367
|
+
"hasDynamicHelp": false,
|
|
368
|
+
"multiple": false,
|
|
369
|
+
"type": "option"
|
|
370
|
+
},
|
|
371
|
+
"timeout": {
|
|
372
|
+
"description": "DNS propagation wait timeout in seconds.",
|
|
373
|
+
"name": "timeout",
|
|
374
|
+
"default": 300,
|
|
375
|
+
"hasDynamicHelp": false,
|
|
376
|
+
"multiple": false,
|
|
377
|
+
"type": "option"
|
|
378
|
+
},
|
|
379
|
+
"ttl": {
|
|
380
|
+
"description": "DNS record TTL in seconds.",
|
|
381
|
+
"name": "ttl",
|
|
382
|
+
"default": 300,
|
|
383
|
+
"hasDynamicHelp": false,
|
|
384
|
+
"multiple": false,
|
|
385
|
+
"type": "option"
|
|
386
|
+
},
|
|
387
|
+
"type": {
|
|
388
|
+
"description": "Record type. Inferred from the target when omitted.",
|
|
389
|
+
"name": "type",
|
|
390
|
+
"hasDynamicHelp": false,
|
|
391
|
+
"multiple": false,
|
|
392
|
+
"options": [
|
|
393
|
+
"A",
|
|
394
|
+
"AAAA",
|
|
395
|
+
"CNAME"
|
|
396
|
+
],
|
|
397
|
+
"type": "option"
|
|
398
|
+
},
|
|
399
|
+
"wait": {
|
|
400
|
+
"description": "Wait for public DNS propagation.",
|
|
401
|
+
"name": "wait",
|
|
402
|
+
"allowNo": true,
|
|
403
|
+
"type": "boolean"
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
"hasDynamicHelp": false,
|
|
407
|
+
"hiddenAliases": [],
|
|
408
|
+
"id": "dns:point",
|
|
409
|
+
"pluginAlias": "doomain",
|
|
410
|
+
"pluginName": "doomain",
|
|
411
|
+
"pluginType": "core",
|
|
412
|
+
"strict": true,
|
|
413
|
+
"enableJsonFlag": false,
|
|
414
|
+
"isESM": true,
|
|
415
|
+
"relativePath": [
|
|
416
|
+
"dist",
|
|
417
|
+
"commands",
|
|
418
|
+
"dns",
|
|
419
|
+
"point.js"
|
|
420
|
+
]
|
|
421
|
+
},
|
|
315
422
|
"domains:find": {
|
|
316
423
|
"aliases": [],
|
|
317
424
|
"args": {
|
|
@@ -894,5 +1001,5 @@
|
|
|
894
1001
|
]
|
|
895
1002
|
}
|
|
896
1003
|
},
|
|
897
|
-
"version": "0.1.
|
|
1004
|
+
"version": "0.1.19"
|
|
898
1005
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doomain",
|
|
3
|
-
"description": "Configure
|
|
4
|
-
"version": "0.1.
|
|
3
|
+
"description": "Configure production domains and DNS records in seconds",
|
|
4
|
+
"version": "0.1.19",
|
|
5
5
|
"author": "Crafter Station",
|
|
6
6
|
"packageManager": "bun@1.3.13",
|
|
7
7
|
"bin": {
|
|
@@ -64,6 +64,9 @@
|
|
|
64
64
|
"domains": {
|
|
65
65
|
"description": "Inspect DNS domains"
|
|
66
66
|
},
|
|
67
|
+
"dns": {
|
|
68
|
+
"description": "Manage DNS records"
|
|
69
|
+
},
|
|
67
70
|
"projects": {
|
|
68
71
|
"description": "Inspect Vercel projects"
|
|
69
72
|
},
|