doomain 0.1.16 → 0.1.18
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 +26 -0
- package/bin/dev.js +2 -2
- package/bin/run.js +2 -2
- package/dist/commands/domains/find.d.ts +15 -0
- package/dist/commands/domains/find.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/clerk-domain.js +2 -1
- package/dist/lib/command-schema.js +17 -0
- package/dist/lib/domain-provider.d.ts +53 -0
- package/dist/lib/domain-provider.js +230 -0
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/link-domain.d.ts +0 -14
- package/dist/lib/link-domain.js +2 -176
- package/oclif.manifest.json +61 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -337,6 +337,7 @@ Useful agent-safe commands:
|
|
|
337
337
|
doomain link app.example.com --project my-app --json
|
|
338
338
|
doomain providers list --json
|
|
339
339
|
doomain providers status --no-verify --json
|
|
340
|
+
doomain domains find hacktheandes.com --json
|
|
340
341
|
doomain domains list --provider cloudflare --domain example.com --json
|
|
341
342
|
doomain projects list --search my-app --json
|
|
342
343
|
doomain clerk domains add example.com --app app_123 --json
|
|
@@ -351,6 +352,19 @@ doomain schema --json
|
|
|
351
352
|
doomain schema "providers connect" --json
|
|
352
353
|
```
|
|
353
354
|
|
|
355
|
+
## Programmatic API
|
|
356
|
+
|
|
357
|
+
Use `findDomainProvider` to perform the same discovery from TypeScript or JavaScript:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import {findDomainProvider} from 'doomain'
|
|
361
|
+
|
|
362
|
+
const match = await findDomainProvider({domain: 'api.hacktheandes.com'})
|
|
363
|
+
console.log(match.provider, match.account, match.zoneDomain)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The API checks configured provider accounts, tolerates failures from individual providers, and returns the longest matching DNS zone. Pass `provider` or `account` to constrain the search. When a provider cannot be checked, `complete` is `false` and `warnings` identifies the affected provider account.
|
|
367
|
+
|
|
354
368
|
## Command Reference
|
|
355
369
|
|
|
356
370
|
Run `doomain help <command>` for oclif-generated help.
|
|
@@ -497,6 +511,18 @@ doomain providers logout namecheap --json
|
|
|
497
511
|
|
|
498
512
|
Environment variables for that provider still override local config after disconnect.
|
|
499
513
|
|
|
514
|
+
### `doomain domains find [domain]`
|
|
515
|
+
|
|
516
|
+
Finds the configured DNS provider account with the longest matching zone. It checks all configured accounts and continues when an individual provider fails, so one expired credential does not hide a match from another provider.
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
doomain domains find hacktheandes.com --json
|
|
520
|
+
doomain domains find api.example.com --json
|
|
521
|
+
doomain domains find --domain example.com --provider spaceship --account personal --json
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Successful JSON includes the provider, account, matching zone, and relative DNS record name. Check `complete` and `warnings` before treating the result as exhaustive; for example, an expired token may prevent one provider from participating in discovery.
|
|
525
|
+
|
|
500
526
|
### `doomain domains list`
|
|
501
527
|
|
|
502
528
|
Lists DNS zones and records.
|
package/bin/dev.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
|
|
2
2
|
|
|
3
|
-
import {execute} from '@oclif/core'
|
|
3
|
+
import { execute } from '@oclif/core'
|
|
4
4
|
|
|
5
|
-
await execute({development: true, dir: import.meta.url})
|
|
5
|
+
await execute({ development: true, dir: import.meta.url })
|
package/bin/run.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {execute} from '@oclif/core'
|
|
3
|
+
import { execute } from '@oclif/core'
|
|
4
4
|
|
|
5
5
|
const args = process.argv.slice(2)
|
|
6
6
|
const routedArgs = args.length === 0 || (args.length === 1 && args[0] === '--json') ? ['wizard', ...args] : args
|
|
7
7
|
|
|
8
|
-
await execute({args: routedArgs, dir: import.meta.url})
|
|
8
|
+
await execute({ args: routedArgs, dir: import.meta.url })
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class DomainsFind extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
domain: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { findDomainProvider } from '../../lib/domain-provider.js';
|
|
3
|
+
import { DoomainError } from '../../lib/errors.js';
|
|
4
|
+
import { accountFlag, domainFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
|
|
5
|
+
import { createOutput, outputError } from '../../lib/output.js';
|
|
6
|
+
export default class DomainsFind extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
domain: Args.string({ description: 'Domain whose DNS provider should be found.', required: false }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Find the configured DNS provider and account for a domain.';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> <%= command.id %> example.com --json',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> api.example.com --json',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --domain example.com --provider spaceship --json',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
account: accountFlag,
|
|
18
|
+
domain: domainFlag,
|
|
19
|
+
json: jsonFlag,
|
|
20
|
+
provider: providerFlag,
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(DomainsFind);
|
|
24
|
+
const out = createOutput({ json: flags.json });
|
|
25
|
+
try {
|
|
26
|
+
const domain = flags.domain ?? args.domain;
|
|
27
|
+
if (!domain)
|
|
28
|
+
throw new DoomainError('MISSING_ARGUMENT', 'Domain is required. Pass it as an argument or use --domain.');
|
|
29
|
+
const result = await findDomainProvider({ account: flags.account, domain, provider: flags.provider });
|
|
30
|
+
const account = result.isDefaultAccount ? result.provider : `${result.provider}/${result.account}`;
|
|
31
|
+
out.info(`${result.domain} is managed by ${account} in DNS zone ${result.zoneDomain}.`);
|
|
32
|
+
for (const warning of result.warnings) {
|
|
33
|
+
const warningAccount = warning.isDefaultAccount ? warning.provider : `${warning.provider}/${warning.account}`;
|
|
34
|
+
out.warn(`${warningAccount} could not be checked: ${warning.error.message}`);
|
|
35
|
+
}
|
|
36
|
+
out.result(result);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
outputError(out.json, error, 'DOMAIN_PROVIDER_DISCOVERY_FAILED');
|
|
40
|
+
this.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/lib/clerk-domain.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createClerkPlatformClient, resolveClerkPlatformConfig } from './clerk.js';
|
|
2
|
+
import { resolveProviderTarget } from './domain-provider.js';
|
|
2
3
|
import { DoomainError } from './errors.js';
|
|
3
|
-
import {
|
|
4
|
+
import { withProviderRecordOptions } from './link-domain.js';
|
|
4
5
|
import { createProvider } from './providers/registry.js';
|
|
5
6
|
import { normalizeDomain } from './validate.js';
|
|
6
7
|
function relativeRecordName(host, zoneDomain) {
|
|
@@ -195,6 +195,23 @@ export const commandSchemas = [
|
|
|
195
195
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
196
196
|
],
|
|
197
197
|
},
|
|
198
|
+
{
|
|
199
|
+
name: 'domains find',
|
|
200
|
+
description: 'Find the configured DNS provider and account for a domain.',
|
|
201
|
+
examples: [
|
|
202
|
+
'doomain domains find hacktheandes.com --json',
|
|
203
|
+
'doomain domains find api.example.com --json',
|
|
204
|
+
'doomain domains find --domain example.com --provider spaceship --account work --json',
|
|
205
|
+
],
|
|
206
|
+
agentHint: 'Use this command when you need to identify who manages DNS for a domain. It checks all configured provider accounts, tolerates individual provider failures, and selects the longest matching DNS zone. Inspect complete and warnings before treating the result as exhaustive.',
|
|
207
|
+
safeForAgents: true,
|
|
208
|
+
flags: [
|
|
209
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
210
|
+
{ name: 'domain', type: 'string', description: 'Domain to find. May also be passed as the positional argument.' },
|
|
211
|
+
{ name: 'provider', type: 'string', description: 'Limit discovery to one DNS provider id.' },
|
|
212
|
+
{ name: 'account', type: 'string', description: 'Limit discovery to one DNS provider profile/account alias.' },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
198
215
|
{
|
|
199
216
|
name: 'domains list',
|
|
200
217
|
description: 'List DNS zones and records for a provider.',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type DoomainErrorCode } from './errors.js';
|
|
2
|
+
export interface FindDomainProviderInput {
|
|
3
|
+
account?: string;
|
|
4
|
+
domain: string;
|
|
5
|
+
provider?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProviderSearchWarning {
|
|
8
|
+
account: string;
|
|
9
|
+
error: {
|
|
10
|
+
code?: DoomainErrorCode;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
isDefaultAccount: boolean;
|
|
14
|
+
provider: string;
|
|
15
|
+
providerName: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DomainProviderResult {
|
|
18
|
+
account: string;
|
|
19
|
+
accountInferred: boolean;
|
|
20
|
+
complete: boolean;
|
|
21
|
+
domain: string;
|
|
22
|
+
isApex: boolean;
|
|
23
|
+
isDefaultAccount: boolean;
|
|
24
|
+
provider: string;
|
|
25
|
+
providerInferred: boolean;
|
|
26
|
+
recordName: string;
|
|
27
|
+
warnings: ProviderSearchWarning[];
|
|
28
|
+
zoneDomain: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ResolveProviderTargetInput extends FindDomainProviderInput {
|
|
31
|
+
apex?: boolean;
|
|
32
|
+
subdomain?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ResolveProviderTargetOptions {
|
|
35
|
+
tolerateProviderAccountErrors?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface ResolvedDnsTarget {
|
|
38
|
+
account: string;
|
|
39
|
+
accountInferred: boolean;
|
|
40
|
+
isDefaultAccount: boolean;
|
|
41
|
+
provider: string;
|
|
42
|
+
providerInferred: boolean;
|
|
43
|
+
target: {
|
|
44
|
+
fullDomain: string;
|
|
45
|
+
isApex: boolean;
|
|
46
|
+
recordName: string;
|
|
47
|
+
zoneDomain: string;
|
|
48
|
+
};
|
|
49
|
+
warnings: ProviderSearchWarning[];
|
|
50
|
+
}
|
|
51
|
+
export declare function resolveProviderTarget(input: ResolveProviderTargetInput, options?: ResolveProviderTargetOptions): Promise<ResolvedDnsTarget>;
|
|
52
|
+
/** Find the configured DNS provider account with the longest zone match for a domain. */
|
|
53
|
+
export declare function findDomainProvider(input: FindDomainProviderInput): Promise<DomainProviderResult>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { DoomainError } from './errors.js';
|
|
3
|
+
import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, } from './providers/core/config.js';
|
|
4
|
+
import { createProvider, getProviderDefinition, listProviderDefinitions } from './providers/registry.js';
|
|
5
|
+
import { listProviderStatuses } from './providers/status.js';
|
|
6
|
+
import { normalizeDomain, normalizeSubdomain } from './validate.js';
|
|
7
|
+
function resolveRequestedDomain(opts) {
|
|
8
|
+
if (opts.apex && opts.subdomain) {
|
|
9
|
+
throw new DoomainError('INVALID_INPUT', 'Use either --apex or --subdomain, not both.');
|
|
10
|
+
}
|
|
11
|
+
const domain = normalizeDomain(opts.domain);
|
|
12
|
+
if (opts.apex)
|
|
13
|
+
return { forceExactZone: true, fullDomain: domain };
|
|
14
|
+
if (!opts.subdomain)
|
|
15
|
+
return { forceExactZone: false, fullDomain: domain };
|
|
16
|
+
return { forceExactZone: false, fullDomain: `${normalizeSubdomain(opts.subdomain)}.${domain}` };
|
|
17
|
+
}
|
|
18
|
+
function zoneMatchesDomain(fullDomain, zoneDomain, forceExactZone) {
|
|
19
|
+
if (fullDomain === zoneDomain)
|
|
20
|
+
return true;
|
|
21
|
+
if (forceExactZone)
|
|
22
|
+
return false;
|
|
23
|
+
return fullDomain.endsWith(`.${zoneDomain}`);
|
|
24
|
+
}
|
|
25
|
+
function targetFromZone(fullDomain, zoneDomain) {
|
|
26
|
+
if (fullDomain === zoneDomain) {
|
|
27
|
+
return { fullDomain, isApex: true, recordName: '@', zoneDomain };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
fullDomain,
|
|
31
|
+
isApex: false,
|
|
32
|
+
recordName: fullDomain.slice(0, -(zoneDomain.length + 1)),
|
|
33
|
+
zoneDomain,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function candidateDetails(candidates) {
|
|
37
|
+
return candidates.map((candidate) => ({
|
|
38
|
+
account: candidate.account,
|
|
39
|
+
isDefaultAccount: candidate.isDefaultAccount,
|
|
40
|
+
provider: candidate.provider,
|
|
41
|
+
providerName: candidate.providerName,
|
|
42
|
+
zoneDomain: candidate.zone.name,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
function defaultAccountRef(providerId) {
|
|
46
|
+
return { account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId };
|
|
47
|
+
}
|
|
48
|
+
function explicitAccountRef(providerId, account) {
|
|
49
|
+
const normalized = normalizeProviderAccount(account);
|
|
50
|
+
return { account: normalized, isDefaultAccount: isDefaultProviderAccount(normalized), providerId };
|
|
51
|
+
}
|
|
52
|
+
function searchError(error) {
|
|
53
|
+
return {
|
|
54
|
+
...(error instanceof DoomainError ? { code: error.code } : {}),
|
|
55
|
+
message: error instanceof Error ? error.message : String(error),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function loadProviderZones(definition, account) {
|
|
59
|
+
const provider = await createProvider(definition.id, { account: account.account });
|
|
60
|
+
const zones = await provider.listZones();
|
|
61
|
+
return {
|
|
62
|
+
candidates: zones.map((zone) => ({
|
|
63
|
+
account: account.account,
|
|
64
|
+
isDefaultAccount: account.isDefaultAccount,
|
|
65
|
+
provider: definition.id,
|
|
66
|
+
providerName: definition.displayName,
|
|
67
|
+
zone,
|
|
68
|
+
})),
|
|
69
|
+
search: {
|
|
70
|
+
account: account.account,
|
|
71
|
+
displayName: definition.displayName,
|
|
72
|
+
id: definition.id,
|
|
73
|
+
isDefaultAccount: account.isDefaultAccount,
|
|
74
|
+
zones: zones.map((zone) => zone.name),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function loadProviderZonesSafely(definition, account) {
|
|
79
|
+
try {
|
|
80
|
+
return await loadProviderZones(definition, account);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
candidates: [],
|
|
85
|
+
search: {
|
|
86
|
+
account: account.account,
|
|
87
|
+
displayName: definition.displayName,
|
|
88
|
+
error: searchError(error),
|
|
89
|
+
id: definition.id,
|
|
90
|
+
isDefaultAccount: account.isDefaultAccount,
|
|
91
|
+
zones: [],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function providerConnectionDetails() {
|
|
97
|
+
return (await listProviderStatuses({ verify: false })).map((provider) => ({
|
|
98
|
+
configured: provider.configured,
|
|
99
|
+
account: provider.account,
|
|
100
|
+
default: provider.default,
|
|
101
|
+
displayName: provider.displayName,
|
|
102
|
+
docsUrl: provider.docsUrl,
|
|
103
|
+
id: provider.id,
|
|
104
|
+
isDefaultAccount: provider.isDefaultAccount,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
function searchWarnings(searches) {
|
|
108
|
+
return searches.flatMap((search) => search.error
|
|
109
|
+
? [
|
|
110
|
+
{
|
|
111
|
+
account: search.account,
|
|
112
|
+
error: search.error,
|
|
113
|
+
isDefaultAccount: search.isDefaultAccount,
|
|
114
|
+
provider: search.id,
|
|
115
|
+
providerName: search.displayName,
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
: []);
|
|
119
|
+
}
|
|
120
|
+
async function loadConfiguredProviderZones(providerId, accountInput, tolerateProviderAccountErrors = false) {
|
|
121
|
+
const config = await loadConfig();
|
|
122
|
+
const account = accountInput ? normalizeProviderAccount(accountInput) : undefined;
|
|
123
|
+
if (providerId) {
|
|
124
|
+
const definition = getProviderDefinition(providerId);
|
|
125
|
+
const accounts = account ? [explicitAccountRef(definition.id, account)] : listConfiguredProviderAccounts(config, definition);
|
|
126
|
+
const selectedAccounts = accounts.length > 0 ? accounts : [defaultAccountRef(definition.id)];
|
|
127
|
+
const tolerateAccountErrors = tolerateProviderAccountErrors && !account && selectedAccounts.length > 1;
|
|
128
|
+
const results = await Promise.all(selectedAccounts.map((ref) => tolerateAccountErrors ? loadProviderZonesSafely(definition, ref) : loadProviderZones(definition, ref)));
|
|
129
|
+
return {
|
|
130
|
+
accountInferred: account === undefined,
|
|
131
|
+
candidates: results.flatMap((result) => result.candidates),
|
|
132
|
+
providerInferred: false,
|
|
133
|
+
searched: results.map((result) => result.search),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const providerAccounts = listProviderDefinitions().flatMap((definition) => listConfiguredProviderAccounts(config, definition)
|
|
137
|
+
.filter((ref) => !account || ref.account === account)
|
|
138
|
+
.map((ref) => ({ definition, ref })));
|
|
139
|
+
if (providerAccounts.length === 0) {
|
|
140
|
+
const message = account
|
|
141
|
+
? `No DNS provider account named ${account} is configured. Run \`doomain providers connect <provider> --account ${account}\` first.`
|
|
142
|
+
: 'No DNS provider is configured. Run `doomain providers connect` first.';
|
|
143
|
+
throw new DoomainError('CONFIG_NOT_FOUND', message, {
|
|
144
|
+
account,
|
|
145
|
+
configuredProviders: await providerConnectionDetails(),
|
|
146
|
+
recovery: 'Connect the DNS provider that owns this domain, then retry `doomain link <domain> --json`.',
|
|
147
|
+
suggestedCommands: account
|
|
148
|
+
? [`doomain providers connect <provider> --account ${account}`, 'doomain link <domain> --json']
|
|
149
|
+
: ['doomain providers connect', 'doomain link <domain> --json'],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const results = await Promise.all(providerAccounts.map(({ definition, ref }) => loadProviderZonesSafely(definition, ref)));
|
|
153
|
+
return {
|
|
154
|
+
accountInferred: account === undefined,
|
|
155
|
+
candidates: results.flatMap((result) => result.candidates),
|
|
156
|
+
providerInferred: true,
|
|
157
|
+
searched: results.map((result) => result.search),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export async function resolveProviderTarget(input, options = {}) {
|
|
161
|
+
const requested = resolveRequestedDomain(input);
|
|
162
|
+
const zones = await loadConfiguredProviderZones(input.provider, input.account, options.tolerateProviderAccountErrors);
|
|
163
|
+
const matches = zones.candidates
|
|
164
|
+
.filter((candidate) => zoneMatchesDomain(requested.fullDomain, candidate.zone.name, requested.forceExactZone))
|
|
165
|
+
.sort((a, b) => b.zone.name.length - a.zone.name.length);
|
|
166
|
+
if (matches.length === 0) {
|
|
167
|
+
const account = input.account ? normalizeProviderAccount(input.account) : undefined;
|
|
168
|
+
const providerMessage = input.provider
|
|
169
|
+
? `${getProviderDefinition(input.provider).displayName}${account ? ` account ${account}` : ''} does not have a matching DNS zone for ${requested.fullDomain}.`
|
|
170
|
+
: `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
|
|
171
|
+
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, {
|
|
172
|
+
account,
|
|
173
|
+
configuredProviders: await providerConnectionDetails(),
|
|
174
|
+
domain: requested.fullDomain,
|
|
175
|
+
recovery: 'Retry with --provider <id> --account <alias> only if another configured provider account owns this zone. Otherwise connect the DNS provider account that owns this domain.',
|
|
176
|
+
searchedZones: zones.searched,
|
|
177
|
+
suggestedCommands: [`doomain link ${requested.fullDomain} --provider <id> --account <alias> --json`, 'doomain providers connect'],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const bestLength = matches[0].zone.name.length;
|
|
181
|
+
const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
|
|
182
|
+
const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider && item.account === candidate.account && item.zone.name === candidate.zone.name) === index);
|
|
183
|
+
if (uniqueBestMatches.length > 1) {
|
|
184
|
+
throw new DoomainError('PROVIDER_ZONE_AMBIGUOUS', `Multiple DNS provider accounts have a matching DNS zone for ${requested.fullDomain}. Pass --provider and --account to choose one.`, { candidates: candidateDetails(uniqueBestMatches), domain: requested.fullDomain });
|
|
185
|
+
}
|
|
186
|
+
const selected = uniqueBestMatches[0];
|
|
187
|
+
return {
|
|
188
|
+
account: selected.account,
|
|
189
|
+
accountInferred: zones.accountInferred,
|
|
190
|
+
isDefaultAccount: selected.isDefaultAccount,
|
|
191
|
+
provider: selected.provider,
|
|
192
|
+
providerInferred: zones.providerInferred,
|
|
193
|
+
target: targetFromZone(requested.fullDomain, selected.zone.name),
|
|
194
|
+
warnings: searchWarnings(zones.searched),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function discoveryError(error, domain) {
|
|
198
|
+
if (error.code !== 'CONFIG_NOT_FOUND' && error.code !== 'PROVIDER_ZONE_NOT_FOUND')
|
|
199
|
+
return error;
|
|
200
|
+
const details = error.details && typeof error.details === 'object' ? error.details : {};
|
|
201
|
+
return new DoomainError(error.code, error.message, {
|
|
202
|
+
...details,
|
|
203
|
+
recovery: `Connect or repair the DNS provider account that owns this domain, then retry \`doomain domains find ${domain} --json\`.`,
|
|
204
|
+
suggestedCommands: ['doomain providers connect', `doomain domains find ${domain} --json`],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Find the configured DNS provider account with the longest zone match for a domain. */
|
|
208
|
+
export async function findDomainProvider(input) {
|
|
209
|
+
try {
|
|
210
|
+
const resolved = await resolveProviderTarget(input, { tolerateProviderAccountErrors: true });
|
|
211
|
+
return {
|
|
212
|
+
account: resolved.account,
|
|
213
|
+
accountInferred: resolved.accountInferred,
|
|
214
|
+
complete: resolved.warnings.length === 0,
|
|
215
|
+
domain: resolved.target.fullDomain,
|
|
216
|
+
isApex: resolved.target.isApex,
|
|
217
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
218
|
+
provider: resolved.provider,
|
|
219
|
+
providerInferred: resolved.providerInferred,
|
|
220
|
+
recordName: resolved.target.recordName,
|
|
221
|
+
warnings: resolved.warnings,
|
|
222
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof DoomainError)
|
|
227
|
+
throw discoveryError(error, input.domain);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
|
|
1
|
+
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_PROVIDER_DISCOVERY_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
|
|
2
2
|
export declare class DoomainError extends Error {
|
|
3
3
|
readonly code: DoomainErrorCode;
|
|
4
4
|
readonly details?: unknown;
|
|
@@ -59,20 +59,6 @@ export interface LinkDomainResult extends LinkDomainPlan {
|
|
|
59
59
|
verified: boolean;
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
export interface ResolvedDnsTarget {
|
|
63
|
-
provider: string;
|
|
64
|
-
providerInferred: boolean;
|
|
65
|
-
account: string;
|
|
66
|
-
accountInferred: boolean;
|
|
67
|
-
isDefaultAccount: boolean;
|
|
68
|
-
target: {
|
|
69
|
-
fullDomain: string;
|
|
70
|
-
isApex: boolean;
|
|
71
|
-
recordName: string;
|
|
72
|
-
zoneDomain: string;
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
export declare function resolveProviderTarget(input: Pick<LinkDomainInput, 'account' | 'apex' | 'domain' | 'provider' | 'subdomain'>): Promise<ResolvedDnsTarget>;
|
|
76
62
|
export declare function withProviderRecordOptions(provider: string, record: DnsRecordInput): DnsRecordInput;
|
|
77
63
|
export declare function verificationRecords(raw: unknown, zoneDomain: string): DnsRecordInput[];
|
|
78
64
|
export declare function createLinkPlan(input: LinkDomainInput): Promise<LinkDomainPlan>;
|
package/dist/lib/link-domain.js
CHANGED
|
@@ -2,12 +2,10 @@ import { resolve4, resolveCname, resolveTxt } from 'node:dns/promises';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, parse } from 'node:path';
|
|
4
4
|
import { loadConfig } from './config.js';
|
|
5
|
+
import { resolveProviderTarget } from './domain-provider.js';
|
|
5
6
|
import { DoomainError } from './errors.js';
|
|
6
7
|
import { detectLocalVercelProject } from './local-vercel.js';
|
|
7
|
-
import {
|
|
8
|
-
import { createProvider, getProviderDefinition, listProviderDefinitions } from './providers/registry.js';
|
|
9
|
-
import { listProviderStatuses } from './providers/status.js';
|
|
10
|
-
import { normalizeDomain, normalizeSubdomain } from './validate.js';
|
|
8
|
+
import { createProvider } from './providers/registry.js';
|
|
11
9
|
import { createVercelClient, resolveVercelConfig, VERCEL_APEX_A_RECORD, VERCEL_CNAME_RECORD } from './vercel.js';
|
|
12
10
|
function cleanDnsValue(value) {
|
|
13
11
|
return value.toLowerCase().replace(/\.$/, '');
|
|
@@ -116,178 +114,6 @@ async function resolveZone(provider, zoneDomain) {
|
|
|
116
114
|
}
|
|
117
115
|
return zone;
|
|
118
116
|
}
|
|
119
|
-
async function providerConnectionDetails() {
|
|
120
|
-
return (await listProviderStatuses({ verify: false })).map((provider) => ({
|
|
121
|
-
configured: provider.configured,
|
|
122
|
-
account: provider.account,
|
|
123
|
-
default: provider.default,
|
|
124
|
-
displayName: provider.displayName,
|
|
125
|
-
docsUrl: provider.docsUrl,
|
|
126
|
-
id: provider.id,
|
|
127
|
-
isDefaultAccount: provider.isDefaultAccount,
|
|
128
|
-
}));
|
|
129
|
-
}
|
|
130
|
-
function resolveRequestedDomain(opts) {
|
|
131
|
-
if (opts.apex && opts.subdomain) {
|
|
132
|
-
throw new DoomainError('INVALID_INPUT', 'Use either --apex or --subdomain, not both.');
|
|
133
|
-
}
|
|
134
|
-
const domain = normalizeDomain(opts.domain);
|
|
135
|
-
if (opts.apex)
|
|
136
|
-
return { forceExactZone: true, fullDomain: domain };
|
|
137
|
-
if (!opts.subdomain)
|
|
138
|
-
return { forceExactZone: false, fullDomain: domain };
|
|
139
|
-
return { forceExactZone: false, fullDomain: `${normalizeSubdomain(opts.subdomain)}.${domain}` };
|
|
140
|
-
}
|
|
141
|
-
function zoneMatchesDomain(fullDomain, zoneDomain, forceExactZone) {
|
|
142
|
-
if (fullDomain === zoneDomain)
|
|
143
|
-
return true;
|
|
144
|
-
if (forceExactZone)
|
|
145
|
-
return false;
|
|
146
|
-
return fullDomain.endsWith(`.${zoneDomain}`);
|
|
147
|
-
}
|
|
148
|
-
function targetFromZone(fullDomain, zoneDomain) {
|
|
149
|
-
if (fullDomain === zoneDomain) {
|
|
150
|
-
return { fullDomain, isApex: true, recordName: '@', zoneDomain };
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
fullDomain,
|
|
154
|
-
isApex: false,
|
|
155
|
-
recordName: fullDomain.slice(0, -(zoneDomain.length + 1)),
|
|
156
|
-
zoneDomain,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
function candidateDetails(candidates) {
|
|
160
|
-
return candidates.map((candidate) => ({
|
|
161
|
-
account: candidate.account,
|
|
162
|
-
isDefaultAccount: candidate.isDefaultAccount,
|
|
163
|
-
provider: candidate.provider,
|
|
164
|
-
providerName: candidate.providerName,
|
|
165
|
-
zoneDomain: candidate.zone.name,
|
|
166
|
-
}));
|
|
167
|
-
}
|
|
168
|
-
function defaultAccountRef(providerId) {
|
|
169
|
-
return { account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId };
|
|
170
|
-
}
|
|
171
|
-
function explicitAccountRef(providerId, account) {
|
|
172
|
-
const normalized = normalizeProviderAccount(account);
|
|
173
|
-
return { account: normalized, isDefaultAccount: isDefaultProviderAccount(normalized), providerId };
|
|
174
|
-
}
|
|
175
|
-
async function loadProviderZones(definition, account) {
|
|
176
|
-
const provider = await createProvider(definition.id, { account: account.account });
|
|
177
|
-
const zones = await provider.listZones();
|
|
178
|
-
return {
|
|
179
|
-
candidates: zones.map((zone) => ({
|
|
180
|
-
account: account.account,
|
|
181
|
-
isDefaultAccount: account.isDefaultAccount,
|
|
182
|
-
provider: definition.id,
|
|
183
|
-
providerName: definition.displayName,
|
|
184
|
-
zone,
|
|
185
|
-
})),
|
|
186
|
-
search: {
|
|
187
|
-
account: account.account,
|
|
188
|
-
displayName: definition.displayName,
|
|
189
|
-
id: definition.id,
|
|
190
|
-
isDefaultAccount: account.isDefaultAccount,
|
|
191
|
-
zones: zones.map((zone) => zone.name),
|
|
192
|
-
},
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
async function loadConfiguredProviderZones(providerId, accountInput) {
|
|
196
|
-
const config = await loadConfig();
|
|
197
|
-
const account = accountInput ? normalizeProviderAccount(accountInput) : undefined;
|
|
198
|
-
if (providerId) {
|
|
199
|
-
const definition = getProviderDefinition(providerId);
|
|
200
|
-
const accounts = account ? [explicitAccountRef(definition.id, account)] : listConfiguredProviderAccounts(config, definition);
|
|
201
|
-
const selectedAccounts = accounts.length > 0 ? accounts : [defaultAccountRef(definition.id)];
|
|
202
|
-
const results = await Promise.all(selectedAccounts.map((ref) => loadProviderZones(definition, ref)));
|
|
203
|
-
return {
|
|
204
|
-
accountInferred: account === undefined,
|
|
205
|
-
candidates: results.flatMap((result) => result.candidates),
|
|
206
|
-
providerInferred: false,
|
|
207
|
-
searched: results.map((result) => result.search),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
const providerAccounts = listProviderDefinitions().flatMap((definition) => listConfiguredProviderAccounts(config, definition)
|
|
211
|
-
.filter((ref) => !account || ref.account === account)
|
|
212
|
-
.map((ref) => ({ definition, ref })));
|
|
213
|
-
if (providerAccounts.length === 0) {
|
|
214
|
-
const message = account
|
|
215
|
-
? `No DNS provider account named ${account} is configured. Run \`doomain providers connect <provider> --account ${account}\` first.`
|
|
216
|
-
: 'No DNS provider is configured. Run `doomain providers connect` first.';
|
|
217
|
-
throw new DoomainError('CONFIG_NOT_FOUND', message, {
|
|
218
|
-
account,
|
|
219
|
-
configuredProviders: await providerConnectionDetails(),
|
|
220
|
-
recovery: 'Connect the DNS provider that owns this domain, then retry `doomain link <domain> --json`.',
|
|
221
|
-
suggestedCommands: account
|
|
222
|
-
? [`doomain providers connect <provider> --account ${account}`, 'doomain link <domain> --json']
|
|
223
|
-
: ['doomain providers connect', 'doomain link <domain> --json'],
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
const results = await Promise.all(providerAccounts.map(async ({ definition, ref }) => {
|
|
227
|
-
try {
|
|
228
|
-
return await loadProviderZones(definition, ref);
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
return {
|
|
232
|
-
candidates: [],
|
|
233
|
-
search: {
|
|
234
|
-
account: ref.account,
|
|
235
|
-
displayName: definition.displayName,
|
|
236
|
-
error: error instanceof Error ? error.message : String(error),
|
|
237
|
-
id: definition.id,
|
|
238
|
-
isDefaultAccount: ref.isDefaultAccount,
|
|
239
|
-
zones: [],
|
|
240
|
-
},
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
}));
|
|
244
|
-
return {
|
|
245
|
-
accountInferred: account === undefined,
|
|
246
|
-
candidates: results.flatMap((result) => result.candidates),
|
|
247
|
-
providerInferred: true,
|
|
248
|
-
searched: results.map((result) => result.search),
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
export async function resolveProviderTarget(input) {
|
|
252
|
-
const requested = resolveRequestedDomain({
|
|
253
|
-
apex: input.apex,
|
|
254
|
-
domain: await resolveConfiguredDomain(input.domain),
|
|
255
|
-
subdomain: input.subdomain,
|
|
256
|
-
});
|
|
257
|
-
const zones = await loadConfiguredProviderZones(input.provider, input.account);
|
|
258
|
-
const matches = zones.candidates
|
|
259
|
-
.filter((candidate) => zoneMatchesDomain(requested.fullDomain, candidate.zone.name, requested.forceExactZone))
|
|
260
|
-
.sort((a, b) => b.zone.name.length - a.zone.name.length);
|
|
261
|
-
if (matches.length === 0) {
|
|
262
|
-
const account = input.account ? normalizeProviderAccount(input.account) : undefined;
|
|
263
|
-
const providerMessage = input.provider
|
|
264
|
-
? `${getProviderDefinition(input.provider).displayName}${account ? ` account ${account}` : ''} does not have a matching DNS zone for ${requested.fullDomain}.`
|
|
265
|
-
: `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
|
|
266
|
-
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, {
|
|
267
|
-
account,
|
|
268
|
-
configuredProviders: await providerConnectionDetails(),
|
|
269
|
-
domain: requested.fullDomain,
|
|
270
|
-
recovery: 'Retry with --provider <id> --account <alias> only if another configured provider account owns this zone. Otherwise connect the DNS provider account that owns this domain.',
|
|
271
|
-
searchedZones: zones.searched,
|
|
272
|
-
suggestedCommands: [`doomain link ${requested.fullDomain} --provider <id> --account <alias> --json`, 'doomain providers connect'],
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
const bestLength = matches[0].zone.name.length;
|
|
276
|
-
const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
|
|
277
|
-
const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider && item.account === candidate.account && item.zone.name === candidate.zone.name) === index);
|
|
278
|
-
if (uniqueBestMatches.length > 1) {
|
|
279
|
-
throw new DoomainError('PROVIDER_ZONE_AMBIGUOUS', `Multiple DNS provider accounts have a matching DNS zone for ${requested.fullDomain}. Pass --provider and --account to choose one.`, { candidates: candidateDetails(uniqueBestMatches), domain: requested.fullDomain });
|
|
280
|
-
}
|
|
281
|
-
const selected = uniqueBestMatches[0];
|
|
282
|
-
return {
|
|
283
|
-
account: selected.account,
|
|
284
|
-
accountInferred: zones.accountInferred,
|
|
285
|
-
isDefaultAccount: selected.isDefaultAccount,
|
|
286
|
-
provider: selected.provider,
|
|
287
|
-
providerInferred: zones.providerInferred,
|
|
288
|
-
target: targetFromZone(requested.fullDomain, selected.zone.name),
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
117
|
export function withProviderRecordOptions(provider, record) {
|
|
292
118
|
if (provider !== 'cloudflare' || !['A', 'AAAA', 'CNAME'].includes(record.type))
|
|
293
119
|
return record;
|
package/oclif.manifest.json
CHANGED
|
@@ -312,6 +312,66 @@
|
|
|
312
312
|
"vercel.js"
|
|
313
313
|
]
|
|
314
314
|
},
|
|
315
|
+
"domains:find": {
|
|
316
|
+
"aliases": [],
|
|
317
|
+
"args": {
|
|
318
|
+
"domain": {
|
|
319
|
+
"description": "Domain whose DNS provider should be found.",
|
|
320
|
+
"name": "domain",
|
|
321
|
+
"required": false
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
"description": "Find the configured DNS provider and account for a domain.",
|
|
325
|
+
"examples": [
|
|
326
|
+
"<%= config.bin %> <%= command.id %> example.com --json",
|
|
327
|
+
"<%= config.bin %> <%= command.id %> api.example.com --json",
|
|
328
|
+
"<%= config.bin %> <%= command.id %> --domain example.com --provider spaceship --json"
|
|
329
|
+
],
|
|
330
|
+
"flags": {
|
|
331
|
+
"account": {
|
|
332
|
+
"description": "DNS provider profile/account alias. Defaults to the provider default account.",
|
|
333
|
+
"name": "account",
|
|
334
|
+
"hasDynamicHelp": false,
|
|
335
|
+
"multiple": false,
|
|
336
|
+
"type": "option"
|
|
337
|
+
},
|
|
338
|
+
"domain": {
|
|
339
|
+
"description": "Target domain or base zone, for example app.example.com or example.com.",
|
|
340
|
+
"name": "domain",
|
|
341
|
+
"hasDynamicHelp": false,
|
|
342
|
+
"multiple": false,
|
|
343
|
+
"type": "option"
|
|
344
|
+
},
|
|
345
|
+
"json": {
|
|
346
|
+
"description": "Output a single JSON object and never prompt.",
|
|
347
|
+
"name": "json",
|
|
348
|
+
"allowNo": false,
|
|
349
|
+
"type": "boolean"
|
|
350
|
+
},
|
|
351
|
+
"provider": {
|
|
352
|
+
"description": "DNS provider id. Inferred from the target domain when omitted.",
|
|
353
|
+
"name": "provider",
|
|
354
|
+
"hasDynamicHelp": false,
|
|
355
|
+
"multiple": false,
|
|
356
|
+
"type": "option"
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
"hasDynamicHelp": false,
|
|
360
|
+
"hiddenAliases": [],
|
|
361
|
+
"id": "domains:find",
|
|
362
|
+
"pluginAlias": "doomain",
|
|
363
|
+
"pluginName": "doomain",
|
|
364
|
+
"pluginType": "core",
|
|
365
|
+
"strict": true,
|
|
366
|
+
"enableJsonFlag": false,
|
|
367
|
+
"isESM": true,
|
|
368
|
+
"relativePath": [
|
|
369
|
+
"dist",
|
|
370
|
+
"commands",
|
|
371
|
+
"domains",
|
|
372
|
+
"find.js"
|
|
373
|
+
]
|
|
374
|
+
},
|
|
315
375
|
"domains:list": {
|
|
316
376
|
"aliases": [],
|
|
317
377
|
"args": {},
|
|
@@ -834,5 +894,5 @@
|
|
|
834
894
|
]
|
|
835
895
|
}
|
|
836
896
|
},
|
|
837
|
-
"version": "0.1.
|
|
897
|
+
"version": "0.1.18"
|
|
838
898
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doomain",
|
|
3
3
|
"description": "Configure Vercel and Clerk production domains in seconds",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.18",
|
|
5
5
|
"author": "Crafter Station",
|
|
6
6
|
"packageManager": "bun@1.3.13",
|
|
7
7
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@types/mocha": "^10",
|
|
22
22
|
"@types/node": "^18",
|
|
23
23
|
"chai": "^4",
|
|
24
|
+
"husky": "^9.1.7",
|
|
24
25
|
"mocha": "^11",
|
|
25
26
|
"oclif": "^4",
|
|
26
27
|
"shx": "^0.3.3",
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"lint": "biome lint .",
|
|
83
84
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
84
85
|
"posttest": "bun run lint",
|
|
86
|
+
"prepare": "husky",
|
|
85
87
|
"prepack": "oclif manifest && oclif readme",
|
|
86
88
|
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
|
87
89
|
"version": "oclif readme && git add README.md"
|