doomain 0.1.10 → 0.1.11

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.
@@ -2,6 +2,7 @@ import { Command } from '@oclif/core';
2
2
  export default class DomainsList extends Command {
3
3
  static description: string;
4
4
  static flags: {
5
+ account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
5
6
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
7
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
8
  provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,8 +1,9 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import { loadConfig } from '../../lib/config.js';
3
- import { domainFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
3
+ import { accountFlag, domainFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
4
4
  import { createOutput, outputError } from '../../lib/output.js';
5
- import { createProvider } from '../../lib/providers/registry.js';
5
+ import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, } from '../../lib/providers/core/config.js';
6
+ import { createProvider, getProviderDefinition } from '../../lib/providers/registry.js';
6
7
  import { normalizeDomain } from '../../lib/validate.js';
7
8
  async function resolveZones(provider, domain) {
8
9
  if (!domain)
@@ -16,6 +17,7 @@ async function resolveZones(provider, domain) {
16
17
  export default class DomainsList extends Command {
17
18
  static description = 'List DNS zones and records for a provider.';
18
19
  static flags = {
20
+ account: accountFlag,
19
21
  domain: domainFlag,
20
22
  json: jsonFlag,
21
23
  provider: providerFlag,
@@ -25,17 +27,37 @@ export default class DomainsList extends Command {
25
27
  const out = createOutput({ json: flags.json });
26
28
  try {
27
29
  const config = await loadConfig();
28
- const provider = await createProvider(flags.provider ?? process.env.DOOMAIN_PROVIDER ?? config.defaults?.provider ?? 'spaceship');
29
- const zones = await resolveZones(provider, flags.domain);
30
+ const providerId = flags.provider ?? process.env.DOOMAIN_PROVIDER ?? config.defaults?.provider ?? 'spaceship';
31
+ const definition = getProviderDefinition(providerId);
32
+ const account = flags.account ? normalizeProviderAccount(flags.account) : undefined;
33
+ const accounts = account
34
+ ? [{ account, isDefaultAccount: isDefaultProviderAccount(account), providerId: definition.id }]
35
+ : listConfiguredProviderAccounts(config, definition);
36
+ const selectedAccounts = accounts.length > 0 ? accounts : [{ account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: definition.id }];
30
37
  const results = [];
31
- for (const zone of zones) {
32
- const records = await provider.listRecords(zone);
33
- results.push({ zone, records });
34
- out.info(`${zone.name} (${records.length} records)`);
35
- for (const record of records)
36
- out.info(` ${record.type} ${record.name} -> ${record.value}`);
38
+ for (const selectedAccount of selectedAccounts) {
39
+ const provider = await createProvider(definition.id, { account: selectedAccount.account });
40
+ const zones = await resolveZones(provider, flags.domain);
41
+ for (const zone of zones) {
42
+ const records = await provider.listRecords(zone);
43
+ results.push({
44
+ account: selectedAccount.account,
45
+ isDefaultAccount: selectedAccount.isDefaultAccount,
46
+ provider: provider.id,
47
+ records,
48
+ zone,
49
+ });
50
+ const accountLabel = selectedAccount.isDefaultAccount ? provider.id : `${provider.id}/${selectedAccount.account}`;
51
+ out.info(`${zone.name} (${records.length} records) via ${accountLabel}`);
52
+ for (const record of records)
53
+ out.info(` ${record.type} ${record.name} -> ${record.value}`);
54
+ }
37
55
  }
38
- out.result({ provider: provider.id, zones: results });
56
+ out.result({
57
+ ...(selectedAccounts.length === 1 ? { account: selectedAccounts[0].account, isDefaultAccount: selectedAccounts[0].isDefaultAccount } : {}),
58
+ provider: definition.id,
59
+ zones: results,
60
+ });
39
61
  }
40
62
  catch (error) {
41
63
  outputError(out.json, error, 'DOMAIN_LINK_FAILED');
@@ -6,6 +6,7 @@ export default class Link extends Command {
6
6
  domain: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
7
  };
8
8
  static flags: {
9
+ account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  apex: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -1,6 +1,6 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import { DoomainError } from '../lib/errors.js';
3
- import { apexFlag, domainFlag, jsonFlag, projectFlag, providerFlag, subdomainFlag } from '../lib/flags.js';
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';
6
6
  export default class Link extends Command {
@@ -11,11 +11,13 @@ export default class Link extends Command {
11
11
  '<%= config.bin %> <%= command.id %> --domain app.example.com --project my-app --json',
12
12
  '<%= config.bin %> <%= command.id %> --domain example.com --subdomain app --project my-app',
13
13
  '<%= config.bin %> <%= command.id %> --provider spaceship --domain example.com --apex --project my-app --json',
14
+ '<%= config.bin %> <%= command.id %> app.example.com --provider spaceship --account work --project my-app --json',
14
15
  ];
15
16
  static args = {
16
17
  domain: Args.string({ description: 'Target domain to link, for example app.example.com.', required: false }),
17
18
  };
18
19
  static flags = {
20
+ account: accountFlag,
19
21
  apex: apexFlag,
20
22
  domain: domainFlag,
21
23
  'dry-run': Flags.boolean({ description: 'Preview changes without writing to Vercel or DNS.' }),
@@ -5,6 +5,7 @@ export default class ProvidersConnect extends Command {
5
5
  };
6
6
  static description: string;
7
7
  static flags: {
8
+ account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  'api-secret': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  credential: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,10 +1,10 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import * as p from '@clack/prompts';
3
3
  import { getConfigPath, loadConfig, maskSecret, updateConfig } from '../../lib/config.js';
4
- import { jsonFlag } from '../../lib/flags.js';
4
+ import { accountFlag, jsonFlag } from '../../lib/flags.js';
5
5
  import { createOutput, outputError } from '../../lib/output.js';
6
+ import { isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount } from '../../lib/providers/core/config.js';
6
7
  import { getProviderDefinition, listProviderDefinitions } from '../../lib/providers/registry.js';
7
- import { isProviderConfigured } from '../../lib/providers/status.js';
8
8
  function requireString(value, message) {
9
9
  if (typeof value === 'string' && value.trim())
10
10
  return value.trim();
@@ -63,7 +63,7 @@ async function promptProvider() {
63
63
  const selected = await p.select({
64
64
  message: 'Choose DNS provider',
65
65
  options: listProviderDefinitions().map((definition) => ({
66
- hint: isProviderConfigured(definition, config) ? 'Connected' : 'Not connected',
66
+ hint: listConfiguredProviderAccounts(config, definition).length > 0 ? 'Connected' : 'Not connected',
67
67
  label: definition.displayName,
68
68
  value: definition.id,
69
69
  })),
@@ -91,6 +91,7 @@ export default class ProvidersConnect extends Command {
91
91
  };
92
92
  static description = 'Save DNS provider credentials locally.';
93
93
  static flags = {
94
+ account: accountFlag,
94
95
  'api-key': Flags.string({ description: 'Compatibility alias for Spaceship apiKey.' }),
95
96
  'api-secret': Flags.string({ description: 'Compatibility alias for Spaceship apiSecret.' }),
96
97
  credential: Flags.string({ char: 'c', description: 'Provider credential as key=value.', multiple: true }),
@@ -107,6 +108,8 @@ export default class ProvidersConnect extends Command {
107
108
  const definition = args.provider ? getProviderDefinition(args.provider) : await promptProvider();
108
109
  if (!definition)
109
110
  return;
111
+ const account = normalizeProviderAccount(flags.account);
112
+ const isDefaultAccount = isDefaultProviderAccount(account);
110
113
  const passedCredentials = parseCredentialFlags(flags.credential);
111
114
  const credentials = {};
112
115
  const detectedPublicIp = !out.json && usesClientIp(definition) ? await fetchPublicIp() : undefined;
@@ -162,13 +165,24 @@ export default class ProvidersConnect extends Command {
162
165
  defaults: setDefault ? { ...config.defaults, provider: definition.id } : config.defaults,
163
166
  providers: {
164
167
  ...config.providers,
165
- [definition.id]: { credentials },
168
+ [definition.id]: isDefaultAccount
169
+ ? { ...config.providers?.[definition.id], credentials }
170
+ : {
171
+ ...config.providers?.[definition.id],
172
+ accounts: {
173
+ ...config.providers?.[definition.id]?.accounts,
174
+ [account]: { credentials },
175
+ },
176
+ },
166
177
  },
167
178
  }));
168
179
  out.result({
180
+ account,
169
181
  configPath: getConfigPath(),
170
182
  credentials: Object.fromEntries(Object.entries(credentials).map(([key, value]) => [key, maskSecret(value)])),
183
+ defaultAccount: isDefaultAccount,
171
184
  domainCount,
185
+ isDefaultAccount,
172
186
  provider: definition.id,
173
187
  verified: !flags['no-verify'],
174
188
  });
@@ -7,6 +7,7 @@ export default class ProvidersDisconnect extends Command {
7
7
  static description: string;
8
8
  static examples: string[];
9
9
  static flags: {
10
+ account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
12
  };
12
13
  run(): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  import { Args, Command } from '@oclif/core';
2
2
  import { getConfigPath, updateConfig } from '../../lib/config.js';
3
- import { jsonFlag } from '../../lib/flags.js';
3
+ import { accountFlag, jsonFlag } from '../../lib/flags.js';
4
4
  import { createOutput, outputError } from '../../lib/output.js';
5
+ import { isDefaultProviderAccount, normalizeProviderAccount } from '../../lib/providers/core/config.js';
5
6
  import { getProviderDefinition } from '../../lib/providers/registry.js';
6
7
  function envOverrides(definition) {
7
8
  return definition.credentials.flatMap((credential) => (process.env[credential.env] ? [credential.env] : []));
@@ -17,6 +18,7 @@ export default class ProvidersDisconnect extends Command {
17
18
  '<%= config.bin %> <%= command.id %> cloudflare --json',
18
19
  ];
19
20
  static flags = {
21
+ account: accountFlag,
20
22
  json: jsonFlag,
21
23
  };
22
24
  async run() {
@@ -24,13 +26,41 @@ export default class ProvidersDisconnect extends Command {
24
26
  const out = createOutput({ json: flags.json });
25
27
  try {
26
28
  const definition = getProviderDefinition(args.provider);
29
+ const account = flags.account ? normalizeProviderAccount(flags.account) : undefined;
27
30
  let removed = false;
28
31
  await updateConfig((config) => {
29
32
  const providers = { ...config.providers };
30
- removed = providers[definition.id] !== undefined;
31
- delete providers[definition.id];
33
+ const provider = providers[definition.id];
34
+ if (!account) {
35
+ removed = provider !== undefined;
36
+ delete providers[definition.id];
37
+ }
38
+ else if (provider) {
39
+ const nextProvider = { ...provider };
40
+ if (isDefaultProviderAccount(account)) {
41
+ removed = nextProvider.credentials !== undefined || (definition.id === 'spaceship' && ('apiKey' in nextProvider || 'apiSecret' in nextProvider));
42
+ delete nextProvider.credentials;
43
+ if (definition.id === 'spaceship') {
44
+ delete nextProvider.apiKey;
45
+ delete nextProvider.apiSecret;
46
+ delete nextProvider.domains;
47
+ }
48
+ }
49
+ else {
50
+ const accounts = { ...nextProvider.accounts };
51
+ removed = accounts[account] !== undefined;
52
+ delete accounts[account];
53
+ nextProvider.accounts = Object.keys(accounts).length > 0 ? accounts : undefined;
54
+ }
55
+ if (nextProvider.credentials || nextProvider.settings || (nextProvider.accounts && Object.keys(nextProvider.accounts).length > 0)) {
56
+ providers[definition.id] = nextProvider;
57
+ }
58
+ else {
59
+ delete providers[definition.id];
60
+ }
61
+ }
32
62
  const defaults = { ...config.defaults };
33
- if (defaults.provider === definition.id)
63
+ if (defaults.provider === definition.id && providers[definition.id] === undefined)
34
64
  delete defaults.provider;
35
65
  return {
36
66
  ...config,
@@ -39,7 +69,7 @@ export default class ProvidersDisconnect extends Command {
39
69
  };
40
70
  });
41
71
  const overrides = envOverrides(definition);
42
- out.result({ configPath: getConfigPath(), environmentOverrides: overrides, provider: definition.id, removed });
72
+ out.result({ account, configPath: getConfigPath(), environmentOverrides: overrides, provider: definition.id, removed });
43
73
  if (overrides.length > 0)
44
74
  out.warn(`${definition.displayName} environment credentials are still set: ${overrides.join(', ')}.`);
45
75
  out.success(removed ? `${definition.displayName} credentials removed from ${getConfigPath()}.` : `${definition.displayName} was not connected.`);
@@ -24,8 +24,10 @@ export default class ProvidersStatus extends Command {
24
24
  spinner?.start('Checking DNS providers');
25
25
  const providers = await listProviderStatuses({ verify: !flags['no-verify'] });
26
26
  spinner?.stop('Checked DNS providers');
27
- for (const provider of providers)
28
- out.info(`${provider.displayName} (${provider.id}) - ${formatStatus(provider)}${provider.default ? ' [default]' : ''}`);
27
+ for (const provider of providers) {
28
+ const account = provider.isDefaultAccount ? provider.id : `${provider.id}/${provider.account}`;
29
+ out.info(`${provider.displayName} (${account}) - ${formatStatus(provider)}${provider.default ? ' [default]' : ''}`);
30
+ }
29
31
  out.result({ providers });
30
32
  }
31
33
  }
@@ -5,6 +5,7 @@ export default class ProvidersVerify extends Command {
5
5
  };
6
6
  static description: string;
7
7
  static flags: {
8
+ account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  };
10
11
  run(): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { Args, Command } from '@oclif/core';
2
- import { jsonFlag } from '../../lib/flags.js';
2
+ import { accountFlag, jsonFlag } from '../../lib/flags.js';
3
3
  import { createOutput, outputError } from '../../lib/output.js';
4
+ import { isDefaultProviderAccount, normalizeProviderAccount } from '../../lib/providers/core/config.js';
4
5
  import { createProvider } from '../../lib/providers/registry.js';
5
6
  export default class ProvidersVerify extends Command {
6
7
  static args = {
@@ -8,15 +9,17 @@ export default class ProvidersVerify extends Command {
8
9
  };
9
10
  static description = 'Verify saved DNS provider credentials.';
10
11
  static flags = {
12
+ account: accountFlag,
11
13
  json: jsonFlag,
12
14
  };
13
15
  async run() {
14
16
  const { args, flags } = await this.parse(ProvidersVerify);
15
17
  const out = createOutput({ json: flags.json });
16
18
  try {
17
- const provider = await createProvider(args.provider);
19
+ const account = normalizeProviderAccount(flags.account);
20
+ const provider = await createProvider(args.provider, { account });
18
21
  const health = await provider.verifyCredentials();
19
- out.result({ health, provider: provider.id });
22
+ out.result({ account, health, isDefaultAccount: isDefaultProviderAccount(account), provider: provider.id });
20
23
  out.success(`${provider.name} credentials verified.`);
21
24
  }
22
25
  catch (error) {
@@ -6,8 +6,8 @@ import { jsonFlag } from '../lib/flags.js';
6
6
  import { createLinkPlan, linkDomain } from '../lib/link-domain.js';
7
7
  import { detectLocalVercelProject } from '../lib/local-vercel.js';
8
8
  import { createOutput, outputError } from '../lib/output.js';
9
+ import { DEFAULT_PROVIDER_ACCOUNT, listConfiguredProviderAccounts } from '../lib/providers/core/config.js';
9
10
  import { createProvider, getProviderDefinition, listProviderDefinitions } from '../lib/providers/registry.js';
10
- import { isProviderConfigured } from '../lib/providers/status.js';
11
11
  import { listGlobalVercelTokens } from '../lib/vercel-auth.js';
12
12
  import { createVercelClient } from '../lib/vercel.js';
13
13
  const PERSONAL_ACCOUNT = '__personal__';
@@ -72,7 +72,7 @@ async function promptProviderDefinition(definitions, config) {
72
72
  const selected = await p.select({
73
73
  message: 'Choose DNS provider',
74
74
  options: definitions.map((definition) => ({
75
- hint: isProviderConfigured(definition, config) ? 'Connected' : 'Not connected',
75
+ hint: listConfiguredProviderAccounts(config, definition).length > 0 ? 'Connected' : 'Not connected',
76
76
  label: definition.displayName,
77
77
  value: definition.id,
78
78
  })),
@@ -87,19 +87,24 @@ function showProviderSetup(definition) {
87
87
  return;
88
88
  p.note(definition.setup.notes.join('\n'), `${definition.displayName} setup`);
89
89
  }
90
- async function listProviderDomainOptions(definition) {
91
- const provider = await createProvider(definition.id);
90
+ async function listProviderDomainOptions(definition, account) {
91
+ const provider = await createProvider(definition.id, { account: account.account });
92
92
  const zones = await provider.listZones();
93
- return zones.map((zone) => toProviderDomainOption(definition, zone));
93
+ return zones.map((zone) => toProviderDomainOption(definition, account, zone));
94
94
  }
95
- function toProviderDomainOption(definition, zone) {
95
+ function toProviderDomainOption(definition, account, zone) {
96
96
  return {
97
+ account: account.account,
97
98
  domain: zone.name,
98
- id: `${definition.id}:${zone.name}`,
99
+ id: `${definition.id}:${account.account}:${zone.name}`,
100
+ isDefaultAccount: account.isDefaultAccount,
99
101
  providerId: definition.id,
100
102
  providerName: definition.displayName,
101
103
  };
102
104
  }
105
+ function providerAccountLabel(option) {
106
+ return option.isDefaultAccount ? option.providerName : `${option.providerName}/${option.account}`;
107
+ }
103
108
  function projectLabel(project) {
104
109
  return project.name ? `${project.name} (${project.id})` : project.id;
105
110
  }
@@ -237,10 +242,10 @@ export default class Wizard extends Command {
237
242
  const project = resolved;
238
243
  const projectDisplay = projectLabel(projects.find((item) => item.id === project) ?? { id: project });
239
244
  p.log.success(`Vercel ready: ${projectDisplay}`);
240
- const configuredProviderDefinitions = providerDefinitions.filter((definition) => isProviderConfigured(definition, config));
245
+ const configuredProviderAccounts = providerDefinitions.flatMap((definition) => listConfiguredProviderAccounts(config, definition).map((account) => ({ account, definition })));
241
246
  const providerFailures = [];
242
247
  const domainOptions = [];
243
- if (configuredProviderDefinitions.length === 0) {
248
+ if (configuredProviderAccounts.length === 0) {
244
249
  p.log.info('No DNS provider is configured yet. Connect one to continue.');
245
250
  const selectedDefinition = await promptProviderDefinition(providerDefinitions, config);
246
251
  if (!selectedDefinition)
@@ -256,7 +261,7 @@ export default class Wizard extends Command {
256
261
  const zones = await provider.listZones();
257
262
  domainSpinner.stop(`Connected ${selectedDefinition.displayName} and loaded ${zones.length} domain${zones.length === 1 ? '' : 's'}`);
258
263
  activeSpinner = undefined;
259
- domainOptions.push(...zones.map((zone) => toProviderDomainOption(selectedDefinition, zone)));
264
+ domainOptions.push(...zones.map((zone) => toProviderDomainOption(selectedDefinition, { account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: selectedDefinition.id }, zone)));
260
265
  await updateConfig((current) => ({
261
266
  ...current,
262
267
  defaults: { ...current.defaults, provider: selectedDefinition.id },
@@ -267,13 +272,14 @@ export default class Wizard extends Command {
267
272
  else {
268
273
  const domainSpinner = p.spinner();
269
274
  activeSpinner = domainSpinner;
270
- domainSpinner.start(`Loading domains from ${configuredProviderDefinitions.length} provider${configuredProviderDefinitions.length === 1 ? '' : 's'}`);
271
- for (const definition of configuredProviderDefinitions) {
275
+ domainSpinner.start(`Loading domains from ${configuredProviderAccounts.length} provider account${configuredProviderAccounts.length === 1 ? '' : 's'}`);
276
+ for (const { account, definition } of configuredProviderAccounts) {
272
277
  try {
273
- domainOptions.push(...(await listProviderDomainOptions(definition)));
278
+ domainOptions.push(...(await listProviderDomainOptions(definition, account)));
274
279
  }
275
280
  catch (error) {
276
- providerFailures.push(`${definition.displayName}: ${error instanceof Error ? error.message : String(error)}`);
281
+ const label = account.isDefaultAccount ? definition.displayName : `${definition.displayName}/${account.account}`;
282
+ providerFailures.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
277
283
  }
278
284
  }
279
285
  domainSpinner.stop(`Loaded ${domainOptions.length} domain${domainOptions.length === 1 ? '' : 's'}`);
@@ -299,14 +305,14 @@ export default class Wizard extends Command {
299
305
  placeholder: 'Type to filter domains...',
300
306
  maxItems: 10,
301
307
  initialValue,
302
- options: domainOptions.map((option) => ({ label: option.domain, value: option.id, hint: option.providerName })),
308
+ options: domainOptions.map((option) => ({ label: option.domain, value: option.id, hint: providerAccountLabel(option) })),
303
309
  });
304
310
  const selectedId = cancelIfNeeded(selectedDomainId);
305
311
  if (selectedId === null)
306
312
  return;
307
313
  selectedDomain = domainOptions.find((option) => option.id === selectedId) ?? domainOptions[0];
308
314
  }
309
- p.log.info(`Using domain ${selectedDomain.domain} from ${selectedDomain.providerName}.`);
315
+ p.log.info(`Using domain ${selectedDomain.domain} from ${providerAccountLabel(selectedDomain)}.`);
310
316
  const domain = selectedDomain.domain;
311
317
  await updateConfig((current) => ({
312
318
  ...current,
@@ -330,8 +336,8 @@ export default class Wizard extends Command {
330
336
  return;
331
337
  }
332
338
  const fullDomain = apex ? domain : `${subdomain}.${domain}`;
333
- const preview = await createLinkPlan({ provider: selectedDomain.providerId, domain, subdomain, apex, project });
334
- p.note([`Vercel: add ${preview.domain} to ${projectDisplay}`, ...preview.records.map((record) => recordPreview(record, selectedDomain.providerName))].join('\n'), 'Preview');
339
+ const preview = await createLinkPlan({ account: selectedDomain.account, provider: selectedDomain.providerId, domain, subdomain, apex, project });
340
+ p.note([`Vercel: add ${preview.domain} to ${projectDisplay}`, ...preview.records.map((record) => recordPreview(record, providerAccountLabel(selectedDomain)))].join('\n'), 'Preview');
335
341
  const confirmed = await p.confirm({
336
342
  message: `Link ${fullDomain} via ${selectedDomain.providerName} to Vercel project ${projectDisplay}?`,
337
343
  initialValue: true,
@@ -343,6 +349,7 @@ export default class Wizard extends Command {
343
349
  activeSpinner = spinner;
344
350
  spinner.start('Adding domain to Vercel');
345
351
  const result = await linkDomain({
352
+ account: selectedDomain.account,
346
353
  provider: selectedDomain.providerId,
347
354
  domain,
348
355
  subdomain,
@@ -1,5 +1,5 @@
1
1
  import { type ProviderStatus } from './providers/status.js';
2
- export type ProviderConnectionStatus = Pick<ProviderStatus, 'configured' | 'default' | 'displayName' | 'docsUrl' | 'id'>;
2
+ export type ProviderConnectionStatus = Pick<ProviderStatus, 'account' | 'configured' | 'default' | 'displayName' | 'docsUrl' | 'id' | 'isDefaultAccount'>;
3
3
  export interface CommandSchema {
4
4
  name: string;
5
5
  description: string;
@@ -9,6 +9,7 @@ export const commandSchemas = [
9
9
  'doomain link --domain app.example.com --project my-app --json',
10
10
  'doomain link --domain example.com --subdomain app --project my-app --json',
11
11
  'doomain link --provider spaceship --domain example.com --apex --project my-app --dry-run --json',
12
+ 'doomain link app.example.com --provider spaceship --account work --project my-app --json',
12
13
  ],
13
14
  agentHint: 'For agent use, try `doomain link <domain> --json` first. Do not inspect project files, run provider status, or use --dry-run unless the user explicitly asks for a preview. The command infers project/provider and returns structured recovery errors when inference fails.',
14
15
  agentInstructions: [
@@ -25,6 +26,7 @@ export const commandSchemas = [
25
26
  flags: [
26
27
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
27
28
  { name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
29
+ { name: 'account', type: 'string', description: 'DNS provider account alias. Defaults to the provider default account.' },
28
30
  { name: 'domain', type: 'string', description: 'Target domain or base zone, for example app.example.com or example.com.' },
29
31
  { name: 'subdomain', type: 'string', description: 'Subdomain to add.' },
30
32
  { name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
@@ -41,12 +43,14 @@ export const commandSchemas = [
41
43
  examples: [
42
44
  'doomain providers connect',
43
45
  'doomain providers connect spaceship --credential apiKey=key --credential apiSecret=secret --json',
46
+ 'doomain providers connect spaceship --account work --credential apiKey=key --credential apiSecret=secret --json',
44
47
  'doomain providers connect namecheap --credential apiUser=user --credential apiKey=key --credential clientIp=127.0.0.1 --json',
45
48
  'doomain providers connect cloudflare --credential apiToken=token --credential accountId=account_id --json',
46
49
  'doomain providers connect hostinger --credential apiToken=token --json',
47
50
  ],
48
51
  flags: [
49
52
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
53
+ { name: 'account', type: 'string', description: 'DNS provider account alias. Defaults to the provider default account.' },
50
54
  { name: 'credential', type: 'string', description: 'Provider credential as key=value. Can be repeated.' },
51
55
  { name: 'api-key', type: 'string', description: 'Spaceship API key.' },
52
56
  { name: 'api-secret', type: 'string', description: 'Spaceship API secret.' },
@@ -56,9 +60,10 @@ export const commandSchemas = [
56
60
  {
57
61
  name: 'providers add',
58
62
  description: 'Alias for providers connect.',
59
- examples: ['doomain providers add', 'doomain providers add namecheap'],
63
+ examples: ['doomain providers add', 'doomain providers add namecheap', 'doomain providers add spaceship --account work'],
60
64
  flags: [
61
65
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
66
+ { name: 'account', type: 'string', description: 'DNS provider account alias. Defaults to the provider default account.' },
62
67
  { name: 'credential', type: 'string', description: 'Provider credential as key=value. Can be repeated.' },
63
68
  { name: 'no-verify', type: 'boolean', description: 'Save credentials without verifying them first.' },
64
69
  ],
@@ -75,17 +80,29 @@ export const commandSchemas = [
75
80
  {
76
81
  name: 'providers disconnect',
77
82
  description: 'Remove saved DNS provider credentials locally.',
78
- examples: ['doomain providers disconnect namecheap --json', 'doomain providers disconnect cloudflare --json', 'doomain providers disconnect hostinger --json'],
83
+ examples: [
84
+ 'doomain providers disconnect namecheap --json',
85
+ 'doomain providers disconnect cloudflare --json',
86
+ 'doomain providers disconnect spaceship --account work --json',
87
+ 'doomain providers disconnect hostinger --json',
88
+ ],
79
89
  flags: [
80
90
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
91
+ { name: 'account', type: 'string', description: 'DNS provider account alias. Omit to remove all accounts for the provider.' },
81
92
  ],
82
93
  },
83
94
  {
84
95
  name: 'providers verify',
85
96
  description: 'Verify saved DNS provider credentials.',
86
- examples: ['doomain providers verify spaceship --json', 'doomain providers verify namecheap --json', 'doomain providers verify hostinger --json'],
97
+ examples: [
98
+ 'doomain providers verify spaceship --json',
99
+ 'doomain providers verify spaceship --account work --json',
100
+ 'doomain providers verify namecheap --json',
101
+ 'doomain providers verify hostinger --json',
102
+ ],
87
103
  flags: [
88
104
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
105
+ { name: 'account', type: 'string', description: 'DNS provider account alias. Defaults to the provider default account.' },
89
106
  ],
90
107
  },
91
108
  {
@@ -114,11 +131,13 @@ export function getCommandSchema(name) {
114
131
  }
115
132
  async function configuredProviders() {
116
133
  return (await listProviderStatuses({ verify: false })).map((provider) => ({
134
+ account: provider.account,
117
135
  configured: provider.configured,
118
136
  default: provider.default,
119
137
  displayName: provider.displayName,
120
138
  docsUrl: provider.docsUrl,
121
139
  id: provider.id,
140
+ isDefaultAccount: provider.isDefaultAccount,
122
141
  }));
123
142
  }
124
143
  function withProviderConnections(schema, providers) {
@@ -6,10 +6,13 @@ export interface VercelConfig {
6
6
  token?: string;
7
7
  teamId?: string;
8
8
  }
9
- export interface ProviderConfig {
9
+ export interface ProviderAccountConfig {
10
10
  credentials?: Record<string, string>;
11
11
  settings?: Record<string, unknown>;
12
12
  }
13
+ export interface ProviderConfig extends ProviderAccountConfig {
14
+ accounts?: Record<string, ProviderAccountConfig | undefined>;
15
+ }
13
16
  export interface SpaceshipProviderConfig extends ProviderConfig {
14
17
  apiKey?: string;
15
18
  apiSecret?: string;
@@ -1,5 +1,6 @@
1
1
  export declare const jsonFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
2
2
  export declare const providerFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
3
+ export declare const accountFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
3
4
  export declare const domainFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
4
5
  export declare const subdomainFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
5
6
  export declare const apexFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
package/dist/lib/flags.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  export const jsonFlag = Flags.boolean({ description: 'Output a single JSON object and never prompt.' });
3
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 account alias. Defaults to the provider default account.' });
4
5
  export const domainFlag = Flags.string({ description: 'Target domain or base zone, for example app.example.com or example.com.' });
5
6
  export const subdomainFlag = Flags.string({ description: 'Subdomain to add, for example app for app.example.com.' });
6
7
  export const apexFlag = Flags.boolean({ description: 'Use the root/apex domain instead of a subdomain.' });
@@ -1,6 +1,7 @@
1
1
  import type { DnsRecordInput } from './providers/types.js';
2
2
  export interface LinkDomainInput {
3
3
  provider?: string;
4
+ account?: string;
4
5
  domain?: string;
5
6
  subdomain?: string;
6
7
  apex?: boolean;
@@ -21,6 +22,9 @@ export type LinkDomainProgressCallback = (progress: LinkDomainProgress) => void;
21
22
  export interface LinkDomainPlan {
22
23
  provider: string;
23
24
  providerInferred: boolean;
25
+ account: string;
26
+ accountInferred: boolean;
27
+ isDefaultAccount: boolean;
24
28
  project: string;
25
29
  projectSource: LinkDomainProjectSource;
26
30
  recordName: string;
@@ -4,8 +4,9 @@ import { dirname, join, parse } from 'node:path';
4
4
  import { loadConfig } from './config.js';
5
5
  import { DoomainError } from './errors.js';
6
6
  import { detectLocalVercelProject } from './local-vercel.js';
7
+ import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, } from './providers/core/config.js';
7
8
  import { createProvider, getProviderDefinition, listProviderDefinitions } from './providers/registry.js';
8
- import { isProviderConfigured, listProviderStatuses } from './providers/status.js';
9
+ import { listProviderStatuses } from './providers/status.js';
9
10
  import { normalizeDomain, normalizeSubdomain } from './validate.js';
10
11
  import { createVercelClient, resolveVercelConfig, VERCEL_APEX_A_RECORD, VERCEL_CNAME_RECORD } from './vercel.js';
11
12
  function cleanDnsValue(value) {
@@ -118,10 +119,12 @@ async function resolveZone(provider, zoneDomain) {
118
119
  async function providerConnectionDetails() {
119
120
  return (await listProviderStatuses({ verify: false })).map((provider) => ({
120
121
  configured: provider.configured,
122
+ account: provider.account,
121
123
  default: provider.default,
122
124
  displayName: provider.displayName,
123
125
  docsUrl: provider.docsUrl,
124
126
  id: provider.id,
127
+ isDefaultAccount: provider.isDefaultAccount,
125
128
  }));
126
129
  }
127
130
  function resolveRequestedDomain(opts) {
@@ -155,51 +158,91 @@ function targetFromZone(fullDomain, zoneDomain) {
155
158
  }
156
159
  function candidateDetails(candidates) {
157
160
  return candidates.map((candidate) => ({
161
+ account: candidate.account,
162
+ isDefaultAccount: candidate.isDefaultAccount,
158
163
  provider: candidate.provider,
159
164
  providerName: candidate.providerName,
160
165
  zoneDomain: candidate.zone.name,
161
166
  }));
162
167
  }
163
- async function loadProviderZones(definition) {
164
- const provider = await createProvider(definition.id);
168
+ function defaultAccountRef(providerId) {
169
+ return { account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId };
170
+ }
171
+ function explicitAccountRef(providerId, account) {
172
+ const normalized = normalizeProviderAccount(account);
173
+ return { account: normalized, isDefaultAccount: isDefaultProviderAccount(normalized), providerId };
174
+ }
175
+ async function loadProviderZones(definition, account) {
176
+ const provider = await createProvider(definition.id, { account: account.account });
165
177
  const zones = await provider.listZones();
166
178
  return {
167
- candidates: zones.map((zone) => ({ provider: definition.id, providerName: definition.displayName, zone })),
168
- search: { displayName: definition.displayName, id: definition.id, zones: zones.map((zone) => zone.name) },
179
+ candidates: zones.map((zone) => ({
180
+ account: account.account,
181
+ isDefaultAccount: account.isDefaultAccount,
182
+ provider: definition.id,
183
+ providerName: definition.displayName,
184
+ zone,
185
+ })),
186
+ search: {
187
+ account: account.account,
188
+ displayName: definition.displayName,
189
+ id: definition.id,
190
+ isDefaultAccount: account.isDefaultAccount,
191
+ zones: zones.map((zone) => zone.name),
192
+ },
169
193
  };
170
194
  }
171
- async function loadConfiguredProviderZones(providerId) {
195
+ async function loadConfiguredProviderZones(providerId, accountInput) {
196
+ const config = await loadConfig();
197
+ const account = accountInput ? normalizeProviderAccount(accountInput) : undefined;
172
198
  if (providerId) {
173
199
  const definition = getProviderDefinition(providerId);
174
- const result = await loadProviderZones(definition);
175
- return { candidates: result.candidates, providerInferred: false, searched: [result.search] };
200
+ const accounts = account ? [explicitAccountRef(definition.id, account)] : listConfiguredProviderAccounts(config, definition);
201
+ const selectedAccounts = accounts.length > 0 ? accounts : [defaultAccountRef(definition.id)];
202
+ const results = await Promise.all(selectedAccounts.map((ref) => loadProviderZones(definition, ref)));
203
+ return {
204
+ accountInferred: account === undefined,
205
+ candidates: results.flatMap((result) => result.candidates),
206
+ providerInferred: false,
207
+ searched: results.map((result) => result.search),
208
+ };
176
209
  }
177
- const config = await loadConfig();
178
- const definitions = listProviderDefinitions().filter((definition) => isProviderConfigured(definition, config));
179
- if (definitions.length === 0) {
180
- throw new DoomainError('CONFIG_NOT_FOUND', 'No DNS provider is configured. Run `doomain providers connect` first.', {
210
+ const providerAccounts = listProviderDefinitions().flatMap((definition) => listConfiguredProviderAccounts(config, definition)
211
+ .filter((ref) => !account || ref.account === account)
212
+ .map((ref) => ({ definition, ref })));
213
+ if (providerAccounts.length === 0) {
214
+ const message = account
215
+ ? `No DNS provider account named ${account} is configured. Run \`doomain providers connect <provider> --account ${account}\` first.`
216
+ : 'No DNS provider is configured. Run `doomain providers connect` first.';
217
+ throw new DoomainError('CONFIG_NOT_FOUND', message, {
218
+ account,
181
219
  configuredProviders: await providerConnectionDetails(),
182
220
  recovery: 'Connect the DNS provider that owns this domain, then retry `doomain link <domain> --json`.',
183
- suggestedCommands: ['doomain providers connect', 'doomain link <domain> --json'],
221
+ suggestedCommands: account
222
+ ? [`doomain providers connect <provider> --account ${account}`, 'doomain link <domain> --json']
223
+ : ['doomain providers connect', 'doomain link <domain> --json'],
184
224
  });
185
225
  }
186
- const results = await Promise.all(definitions.map(async (definition) => {
226
+ const results = await Promise.all(providerAccounts.map(async ({ definition, ref }) => {
187
227
  try {
188
- return await loadProviderZones(definition);
228
+ return await loadProviderZones(definition, ref);
189
229
  }
190
230
  catch (error) {
191
231
  return {
192
232
  candidates: [],
193
233
  search: {
234
+ account: ref.account,
194
235
  displayName: definition.displayName,
195
236
  error: error instanceof Error ? error.message : String(error),
196
237
  id: definition.id,
238
+ isDefaultAccount: ref.isDefaultAccount,
197
239
  zones: [],
198
240
  },
199
241
  };
200
242
  }
201
243
  }));
202
244
  return {
245
+ accountInferred: account === undefined,
203
246
  candidates: results.flatMap((result) => result.candidates),
204
247
  providerInferred: true,
205
248
  searched: results.map((result) => result.search),
@@ -211,30 +254,35 @@ async function resolveProviderTarget(input) {
211
254
  domain: await resolveConfiguredDomain(input.domain),
212
255
  subdomain: input.subdomain,
213
256
  });
214
- const zones = await loadConfiguredProviderZones(input.provider);
257
+ const zones = await loadConfiguredProviderZones(input.provider, input.account);
215
258
  const matches = zones.candidates
216
259
  .filter((candidate) => zoneMatchesDomain(requested.fullDomain, candidate.zone.name, requested.forceExactZone))
217
260
  .sort((a, b) => b.zone.name.length - a.zone.name.length);
218
261
  if (matches.length === 0) {
262
+ const account = input.account ? normalizeProviderAccount(input.account) : undefined;
219
263
  const providerMessage = input.provider
220
- ? `${getProviderDefinition(input.provider).displayName} does not have a matching DNS zone for ${requested.fullDomain}.`
264
+ ? `${getProviderDefinition(input.provider).displayName}${account ? ` account ${account}` : ''} does not have a matching DNS zone for ${requested.fullDomain}.`
221
265
  : `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
222
266
  throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, {
267
+ account,
223
268
  configuredProviders: await providerConnectionDetails(),
224
269
  domain: requested.fullDomain,
225
- recovery: 'Retry with --provider <id> only if another configured provider owns this zone. Otherwise connect the DNS provider that owns this domain.',
270
+ 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.',
226
271
  searchedZones: zones.searched,
227
- suggestedCommands: [`doomain link ${requested.fullDomain} --provider <id> --json`, 'doomain providers connect'],
272
+ suggestedCommands: [`doomain link ${requested.fullDomain} --provider <id> --account <alias> --json`, 'doomain providers connect'],
228
273
  });
229
274
  }
230
275
  const bestLength = matches[0].zone.name.length;
231
276
  const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
232
- const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider && item.zone.name === candidate.zone.name) === index);
277
+ 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);
233
278
  if (uniqueBestMatches.length > 1) {
234
- throw new DoomainError('PROVIDER_ZONE_AMBIGUOUS', `Multiple DNS providers have a matching DNS zone for ${requested.fullDomain}. Pass --provider to choose one.`, { candidates: candidateDetails(uniqueBestMatches), domain: requested.fullDomain });
279
+ 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 });
235
280
  }
236
281
  const selected = uniqueBestMatches[0];
237
282
  return {
283
+ account: selected.account,
284
+ accountInferred: zones.accountInferred,
285
+ isDefaultAccount: selected.isDefaultAccount,
238
286
  provider: selected.provider,
239
287
  providerInferred: zones.providerInferred,
240
288
  target: targetFromZone(requested.fullDomain, selected.zone.name),
@@ -414,9 +462,12 @@ function reportProgress(input, stage, message) {
414
462
  export async function createLinkPlan(input) {
415
463
  const project = await resolveProject(input.project);
416
464
  const resolved = await resolveProviderTarget(input);
417
- const { provider, providerInferred, target } = resolved;
465
+ const { account, accountInferred, isDefaultAccount, provider, providerInferred, target } = resolved;
418
466
  const record = planBaseRecord({ isApex: target.isApex, provider, recordName: target.recordName });
419
467
  return {
468
+ account,
469
+ accountInferred,
470
+ isDefaultAccount,
420
471
  provider,
421
472
  providerInferred,
422
473
  project: project.project,
@@ -441,7 +492,7 @@ export async function linkDomain(input) {
441
492
  };
442
493
  }
443
494
  const vercel = createVercelClient(await resolveVercelConfig());
444
- const provider = await createProvider(plan.provider);
495
+ const provider = await createProvider(plan.provider, { account: plan.account });
445
496
  reportProgress(input, 'dns:resolve-zone', `Finding ${provider.name} DNS zone`);
446
497
  const zone = await resolveZone(provider, plan.zoneDomain);
447
498
  reportProgress(input, 'vercel:add-domain', 'Adding domain to Vercel');
@@ -1,5 +1,18 @@
1
1
  import { type DoomainConfig } from '../../config.js';
2
2
  import type { CredentialDefinition, DnsProviderDefinition, ProviderContext } from './types.js';
3
- export declare function getProviderCredentials(config: DoomainConfig, providerId: string): Record<string, string>;
4
- export declare function getProviderCredential(config: DoomainConfig, providerId: string, credential: CredentialDefinition): string | undefined;
5
- export declare function createProviderContext(definition: DnsProviderDefinition): Promise<ProviderContext>;
3
+ export declare const DEFAULT_PROVIDER_ACCOUNT = "default";
4
+ export interface ProviderAccountRef {
5
+ account: string;
6
+ isDefaultAccount: boolean;
7
+ providerId: string;
8
+ }
9
+ export interface ProviderAccountOptions {
10
+ account?: string;
11
+ }
12
+ export declare function normalizeProviderAccount(account?: string): string;
13
+ export declare function isDefaultProviderAccount(account?: string): boolean;
14
+ export declare function getProviderCredentials(config: DoomainConfig, providerId: string, opts?: ProviderAccountOptions): Record<string, string>;
15
+ export declare function getProviderCredential(config: DoomainConfig, providerId: string, credential: CredentialDefinition, opts?: ProviderAccountOptions): string | undefined;
16
+ export declare function isProviderAccountConfigured(definition: DnsProviderDefinition, config: DoomainConfig, opts?: ProviderAccountOptions): boolean;
17
+ export declare function listConfiguredProviderAccounts(config: DoomainConfig, definition: DnsProviderDefinition): ProviderAccountRef[];
18
+ export declare function createProviderContext(definition: DnsProviderDefinition, opts?: ProviderAccountOptions): Promise<ProviderContext>;
@@ -1,5 +1,19 @@
1
1
  import { loadConfig } from '../../config.js';
2
2
  import { DoomainError } from '../../errors.js';
3
+ import { ensureProviderAccount } from '../../validate.js';
4
+ export const DEFAULT_PROVIDER_ACCOUNT = 'default';
5
+ export function normalizeProviderAccount(account) {
6
+ if (!account?.trim())
7
+ return DEFAULT_PROVIDER_ACCOUNT;
8
+ return ensureProviderAccount(account);
9
+ }
10
+ export function isDefaultProviderAccount(account) {
11
+ return normalizeProviderAccount(account) === DEFAULT_PROVIDER_ACCOUNT;
12
+ }
13
+ function providerConfig(config, providerId) {
14
+ const value = config.providers?.[providerId];
15
+ return value && typeof value === 'object' ? value : undefined;
16
+ }
3
17
  function legacyCredential(config, providerId, key) {
4
18
  if (providerId !== 'spaceship')
5
19
  return undefined;
@@ -8,25 +22,53 @@ function legacyCredential(config, providerId, key) {
8
22
  return undefined;
9
23
  return legacy[key];
10
24
  }
11
- export function getProviderCredentials(config, providerId) {
12
- const providerConfig = config.providers?.[providerId];
13
- const credentials = providerConfig && typeof providerConfig === 'object' && 'credentials' in providerConfig
14
- ? (providerConfig.credentials ?? {})
15
- : {};
16
- return { ...credentials };
25
+ function credentialFromSavedConfig(config, providerId, account, key) {
26
+ const credentials = getProviderCredentials(config, providerId, { account });
27
+ if (credentials[key])
28
+ return credentials[key];
29
+ if (account === DEFAULT_PROVIDER_ACCOUNT)
30
+ return legacyCredential(config, providerId, key);
31
+ return undefined;
32
+ }
33
+ export function getProviderCredentials(config, providerId, opts = {}) {
34
+ const account = normalizeProviderAccount(opts.account);
35
+ const current = providerConfig(config, providerId);
36
+ const credentials = account === DEFAULT_PROVIDER_ACCOUNT ? current?.credentials : current?.accounts?.[account]?.credentials;
37
+ return { ...(credentials ?? {}) };
17
38
  }
18
- export function getProviderCredential(config, providerId, credential) {
19
- return process.env[credential.env] || getProviderCredentials(config, providerId)[credential.key] || legacyCredential(config, providerId, credential.key);
39
+ export function getProviderCredential(config, providerId, credential, opts = {}) {
40
+ const account = normalizeProviderAccount(opts.account);
41
+ return process.env[credential.env] || credentialFromSavedConfig(config, providerId, account, credential.key);
42
+ }
43
+ export function isProviderAccountConfigured(definition, config, opts = {}) {
44
+ return definition.credentials.every((credential) => credential.required === false || Boolean(getProviderCredential(config, definition.id, credential, opts)));
45
+ }
46
+ export function listConfiguredProviderAccounts(config, definition) {
47
+ const accounts = [];
48
+ if (isProviderAccountConfigured(definition, config, { account: DEFAULT_PROVIDER_ACCOUNT })) {
49
+ accounts.push({ account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: definition.id });
50
+ }
51
+ for (const account of Object.keys(providerConfig(config, definition.id)?.accounts ?? {}).sort()) {
52
+ const normalized = normalizeProviderAccount(account);
53
+ if (normalized === DEFAULT_PROVIDER_ACCOUNT)
54
+ continue;
55
+ if (!isProviderAccountConfigured(definition, config, { account: normalized }))
56
+ continue;
57
+ accounts.push({ account: normalized, isDefaultAccount: false, providerId: definition.id });
58
+ }
59
+ return accounts;
20
60
  }
21
- export async function createProviderContext(definition) {
61
+ export async function createProviderContext(definition, opts = {}) {
22
62
  const config = await loadConfig();
23
63
  const credentials = {};
64
+ const account = normalizeProviderAccount(opts.account);
24
65
  for (const credential of definition.credentials) {
25
- const value = getProviderCredential(config, definition.id, credential);
66
+ const value = getProviderCredential(config, definition.id, credential, { account });
26
67
  if (value)
27
68
  credentials[credential.key] = value;
28
69
  else if (credential.required !== false) {
29
- throw new DoomainError('MISSING_CREDENTIALS', `Missing ${definition.displayName} ${credential.label}. Run \`doomain providers connect ${definition.id}\` or set ${credential.env}.`);
70
+ const accountHint = account === DEFAULT_PROVIDER_ACCOUNT ? '' : ` for account ${account}`;
71
+ throw new DoomainError('MISSING_CREDENTIALS', `Missing ${definition.displayName} ${credential.label}${accountHint}. Run \`doomain providers connect ${definition.id}${account === DEFAULT_PROVIDER_ACCOUNT ? '' : ` --account ${account}`}\` or set ${credential.env}.`, { account, provider: definition.id });
30
72
  }
31
73
  }
32
74
  return { credentials, debug: process.env.DOOMAIN_DEBUG === '1' };
@@ -1,4 +1,6 @@
1
1
  import type { DnsProvider, DnsProviderDefinition } from './types.js';
2
2
  export declare function listProviderDefinitions(): DnsProviderDefinition[];
3
3
  export declare function getProviderDefinition(id: string): DnsProviderDefinition;
4
- export declare function createProvider(id: string): Promise<DnsProvider>;
4
+ export declare function createProvider(id: string, opts?: {
5
+ account?: string;
6
+ }): Promise<DnsProvider>;
@@ -16,7 +16,7 @@ export function getProviderDefinition(id) {
16
16
  throw new DoomainError('PROVIDER_NOT_FOUND', `Unsupported DNS provider: ${id}`);
17
17
  return definition;
18
18
  }
19
- export async function createProvider(id) {
19
+ export async function createProvider(id, opts = {}) {
20
20
  const definition = getProviderDefinition(id);
21
- return definition.create(await createProviderContext(definition));
21
+ return definition.create(await createProviderContext(definition, opts));
22
22
  }
@@ -1,6 +1,7 @@
1
1
  import { type DoomainConfig } from '../config.js';
2
2
  import type { DnsProviderDefinition } from './types.js';
3
3
  export interface ProviderStatus {
4
+ account: string;
4
5
  configured: boolean;
5
6
  default: boolean;
6
7
  displayName: string;
@@ -8,9 +9,12 @@ export interface ProviderStatus {
8
9
  domainCount?: number;
9
10
  error?: string;
10
11
  id: string;
12
+ isDefaultAccount: boolean;
11
13
  verified?: boolean;
12
14
  }
13
- export declare function isProviderConfigured(definition: DnsProviderDefinition, config: DoomainConfig): boolean;
15
+ export declare function isProviderConfigured(definition: DnsProviderDefinition, config: DoomainConfig, opts?: {
16
+ account?: string;
17
+ }): boolean;
14
18
  export declare function listProviderStatuses(opts?: {
15
19
  verify?: boolean;
16
20
  }): Promise<ProviderStatus[]>;
@@ -1,33 +1,40 @@
1
1
  import { loadConfig } from '../config.js';
2
- import { getProviderCredential } from './core/config.js';
2
+ import { DEFAULT_PROVIDER_ACCOUNT, isProviderAccountConfigured, listConfiguredProviderAccounts, normalizeProviderAccount, } from './core/config.js';
3
3
  import { createProvider, listProviderDefinitions } from './registry.js';
4
- export function isProviderConfigured(definition, config) {
5
- return definition.credentials.every((credential) => credential.required === false || Boolean(getProviderCredential(config, definition.id, credential)));
4
+ export function isProviderConfigured(definition, config, opts = {}) {
5
+ return isProviderAccountConfigured(definition, config, opts);
6
6
  }
7
7
  export async function listProviderStatuses(opts = {}) {
8
8
  const config = await loadConfig();
9
9
  const statuses = [];
10
10
  for (const definition of listProviderDefinitions()) {
11
- const configured = isProviderConfigured(definition, config);
12
- const status = {
13
- configured,
14
- default: config.defaults?.provider === definition.id,
15
- displayName: definition.displayName,
16
- docsUrl: definition.docsUrl,
17
- id: definition.id,
18
- };
19
- if (configured && opts.verify) {
20
- try {
21
- const zones = await (await createProvider(definition.id)).listZones();
22
- status.domainCount = zones.length;
23
- status.verified = true;
24
- }
25
- catch (error) {
26
- status.error = error instanceof Error ? error.message : String(error);
27
- status.verified = false;
11
+ const accounts = listConfiguredProviderAccounts(config, definition);
12
+ const refs = accounts.length > 0 ? accounts : [{ account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: definition.id }];
13
+ for (const ref of refs) {
14
+ const account = normalizeProviderAccount(ref.account);
15
+ const configured = accounts.some((item) => item.account === account);
16
+ const status = {
17
+ account,
18
+ configured,
19
+ default: config.defaults?.provider === definition.id,
20
+ displayName: definition.displayName,
21
+ docsUrl: definition.docsUrl,
22
+ id: definition.id,
23
+ isDefaultAccount: ref.isDefaultAccount,
24
+ };
25
+ if (configured && opts.verify) {
26
+ try {
27
+ const zones = await (await createProvider(definition.id, { account })).listZones();
28
+ status.domainCount = zones.length;
29
+ status.verified = true;
30
+ }
31
+ catch (error) {
32
+ status.error = error instanceof Error ? error.message : String(error);
33
+ status.verified = false;
34
+ }
28
35
  }
36
+ statuses.push(status);
29
37
  }
30
- statuses.push(status);
31
38
  }
32
39
  return statuses;
33
40
  }
@@ -12,4 +12,5 @@ export declare function resolveDomainTarget(opts: {
12
12
  apex?: boolean;
13
13
  }): DomainTarget;
14
14
  export declare function ensureProviderId(value: string): string;
15
+ export declare function ensureProviderAccount(value: string): string;
15
16
  export declare function ensureProject(value?: string): string;
@@ -54,6 +54,13 @@ export function ensureProviderId(value) {
54
54
  }
55
55
  return provider;
56
56
  }
57
+ export function ensureProviderAccount(value) {
58
+ const account = value.trim().toLowerCase();
59
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(account)) {
60
+ throw new DoomainError('INVALID_INPUT', `Invalid provider account alias: ${value}`);
61
+ }
62
+ return account;
63
+ }
57
64
  export function ensureProject(value) {
58
65
  const project = value?.trim();
59
66
  if (!project)
@@ -15,9 +15,17 @@
15
15
  "<%= config.bin %> <%= command.id %> app.example.com --project my-app --json",
16
16
  "<%= config.bin %> <%= command.id %> --domain app.example.com --project my-app --json",
17
17
  "<%= config.bin %> <%= command.id %> --domain example.com --subdomain app --project my-app",
18
- "<%= config.bin %> <%= command.id %> --provider spaceship --domain example.com --apex --project my-app --json"
18
+ "<%= config.bin %> <%= command.id %> --provider spaceship --domain example.com --apex --project my-app --json",
19
+ "<%= config.bin %> <%= command.id %> app.example.com --provider spaceship --account work --project my-app --json"
19
20
  ],
20
21
  "flags": {
22
+ "account": {
23
+ "description": "DNS provider account alias. Defaults to the provider default account.",
24
+ "name": "account",
25
+ "hasDynamicHelp": false,
26
+ "multiple": false,
27
+ "type": "option"
28
+ },
21
29
  "apex": {
22
30
  "description": "Use the root/apex domain instead of a subdomain.",
23
31
  "name": "apex",
@@ -264,6 +272,13 @@
264
272
  "args": {},
265
273
  "description": "List DNS zones and records for a provider.",
266
274
  "flags": {
275
+ "account": {
276
+ "description": "DNS provider account alias. Defaults to the provider default account.",
277
+ "name": "account",
278
+ "hasDynamicHelp": false,
279
+ "multiple": false,
280
+ "type": "option"
281
+ },
267
282
  "domain": {
268
283
  "description": "Target domain or base zone, for example app.example.com or example.com.",
269
284
  "name": "domain",
@@ -301,6 +316,41 @@
301
316
  "list.js"
302
317
  ]
303
318
  },
319
+ "projects:list": {
320
+ "aliases": [],
321
+ "args": {},
322
+ "description": "List Vercel projects.",
323
+ "flags": {
324
+ "json": {
325
+ "description": "Output a single JSON object and never prompt.",
326
+ "name": "json",
327
+ "allowNo": false,
328
+ "type": "boolean"
329
+ },
330
+ "search": {
331
+ "description": "Filter projects by search term.",
332
+ "name": "search",
333
+ "hasDynamicHelp": false,
334
+ "multiple": false,
335
+ "type": "option"
336
+ }
337
+ },
338
+ "hasDynamicHelp": false,
339
+ "hiddenAliases": [],
340
+ "id": "projects:list",
341
+ "pluginAlias": "doomain",
342
+ "pluginName": "doomain",
343
+ "pluginType": "core",
344
+ "strict": true,
345
+ "enableJsonFlag": false,
346
+ "isESM": true,
347
+ "relativePath": [
348
+ "dist",
349
+ "commands",
350
+ "projects",
351
+ "list.js"
352
+ ]
353
+ },
304
354
  "providers:add": {
305
355
  "aliases": [],
306
356
  "args": {
@@ -312,6 +362,13 @@
312
362
  },
313
363
  "description": "Save DNS provider credentials locally.",
314
364
  "flags": {
365
+ "account": {
366
+ "description": "DNS provider account alias. Defaults to the provider default account.",
367
+ "name": "account",
368
+ "hasDynamicHelp": false,
369
+ "multiple": false,
370
+ "type": "option"
371
+ },
315
372
  "api-key": {
316
373
  "description": "Compatibility alias for Spaceship apiKey.",
317
374
  "name": "api-key",
@@ -374,6 +431,13 @@
374
431
  },
375
432
  "description": "Save DNS provider credentials locally.",
376
433
  "flags": {
434
+ "account": {
435
+ "description": "DNS provider account alias. Defaults to the provider default account.",
436
+ "name": "account",
437
+ "hasDynamicHelp": false,
438
+ "multiple": false,
439
+ "type": "option"
440
+ },
377
441
  "api-key": {
378
442
  "description": "Compatibility alias for Spaceship apiKey.",
379
443
  "name": "api-key",
@@ -442,6 +506,13 @@
442
506
  "<%= config.bin %> <%= command.id %> cloudflare --json"
443
507
  ],
444
508
  "flags": {
509
+ "account": {
510
+ "description": "DNS provider account alias. Defaults to the provider default account.",
511
+ "name": "account",
512
+ "hasDynamicHelp": false,
513
+ "multiple": false,
514
+ "type": "option"
515
+ },
445
516
  "json": {
446
517
  "description": "Output a single JSON object and never prompt.",
447
518
  "name": "json",
@@ -538,6 +609,13 @@
538
609
  },
539
610
  "description": "Verify saved DNS provider credentials.",
540
611
  "flags": {
612
+ "account": {
613
+ "description": "DNS provider account alias. Defaults to the provider default account.",
614
+ "name": "account",
615
+ "hasDynamicHelp": false,
616
+ "multiple": false,
617
+ "type": "option"
618
+ },
541
619
  "json": {
542
620
  "description": "Output a single JSON object and never prompt.",
543
621
  "name": "json",
@@ -561,41 +639,6 @@
561
639
  "verify.js"
562
640
  ]
563
641
  },
564
- "projects:list": {
565
- "aliases": [],
566
- "args": {},
567
- "description": "List Vercel projects.",
568
- "flags": {
569
- "json": {
570
- "description": "Output a single JSON object and never prompt.",
571
- "name": "json",
572
- "allowNo": false,
573
- "type": "boolean"
574
- },
575
- "search": {
576
- "description": "Filter projects by search term.",
577
- "name": "search",
578
- "hasDynamicHelp": false,
579
- "multiple": false,
580
- "type": "option"
581
- }
582
- },
583
- "hasDynamicHelp": false,
584
- "hiddenAliases": [],
585
- "id": "projects:list",
586
- "pluginAlias": "doomain",
587
- "pluginName": "doomain",
588
- "pluginType": "core",
589
- "strict": true,
590
- "enableJsonFlag": false,
591
- "isESM": true,
592
- "relativePath": [
593
- "dist",
594
- "commands",
595
- "projects",
596
- "list.js"
597
- ]
598
- },
599
642
  "auth:logout:vercel": {
600
643
  "aliases": [],
601
644
  "args": {},
@@ -630,5 +673,5 @@
630
673
  ]
631
674
  }
632
675
  },
633
- "version": "0.1.10"
676
+ "version": "0.1.11"
634
677
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "doomain",
3
3
  "description": "Link your vercel project and domain in seconds",
4
- "version": "0.1.10",
4
+ "version": "0.1.11",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {