domainstorm 0.2.2 → 0.2.3
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 +43 -6
- package/check-domains.mjs +270 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ It is built for AI and agent products where naming cycles happen fast and every
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm i -g domainstorm
|
|
22
|
+
npm i -g domainstorm@latest
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Run instantly without global install:
|
|
@@ -28,21 +28,27 @@ Run instantly without global install:
|
|
|
28
28
|
npx --yes domainstorm --help
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Update to latest:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm update -g domainstorm
|
|
35
|
+
```
|
|
36
|
+
|
|
31
37
|
## Quick Wins
|
|
32
38
|
|
|
33
39
|
Brainstorm + check + return only likely available:
|
|
34
40
|
|
|
35
41
|
```bash
|
|
36
|
-
domainstorm --brainstorm "agent orchestration" "mcp broker" --tld md --server whois.nic.md --only-available --table
|
|
42
|
+
domainstorm --brainstorm "agent orchestration" "mcp broker" --tld md --server whois.nic.md --dns-prefilter --only-available --table
|
|
37
43
|
```
|
|
38
44
|
|
|
39
45
|
Check exact domains:
|
|
40
46
|
|
|
41
47
|
```bash
|
|
42
|
-
domainstorm
|
|
48
|
+
domainstorm broker.md --server whois.nic.md --dns-prefilter
|
|
43
49
|
```
|
|
44
50
|
|
|
45
|
-
Default output is plain English (for example:
|
|
51
|
+
Default output is plain English (for example: `✅ broker.md is available.`).
|
|
46
52
|
Use `--table` if you want a structured table view.
|
|
47
53
|
|
|
48
54
|
Check from file and export CSV:
|
|
@@ -73,11 +79,29 @@ Compatibility alias:
|
|
|
73
79
|
domain-check --help
|
|
74
80
|
```
|
|
75
81
|
|
|
82
|
+
## Local Dev Test (npm)
|
|
83
|
+
|
|
84
|
+
Run the current repo version without publishing:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm run check
|
|
88
|
+
npm link
|
|
89
|
+
domainstorm --help
|
|
90
|
+
domainstorm broker.md --table
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Remove local link when done:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm unlink -g domainstorm
|
|
97
|
+
```
|
|
98
|
+
|
|
76
99
|
## Example Output
|
|
77
100
|
|
|
78
101
|
```txt
|
|
79
|
-
|
|
80
|
-
agentops.md
|
|
102
|
+
✅ broker.md is available.
|
|
103
|
+
❌ agentops.md is not available (already registered).
|
|
104
|
+
Summary: ✅ 1 available, ❌ 1 taken, ⚠️ 0 unknown.
|
|
81
105
|
```
|
|
82
106
|
|
|
83
107
|
Output columns:
|
|
@@ -85,6 +109,7 @@ Output columns:
|
|
|
85
109
|
- `domain`
|
|
86
110
|
- `status` (`registered`, `likely_available`, `unknown`)
|
|
87
111
|
- `reason`
|
|
112
|
+
- `state` (registry/DNS state when available)
|
|
88
113
|
- `legal_risk` (`low`, `high_tm_risk`)
|
|
89
114
|
- `story` (brainstorm narrative tag)
|
|
90
115
|
- `error`
|
|
@@ -94,10 +119,12 @@ Output columns:
|
|
|
94
119
|
- `--brainstorm` / `--storm`
|
|
95
120
|
- `--max-suggestions <n>` (default: `120`)
|
|
96
121
|
- `--table`
|
|
122
|
+
- `--plain`
|
|
97
123
|
- `--input <file>`
|
|
98
124
|
- `--output <file>`
|
|
99
125
|
- `--tld <tld>` (default: `md`)
|
|
100
126
|
- `--server <whois-host>`
|
|
127
|
+
- `--dns-prefilter`
|
|
101
128
|
- `--only-available`
|
|
102
129
|
- `--concurrency <n>`
|
|
103
130
|
- `--timeout-ms <n>`
|
|
@@ -143,8 +170,18 @@ npm login
|
|
|
143
170
|
npm publish --access public
|
|
144
171
|
```
|
|
145
172
|
|
|
173
|
+
If local provenance is unsupported in your shell/provider, use:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npm publish --access public --provenance=false
|
|
177
|
+
```
|
|
178
|
+
|
|
146
179
|
## Notes
|
|
147
180
|
|
|
148
181
|
- WHOIS formats vary by registry; treat `likely_available` as a pre-check, not final registrar checkout.
|
|
149
182
|
- Registries can rate-limit bulk lookups; retry `unknown` rows after cooldown.
|
|
183
|
+
- Domainstorm includes registry adapters for `.md`, `.com`, `.org`, and `.ai` to improve state parsing.
|
|
184
|
+
- Domainstorm auto-throttles WHOIS to concurrency `2` across TLDs (override with `--concurrency`).
|
|
185
|
+
- For `.md`, `Domain state` is parsed explicitly (`Inactive`, `OK`, `OK Delegated`, and no-match states).
|
|
186
|
+
- `--dns-prefilter` runs DNS first and marks resolving domains as taken before WHOIS (faster and lighter on WHOIS quota).
|
|
150
187
|
- Install `whois` locally (`brew install whois` on macOS).
|
package/check-domains.mjs
CHANGED
|
@@ -5,9 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_CONCURRENCY = 6;
|
|
8
|
+
const SAFE_WHOIS_CONCURRENCY = 2;
|
|
8
9
|
const DEFAULT_TIMEOUT_MS = 12000;
|
|
9
10
|
const DEFAULT_TLD = "md";
|
|
10
11
|
const DEFAULT_MAX_SUGGESTIONS = 120;
|
|
12
|
+
const MD_WHOIS_SERVER = "whois.nic.md";
|
|
11
13
|
const AVAILABILITY_PATTERNS = [
|
|
12
14
|
"no match for",
|
|
13
15
|
"not found",
|
|
@@ -112,8 +114,9 @@ function printUsage() {
|
|
|
112
114
|
" --output <file> Optional CSV output path",
|
|
113
115
|
" --tld <tld> Default TLD for bare labels (default: md)",
|
|
114
116
|
" --server <host> Optional WHOIS server (ex: whois.nic.md)",
|
|
115
|
-
" --concurrency <n> Parallel WHOIS requests (default: 6)",
|
|
117
|
+
" --concurrency <n> Parallel WHOIS requests (default: 6, auto-throttled to 2 unless set)",
|
|
116
118
|
" --timeout-ms <n> WHOIS timeout per domain (default: 12000)",
|
|
119
|
+
" --dns-prefilter Run DNS pre-check (A/AAAA/CNAME); skip WHOIS for resolving domains",
|
|
117
120
|
" --brainstorm Generate domain ideas from seeds, then check availability",
|
|
118
121
|
" --max-suggestions <n> Max brainstormed candidates (default: 120)",
|
|
119
122
|
" --table Render results in a readable terminal table",
|
|
@@ -124,6 +127,7 @@ function printUsage() {
|
|
|
124
127
|
"",
|
|
125
128
|
"Notes:",
|
|
126
129
|
" - Labels without a dot are auto-converted to <label>.<tld>",
|
|
130
|
+
" - WHOIS is auto-throttled to concurrency=2 unless you set --concurrency",
|
|
127
131
|
" - Brainstorm mode uses your seed words only (no hardcoded keyword packs)",
|
|
128
132
|
" - No seed ideas? Ask your agent (Codex, Claude, etc.) and pass them to --brainstorm",
|
|
129
133
|
" - WHOIS-based checks are heuristic; always confirm at registrar checkout",
|
|
@@ -136,9 +140,11 @@ function parseArgs(argv) {
|
|
|
136
140
|
input: null,
|
|
137
141
|
output: null,
|
|
138
142
|
concurrency: DEFAULT_CONCURRENCY,
|
|
143
|
+
concurrencyExplicit: false,
|
|
139
144
|
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
140
145
|
tld: DEFAULT_TLD,
|
|
141
146
|
server: null,
|
|
147
|
+
dnsPrefilter: false,
|
|
142
148
|
brainstorm: false,
|
|
143
149
|
maxSuggestions: DEFAULT_MAX_SUGGESTIONS,
|
|
144
150
|
table: false,
|
|
@@ -162,6 +168,7 @@ function parseArgs(argv) {
|
|
|
162
168
|
case "--concurrency":
|
|
163
169
|
case "-c":
|
|
164
170
|
options.concurrency = Number.parseInt(argv[++i], 10);
|
|
171
|
+
options.concurrencyExplicit = true;
|
|
165
172
|
break;
|
|
166
173
|
case "--tld":
|
|
167
174
|
options.tld = argv[++i];
|
|
@@ -172,6 +179,9 @@ function parseArgs(argv) {
|
|
|
172
179
|
case "--timeout-ms":
|
|
173
180
|
options.timeoutMs = Number.parseInt(argv[++i], 10);
|
|
174
181
|
break;
|
|
182
|
+
case "--dns-prefilter":
|
|
183
|
+
options.dnsPrefilter = true;
|
|
184
|
+
break;
|
|
175
185
|
case "--brainstorm":
|
|
176
186
|
case "--storm":
|
|
177
187
|
options.brainstorm = true;
|
|
@@ -409,7 +419,98 @@ function brainstormCandidates(rawSeeds, maxSuggestions) {
|
|
|
409
419
|
};
|
|
410
420
|
}
|
|
411
421
|
|
|
412
|
-
function
|
|
422
|
+
function isMdDomain(domain) {
|
|
423
|
+
return domain.endsWith(".md");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getDomainTld(domain) {
|
|
427
|
+
const idx = domain.lastIndexOf(".");
|
|
428
|
+
if (idx < 0 || idx === domain.length - 1) {
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
return domain.slice(idx + 1).toLowerCase();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function shouldUseMdRules(domain, server) {
|
|
435
|
+
if (isMdDomain(domain)) {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
if (!server) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return server === MD_WHOIS_SERVER || server.endsWith(".nic.md") || server.endsWith(".md");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function classifyMdWhois(raw) {
|
|
445
|
+
const text = raw.toLowerCase();
|
|
446
|
+
if (
|
|
447
|
+
text.includes("no entries found [ no match for ]") ||
|
|
448
|
+
(text.includes("no entries found") && text.includes("no match for"))
|
|
449
|
+
) {
|
|
450
|
+
return {
|
|
451
|
+
status: "likely_available",
|
|
452
|
+
reason: "md_no_match",
|
|
453
|
+
state: "No match",
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const stateMatch = raw.match(/domain state:\s*([^\r\n]+)/i);
|
|
458
|
+
if (!stateMatch) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const state = stateMatch[1].trim();
|
|
463
|
+
const lower = state.toLowerCase();
|
|
464
|
+
if (lower.includes("inactive")) {
|
|
465
|
+
return { status: "registered", reason: "md_state_inactive", state };
|
|
466
|
+
}
|
|
467
|
+
if (lower.includes("ok delegated")) {
|
|
468
|
+
return { status: "registered", reason: "md_state_ok_delegated", state };
|
|
469
|
+
}
|
|
470
|
+
if (lower === "ok" || lower.startsWith("ok ")) {
|
|
471
|
+
return { status: "registered", reason: "md_state_ok", state };
|
|
472
|
+
}
|
|
473
|
+
return { status: "registered", reason: "md_state_registered", state };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function cleanDomainStatus(value) {
|
|
477
|
+
return value
|
|
478
|
+
.replace(/\s+https?:\/\/\S+$/i, "")
|
|
479
|
+
.replace(/\s+\(https?:\/\/\S+\)$/i, "")
|
|
480
|
+
.trim();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function classifyComOrgAiWhois(raw, domain) {
|
|
484
|
+
const tld = getDomainTld(domain);
|
|
485
|
+
if (!["com", "org", "ai"].includes(tld)) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const text = raw.toLowerCase();
|
|
490
|
+
if (tld === "com" && text.includes('no match for domain "')) {
|
|
491
|
+
return { status: "likely_available", reason: "com_no_match", state: "No match" };
|
|
492
|
+
}
|
|
493
|
+
if ((tld === "org" || tld === "ai") && text.includes("domain not found")) {
|
|
494
|
+
return { status: "likely_available", reason: `${tld}_domain_not_found`, state: "No match" };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const statusMatch = raw.match(/Domain Status:\s*([^\r\n]+)/i);
|
|
498
|
+
if (statusMatch) {
|
|
499
|
+
return {
|
|
500
|
+
status: "registered",
|
|
501
|
+
reason: `${tld}_domain_status`,
|
|
502
|
+
state: cleanDomainStatus(statusMatch[1]),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (/Domain Name:\s*[^\r\n]+/i.test(raw) || /Registry Domain ID:\s*[^\r\n]+/i.test(raw)) {
|
|
507
|
+
return { status: "registered", reason: `${tld}_registered`, state: "" };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function classifyWhois(raw, domain, server) {
|
|
413
514
|
const text = raw.toLowerCase();
|
|
414
515
|
if (!text.trim()) {
|
|
415
516
|
return { status: "unknown", reason: "empty_whois_response" };
|
|
@@ -420,6 +521,16 @@ function classifyWhois(raw) {
|
|
|
420
521
|
if (RATE_LIMIT_PATTERNS.some((p) => text.includes(p))) {
|
|
421
522
|
return { status: "unknown", reason: "rate_limited_or_blocked" };
|
|
422
523
|
}
|
|
524
|
+
if (shouldUseMdRules(domain, server)) {
|
|
525
|
+
const md = classifyMdWhois(raw);
|
|
526
|
+
if (md) {
|
|
527
|
+
return md;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const common = classifyComOrgAiWhois(raw, domain);
|
|
531
|
+
if (common) {
|
|
532
|
+
return common;
|
|
533
|
+
}
|
|
423
534
|
if (AVAILABILITY_PATTERNS.some((p) => text.includes(p))) {
|
|
424
535
|
return { status: "likely_available", reason: "availability_pattern_match" };
|
|
425
536
|
}
|
|
@@ -429,6 +540,75 @@ function classifyWhois(raw) {
|
|
|
429
540
|
return { status: "unknown", reason: "unrecognized_whois_format" };
|
|
430
541
|
}
|
|
431
542
|
|
|
543
|
+
function runDigType(domain, type, timeoutMs) {
|
|
544
|
+
return new Promise((resolve) => {
|
|
545
|
+
const child = spawn("dig", ["+short", domain, type], {
|
|
546
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
let stdout = "";
|
|
550
|
+
let stderr = "";
|
|
551
|
+
let done = false;
|
|
552
|
+
const timer = setTimeout(() => {
|
|
553
|
+
if (done) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
done = true;
|
|
557
|
+
child.kill("SIGKILL");
|
|
558
|
+
resolve({ values: [], error: `dig ${type} timeout` });
|
|
559
|
+
}, Math.min(timeoutMs, 5000));
|
|
560
|
+
|
|
561
|
+
child.stdout.on("data", (chunk) => {
|
|
562
|
+
stdout += chunk.toString();
|
|
563
|
+
});
|
|
564
|
+
child.stderr.on("data", (chunk) => {
|
|
565
|
+
stderr += chunk.toString();
|
|
566
|
+
});
|
|
567
|
+
child.on("error", (err) => {
|
|
568
|
+
if (done) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
done = true;
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
resolve({ values: [], error: err.message });
|
|
574
|
+
});
|
|
575
|
+
child.on("close", () => {
|
|
576
|
+
if (done) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
done = true;
|
|
580
|
+
clearTimeout(timer);
|
|
581
|
+
const values = stdout
|
|
582
|
+
.split(/\r?\n/)
|
|
583
|
+
.map((line) => line.trim())
|
|
584
|
+
.filter(Boolean);
|
|
585
|
+
resolve({ values, error: stderr.trim() || null });
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function runDnsPrefilter(domain, timeoutMs) {
|
|
591
|
+
const a = await runDigType(domain, "A", timeoutMs);
|
|
592
|
+
if (a.values.length > 0) {
|
|
593
|
+
return { domain, resolves: true, recordType: "A", values: a.values, error: null };
|
|
594
|
+
}
|
|
595
|
+
const aaaa = await runDigType(domain, "AAAA", timeoutMs);
|
|
596
|
+
if (aaaa.values.length > 0) {
|
|
597
|
+
return { domain, resolves: true, recordType: "AAAA", values: aaaa.values, error: null };
|
|
598
|
+
}
|
|
599
|
+
const cname = await runDigType(domain, "CNAME", timeoutMs);
|
|
600
|
+
if (cname.values.length > 0) {
|
|
601
|
+
return { domain, resolves: true, recordType: "CNAME", values: cname.values, error: null };
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
domain,
|
|
605
|
+
resolves: false,
|
|
606
|
+
recordType: "",
|
|
607
|
+
values: [],
|
|
608
|
+
error: a.error || aaaa.error || cname.error || null,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
432
612
|
function runWhois(domain, timeoutMs, server) {
|
|
433
613
|
return new Promise((resolve) => {
|
|
434
614
|
const args = server ? ["-h", server, domain] : [domain];
|
|
@@ -483,12 +663,13 @@ function runWhois(domain, timeoutMs, server) {
|
|
|
483
663
|
done = true;
|
|
484
664
|
clearTimeout(timer);
|
|
485
665
|
const merged = [stdout, stderr].filter(Boolean).join("\n");
|
|
486
|
-
const classification = classifyWhois(merged);
|
|
666
|
+
const classification = classifyWhois(merged, domain, server);
|
|
487
667
|
const stderrText = stderr.trim();
|
|
488
668
|
resolve({
|
|
489
669
|
domain,
|
|
490
670
|
status: classification.status,
|
|
491
671
|
reason: classification.reason,
|
|
672
|
+
state: classification.state ?? "",
|
|
492
673
|
raw: merged.trim(),
|
|
493
674
|
error: classification.reason === "whois_lookup_failed" ? stderrText || "WHOIS lookup failed" : null,
|
|
494
675
|
});
|
|
@@ -528,7 +709,7 @@ async function maybeWriteCsv(results, outputPath) {
|
|
|
528
709
|
if (!outputPath) {
|
|
529
710
|
return;
|
|
530
711
|
}
|
|
531
|
-
const header = ["domain", "status", "reason", "legal_risk", "story", "error"];
|
|
712
|
+
const header = ["domain", "status", "reason", "state", "legal_risk", "story", "error"];
|
|
532
713
|
const lines = [header.join(",")];
|
|
533
714
|
for (const row of results) {
|
|
534
715
|
lines.push(
|
|
@@ -536,6 +717,7 @@ async function maybeWriteCsv(results, outputPath) {
|
|
|
536
717
|
row.domain,
|
|
537
718
|
row.status,
|
|
538
719
|
row.reason,
|
|
720
|
+
row.state ?? "",
|
|
539
721
|
row.legalRisk,
|
|
540
722
|
row.story ?? "",
|
|
541
723
|
row.error ?? "",
|
|
@@ -570,22 +752,37 @@ function snippet(raw) {
|
|
|
570
752
|
function verdictForStatus(status) {
|
|
571
753
|
switch (status) {
|
|
572
754
|
case "likely_available":
|
|
573
|
-
return "AVAILABLE";
|
|
755
|
+
return "✅ AVAILABLE";
|
|
574
756
|
case "registered":
|
|
575
|
-
return "TAKEN";
|
|
757
|
+
return "❌ TAKEN";
|
|
576
758
|
default:
|
|
577
|
-
return "UNKNOWN";
|
|
759
|
+
return "⚠️ UNKNOWN";
|
|
578
760
|
}
|
|
579
761
|
}
|
|
580
762
|
|
|
581
763
|
function sentenceForRow(row) {
|
|
582
764
|
if (row.status === "likely_available") {
|
|
583
|
-
return
|
|
765
|
+
return `✅ ${row.domain} is available.`;
|
|
584
766
|
}
|
|
585
767
|
if (row.status === "registered") {
|
|
586
|
-
|
|
768
|
+
if (row.reason === "dns_resolves_prefilter") {
|
|
769
|
+
return `❌ ${row.domain} is not available (DNS resolves).`;
|
|
770
|
+
}
|
|
771
|
+
if (row.reason === "md_state_inactive") {
|
|
772
|
+
return `❌ ${row.domain} is not available (registered, inactive).`;
|
|
773
|
+
}
|
|
774
|
+
if (row.reason === "md_state_ok") {
|
|
775
|
+
return `❌ ${row.domain} is not available (registered, state: OK).`;
|
|
776
|
+
}
|
|
777
|
+
if (row.reason === "md_state_ok_delegated") {
|
|
778
|
+
return `❌ ${row.domain} is not available (registered, state: OK Delegated).`;
|
|
779
|
+
}
|
|
780
|
+
if (row.state) {
|
|
781
|
+
return `❌ ${row.domain} is not available (registered, state: ${row.state}).`;
|
|
782
|
+
}
|
|
783
|
+
return `❌ ${row.domain} is not available (already registered).`;
|
|
587
784
|
}
|
|
588
|
-
return
|
|
785
|
+
return `⚠️ ${row.domain} availability is unknown (whois could not confirm).`;
|
|
589
786
|
}
|
|
590
787
|
|
|
591
788
|
function toDisplayRow(row, options) {
|
|
@@ -593,6 +790,7 @@ function toDisplayRow(row, options) {
|
|
|
593
790
|
verdict: verdictForStatus(row.status),
|
|
594
791
|
domain: row.domain,
|
|
595
792
|
whois: row.status,
|
|
793
|
+
state: row.state ?? "",
|
|
596
794
|
reason: row.reason,
|
|
597
795
|
risk: row.legalRisk,
|
|
598
796
|
story: row.story ?? "",
|
|
@@ -614,12 +812,14 @@ function truncateCell(value, maxWidth) {
|
|
|
614
812
|
|
|
615
813
|
function printTable(rows, options) {
|
|
616
814
|
const displayRows = rows.map((row) => toDisplayRow(row, options));
|
|
815
|
+
const hasState = displayRows.some((row) => row.state);
|
|
617
816
|
const hasStory = displayRows.some((row) => row.story);
|
|
618
817
|
const hasError = displayRows.some((row) => row.error);
|
|
619
818
|
const columns = [
|
|
620
|
-
{ key: "verdict", label: "Verdict", maxWidth:
|
|
819
|
+
{ key: "verdict", label: "Verdict", maxWidth: 14 },
|
|
621
820
|
{ key: "domain", label: "Domain", maxWidth: 44 },
|
|
622
821
|
{ key: "whois", label: "Whois", maxWidth: 18 },
|
|
822
|
+
...(hasState ? [{ key: "state", label: "State", maxWidth: 22 }] : []),
|
|
623
823
|
{ key: "reason", label: "Reason", maxWidth: 34 },
|
|
624
824
|
{ key: "risk", label: "Risk", maxWidth: 14 },
|
|
625
825
|
...(hasStory ? [{ key: "story", label: "Story", maxWidth: 36 }] : []),
|
|
@@ -686,6 +886,13 @@ function printNarrativeResults(rows, options) {
|
|
|
686
886
|
}
|
|
687
887
|
}
|
|
688
888
|
|
|
889
|
+
function shouldAutoThrottleWhois(domains) {
|
|
890
|
+
if (domains.length === 0) {
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
|
|
689
896
|
async function main() {
|
|
690
897
|
const options = parseArgs(process.argv.slice(2));
|
|
691
898
|
let rawLabels = await loadLabels(options);
|
|
@@ -720,11 +927,56 @@ async function main() {
|
|
|
720
927
|
throw new Error("No valid domains found after normalization.");
|
|
721
928
|
}
|
|
722
929
|
|
|
930
|
+
const effectiveConcurrency =
|
|
931
|
+
!options.concurrencyExplicit && shouldAutoThrottleWhois(domains)
|
|
932
|
+
? Math.min(options.concurrency, SAFE_WHOIS_CONCURRENCY)
|
|
933
|
+
: options.concurrency;
|
|
934
|
+
|
|
935
|
+
if (effectiveConcurrency !== options.concurrency) {
|
|
936
|
+
console.error(
|
|
937
|
+
`Auto-throttle: using concurrency=${effectiveConcurrency} for WHOIS (override with --concurrency).`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
723
941
|
const serverLabel = options.server ? `server=${options.server}` : "server=auto";
|
|
724
|
-
console.error(`Checking ${domains.length} domain(s) (${serverLabel}) with concurrency=${
|
|
942
|
+
console.error(`Checking ${domains.length} domain(s) (${serverLabel}) with concurrency=${effectiveConcurrency}`);
|
|
943
|
+
|
|
944
|
+
const dnsPrefilterResults = [];
|
|
945
|
+
let whoisTargets = domains;
|
|
946
|
+
if (options.dnsPrefilter) {
|
|
947
|
+
const dnsConcurrency = options.concurrencyExplicit ? options.concurrency : Math.min(20, domains.length || 1);
|
|
948
|
+
const dnsChecks = await mapLimit(domains, dnsConcurrency, async (domain) =>
|
|
949
|
+
runDnsPrefilter(domain, options.timeoutMs),
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
const dnsTaken = dnsChecks.filter((item) => item.resolves);
|
|
953
|
+
if (dnsTaken.length > 0) {
|
|
954
|
+
console.error(`DNS prefilter: ${dnsTaken.length} domain(s) resolve; marked as taken without WHOIS.`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
for (const item of dnsTaken) {
|
|
958
|
+
dnsPrefilterResults.push({
|
|
959
|
+
domain: item.domain,
|
|
960
|
+
status: "registered",
|
|
961
|
+
reason: "dns_resolves_prefilter",
|
|
962
|
+
state: `DNS ${item.recordType}`,
|
|
963
|
+
raw: "",
|
|
964
|
+
error: null,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
whoisTargets = dnsChecks.filter((item) => !item.resolves).map((item) => item.domain);
|
|
968
|
+
}
|
|
725
969
|
|
|
726
|
-
const
|
|
727
|
-
|
|
970
|
+
const whoisResults = await mapLimit(whoisTargets, effectiveConcurrency, async (domain) =>
|
|
971
|
+
runWhois(domain, options.timeoutMs, options.server),
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
const mergedByDomain = new Map();
|
|
975
|
+
for (const result of [...dnsPrefilterResults, ...whoisResults]) {
|
|
976
|
+
mergedByDomain.set(result.domain, result);
|
|
977
|
+
}
|
|
978
|
+
const checked = domains.map((domain) => {
|
|
979
|
+
const result = mergedByDomain.get(domain);
|
|
728
980
|
const label = domain.slice(0, domain.lastIndexOf("."));
|
|
729
981
|
return {
|
|
730
982
|
...result,
|
|
@@ -740,6 +992,9 @@ async function main() {
|
|
|
740
992
|
if (options.plain) {
|
|
741
993
|
for (const row of filtered) {
|
|
742
994
|
let line = `${row.domain}\t${row.status}\t${row.reason}\t${row.legalRisk}`;
|
|
995
|
+
if (row.state) {
|
|
996
|
+
line += `\tstate=${row.state}`;
|
|
997
|
+
}
|
|
743
998
|
if (row.story) {
|
|
744
999
|
line += `\tstory=${row.story}`;
|
|
745
1000
|
}
|
|
@@ -760,7 +1015,7 @@ async function main() {
|
|
|
760
1015
|
await maybeWriteCsv(checked, options.output);
|
|
761
1016
|
|
|
762
1017
|
const counts = summarize(checked);
|
|
763
|
-
console.error(`Summary: ${counts.likelyAvailable} available, ${counts.registered} taken, ${counts.unknown} unknown.`);
|
|
1018
|
+
console.error(`Summary: ✅ ${counts.likelyAvailable} available, ❌ ${counts.registered} taken, ⚠️ ${counts.unknown} unknown.`);
|
|
764
1019
|
if (options.output) {
|
|
765
1020
|
console.error(`CSV written: ${path.resolve(options.output)}`);
|
|
766
1021
|
}
|