@gtld/whois 0.0.0-stage → 0.1.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -2
  3. package/dist/cli.js +477 -0
  4. package/package.json +54 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 planetabhi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
- # Temporary Holding Version
1
+ # @gtld/whois
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Look up WHOIS and RDAP details for any domain, printed in a clean, scannable
4
+ format in your terminal.
5
+
6
+ ```sh
7
+ npx @gtld/whois example.com
8
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env node
2
+
3
+ // package.json
4
+ var package_default = {
5
+ name: "@gtld/whois",
6
+ version: "0.1.0",
7
+ description: "Scannable WHOIS/RDAP domain lookups in your terminal.",
8
+ type: "module",
9
+ bin: {
10
+ "gtld-whois": "dist/cli.js"
11
+ },
12
+ files: [
13
+ "dist"
14
+ ],
15
+ engines: {
16
+ node: ">=20.12"
17
+ },
18
+ scripts: {
19
+ dev: "tsx src/cli.ts",
20
+ build: "tsup",
21
+ test: "vitest run",
22
+ lint: "tsc --noEmit",
23
+ format: "prettier --write .",
24
+ pack: "npm run build && npm pack",
25
+ prepublishOnly: "npm run build && npm test"
26
+ },
27
+ keywords: [
28
+ "whois",
29
+ "rdap",
30
+ "domain",
31
+ "cli",
32
+ "dns",
33
+ "lookup"
34
+ ],
35
+ author: "@planetabhi",
36
+ license: "MIT",
37
+ homepage: "https://github.com/planetabhi/whois#readme",
38
+ repository: {
39
+ type: "git",
40
+ url: "git+https://github.com/planetabhi/whois.git"
41
+ },
42
+ bugs: {
43
+ url: "https://github.com/planetabhi/whois/issues"
44
+ },
45
+ publishConfig: {
46
+ access: "public"
47
+ },
48
+ dependencies: {
49
+ "@clack/prompts": "^1.8.1"
50
+ },
51
+ devDependencies: {
52
+ "@types/node": "^26.6.5",
53
+ prettier: "^3.9.10",
54
+ tsup: "^9.0.0",
55
+ tsx: "^4.23.15",
56
+ typescript: "^7.0.2",
57
+ vitest: "^5.0.5"
58
+ }
59
+ };
60
+
61
+ // src/args.ts
62
+ function parseArgs(argv) {
63
+ const parsed = {
64
+ json: false,
65
+ raw: false,
66
+ help: false,
67
+ version: false
68
+ };
69
+ for (let i = 0; i < argv.length; i++) {
70
+ const arg = argv[i];
71
+ switch (arg) {
72
+ case "-h":
73
+ case "--help":
74
+ parsed.help = true;
75
+ break;
76
+ case "-v":
77
+ case "--version":
78
+ parsed.version = true;
79
+ break;
80
+ case "--json":
81
+ parsed.json = true;
82
+ break;
83
+ case "--raw":
84
+ parsed.raw = true;
85
+ break;
86
+ case "--color":
87
+ parsed.color = true;
88
+ break;
89
+ case "--no-color":
90
+ parsed.color = false;
91
+ break;
92
+ case "--api":
93
+ parsed.apiBase = argv[++i];
94
+ break;
95
+ default:
96
+ if (arg.startsWith("--api=")) {
97
+ parsed.apiBase = arg.slice("--api=".length);
98
+ } else if (!arg.startsWith("-") && parsed.domain === void 0) {
99
+ parsed.domain = arg;
100
+ }
101
+ break;
102
+ }
103
+ }
104
+ return parsed;
105
+ }
106
+ function cleanDomain(input) {
107
+ return input.trim().toLowerCase().replace(/^[a-z]+:\/\//, "").replace(/\/.*$/, "").replace(/^www\./, "");
108
+ }
109
+ function looksLikeDomain(domain) {
110
+ return /^[^\s.]+(\.[^\s.]+)+$/.test(domain);
111
+ }
112
+
113
+ // src/api.ts
114
+ var DEFAULT_API_BASE = "https://who-dat.as93.net";
115
+ var WhoisError = class extends Error {
116
+ constructor(message, code, kind) {
117
+ super(message);
118
+ this.code = code;
119
+ this.kind = kind;
120
+ this.name = "WhoisError";
121
+ }
122
+ code;
123
+ kind;
124
+ };
125
+ function lookupUrl(domain, apiBase) {
126
+ const base = apiBase.replace(/\/+$/, "");
127
+ return new URL(`${base}/v1/whois/${encodeURIComponent(domain)}`);
128
+ }
129
+ async function request(url, signal) {
130
+ try {
131
+ return await fetch(url, {
132
+ headers: { accept: "application/json" },
133
+ signal
134
+ });
135
+ } catch (err) {
136
+ if (err instanceof Error && err.name === "AbortError") {
137
+ throw new WhoisError("Request timed out.", "TIMEOUT", "network");
138
+ }
139
+ throw new WhoisError(
140
+ `Could not reach the API at ${url.origin}.`,
141
+ "NETWORK",
142
+ "network"
143
+ );
144
+ }
145
+ }
146
+ async function fetchWhois(domain, opts = {}) {
147
+ const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
148
+ const res = await request(lookupUrl(domain, apiBase), opts.signal);
149
+ if (!res.ok) {
150
+ const body = await res.json().catch(() => null);
151
+ const detail = body?.error;
152
+ throw new WhoisError(
153
+ detail?.message ?? `Lookup failed (HTTP ${res.status}).`,
154
+ detail?.code ?? `HTTP_${res.status}`,
155
+ "api"
156
+ );
157
+ }
158
+ return await res.json();
159
+ }
160
+ async function fetchRaw(domain, opts = {}) {
161
+ const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
162
+ const url = lookupUrl(domain, apiBase);
163
+ url.searchParams.set("raw", "true");
164
+ const res = await request(url, opts.signal);
165
+ if (!res.ok) {
166
+ const body = await res.json().catch(() => null);
167
+ const detail = body?.error;
168
+ throw new WhoisError(
169
+ detail?.message ?? `Lookup failed (HTTP ${res.status}).`,
170
+ detail?.code ?? `HTTP_${res.status}`,
171
+ "api"
172
+ );
173
+ }
174
+ return await res.text();
175
+ }
176
+
177
+ // src/render/format.ts
178
+ function wrap(code, enabled) {
179
+ return (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : s;
180
+ }
181
+ function makeColors(enabled) {
182
+ return {
183
+ bold: wrap(1, enabled),
184
+ dim: wrap(2, enabled),
185
+ green: wrap(32, enabled),
186
+ yellow: wrap(33, enabled),
187
+ cyan: wrap(36, enabled)
188
+ };
189
+ }
190
+ var LABEL_WIDTH = 13;
191
+ function row(label, value, c) {
192
+ return ` ${c.dim(label.padEnd(LABEL_WIDTH))}${value}`;
193
+ }
194
+ function subRow(label, value, c) {
195
+ return ` ${c.dim(label.padEnd(LABEL_WIDTH - 2))}${value}`;
196
+ }
197
+ function prettyStatus(status) {
198
+ const token = status.split(/\s+/)[0] ?? status;
199
+ return token.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
200
+ }
201
+ function formatDate(iso) {
202
+ const d = new Date(iso);
203
+ if (Number.isNaN(d.getTime())) return iso;
204
+ return d.toISOString().slice(0, 10);
205
+ }
206
+ var UNITS = [
207
+ [31536e6, "year"],
208
+ [2592e6, "month"],
209
+ [864e5, "day"],
210
+ [36e5, "hour"],
211
+ [6e4, "minute"]
212
+ ];
213
+ function relativeFromNow(iso) {
214
+ const then = new Date(iso).getTime();
215
+ if (Number.isNaN(then)) return "";
216
+ const diff = then - Date.now();
217
+ const abs = Math.abs(diff);
218
+ for (const [ms, name] of UNITS) {
219
+ const value = Math.floor(abs / ms);
220
+ if (value >= 1) {
221
+ const unit = value > 1 ? `${name}s` : name;
222
+ return diff > 0 ? `in ${value} ${unit}` : `${value} ${unit} ago`;
223
+ }
224
+ }
225
+ return "just now";
226
+ }
227
+
228
+ // src/render/sections.ts
229
+ function header(r, c) {
230
+ const badge = r.isRegistered ? c.green("\u25CF registered") : c.yellow("\u25CB available");
231
+ const lines = [` ${c.bold(r.domain)} ${badge}`];
232
+ if (r.domainUnicode && r.domainUnicode !== r.domain) {
233
+ lines.push(` ${c.dim(r.domainUnicode)}`);
234
+ }
235
+ return lines;
236
+ }
237
+ function overview(r, c) {
238
+ const lines = [];
239
+ if (r.registrar.name) lines.push(row("Registrar", r.registrar.name, c));
240
+ if (r.registrar.url) lines.push(row("URL", c.cyan(r.registrar.url), c));
241
+ if (r.registrar.abuseEmail) {
242
+ lines.push(row("Abuse", r.registrar.abuseEmail, c));
243
+ }
244
+ if (r.registrar.reseller)
245
+ lines.push(row("Reseller", r.registrar.reseller, c));
246
+ if (r.status.length > 0) {
247
+ lines.push(row("Status", r.status.map(prettyStatus).join(", "), c));
248
+ }
249
+ lines.push(
250
+ row("DNSSEC", r.dnssec.signed ? c.green("signed") : c.dim("unsigned"), c)
251
+ );
252
+ if (r.id) lines.push(row("Registry ID", r.id, c));
253
+ return lines;
254
+ }
255
+ function dates(r, c) {
256
+ const { created, updated, expires } = r.dates;
257
+ if (!created && !updated && !expires) return [];
258
+ const lines = [c.bold(" Dates")];
259
+ const add = (label, iso) => {
260
+ if (!iso) return;
261
+ const rel = relativeFromNow(iso);
262
+ lines.push(subRow(label, `${formatDate(iso)} ${c.dim(`(${rel})`)}`, c));
263
+ };
264
+ add("Created", created);
265
+ add("Updated", updated);
266
+ add("Expires", expires);
267
+ return lines;
268
+ }
269
+ function nameservers(r, c) {
270
+ if (r.nameservers.length === 0) return [];
271
+ const lines = [c.bold(" Nameservers")];
272
+ for (const ns of r.nameservers) {
273
+ const ips = [...ns.ipv4, ...ns.ipv6];
274
+ const suffix = ips.length > 0 ? ` ${c.dim(ips.join(", "))}` : "";
275
+ lines.push(` ${ns.name}${suffix}`);
276
+ }
277
+ return lines;
278
+ }
279
+ function contactLines(label, contact, c) {
280
+ if (contact.redacted) return [];
281
+ const parts = [];
282
+ const name = contact.name ?? contact.organization;
283
+ if (name) parts.push(name);
284
+ if (contact.email) parts.push(contact.email);
285
+ if (contact.phone) parts.push(contact.phone);
286
+ const loc = [contact.address.city, contact.address.country].filter(Boolean).join(", ");
287
+ if (loc) parts.push(loc);
288
+ if (parts.length === 0) return [];
289
+ return [subRow(label, parts.join(c.dim(" \xB7 ")), c)];
290
+ }
291
+ function contacts(r, c) {
292
+ const roles = [
293
+ ["Registrant", r.contacts.registrant],
294
+ ["Admin", r.contacts.admin],
295
+ ["Tech", r.contacts.tech],
296
+ ["Billing", r.contacts.billing]
297
+ ];
298
+ const body = roles.flatMap(
299
+ ([label, contact]) => contactLines(label, contact, c)
300
+ );
301
+ if (body.length === 0) return [];
302
+ return [c.bold(" Contacts"), ...body];
303
+ }
304
+ function meta(r, c) {
305
+ const bits = [r.meta.source];
306
+ if (r.meta.server) bits.push(r.meta.server);
307
+ bits.push(`fetched ${relativeFromNow(r.meta.fetchedAt) || "just now"}`);
308
+ if (r.meta.cached) bits.push("cached");
309
+ return [c.dim(` ${bits.join(" \xB7 ")}`)];
310
+ }
311
+
312
+ // src/render/index.ts
313
+ function renderResult(result, opts) {
314
+ const c = makeColors(opts.color);
315
+ const groups = [
316
+ header(result, c),
317
+ overview(result, c),
318
+ dates(result, c),
319
+ nameservers(result, c),
320
+ contacts(result, c),
321
+ meta(result, c)
322
+ ].filter((group) => group.length > 0);
323
+ return "\n" + groups.map((group) => group.join("\n")).join("\n\n") + "\n";
324
+ }
325
+
326
+ // src/ui.ts
327
+ import {
328
+ intro,
329
+ outro,
330
+ spinner,
331
+ text,
332
+ isCancel,
333
+ cancel,
334
+ log
335
+ } from "@clack/prompts";
336
+ function isInteractive() {
337
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
338
+ }
339
+ function showIntro() {
340
+ intro("@gtld/whois");
341
+ }
342
+ function showOutro(message) {
343
+ outro(message ?? "Done");
344
+ }
345
+ async function promptDomain() {
346
+ const value = await text({
347
+ message: "Which domain do you want to look up?",
348
+ placeholder: "example.com",
349
+ validate(input) {
350
+ if (!input || input.trim().length === 0) return "Please enter a domain.";
351
+ return void 0;
352
+ }
353
+ });
354
+ if (isCancel(value)) {
355
+ cancel("Cancelled.");
356
+ process.exit(130);
357
+ }
358
+ return value;
359
+ }
360
+ function startSpinner(message, interactive) {
361
+ if (!interactive) {
362
+ return { stop: () => {
363
+ }, fail: () => {
364
+ } };
365
+ }
366
+ const s = spinner();
367
+ s.start(message);
368
+ return {
369
+ stop: (msg) => s.stop(msg),
370
+ fail: (msg) => s.stop(msg)
371
+ };
372
+ }
373
+ function logError(message) {
374
+ log.error(message);
375
+ }
376
+
377
+ // src/cli.ts
378
+ var LOOKUP_TIMEOUT_MS = 15e3;
379
+ var HELP = `
380
+ @gtld/whois \u2014 beautiful WHOIS/RDAP lookups in your terminal
381
+
382
+ Usage
383
+ npx @gtld/whois <domain> [options]
384
+
385
+ Options
386
+ --json Print the normalized JSON result
387
+ --raw Print the raw upstream RDAP/WHOIS payload
388
+ --api <url> Override the API base URL (env: GTLD_WHOIS_API)
389
+ --no-color Disable ANSI colors
390
+ -h, --help Show this help
391
+ -v, --version Show the version
392
+
393
+ Examples
394
+ npx @gtld/whois example.com
395
+ npx @gtld/whois example.com --json
396
+ `;
397
+ function resolveColor(args) {
398
+ if (args.color !== void 0) return args.color;
399
+ if (process.env.NO_COLOR) return false;
400
+ return Boolean(process.stdout.isTTY);
401
+ }
402
+ function exitCodeFor(err) {
403
+ return err.kind === "network" ? 2 : 1;
404
+ }
405
+ async function main() {
406
+ const args = parseArgs(process.argv.slice(2));
407
+ if (args.help) {
408
+ process.stdout.write(HELP);
409
+ return 0;
410
+ }
411
+ if (args.version) {
412
+ process.stdout.write(`${package_default.version}
413
+ `);
414
+ return 0;
415
+ }
416
+ const apiBase = args.apiBase ?? process.env.GTLD_WHOIS_API ?? DEFAULT_API_BASE;
417
+ const plain = args.json || args.raw;
418
+ const interactive = isInteractive() && !plain;
419
+ let rawInput = args.domain;
420
+ if (!rawInput) {
421
+ if (!interactive) {
422
+ logError("No domain provided. Usage: gtld-whois <domain>");
423
+ return 1;
424
+ }
425
+ showIntro();
426
+ rawInput = await promptDomain();
427
+ }
428
+ const domain = cleanDomain(rawInput);
429
+ if (!looksLikeDomain(domain)) {
430
+ logError(`"${rawInput}" doesn't look like a valid domain.`);
431
+ return 1;
432
+ }
433
+ const controller = new AbortController();
434
+ const timer = setTimeout(() => controller.abort(), LOOKUP_TIMEOUT_MS);
435
+ const spin = startSpinner(`Looking up ${domain}\u2026`, interactive);
436
+ try {
437
+ if (args.raw) {
438
+ const body = await fetchRaw(domain, {
439
+ apiBase,
440
+ signal: controller.signal
441
+ });
442
+ spin.stop();
443
+ process.stdout.write(body.endsWith("\n") ? body : `${body}
444
+ `);
445
+ return 0;
446
+ }
447
+ const result = await fetchWhois(domain, {
448
+ apiBase,
449
+ signal: controller.signal
450
+ });
451
+ spin.stop();
452
+ if (args.json) {
453
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
454
+ `);
455
+ return 0;
456
+ }
457
+ process.stdout.write(renderResult(result, { color: resolveColor(args) }));
458
+ if (interactive)
459
+ showOutro(result.isRegistered ? "Registered" : "Available");
460
+ return 0;
461
+ } catch (err) {
462
+ const message = err instanceof Error ? err.message : String(err);
463
+ spin.fail("Lookup failed");
464
+ logError(message);
465
+ return err instanceof WhoisError ? exitCodeFor(err) : 1;
466
+ } finally {
467
+ clearTimeout(timer);
468
+ }
469
+ }
470
+ main().then(
471
+ (code) => process.exit(code),
472
+ (err) => {
473
+ process.stderr.write(`${err instanceof Error ? err.stack : String(err)}
474
+ `);
475
+ process.exit(1);
476
+ }
477
+ );
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@gtld/whois",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Scannable WHOIS/RDAP domain lookups in your terminal.",
5
+ "type": "module",
6
+ "bin": {
7
+ "gtld-whois": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=20.12"
14
+ },
15
+ "scripts": {
16
+ "dev": "tsx src/cli.ts",
17
+ "build": "tsup",
18
+ "test": "vitest run",
19
+ "lint": "tsc --noEmit",
20
+ "format": "prettier --write .",
21
+ "pack": "npm run build && npm pack",
22
+ "prepublishOnly": "npm run build && npm test"
23
+ },
24
+ "keywords": [
25
+ "whois",
26
+ "rdap",
27
+ "domain",
28
+ "cli",
29
+ "dns",
30
+ "lookup"
31
+ ],
32
+ "author": "@planetabhi",
33
+ "license": "MIT",
34
+ "homepage": "https://github.com/planetabhi/whois#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/planetabhi/whois.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/planetabhi/whois/issues"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@clack/prompts": "^1.8.1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.6.5",
50
+ "prettier": "^3.9.10",
51
+ "tsup": "^9.0.0",
52
+ "tsx": "^4.23.15",
53
+ "typescript": "^7.0.2",
54
+ "vitest": "^5.0.5"
55
+ }
56
+ }