doomain 0.1.7 → 0.1.9

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/README.md CHANGED
@@ -14,7 +14,7 @@ Use the interactive wizard when working by hand. Use explicit commands with `--j
14
14
  - DNS provider inference by longest matching configured zone.
15
15
  - Dry-run plans before writing changes.
16
16
  - DNS propagation and Vercel verification wait loop.
17
- - DNS provider support for Spaceship, Namecheap, and Cloudflare.
17
+ - DNS provider support for Spaceship, Namecheap, Cloudflare, and Hostinger.
18
18
 
19
19
  ## Install
20
20
 
@@ -93,6 +93,7 @@ Doomain stores local credentials in `~/.doomain/config.json` with `0600` file pe
93
93
  | Spaceship | `spaceship` | `apiKey`, `apiSecret` | `SPACESHIP_API_KEY`, `SPACESHIP_API_SECRET` | API key needs domain read access and DNS record read/write access. |
94
94
  | Namecheap | `namecheap` | `apiUser`, `apiKey`, `clientIp` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY`, `NAMECHEAP_CLIENT_IP` | API access must be enabled and `clientIp` must be your whitelisted public IPv4. Optional: `username`, `sandbox`. |
95
95
  | Cloudflare | `cloudflare` | `apiToken`, `accountId` | `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` | API token needs `Zone:Read` and `DNS:Edit`. Vercel records are written as DNS-only records, not proxied. |
96
+ | Hostinger | `hostinger` | `apiToken` | `HOSTINGER_API_TOKEN` | API token needs access to domain portfolio and DNS zone records. |
96
97
 
97
98
  ### Spaceship
98
99
 
@@ -139,6 +140,15 @@ doomain providers connect cloudflare \
139
140
 
140
141
  Cloudflare records created for Vercel `A`, `AAAA`, and `CNAME` targets are set to `proxied: false` so Vercel can validate the domain.
141
142
 
143
+ ### Hostinger
144
+
145
+ ```bash
146
+ doomain providers connect hostinger \
147
+ --credential apiToken=your_hostinger_api_token
148
+ ```
149
+
150
+ Create Hostinger API tokens from hPanel Account > API. Doomain lists active zones from the Hostinger domain portfolio and updates records through the DNS zone API.
151
+
142
152
  ## Linking Domains
143
153
 
144
154
  You can pass the full target domain as a positional argument:
@@ -373,6 +383,7 @@ Saves DNS provider credentials locally.
373
383
  ```bash
374
384
  doomain providers connect cloudflare -c apiToken=token -c accountId=account_id
375
385
  doomain providers connect namecheap -c apiUser=user -c apiKey=key -c clientIp=127.0.0.1 --json
386
+ doomain providers connect hostinger -c apiToken=token --json
376
387
  doomain providers connect spaceship --api-key key --api-secret secret
377
388
  ```
378
389
 
@@ -495,6 +506,12 @@ CLOUDFLARE_API_TOKEN
495
506
  CLOUDFLARE_ACCOUNT_ID
496
507
  ```
497
508
 
509
+ Hostinger:
510
+
511
+ ```bash
512
+ HOSTINGER_API_TOKEN
513
+ ```
514
+
498
515
  Doomain defaults and config:
499
516
 
500
517
  ```bash
@@ -528,7 +545,7 @@ The selected provider does not have a DNS zone matching the target domain. Check
528
545
 
529
546
  `PROVIDER_ZONE_AMBIGUOUS`
530
547
 
531
- More than one configured provider has the same best matching zone. Re-run with `--provider cloudflare`, `--provider namecheap`, or `--provider spaceship`.
548
+ More than one configured provider has the same best matching zone. Re-run with `--provider cloudflare`, `--provider namecheap`, `--provider spaceship`, or `--provider hostinger`.
532
549
 
533
550
  Namecheap authentication or permission errors
534
551
 
@@ -538,6 +555,14 @@ Cloudflare permission errors
538
555
 
539
556
  Make sure the API token has `Zone:Read` and `DNS:Edit` permissions for the account that owns the zones.
540
557
 
558
+ Hostinger authentication errors
559
+
560
+ Make sure the API token is active and can access the domains you want Doomain to manage.
561
+
562
+ Hostinger DNS zone not found
563
+
564
+ Make sure the domain is active in Hostinger before linking it. Domains shown as `pending_setup` in Hostinger's portfolio API are not writable through the DNS zone API yet.
565
+
541
566
  DNS propagation timeout
542
567
 
543
568
  The DNS records may have been saved even if Vercel verification timed out. Check the domain in Vercel, inspect records with `doomain domains list`, or re-run verification with `doomain verify`.
@@ -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,8 +1,16 @@
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[];
5
7
  agentHint?: string;
8
+ agentInstructions?: string[];
9
+ agentQuickstart?: {
10
+ doNotPreflight: boolean;
11
+ preferredFirstCommand: string;
12
+ };
13
+ configuredProviders?: ProviderConnectionStatus[];
6
14
  mutates?: boolean;
7
15
  safeForAgents?: boolean;
8
16
  flags: Array<{
@@ -15,3 +23,4 @@ export interface CommandSchema {
15
23
  }
16
24
  export declare const commandSchemas: CommandSchema[];
17
25
  export declare function getCommandSchema(name?: string): CommandSchema[] | CommandSchema | undefined;
26
+ export declare function getCommandSchemaForAgents(name?: string): Promise<CommandSchema[] | CommandSchema | undefined>;
@@ -1,3 +1,4 @@
1
+ import { listProviderStatuses } from './providers/status.js';
1
2
  export const commandSchemas = [
2
3
  {
3
4
  name: 'link',
@@ -9,7 +10,16 @@ export const commandSchemas = [
9
10
  'doomain link --domain example.com --subdomain app --project my-app --json',
10
11
  'doomain link --provider spaceship --domain example.com --apex --project my-app --dry-run --json',
11
12
  ],
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
+ 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
+ },
13
23
  mutates: true,
14
24
  safeForAgents: true,
15
25
  flags: [
@@ -19,7 +29,7 @@ export const commandSchemas = [
19
29
  { name: 'subdomain', type: 'string', description: 'Subdomain to add.' },
20
30
  { name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
21
31
  { name: 'project', type: 'string', description: 'Vercel project id/name. Optional when project inference succeeds.' },
22
- { name: 'dry-run', type: 'boolean', description: 'Preview changes without writing.' },
32
+ { name: 'dry-run', type: 'boolean', description: 'Preview changes without writing. Intended for human previews; agents should not use this unless explicitly asked.' },
23
33
  { name: 'force', type: 'boolean', description: 'Overwrite conflicting DNS records.' },
24
34
  { name: 'wait', type: 'boolean', description: 'Wait for DNS and Vercel verification.', default: true },
25
35
  { name: 'timeout', type: 'integer', description: 'Wait timeout in seconds.', default: 300 },
@@ -33,6 +43,7 @@ export const commandSchemas = [
33
43
  'doomain providers connect spaceship --credential apiKey=key --credential apiSecret=secret --json',
34
44
  'doomain providers connect namecheap --credential apiUser=user --credential apiKey=key --credential clientIp=127.0.0.1 --json',
35
45
  'doomain providers connect cloudflare --credential apiToken=token --credential accountId=account_id --json',
46
+ 'doomain providers connect hostinger --credential apiToken=token --json',
36
47
  ],
37
48
  flags: [
38
49
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
@@ -64,7 +75,7 @@ export const commandSchemas = [
64
75
  {
65
76
  name: 'providers disconnect',
66
77
  description: 'Remove saved DNS provider credentials locally.',
67
- examples: ['doomain providers disconnect namecheap --json', 'doomain providers disconnect cloudflare --json'],
78
+ examples: ['doomain providers disconnect namecheap --json', 'doomain providers disconnect cloudflare --json', 'doomain providers disconnect hostinger --json'],
68
79
  flags: [
69
80
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
70
81
  ],
@@ -72,7 +83,7 @@ export const commandSchemas = [
72
83
  {
73
84
  name: 'providers verify',
74
85
  description: 'Verify saved DNS provider credentials.',
75
- examples: ['doomain providers verify spaceship --json', 'doomain providers verify namecheap --json'],
86
+ examples: ['doomain providers verify spaceship --json', 'doomain providers verify namecheap --json', 'doomain providers verify hostinger --json'],
76
87
  flags: [
77
88
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
78
89
  ],
@@ -101,3 +112,26 @@ export function getCommandSchema(name) {
101
112
  return commandSchemas;
102
113
  return commandSchemas.find((schema) => schema.name === name);
103
114
  }
115
+ async function configuredProviders() {
116
+ return (await listProviderStatuses({ verify: false })).map((provider) => ({
117
+ configured: provider.configured,
118
+ default: provider.default,
119
+ displayName: provider.displayName,
120
+ docsUrl: provider.docsUrl,
121
+ id: provider.id,
122
+ }));
123
+ }
124
+ function withProviderConnections(schema, providers) {
125
+ if (schema.name !== 'link')
126
+ return schema;
127
+ return { ...schema, configuredProviders: providers };
128
+ }
129
+ export async function getCommandSchemaForAgents(name) {
130
+ const schema = getCommandSchema(name);
131
+ if (!schema)
132
+ return undefined;
133
+ const providers = await configuredProviders();
134
+ if (Array.isArray(schema))
135
+ return schema.map((item) => withProviderConnections(item, providers));
136
+ return withProviderConnections(schema, providers);
137
+ }
@@ -5,7 +5,7 @@ import { loadConfig } from './config.js';
5
5
  import { DoomainError } from './errors.js';
6
6
  import { detectLocalVercelProject } from './local-vercel.js';
7
7
  import { createProvider, getProviderDefinition, listProviderDefinitions } from './providers/registry.js';
8
- import { isProviderConfigured } from './providers/status.js';
8
+ import { isProviderConfigured, listProviderStatuses } from './providers/status.js';
9
9
  import { normalizeDomain, normalizeSubdomain } from './validate.js';
10
10
  import { createVercelClient, resolveVercelConfig, VERCEL_APEX_A_RECORD, VERCEL_CNAME_RECORD } from './vercel.js';
11
11
  function cleanDnsValue(value) {
@@ -115,6 +115,15 @@ async function resolveZone(provider, zoneDomain) {
115
115
  }
116
116
  return zone;
117
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
+ }
118
127
  function resolveRequestedDomain(opts) {
119
128
  if (opts.apex && opts.subdomain) {
120
129
  throw new DoomainError('INVALID_INPUT', 'Use either --apex or --subdomain, not both.');
@@ -168,7 +177,11 @@ async function loadConfiguredProviderZones(providerId) {
168
177
  const config = await loadConfig();
169
178
  const definitions = listProviderDefinitions().filter((definition) => isProviderConfigured(definition, config));
170
179
  if (definitions.length === 0) {
171
- 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
+ });
172
185
  }
173
186
  const results = await Promise.all(definitions.map(async (definition) => {
174
187
  try {
@@ -206,7 +219,13 @@ async function resolveProviderTarget(input) {
206
219
  const providerMessage = input.provider
207
220
  ? `${getProviderDefinition(input.provider).displayName} does not have a matching DNS zone for ${requested.fullDomain}.`
208
221
  : `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
209
- 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
+ });
210
229
  }
211
230
  const bestLength = matches[0].zone.name.length;
212
231
  const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
@@ -0,0 +1,23 @@
1
+ import type { DnsChange, DnsChangePlan, DnsProvider, DnsProviderDefinition, DnsRecord, DnsRecordInput, DnsZone, ProviderCapabilities, ProviderContext, ProviderHealth } from '../types.js';
2
+ export declare class HostingerProvider implements DnsProvider {
3
+ readonly capabilities: ProviderCapabilities;
4
+ readonly id = "hostinger";
5
+ readonly name = "Hostinger";
6
+ private readonly http;
7
+ constructor(context: ProviderContext);
8
+ verifyCredentials(): Promise<ProviderHealth>;
9
+ listZones(): Promise<DnsZone[]>;
10
+ getZone(domain: string): Promise<DnsZone | null>;
11
+ listRecords(zone: DnsZone): Promise<DnsRecord[]>;
12
+ planChanges(zone: DnsZone, desired: DnsRecordInput[], opts?: {
13
+ force?: boolean;
14
+ }): Promise<DnsChangePlan>;
15
+ applyChanges(zone: DnsZone, plan: DnsChangePlan): Promise<{
16
+ applied: DnsChange[];
17
+ skipped: DnsRecordInput[];
18
+ }>;
19
+ upsertRecord(zone: DnsZone, record: DnsRecordInput): Promise<DnsRecord>;
20
+ deleteRecord(zone: DnsZone, record: DnsRecord): Promise<void>;
21
+ private putRecords;
22
+ }
23
+ export declare const hostingerProviderDefinition: DnsProviderDefinition;
@@ -0,0 +1,162 @@
1
+ import { normalizeDomain } from '../../validate.js';
2
+ import { createProviderHttpClient } from '../core/http.js';
3
+ import { assertNoConflicts, planDnsChanges } from '../core/planner.js';
4
+ const HOSTINGER_API_URL = 'https://developers.hostinger.com';
5
+ const capabilities = {
6
+ defaultTtl: 14_400,
7
+ recordTypes: ['A', 'AAAA', 'CNAME', 'MX', 'TXT'],
8
+ supportsApexCname: false,
9
+ supportsBulkWrites: true,
10
+ supportsPagination: false,
11
+ supportsProxying: false,
12
+ supportsRecordIds: false,
13
+ };
14
+ function isSupportedRecordType(type) {
15
+ return capabilities.recordTypes.includes(type);
16
+ }
17
+ function cleanRecordName(name, zoneName) {
18
+ const normalized = name.trim().toLowerCase().replace(/\.$/, '');
19
+ const normalizedZone = normalizeDomain(zoneName);
20
+ if (normalized === '@' || normalized === normalizedZone)
21
+ return '@';
22
+ if (normalized.endsWith(`.${normalizedZone}`))
23
+ return normalized.slice(0, -(normalizedZone.length + 1)) || '@';
24
+ return normalized;
25
+ }
26
+ function toZone(domain) {
27
+ if (!domain.domain)
28
+ return null;
29
+ if (domain.status && domain.status !== 'active')
30
+ return null;
31
+ try {
32
+ const name = normalizeDomain(domain.domain);
33
+ return { id: name, metadata: { hostinger: domain }, name };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function toDnsRecords(record, zone) {
40
+ if (!record.name || !record.type || !isSupportedRecordType(record.type))
41
+ return [];
42
+ const name = cleanRecordName(record.name, zone.name);
43
+ const type = record.type;
44
+ return (record.records ?? []).flatMap((item) => {
45
+ const content = item.content;
46
+ if (!content || item.is_disabled)
47
+ return [];
48
+ return [
49
+ {
50
+ metadata: { hostinger: { ...record, records: [item] } },
51
+ name,
52
+ ttl: record.ttl,
53
+ type,
54
+ value: content,
55
+ },
56
+ ];
57
+ });
58
+ }
59
+ function toHostingerRecord(record) {
60
+ return {
61
+ name: record.name,
62
+ records: [{ content: record.value }],
63
+ ttl: record.ttl ?? capabilities.defaultTtl,
64
+ type: record.type,
65
+ };
66
+ }
67
+ function deleteFilter(record) {
68
+ return { name: record.name, type: record.type };
69
+ }
70
+ export class HostingerProvider {
71
+ capabilities = capabilities;
72
+ id = 'hostinger';
73
+ name = 'Hostinger';
74
+ http;
75
+ constructor(context) {
76
+ this.http = createProviderHttpClient({
77
+ baseUrl: HOSTINGER_API_URL,
78
+ errorMessages: {
79
+ 401: 'Hostinger rejected the API token. Re-run `doomain providers connect hostinger` with a valid token.',
80
+ 404: 'Hostinger could not find a writable DNS zone for this domain. Make sure the domain is active in Hostinger before linking it.',
81
+ 422: 'Hostinger rejected the DNS record payload.',
82
+ 429: 'Hostinger rate limit exceeded. Try again later.',
83
+ },
84
+ headers: { Authorization: `Bearer ${context.credentials.apiToken}` },
85
+ providerId: this.id,
86
+ signal: context.signal,
87
+ });
88
+ }
89
+ async verifyCredentials() {
90
+ await this.listZones();
91
+ return { ok: true };
92
+ }
93
+ async listZones() {
94
+ const domains = await this.http.request('/api/domains/v1/portfolio');
95
+ return domains.flatMap((domain) => {
96
+ const zone = toZone(domain);
97
+ return zone ? [zone] : [];
98
+ });
99
+ }
100
+ async getZone(domain) {
101
+ const normalized = normalizeDomain(domain);
102
+ const zones = await this.listZones();
103
+ return zones.find((zone) => zone.name === normalized) ?? null;
104
+ }
105
+ async listRecords(zone) {
106
+ const records = await this.http.request(`/api/dns/v1/zones/${encodeURIComponent(zone.name)}`);
107
+ return records.flatMap((record) => toDnsRecords(record, zone));
108
+ }
109
+ async planChanges(zone, desired, opts = {}) {
110
+ return planDnsChanges({ desired, existing: await this.listRecords(zone), force: opts.force, providerId: this.id, zone });
111
+ }
112
+ async applyChanges(zone, plan) {
113
+ assertNoConflicts(this.id, plan);
114
+ const applied = [];
115
+ const skipped = [];
116
+ for (const change of plan.changes) {
117
+ if (change.action === 'skip') {
118
+ skipped.push(change.record);
119
+ continue;
120
+ }
121
+ if (change.action === 'delete')
122
+ await this.deleteRecord(zone, change.existing);
123
+ else
124
+ await this.putRecords(zone, [change.record], change.action === 'update');
125
+ applied.push(change);
126
+ }
127
+ return { applied, skipped };
128
+ }
129
+ async upsertRecord(zone, record) {
130
+ await this.putRecords(zone, [record], true);
131
+ return { ...record, ttl: record.ttl ?? capabilities.defaultTtl };
132
+ }
133
+ async deleteRecord(zone, record) {
134
+ await this.http.request(`/api/dns/v1/zones/${encodeURIComponent(zone.name)}`, {
135
+ body: { filters: [deleteFilter(record)] },
136
+ method: 'DELETE',
137
+ });
138
+ }
139
+ async putRecords(zone, records, overwrite) {
140
+ await this.http.request(`/api/dns/v1/zones/${encodeURIComponent(zone.name)}`, {
141
+ body: { overwrite, zone: records.map(toHostingerRecord) },
142
+ method: 'PUT',
143
+ });
144
+ }
145
+ }
146
+ export const hostingerProviderDefinition = {
147
+ capabilities,
148
+ credentials: [{ env: 'HOSTINGER_API_TOKEN', key: 'apiToken', label: 'API token', required: true, secret: true }],
149
+ displayName: 'Hostinger',
150
+ docsUrl: 'https://developers.hostinger.com/',
151
+ id: 'hostinger',
152
+ name: 'Hostinger',
153
+ setup: {
154
+ notes: [
155
+ 'Create a Hostinger API token from hPanel Account > API with access to the domains you want Doomain to manage.',
156
+ 'Doomain uses the Hostinger domain portfolio API to infer zones and the DNS zone API to update records.',
157
+ ],
158
+ },
159
+ create(context) {
160
+ return new HostingerProvider(context);
161
+ },
162
+ };
@@ -2,9 +2,10 @@ import { DoomainError } from '../errors.js';
2
2
  import { ensureProviderId } from '../validate.js';
3
3
  import { cloudflareProviderDefinition } from './cloudflare/index.js';
4
4
  import { createProviderContext } from './core/config.js';
5
+ import { hostingerProviderDefinition } from './hostinger/index.js';
5
6
  import { namecheapProviderDefinition } from './namecheap/index.js';
6
7
  import { spaceshipProviderDefinition } from './spaceship/index.js';
7
- const definitions = [spaceshipProviderDefinition, namecheapProviderDefinition, cloudflareProviderDefinition];
8
+ const definitions = [spaceshipProviderDefinition, namecheapProviderDefinition, cloudflareProviderDefinition, hostingerProviderDefinition];
8
9
  export function listProviderDefinitions() {
9
10
  return definitions;
10
11
  }
@@ -301,41 +301,6 @@
301
301
  "list.js"
302
302
  ]
303
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
- },
339
304
  "providers:add": {
340
305
  "aliases": [],
341
306
  "args": {
@@ -596,6 +561,41 @@
596
561
  "verify.js"
597
562
  ]
598
563
  },
564
+ "projects:list": {
565
+ "aliases": [],
566
+ "args": {},
567
+ "description": "List Vercel projects.",
568
+ "flags": {
569
+ "json": {
570
+ "description": "Output a single JSON object and never prompt.",
571
+ "name": "json",
572
+ "allowNo": false,
573
+ "type": "boolean"
574
+ },
575
+ "search": {
576
+ "description": "Filter projects by search term.",
577
+ "name": "search",
578
+ "hasDynamicHelp": false,
579
+ "multiple": false,
580
+ "type": "option"
581
+ }
582
+ },
583
+ "hasDynamicHelp": false,
584
+ "hiddenAliases": [],
585
+ "id": "projects:list",
586
+ "pluginAlias": "doomain",
587
+ "pluginName": "doomain",
588
+ "pluginType": "core",
589
+ "strict": true,
590
+ "enableJsonFlag": false,
591
+ "isESM": true,
592
+ "relativePath": [
593
+ "dist",
594
+ "commands",
595
+ "projects",
596
+ "list.js"
597
+ ]
598
+ },
599
599
  "auth:logout:vercel": {
600
600
  "aliases": [],
601
601
  "args": {},
@@ -630,5 +630,5 @@
630
630
  ]
631
631
  }
632
632
  },
633
- "version": "0.1.7"
633
+ "version": "0.1.9"
634
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.7",
4
+ "version": "0.1.9",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {