builtwith-official-cli 1.0.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 ADDED
@@ -0,0 +1,396 @@
1
+ # BuiltWith CLI 🔍
2
+
3
+ > Non-interactive, scriptable CLI for the [BuiltWith API](https://api.builtwith.com) — designed for automation, CI/CD pipelines, and AI agent consumption.
4
+
5
+ ```bash
6
+ bw domain lookup shopify.com --format table
7
+ bw domain lookup shopify.com --nopii | jq '.Results[0].Technologies[].Name'
8
+ bw live feed --duration 60 > events.ndjson
9
+ bw mcp # start MCP server for Claude Desktop, VS Code, etc.
10
+ ```
11
+
12
+ ## 🤔 Why this exists
13
+
14
+ The [BuiltWith TUI](https://github.com/builtwith/builtwith-tui) is great for interactive exploration. This CLI is intentionally different:
15
+
16
+ - **stdout = data only** (JSON/table/CSV) — safe to pipe anywhere
17
+ - **stderr = human output** (spinners, errors, debug info)
18
+ - **Structured exit codes** — scripts can distinguish auth failures from rate limits from network errors
19
+ - **Multiple auth paths** — works in CI with env vars, locally with rc files, or inline with `--key`
20
+
21
+ ---
22
+
23
+ ## 📦 Installation
24
+
25
+ ```bash
26
+ npm install -g builtwith-official-cli
27
+ ```
28
+
29
+ Or run directly without installing:
30
+
31
+ ```bash
32
+ npx builtwith-official-cli domain lookup example.com --key YOUR_KEY
33
+ ```
34
+
35
+ Registers as both `bw` (short) and `builtwith` (discoverable).
36
+
37
+ ---
38
+
39
+ ## 🔑 Authentication
40
+
41
+ API key is resolved in priority order:
42
+
43
+ | Priority | Method |
44
+ |---|---|
45
+ | 1 | `--key <value>` CLI flag |
46
+ | 2 | `BUILTWITH_API_KEY` environment variable |
47
+ | 3 | `.builtwithrc` in current directory |
48
+ | 4 | `.builtwithrc` in home directory |
49
+
50
+ `.env` files in the current directory are loaded automatically, so `BUILTWITH_API_KEY=xxx` in `.env` works too.
51
+
52
+ **`.builtwithrc` format:**
53
+ ```json
54
+ {"key": "YOUR_API_KEY"}
55
+ ```
56
+
57
+ Copy `.builtwithrc.example` to get started:
58
+ ```bash
59
+ cp .builtwithrc.example ~/.builtwithrc
60
+ # then edit with your key
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 💻 Commands
66
+
67
+ ### 🌐 Domain
68
+
69
+ ```bash
70
+ bw domain lookup <domain> [flags]
71
+ ```
72
+
73
+ | Flag | Description |
74
+ |---|---|
75
+ | `--nopii` | Exclude PII data |
76
+ | `--nometa` | Exclude meta data |
77
+ | `--noattr` | Exclude attribution data |
78
+ | `--liveonly` | Only currently-live technologies |
79
+ | `--fdrange <YYYYMMDD-YYYYMMDD>` | First-detected date range |
80
+ | `--ldrange <YYYYMMDD-YYYYMMDD>` | Last-detected date range |
81
+
82
+ ```bash
83
+ bw domain lookup shopify.com
84
+ bw domain lookup shopify.com --format table
85
+ bw domain lookup shopify.com --nopii --liveonly | jq '.Results[0].Technologies[].Name'
86
+ bw domain lookup shopify.com --fdrange 20240101-20241231
87
+ ```
88
+
89
+ ### 📋 Lists
90
+
91
+ ```bash
92
+ bw lists tech <tech> [--offset <n>] [--limit <n>]
93
+ ```
94
+
95
+ ```bash
96
+ bw lists tech WordPress
97
+ bw lists tech Shopify --limit 50 --offset 100
98
+ ```
99
+
100
+ ### 🔗 Relationships
101
+
102
+ ```bash
103
+ bw relationships lookup <domain>
104
+ ```
105
+
106
+ ### 🆓 Free
107
+
108
+ ```bash
109
+ bw free lookup <domain>
110
+ ```
111
+
112
+ ### 🏢 Company
113
+
114
+ ```bash
115
+ bw company find <name>
116
+ ```
117
+
118
+ ```bash
119
+ bw company find "Shopify"
120
+ ```
121
+
122
+ ### 🏷️ Tags
123
+
124
+ ```bash
125
+ bw tags lookup <lookup>
126
+ ```
127
+
128
+ ### 💡 Recommendations
129
+
130
+ ```bash
131
+ bw recommendations lookup <domain>
132
+ ```
133
+
134
+ ### ↪️ Redirects
135
+
136
+ ```bash
137
+ bw redirects lookup <domain>
138
+ ```
139
+
140
+ ### 🔤 Keywords
141
+
142
+ ```bash
143
+ bw keywords lookup <domain>
144
+ ```
145
+
146
+ ### 📈 Trends
147
+
148
+ ```bash
149
+ bw trends tech <tech>
150
+ ```
151
+
152
+ ```bash
153
+ bw trends tech React
154
+ ```
155
+
156
+ ### 🛍️ Products
157
+
158
+ ```bash
159
+ bw products search <query> [--page <n>] [--limit <n>]
160
+ ```
161
+
162
+ ```bash
163
+ bw products search "coffee maker"
164
+ bw products search "running shoes" --page 2 --limit 50
165
+ ```
166
+
167
+ ### 🛡️ Trust
168
+
169
+ ```bash
170
+ bw trust lookup <domain>
171
+ ```
172
+
173
+ ### 👤 Account
174
+
175
+ ```bash
176
+ bw account whoami
177
+ bw account usage
178
+ ```
179
+
180
+ ### 📡 Live Feed
181
+
182
+ Stream live technology detection events as [NDJSON](https://jsonlines.org/), one event per line.
183
+
184
+ ```bash
185
+ bw live feed [--duration <seconds>]
186
+ ```
187
+
188
+ ```bash
189
+ # Stream indefinitely (Ctrl+C to stop)
190
+ bw live feed
191
+
192
+ # Capture 60 seconds of events
193
+ bw live feed --duration 60 > events.ndjson
194
+
195
+ # Pipe to jq in real time
196
+ bw live feed | jq --unbuffered '.domain'
197
+ ```
198
+
199
+ ---
200
+
201
+ ## 🚩 Global Flags
202
+
203
+ Available on every command:
204
+
205
+ | Flag | Description |
206
+ |---|---|
207
+ | `--key <apikey>` | API key (highest priority) |
208
+ | `--format <fmt>` | `json` (default) \| `table` \| `csv` |
209
+ | `--no-color` | Disable color on stderr |
210
+ | `--dry-run` | Print request URL (key masked) and exit |
211
+ | `--debug` | Print HTTP metadata to stderr |
212
+ | `--quiet` | Suppress spinner/info stderr output |
213
+
214
+ ---
215
+
216
+ ## 🖨️ Output Formats
217
+
218
+ ### JSON (default)
219
+
220
+ ```bash
221
+ bw domain lookup example.com | jq '.Results[0].Technologies[].Name'
222
+ ```
223
+
224
+ ### Table
225
+
226
+ ```bash
227
+ bw domain lookup example.com --format table
228
+ ```
229
+
230
+ ### CSV
231
+
232
+ ```bash
233
+ bw domain lookup example.com --format csv > results.csv
234
+ ```
235
+
236
+ ---
237
+
238
+ ## 🚦 Exit Codes
239
+
240
+ Scripts can use exit codes to handle different failure modes:
241
+
242
+ | Code | Meaning |
243
+ |---|---|
244
+ | `0` | ✅ Success |
245
+ | `1` | 💥 Unexpected error |
246
+ | `2` | 🔐 Auth failure (missing key, 401, 403) |
247
+ | `3` | 🔍 Not found (404) |
248
+ | `4` | ⏱️ Rate limit (429) |
249
+ | `5` | ⚠️ Other API error |
250
+ | `6` | 🌐 Network failure |
251
+ | `7` | ❌ Invalid input |
252
+ | `8` | 🛑 Interrupted (SIGINT) |
253
+
254
+ ```bash
255
+ bw domain lookup example.com
256
+ case $? in
257
+ 0) echo "success" ;;
258
+ 2) echo "check your API key" ;;
259
+ 4) echo "rate limited — slow down" ;;
260
+ 6) echo "network error" ;;
261
+ esac
262
+ ```
263
+
264
+ ---
265
+
266
+ ## 🔧 Pipeline Examples
267
+
268
+ ```bash
269
+ # Get all live tech names for a domain
270
+ bw domain lookup shopify.com --liveonly | \
271
+ jq -r '.Results[0].Technologies[].Name' | sort
272
+
273
+ # Check if a domain uses WordPress
274
+ bw domain lookup example.com --quiet --liveonly | \
275
+ jq -e '.Results[0].Technologies[] | select(.Name == "WordPress")' > /dev/null \
276
+ && echo "uses WordPress"
277
+
278
+ # Export tech stack to CSV
279
+ bw domain lookup shopify.com --format csv > shopify-tech.csv
280
+
281
+ # Capture 5 minutes of live events
282
+ bw live feed --duration 300 --quiet > feed.ndjson
283
+
284
+ # Find all sites using a technology (paginated)
285
+ for offset in 0 20 40 60 80; do
286
+ bw domain lists tech React --offset $offset --limit 20 --quiet
287
+ done | jq -s 'add'
288
+
289
+ # CI/CD: fail build if domain check fails
290
+ bw domain lookup mysite.com --key "$BUILTWITH_API_KEY" --quiet || exit 1
291
+ ```
292
+
293
+ ---
294
+
295
+ ## 🐛 Dry Run & Debugging
296
+
297
+ ```bash
298
+ # Preview the URL that would be called (key is masked)
299
+ bw domain lookup example.com --key MYKEY --dry-run
300
+ # → https://api.builtwith.com/v22/api.json?KEY=REDACTED&LOOKUP=example.com
301
+
302
+ # See HTTP response metadata
303
+ bw domain lookup example.com --debug
304
+ ```
305
+
306
+ ---
307
+
308
+ ## 🤖 MCP Server
309
+
310
+ `bw mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio, exposing all BuiltWith API endpoints as structured tools that any MCP-compatible client can call — Claude Desktop, VS Code, Cursor, Zed, and more.
311
+
312
+ ```bash
313
+ bw mcp
314
+ bw mcp --key YOUR_API_KEY # pass key inline instead of env/rc file
315
+ bw mcp --debug # log JSON-RPC traffic to stderr
316
+ ```
317
+
318
+ ### ⚙️ Client configuration
319
+
320
+ Add to your MCP client config (e.g. `claude_desktop_config.json`):
321
+
322
+ ```json
323
+ {
324
+ "mcpServers": {
325
+ "builtwith": {
326
+ "command": "bw",
327
+ "args": ["mcp"]
328
+ }
329
+ }
330
+ }
331
+ ```
332
+
333
+ If your API key isn't in an env var or `.builtwithrc`, pass it inline:
334
+
335
+ ```json
336
+ {
337
+ "mcpServers": {
338
+ "builtwith": {
339
+ "command": "bw",
340
+ "args": ["mcp", "--key", "YOUR_API_KEY"]
341
+ }
342
+ }
343
+ }
344
+ ```
345
+
346
+ ### 🧰 Available tools
347
+
348
+ | Tool | Description |
349
+ |---|---|
350
+ | `domain_lookup` | 🌐 Technology stack for a domain (supports `nopii`, `liveonly`, date ranges) |
351
+ | `lists_tech` | 📋 Domains currently using a technology |
352
+ | `relationships_lookup` | 🔗 Related domains (shared infra, ownership) |
353
+ | `free_lookup` | 🆓 Free-tier category counts for a domain |
354
+ | `company_find` | 🏢 Domains associated with a company name |
355
+ | `tags_lookup` | 🏷️ Domains related to an IP or tag attribute |
356
+ | `recommendations_lookup` | 💡 Technology recommendations for a domain |
357
+ | `redirects_lookup` | ↪️ Live and historical redirect chains |
358
+ | `keywords_lookup` | 🔤 Keyword data for a domain |
359
+ | `trends_tech` | 📈 Historical adoption trend for a technology |
360
+ | `products_search` | 🛍️ Search ecommerce products across indexed stores |
361
+ | `trust_lookup` | 🛡️ Trust/quality score for a domain |
362
+ | `account_whoami` | 👤 Authenticated account identity |
363
+ | `account_usage` | 📊 API usage statistics |
364
+
365
+ ### 🔬 Implementation note
366
+
367
+ The MCP server is implemented as a pure JSON-RPC 2.0 stdio server with no additional dependencies — auth, HTTP calls, and error handling all use the same code paths as the regular CLI commands.
368
+
369
+ ---
370
+
371
+ ## 🛠️ Development
372
+
373
+ ```bash
374
+ git clone https://github.com/builtwith/builtwith-cli
375
+ cd builtwith-cli
376
+ npm install
377
+ npm test # 24 tests, node:test built-in (no extra framework)
378
+ ```
379
+
380
+ ```bash
381
+ # Run without installing globally
382
+ node bin/bw.js domain lookup example.com --key YOUR_KEY
383
+ ```
384
+
385
+ ---
386
+
387
+ ## 🔗 Related
388
+
389
+ - [BuiltWith TUI](https://github.com/builtwith/builtwith-tui) — interactive terminal UI for the BuiltWith API
390
+ - [BuiltWith API Docs](https://api.builtwith.com) — full API reference
391
+
392
+ ---
393
+
394
+ ## 📄 License
395
+
396
+ MIT
package/bin/bw.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ require('../lib/cli').run();
package/lib/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const { EXIT_CODES, BuiltWithError } = require('./errors');
5
+ const output = require('./output');
6
+
7
+ function run() {
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('bw')
12
+ .description('Non-interactive, scriptable CLI for the BuiltWith API')
13
+ .version('1.0.0')
14
+ .exitOverride()
15
+ .addHelpCommand()
16
+ .option('--key <apikey>', 'API key (overrides env and rc file)')
17
+ .option('--format <fmt>', 'Output format: json (default) | table | csv', 'json')
18
+ .option('--no-color', 'Disable color on stderr')
19
+ .option('--dry-run', 'Print request URL (key masked) and exit')
20
+ .option('--debug', 'Print HTTP metadata to stderr')
21
+ .option('--quiet', 'Suppress spinner/info stderr output');
22
+
23
+ // Register all command modules
24
+ require('./commands/domain')(program);
25
+ require('./commands/lists')(program);
26
+ require('./commands/relationships')(program);
27
+ require('./commands/free')(program);
28
+ require('./commands/company')(program);
29
+ require('./commands/tags')(program);
30
+ require('./commands/recommendations')(program);
31
+ require('./commands/redirects')(program);
32
+ require('./commands/keywords')(program);
33
+ require('./commands/trends')(program);
34
+ require('./commands/products')(program);
35
+ require('./commands/trust')(program);
36
+ require('./commands/account')(program);
37
+ require('./commands/live')(program);
38
+ require('./commands/mcp')(program);
39
+
40
+ program.parseAsync(process.argv).catch(err => {
41
+ if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
42
+ process.exit(EXIT_CODES.SUCCESS);
43
+ }
44
+ if (err instanceof BuiltWithError) {
45
+ output.error(err.message);
46
+ process.exit(err.exitCode);
47
+ }
48
+ // Commander parse errors (missing args, unknown options)
49
+ if (err.code && err.code.startsWith('commander.')) {
50
+ output.error(err.message);
51
+ process.exit(EXIT_CODES.INVALID_INPUT);
52
+ }
53
+ output.error(err.message || String(err));
54
+ if (process.env.DEBUG) console.error(err.stack);
55
+ process.exit(EXIT_CODES.UNEXPECTED);
56
+ });
57
+ }
58
+
59
+ module.exports = { run };
package/lib/client.js ADDED
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const fetch = require('node-fetch');
4
+ const { ApiError, NetworkError } = require('./errors');
5
+
6
+ const BASE_URLS = {
7
+ domain: 'https://api.builtwith.com/v22/api.json',
8
+ lists: 'https://api.builtwith.com/lists12/api.json',
9
+ relationships: 'https://api.builtwith.com/rv4/api.json',
10
+ free: 'https://api.builtwith.com/free1/api.json',
11
+ company: 'https://api.builtwith.com/ctu3/api.json',
12
+ tags: 'https://api.builtwith.com/tag1/api.json',
13
+ recommendations: 'https://api.builtwith.com/rec1/api.json',
14
+ redirects: 'https://api.builtwith.com/redirect1/api.json',
15
+ keywords: 'https://api.builtwith.com/kw2/api.json',
16
+ trends: 'https://api.builtwith.com/trends/v6/api.json',
17
+ products: 'https://api.builtwith.com/productv1/api.json',
18
+ trust: 'https://api.builtwith.com/trustv1/api.json',
19
+ whoami: 'https://api.builtwith.com/whoamiv1/api.json',
20
+ usage: 'https://api.builtwith.com/usagev2/api.json',
21
+ };
22
+
23
+ /**
24
+ * Build a URL with query params.
25
+ * Boolean true values become empty string (e.g., NOPII=) which BuiltWith treats as flag present.
26
+ */
27
+ function buildUrl(endpoint, params) {
28
+ const base = BASE_URLS[endpoint];
29
+ if (!base) throw new Error(`Unknown endpoint: ${endpoint}`);
30
+ const url = new URL(base);
31
+ for (const [k, v] of Object.entries(params)) {
32
+ if (v === null || v === undefined || v === false) continue;
33
+ url.searchParams.set(k, v === true ? '' : String(v));
34
+ }
35
+ return url.toString();
36
+ }
37
+
38
+ function maskKey(url, key) {
39
+ if (!key) return url;
40
+ return url.replace(encodeURIComponent(key), 'REDACTED').replace(key, 'REDACTED');
41
+ }
42
+
43
+ async function request(endpoint, params, opts = {}) {
44
+ const { dryRun, debug, spinner } = opts;
45
+ const url = buildUrl(endpoint, params);
46
+
47
+ if (dryRun) {
48
+ const masked = maskKey(url, params.KEY || params.key);
49
+ process.stdout.write(masked + '\n');
50
+ process.exit(0);
51
+ }
52
+
53
+ if (debug) {
54
+ process.stderr.write(`[debug] GET ${maskKey(url, params.KEY || params.key)}\n`);
55
+ }
56
+
57
+ if (spinner) spinner.start();
58
+
59
+ let res;
60
+ try {
61
+ res = await fetch(url, {
62
+ headers: { 'User-Agent': 'builtwith-cli/1.0.0' },
63
+ timeout: 30000,
64
+ });
65
+ } catch (err) {
66
+ if (spinner) spinner.stop();
67
+ throw new NetworkError(`Network request failed: ${err.message}`);
68
+ }
69
+
70
+ if (debug) {
71
+ process.stderr.write(`[debug] HTTP ${res.status} ${res.statusText}\n`);
72
+ }
73
+
74
+ if (spinner) spinner.stop();
75
+
76
+ let body;
77
+ try {
78
+ body = await res.json();
79
+ } catch (_) {
80
+ throw new ApiError(`Invalid JSON response (HTTP ${res.status})`, res.status);
81
+ }
82
+
83
+ if (!res.ok) {
84
+ const msg = body && body.Errors
85
+ ? body.Errors.map(e => e.Message || e).join('; ')
86
+ : `HTTP ${res.status} ${res.statusText}`;
87
+ throw new ApiError(msg, res.status);
88
+ }
89
+
90
+ // BuiltWith API sometimes returns error info in the body with HTTP 200
91
+ if (body && body.Errors && body.Errors.length > 0) {
92
+ const msg = body.Errors.map(e => e.Message || e).join('; ');
93
+ // Treat auth-related messages as auth errors
94
+ const isAuth = /key|auth|unauthori/i.test(msg);
95
+ throw new ApiError(msg, isAuth ? 403 : 400);
96
+ }
97
+
98
+ return body;
99
+ }
100
+
101
+ module.exports = { buildUrl, maskKey, request, BASE_URLS };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const { requireKey } = require('../config');
5
+ const { request } = require('../client');
6
+ const output = require('../output');
7
+ const ora = require('ora');
8
+
9
+ module.exports = function registerAccount(program) {
10
+ const account = program.command('account').description('Account information');
11
+
12
+ account
13
+ .command('whoami')
14
+ .description('Show account identity')
15
+ .action(async () => {
16
+ const opts = program.opts();
17
+ if (opts.noColor) output.setNoColor(true);
18
+ const key = requireKey(opts.key);
19
+ const spinner = opts.quiet ? null : ora({ text: 'Fetching account...', stream: process.stderr }).start();
20
+ try {
21
+ const data = await request('whoami', { KEY: key }, { dryRun: opts.dryRun, debug: opts.debug, spinner });
22
+ output.print(data, { format: opts.format });
23
+ } catch (err) {
24
+ if (spinner) spinner.stop();
25
+ throw err;
26
+ }
27
+ });
28
+
29
+ account
30
+ .command('usage')
31
+ .description('Show API usage')
32
+ .action(async () => {
33
+ const opts = program.opts();
34
+ if (opts.noColor) output.setNoColor(true);
35
+ const key = requireKey(opts.key);
36
+ const spinner = opts.quiet ? null : ora({ text: 'Fetching usage...', stream: process.stderr }).start();
37
+ try {
38
+ const data = await request('usage', { KEY: key }, { dryRun: opts.dryRun, debug: opts.debug, spinner });
39
+ output.print(data, { format: opts.format });
40
+ } catch (err) {
41
+ if (spinner) spinner.stop();
42
+ throw err;
43
+ }
44
+ });
45
+ };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { requireKey } = require('../config');
4
+ const { request } = require('../client');
5
+ const output = require('../output');
6
+ const ora = require('ora');
7
+
8
+ module.exports = function registerCompany(program) {
9
+ const company = program.command('company').description('Company to URL lookup');
10
+
11
+ company
12
+ .command('find <name>')
13
+ .description('Find domains associated with a company name')
14
+ .action(async (name) => {
15
+ const opts = program.opts();
16
+ if (opts.noColor) output.setNoColor(true);
17
+ const key = requireKey(opts.key);
18
+ const params = { KEY: key, COMPANY: name };
19
+ const spinner = opts.quiet ? null : ora({ text: `Finding company "${name}"...`, stream: process.stderr }).start();
20
+ try {
21
+ const data = await request('company', params, { dryRun: opts.dryRun, debug: opts.debug, spinner });
22
+ output.print(data, { format: opts.format });
23
+ } catch (err) {
24
+ if (spinner) spinner.stop();
25
+ throw err;
26
+ }
27
+ });
28
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const { requireKey } = require('../config');
4
+ const { request } = require('../client');
5
+ const { InputError } = require('../errors');
6
+ const output = require('../output');
7
+ const ora = require('ora');
8
+
9
+ module.exports = function registerDomain(program) {
10
+ const domain = program.command('domain').description('Domain technology lookup');
11
+
12
+ domain
13
+ .command('lookup <domain>')
14
+ .description('Look up technologies used by a domain')
15
+ .option('--nopii', 'Exclude PII data')
16
+ .option('--nometa', 'Exclude meta data')
17
+ .option('--noattr', 'Exclude attribution data')
18
+ .option('--liveonly', 'Only return currently-live technologies')
19
+ .option('--fdrange <YYYYMMDD-YYYYMMDD>', 'First-detected date range')
20
+ .option('--ldrange <YYYYMMDD-YYYYMMDD>', 'Last-detected date range')
21
+ .action(async (domainArg, cmdOpts) => {
22
+ const opts = program.opts();
23
+ if (opts.noColor) output.setNoColor(true);
24
+ if (!domainArg) throw new InputError('Domain argument is required');
25
+ const key = requireKey(opts.key);
26
+
27
+ const params = { KEY: key, LOOKUP: domainArg };
28
+ // Boolean flags: true → '' so URL gets ?NOPII= which BuiltWith treats as present
29
+ if (cmdOpts.nopii) params.NOPII = true;
30
+ if (cmdOpts.nometa) params.NOMETA = true;
31
+ if (cmdOpts.noattr) params.NOATTR = true;
32
+ if (cmdOpts.liveonly) params.LIVEONLY = true;
33
+ if (cmdOpts.fdrange) params.FDRANGE = cmdOpts.fdrange;
34
+ if (cmdOpts.ldrange) params.LDRANGE = cmdOpts.ldrange;
35
+
36
+ const spinner = opts.quiet ? null : ora({ text: `Looking up ${domainArg}...`, stream: process.stderr }).start();
37
+ try {
38
+ const data = await request('domain', params, { dryRun: opts.dryRun, debug: opts.debug, spinner });
39
+ output.print(data, { format: opts.format });
40
+ } catch (err) {
41
+ if (spinner) spinner.stop();
42
+ throw err;
43
+ }
44
+ });
45
+ };