doomain 0.1.21 → 0.1.23
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 +59 -2
- package/bin/dev.js +3 -1
- package/bin/ensure-current-manifest.js +18 -0
- package/bin/run.js +4 -1
- package/dist/commands/dns/delete.d.ts +1 -0
- package/dist/commands/dns/delete.js +1 -0
- package/dist/commands/dns/diagnose.d.ts +16 -0
- package/dist/commands/dns/diagnose.js +49 -0
- package/dist/commands/dns/point.js +17 -0
- package/dist/commands/dns/remove.d.ts +18 -0
- package/dist/commands/dns/remove.js +77 -0
- package/dist/commands/providers/status.js +1 -2
- package/dist/commands/version.d.ts +5 -0
- package/dist/commands/version.js +8 -0
- package/dist/lib/command-schema.d.ts +1 -1
- package/dist/lib/command-schema.js +69 -1
- package/dist/lib/diagnose-dns.d.ts +53 -0
- package/dist/lib/diagnose-dns.js +156 -0
- package/dist/lib/dns-propagation.d.ts +45 -0
- package/dist/lib/dns-propagation.js +114 -0
- package/dist/lib/dns-reconciliation.d.ts +31 -0
- package/dist/lib/dns-reconciliation.js +53 -0
- package/dist/lib/dns-records.d.ts +18 -0
- package/dist/lib/dns-records.js +73 -0
- package/dist/lib/domain-provider.js +1 -1
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/point-domain.d.ts +6 -3
- package/dist/lib/point-domain.js +36 -70
- package/dist/lib/providers/hostinger/index.d.ts +1 -0
- package/dist/lib/providers/hostinger/index.js +46 -10
- package/dist/lib/providers/spaceship/index.js +16 -7
- package/dist/lib/providers/status.d.ts +3 -0
- package/dist/lib/providers/status.js +2 -0
- package/dist/lib/remove-domain.d.ts +38 -0
- package/dist/lib/remove-domain.js +125 -0
- package/oclif.manifest.json +264 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ Use the interactive wizard when working by hand. Use explicit commands with `--j
|
|
|
13
13
|
- Vercel project detection from `.vercel/project.json`.
|
|
14
14
|
- DNS provider inference by longest matching configured zone.
|
|
15
15
|
- Generic DNS pointing for VPS, load balancer, and canonical-hostname targets.
|
|
16
|
+
- Exact-match DNS removal with dry-run and post-delete verification.
|
|
17
|
+
- DNS diagnosis across provider, system/VPN, Cloudflare, and Google views.
|
|
16
18
|
- Dry-run plans before writing changes.
|
|
17
19
|
- Safety checks before replacing DNS records that point elsewhere.
|
|
18
20
|
- DNS propagation and Vercel verification wait loop.
|
|
@@ -257,10 +259,38 @@ doomain dns point app.example.com --target 203.0.113.10 --dry-run --json
|
|
|
257
259
|
|
|
258
260
|
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
261
|
|
|
260
|
-
|
|
262
|
+
Every real write is re-read from the provider. Success is returned only when `reconciled: true` and the non-TXT slot contains only the desired record. If an eventually consistent provider does not reach that postcondition, the command fails with `DNS_RECONCILIATION_INCOMPLETE` and includes the observed records; do not treat accepted API requests as a completed cutover.
|
|
263
|
+
|
|
264
|
+
By default, a successful write waits up to 300 seconds and compares the system resolver with Cloudflare and Google public DNS. Use `--no-wait` to skip resolver verification or `--timeout <seconds>` to change the limit. The JSON `propagation` object includes each resolver's answers, TTLs, expected target, elapsed time, status, and timeout reason. Node's resolver API does not expose CNAME TTLs, so those answers use `ttl: null` with `ttlUnavailableReason` instead of silently omitting the field.
|
|
265
|
+
|
|
266
|
+
- `propagated`: public and system DNS match.
|
|
267
|
+
- `local_or_vpn_cache_stale`: public DNS matches but the system/VPN resolver still serves cached data. The mutation is deployed, and `propagated` is `true`.
|
|
268
|
+
- `system_resolver_unavailable`: public DNS matches, but the system resolver query failed. Public propagation is complete, and the resolver error is preserved for diagnosis.
|
|
269
|
+
- `public_propagation_pending`: public DNS did not match before the timeout. The reconciled provider mutation succeeded, but `propagated` is `false`; this remains an exit-code-0 partial verification result.
|
|
270
|
+
- `not_checked`: resolver verification was skipped or this was a dry run.
|
|
261
271
|
|
|
262
272
|
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
273
|
|
|
274
|
+
## Removing And Diagnosing DNS Records
|
|
275
|
+
|
|
276
|
+
Removal defaults to an exact name/type/value match and verifies the record is absent after deletion:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
doomain dns remove app.example.com --type A --value 203.0.113.10 --dry-run --json
|
|
280
|
+
doomain dns remove app.example.com --type A --value 203.0.113.10 --json
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`dns delete` is an alias. If more than one record matches, non-interactive mode fails with `DNS_DELETE_AMBIGUOUS`. Pass `--all-matching` only after reviewing the dry-run plan; interactive mode asks before deleting multiple records.
|
|
284
|
+
|
|
285
|
+
Use the read-only diagnosis command to compare provider control-plane records with system/VPN and public DNS answers:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
doomain dns diagnose example.com --json
|
|
289
|
+
doomain dns diagnose app.example.com --type A --target 203.0.113.10 --json
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Diagnosis reports record conflicts, answer TTLs, resolver addresses, active interface names, scoped macOS resolver/interface metadata when available, and one of `provider_not_updated`, `public_propagation_pending`, `local_or_vpn_cache_stale`, `system_resolver_unavailable`, or `consistent`.
|
|
293
|
+
|
|
264
294
|
## Clerk Production Domains
|
|
265
295
|
|
|
266
296
|
`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.
|
|
@@ -408,7 +438,7 @@ The API checks configured provider accounts, tolerates failures from individual
|
|
|
408
438
|
|
|
409
439
|
## Command Reference
|
|
410
440
|
|
|
411
|
-
|
|
441
|
+
Use `doomain --help`, `doomain -h`, or `doomain help <command>` for oclif-generated help. Use `doomain --version`, `doomain -v`, or `doomain version` to print the installed version.
|
|
412
442
|
|
|
413
443
|
### `doomain`
|
|
414
444
|
|
|
@@ -476,6 +506,33 @@ Common flags:
|
|
|
476
506
|
- `--timeout <seconds>`: propagation wait timeout. Default is `300`.
|
|
477
507
|
- `--json`: output one JSON object.
|
|
478
508
|
|
|
509
|
+
### `doomain dns remove <domain>`
|
|
510
|
+
|
|
511
|
+
Removes exact DNS records and verifies their absence. `dns delete` is an alias.
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
doomain dns remove app.example.com --type A --value 203.0.113.10 --dry-run --json
|
|
515
|
+
doomain dns remove app.example.com --type A --value 203.0.113.10 --json
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Common flags:
|
|
519
|
+
|
|
520
|
+
- `--type <A|AAAA|CNAME|MX|TXT>`: required record type.
|
|
521
|
+
- `--value <value>`: expected exact value; required unless `--all-matching` is used.
|
|
522
|
+
- `--all-matching`: explicitly select every record matching the name and type.
|
|
523
|
+
- `--provider <id>`, `--account <alias>`: select a configured provider account.
|
|
524
|
+
- `--dry-run`: list selected records without deleting them.
|
|
525
|
+
- `--json`: output one JSON object and never prompt.
|
|
526
|
+
|
|
527
|
+
### `doomain dns diagnose <domain>`
|
|
528
|
+
|
|
529
|
+
Compares DNS provider records with the system resolver and independent public resolvers.
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
doomain dns diagnose example.com --json
|
|
533
|
+
doomain dns diagnose app.example.com --type A --target 203.0.113.10 --json
|
|
534
|
+
```
|
|
535
|
+
|
|
479
536
|
### `doomain auth logout vercel`
|
|
480
537
|
|
|
481
538
|
Removes saved Vercel credentials from the local config file.
|
package/bin/dev.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { removeStaleDevelopmentManifest } from './ensure-current-manifest.js'
|
|
4
4
|
|
|
5
|
+
await removeStaleDevelopmentManifest(import.meta.url)
|
|
6
|
+
const { execute } = await import('@oclif/core')
|
|
5
7
|
await execute({ development: true, dir: import.meta.url })
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFile, unlink } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
export async function removeStaleDevelopmentManifest(binUrl) {
|
|
6
|
+
const root = dirname(dirname(fileURLToPath(binUrl)))
|
|
7
|
+
const packagePath = join(root, 'package.json')
|
|
8
|
+
const manifestPath = join(root, 'oclif.manifest.json')
|
|
9
|
+
try {
|
|
10
|
+
const [packageJson, manifest] = await Promise.all([
|
|
11
|
+
readFile(packagePath, 'utf8').then(JSON.parse),
|
|
12
|
+
readFile(manifestPath, 'utf8').then(JSON.parse),
|
|
13
|
+
])
|
|
14
|
+
if (packageJson.version !== manifest.version) await unlink(manifestPath)
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error?.code !== 'ENOENT') throw error
|
|
17
|
+
}
|
|
18
|
+
}
|
package/bin/run.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { removeStaleDevelopmentManifest } from './ensure-current-manifest.js'
|
|
4
|
+
|
|
5
|
+
await removeStaleDevelopmentManifest(import.meta.url)
|
|
6
|
+
const { execute } = await import('@oclif/core')
|
|
4
7
|
|
|
5
8
|
const args = process.argv.slice(2)
|
|
6
9
|
const routedArgs = args.length === 0 || (args.length === 1 && args[0] === '--json') ? ['wizard', ...args] : args
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './remove.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './remove.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class DnsDiagnose 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
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
target: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { diagnoseDns } from '../../lib/diagnose-dns.js';
|
|
3
|
+
import { accountFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
|
|
4
|
+
import { createOutput, outputError } from '../../lib/output.js';
|
|
5
|
+
export default class DnsDiagnose extends Command {
|
|
6
|
+
static description = 'Compare DNS provider records with system and public resolver answers.';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> <%= command.id %> example.com --json',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> app.example.com --type A --target 203.0.113.10 --json',
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
domain: Args.string({ description: 'Fully qualified DNS name to diagnose.', required: true }),
|
|
13
|
+
};
|
|
14
|
+
static flags = {
|
|
15
|
+
account: accountFlag,
|
|
16
|
+
json: jsonFlag,
|
|
17
|
+
provider: providerFlag,
|
|
18
|
+
target: Flags.string({ description: 'Expected IP address or canonical hostname.' }),
|
|
19
|
+
type: Flags.string({ description: 'Record type to compare.', options: ['A', 'AAAA', 'CNAME'] }),
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
const { args, flags } = await this.parse(DnsDiagnose);
|
|
23
|
+
const out = createOutput({ json: flags.json });
|
|
24
|
+
const spinner = out.json ? undefined : out.spinner();
|
|
25
|
+
try {
|
|
26
|
+
spinner?.start(`Diagnosing ${args.domain}`);
|
|
27
|
+
const result = await diagnoseDns({
|
|
28
|
+
account: flags.account,
|
|
29
|
+
domain: args.domain,
|
|
30
|
+
provider: flags.provider,
|
|
31
|
+
recordType: flags.type,
|
|
32
|
+
target: flags.target,
|
|
33
|
+
});
|
|
34
|
+
spinner?.stop('DNS diagnosis complete');
|
|
35
|
+
out.result(result);
|
|
36
|
+
out.info(`Provider: ${result.provider}/${result.account} (${result.zoneDomain})`);
|
|
37
|
+
out.info(`Status: ${result.status}`);
|
|
38
|
+
for (const observation of result.observations) {
|
|
39
|
+
const answers = observation.answers.map((answer) => `${answer.value}${answer.ttl == null ? '' : ` (TTL ${answer.ttl}s)`}`);
|
|
40
|
+
out.info(`${observation.resolver}: ${answers.join(', ') || observation.error || 'no answer'}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
spinner?.error('DNS diagnosis failed');
|
|
45
|
+
outputError(out.json, error, 'DNS_DIAGNOSE_FAILED');
|
|
46
|
+
this.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import { Args, Command, Flags } from '@oclif/core';
|
|
3
|
+
import { normalizeDnsValue } from '../../lib/dns-records.js';
|
|
3
4
|
import { accountFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
|
|
4
5
|
import { createOutput, outputError } from '../../lib/output.js';
|
|
5
6
|
import { pointDomain } from '../../lib/point-domain.js';
|
|
@@ -22,6 +23,22 @@ function conflictNote(warning) {
|
|
|
22
23
|
].join('\n');
|
|
23
24
|
}
|
|
24
25
|
function successMessages(result, waited) {
|
|
26
|
+
if (result.propagation.status === 'system_resolver_unavailable') {
|
|
27
|
+
return {
|
|
28
|
+
outro: 'Public DNS is deployed, but the system resolver could not be queried. See resolver observations for details.',
|
|
29
|
+
spinner: 'Public DNS deployed; system resolver unavailable',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (result.propagation.status === 'local_or_vpn_cache_stale') {
|
|
33
|
+
const system = result.propagation.observations.find((observation) => observation.kind === 'system');
|
|
34
|
+
const remainingTtl = system?.answers
|
|
35
|
+
.filter((answer) => normalizeDnsValue(answer.value) !== normalizeDnsValue(result.propagation.expected))
|
|
36
|
+
.reduce((longest, answer) => (answer.ttl == null ? longest : Math.max(longest ?? 0, answer.ttl)), undefined);
|
|
37
|
+
return {
|
|
38
|
+
outro: `DNS is deployed; your active VPN/local resolver is serving a cached record${remainingTtl === undefined ? '' : ` for up to ${remainingTtl} more seconds`}.`,
|
|
39
|
+
spinner: 'DNS deployed; local resolver cache is stale',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
25
42
|
if (result.propagated) {
|
|
26
43
|
return {
|
|
27
44
|
outro: result.updated
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class DnsRemove 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
|
+
'all-matching': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
'dry-run': 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
|
+
type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
value: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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 { removeDomain } from '../../lib/remove-domain.js';
|
|
6
|
+
function recordLines(records) {
|
|
7
|
+
return records.map((record) => `- ${record.type} ${record.name} -> ${record.value}`).join('\n');
|
|
8
|
+
}
|
|
9
|
+
export default class DnsRemove extends Command {
|
|
10
|
+
static description = 'Safely remove exact DNS records and verify their absence.';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> <%= command.id %> app.example.com --type A --value 203.0.113.10 --dry-run --json',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> app.example.com --type A --all-matching --json',
|
|
14
|
+
];
|
|
15
|
+
static args = {
|
|
16
|
+
domain: Args.string({ description: 'Fully qualified DNS name whose record should be removed.', required: true }),
|
|
17
|
+
};
|
|
18
|
+
static flags = {
|
|
19
|
+
account: accountFlag,
|
|
20
|
+
'all-matching': Flags.boolean({
|
|
21
|
+
description: 'Delete every record matching the name and type (and value, if set).',
|
|
22
|
+
}),
|
|
23
|
+
'dry-run': Flags.boolean({ description: 'Preview the exact records without deleting them.' }),
|
|
24
|
+
json: jsonFlag,
|
|
25
|
+
provider: providerFlag,
|
|
26
|
+
type: Flags.string({
|
|
27
|
+
description: 'DNS record type to remove.',
|
|
28
|
+
options: ['A', 'AAAA', 'CNAME', 'MX', 'TXT'],
|
|
29
|
+
required: true,
|
|
30
|
+
}),
|
|
31
|
+
value: Flags.string({ description: 'Expected record value. Required unless --all-matching is passed.' }),
|
|
32
|
+
};
|
|
33
|
+
async run() {
|
|
34
|
+
const { args, flags } = await this.parse(DnsRemove);
|
|
35
|
+
const out = createOutput({ json: flags.json });
|
|
36
|
+
const spinner = flags['dry-run'] || out.json ? undefined : out.spinner();
|
|
37
|
+
try {
|
|
38
|
+
spinner?.start(`Inspecting ${args.domain}`);
|
|
39
|
+
const result = await removeDomain({
|
|
40
|
+
account: flags.account,
|
|
41
|
+
allMatching: flags['all-matching'],
|
|
42
|
+
confirmMultiple: out.json
|
|
43
|
+
? undefined
|
|
44
|
+
: async (records) => {
|
|
45
|
+
spinner?.stop('Multiple matching records found');
|
|
46
|
+
p.note(recordLines(records), 'Records selected for deletion');
|
|
47
|
+
const confirmed = await p.confirm({
|
|
48
|
+
initialValue: false,
|
|
49
|
+
message: `Delete all ${records.length} matching records?`,
|
|
50
|
+
});
|
|
51
|
+
if (confirmed === true)
|
|
52
|
+
spinner?.start(`Deleting records from ${args.domain}`);
|
|
53
|
+
return confirmed === true;
|
|
54
|
+
},
|
|
55
|
+
domain: args.domain,
|
|
56
|
+
dryRun: flags['dry-run'],
|
|
57
|
+
progress: out.json ? undefined : (message) => spinner?.message(message),
|
|
58
|
+
provider: flags.provider,
|
|
59
|
+
recordType: flags.type,
|
|
60
|
+
value: flags.value,
|
|
61
|
+
});
|
|
62
|
+
spinner?.stop(result.removed === 0 ? 'No matching records found' : 'DNS records removed');
|
|
63
|
+
out.result(result);
|
|
64
|
+
if (flags['dry-run'] && !out.json)
|
|
65
|
+
p.note(recordLines(result.matched) || 'No matching records.', 'Dry run');
|
|
66
|
+
else
|
|
67
|
+
out.outro(result.removed === 0
|
|
68
|
+
? `No matching DNS record exists for ${result.domain}.`
|
|
69
|
+
: `Removed ${result.removed} DNS record${result.removed === 1 ? '' : 's'} from ${result.domain}.`);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
spinner?.error('DNS removal failed');
|
|
73
|
+
outputError(out.json, error, 'DNS_REMOVE_FAILED');
|
|
74
|
+
this.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -25,8 +25,7 @@ export default class ProvidersStatus extends Command {
|
|
|
25
25
|
const providers = await listProviderStatuses({ verify: !flags['no-verify'] });
|
|
26
26
|
spinner?.stop('Checked DNS providers');
|
|
27
27
|
for (const provider of providers) {
|
|
28
|
-
|
|
29
|
-
out.info(`${provider.displayName} (${account}) - ${formatStatus(provider)}${provider.default ? ' [default]' : ''}`);
|
|
28
|
+
out.info(`${provider.displayName} (${provider.accountLabel}) - ${formatStatus(provider)}${provider.isPreferredProvider ? ' [preferred provider]' : ''}`);
|
|
30
29
|
}
|
|
31
30
|
out.result({ providers });
|
|
32
31
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProviderStatus } from './providers/status.js';
|
|
2
|
-
export type ProviderConnectionStatus = Pick<ProviderStatus, 'account' | 'configured' | 'default' | 'displayName' | 'docsUrl' | 'id' | 'isDefaultAccount'>;
|
|
2
|
+
export type ProviderConnectionStatus = Pick<ProviderStatus, 'account' | 'accountLabel' | 'configured' | 'default' | 'displayName' | 'docsUrl' | 'id' | 'isDefaultAccount' | 'isPreferredProvider'>;
|
|
3
3
|
export interface CommandSchema {
|
|
4
4
|
name: string;
|
|
5
5
|
description: string;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { listProviderStatuses } from './providers/status.js';
|
|
2
|
+
const dnsRemovalFlags = [
|
|
3
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
4
|
+
{ name: 'domain', type: 'string', description: 'Fully qualified DNS name.', required: true },
|
|
5
|
+
{ name: 'type', type: 'string', description: 'A, AAAA, CNAME, MX, or TXT.', required: true },
|
|
6
|
+
{ name: 'value', type: 'string', description: 'Expected exact value. Required unless --all-matching.' },
|
|
7
|
+
{ name: 'provider', type: 'string', description: 'DNS provider id. Inferred when omitted.' },
|
|
8
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias.' },
|
|
9
|
+
{ name: 'all-matching', type: 'boolean', description: 'Delete every matching record after explicit approval.' },
|
|
10
|
+
{ name: 'dry-run', type: 'boolean', description: 'Preview exact records without deleting.' },
|
|
11
|
+
];
|
|
2
12
|
export const commandSchemas = [
|
|
3
13
|
{
|
|
4
14
|
name: 'dns point',
|
|
@@ -12,6 +22,7 @@ export const commandSchemas = [
|
|
|
12
22
|
agentInstructions: [
|
|
13
23
|
'When a user asks to point a domain at a VPS or hostname, run `doomain dns point <domain> --target <ip-or-hostname> --json`.',
|
|
14
24
|
'Pass --force only after the user has approved replacing an existing DNS target.',
|
|
25
|
+
'A successful mutation always has reconciled=true. propagated=false with public_propagation_pending means the provider mutation succeeded but public verification timed out; system_resolver_unavailable means public DNS matches but the local resolver check failed. Inspect propagation.observations before retrying.',
|
|
15
26
|
],
|
|
16
27
|
agentQuickstart: {
|
|
17
28
|
doNotPreflight: true,
|
|
@@ -42,6 +53,49 @@ export const commandSchemas = [
|
|
|
42
53
|
{ name: 'timeout', type: 'integer', description: 'DNS propagation wait timeout in seconds.', default: 300 },
|
|
43
54
|
],
|
|
44
55
|
},
|
|
56
|
+
{
|
|
57
|
+
name: 'dns remove',
|
|
58
|
+
description: 'Safely remove exact DNS records and verify their absence.',
|
|
59
|
+
examples: [
|
|
60
|
+
'doomain dns remove app.example.com --type A --value 203.0.113.10 --dry-run --json',
|
|
61
|
+
'doomain dns remove app.example.com --type A --value 203.0.113.10 --json',
|
|
62
|
+
'doomain dns delete app.example.com --type A --all-matching --json',
|
|
63
|
+
],
|
|
64
|
+
agentHint: 'Default to exact name/type/value deletion. Preview with --dry-run when the expected value is unknown. Never pass --all-matching without explicit approval.',
|
|
65
|
+
mutates: true,
|
|
66
|
+
safeForAgents: true,
|
|
67
|
+
flags: dnsRemovalFlags,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'dns diagnose',
|
|
71
|
+
description: 'Compare provider control-plane records with system and public DNS resolvers.',
|
|
72
|
+
examples: [
|
|
73
|
+
'doomain dns diagnose example.com --json',
|
|
74
|
+
'doomain dns diagnose app.example.com --type A --target 203.0.113.10 --json',
|
|
75
|
+
],
|
|
76
|
+
agentHint: 'Use this read-only command to distinguish provider_not_updated, public_propagation_pending, and local_or_vpn_cache_stale. Resolver observations include answers and TTLs.',
|
|
77
|
+
safeForAgents: true,
|
|
78
|
+
flags: [
|
|
79
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
80
|
+
{ name: 'domain', type: 'string', description: 'Fully qualified DNS name.', required: true },
|
|
81
|
+
{ name: 'type', type: 'string', description: 'A, AAAA, or CNAME. Defaults to A.' },
|
|
82
|
+
{ name: 'target', type: 'string', description: 'Optional expected IP address or canonical hostname.' },
|
|
83
|
+
{ name: 'provider', type: 'string', description: 'DNS provider id. Inferred when omitted.' },
|
|
84
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias.' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'dns delete',
|
|
89
|
+
description: 'Alias for dns remove.',
|
|
90
|
+
examples: [
|
|
91
|
+
'doomain dns delete app.example.com --type A --value 203.0.113.10 --dry-run --json',
|
|
92
|
+
'doomain dns delete app.example.com --type A --all-matching --json',
|
|
93
|
+
],
|
|
94
|
+
agentHint: 'Alias for dns remove; use the same exact-match and explicit --all-matching safety rules.',
|
|
95
|
+
mutates: true,
|
|
96
|
+
safeForAgents: true,
|
|
97
|
+
flags: dnsRemovalFlags,
|
|
98
|
+
},
|
|
45
99
|
{
|
|
46
100
|
name: 'link',
|
|
47
101
|
description: 'Link a Vercel project to a domain and create DNS records.',
|
|
@@ -164,6 +218,13 @@ export const commandSchemas = [
|
|
|
164
218
|
safeForAgents: true,
|
|
165
219
|
flags: [{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' }],
|
|
166
220
|
},
|
|
221
|
+
{
|
|
222
|
+
name: 'version',
|
|
223
|
+
description: 'Display the installed doomain version.',
|
|
224
|
+
examples: ['doomain version'],
|
|
225
|
+
safeForAgents: true,
|
|
226
|
+
flags: [],
|
|
227
|
+
},
|
|
167
228
|
{
|
|
168
229
|
name: 'update',
|
|
169
230
|
description: 'Install the latest doomain version from npm without using the existing npm cache.',
|
|
@@ -391,16 +452,23 @@ export function getCommandSchema(name) {
|
|
|
391
452
|
async function configuredProviders() {
|
|
392
453
|
return (await listProviderStatuses({ verify: false })).map((provider) => ({
|
|
393
454
|
account: provider.account,
|
|
455
|
+
accountLabel: provider.accountLabel,
|
|
394
456
|
configured: provider.configured,
|
|
395
457
|
default: provider.default,
|
|
396
458
|
displayName: provider.displayName,
|
|
397
459
|
docsUrl: provider.docsUrl,
|
|
398
460
|
id: provider.id,
|
|
399
461
|
isDefaultAccount: provider.isDefaultAccount,
|
|
462
|
+
isPreferredProvider: provider.isPreferredProvider,
|
|
400
463
|
}));
|
|
401
464
|
}
|
|
402
465
|
function withProviderConnections(schema, providers) {
|
|
403
|
-
if (schema.name !== 'link' &&
|
|
466
|
+
if (schema.name !== 'link' &&
|
|
467
|
+
schema.name !== 'clerk domains add' &&
|
|
468
|
+
schema.name !== 'dns point' &&
|
|
469
|
+
schema.name !== 'dns remove' &&
|
|
470
|
+
schema.name !== 'dns delete' &&
|
|
471
|
+
schema.name !== 'dns diagnose')
|
|
404
472
|
return schema;
|
|
405
473
|
return { ...schema, configuredProviders: providers };
|
|
406
474
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type DnsResolverObservation } from './dns-propagation.js';
|
|
2
|
+
import { type ResolvedDnsTarget } from './domain-provider.js';
|
|
3
|
+
import type { DnsProvider, DnsRecord, DnsRecordInput, DnsRecordType } from './providers/types.js';
|
|
4
|
+
export type DnsDiagnosisStatus = 'consistent' | 'local_or_vpn_cache_stale' | 'provider_not_updated' | 'public_propagation_pending' | 'system_resolver_unavailable';
|
|
5
|
+
export interface DiagnoseDnsInput {
|
|
6
|
+
account?: string;
|
|
7
|
+
domain: string;
|
|
8
|
+
provider?: string;
|
|
9
|
+
recordType?: 'A' | 'AAAA' | 'CNAME';
|
|
10
|
+
target?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DnsRecordConflict {
|
|
13
|
+
reason: 'cname_slot_conflict' | 'multiple_values';
|
|
14
|
+
records: DnsRecord[];
|
|
15
|
+
type?: DnsRecordType;
|
|
16
|
+
}
|
|
17
|
+
export interface MacOsResolverMetadata {
|
|
18
|
+
flags?: string;
|
|
19
|
+
interfaceIndex?: number;
|
|
20
|
+
interfaceName?: string;
|
|
21
|
+
nameservers: string[];
|
|
22
|
+
resolver: number;
|
|
23
|
+
}
|
|
24
|
+
export interface DiagnoseDnsResult {
|
|
25
|
+
account: string;
|
|
26
|
+
accountInferred: boolean;
|
|
27
|
+
conflicts: DnsRecordConflict[];
|
|
28
|
+
domain: string;
|
|
29
|
+
expected?: string;
|
|
30
|
+
interfaces: string[];
|
|
31
|
+
isDefaultAccount: boolean;
|
|
32
|
+
macOsResolvers?: MacOsResolverMetadata[];
|
|
33
|
+
observations: DnsResolverObservation[];
|
|
34
|
+
platform: NodeJS.Platform;
|
|
35
|
+
provider: string;
|
|
36
|
+
providerInferred: boolean;
|
|
37
|
+
providerRecords: DnsRecord[];
|
|
38
|
+
recordType: 'A' | 'AAAA' | 'CNAME';
|
|
39
|
+
status: DnsDiagnosisStatus;
|
|
40
|
+
systemResolverServers: string[];
|
|
41
|
+
warnings: ResolvedDnsTarget['warnings'];
|
|
42
|
+
zoneDomain: string;
|
|
43
|
+
}
|
|
44
|
+
interface DiagnoseDnsDependencies {
|
|
45
|
+
createProvider: (provider: string, opts: {
|
|
46
|
+
account?: string;
|
|
47
|
+
}) => Promise<DnsProvider>;
|
|
48
|
+
observeDns: (fqdn: string, target: Pick<DnsRecordInput, 'type' | 'value'>, elapsedMs: number) => Promise<DnsResolverObservation[]>;
|
|
49
|
+
resolveTarget: (input: Pick<DiagnoseDnsInput, 'account' | 'domain' | 'provider'>) => Promise<ResolvedDnsTarget>;
|
|
50
|
+
macOsResolvers?: () => Promise<MacOsResolverMetadata[]>;
|
|
51
|
+
}
|
|
52
|
+
export declare function diagnoseDns(input: DiagnoseDnsInput, dependencies?: DiagnoseDnsDependencies): Promise<DiagnoseDnsResult>;
|
|
53
|
+
export {};
|