doomain 0.1.18 → 0.1.20

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