doomain 0.1.0 → 0.1.1
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/dist/commands/link.js +5 -1
- package/dist/commands/wizard.js +28 -29
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/link-domain.js +1 -1
- package/dist/lib/vercel.d.ts +17 -1
- package/dist/lib/vercel.js +50 -8
- package/oclif.manifest.json +2 -2
- package/package.json +1 -1
package/dist/commands/link.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { DoomainError } from '../lib/errors.js';
|
|
2
3
|
import { apexFlag, domainFlag, jsonFlag, projectFlag, providerFlag, subdomainFlag } from '../lib/flags.js';
|
|
3
4
|
import { linkDomain } from '../lib/link-domain.js';
|
|
4
5
|
import { createOutput, outputError } from '../lib/output.js';
|
|
@@ -17,7 +18,7 @@ export default class Link extends Command {
|
|
|
17
18
|
apex: apexFlag,
|
|
18
19
|
domain: domainFlag,
|
|
19
20
|
'dry-run': Flags.boolean({ description: 'Preview changes without writing to Vercel or DNS.' }),
|
|
20
|
-
force: Flags.boolean({ description: '
|
|
21
|
+
force: Flags.boolean({ description: 'Move existing Vercel aliases and overwrite conflicting DNS records.' }),
|
|
21
22
|
json: jsonFlag,
|
|
22
23
|
project: projectFlag,
|
|
23
24
|
provider: providerFlag,
|
|
@@ -31,6 +32,9 @@ export default class Link extends Command {
|
|
|
31
32
|
let spinner;
|
|
32
33
|
const domain = flags.domain ?? args.domain;
|
|
33
34
|
try {
|
|
35
|
+
if (!domain) {
|
|
36
|
+
throw new DoomainError('MISSING_ARGUMENT', 'Domain is required. Use `doomain link <domain>` or pass --domain.');
|
|
37
|
+
}
|
|
34
38
|
if (flags['dry-run']) {
|
|
35
39
|
const result = await linkDomain({ ...flags, domain, dryRun: true, timeoutSeconds: flags.timeout });
|
|
36
40
|
out.result(result);
|
package/dist/commands/wizard.js
CHANGED
|
@@ -127,8 +127,9 @@ export default class Wizard extends Command {
|
|
|
127
127
|
p.intro('Doomain');
|
|
128
128
|
const config = await loadConfig();
|
|
129
129
|
const providerDefinitions = listProviderDefinitions();
|
|
130
|
+
const localProject = detectLocalVercelProject();
|
|
130
131
|
let vercelToken = process.env.VERCEL_TOKEN || config.vercel?.token;
|
|
131
|
-
let vercelTeamId = process.env.VERCEL_TEAM_ID || config.vercel?.teamId;
|
|
132
|
+
let vercelTeamId = process.env.VERCEL_TEAM_ID || localProject?.orgId || config.vercel?.teamId;
|
|
132
133
|
const defaultProvider = process.env.DOOMAIN_PROVIDER || config.defaults?.provider;
|
|
133
134
|
const defaultDomain = process.env.DOOMAIN_DOMAIN || config.defaults?.domain;
|
|
134
135
|
if (!vercelToken) {
|
|
@@ -136,7 +137,6 @@ export default class Wizard extends Command {
|
|
|
136
137
|
if (!vercelToken)
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
139
|
-
const localProject = detectLocalVercelProject();
|
|
140
140
|
const teamSpinner = p.spinner();
|
|
141
141
|
let teams = [];
|
|
142
142
|
if (process.env.VERCEL_TEAM_ID) {
|
|
@@ -164,34 +164,33 @@ export default class Wizard extends Command {
|
|
|
164
164
|
const selectedTeam = teams.find((team) => team.id === vercelTeamId);
|
|
165
165
|
const teamDisplay = vercelTeamId ? teamLabel(selectedTeam ?? { id: vercelTeamId, name: null, role: null, slug: vercelTeamId }) : 'Personal account';
|
|
166
166
|
p.log.success(`Vercel account ready: ${teamDisplay}`);
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 });
|
|
167
|
+
const projectSpinner = p.spinner();
|
|
168
|
+
activeSpinner = projectSpinner;
|
|
169
|
+
projectSpinner.start('Loading Vercel projects');
|
|
170
|
+
const projects = await createVercelClient({ token: vercelToken, teamId: vercelTeamId }).listProjects();
|
|
171
|
+
projectSpinner.stop(`Loaded ${projects.length} projects`);
|
|
172
|
+
activeSpinner = undefined;
|
|
173
|
+
if (projects.length === 0) {
|
|
174
|
+
throw new DoomainError('PROJECT_NOT_FOUND', `No Vercel projects found in ${teamDisplay}.`);
|
|
194
175
|
}
|
|
176
|
+
const localProjectMatchesTeam = localProject && (vercelTeamId ? localProject.orgId === vercelTeamId : !localProject.orgId);
|
|
177
|
+
const initialProject = localProjectMatchesTeam && projects.some((item) => item.id === localProject.projectId) ? localProject.projectId : undefined;
|
|
178
|
+
const selected = await p.autocomplete({
|
|
179
|
+
message: 'Select Vercel project',
|
|
180
|
+
placeholder: 'Type to filter projects...',
|
|
181
|
+
maxItems: 10,
|
|
182
|
+
initialValue: initialProject,
|
|
183
|
+
options: projects.map((item) => ({
|
|
184
|
+
label: projectLabel(item),
|
|
185
|
+
value: item.id,
|
|
186
|
+
hint: item.id === initialProject ? 'linked in .vercel' : (item.framework ?? undefined),
|
|
187
|
+
})),
|
|
188
|
+
});
|
|
189
|
+
const resolved = cancelIfNeeded(selected);
|
|
190
|
+
if (resolved === null)
|
|
191
|
+
return;
|
|
192
|
+
const project = resolved;
|
|
193
|
+
const projectDisplay = projectLabel(projects.find((item) => item.id === project) ?? { id: project });
|
|
195
194
|
p.log.success(`Vercel ready: ${projectDisplay}`);
|
|
196
195
|
const configuredProviderDefinitions = providerDefinitions.filter((definition) => isProviderConfigured(definition, config));
|
|
197
196
|
const providerFailures = [];
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
1
|
+
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'VERCEL_PROJECT_NOT_LINKED';
|
|
2
2
|
export declare class DoomainError extends Error {
|
|
3
3
|
readonly code: DoomainErrorCode;
|
|
4
4
|
readonly details?: unknown;
|
package/dist/lib/link-domain.js
CHANGED
|
@@ -304,7 +304,7 @@ export async function linkDomain(input) {
|
|
|
304
304
|
reportProgress(input, 'dns:resolve-zone', `Finding ${provider.name} DNS zone`);
|
|
305
305
|
const zone = await resolveZone(provider, plan.zoneDomain);
|
|
306
306
|
reportProgress(input, 'vercel:add-domain', 'Adding domain to Vercel');
|
|
307
|
-
const addResult = await vercel.addDomainToProject(plan.project, plan.domain);
|
|
307
|
+
const addResult = await vercel.addDomainToProject(plan.project, plan.domain, { force: input.force });
|
|
308
308
|
reportProgress(input, 'vercel:get-target', 'Reading Vercel DNS target');
|
|
309
309
|
const cname = plan.isApex ? undefined : await vercel.getRecommendedCname(plan.domain);
|
|
310
310
|
reportProgress(input, 'vercel:get-domain', 'Reading Vercel verification records');
|
package/dist/lib/vercel.d.ts
CHANGED
|
@@ -16,17 +16,33 @@ export interface VercelConfig {
|
|
|
16
16
|
token: string;
|
|
17
17
|
teamId?: string;
|
|
18
18
|
}
|
|
19
|
+
interface VercelProjectDomainOwner {
|
|
20
|
+
domain: Record<string, unknown> & {
|
|
21
|
+
name?: string;
|
|
22
|
+
projectId?: string;
|
|
23
|
+
};
|
|
24
|
+
project: VercelProject;
|
|
25
|
+
}
|
|
19
26
|
export declare function resolveVercelConfig(): Promise<VercelConfig>;
|
|
20
27
|
export declare function createVercelClient(config: VercelConfig): {
|
|
21
28
|
listTeams(): Promise<VercelTeam[]>;
|
|
22
29
|
listProjects(search?: string): Promise<VercelProject[]>;
|
|
23
|
-
addDomainToProject(project: string, domain: string
|
|
30
|
+
addDomainToProject(project: string, domain: string, opts?: {
|
|
31
|
+
force?: boolean;
|
|
32
|
+
}): Promise<{
|
|
24
33
|
alreadyAdded: boolean;
|
|
25
34
|
raw?: unknown;
|
|
26
35
|
}>;
|
|
36
|
+
findProjectDomainOwner(domain: string): Promise<VercelProjectDomainOwner | undefined>;
|
|
27
37
|
getDomainConfig(domain: string): Promise<Record<string, unknown>>;
|
|
28
38
|
getRecommendedCname(domain: string): Promise<string>;
|
|
29
39
|
getProjectDomain(project: string, domain: string): Promise<Record<string, unknown>>;
|
|
40
|
+
listProjectDomains(project: string): Promise<Array<Record<string, unknown> & {
|
|
41
|
+
name?: string;
|
|
42
|
+
projectId?: string;
|
|
43
|
+
}>>;
|
|
44
|
+
removeDomainFromProject(project: string, domain: string): Promise<void>;
|
|
30
45
|
verifyProjectDomain(project: string, domain: string): Promise<Record<string, unknown>>;
|
|
31
46
|
};
|
|
32
47
|
export type VercelClient = ReturnType<typeof createVercelClient>;
|
|
48
|
+
export {};
|
package/dist/lib/vercel.js
CHANGED
|
@@ -21,12 +21,23 @@ function appendTeam(path, teamId) {
|
|
|
21
21
|
function apiErrorMessage(status, body) {
|
|
22
22
|
return body?.error?.message ?? `Vercel API error (${status}).`;
|
|
23
23
|
}
|
|
24
|
-
function
|
|
24
|
+
function isDomainConflictError(error) {
|
|
25
25
|
if (!(error instanceof DoomainError))
|
|
26
26
|
return false;
|
|
27
27
|
const details = error.details;
|
|
28
28
|
const text = `${error.message} ${details?.error?.code ?? ''}`.toLowerCase();
|
|
29
|
-
return text.includes('already') ||
|
|
29
|
+
return (text.includes('already') ||
|
|
30
|
+
text.includes('conflict') ||
|
|
31
|
+
text.includes('domain_already') ||
|
|
32
|
+
text.includes('already assigned') ||
|
|
33
|
+
text.includes('already in use'));
|
|
34
|
+
}
|
|
35
|
+
function isSameDomain(value, domain) {
|
|
36
|
+
return typeof value === 'string' && value.toLowerCase() === domain.toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
function findAliasTarget(raw, domain) {
|
|
39
|
+
const targets = Array.isArray(raw) ? raw : [raw];
|
|
40
|
+
return targets.find((target) => target && typeof target === 'object' && isSameDomain(target.domain, domain));
|
|
30
41
|
}
|
|
31
42
|
export function createVercelClient(config) {
|
|
32
43
|
async function request(path, init = {}, opts = {}) {
|
|
@@ -99,20 +110,44 @@ export function createVercelClient(config) {
|
|
|
99
110
|
}
|
|
100
111
|
return [...projectsById.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
101
112
|
},
|
|
102
|
-
async addDomainToProject(project, domain) {
|
|
113
|
+
async addDomainToProject(project, domain, opts = {}) {
|
|
103
114
|
try {
|
|
104
|
-
const raw = await request(`/
|
|
115
|
+
const raw = await request(`/projects/${encodeURIComponent(project)}/alias`, {
|
|
105
116
|
method: 'POST',
|
|
106
|
-
body: JSON.stringify({
|
|
117
|
+
body: JSON.stringify({ domain, target: 'PRODUCTION' }),
|
|
107
118
|
});
|
|
108
|
-
|
|
119
|
+
const aliasTarget = findAliasTarget(raw, domain);
|
|
120
|
+
if (!aliasTarget) {
|
|
121
|
+
throw new DoomainError('DOMAIN_LINK_FAILED', `Vercel did not return ${domain} after adding it to project ${project}.`, raw);
|
|
122
|
+
}
|
|
123
|
+
return { alreadyAdded: false, raw: aliasTarget };
|
|
109
124
|
}
|
|
110
125
|
catch (error) {
|
|
111
|
-
if (
|
|
112
|
-
|
|
126
|
+
if (isDomainConflictError(error)) {
|
|
127
|
+
const projectDomain = await this.getProjectDomain(project, domain).catch(() => undefined);
|
|
128
|
+
if (projectDomain)
|
|
129
|
+
return { alreadyAdded: true, raw: projectDomain };
|
|
130
|
+
if (opts.force) {
|
|
131
|
+
const owner = await this.findProjectDomainOwner(domain);
|
|
132
|
+
if (owner && owner.project.id !== project) {
|
|
133
|
+
await this.removeDomainFromProject(owner.project.id, domain);
|
|
134
|
+
return this.addDomainToProject(project, domain);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw new DoomainError('DOMAIN_ALREADY_ASSIGNED', `Vercel reports ${domain} is already assigned to another project. Re-run with --force if you intend to move it to ${project}.`, error instanceof DoomainError ? error.details : undefined);
|
|
138
|
+
}
|
|
113
139
|
throw error;
|
|
114
140
|
}
|
|
115
141
|
},
|
|
142
|
+
async findProjectDomainOwner(domain) {
|
|
143
|
+
for (const project of await this.listProjects()) {
|
|
144
|
+
const domains = await this.listProjectDomains(project.id).catch(() => []);
|
|
145
|
+
const match = domains.find((item) => isSameDomain(item.name, domain));
|
|
146
|
+
if (match)
|
|
147
|
+
return { domain: match, project };
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
},
|
|
116
151
|
async getDomainConfig(domain) {
|
|
117
152
|
return request(`/v6/domains/${encodeURIComponent(domain)}/config`);
|
|
118
153
|
},
|
|
@@ -124,6 +159,13 @@ export function createVercelClient(config) {
|
|
|
124
159
|
async getProjectDomain(project, domain) {
|
|
125
160
|
return request(`/v9/projects/${encodeURIComponent(project)}/domains/${encodeURIComponent(domain)}`);
|
|
126
161
|
},
|
|
162
|
+
async listProjectDomains(project) {
|
|
163
|
+
const result = await request(`/v9/projects/${encodeURIComponent(project)}/domains`);
|
|
164
|
+
return result.domains ?? [];
|
|
165
|
+
},
|
|
166
|
+
async removeDomainFromProject(project, domain) {
|
|
167
|
+
await request(`/projects/${encodeURIComponent(project)}/alias?domain=${encodeURIComponent(domain)}`, { method: 'DELETE' });
|
|
168
|
+
},
|
|
127
169
|
async verifyProjectDomain(project, domain) {
|
|
128
170
|
return request(`/v9/projects/${encodeURIComponent(project)}/domains/${encodeURIComponent(domain)}/verify`, { method: 'POST' });
|
|
129
171
|
},
|
package/oclif.manifest.json
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"type": "boolean"
|
|
38
38
|
},
|
|
39
39
|
"force": {
|
|
40
|
-
"description": "
|
|
40
|
+
"description": "Move existing Vercel aliases and overwrite conflicting DNS records.",
|
|
41
41
|
"name": "force",
|
|
42
42
|
"allowNo": false,
|
|
43
43
|
"type": "boolean"
|
|
@@ -629,5 +629,5 @@
|
|
|
629
629
|
]
|
|
630
630
|
}
|
|
631
631
|
},
|
|
632
|
-
"version": "0.1.
|
|
632
|
+
"version": "0.1.1"
|
|
633
633
|
}
|