doomain 0.1.5 → 0.1.7

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',
@@ -18,7 +19,7 @@ export default class Link extends Command {
18
19
  apex: apexFlag,
19
20
  domain: domainFlag,
20
21
  'dry-run': Flags.boolean({ description: 'Preview changes without writing to Vercel or DNS.' }),
21
- force: Flags.boolean({ description: 'Move existing Vercel aliases and overwrite conflicting DNS records.' }),
22
+ force: Flags.boolean({ description: 'Move existing Vercel project domains and overwrite conflicting DNS records.' }),
22
23
  json: jsonFlag,
23
24
  project: projectFlag,
24
25
  provider: providerFlag,
@@ -2,6 +2,9 @@ export interface CommandSchema {
2
2
  name: string;
3
3
  description: string;
4
4
  examples: string[];
5
+ agentHint?: string;
6
+ mutates?: boolean;
7
+ safeForAgents?: boolean;
5
8
  flags: Array<{
6
9
  name: string;
7
10
  type: 'boolean' | 'integer' | 'string';
@@ -3,18 +3,22 @@ export const commandSchemas = [
3
3
  name: 'link',
4
4
  description: 'Link a Vercel project to a domain and create DNS records.',
5
5
  examples: [
6
+ 'doomain link app.example.com --json',
6
7
  'doomain link app.example.com --project my-app --json',
7
8
  'doomain link --domain app.example.com --project my-app --json',
8
9
  'doomain link --domain example.com --subdomain app --project my-app --json',
9
10
  'doomain link --provider spaceship --domain example.com --apex --project my-app --dry-run --json',
10
11
  ],
12
+ agentHint: 'For agent use, run `doomain link <domain> --json`. The project is inferred from --project, DOOMAIN_PROJECT, config defaults, .vercel/project.json, or nearest package.json name.',
13
+ mutates: true,
14
+ safeForAgents: true,
11
15
  flags: [
12
16
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
13
17
  { name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
14
18
  { name: 'domain', type: 'string', description: 'Target domain or base zone, for example app.example.com or example.com.' },
15
19
  { name: 'subdomain', type: 'string', description: 'Subdomain to add.' },
16
20
  { name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
17
- { name: 'project', type: 'string', description: 'Vercel project id/name.' },
21
+ { name: 'project', type: 'string', description: 'Vercel project id/name. Optional when project inference succeeds.' },
18
22
  { name: 'dry-run', type: 'boolean', description: 'Preview changes without writing.' },
19
23
  { name: 'force', type: 'boolean', description: 'Overwrite conflicting DNS records.' },
20
24
  { name: 'wait', type: 'boolean', description: 'Wait for DNS and Vercel verification.', default: true },
@@ -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,4 +1,6 @@
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';
@@ -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);
@@ -312,7 +393,7 @@ function reportProgress(input, stage, message) {
312
393
  input.progress?.({ message, stage });
313
394
  }
314
395
  export async function createLinkPlan(input) {
315
- const project = resolveProject(input.project);
396
+ const project = await resolveProject(input.project);
316
397
  const resolved = await resolveProviderTarget(input);
317
398
  const { provider, providerInferred, target } = resolved;
318
399
  const record = planBaseRecord({ isApex: target.isApex, provider, recordName: target.recordName });
@@ -320,6 +401,7 @@ export async function createLinkPlan(input) {
320
401
  provider,
321
402
  providerInferred,
322
403
  project: project.project,
404
+ projectSource: project.projectSource,
323
405
  recordName: target.recordName,
324
406
  zoneDomain: target.zoneDomain,
325
407
  domain: target.fullDomain,
@@ -43,9 +43,11 @@ function isDomainConflictError(error) {
43
43
  function isSameDomain(value, domain) {
44
44
  return typeof value === 'string' && value.toLowerCase() === domain.toLowerCase();
45
45
  }
46
- function findAliasTarget(raw, domain) {
46
+ function findProjectDomainTarget(raw, domain) {
47
47
  const targets = Array.isArray(raw) ? raw : [raw];
48
- return targets.find((target) => target && typeof target === 'object' && isSameDomain(target.domain, domain));
48
+ return targets.find((target) => target &&
49
+ typeof target === 'object' &&
50
+ (isSameDomain(target.domain, domain) || isSameDomain(target.name, domain)));
49
51
  }
50
52
  export function createVercelClient(config) {
51
53
  async function request(path, init = {}, opts = {}) {
@@ -59,7 +61,7 @@ export function createVercelClient(config) {
59
61
  });
60
62
  if (!response.ok) {
61
63
  const body = (await response.json().catch(() => undefined));
62
- if (response.status === 401 || response.status === 403) {
64
+ if (response.status === 401) {
63
65
  throw new DoomainError('VERCEL_AUTH_FAILED', vercelAuthErrorMessage(body), body);
64
66
  }
65
67
  throw new DoomainError('DOMAIN_LINK_FAILED', apiErrorMessage(response.status, body), body);
@@ -123,15 +125,15 @@ export function createVercelClient(config) {
123
125
  },
124
126
  async addDomainToProject(project, domain, opts = {}) {
125
127
  try {
126
- const raw = await request(`/projects/${encodeURIComponent(project)}/alias`, {
128
+ const raw = await request(`/v10/projects/${encodeURIComponent(project)}/domains`, {
127
129
  method: 'POST',
128
- body: JSON.stringify({ domain, target: 'PRODUCTION' }),
130
+ body: JSON.stringify({ name: domain }),
129
131
  });
130
- const aliasTarget = findAliasTarget(raw, domain);
131
- if (!aliasTarget) {
132
+ const projectDomain = findProjectDomainTarget(raw, domain);
133
+ if (!projectDomain) {
132
134
  throw new DoomainError('DOMAIN_LINK_FAILED', `Vercel did not return ${domain} after adding it to project ${project}.`, raw);
133
135
  }
134
- return { alreadyAdded: false, raw: aliasTarget };
136
+ return { alreadyAdded: false, raw: projectDomain };
135
137
  }
136
138
  catch (error) {
137
139
  if (isDomainConflictError(error)) {
@@ -175,7 +177,7 @@ export function createVercelClient(config) {
175
177
  return result.domains ?? [];
176
178
  },
177
179
  async removeDomainFromProject(project, domain) {
178
- await request(`/projects/${encodeURIComponent(project)}/alias?domain=${encodeURIComponent(domain)}`, { method: 'DELETE' });
180
+ await request(`/v9/projects/${encodeURIComponent(project)}/domains/${encodeURIComponent(domain)}`, { method: 'DELETE' });
179
181
  },
180
182
  async verifyProjectDomain(project, domain) {
181
183
  return request(`/v9/projects/${encodeURIComponent(project)}/domains/${encodeURIComponent(domain)}/verify`, { method: 'POST' });
@@ -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",
@@ -37,7 +38,7 @@
37
38
  "type": "boolean"
38
39
  },
39
40
  "force": {
40
- "description": "Move existing Vercel aliases and overwrite conflicting DNS records.",
41
+ "description": "Move existing Vercel project domains and overwrite conflicting DNS records.",
41
42
  "name": "force",
42
43
  "allowNo": false,
43
44
  "type": "boolean"
@@ -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,
@@ -335,39 +336,6 @@
335
336
  "list.js"
336
337
  ]
337
338
  },
338
- "auth:logout:vercel": {
339
- "aliases": [],
340
- "args": {},
341
- "description": "Remove saved Vercel credentials locally.",
342
- "examples": [
343
- "<%= config.bin %> <%= command.id %>",
344
- "<%= config.bin %> <%= command.id %> --json"
345
- ],
346
- "flags": {
347
- "json": {
348
- "description": "Output a single JSON object and never prompt.",
349
- "name": "json",
350
- "allowNo": false,
351
- "type": "boolean"
352
- }
353
- },
354
- "hasDynamicHelp": false,
355
- "hiddenAliases": [],
356
- "id": "auth:logout:vercel",
357
- "pluginAlias": "doomain",
358
- "pluginName": "doomain",
359
- "pluginType": "core",
360
- "strict": true,
361
- "enableJsonFlag": false,
362
- "isESM": true,
363
- "relativePath": [
364
- "dist",
365
- "commands",
366
- "auth",
367
- "logout",
368
- "vercel.js"
369
- ]
370
- },
371
339
  "providers:add": {
372
340
  "aliases": [],
373
341
  "args": {
@@ -627,7 +595,40 @@
627
595
  "providers",
628
596
  "verify.js"
629
597
  ]
598
+ },
599
+ "auth:logout:vercel": {
600
+ "aliases": [],
601
+ "args": {},
602
+ "description": "Remove saved Vercel credentials locally.",
603
+ "examples": [
604
+ "<%= config.bin %> <%= command.id %>",
605
+ "<%= config.bin %> <%= command.id %> --json"
606
+ ],
607
+ "flags": {
608
+ "json": {
609
+ "description": "Output a single JSON object and never prompt.",
610
+ "name": "json",
611
+ "allowNo": false,
612
+ "type": "boolean"
613
+ }
614
+ },
615
+ "hasDynamicHelp": false,
616
+ "hiddenAliases": [],
617
+ "id": "auth:logout:vercel",
618
+ "pluginAlias": "doomain",
619
+ "pluginName": "doomain",
620
+ "pluginType": "core",
621
+ "strict": true,
622
+ "enableJsonFlag": false,
623
+ "isESM": true,
624
+ "relativePath": [
625
+ "dist",
626
+ "commands",
627
+ "auth",
628
+ "logout",
629
+ "vercel.js"
630
+ ]
630
631
  }
631
632
  },
632
- "version": "0.1.5"
633
+ "version": "0.1.7"
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.5",
4
+ "version": "0.1.7",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {