doomain 0.1.19 → 0.1.21

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.
@@ -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);
@@ -1,7 +1,7 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
- import { jsonFlag, providerFlag, accountFlag } from '../../lib/flags.js';
4
- import { outputError, createOutput } from '../../lib/output.js';
3
+ import { accountFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
4
+ import { createOutput, outputError } from '../../lib/output.js';
5
5
  import { pointDomain } from '../../lib/point-domain.js';
6
6
  function preview(result) {
7
7
  return [
@@ -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 = {
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Update extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,28 @@
1
+ import { Command } from '@oclif/core';
2
+ import { jsonFlag } from '../lib/flags.js';
3
+ import { createOutput, outputError } from '../lib/output.js';
4
+ import { installLatestVersion } from '../lib/self-update.js';
5
+ export default class Update extends Command {
6
+ static description = 'Install the latest doomain version from npm without using the existing npm cache.';
7
+ static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --json'];
8
+ static flags = {
9
+ json: jsonFlag,
10
+ };
11
+ async run() {
12
+ const { flags } = await this.parse(Update);
13
+ const out = createOutput({ json: flags.json });
14
+ const spinner = out.spinner();
15
+ try {
16
+ spinner.start('Downloading the latest doomain version from npm');
17
+ const result = await installLatestVersion();
18
+ spinner.stop('Installed the latest doomain version');
19
+ out.result(result);
20
+ out.success('Installed the latest doomain version from npm.');
21
+ }
22
+ catch (error) {
23
+ spinner.error('Update failed');
24
+ outputError(out.json, error, 'SELF_UPDATE_FAILED');
25
+ this.exit(1);
26
+ }
27
+ }
28
+ }
@@ -0,0 +1 @@
1
+ export { default } from './update.js';
@@ -0,0 +1 @@
1
+ export { default } from './update.js';
@@ -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;
@@ -24,12 +24,21 @@ export const commandSchemas = [
24
24
  { name: 'domain', type: 'string', description: 'Fully qualified apex or subdomain to point.', required: true },
25
25
  { name: 'target', type: 'string', description: 'IPv4, IPv6, or hostname target.', required: true },
26
26
  { name: 'type', type: 'string', description: 'A, AAAA, or CNAME. Inferred from the target when omitted.' },
27
- { name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
27
+ {
28
+ name: 'provider',
29
+ type: 'string',
30
+ description: 'DNS provider id. Inferred from the target domain when omitted.',
31
+ },
28
32
  { name: 'account', type: 'string', description: 'DNS provider profile/account alias.' },
29
33
  { name: 'ttl', type: 'integer', description: 'DNS record TTL in seconds.', default: 300 },
30
34
  { name: 'dry-run', type: 'boolean', description: 'Preview without writing.' },
31
35
  { name: 'force', type: 'boolean', description: 'Overwrite conflicting DNS records.' },
32
- { name: 'wait', type: 'boolean', description: 'Wait for public DNS propagation. Use --no-wait to skip.', default: true },
36
+ {
37
+ name: 'wait',
38
+ type: 'boolean',
39
+ description: 'Wait for public DNS propagation. Use --no-wait to skip.',
40
+ default: true,
41
+ },
33
42
  { name: 'timeout', type: 'integer', description: 'DNS propagation wait timeout in seconds.', default: 300 },
34
43
  ],
35
44
  },
@@ -59,8 +68,16 @@ export const commandSchemas = [
59
68
  safeForAgents: true,
60
69
  flags: [
61
70
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
62
- { name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
63
- { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
71
+ {
72
+ name: 'provider',
73
+ type: 'string',
74
+ description: 'DNS provider id. Inferred from the target domain when omitted.',
75
+ },
76
+ {
77
+ name: 'account',
78
+ type: 'string',
79
+ description: 'DNS provider profile/account alias. Defaults to the provider default account.',
80
+ },
64
81
  {
65
82
  name: 'domain',
66
83
  type: 'string',
@@ -68,14 +85,27 @@ export const commandSchemas = [
68
85
  },
69
86
  { name: 'subdomain', type: 'string', description: 'Subdomain to add.' },
70
87
  { name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
71
- { name: 'project', type: 'string', description: 'Vercel project id/name. Optional when project inference succeeds.' },
72
- { name: 'dry-run', type: 'boolean', description: 'Preview changes without writing. Intended for human previews; agents should not use this unless explicitly asked.' },
88
+ {
89
+ name: 'project',
90
+ type: 'string',
91
+ description: 'Vercel project id/name. Optional when project inference succeeds.',
92
+ },
93
+ {
94
+ name: 'dry-run',
95
+ type: 'boolean',
96
+ description: 'Preview changes without writing. Intended for human previews; agents should not use this unless explicitly asked.',
97
+ },
73
98
  {
74
99
  name: 'force',
75
100
  type: 'boolean',
76
101
  description: 'Move existing Vercel project domains and overwrite conflicting DNS records. Interactive DNS override confirmation does not move Vercel aliases; pass --force for that.',
77
102
  },
78
- { name: 'wait', type: 'boolean', description: 'Wait for DNS and Vercel verification. Use --no-wait to skip waiting.', default: true },
103
+ {
104
+ name: 'wait',
105
+ type: 'boolean',
106
+ description: 'Wait for DNS and Vercel verification. Use --no-wait to skip waiting.',
107
+ default: true,
108
+ },
79
109
  { name: 'timeout', type: 'integer', description: 'Wait timeout in seconds.', default: 300 },
80
110
  ],
81
111
  },
@@ -97,12 +127,33 @@ export const commandSchemas = [
97
127
  safeForAgents: true,
98
128
  flags: [
99
129
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
100
- { name: 'app', type: 'string', description: 'Clerk application id. Defaults to CLERK_APPLICATION_ID or saved Clerk config.' },
101
- { name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
102
- { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
103
- { name: 'dry-run', type: 'boolean', description: 'Check application eligibility and DNS zone without creating production.' },
130
+ {
131
+ name: 'app',
132
+ type: 'string',
133
+ description: 'Clerk application id. Defaults to CLERK_APPLICATION_ID or saved Clerk config.',
134
+ },
135
+ {
136
+ name: 'provider',
137
+ type: 'string',
138
+ description: 'DNS provider id. Inferred from the target domain when omitted.',
139
+ },
140
+ {
141
+ name: 'account',
142
+ type: 'string',
143
+ description: 'DNS provider profile/account alias. Defaults to the provider default account.',
144
+ },
145
+ {
146
+ name: 'dry-run',
147
+ type: 'boolean',
148
+ description: 'Check application eligibility and DNS zone without creating production.',
149
+ },
104
150
  { name: 'force', type: 'boolean', description: 'Overwrite DNS records that conflict with Clerk requirements.' },
105
- { name: 'wait', type: 'boolean', description: 'Wait for Clerk DNS, SSL, and email DNS verification. Use --no-wait to skip waiting.', default: true },
151
+ {
152
+ name: 'wait',
153
+ type: 'boolean',
154
+ description: 'Wait for Clerk DNS, SSL, and email DNS verification. Use --no-wait to skip waiting.',
155
+ default: true,
156
+ },
106
157
  { name: 'timeout', type: 'integer', description: 'Wait timeout in seconds.', default: 300 },
107
158
  ],
108
159
  },
@@ -111,18 +162,28 @@ export const commandSchemas = [
111
162
  description: 'Print machine-readable command schemas for agents.',
112
163
  examples: ['doomain schema --json', 'doomain schema link --json'],
113
164
  safeForAgents: true,
114
- flags: [
115
- { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
116
- ],
165
+ flags: [{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' }],
166
+ },
167
+ {
168
+ name: 'update',
169
+ description: 'Install the latest doomain version from npm without using the existing npm cache.',
170
+ examples: ['doomain update', 'doomain update --json'],
171
+ mutates: true,
172
+ flags: [{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' }],
173
+ },
174
+ {
175
+ name: 'upgrade',
176
+ description: 'Alias for update.',
177
+ examples: ['doomain upgrade', 'doomain upgrade --json'],
178
+ mutates: true,
179
+ flags: [{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' }],
117
180
  },
118
181
  {
119
182
  name: 'providers list',
120
183
  description: 'List supported DNS providers.',
121
184
  examples: ['doomain providers list --json'],
122
185
  safeForAgents: true,
123
- flags: [
124
- { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
125
- ],
186
+ flags: [{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' }],
126
187
  },
127
188
  {
128
189
  name: 'providers connect',
@@ -137,7 +198,11 @@ export const commandSchemas = [
137
198
  ],
138
199
  flags: [
139
200
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
140
- { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
201
+ {
202
+ name: 'account',
203
+ type: 'string',
204
+ description: 'DNS provider profile/account alias. Defaults to the provider default account.',
205
+ },
141
206
  { name: 'credential', type: 'string', description: 'Provider credential as key=value. Can be repeated.' },
142
207
  { name: 'api-key', type: 'string', description: 'Spaceship API key.' },
143
208
  { name: 'api-secret', type: 'string', description: 'Spaceship API secret.' },
@@ -147,10 +212,18 @@ export const commandSchemas = [
147
212
  {
148
213
  name: 'providers add',
149
214
  description: 'Alias for providers connect.',
150
- examples: ['doomain providers add', 'doomain providers add namecheap', 'doomain providers add spaceship --account work'],
215
+ examples: [
216
+ 'doomain providers add',
217
+ 'doomain providers add namecheap',
218
+ 'doomain providers add spaceship --account work',
219
+ ],
151
220
  flags: [
152
221
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
153
- { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
222
+ {
223
+ name: 'account',
224
+ type: 'string',
225
+ description: 'DNS provider profile/account alias. Defaults to the provider default account.',
226
+ },
154
227
  { name: 'credential', type: 'string', description: 'Provider credential as key=value. Can be repeated.' },
155
228
  { name: 'no-verify', type: 'boolean', description: 'Save credentials without verifying them first.' },
156
229
  ],
@@ -175,7 +248,11 @@ export const commandSchemas = [
175
248
  ],
176
249
  flags: [
177
250
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
178
- { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Omit to remove all accounts for the provider.' },
251
+ {
252
+ name: 'account',
253
+ type: 'string',
254
+ description: 'DNS provider profile/account alias. Omit to remove all accounts for the provider.',
255
+ },
179
256
  ],
180
257
  },
181
258
  {
@@ -189,7 +266,11 @@ export const commandSchemas = [
189
266
  ],
190
267
  flags: [
191
268
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
192
- { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
269
+ {
270
+ name: 'account',
271
+ type: 'string',
272
+ description: 'DNS provider profile/account alias. Defaults to the provider default account.',
273
+ },
193
274
  ],
194
275
  },
195
276
  {
@@ -206,9 +287,7 @@ export const commandSchemas = [
206
287
  name: 'auth logout clerk',
207
288
  description: 'Remove saved Clerk credentials locally.',
208
289
  examples: ['doomain auth logout clerk --json'],
209
- flags: [
210
- { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
211
- ],
290
+ flags: [{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' }],
212
291
  },
213
292
  {
214
293
  name: 'auth vercel',
@@ -217,16 +296,18 @@ export const commandSchemas = [
217
296
  flags: [
218
297
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
219
298
  { name: 'token', type: 'string', description: 'Vercel API token.' },
220
- { name: 'team-id', type: 'string', description: 'Optional Vercel team id. Interactive mode can fetch and select it.' },
299
+ {
300
+ name: 'team-id',
301
+ type: 'string',
302
+ description: 'Optional Vercel team id. Interactive mode can fetch and select it.',
303
+ },
221
304
  ],
222
305
  },
223
306
  {
224
307
  name: 'auth logout vercel',
225
308
  description: 'Remove saved Vercel credentials locally.',
226
309
  examples: ['doomain auth logout vercel --json'],
227
- flags: [
228
- { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
229
- ],
310
+ flags: [{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' }],
230
311
  },
231
312
  {
232
313
  name: 'domains find',
@@ -252,8 +333,16 @@ export const commandSchemas = [
252
333
  safeForAgents: true,
253
334
  flags: [
254
335
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
255
- { name: 'provider', type: 'string', description: 'DNS provider id. Defaults to DOOMAIN_PROVIDER, configured default provider, then spaceship.' },
256
- { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Omit to list all configured accounts for the provider.' },
336
+ {
337
+ name: 'provider',
338
+ type: 'string',
339
+ description: 'DNS provider id. Defaults to DOOMAIN_PROVIDER, configured default provider, then spaceship.',
340
+ },
341
+ {
342
+ name: 'account',
343
+ type: 'string',
344
+ description: 'DNS provider profile/account alias. Omit to list all configured accounts for the provider.',
345
+ },
257
346
  { name: 'domain', type: 'string', description: 'Limit output to one DNS zone.' },
258
347
  ],
259
348
  },
@@ -270,15 +359,27 @@ export const commandSchemas = [
270
359
  {
271
360
  name: 'verify',
272
361
  description: 'Ask Vercel to verify a project domain.',
273
- examples: ['doomain verify --domain app.example.com --project my-app --json', 'doomain verify --domain example.com --apex --project my-app --json'],
362
+ examples: [
363
+ 'doomain verify --domain app.example.com --project my-app --json',
364
+ 'doomain verify --domain example.com --apex --project my-app --json',
365
+ ],
274
366
  mutates: true,
275
367
  safeForAgents: true,
276
368
  flags: [
277
369
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
278
- { name: 'domain', type: 'string', description: 'Target domain or base zone, for example app.example.com or example.com.', required: true },
370
+ {
371
+ name: 'domain',
372
+ type: 'string',
373
+ description: 'Target domain or base zone, for example app.example.com or example.com.',
374
+ required: true,
375
+ },
279
376
  { name: 'subdomain', type: 'string', description: 'Subdomain to verify.' },
280
377
  { name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
281
- { name: 'project', type: 'string', description: 'Vercel project id/name. Optional when local .vercel/project.json is available.' },
378
+ {
379
+ name: 'project',
380
+ type: 'string',
381
+ description: 'Vercel project id/name. Optional when local .vercel/project.json is available.',
382
+ },
282
383
  ],
283
384
  },
284
385
  ];
@@ -122,7 +122,9 @@ async function loadConfiguredProviderZones(providerId, accountInput, toleratePro
122
122
  const account = accountInput ? normalizeProviderAccount(accountInput) : undefined;
123
123
  if (providerId) {
124
124
  const definition = getProviderDefinition(providerId);
125
- const accounts = account ? [explicitAccountRef(definition.id, account)] : listConfiguredProviderAccounts(config, definition);
125
+ const accounts = account
126
+ ? [explicitAccountRef(definition.id, account)]
127
+ : listConfiguredProviderAccounts(config, definition);
126
128
  const selectedAccounts = accounts.length > 0 ? accounts : [defaultAccountRef(definition.id)];
127
129
  const tolerateAccountErrors = tolerateProviderAccountErrors && !account && selectedAccounts.length > 1;
128
130
  const results = await Promise.all(selectedAccounts.map((ref) => tolerateAccountErrors ? loadProviderZonesSafely(definition, ref) : loadProviderZones(definition, ref)));
@@ -174,12 +176,17 @@ export async function resolveProviderTarget(input, options = {}) {
174
176
  domain: requested.fullDomain,
175
177
  recovery: 'Retry with --provider <id> --account <alias> only if another configured provider account owns this zone. Otherwise connect the DNS provider account that owns this domain.',
176
178
  searchedZones: zones.searched,
177
- suggestedCommands: [`doomain link ${requested.fullDomain} --provider <id> --account <alias> --json`, 'doomain providers connect'],
179
+ suggestedCommands: [
180
+ `doomain link ${requested.fullDomain} --provider <id> --account <alias> --json`,
181
+ 'doomain providers connect',
182
+ ],
178
183
  });
179
184
  }
180
185
  const bestLength = matches[0].zone.name.length;
181
186
  const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
182
- const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider && item.account === candidate.account && item.zone.name === candidate.zone.name) === index);
187
+ const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider &&
188
+ item.account === candidate.account &&
189
+ item.zone.name === candidate.zone.name) === index);
183
190
  if (uniqueBestMatches.length > 1) {
184
191
  throw new DoomainError('PROVIDER_ZONE_AMBIGUOUS', `Multiple DNS provider accounts have a matching DNS zone for ${requested.fullDomain}. Pass --provider and --account to choose one.`, { candidates: candidateDetails(uniqueBestMatches), domain: requested.fullDomain });
185
192
  }
@@ -1,4 +1,4 @@
1
- export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_POINT_FAILED' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_PROVIDER_DISCOVERY_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
1
+ export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_POINT_FAILED' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_PROVIDER_DISCOVERY_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'SELF_UPDATE_FAILED' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
2
2
  export declare class DoomainError extends Error {
3
3
  readonly code: DoomainErrorCode;
4
4
  readonly details?: unknown;
package/dist/lib/flags.js CHANGED
@@ -1,8 +1,14 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  export const jsonFlag = Flags.boolean({ description: 'Output a single JSON object and never prompt.' });
3
- export const providerFlag = Flags.string({ description: 'DNS provider id. Inferred from the target domain when omitted.' });
4
- export const accountFlag = Flags.string({ description: 'DNS provider profile/account alias. Defaults to the provider default account.' });
5
- export const domainFlag = Flags.string({ description: 'Target domain or base zone, for example app.example.com or example.com.' });
3
+ export const providerFlag = Flags.string({
4
+ description: 'DNS provider id. Inferred from the target domain when omitted.',
5
+ });
6
+ export const accountFlag = Flags.string({
7
+ description: 'DNS provider profile/account alias. Defaults to the provider default account.',
8
+ });
9
+ export const domainFlag = Flags.string({
10
+ description: 'Target domain or base zone, for example app.example.com or example.com.',
11
+ });
6
12
  export const subdomainFlag = Flags.string({ description: 'Subdomain to add, for example app for app.example.com.' });
7
13
  export const apexFlag = Flags.boolean({ description: 'Use the root/apex domain instead of a subdomain.' });
8
14
  export const projectFlag = Flags.string({
@@ -6,7 +6,7 @@ import { resolveProviderTarget } from './domain-provider.js';
6
6
  import { DoomainError } from './errors.js';
7
7
  import { detectLocalVercelProject } from './local-vercel.js';
8
8
  import { createProvider } from './providers/registry.js';
9
- import { createVercelClient, resolveVercelConfig, VERCEL_APEX_A_RECORD, VERCEL_CNAME_RECORD } from './vercel.js';
9
+ import { createVercelClient, resolveVercelConfig, VERCEL_APEX_A_RECORD, VERCEL_CNAME_RECORD, } from './vercel.js';
10
10
  function cleanDnsValue(value) {
11
11
  return value.toLowerCase().replace(/\.$/, '');
12
12
  }
@@ -146,7 +146,7 @@ function collectVerificationRecords(raw, seen = new Set()) {
146
146
  const object = raw;
147
147
  const verification = object.verification;
148
148
  const records = Array.isArray(verification) ? verification : [];
149
- const nested = Object.entries(object).flatMap(([key, value]) => (key === 'verification' ? [] : collectVerificationRecords(value, seen)));
149
+ const nested = Object.entries(object).flatMap(([key, value]) => key === 'verification' ? [] : collectVerificationRecords(value, seen));
150
150
  return [...records, ...nested];
151
151
  }
152
152
  function uniqueRecords(records) {
@@ -359,7 +359,12 @@ export async function linkDomain(input) {
359
359
  const zone = await resolveZone(provider, plan.zoneDomain);
360
360
  reportProgress(input, 'vercel:get-target', 'Reading Vercel DNS target');
361
361
  const cname = plan.isApex ? undefined : await vercel.getRecommendedCname(plan.domain);
362
- const baseRecord = planBaseRecord({ isApex: plan.isApex, provider: plan.provider, recordName: plan.recordName, cname });
362
+ const baseRecord = planBaseRecord({
363
+ isApex: plan.isApex,
364
+ provider: plan.provider,
365
+ recordName: plan.recordName,
366
+ cname,
367
+ });
363
368
  const forceDns = await resolveDnsForce(input, { baseRecord, plan, provider, zone });
364
369
  reportProgress(input, 'vercel:add-domain', 'Adding domain to Vercel');
365
370
  const addResult = await vercel.addDomainToProject(plan.project, plan.domain, { force: input.force });
@@ -376,7 +381,9 @@ export async function linkDomain(input) {
376
381
  const dnsResult = await provider.applyChanges(zone, dnsPlan, { force: forceDns });
377
382
  const shouldWait = input.wait ?? true;
378
383
  if (shouldWait) {
379
- reportProgress(input, 'dns:wait', verificationDnsRecords.length > 0 ? 'DNS records saved; asking Vercel to verify ownership' : 'DNS records saved; asking Vercel to verify');
384
+ reportProgress(input, 'dns:wait', verificationDnsRecords.length > 0
385
+ ? 'DNS records saved; asking Vercel to verify ownership'
386
+ : 'DNS records saved; asking Vercel to verify');
380
387
  }
381
388
  const waitResult = shouldWait
382
389
  ? await waitForVercelDomainReady({
@@ -1,7 +1,7 @@
1
1
  import { normalizeDomain } from '../../validate.js';
2
+ import { ProviderError } from '../core/errors.js';
2
3
  import { createProviderHttpClient } from '../core/http.js';
3
4
  import { assertNoConflicts, planDnsChanges } from '../core/planner.js';
4
- import { ProviderError } from '../core/errors.js';
5
5
  const CLOUDFLARE_API_URL = 'https://api.cloudflare.com/client/v4';
6
6
  const capabilities = {
7
7
  defaultTtl: 3600,
@@ -60,7 +60,9 @@ function toCloudflareRecord(record, zone) {
60
60
  ttl: record.ttl ?? capabilities.defaultTtl,
61
61
  type: record.type,
62
62
  ...(record.priority === undefined ? {} : { priority: record.priority }),
63
- ...(record.proxied === undefined || !['A', 'AAAA', 'CNAME'].includes(record.type) ? {} : { proxied: record.proxied }),
63
+ ...(record.proxied === undefined || !['A', 'AAAA', 'CNAME'].includes(record.type)
64
+ ? {}
65
+ : { proxied: record.proxied }),
64
66
  };
65
67
  }
66
68
  function toZone(zone) {
@@ -102,7 +104,7 @@ export class CloudflareProvider {
102
104
  const zones = [];
103
105
  for (let page = 1; page <= 100; page += 1) {
104
106
  const response = await this.request('/zones', {
105
- query: { 'account.id': this.accountId, direction: 'asc', order: 'name', page, 'per_page': 50 },
107
+ query: { 'account.id': this.accountId, direction: 'asc', order: 'name', page, per_page: 50 },
106
108
  });
107
109
  for (const zone of response.result ?? []) {
108
110
  const dnsZone = toZone(zone);
@@ -124,7 +126,7 @@ export class CloudflareProvider {
124
126
  const records = [];
125
127
  for (let page = 1; page <= 100; page += 1) {
126
128
  const response = await this.request(`/zones/${zone.id}/dns_records`, {
127
- query: { page, 'per_page': 100 },
129
+ query: { page, per_page: 100 },
128
130
  });
129
131
  for (const record of response.result ?? []) {
130
132
  const dnsRecord = toDnsRecord(record, zone);
@@ -138,7 +140,13 @@ export class CloudflareProvider {
138
140
  return records;
139
141
  }
140
142
  async planChanges(zone, desired, opts = {}) {
141
- return planDnsChanges({ desired, existing: await this.listRecords(zone), force: opts.force, providerId: this.id, zone });
143
+ return planDnsChanges({
144
+ desired,
145
+ existing: await this.listRecords(zone),
146
+ force: opts.force,
147
+ providerId: this.id,
148
+ zone,
149
+ });
142
150
  }
143
151
  async applyChanges(zone, plan) {
144
152
  assertNoConflicts(this.id, plan);
@@ -50,7 +50,8 @@ export function providerAccountHasCredentials(config, providerId, accountInput)
50
50
  return false;
51
51
  if (account !== DEFAULT_PROVIDER_ACCOUNT)
52
52
  return hasCredentials(current.accounts?.[account]?.credentials);
53
- return hasCredentials(current.credentials) || Boolean(legacyCredential(config, providerId, 'apiKey') || legacyCredential(config, providerId, 'apiSecret'));
53
+ return (hasCredentials(current.credentials) ||
54
+ Boolean(legacyCredential(config, providerId, 'apiKey') || legacyCredential(config, providerId, 'apiSecret')));
54
55
  }
55
56
  export function withProviderAccountCredentials(current, accountInput, credentials) {
56
57
  const account = normalizeProviderAccount(accountInput);
@@ -50,7 +50,11 @@ export function planDnsChanges(input) {
50
50
  continue;
51
51
  }
52
52
  const [replace, ...remainingSameTyped] = sameTyped;
53
- changes.push(...(replace ? [{ action: 'update', existing: replace, record }] : []), ...remainingSameTyped.map((existing) => ({ action: 'delete', existing, reason: 'same_type_record_exists' })), ...cnameConflicts.map((existing) => ({ action: 'delete', existing, reason: 'cname_slot_conflict' })), ...(replace ? [] : [{ action: 'create', record }]));
53
+ changes.push(...(replace ? [{ action: 'update', existing: replace, record }] : []), ...remainingSameTyped.map((existing) => ({
54
+ action: 'delete',
55
+ existing,
56
+ reason: 'same_type_record_exists',
57
+ })), ...cnameConflicts.map((existing) => ({ action: 'delete', existing, reason: 'cname_slot_conflict' })), ...(replace ? [] : [{ action: 'create', record }]));
54
58
  }
55
59
  return { changes, conflicts, desired: input.desired, existing: input.existing, zone: input.zone };
56
60
  }
@@ -129,7 +129,13 @@ export class HostingerProvider {
129
129
  return records.flatMap((record) => toDnsRecords(record, zone));
130
130
  }
131
131
  async planChanges(zone, desired, opts = {}) {
132
- const plan = planDnsChanges({ desired, existing: await this.listRecords(zone), force: opts.force, providerId: this.id, zone });
132
+ const plan = planDnsChanges({
133
+ desired,
134
+ existing: await this.listRecords(zone),
135
+ force: opts.force,
136
+ providerId: this.id,
137
+ zone,
138
+ });
133
139
  return collapseRecordSetWrites(plan);
134
140
  }
135
141
  async applyChanges(zone, plan) {
@@ -53,7 +53,10 @@ function providerCodeFromNamecheapError(message) {
53
53
  const lower = message.toLowerCase();
54
54
  if (lower.includes('clientip') || lower.includes('client ip') || lower.includes('whitelist'))
55
55
  return 'PROVIDER_PERMISSION_DENIED';
56
- if (lower.includes('api key') || lower.includes('apiuser') || lower.includes('username') || lower.includes('authentication')) {
56
+ if (lower.includes('api key') ||
57
+ lower.includes('apiuser') ||
58
+ lower.includes('username') ||
59
+ lower.includes('authentication')) {
57
60
  return 'PROVIDER_AUTH_FAILED';
58
61
  }
59
62
  if (lower.includes('rate'))
@@ -176,7 +179,13 @@ export class NamecheapProvider {
176
179
  });
177
180
  }
178
181
  async planChanges(zone, desired, opts = {}) {
179
- return planDnsChanges({ desired, existing: await this.listRecords(zone), force: opts.force, providerId: this.id, zone });
182
+ return planDnsChanges({
183
+ desired,
184
+ existing: await this.listRecords(zone),
185
+ force: opts.force,
186
+ providerId: this.id,
187
+ zone,
188
+ });
180
189
  }
181
190
  async applyChanges(zone, plan) {
182
191
  assertNoConflicts(this.id, plan);
@@ -5,7 +5,12 @@ import { createProviderContext } from './core/config.js';
5
5
  import { hostingerProviderDefinition } from './hostinger/index.js';
6
6
  import { namecheapProviderDefinition } from './namecheap/index.js';
7
7
  import { spaceshipProviderDefinition } from './spaceship/index.js';
8
- const definitions = [spaceshipProviderDefinition, namecheapProviderDefinition, cloudflareProviderDefinition, hostingerProviderDefinition];
8
+ const definitions = [
9
+ spaceshipProviderDefinition,
10
+ namecheapProviderDefinition,
11
+ cloudflareProviderDefinition,
12
+ hostingerProviderDefinition,
13
+ ];
9
14
  export function listProviderDefinitions() {
10
15
  return definitions;
11
16
  }
@@ -9,7 +9,9 @@ export async function listProviderStatuses(opts = {}) {
9
9
  const statuses = [];
10
10
  for (const definition of listProviderDefinitions()) {
11
11
  const accounts = listConfiguredProviderAccounts(config, definition);
12
- const refs = accounts.length > 0 ? accounts : [{ account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: definition.id }];
12
+ const refs = accounts.length > 0
13
+ ? accounts
14
+ : [{ account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: definition.id }];
13
15
  for (const ref of refs) {
14
16
  const account = normalizeProviderAccount(ref.account);
15
17
  const configured = accounts.some((item) => item.account === account);
@@ -0,0 +1,27 @@
1
+ interface ProcessResult {
2
+ exitCode: number | null;
3
+ stderr: string;
4
+ stdout: string;
5
+ }
6
+ interface ProcessOptions {
7
+ env: NodeJS.ProcessEnv;
8
+ shell: boolean;
9
+ }
10
+ type ProcessRunner = (command: string, args: string[], options: ProcessOptions) => Promise<ProcessResult>;
11
+ export interface SelfUpdateResult {
12
+ package: string;
13
+ packageSpec: string;
14
+ packageManager: 'npm';
15
+ }
16
+ export interface SelfUpdateOptions {
17
+ cacheRoot?: string;
18
+ platform?: NodeJS.Platform;
19
+ runner?: ProcessRunner;
20
+ }
21
+ export declare function npmInstallCommand(cacheDirectory: string, platform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv): {
22
+ args: string[];
23
+ command: string;
24
+ options: ProcessOptions;
25
+ };
26
+ export declare function installLatestVersion(options?: SelfUpdateOptions): Promise<SelfUpdateResult>;
27
+ export {};
@@ -0,0 +1,63 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { DoomainError } from './errors.js';
6
+ function runProcess(command, args, options) {
7
+ return new Promise((resolve, reject) => {
8
+ const child = spawn(command, args, {
9
+ ...options,
10
+ stdio: ['ignore', 'pipe', 'pipe'],
11
+ windowsHide: true,
12
+ });
13
+ let stderr = '';
14
+ let stdout = '';
15
+ child.stderr.setEncoding('utf8');
16
+ child.stderr.on('data', (chunk) => {
17
+ stderr += chunk;
18
+ });
19
+ child.stdout.setEncoding('utf8');
20
+ child.stdout.on('data', (chunk) => {
21
+ stdout += chunk;
22
+ });
23
+ child.once('error', reject);
24
+ child.once('close', (exitCode) => resolve({ exitCode, stderr, stdout }));
25
+ });
26
+ }
27
+ export function npmInstallCommand(cacheDirectory, platform = process.platform, environment = process.env) {
28
+ const env = Object.fromEntries(Object.entries(environment).filter(([key]) => key.toLowerCase() !== 'npm_config_cache'));
29
+ env.npm_config_cache = cacheDirectory;
30
+ return {
31
+ command: 'npm',
32
+ args: ['install', '--global', 'doomain@latest', '--prefer-online', '--offline=false'],
33
+ options: {
34
+ env,
35
+ shell: platform === 'win32',
36
+ },
37
+ };
38
+ }
39
+ export async function installLatestVersion(options = {}) {
40
+ const cacheDirectory = await mkdtemp(join(options.cacheRoot ?? tmpdir(), 'doomain-npm-cache-'));
41
+ try {
42
+ const invocation = npmInstallCommand(cacheDirectory, options.platform);
43
+ const result = await (options.runner ?? runProcess)(invocation.command, invocation.args, invocation.options);
44
+ if (result.exitCode !== 0) {
45
+ const reason = result.stderr.trim() || result.stdout.trim() || `npm exited with code ${result.exitCode ?? 'unknown'}`;
46
+ throw new DoomainError('SELF_UPDATE_FAILED', `Unable to update doomain: ${reason}`);
47
+ }
48
+ return {
49
+ package: 'doomain',
50
+ packageSpec: 'doomain@latest',
51
+ packageManager: 'npm',
52
+ };
53
+ }
54
+ catch (error) {
55
+ if (error instanceof DoomainError)
56
+ throw error;
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ throw new DoomainError('SELF_UPDATE_FAILED', `Unable to update doomain: ${message}`);
59
+ }
60
+ finally {
61
+ await rm(cacheDirectory, { force: true, recursive: true }).catch(() => undefined);
62
+ }
63
+ }
@@ -1,7 +1,11 @@
1
1
  import { DoomainError } from './errors.js';
2
2
  const DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
3
3
  export function normalizeDomain(input) {
4
- const value = input.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
4
+ const value = input
5
+ .trim()
6
+ .toLowerCase()
7
+ .replace(/^https?:\/\//, '')
8
+ .replace(/\/$/, '');
5
9
  const domain = value.split('/')[0];
6
10
  if (!domain || domain.length > 253) {
7
11
  throw new DoomainError('INVALID_INPUT', 'Domain is required.');
@@ -13,7 +17,10 @@ export function normalizeDomain(input) {
13
17
  return domain;
14
18
  }
15
19
  export function normalizeSubdomain(input) {
16
- const subdomain = input.trim().toLowerCase().replace(/^\.+|\.+$/g, '');
20
+ const subdomain = input
21
+ .trim()
22
+ .toLowerCase()
23
+ .replace(/^\.+|\.+$/g, '');
17
24
  if (!subdomain || subdomain === '@') {
18
25
  throw new DoomainError('INVALID_INPUT', 'Subdomain is required unless --apex is used.');
19
26
  }
@@ -47,7 +47,8 @@ function findProjectDomainTarget(raw, domain) {
47
47
  const targets = Array.isArray(raw) ? raw : [raw];
48
48
  return targets.find((target) => target &&
49
49
  typeof target === 'object' &&
50
- (isSameDomain(target.domain, domain) || isSameDomain(target.name, domain)));
50
+ (isSameDomain(target.domain, domain) ||
51
+ isSameDomain(target.name, domain)));
51
52
  }
52
53
  export function createVercelClient(config) {
53
54
  async function request(path, init = {}, opts = {}) {
@@ -177,7 +178,9 @@ export function createVercelClient(config) {
177
178
  return result.domains ?? [];
178
179
  },
179
180
  async removeDomainFromProject(project, domain) {
180
- await request(`/v9/projects/${encodeURIComponent(project)}/domains/${encodeURIComponent(domain)}`, { method: 'DELETE' });
181
+ await request(`/v9/projects/${encodeURIComponent(project)}/domains/${encodeURIComponent(domain)}`, {
182
+ method: 'DELETE',
183
+ });
181
184
  },
182
185
  async verifyProjectDomain(project, domain) {
183
186
  return request(`/v9/projects/${encodeURIComponent(project)}/domains/${encodeURIComponent(domain)}/verify`, { method: 'POST' });
@@ -142,6 +142,68 @@
142
142
  "schema.js"
143
143
  ]
144
144
  },
145
+ "update": {
146
+ "aliases": [],
147
+ "args": {},
148
+ "description": "Install the latest doomain version from npm without using the existing npm cache.",
149
+ "examples": [
150
+ "<%= config.bin %> <%= command.id %>",
151
+ "<%= config.bin %> <%= command.id %> --json"
152
+ ],
153
+ "flags": {
154
+ "json": {
155
+ "description": "Output a single JSON object and never prompt.",
156
+ "name": "json",
157
+ "allowNo": false,
158
+ "type": "boolean"
159
+ }
160
+ },
161
+ "hasDynamicHelp": false,
162
+ "hiddenAliases": [],
163
+ "id": "update",
164
+ "pluginAlias": "doomain",
165
+ "pluginName": "doomain",
166
+ "pluginType": "core",
167
+ "strict": true,
168
+ "enableJsonFlag": false,
169
+ "isESM": true,
170
+ "relativePath": [
171
+ "dist",
172
+ "commands",
173
+ "update.js"
174
+ ]
175
+ },
176
+ "upgrade": {
177
+ "aliases": [],
178
+ "args": {},
179
+ "description": "Install the latest doomain version from npm without using the existing npm cache.",
180
+ "examples": [
181
+ "<%= config.bin %> <%= command.id %>",
182
+ "<%= config.bin %> <%= command.id %> --json"
183
+ ],
184
+ "flags": {
185
+ "json": {
186
+ "description": "Output a single JSON object and never prompt.",
187
+ "name": "json",
188
+ "allowNo": false,
189
+ "type": "boolean"
190
+ }
191
+ },
192
+ "hasDynamicHelp": false,
193
+ "hiddenAliases": [],
194
+ "id": "upgrade",
195
+ "pluginAlias": "doomain",
196
+ "pluginName": "doomain",
197
+ "pluginType": "core",
198
+ "strict": true,
199
+ "enableJsonFlag": false,
200
+ "isESM": true,
201
+ "relativePath": [
202
+ "dist",
203
+ "commands",
204
+ "upgrade.js"
205
+ ]
206
+ },
145
207
  "verify": {
146
208
  "aliases": [],
147
209
  "args": {},
@@ -1001,5 +1063,5 @@
1001
1063
  ]
1002
1064
  }
1003
1065
  },
1004
- "version": "0.1.19"
1066
+ "version": "0.1.21"
1005
1067
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "doomain",
3
3
  "description": "Configure production domains and DNS records in seconds",
4
- "version": "0.1.19",
4
+ "version": "0.1.21",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {