doomain 0.1.6 → 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',
@@ -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,
@@ -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,
@@ -300,6 +301,41 @@
300
301
  "list.js"
301
302
  ]
302
303
  },
304
+ "projects:list": {
305
+ "aliases": [],
306
+ "args": {},
307
+ "description": "List Vercel projects.",
308
+ "flags": {
309
+ "json": {
310
+ "description": "Output a single JSON object and never prompt.",
311
+ "name": "json",
312
+ "allowNo": false,
313
+ "type": "boolean"
314
+ },
315
+ "search": {
316
+ "description": "Filter projects by search term.",
317
+ "name": "search",
318
+ "hasDynamicHelp": false,
319
+ "multiple": false,
320
+ "type": "option"
321
+ }
322
+ },
323
+ "hasDynamicHelp": false,
324
+ "hiddenAliases": [],
325
+ "id": "projects:list",
326
+ "pluginAlias": "doomain",
327
+ "pluginName": "doomain",
328
+ "pluginType": "core",
329
+ "strict": true,
330
+ "enableJsonFlag": false,
331
+ "isESM": true,
332
+ "relativePath": [
333
+ "dist",
334
+ "commands",
335
+ "projects",
336
+ "list.js"
337
+ ]
338
+ },
303
339
  "providers:add": {
304
340
  "aliases": [],
305
341
  "args": {
@@ -560,41 +596,6 @@
560
596
  "verify.js"
561
597
  ]
562
598
  },
563
- "projects:list": {
564
- "aliases": [],
565
- "args": {},
566
- "description": "List Vercel projects.",
567
- "flags": {
568
- "json": {
569
- "description": "Output a single JSON object and never prompt.",
570
- "name": "json",
571
- "allowNo": false,
572
- "type": "boolean"
573
- },
574
- "search": {
575
- "description": "Filter projects by search term.",
576
- "name": "search",
577
- "hasDynamicHelp": false,
578
- "multiple": false,
579
- "type": "option"
580
- }
581
- },
582
- "hasDynamicHelp": false,
583
- "hiddenAliases": [],
584
- "id": "projects:list",
585
- "pluginAlias": "doomain",
586
- "pluginName": "doomain",
587
- "pluginType": "core",
588
- "strict": true,
589
- "enableJsonFlag": false,
590
- "isESM": true,
591
- "relativePath": [
592
- "dist",
593
- "commands",
594
- "projects",
595
- "list.js"
596
- ]
597
- },
598
599
  "auth:logout:vercel": {
599
600
  "aliases": [],
600
601
  "args": {},
@@ -629,5 +630,5 @@
629
630
  ]
630
631
  }
631
632
  },
632
- "version": "0.1.6"
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.6",
4
+ "version": "0.1.7",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {