whoispk 2.0.0 → 2.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.
package/README.md CHANGED
@@ -27,6 +27,12 @@ npx whoispk yourbrand
27
27
 
28
28
  `.pk` is added for you. `yourbrand`, `yourbrand.pk` and `https://www.yourbrand.pk/` all look up the same domain, and `.com.pk`, `.org.pk` and the other second-level names work too.
29
29
 
30
+ To check a name under every extension at once:
31
+
32
+ ```sh
33
+ npx whoispk --all yourbrand
34
+ ```
35
+
30
36
  To have the `whoispk` command always available:
31
37
 
32
38
  ```sh
@@ -36,6 +42,7 @@ npm install -g whoispk
36
42
  ## Features
37
43
 
38
44
  - **Clear answers.** Each domain is registered, available, or not registrable. For a name PKNIC rejects, you get its reason.
45
+ - **Every extension at once.** `--all` checks a name under `.pk`, `.com.pk`, `.net.pk`, `.org.pk`, `.biz.pk`, `.web.pk` and `.fam.pk`, one line each.
39
46
  - **Many domains at once.** Pass several names, or pipe in a list. Lookups run in parallel and print in your order.
40
47
  - **Made for scripts.** `--json` output, exit codes you can rely on, and no spinner in piped output.
41
48
  - **Also a library.** A promise-based API with TypeScript types, timeouts, retries and cancelling.
@@ -44,6 +51,16 @@ npm install -g whoispk
44
51
 
45
52
  ## Recipes
46
53
 
54
+ **Find a free name** across every extension for a few ideas:
55
+
56
+ ```console
57
+ $ whoispk --all chaiwala dhaba
58
+ chaiwala.pk ● Registered expires Mar 20, 2027
59
+ chaiwala.com.pk ● Registered expires Oct 10, 2030
60
+ chaiwala.net.pk ● Available
61
+ …
62
+ ```
63
+
47
64
  **Check a shortlist of name ideas**, one per line (`#` starts a comment):
48
65
 
49
66
  ```sh
@@ -77,6 +94,8 @@ for (const name of ['mybrand.pk', 'mybrand.com.pk']) {
77
94
  ```
78
95
  whoispk [options] <domain...>
79
96
 
97
+ -a, --all Check every public extension (.pk, .com.pk, .net.pk, …)
98
+ -s, --short One line per domain (default with --all)
80
99
  -j, --json Print results as JSON
81
100
  -t, --timeout <ms> Per-request timeout (default: 15000)
82
101
  -c, --concurrency <n> Parallel lookups (default: 3)
package/bin/whoispk.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { parseArgs } from 'node:util';
4
- import { lookup, ENDPOINT } from '../src/index.js';
4
+ import { lookup, normalizeDomain, expandExtensions, EXTENSIONS, ENDPOINT } from '../src/index.js';
5
5
 
6
6
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
7
7
  const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@@ -14,12 +14,19 @@ Check whether .pk domains are registered, using PKNIC's public lookup.
14
14
  Domains can also be piped in, one per line: cat domains.txt | whoispk
15
15
 
16
16
  Options:
17
- -j, --json Print results as JSON
18
- -t, --timeout <ms> Per-request timeout in milliseconds (default: 15000)
17
+ -a, --all Check every public extension: ${EXTENSIONS.map((e) => '.' + e).join(' ')}
18
+ -s, --short One line per domain (the default with --all; --no-short for details)
19
+ -j, --json Print results as JSON
20
+ -t, --timeout <ms> Per-request timeout in milliseconds (default: 15000)
19
21
  -c, --concurrency <n> Parallel lookups when checking several domains (default: 3)
20
- --no-color Disable colours (also respects NO_COLOR)
21
- -v, --version Print the version
22
- -h, --help Show this help
22
+ --no-color Disable colours (also respects NO_COLOR)
23
+ -v, --version Print the version
24
+ -h, --help Show this help
25
+
26
+ Examples:
27
+ whoispk daraz.pk
28
+ whoispk --all chaiwala
29
+ whoispk --json daraz google.com.pk
23
30
 
24
31
  Exit codes:
25
32
  0 all lookups completed
@@ -36,6 +43,9 @@ try {
36
43
  args = parseArgs({
37
44
  allowPositionals: true,
38
45
  options: {
46
+ all: { type: 'boolean', short: 'a' },
47
+ short: { type: 'boolean', short: 's' },
48
+ 'no-short': { type: 'boolean' },
39
49
  json: { type: 'boolean', short: 'j' },
40
50
  timeout: { type: 'string', short: 't' },
41
51
  concurrency: { type: 'string', short: 'c' },
@@ -62,18 +72,30 @@ if (opts.version) {
62
72
  const timeout = parsePositiveInt(opts.timeout, 15_000, '--timeout');
63
73
  const concurrency = parsePositiveInt(opts.concurrency, 3, '--concurrency');
64
74
 
65
- let domains = positionals;
66
- if (!domains.length && !process.stdin.isTTY) {
67
- domains = (await readStdin())
75
+ let inputs = positionals;
76
+ if (!inputs.length && !process.stdin.isTTY) {
77
+ inputs = (await readStdin())
68
78
  .split(/\r?\n/)
69
79
  .flatMap((line) => line.replace(/#.*/, '').split(/[\s,]+/))
70
80
  .filter(Boolean);
71
81
  }
72
- if (!domains.length) {
82
+ if (!inputs.length) {
73
83
  process.stdout.write(HELP.trimStart());
74
84
  process.exit(2);
75
85
  }
76
86
 
87
+ // Expand --all, normalise, and drop duplicates. Inputs that can't be normalised are kept
88
+ // as typed, so their lookup reports the problem in place.
89
+ const domains = [
90
+ ...new Set(
91
+ inputs
92
+ .flatMap((input) => (opts.all ? attempt(() => expandExtensions(input), [input]) : [input]))
93
+ .map((input) => attempt(() => normalizeDomain(input), input)),
94
+ ),
95
+ ];
96
+ const short = opts['no-short'] ? false : Boolean(opts.short || opts.all);
97
+ const nameWidth = Math.max(...domains.map((d) => d.length));
98
+
77
99
  // ---------------------------------------------------------------------------
78
100
  // Styling
79
101
  // ---------------------------------------------------------------------------
@@ -130,8 +152,14 @@ for (const [i, promise] of pending.entries()) {
130
152
  }
131
153
 
132
154
  spinner.clear();
133
- if (i > 0) console.log('');
134
- console.log(outcome.ok ? formatResult(outcome.result) : formatError(outcome.input, outcome.error));
155
+ if (short) {
156
+ // Blank line between groups of names, e.g. between "magic.*" and "chaiwala.*" with --all.
157
+ if (i > 0 && opts.all && baseName(domains[i]) !== baseName(domains[i - 1])) console.log('');
158
+ console.log(formatShort(outcome));
159
+ } else {
160
+ if (i > 0) console.log('');
161
+ console.log(outcome.ok ? formatResult(outcome.result) : formatError(outcome.input, outcome.error));
162
+ }
135
163
  }
136
164
 
137
165
  spinner.stop();
@@ -166,6 +194,22 @@ function formatResult(r) {
166
194
  return lines.join('\n');
167
195
  }
168
196
 
197
+ function formatShort({ ok, input, result: r, error }) {
198
+ const name = c.bold((ok ? r.domain : input).padEnd(nameWidth));
199
+ const status = (text, colour) => colour(text.padEnd(18));
200
+ if (!ok) return `${name} ${status('✖ Lookup failed', c.red)} ${c.dim(error.message)}`;
201
+ if (r.status === 'registered') {
202
+ const expiry = r.expireDate ? c.dim(`expires ${formatDate(r.expireDate)}`) : '';
203
+ return `${name} ${status('● Registered', c.red)} ${expiry}`.trimEnd();
204
+ }
205
+ if (r.status === 'available') return `${name} ${c.green('● Available')}`;
206
+ return `${name} ${status('● Not registrable', c.yellow)} ${c.dim(r.reasons.join('; '))}`.trimEnd();
207
+ }
208
+
209
+ function baseName(domain) {
210
+ return domain.split('.')[0];
211
+ }
212
+
169
213
  function formatError(domain, err) {
170
214
  return `${c.bold(domain)} ${c.red('✖ Lookup failed')}\n ${c.dim(err.message)}`;
171
215
  }
@@ -240,6 +284,14 @@ function createLimiter(max) {
240
284
  });
241
285
  }
242
286
 
287
+ function attempt(fn, fallback) {
288
+ try {
289
+ return fn();
290
+ } catch {
291
+ return fallback;
292
+ }
293
+ }
294
+
243
295
  function parsePositiveInt(raw, fallback, name) {
244
296
  if (raw === undefined) return fallback;
245
297
  const n = Number(raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoispk",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Check .pk domain availability and registration details from PKNIC, from the command line or Node.js",
5
5
  "keywords": [
6
6
  "whois",
package/src/index.d.ts CHANGED
@@ -52,10 +52,17 @@ export function lookup(domain: string, options?: LookupOptions): Promise<LookupR
52
52
  export function parse(html: string): Omit<LookupResult, 'domain'> & { domain: string | null };
53
53
  export function normalizeDomain(input: string): string;
54
54
 
55
+ /** The .pk extensions anyone can register: pk, com.pk, net.pk, org.pk, biz.pk, web.pk, fam.pk. */
56
+ export const EXTENSIONS: readonly string[];
57
+ /** "magic" or "magic.com.pk" -> one domain per entry in EXTENSIONS. */
58
+ export function expandExtensions(input: string): string[];
59
+
55
60
  declare const whoispk: {
56
61
  lookup: typeof lookup;
57
62
  parse: typeof parse;
58
63
  normalizeDomain: typeof normalizeDomain;
64
+ expandExtensions: typeof expandExtensions;
65
+ EXTENSIONS: readonly string[];
59
66
  WhoisPkError: typeof WhoisPkError;
60
67
  ENDPOINT: string;
61
68
  };
package/src/index.js CHANGED
@@ -40,15 +40,47 @@ export function normalizeDomain(input) {
40
40
  return domain;
41
41
  }
42
42
 
43
+ /**
44
+ * The .pk extensions anyone can register. PKNIC also has restricted ones
45
+ * (edu.pk, gov.pk, gok.pk, gob.pk, gop.pk, gos.pk) for institutions only.
46
+ */
47
+ export const EXTENSIONS = Object.freeze(['pk', 'com.pk', 'net.pk', 'org.pk', 'biz.pk', 'web.pk', 'fam.pk']);
48
+
49
+ const SECOND_LEVEL = /\.(?:com|net|org|biz|web|fam|edu|gov|gok|gob|gop|gos)$/;
50
+
51
+ /**
52
+ * Expand a name into one domain per public .pk extension.
53
+ *
54
+ * "magic" or "magic.com.pk" -> ["magic.pk", "magic.com.pk", "magic.net.pk", …]
55
+ *
56
+ * @param {string} input
57
+ * @returns {string[]}
58
+ */
59
+ export function expandExtensions(input) {
60
+ const base = normalizeDomain(input).replace(/\.pk$/, '').replace(SECOND_LEVEL, '');
61
+ if (!base) {
62
+ throw new WhoisPkError(`"${input}" has no name to check`, { code: 'INVALID_INPUT' });
63
+ }
64
+ return EXTENSIONS.map((ext) => `${base}.${ext}`);
65
+ }
66
+
43
67
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
44
68
 
45
69
  async function request(domain, { endpoint, timeout, signal, fetch: fetchImpl }) {
46
- const signals = [AbortSignal.timeout(timeout)];
47
- if (signal) signals.push(signal);
70
+ // A regular (ref'd) timer rather than AbortSignal.timeout(): the latter doesn't keep the
71
+ // process alive, so a stalled request could let Node exit before the timeout ever fires.
72
+ const controller = new AbortController();
73
+ let timedOut = false;
74
+ const timer = setTimeout(() => {
75
+ timedOut = true;
76
+ controller.abort(new DOMException(`Timed out after ${timeout} ms`, 'TimeoutError'));
77
+ }, timeout);
78
+ const forwardAbort = () => controller.abort(signal.reason);
79
+ if (signal?.aborted) forwardAbort();
80
+ else signal?.addEventListener('abort', forwardAbort, { once: true });
48
81
 
49
- let response;
50
82
  try {
51
- response = await fetchImpl(endpoint, {
83
+ const response = await fetchImpl(endpoint, {
52
84
  method: 'POST',
53
85
  headers: {
54
86
  'content-type': 'application/x-www-form-urlencoded',
@@ -56,29 +88,33 @@ async function request(domain, { endpoint, timeout, signal, fetch: fetchImpl })
56
88
  'user-agent': USER_AGENT,
57
89
  },
58
90
  body: new URLSearchParams({ name: domain }).toString(),
59
- signal: AbortSignal.any(signals),
91
+ signal: controller.signal,
60
92
  });
93
+
94
+ if (!response.ok) {
95
+ throw new WhoisPkError(`PKNIC responded with HTTP ${response.status}`, {
96
+ code: 'HTTP_ERROR',
97
+ status: response.status,
98
+ });
99
+ }
100
+
101
+ return await response.text();
61
102
  } catch (err) {
103
+ if (err instanceof WhoisPkError) throw err;
104
+ if (timedOut) {
105
+ throw new WhoisPkError(`PKNIC did not respond within ${timeout} ms`, { code: 'TIMEOUT', cause: err });
106
+ }
62
107
  if (signal?.aborted) {
63
108
  throw new WhoisPkError('Lookup aborted', { code: 'ABORTED', cause: err });
64
109
  }
65
- if (err?.name === 'TimeoutError') {
66
- throw new WhoisPkError(`PKNIC did not respond within ${timeout} ms`, { code: 'TIMEOUT', cause: err });
67
- }
68
110
  throw new WhoisPkError(`Could not reach PKNIC: ${err?.cause?.message ?? err?.message ?? err}`, {
69
111
  code: 'NETWORK_ERROR',
70
112
  cause: err,
71
113
  });
114
+ } finally {
115
+ clearTimeout(timer);
116
+ signal?.removeEventListener('abort', forwardAbort);
72
117
  }
73
-
74
- if (!response.ok) {
75
- throw new WhoisPkError(`PKNIC responded with HTTP ${response.status}`, {
76
- code: 'HTTP_ERROR',
77
- status: response.status,
78
- });
79
- }
80
-
81
- return response.text();
82
118
  }
83
119
 
84
120
  /**
@@ -115,4 +151,4 @@ export async function lookup(input, options = {}) {
115
151
  }
116
152
  }
117
153
 
118
- export default { lookup, parse, normalizeDomain, WhoisPkError, ENDPOINT };
154
+ export default { lookup, parse, normalizeDomain, expandExtensions, EXTENSIONS, WhoisPkError, ENDPOINT };