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.
@@ -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',
@@ -1,5 +1,5 @@
1
1
  import { Args, Command } from '@oclif/core';
2
- import { getCommandSchema } from '../lib/command-schema.js';
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 = getCommandSchema(args.command);
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
+ }
@@ -22,6 +22,7 @@ export interface DoomainConfig {
22
22
  [provider: string]: ProviderConfig | SpaceshipProviderConfig | undefined;
23
23
  };
24
24
  defaults?: {
25
+ project?: string;
25
26
  provider?: string;
26
27
  domain?: string;
27
28
  };
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. Auto-detected from .vercel/project.json when omitted.',
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;
@@ -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 resolveProject(project) {
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
- throw new DoomainError('VERCEL_PROJECT_NOT_LINKED', 'No linked Vercel project found. Run inside a Vercel project or pass --project.');
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, { domain: requested.fullDomain, providers: zones.searched });
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,
@@ -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. Auto-detected from .vercel/project.json when omitted.",
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. Auto-detected from .vercel/project.json when omitted.",
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.6"
633
+ "version": "0.1.8"
633
634
  }
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.6",
4
+ "version": "0.1.8",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {