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 +396 -0
- package/bin/bw.js +4 -0
- package/lib/cli.js +59 -0
- package/lib/client.js +101 -0
- package/lib/commands/account.js +45 -0
- package/lib/commands/company.js +28 -0
- package/lib/commands/domain.js +45 -0
- package/lib/commands/free.js +28 -0
- package/lib/commands/keywords.js +28 -0
- package/lib/commands/lists.js +30 -0
- package/lib/commands/live.js +61 -0
- package/lib/commands/mcp.js +333 -0
- package/lib/commands/products.js +30 -0
- package/lib/commands/recommendations.js +28 -0
- package/lib/commands/redirects.js +28 -0
- package/lib/commands/relationships.js +28 -0
- package/lib/commands/tags.js +28 -0
- package/lib/commands/trends.js +28 -0
- package/lib/commands/trust.js +28 -0
- package/lib/config.js +51 -0
- package/lib/errors.js +52 -0
- package/lib/output.js +100 -0
- package/lib/ws-client.js +53 -0
- package/package.json +40 -0
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
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
|
+
};
|