doomain 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +293 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +8 -0
- package/dist/commands/auth/logout/vercel.d.ts +9 -0
- package/dist/commands/auth/logout/vercel.js +35 -0
- package/dist/commands/auth/vercel.d.ts +10 -0
- package/dist/commands/auth/vercel.js +69 -0
- package/dist/commands/domains/list.d.ts +10 -0
- package/dist/commands/domains/list.js +36 -0
- package/dist/commands/link.d.ts +21 -0
- package/dist/commands/link.js +63 -0
- package/dist/commands/projects/list.d.ts +9 -0
- package/dist/commands/projects/list.js +26 -0
- package/dist/commands/providers/add.d.ts +1 -0
- package/dist/commands/providers/add.js +1 -0
- package/dist/commands/providers/connect.d.ts +15 -0
- package/dist/commands/providers/connect.js +183 -0
- package/dist/commands/providers/disconnect.d.ts +13 -0
- package/dist/commands/providers/disconnect.js +52 -0
- package/dist/commands/providers/list.d.ts +8 -0
- package/dist/commands/providers/list.js +25 -0
- package/dist/commands/providers/status.d.ts +9 -0
- package/dist/commands/providers/status.js +31 -0
- package/dist/commands/providers/verify.d.ts +11 -0
- package/dist/commands/providers/verify.js +27 -0
- package/dist/commands/schema.d.ts +11 -0
- package/dist/commands/schema.js +29 -0
- package/dist/commands/verify.d.ts +12 -0
- package/dist/commands/verify.js +36 -0
- package/dist/commands/wizard.d.ts +9 -0
- package/dist/commands/wizard.js +322 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/command-schema.d.ts +14 -0
- package/dist/lib/command-schema.js +99 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +49 -0
- package/dist/lib/errors.d.ts +7 -0
- package/dist/lib/errors.js +17 -0
- package/dist/lib/flags.d.ts +6 -0
- package/dist/lib/flags.js +10 -0
- package/dist/lib/link-domain.d.ts +47 -0
- package/dist/lib/link-domain.js +336 -0
- package/dist/lib/local-vercel.d.ts +6 -0
- package/dist/lib/local-vercel.js +27 -0
- package/dist/lib/output.d.ts +36 -0
- package/dist/lib/output.js +51 -0
- package/dist/lib/providers/cloudflare/index.d.ts +26 -0
- package/dist/lib/providers/cloudflare/index.js +219 -0
- package/dist/lib/providers/core/config.d.ts +5 -0
- package/dist/lib/providers/core/config.js +33 -0
- package/dist/lib/providers/core/errors.d.ts +6 -0
- package/dist/lib/providers/core/errors.js +18 -0
- package/dist/lib/providers/core/http.d.ts +17 -0
- package/dist/lib/providers/core/http.js +43 -0
- package/dist/lib/providers/core/pagination.d.ts +12 -0
- package/dist/lib/providers/core/pagination.js +15 -0
- package/dist/lib/providers/core/planner.d.ts +19 -0
- package/dist/lib/providers/core/planner.js +79 -0
- package/dist/lib/providers/core/types.d.ts +123 -0
- package/dist/lib/providers/core/types.js +1 -0
- package/dist/lib/providers/namecheap/index.d.ts +28 -0
- package/dist/lib/providers/namecheap/index.js +289 -0
- package/dist/lib/providers/registry.d.ts +4 -0
- package/dist/lib/providers/registry.js +21 -0
- package/dist/lib/providers/spaceship/index.d.ts +22 -0
- package/dist/lib/providers/spaceship/index.js +148 -0
- package/dist/lib/providers/status.d.ts +16 -0
- package/dist/lib/providers/status.js +33 -0
- package/dist/lib/providers/types.d.ts +1 -0
- package/dist/lib/providers/types.js +1 -0
- package/dist/lib/validate.d.ts +15 -0
- package/dist/lib/validate.js +62 -0
- package/dist/lib/vercel.d.ts +32 -0
- package/dist/lib/vercel.js +131 -0
- package/oclif.manifest.json +633 -0
- package/package.json +86 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import { loadConfig, updateConfig } from '../lib/config.js';
|
|
4
|
+
import { DoomainError } from '../lib/errors.js';
|
|
5
|
+
import { jsonFlag } from '../lib/flags.js';
|
|
6
|
+
import { createLinkPlan, linkDomain } from '../lib/link-domain.js';
|
|
7
|
+
import { detectLocalVercelProject } from '../lib/local-vercel.js';
|
|
8
|
+
import { createOutput, outputError } from '../lib/output.js';
|
|
9
|
+
import { createProvider, getProviderDefinition, listProviderDefinitions } from '../lib/providers/registry.js';
|
|
10
|
+
import { isProviderConfigured } from '../lib/providers/status.js';
|
|
11
|
+
import { createVercelClient } from '../lib/vercel.js';
|
|
12
|
+
const PERSONAL_ACCOUNT = '__personal__';
|
|
13
|
+
function cancelIfNeeded(value) {
|
|
14
|
+
if (p.isCancel(value)) {
|
|
15
|
+
p.cancel('Cancelled');
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
async function promptRequired(message, opts = {}) {
|
|
21
|
+
const value = opts.password
|
|
22
|
+
? await p.password({ message })
|
|
23
|
+
: await p.text({ message, placeholder: opts.placeholder, validate: (input) => (input?.trim() ? undefined : 'Required') });
|
|
24
|
+
const resolved = cancelIfNeeded(value);
|
|
25
|
+
return typeof resolved === 'string' ? resolved.trim() : null;
|
|
26
|
+
}
|
|
27
|
+
async function fetchPublicIp() {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch('https://api.ipify.org', { signal: controller.signal });
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
return undefined;
|
|
34
|
+
const ip = (await response.text()).trim();
|
|
35
|
+
return ip || undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function credentialInitialValue(credential) {
|
|
45
|
+
if (credential.key !== 'clientIp')
|
|
46
|
+
return undefined;
|
|
47
|
+
return fetchPublicIp();
|
|
48
|
+
}
|
|
49
|
+
async function promptCredential(credential) {
|
|
50
|
+
const initialValue = credential.secret ? undefined : await credentialInitialValue(credential);
|
|
51
|
+
const value = credential.secret
|
|
52
|
+
? await p.password({ message: credential.label })
|
|
53
|
+
: await p.text({ message: credential.label, initialValue, placeholder: credential.placeholder ?? credential.hint });
|
|
54
|
+
const resolved = cancelIfNeeded(value);
|
|
55
|
+
return typeof resolved === 'string' && resolved.trim() ? resolved.trim() : null;
|
|
56
|
+
}
|
|
57
|
+
async function promptProviderCredentials(definition) {
|
|
58
|
+
const credentials = {};
|
|
59
|
+
for (const credential of definition.credentials) {
|
|
60
|
+
if (credential.required === false)
|
|
61
|
+
continue;
|
|
62
|
+
const value = await promptCredential(credential);
|
|
63
|
+
if (!value)
|
|
64
|
+
return null;
|
|
65
|
+
credentials[credential.key] = value;
|
|
66
|
+
}
|
|
67
|
+
return credentials;
|
|
68
|
+
}
|
|
69
|
+
async function promptProviderDefinition(definitions, config) {
|
|
70
|
+
const selected = await p.select({
|
|
71
|
+
message: 'Choose DNS provider',
|
|
72
|
+
options: definitions.map((definition) => ({
|
|
73
|
+
hint: isProviderConfigured(definition, config) ? 'Connected' : 'Not connected',
|
|
74
|
+
label: definition.displayName,
|
|
75
|
+
value: definition.id,
|
|
76
|
+
})),
|
|
77
|
+
});
|
|
78
|
+
const selectedId = cancelIfNeeded(selected);
|
|
79
|
+
if (selectedId === null)
|
|
80
|
+
return null;
|
|
81
|
+
return getProviderDefinition(selectedId);
|
|
82
|
+
}
|
|
83
|
+
function showProviderSetup(definition) {
|
|
84
|
+
if (!definition.setup?.notes?.length)
|
|
85
|
+
return;
|
|
86
|
+
p.note(definition.setup.notes.join('\n'), `${definition.displayName} setup`);
|
|
87
|
+
}
|
|
88
|
+
async function listProviderDomainOptions(definition) {
|
|
89
|
+
const provider = await createProvider(definition.id);
|
|
90
|
+
const zones = await provider.listZones();
|
|
91
|
+
return zones.map((zone) => toProviderDomainOption(definition, zone));
|
|
92
|
+
}
|
|
93
|
+
function toProviderDomainOption(definition, zone) {
|
|
94
|
+
return {
|
|
95
|
+
domain: zone.name,
|
|
96
|
+
id: `${definition.id}:${zone.name}`,
|
|
97
|
+
providerId: definition.id,
|
|
98
|
+
providerName: definition.displayName,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function projectLabel(project) {
|
|
102
|
+
return project.name ? `${project.name} (${project.id})` : project.id;
|
|
103
|
+
}
|
|
104
|
+
function teamLabel(team) {
|
|
105
|
+
const name = team.name ?? team.slug;
|
|
106
|
+
return `${name} (${team.id})`;
|
|
107
|
+
}
|
|
108
|
+
function recordPreview(record, providerName) {
|
|
109
|
+
const name = record.name === '@' ? 'root' : record.name;
|
|
110
|
+
return `DNS: ${record.type} ${name} -> ${record.value} in ${providerName}`;
|
|
111
|
+
}
|
|
112
|
+
export default class Wizard extends Command {
|
|
113
|
+
static description = 'Interactive Vercel domain linker.';
|
|
114
|
+
static hidden = true;
|
|
115
|
+
static flags = {
|
|
116
|
+
json: jsonFlag,
|
|
117
|
+
};
|
|
118
|
+
async run() {
|
|
119
|
+
const { flags } = await this.parse(Wizard);
|
|
120
|
+
const out = createOutput({ json: flags.json });
|
|
121
|
+
let activeSpinner;
|
|
122
|
+
if (out.json) {
|
|
123
|
+
outputError(true, new DoomainError('MISSING_ARGUMENT', 'The root `doomain` command is interactive. Use `doomain link --json` for agents.'), 'MISSING_ARGUMENT');
|
|
124
|
+
this.exit(1);
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
p.intro('Doomain');
|
|
128
|
+
const config = await loadConfig();
|
|
129
|
+
const providerDefinitions = listProviderDefinitions();
|
|
130
|
+
let vercelToken = process.env.VERCEL_TOKEN || config.vercel?.token;
|
|
131
|
+
let vercelTeamId = process.env.VERCEL_TEAM_ID || config.vercel?.teamId;
|
|
132
|
+
const defaultProvider = process.env.DOOMAIN_PROVIDER || config.defaults?.provider;
|
|
133
|
+
const defaultDomain = process.env.DOOMAIN_DOMAIN || config.defaults?.domain;
|
|
134
|
+
if (!vercelToken) {
|
|
135
|
+
vercelToken = (await promptRequired('Vercel token', { password: true })) ?? undefined;
|
|
136
|
+
if (!vercelToken)
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const localProject = detectLocalVercelProject();
|
|
140
|
+
const teamSpinner = p.spinner();
|
|
141
|
+
let teams = [];
|
|
142
|
+
if (process.env.VERCEL_TEAM_ID) {
|
|
143
|
+
p.log.info(`Using Vercel team ${vercelTeamId} from VERCEL_TEAM_ID.`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
activeSpinner = teamSpinner;
|
|
147
|
+
teamSpinner.start('Loading Vercel teams');
|
|
148
|
+
teams = await createVercelClient({ token: vercelToken }).listTeams();
|
|
149
|
+
teamSpinner.stop(`Loaded ${teams.length} Vercel team${teams.length === 1 ? '' : 's'}`);
|
|
150
|
+
activeSpinner = undefined;
|
|
151
|
+
const selected = await p.select({
|
|
152
|
+
message: 'Select Vercel account/team',
|
|
153
|
+
initialValue: vercelTeamId ?? localProject?.orgId ?? PERSONAL_ACCOUNT,
|
|
154
|
+
options: [
|
|
155
|
+
{ label: 'Personal account', value: PERSONAL_ACCOUNT, hint: 'No team id' },
|
|
156
|
+
...teams.map((team) => ({ label: teamLabel(team), value: team.id, hint: team.role ?? team.slug })),
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
const resolved = cancelIfNeeded(selected);
|
|
160
|
+
if (resolved === null)
|
|
161
|
+
return;
|
|
162
|
+
vercelTeamId = resolved === PERSONAL_ACCOUNT ? undefined : resolved;
|
|
163
|
+
}
|
|
164
|
+
const selectedTeam = teams.find((team) => team.id === vercelTeamId);
|
|
165
|
+
const teamDisplay = vercelTeamId ? teamLabel(selectedTeam ?? { id: vercelTeamId, name: null, role: null, slug: vercelTeamId }) : 'Personal account';
|
|
166
|
+
p.log.success(`Vercel account ready: ${teamDisplay}`);
|
|
167
|
+
const localProjectMatchesTeam = localProject && (vercelTeamId ? localProject.orgId === vercelTeamId : !localProject.orgId);
|
|
168
|
+
let project = localProjectMatchesTeam ? localProject.projectId : undefined;
|
|
169
|
+
let projectDisplay = project ? projectLabel({ id: project }) : '';
|
|
170
|
+
if (project) {
|
|
171
|
+
p.log.info(`Using linked Vercel project ${projectDisplay}.`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const spinner = p.spinner();
|
|
175
|
+
activeSpinner = spinner;
|
|
176
|
+
spinner.start('Loading Vercel projects');
|
|
177
|
+
const projects = await createVercelClient({ token: vercelToken, teamId: vercelTeamId }).listProjects();
|
|
178
|
+
spinner.stop(`Loaded ${projects.length} projects`);
|
|
179
|
+
activeSpinner = undefined;
|
|
180
|
+
if (projects.length === 0) {
|
|
181
|
+
throw new DoomainError('PROJECT_NOT_FOUND', `No Vercel projects found in ${teamDisplay}.`);
|
|
182
|
+
}
|
|
183
|
+
const selected = await p.autocomplete({
|
|
184
|
+
message: 'Select Vercel project',
|
|
185
|
+
placeholder: 'Type to filter projects...',
|
|
186
|
+
maxItems: 10,
|
|
187
|
+
options: projects.map((item) => ({ label: projectLabel(item), value: item.id, hint: item.framework ?? undefined })),
|
|
188
|
+
});
|
|
189
|
+
const resolved = cancelIfNeeded(selected);
|
|
190
|
+
if (resolved === null)
|
|
191
|
+
return;
|
|
192
|
+
project = resolved;
|
|
193
|
+
projectDisplay = projectLabel(projects.find((item) => item.id === project) ?? { id: project });
|
|
194
|
+
}
|
|
195
|
+
p.log.success(`Vercel ready: ${projectDisplay}`);
|
|
196
|
+
const configuredProviderDefinitions = providerDefinitions.filter((definition) => isProviderConfigured(definition, config));
|
|
197
|
+
const providerFailures = [];
|
|
198
|
+
const domainOptions = [];
|
|
199
|
+
if (configuredProviderDefinitions.length === 0) {
|
|
200
|
+
p.log.info('No DNS provider is configured yet. Connect one to continue.');
|
|
201
|
+
const selectedDefinition = await promptProviderDefinition(providerDefinitions, config);
|
|
202
|
+
if (!selectedDefinition)
|
|
203
|
+
return;
|
|
204
|
+
showProviderSetup(selectedDefinition);
|
|
205
|
+
const credentials = await promptProviderCredentials(selectedDefinition);
|
|
206
|
+
if (!credentials)
|
|
207
|
+
return;
|
|
208
|
+
const domainSpinner = p.spinner();
|
|
209
|
+
activeSpinner = domainSpinner;
|
|
210
|
+
domainSpinner.start(`Verifying ${selectedDefinition.displayName} credentials and loading domains`);
|
|
211
|
+
const provider = selectedDefinition.create({ credentials, debug: process.env.DOOMAIN_DEBUG === '1' });
|
|
212
|
+
const zones = await provider.listZones();
|
|
213
|
+
domainSpinner.stop(`Connected ${selectedDefinition.displayName} and loaded ${zones.length} domain${zones.length === 1 ? '' : 's'}`);
|
|
214
|
+
activeSpinner = undefined;
|
|
215
|
+
domainOptions.push(...zones.map((zone) => toProviderDomainOption(selectedDefinition, zone)));
|
|
216
|
+
await updateConfig((current) => ({
|
|
217
|
+
...current,
|
|
218
|
+
defaults: { ...current.defaults, provider: selectedDefinition.id },
|
|
219
|
+
providers: { ...current.providers, [selectedDefinition.id]: { credentials } },
|
|
220
|
+
vercel: { token: vercelToken, teamId: vercelTeamId },
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const domainSpinner = p.spinner();
|
|
225
|
+
activeSpinner = domainSpinner;
|
|
226
|
+
domainSpinner.start(`Loading domains from ${configuredProviderDefinitions.length} provider${configuredProviderDefinitions.length === 1 ? '' : 's'}`);
|
|
227
|
+
for (const definition of configuredProviderDefinitions) {
|
|
228
|
+
try {
|
|
229
|
+
domainOptions.push(...(await listProviderDomainOptions(definition)));
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
providerFailures.push(`${definition.displayName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
domainSpinner.stop(`Loaded ${domainOptions.length} domain${domainOptions.length === 1 ? '' : 's'}`);
|
|
236
|
+
activeSpinner = undefined;
|
|
237
|
+
for (const failure of providerFailures)
|
|
238
|
+
p.log.warning(failure);
|
|
239
|
+
await updateConfig((current) => ({
|
|
240
|
+
...current,
|
|
241
|
+
vercel: { token: vercelToken, teamId: vercelTeamId },
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
if (domainOptions.length === 0) {
|
|
245
|
+
throw new DoomainError('CONFIG_NOT_FOUND', 'No domains found in configured DNS providers. Verify provider credentials and domain permissions.');
|
|
246
|
+
}
|
|
247
|
+
const providerNames = [...new Set(domainOptions.map((option) => option.providerName))].join(', ');
|
|
248
|
+
p.log.success(`DNS ready: ${providerNames} (${domainOptions.length} domains)`);
|
|
249
|
+
let selectedDomain = domainOptions[0];
|
|
250
|
+
if (domainOptions.length > 1) {
|
|
251
|
+
const initialValue = domainOptions.find((option) => option.providerId === defaultProvider && option.domain === defaultDomain)?.id ??
|
|
252
|
+
domainOptions.find((option) => option.domain === defaultDomain)?.id;
|
|
253
|
+
const selectedDomainId = await p.autocomplete({
|
|
254
|
+
message: 'Select domain',
|
|
255
|
+
placeholder: 'Type to filter domains...',
|
|
256
|
+
maxItems: 10,
|
|
257
|
+
initialValue,
|
|
258
|
+
options: domainOptions.map((option) => ({ label: option.domain, value: option.id, hint: option.providerName })),
|
|
259
|
+
});
|
|
260
|
+
const selectedId = cancelIfNeeded(selectedDomainId);
|
|
261
|
+
if (selectedId === null)
|
|
262
|
+
return;
|
|
263
|
+
selectedDomain = domainOptions.find((option) => option.id === selectedId) ?? domainOptions[0];
|
|
264
|
+
}
|
|
265
|
+
p.log.info(`Using domain ${selectedDomain.domain} from ${selectedDomain.providerName}.`);
|
|
266
|
+
const domain = selectedDomain.domain;
|
|
267
|
+
await updateConfig((current) => ({
|
|
268
|
+
...current,
|
|
269
|
+
defaults: { ...current.defaults, domain, provider: selectedDomain.providerId },
|
|
270
|
+
}));
|
|
271
|
+
const mode = await p.select({
|
|
272
|
+
message: 'What should Doomain add?',
|
|
273
|
+
options: [
|
|
274
|
+
{ label: `Subdomain under ${domain}`, value: 'subdomain' },
|
|
275
|
+
{ label: `Root domain (${domain})`, value: 'apex' },
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
const resolvedMode = cancelIfNeeded(mode);
|
|
279
|
+
if (resolvedMode === null)
|
|
280
|
+
return;
|
|
281
|
+
let subdomain;
|
|
282
|
+
const apex = resolvedMode === 'apex';
|
|
283
|
+
if (!apex) {
|
|
284
|
+
subdomain = (await promptRequired('Subdomain', { placeholder: 'app' })) ?? undefined;
|
|
285
|
+
if (!subdomain)
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const fullDomain = apex ? domain : `${subdomain}.${domain}`;
|
|
289
|
+
const preview = await createLinkPlan({ provider: selectedDomain.providerId, domain, subdomain, apex, project });
|
|
290
|
+
p.note([`Vercel: add ${preview.domain} to ${projectDisplay}`, ...preview.records.map((record) => recordPreview(record, selectedDomain.providerName))].join('\n'), 'Preview');
|
|
291
|
+
const confirmed = await p.confirm({
|
|
292
|
+
message: `Link ${fullDomain} via ${selectedDomain.providerName} to Vercel project ${projectDisplay}?`,
|
|
293
|
+
initialValue: true,
|
|
294
|
+
});
|
|
295
|
+
const shouldContinue = cancelIfNeeded(confirmed);
|
|
296
|
+
if (!shouldContinue)
|
|
297
|
+
return;
|
|
298
|
+
const spinner = p.spinner();
|
|
299
|
+
activeSpinner = spinner;
|
|
300
|
+
spinner.start('Adding domain to Vercel');
|
|
301
|
+
const result = await linkDomain({
|
|
302
|
+
provider: selectedDomain.providerId,
|
|
303
|
+
domain,
|
|
304
|
+
subdomain,
|
|
305
|
+
apex,
|
|
306
|
+
project,
|
|
307
|
+
progress: ({ message }) => spinner.message(message),
|
|
308
|
+
wait: true,
|
|
309
|
+
});
|
|
310
|
+
spinner.stop(result.vercel.verified ? 'Domain linked and verified' : 'Domain linked');
|
|
311
|
+
activeSpinner = undefined;
|
|
312
|
+
p.outro(result.vercel.verified
|
|
313
|
+
? `${result.domain} is linked to ${projectDisplay}. SSL certificate provisioning may take a few minutes in Vercel.`
|
|
314
|
+
: `${result.domain} is linked to ${projectDisplay}.`);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
activeSpinner?.error('Operation failed');
|
|
318
|
+
outputError(false, error, 'DOMAIN_LINK_FAILED');
|
|
319
|
+
this.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CommandSchema {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
examples: string[];
|
|
5
|
+
flags: Array<{
|
|
6
|
+
name: string;
|
|
7
|
+
type: 'boolean' | 'integer' | 'string';
|
|
8
|
+
description: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
default?: unknown;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export declare const commandSchemas: CommandSchema[];
|
|
14
|
+
export declare function getCommandSchema(name?: string): CommandSchema[] | CommandSchema | undefined;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const commandSchemas = [
|
|
2
|
+
{
|
|
3
|
+
name: 'link',
|
|
4
|
+
description: 'Link a Vercel project to a domain and create DNS records.',
|
|
5
|
+
examples: [
|
|
6
|
+
'doomain link app.example.com --project my-app --json',
|
|
7
|
+
'doomain link --domain app.example.com --project my-app --json',
|
|
8
|
+
'doomain link --domain example.com --subdomain app --project my-app --json',
|
|
9
|
+
'doomain link --provider spaceship --domain example.com --apex --project my-app --dry-run --json',
|
|
10
|
+
],
|
|
11
|
+
flags: [
|
|
12
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
13
|
+
{ name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
|
|
14
|
+
{ name: 'domain', type: 'string', description: 'Target domain or base zone, for example app.example.com or example.com.' },
|
|
15
|
+
{ name: 'subdomain', type: 'string', description: 'Subdomain to add.' },
|
|
16
|
+
{ name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
|
|
17
|
+
{ name: 'project', type: 'string', description: 'Vercel project id/name.' },
|
|
18
|
+
{ name: 'dry-run', type: 'boolean', description: 'Preview changes without writing.' },
|
|
19
|
+
{ name: 'force', type: 'boolean', description: 'Overwrite conflicting DNS records.' },
|
|
20
|
+
{ name: 'wait', type: 'boolean', description: 'Wait for DNS and Vercel verification.', default: true },
|
|
21
|
+
{ name: 'timeout', type: 'integer', description: 'Wait timeout in seconds.', default: 300 },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'providers connect',
|
|
26
|
+
description: 'Save DNS provider credentials locally after verifying them. Prompts for a provider when omitted.',
|
|
27
|
+
examples: [
|
|
28
|
+
'doomain providers connect',
|
|
29
|
+
'doomain providers connect spaceship --credential apiKey=key --credential apiSecret=secret --json',
|
|
30
|
+
'doomain providers connect namecheap --credential apiUser=user --credential apiKey=key --credential clientIp=127.0.0.1 --json',
|
|
31
|
+
'doomain providers connect cloudflare --credential apiToken=token --credential accountId=account_id --json',
|
|
32
|
+
],
|
|
33
|
+
flags: [
|
|
34
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
35
|
+
{ name: 'credential', type: 'string', description: 'Provider credential as key=value. Can be repeated.' },
|
|
36
|
+
{ name: 'api-key', type: 'string', description: 'Spaceship API key.' },
|
|
37
|
+
{ name: 'api-secret', type: 'string', description: 'Spaceship API secret.' },
|
|
38
|
+
{ name: 'no-verify', type: 'boolean', description: 'Save credentials without verifying them first.' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'providers add',
|
|
43
|
+
description: 'Alias for providers connect.',
|
|
44
|
+
examples: ['doomain providers add', 'doomain providers add namecheap'],
|
|
45
|
+
flags: [
|
|
46
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
47
|
+
{ name: 'credential', type: 'string', description: 'Provider credential as key=value. Can be repeated.' },
|
|
48
|
+
{ name: 'no-verify', type: 'boolean', description: 'Save credentials without verifying them first.' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'providers status',
|
|
53
|
+
description: 'Show configured DNS providers and credential health.',
|
|
54
|
+
examples: ['doomain providers status', 'doomain providers status --no-verify --json'],
|
|
55
|
+
flags: [
|
|
56
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
57
|
+
{ name: 'no-verify', type: 'boolean', description: 'Skip provider API calls.' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'providers disconnect',
|
|
62
|
+
description: 'Remove saved DNS provider credentials locally.',
|
|
63
|
+
examples: ['doomain providers disconnect namecheap --json', 'doomain providers disconnect cloudflare --json'],
|
|
64
|
+
flags: [
|
|
65
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'providers verify',
|
|
70
|
+
description: 'Verify saved DNS provider credentials.',
|
|
71
|
+
examples: ['doomain providers verify spaceship --json', 'doomain providers verify namecheap --json'],
|
|
72
|
+
flags: [
|
|
73
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'auth vercel',
|
|
78
|
+
description: 'Save Vercel credentials locally.',
|
|
79
|
+
examples: ['doomain auth vercel --token token --team-id team_123 --json', 'doomain auth vercel'],
|
|
80
|
+
flags: [
|
|
81
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
82
|
+
{ name: 'token', type: 'string', description: 'Vercel API token.' },
|
|
83
|
+
{ name: 'team-id', type: 'string', description: 'Optional Vercel team id. Interactive mode can fetch and select it.' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'auth logout vercel',
|
|
88
|
+
description: 'Remove saved Vercel credentials locally.',
|
|
89
|
+
examples: ['doomain auth logout vercel --json'],
|
|
90
|
+
flags: [
|
|
91
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
export function getCommandSchema(name) {
|
|
96
|
+
if (!name)
|
|
97
|
+
return commandSchemas;
|
|
98
|
+
return commandSchemas.find((schema) => schema.name === name);
|
|
99
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare function getConfigDir(): string;
|
|
2
|
+
export declare function getConfigFile(): string;
|
|
3
|
+
export declare const CONFIG_DIR: string;
|
|
4
|
+
export declare const CONFIG_FILE: string;
|
|
5
|
+
export interface VercelConfig {
|
|
6
|
+
token?: string;
|
|
7
|
+
teamId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ProviderConfig {
|
|
10
|
+
credentials?: Record<string, string>;
|
|
11
|
+
settings?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface SpaceshipProviderConfig extends ProviderConfig {
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
apiSecret?: string;
|
|
16
|
+
domains?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface DoomainConfig {
|
|
19
|
+
vercel?: VercelConfig;
|
|
20
|
+
providers?: {
|
|
21
|
+
spaceship?: SpaceshipProviderConfig;
|
|
22
|
+
[provider: string]: ProviderConfig | SpaceshipProviderConfig | undefined;
|
|
23
|
+
};
|
|
24
|
+
defaults?: {
|
|
25
|
+
provider?: string;
|
|
26
|
+
domain?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function loadConfig(): Promise<DoomainConfig>;
|
|
30
|
+
export declare function saveConfig(config: DoomainConfig): Promise<void>;
|
|
31
|
+
export declare function updateConfig(updater: (config: DoomainConfig) => DoomainConfig): Promise<DoomainConfig>;
|
|
32
|
+
export declare function clearConfig(): Promise<boolean>;
|
|
33
|
+
export declare function getConfigPath(): string;
|
|
34
|
+
export declare function maskSecret(value?: string): string | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export function getConfigDir() {
|
|
5
|
+
return process.env.DOOMAIN_CONFIG_DIR || join(homedir(), '.doomain');
|
|
6
|
+
}
|
|
7
|
+
export function getConfigFile() {
|
|
8
|
+
return process.env.DOOMAIN_CONFIG_FILE || join(getConfigDir(), 'config.json');
|
|
9
|
+
}
|
|
10
|
+
export const CONFIG_DIR = getConfigDir();
|
|
11
|
+
export const CONFIG_FILE = getConfigFile();
|
|
12
|
+
export async function loadConfig() {
|
|
13
|
+
try {
|
|
14
|
+
const data = await readFile(getConfigFile(), 'utf8');
|
|
15
|
+
return JSON.parse(data);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function saveConfig(config) {
|
|
22
|
+
const configFile = getConfigFile();
|
|
23
|
+
await mkdir(dirname(configFile), { recursive: true });
|
|
24
|
+
await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
export async function updateConfig(updater) {
|
|
27
|
+
const next = updater(await loadConfig());
|
|
28
|
+
await saveConfig(next);
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
export async function clearConfig() {
|
|
32
|
+
try {
|
|
33
|
+
await unlink(getConfigFile());
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function getConfigPath() {
|
|
41
|
+
return getConfigFile();
|
|
42
|
+
}
|
|
43
|
+
export function maskSecret(value) {
|
|
44
|
+
if (!value)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (value.length <= 8)
|
|
47
|
+
return '********';
|
|
48
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'DOMAIN_LINK_FAILED' | '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_PROJECT_NOT_LINKED';
|
|
2
|
+
export declare class DoomainError extends Error {
|
|
3
|
+
readonly code: DoomainErrorCode;
|
|
4
|
+
readonly details?: unknown;
|
|
5
|
+
constructor(code: DoomainErrorCode, message: string, details?: unknown);
|
|
6
|
+
}
|
|
7
|
+
export declare function toDoomainError(error: unknown, fallbackCode: DoomainErrorCode): DoomainError;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class DoomainError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
constructor(code, message, details) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'DoomainError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function toDoomainError(error, fallbackCode) {
|
|
12
|
+
if (error instanceof DoomainError)
|
|
13
|
+
return error;
|
|
14
|
+
if (error instanceof Error)
|
|
15
|
+
return new DoomainError(fallbackCode, error.message);
|
|
16
|
+
return new DoomainError(fallbackCode, String(error));
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const jsonFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
2
|
+
export declare const providerFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
3
|
+
export declare const domainFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
4
|
+
export declare const subdomainFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
|
+
export declare const apexFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
export declare const projectFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
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 domainFlag = Flags.string({ description: 'Target domain or base zone, for example app.example.com or example.com.' });
|
|
5
|
+
export const subdomainFlag = Flags.string({ description: 'Subdomain to add, for example app for app.example.com.' });
|
|
6
|
+
export const apexFlag = Flags.boolean({ description: 'Use the root/apex domain instead of a subdomain.' });
|
|
7
|
+
export const projectFlag = Flags.string({
|
|
8
|
+
char: 'p',
|
|
9
|
+
description: 'Vercel project id/name. Auto-detected from .vercel/project.json when omitted.',
|
|
10
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DnsRecordInput } from './providers/types.js';
|
|
2
|
+
export interface LinkDomainInput {
|
|
3
|
+
provider?: string;
|
|
4
|
+
domain?: string;
|
|
5
|
+
subdomain?: string;
|
|
6
|
+
apex?: boolean;
|
|
7
|
+
project?: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
force?: boolean;
|
|
10
|
+
wait?: boolean;
|
|
11
|
+
timeoutSeconds?: number;
|
|
12
|
+
progress?: LinkDomainProgressCallback;
|
|
13
|
+
}
|
|
14
|
+
export type LinkDomainProgressStage = 'dns:apply' | 'dns:plan' | 'dns:resolve-zone' | 'dns:wait' | 'vercel:add-domain' | 'vercel:get-domain' | 'vercel:get-target' | 'vercel:verify';
|
|
15
|
+
export interface LinkDomainProgress {
|
|
16
|
+
message: string;
|
|
17
|
+
stage: LinkDomainProgressStage;
|
|
18
|
+
}
|
|
19
|
+
export type LinkDomainProgressCallback = (progress: LinkDomainProgress) => void;
|
|
20
|
+
export interface LinkDomainPlan {
|
|
21
|
+
provider: string;
|
|
22
|
+
providerInferred: boolean;
|
|
23
|
+
project: string;
|
|
24
|
+
recordName: string;
|
|
25
|
+
zoneDomain: string;
|
|
26
|
+
domain: string;
|
|
27
|
+
isApex: boolean;
|
|
28
|
+
records: DnsRecordInput[];
|
|
29
|
+
actions: string[];
|
|
30
|
+
localProjectDetected: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface LinkDomainResult extends LinkDomainPlan {
|
|
33
|
+
dryRun: boolean;
|
|
34
|
+
dns: {
|
|
35
|
+
updated: boolean;
|
|
36
|
+
propagated: boolean;
|
|
37
|
+
skipped: DnsRecordInput[];
|
|
38
|
+
};
|
|
39
|
+
vercel: {
|
|
40
|
+
added: boolean;
|
|
41
|
+
alreadyAdded: boolean;
|
|
42
|
+
verified: boolean;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export declare function verificationRecords(raw: unknown, zoneDomain: string): DnsRecordInput[];
|
|
46
|
+
export declare function createLinkPlan(input: LinkDomainInput): Promise<LinkDomainPlan>;
|
|
47
|
+
export declare function linkDomain(input: LinkDomainInput): Promise<LinkDomainResult>;
|