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.
Files changed (37) hide show
  1. package/README.md +59 -2
  2. package/bin/dev.js +3 -1
  3. package/bin/ensure-current-manifest.js +18 -0
  4. package/bin/run.js +4 -1
  5. package/dist/commands/dns/delete.d.ts +1 -0
  6. package/dist/commands/dns/delete.js +1 -0
  7. package/dist/commands/dns/diagnose.d.ts +16 -0
  8. package/dist/commands/dns/diagnose.js +49 -0
  9. package/dist/commands/dns/point.js +17 -0
  10. package/dist/commands/dns/remove.d.ts +18 -0
  11. package/dist/commands/dns/remove.js +77 -0
  12. package/dist/commands/providers/status.js +1 -2
  13. package/dist/commands/version.d.ts +5 -0
  14. package/dist/commands/version.js +8 -0
  15. package/dist/lib/command-schema.d.ts +1 -1
  16. package/dist/lib/command-schema.js +69 -1
  17. package/dist/lib/diagnose-dns.d.ts +53 -0
  18. package/dist/lib/diagnose-dns.js +156 -0
  19. package/dist/lib/dns-propagation.d.ts +45 -0
  20. package/dist/lib/dns-propagation.js +114 -0
  21. package/dist/lib/dns-reconciliation.d.ts +31 -0
  22. package/dist/lib/dns-reconciliation.js +53 -0
  23. package/dist/lib/dns-records.d.ts +18 -0
  24. package/dist/lib/dns-records.js +73 -0
  25. package/dist/lib/domain-provider.js +1 -1
  26. package/dist/lib/errors.d.ts +1 -1
  27. package/dist/lib/point-domain.d.ts +6 -3
  28. package/dist/lib/point-domain.js +36 -70
  29. package/dist/lib/providers/hostinger/index.d.ts +1 -0
  30. package/dist/lib/providers/hostinger/index.js +46 -10
  31. package/dist/lib/providers/spaceship/index.js +16 -7
  32. package/dist/lib/providers/status.d.ts +3 -0
  33. package/dist/lib/providers/status.js +2 -0
  34. package/dist/lib/remove-domain.d.ts +38 -0
  35. package/dist/lib/remove-domain.js +125 -0
  36. package/oclif.manifest.json +264 -1
  37. 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
- 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.
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
- Run `doomain help <command>` for oclif-generated help.
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 { execute } from '@oclif/core'
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 { execute } from '@oclif/core'
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
- const account = provider.isDefaultAccount ? provider.id : `${provider.id}/${provider.account}`;
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
  }
@@ -0,0 +1,5 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Version extends Command {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Version extends Command {
3
+ static description = 'Display the installed doomain version.';
4
+ async run() {
5
+ await this.parse(Version);
6
+ this.log(this.config.userAgent);
7
+ }
8
+ }
@@ -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' && schema.name !== 'clerk domains add' && schema.name !== 'dns point')
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 {};