doomain 0.1.6 → 0.1.8
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 +1 -0
- package/dist/commands/schema.js +2 -2
- package/dist/lib/command-schema.d.ts +12 -0
- package/dist/lib/command-schema.js +39 -2
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/flags.js +1 -1
- package/dist/lib/link-domain.d.ts +2 -0
- package/dist/lib/link-domain.js +109 -8
- package/oclif.manifest.json +4 -3
- package/package.json +1 -1
package/dist/commands/link.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createOutput, outputError } from '../lib/output.js';
|
|
|
6
6
|
export default class Link extends Command {
|
|
7
7
|
static description = 'Link a Vercel project to a domain and create DNS records.';
|
|
8
8
|
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %> app.example.com --json',
|
|
9
10
|
'<%= config.bin %> <%= command.id %> app.example.com --project my-app --json',
|
|
10
11
|
'<%= config.bin %> <%= command.id %> --domain app.example.com --project my-app --json',
|
|
11
12
|
'<%= config.bin %> <%= command.id %> --domain example.com --subdomain app --project my-app',
|
package/dist/commands/schema.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args, Command } from '@oclif/core';
|
|
2
|
-
import {
|
|
2
|
+
import { getCommandSchemaForAgents } from '../lib/command-schema.js';
|
|
3
3
|
import { jsonFlag } from '../lib/flags.js';
|
|
4
4
|
import { createOutput, outputError } from '../lib/output.js';
|
|
5
5
|
export default class Schema extends Command {
|
|
@@ -14,7 +14,7 @@ export default class Schema extends Command {
|
|
|
14
14
|
const { args, flags } = await this.parse(Schema);
|
|
15
15
|
const out = createOutput({ json: flags.json });
|
|
16
16
|
try {
|
|
17
|
-
const schema =
|
|
17
|
+
const schema = await getCommandSchemaForAgents(args.command);
|
|
18
18
|
if (!schema)
|
|
19
19
|
throw new Error(`Unknown command schema: ${args.command}`);
|
|
20
20
|
if (!out.json)
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import { type ProviderStatus } from './providers/status.js';
|
|
2
|
+
export type ProviderConnectionStatus = Pick<ProviderStatus, 'configured' | 'default' | 'displayName' | 'docsUrl' | 'id'>;
|
|
1
3
|
export interface CommandSchema {
|
|
2
4
|
name: string;
|
|
3
5
|
description: string;
|
|
4
6
|
examples: string[];
|
|
7
|
+
agentHint?: string;
|
|
8
|
+
agentInstructions?: string[];
|
|
9
|
+
agentQuickstart?: {
|
|
10
|
+
doNotPreflight: boolean;
|
|
11
|
+
preferredFirstCommand: string;
|
|
12
|
+
};
|
|
13
|
+
configuredProviders?: ProviderConnectionStatus[];
|
|
14
|
+
mutates?: boolean;
|
|
15
|
+
safeForAgents?: boolean;
|
|
5
16
|
flags: Array<{
|
|
6
17
|
name: string;
|
|
7
18
|
type: 'boolean' | 'integer' | 'string';
|
|
@@ -12,3 +23,4 @@ export interface CommandSchema {
|
|
|
12
23
|
}
|
|
13
24
|
export declare const commandSchemas: CommandSchema[];
|
|
14
25
|
export declare function getCommandSchema(name?: string): CommandSchema[] | CommandSchema | undefined;
|
|
26
|
+
export declare function getCommandSchemaForAgents(name?: string): Promise<CommandSchema[] | CommandSchema | undefined>;
|
|
@@ -1,21 +1,35 @@
|
|
|
1
|
+
import { listProviderStatuses } from './providers/status.js';
|
|
1
2
|
export const commandSchemas = [
|
|
2
3
|
{
|
|
3
4
|
name: 'link',
|
|
4
5
|
description: 'Link a Vercel project to a domain and create DNS records.',
|
|
5
6
|
examples: [
|
|
7
|
+
'doomain link app.example.com --json',
|
|
6
8
|
'doomain link app.example.com --project my-app --json',
|
|
7
9
|
'doomain link --domain app.example.com --project my-app --json',
|
|
8
10
|
'doomain link --domain example.com --subdomain app --project my-app --json',
|
|
9
11
|
'doomain link --provider spaceship --domain example.com --apex --project my-app --dry-run --json',
|
|
10
12
|
],
|
|
13
|
+
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
|
+
agentInstructions: [
|
|
15
|
+
'When the user asks to add or link a domain, run `doomain link <domain> --json` first.',
|
|
16
|
+
'Do not run `providers status`, `projects list`, `--help`, or `--dry-run` before the first link attempt unless the user asks for a preview or diagnosis.',
|
|
17
|
+
'Only run follow-up commands when the JSON error includes a recovery action that requires them.',
|
|
18
|
+
],
|
|
19
|
+
agentQuickstart: {
|
|
20
|
+
doNotPreflight: true,
|
|
21
|
+
preferredFirstCommand: 'doomain link <domain> --json',
|
|
22
|
+
},
|
|
23
|
+
mutates: true,
|
|
24
|
+
safeForAgents: true,
|
|
11
25
|
flags: [
|
|
12
26
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
13
27
|
{ name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
|
|
14
28
|
{ name: 'domain', type: 'string', description: 'Target domain or base zone, for example app.example.com or example.com.' },
|
|
15
29
|
{ name: 'subdomain', type: 'string', description: 'Subdomain to add.' },
|
|
16
30
|
{ 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.' },
|
|
31
|
+
{ name: 'project', type: 'string', description: 'Vercel project id/name. Optional when project inference succeeds.' },
|
|
32
|
+
{ name: 'dry-run', type: 'boolean', description: 'Preview changes without writing. Intended for human previews; agents should not use this unless explicitly asked.' },
|
|
19
33
|
{ name: 'force', type: 'boolean', description: 'Overwrite conflicting DNS records.' },
|
|
20
34
|
{ name: 'wait', type: 'boolean', description: 'Wait for DNS and Vercel verification.', default: true },
|
|
21
35
|
{ name: 'timeout', type: 'integer', description: 'Wait timeout in seconds.', default: 300 },
|
|
@@ -97,3 +111,26 @@ export function getCommandSchema(name) {
|
|
|
97
111
|
return commandSchemas;
|
|
98
112
|
return commandSchemas.find((schema) => schema.name === name);
|
|
99
113
|
}
|
|
114
|
+
async function configuredProviders() {
|
|
115
|
+
return (await listProviderStatuses({ verify: false })).map((provider) => ({
|
|
116
|
+
configured: provider.configured,
|
|
117
|
+
default: provider.default,
|
|
118
|
+
displayName: provider.displayName,
|
|
119
|
+
docsUrl: provider.docsUrl,
|
|
120
|
+
id: provider.id,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
function withProviderConnections(schema, providers) {
|
|
124
|
+
if (schema.name !== 'link')
|
|
125
|
+
return schema;
|
|
126
|
+
return { ...schema, configuredProviders: providers };
|
|
127
|
+
}
|
|
128
|
+
export async function getCommandSchemaForAgents(name) {
|
|
129
|
+
const schema = getCommandSchema(name);
|
|
130
|
+
if (!schema)
|
|
131
|
+
return undefined;
|
|
132
|
+
const providers = await configuredProviders();
|
|
133
|
+
if (Array.isArray(schema))
|
|
134
|
+
return schema.map((item) => withProviderConnections(item, providers));
|
|
135
|
+
return withProviderConnections(schema, providers);
|
|
136
|
+
}
|
package/dist/lib/config.d.ts
CHANGED
package/dist/lib/flags.js
CHANGED
|
@@ -6,5 +6,5 @@ export const subdomainFlag = Flags.string({ description: 'Subdomain to add, for
|
|
|
6
6
|
export const apexFlag = Flags.boolean({ description: 'Use the root/apex domain instead of a subdomain.' });
|
|
7
7
|
export const projectFlag = Flags.string({
|
|
8
8
|
char: 'p',
|
|
9
|
-
description: 'Vercel project id/name.
|
|
9
|
+
description: 'Vercel project id/name. Inferred from env, config, .vercel/project.json, or package.json when omitted.',
|
|
10
10
|
});
|
|
@@ -11,6 +11,7 @@ export interface LinkDomainInput {
|
|
|
11
11
|
timeoutSeconds?: number;
|
|
12
12
|
progress?: LinkDomainProgressCallback;
|
|
13
13
|
}
|
|
14
|
+
export type LinkDomainProjectSource = 'config' | 'env' | 'flag' | 'packageJson' | 'vercelProjectFile';
|
|
14
15
|
export type LinkDomainProgressStage = 'dns:apply' | 'dns:plan' | 'dns:resolve-zone' | 'dns:wait' | 'vercel:add-domain' | 'vercel:get-domain' | 'vercel:get-target' | 'vercel:verify';
|
|
15
16
|
export interface LinkDomainProgress {
|
|
16
17
|
message: string;
|
|
@@ -21,6 +22,7 @@ export interface LinkDomainPlan {
|
|
|
21
22
|
provider: string;
|
|
22
23
|
providerInferred: boolean;
|
|
23
24
|
project: string;
|
|
25
|
+
projectSource: LinkDomainProjectSource;
|
|
24
26
|
recordName: string;
|
|
25
27
|
zoneDomain: string;
|
|
26
28
|
domain: string;
|
package/dist/lib/link-domain.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { resolve4, resolveCname, resolveTxt } from 'node:dns/promises';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, parse } from 'node:path';
|
|
2
4
|
import { loadConfig } from './config.js';
|
|
3
5
|
import { DoomainError } from './errors.js';
|
|
4
6
|
import { detectLocalVercelProject } from './local-vercel.js';
|
|
5
7
|
import { createProvider, getProviderDefinition, listProviderDefinitions } from './providers/registry.js';
|
|
6
|
-
import { isProviderConfigured } from './providers/status.js';
|
|
8
|
+
import { isProviderConfigured, listProviderStatuses } from './providers/status.js';
|
|
7
9
|
import { normalizeDomain, normalizeSubdomain } from './validate.js';
|
|
8
10
|
import { createVercelClient, resolveVercelConfig, VERCEL_APEX_A_RECORD, VERCEL_CNAME_RECORD } from './vercel.js';
|
|
9
11
|
function cleanDnsValue(value) {
|
|
@@ -19,13 +21,92 @@ async function resolveConfiguredDomain(domain) {
|
|
|
19
21
|
throw new DoomainError('MISSING_ARGUMENT', 'Domain is required. Use --domain or set a default domain.');
|
|
20
22
|
return resolved;
|
|
21
23
|
}
|
|
22
|
-
function
|
|
24
|
+
function findPackageProjectName(start = process.cwd()) {
|
|
25
|
+
let current = start;
|
|
26
|
+
const root = parse(start).root;
|
|
27
|
+
while (true) {
|
|
28
|
+
const packagePath = join(current, 'package.json');
|
|
29
|
+
if (existsSync(packagePath)) {
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
32
|
+
if (typeof data.name === 'string' && data.name.trim())
|
|
33
|
+
return data.name.trim();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (current === root)
|
|
40
|
+
return undefined;
|
|
41
|
+
current = dirname(current);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function projectSuggestionScore(projectName, project) {
|
|
45
|
+
const target = projectName.toLowerCase();
|
|
46
|
+
const name = project.name.toLowerCase();
|
|
47
|
+
const id = project.id.toLowerCase();
|
|
48
|
+
if (name === target || id === target)
|
|
49
|
+
return 0;
|
|
50
|
+
if (name.startsWith(target))
|
|
51
|
+
return 1;
|
|
52
|
+
if (name.includes(target))
|
|
53
|
+
return 2;
|
|
54
|
+
if (target.includes(name))
|
|
55
|
+
return 3;
|
|
56
|
+
const targetParts = target.split(/[^a-z0-9]+/).filter(Boolean);
|
|
57
|
+
const matchingParts = targetParts.filter((part) => name.includes(part)).length;
|
|
58
|
+
return matchingParts > 0 ? 4 + (targetParts.length - matchingParts) : 99;
|
|
59
|
+
}
|
|
60
|
+
function projectSuggestions(projectName, projects) {
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
return projects
|
|
63
|
+
.filter((project) => {
|
|
64
|
+
if (seen.has(project.id))
|
|
65
|
+
return false;
|
|
66
|
+
seen.add(project.id);
|
|
67
|
+
return true;
|
|
68
|
+
})
|
|
69
|
+
.map((project) => ({ project, score: projectSuggestionScore(projectName, project) }))
|
|
70
|
+
.filter(({ score }) => score < 99)
|
|
71
|
+
.sort((a, b) => a.score - b.score || a.project.name.localeCompare(b.project.name))
|
|
72
|
+
.slice(0, 5)
|
|
73
|
+
.map(({ project }) => ({ id: project.id, name: project.name }));
|
|
74
|
+
}
|
|
75
|
+
async function resolvePackageProject(projectName) {
|
|
76
|
+
const vercel = createVercelClient(await resolveVercelConfig());
|
|
77
|
+
const projects = await vercel.listProjects(projectName);
|
|
78
|
+
const match = projects.find((project) => project.name === projectName || project.id === projectName);
|
|
79
|
+
if (match)
|
|
80
|
+
return match.name;
|
|
81
|
+
const allProjects = await vercel.listProjects().catch(() => []);
|
|
82
|
+
const suggestions = projectSuggestions(projectName, [...projects, ...allProjects]);
|
|
83
|
+
throw new DoomainError('VERCEL_PROJECT_NOT_LINKED', `No Vercel project named ${projectName} was found. Pass --project to choose a project.`, {
|
|
84
|
+
project: projectName,
|
|
85
|
+
projectSource: 'packageJson',
|
|
86
|
+
suggestions,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function resolveProject(project) {
|
|
23
90
|
if (project)
|
|
24
|
-
return { project, localProjectDetected: false };
|
|
91
|
+
return { project, projectSource: 'flag', localProjectDetected: false };
|
|
92
|
+
const config = await loadConfig();
|
|
93
|
+
const envProject = process.env.DOOMAIN_PROJECT;
|
|
94
|
+
if (envProject)
|
|
95
|
+
return { project: envProject, projectSource: 'env', localProjectDetected: false };
|
|
96
|
+
if (config.defaults?.project)
|
|
97
|
+
return { project: config.defaults.project, projectSource: 'config', localProjectDetected: false };
|
|
25
98
|
const localProject = detectLocalVercelProject();
|
|
26
99
|
if (localProject)
|
|
27
|
-
return { project: localProject.projectId, localProjectDetected: true };
|
|
28
|
-
|
|
100
|
+
return { project: localProject.projectId, projectSource: 'vercelProjectFile', localProjectDetected: true };
|
|
101
|
+
const packageProject = findPackageProjectName();
|
|
102
|
+
if (packageProject) {
|
|
103
|
+
return {
|
|
104
|
+
project: await resolvePackageProject(packageProject),
|
|
105
|
+
projectSource: 'packageJson',
|
|
106
|
+
localProjectDetected: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
throw new DoomainError('VERCEL_PROJECT_NOT_LINKED', 'No Vercel project could be inferred. Run inside a project with package.json/.vercel/project.json or pass --project.');
|
|
29
110
|
}
|
|
30
111
|
async function resolveZone(provider, zoneDomain) {
|
|
31
112
|
const zone = await provider.getZone(zoneDomain);
|
|
@@ -34,6 +115,15 @@ async function resolveZone(provider, zoneDomain) {
|
|
|
34
115
|
}
|
|
35
116
|
return zone;
|
|
36
117
|
}
|
|
118
|
+
async function providerConnectionDetails() {
|
|
119
|
+
return (await listProviderStatuses({ verify: false })).map((provider) => ({
|
|
120
|
+
configured: provider.configured,
|
|
121
|
+
default: provider.default,
|
|
122
|
+
displayName: provider.displayName,
|
|
123
|
+
docsUrl: provider.docsUrl,
|
|
124
|
+
id: provider.id,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
37
127
|
function resolveRequestedDomain(opts) {
|
|
38
128
|
if (opts.apex && opts.subdomain) {
|
|
39
129
|
throw new DoomainError('INVALID_INPUT', 'Use either --apex or --subdomain, not both.');
|
|
@@ -87,7 +177,11 @@ async function loadConfiguredProviderZones(providerId) {
|
|
|
87
177
|
const config = await loadConfig();
|
|
88
178
|
const definitions = listProviderDefinitions().filter((definition) => isProviderConfigured(definition, config));
|
|
89
179
|
if (definitions.length === 0) {
|
|
90
|
-
throw new DoomainError('CONFIG_NOT_FOUND', 'No DNS provider is configured. Run `doomain providers connect` first.'
|
|
180
|
+
throw new DoomainError('CONFIG_NOT_FOUND', 'No DNS provider is configured. Run `doomain providers connect` first.', {
|
|
181
|
+
configuredProviders: await providerConnectionDetails(),
|
|
182
|
+
recovery: 'Connect the DNS provider that owns this domain, then retry `doomain link <domain> --json`.',
|
|
183
|
+
suggestedCommands: ['doomain providers connect', 'doomain link <domain> --json'],
|
|
184
|
+
});
|
|
91
185
|
}
|
|
92
186
|
const results = await Promise.all(definitions.map(async (definition) => {
|
|
93
187
|
try {
|
|
@@ -125,7 +219,13 @@ async function resolveProviderTarget(input) {
|
|
|
125
219
|
const providerMessage = input.provider
|
|
126
220
|
? `${getProviderDefinition(input.provider).displayName} does not have a matching DNS zone for ${requested.fullDomain}.`
|
|
127
221
|
: `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
|
|
128
|
-
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, {
|
|
222
|
+
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, {
|
|
223
|
+
configuredProviders: await providerConnectionDetails(),
|
|
224
|
+
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.',
|
|
226
|
+
searchedZones: zones.searched,
|
|
227
|
+
suggestedCommands: [`doomain link ${requested.fullDomain} --provider <id> --json`, 'doomain providers connect'],
|
|
228
|
+
});
|
|
129
229
|
}
|
|
130
230
|
const bestLength = matches[0].zone.name.length;
|
|
131
231
|
const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
|
|
@@ -312,7 +412,7 @@ function reportProgress(input, stage, message) {
|
|
|
312
412
|
input.progress?.({ message, stage });
|
|
313
413
|
}
|
|
314
414
|
export async function createLinkPlan(input) {
|
|
315
|
-
const project = resolveProject(input.project);
|
|
415
|
+
const project = await resolveProject(input.project);
|
|
316
416
|
const resolved = await resolveProviderTarget(input);
|
|
317
417
|
const { provider, providerInferred, target } = resolved;
|
|
318
418
|
const record = planBaseRecord({ isApex: target.isApex, provider, recordName: target.recordName });
|
|
@@ -320,6 +420,7 @@ export async function createLinkPlan(input) {
|
|
|
320
420
|
provider,
|
|
321
421
|
providerInferred,
|
|
322
422
|
project: project.project,
|
|
423
|
+
projectSource: project.projectSource,
|
|
323
424
|
recordName: target.recordName,
|
|
324
425
|
zoneDomain: target.zoneDomain,
|
|
325
426
|
domain: target.fullDomain,
|
package/oclif.manifest.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"description": "Link a Vercel project to a domain and create DNS records.",
|
|
13
13
|
"examples": [
|
|
14
|
+
"<%= config.bin %> <%= command.id %> app.example.com --json",
|
|
14
15
|
"<%= config.bin %> <%= command.id %> app.example.com --project my-app --json",
|
|
15
16
|
"<%= config.bin %> <%= command.id %> --domain app.example.com --project my-app --json",
|
|
16
17
|
"<%= config.bin %> <%= command.id %> --domain example.com --subdomain app --project my-app",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
},
|
|
51
52
|
"project": {
|
|
52
53
|
"char": "p",
|
|
53
|
-
"description": "Vercel project id/name.
|
|
54
|
+
"description": "Vercel project id/name. Inferred from env, config, .vercel/project.json, or package.json when omitted.",
|
|
54
55
|
"name": "project",
|
|
55
56
|
"hasDynamicHelp": false,
|
|
56
57
|
"multiple": false,
|
|
@@ -159,7 +160,7 @@
|
|
|
159
160
|
},
|
|
160
161
|
"project": {
|
|
161
162
|
"char": "p",
|
|
162
|
-
"description": "Vercel project id/name.
|
|
163
|
+
"description": "Vercel project id/name. Inferred from env, config, .vercel/project.json, or package.json when omitted.",
|
|
163
164
|
"name": "project",
|
|
164
165
|
"hasDynamicHelp": false,
|
|
165
166
|
"multiple": false,
|
|
@@ -629,5 +630,5 @@
|
|
|
629
630
|
]
|
|
630
631
|
}
|
|
631
632
|
},
|
|
632
|
-
"version": "0.1.
|
|
633
|
+
"version": "0.1.8"
|
|
633
634
|
}
|