domsniper 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 (63) hide show
  1. package/.env.example +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/package.json +72 -0
  5. package/src/app.tsx +2062 -0
  6. package/src/completions.ts +65 -0
  7. package/src/core/db.ts +1313 -0
  8. package/src/core/features/asn-lookup.ts +91 -0
  9. package/src/core/features/backlinks.ts +83 -0
  10. package/src/core/features/blacklist-check.ts +67 -0
  11. package/src/core/features/cert-transparency.ts +87 -0
  12. package/src/core/features/config.ts +81 -0
  13. package/src/core/features/cors-check.ts +90 -0
  14. package/src/core/features/dns-details.ts +27 -0
  15. package/src/core/features/domain-age.ts +33 -0
  16. package/src/core/features/domain-suggest.ts +87 -0
  17. package/src/core/features/drop-catch.ts +159 -0
  18. package/src/core/features/email-security.ts +112 -0
  19. package/src/core/features/expiring-feed.ts +160 -0
  20. package/src/core/features/export.ts +74 -0
  21. package/src/core/features/filter.ts +96 -0
  22. package/src/core/features/http-probe.ts +46 -0
  23. package/src/core/features/marketplace.ts +69 -0
  24. package/src/core/features/path-scanner.ts +123 -0
  25. package/src/core/features/port-scanner.ts +132 -0
  26. package/src/core/features/portfolio-bulk.ts +125 -0
  27. package/src/core/features/portfolio-monitor.ts +214 -0
  28. package/src/core/features/portfolio.ts +98 -0
  29. package/src/core/features/price-compare.ts +39 -0
  30. package/src/core/features/rdap.ts +128 -0
  31. package/src/core/features/reverse-ip.ts +73 -0
  32. package/src/core/features/s3-export.ts +99 -0
  33. package/src/core/features/scoring.ts +121 -0
  34. package/src/core/features/security-headers.ts +162 -0
  35. package/src/core/features/session.ts +74 -0
  36. package/src/core/features/snipe.ts +264 -0
  37. package/src/core/features/social-check.ts +81 -0
  38. package/src/core/features/ssl-check.ts +88 -0
  39. package/src/core/features/subdomain-discovery.ts +53 -0
  40. package/src/core/features/takeover-detect.ts +143 -0
  41. package/src/core/features/tech-stack.ts +135 -0
  42. package/src/core/features/tld-expand.ts +43 -0
  43. package/src/core/features/variations.ts +134 -0
  44. package/src/core/features/version-check.ts +58 -0
  45. package/src/core/features/waf-detect.ts +171 -0
  46. package/src/core/features/watch.ts +120 -0
  47. package/src/core/features/wayback.ts +64 -0
  48. package/src/core/features/webhooks.ts +126 -0
  49. package/src/core/features/whois-history.ts +99 -0
  50. package/src/core/features/zone-transfer.ts +75 -0
  51. package/src/core/index.ts +50 -0
  52. package/src/core/paths.ts +9 -0
  53. package/src/core/registrar.ts +413 -0
  54. package/src/core/theme.ts +140 -0
  55. package/src/core/types.ts +143 -0
  56. package/src/core/validate.ts +58 -0
  57. package/src/core/whois.ts +265 -0
  58. package/src/index.tsx +1888 -0
  59. package/src/market-client.ts +186 -0
  60. package/src/proxy/ca.ts +116 -0
  61. package/src/proxy/db.ts +175 -0
  62. package/src/proxy/server.ts +155 -0
  63. package/tsconfig.json +30 -0
package/src/index.tsx ADDED
@@ -0,0 +1,1888 @@
1
+ #!/usr/bin/env bun
2
+ import { createCliRenderer } from "@opentui/core";
3
+ import { createRoot } from "@opentui/react";
4
+ import { App } from "./app.js";
5
+ import { Command } from "commander";
6
+ import { lookupDns } from "./core/features/dns-details.js";
7
+ import { httpProbe } from "./core/features/http-probe.js";
8
+ import { checkWayback } from "./core/features/wayback.js";
9
+ import { calculateDomainAge } from "./core/features/domain-age.js";
10
+ import { sanitizeDomainList, safePath } from "./core/validate.js";
11
+ import { bashCompletions, zshCompletions, fishCompletions } from "./completions.js";
12
+ import { checkSocialMedia } from "./core/features/social-check.js";
13
+ import { detectTechStack } from "./core/features/tech-stack.js";
14
+ import { checkBlacklists } from "./core/features/blacklist-check.js";
15
+ import { estimateBacklinks } from "./core/features/backlinks.js";
16
+ import { scanPorts } from "./core/features/port-scanner.js";
17
+ import { reverseIpLookup } from "./core/features/reverse-ip.js";
18
+ import { lookupAsn } from "./core/features/asn-lookup.js";
19
+ import { checkEmailSecurity } from "./core/features/email-security.js";
20
+ import { checkZoneTransfer } from "./core/features/zone-transfer.js";
21
+ import { queryCertTransparency } from "./core/features/cert-transparency.js";
22
+ import { detectTakeover } from "./core/features/takeover-detect.js";
23
+ import { auditSecurityHeaders } from "./core/features/security-headers.js";
24
+ import { detectWaf } from "./core/features/waf-detect.js";
25
+ import { scanPaths } from "./core/features/path-scanner.js";
26
+ import { checkCors } from "./core/features/cors-check.js";
27
+ import { rdapLookup } from "./core/features/rdap.js";
28
+ import { checkSsl } from "./core/features/ssl-check.js";
29
+ import type { PortScanResult } from "./core/features/port-scanner.js";
30
+ import type { ReverseIpResult } from "./core/features/reverse-ip.js";
31
+ import type { AsnResult } from "./core/features/asn-lookup.js";
32
+ import type { EmailSecurityResult } from "./core/features/email-security.js";
33
+ import type { ZoneTransferResult } from "./core/features/zone-transfer.js";
34
+ import type { CertTransparencyResult } from "./core/features/cert-transparency.js";
35
+ import type { TakeoverResult } from "./core/features/takeover-detect.js";
36
+ import type { SecurityHeadersResult } from "./core/features/security-headers.js";
37
+ import type { WafResult } from "./core/features/waf-detect.js";
38
+ import type { PathScanResult } from "./core/features/path-scanner.js";
39
+ import type { CorsResult } from "./core/features/cors-check.js";
40
+
41
+ const program = new Command();
42
+
43
+ program
44
+ .name("dsniper")
45
+ .description("All-in-one domain intelligence toolkit — availability checker, security recon, portfolio manager")
46
+ .version("0.1.0")
47
+ .argument("[domains...]", "Domain(s) to check")
48
+ .option("-f, --file <path>", "Path to file with domains (one per line)")
49
+ .option("-a, --auto-register", "Automatically register available domains", false)
50
+ .option("--headless", "Run in non-interactive mode (print results to stdout)", false)
51
+ .option("--json", "Output results as JSON", false)
52
+ .option("-c, --concurrency <n>", "Concurrent lookups (default: 5)", "5")
53
+ .option("--recon", "Enable full recon mode in headless scanning", false)
54
+ .action(async (domains: string[], options: CliOptions) => {
55
+ // Stdin pipe support: read from stdin when no TTY and no domains/file provided
56
+ if (!process.stdin.isTTY && domains.length === 0 && !options.file) {
57
+ const { parseDomainList } = await import("./core/whois.js");
58
+ const chunks: Buffer[] = [];
59
+ for await (const chunk of process.stdin) {
60
+ chunks.push(chunk as Buffer);
61
+ }
62
+ const stdinContent = Buffer.concat(chunks).toString("utf-8");
63
+ const stdinDomains = parseDomainList(stdinContent);
64
+ domains.push(...stdinDomains);
65
+ }
66
+
67
+ if (options.headless || !process.stdout.isTTY) {
68
+ // Non-interactive mode
69
+ await runHeadless(domains, options);
70
+ } else {
71
+ // TUI mode
72
+ const renderer = await createCliRenderer({
73
+ exitOnCtrlC: true,
74
+ screenMode: "alternate-screen",
75
+ });
76
+
77
+ createRoot(renderer).render(
78
+ <App
79
+ initialDomains={domains.length > 0 ? domains : undefined}
80
+ batchFile={options.file}
81
+ autoRegister={options.autoRegister}
82
+ />
83
+ );
84
+ }
85
+ });
86
+
87
+ // ─── Completions subcommand ──────────────────────────────
88
+
89
+ program
90
+ .command("completions <shell>")
91
+ .description("Generate shell completions (bash, zsh, fish)")
92
+ .action((shell: string) => {
93
+ switch (shell.toLowerCase()) {
94
+ case "bash":
95
+ process.stdout.write(bashCompletions());
96
+ break;
97
+ case "zsh":
98
+ process.stdout.write(zshCompletions());
99
+ break;
100
+ case "fish":
101
+ process.stdout.write(fishCompletions());
102
+ break;
103
+ default:
104
+ console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
105
+ process.exit(1);
106
+ }
107
+ });
108
+
109
+ // ─── Suggest subcommand ──────────────────────────────────
110
+
111
+ program
112
+ .command("suggest <keyword>")
113
+ .description("Generate domain name suggestions from a keyword")
114
+ .option("-t, --tld <tld>", "TLD to use", "com")
115
+ .option("-n, --count <n>", "Number of suggestions", "20")
116
+ .option("--check", "Check availability of suggestions", false)
117
+ .action(async (keyword: string, opts: { tld: string; count: string; check: boolean }) => {
118
+ const { generateSuggestions } = await import("./core/features/domain-suggest.js");
119
+ const suggestions = generateSuggestions(keyword, opts.tld, parseInt(opts.count, 10));
120
+
121
+ if (opts.check) {
122
+ const { whoisLookup } = await import("./core/whois.js");
123
+ console.log(`\nChecking ${suggestions.length} suggestions for "${keyword}"...\n`);
124
+ for (const s of suggestions) {
125
+ const whois = await whoisLookup(s.domain);
126
+ const status = whois.available ? "\x1b[32mAVAILABLE\x1b[0m" : "\x1b[31mTAKEN\x1b[0m";
127
+ console.log(` ${status} ${s.domain} (${s.strategy})`);
128
+ await new Promise((r) => setTimeout(r, 1000));
129
+ }
130
+ } else {
131
+ console.log(`\nSuggestions for "${keyword}" (.${opts.tld}):\n`);
132
+ for (const s of suggestions) {
133
+ console.log(` ${s.domain} (${s.strategy})`);
134
+ }
135
+ }
136
+ console.log();
137
+ });
138
+
139
+ // ─── Portfolio subcommand ────────────────────────────────
140
+
141
+ program
142
+ .command("portfolio")
143
+ .description("Manage your domain portfolio")
144
+ .option("--add <domain>", "Add a domain to portfolio")
145
+ .option("--remove <domain>", "Remove a domain from portfolio")
146
+ .option("--status <domain:status>", "Set domain status (active|parked|for-sale|development|archived)")
147
+ .option("--category <domain:category>", "Set domain category")
148
+ .option("--value <domain:amount>", "Set estimated value")
149
+ .option("--transaction <domain:type:amount>", "Record transaction (purchase|renewal|sale|parking-revenue|affiliate-revenue|expense)")
150
+ .option("--expiring [days]", "Show domains expiring within N days")
151
+ .option("--renewals", "Show renewal calendar")
152
+ .option("--health", "Run health check on all portfolio domains")
153
+ .option("--pnl [domain]", "Show P&L (for specific domain or total)")
154
+ .option("--monthly [months]", "Show monthly financial report")
155
+ .option("--pipeline", "Show acquisition pipeline")
156
+ .option("--pipeline-add <domain>", "Add domain to acquisition pipeline")
157
+ .option("--alerts", "Show unacknowledged alerts")
158
+ .option("--dismiss-alerts", "Dismiss all alerts")
159
+ .option("--categories", "List categories")
160
+ .option("--export-csv <path>", "Export portfolio to CSV")
161
+ .option("--export-tax <year>", "Export tax data for year")
162
+ .option("--export-transactions <path>", "Export transactions to CSV")
163
+ .option("--upload-s3", "Upload latest export to S3/R2")
164
+ .option("--dashboard", "Show portfolio dashboard summary")
165
+ .option("--stats", "Show portfolio statistics")
166
+ .option("--json", "Output as JSON")
167
+ .action(async (opts: {
168
+ add?: string; remove?: string; status?: string; category?: string; value?: string;
169
+ transaction?: string; expiring?: string; renewals?: boolean; health?: boolean;
170
+ pnl?: string | boolean; monthly?: string | boolean; pipeline?: boolean; pipelineAdd?: string;
171
+ alerts?: boolean; dismissAlerts?: boolean; categories?: boolean;
172
+ exportCsv?: string; exportTax?: string; exportTransactions?: string;
173
+ uploadS3?: boolean; dashboard?: boolean; stats?: boolean; json?: boolean;
174
+ }) => {
175
+ if (opts.add) {
176
+ const { addToPortfolio } = await import("./core/features/portfolio.js");
177
+ addToPortfolio(opts.add);
178
+ console.log(`Added ${opts.add} to portfolio`);
179
+ return;
180
+ }
181
+
182
+ if (opts.remove) {
183
+ const { removeFromPortfolio } = await import("./core/features/portfolio.js");
184
+ removeFromPortfolio(opts.remove);
185
+ console.log(`Removed ${opts.remove} from portfolio`);
186
+ return;
187
+ }
188
+
189
+ if (opts.status) {
190
+ const [domain, status] = opts.status.split(":");
191
+ if (!domain || !status) { console.error("Usage: --status domain.com:active"); process.exit(1); }
192
+ const { updatePortfolioStatus } = await import("./core/db.js");
193
+ updatePortfolioStatus(domain, status as any);
194
+ console.log(`Set ${domain} status to ${status}`);
195
+ return;
196
+ }
197
+
198
+ if (opts.category) {
199
+ const [domain, category] = opts.category.split(":");
200
+ if (!domain || !category) { console.error("Usage: --category domain.com:investments"); process.exit(1); }
201
+ const { updatePortfolioCategory } = await import("./core/db.js");
202
+ updatePortfolioCategory(domain, category);
203
+ console.log(`Set ${domain} category to ${category}`);
204
+ return;
205
+ }
206
+
207
+ if (opts.value) {
208
+ const [domain, amountStr] = opts.value.split(":");
209
+ if (!domain || !amountStr) { console.error("Usage: --value domain.com:5000"); process.exit(1); }
210
+ const { updatePortfolioValue } = await import("./core/db.js");
211
+ updatePortfolioValue(domain, parseFloat(amountStr));
212
+ console.log(`Set ${domain} estimated value to $${amountStr}`);
213
+ return;
214
+ }
215
+
216
+ if (opts.transaction) {
217
+ const parts = opts.transaction.split(":");
218
+ if (parts.length < 3) { console.error("Usage: --transaction domain.com:purchase:9.99"); process.exit(1); }
219
+ const [domain, type, amountStr] = parts;
220
+ const { addTransaction } = await import("./core/db.js");
221
+ addTransaction(domain!, type as any, parseFloat(amountStr!));
222
+ console.log(`Recorded ${type} of $${amountStr} for ${domain}`);
223
+ return;
224
+ }
225
+
226
+ if (opts.expiring !== undefined) {
227
+ const { getPortfolioExpiring, closeDb } = await import("./core/db.js");
228
+ const days = parseInt(String(opts.expiring) || "30", 10) || 30;
229
+ const expiring = getPortfolioExpiring(days);
230
+ if (opts.json) { console.log(JSON.stringify(expiring, null, 2)); closeDb(); return; }
231
+ console.log(`\nDomains expiring within ${days} days:\n`);
232
+ if (expiring.length === 0) { console.log(" None"); }
233
+ for (const d of expiring) { console.log(` ${d.domain} ${d.expiry_date} ${d.registrar}`); }
234
+ console.log();
235
+ closeDb();
236
+ return;
237
+ }
238
+
239
+ if (opts.renewals) {
240
+ const { generateRenewalCalendar, estimateAnnualRenewalCost } = await import("./core/features/portfolio-monitor.js");
241
+ const calendar = generateRenewalCalendar(12);
242
+ const annualCost = estimateAnnualRenewalCost();
243
+ if (opts.json) { console.log(JSON.stringify({ calendar, annualCost }, null, 2)); return; }
244
+ console.log(`\nRenewal Calendar (est. annual cost: $${annualCost.toFixed(0)}):\n`);
245
+ if (calendar.length === 0) { console.log(" No upcoming renewals."); }
246
+ for (const r of calendar) {
247
+ const urgency = r.daysLeft <= 7 ? "!!" : r.daysLeft <= 30 ? "! " : " ";
248
+ console.log(` ${urgency} ${r.domain.padEnd(30)} ${String(r.daysLeft).padStart(4)}d $${r.renewalPrice}${r.autoRenew ? " (auto-renew)" : ""}`);
249
+ }
250
+ console.log();
251
+ return;
252
+ }
253
+
254
+ if (opts.health) {
255
+ const { runPortfolioHealthCheck } = await import("./core/features/portfolio-monitor.js");
256
+ console.log("\nRunning portfolio health check...\n");
257
+ const report = await runPortfolioHealthCheck((domain, i, total) => {
258
+ process.stdout.write(`\r Checking ${i + 1}/${total}: ${domain}...`);
259
+ });
260
+ console.log("\r" + " ".repeat(60) + "\r");
261
+ if (opts.json) { console.log(JSON.stringify(report, null, 2)); return; }
262
+ console.log(`Health Check Results:`);
263
+ console.log(` Checked: ${report.checked}`);
264
+ console.log(` Healthy: ${report.healthy}`);
265
+ console.log(` Warnings: ${report.warnings}`);
266
+ console.log(` Critical: ${report.critical}`);
267
+ if (report.alerts.length > 0) {
268
+ console.log(`\nAlerts:`);
269
+ for (const a of report.alerts) {
270
+ const icon = a.severity === "critical" ? "!!" : a.severity === "warning" ? "! " : " ";
271
+ console.log(` ${icon} ${a.domain}: ${a.message}`);
272
+ }
273
+ }
274
+ console.log();
275
+ return;
276
+ }
277
+
278
+ if (opts.pnl !== undefined) {
279
+ const { getDomainPnL, getPortfolioPnL } = await import("./core/db.js");
280
+ if (typeof opts.pnl === "string") {
281
+ const pnl = getDomainPnL(opts.pnl);
282
+ if (opts.json) { console.log(JSON.stringify(pnl, null, 2)); return; }
283
+ console.log(`\nP&L for ${opts.pnl}:`);
284
+ console.log(` Costs: $${pnl.costs.toFixed(2)}`);
285
+ console.log(` Revenue: $${pnl.revenue.toFixed(2)}`);
286
+ console.log(` Profit: $${pnl.profit.toFixed(2)}\n`);
287
+ } else {
288
+ const pnl = getPortfolioPnL();
289
+ if (opts.json) { console.log(JSON.stringify(pnl, null, 2)); return; }
290
+ console.log(`\nPortfolio P&L:`);
291
+ console.log(` Total Costs: $${pnl.totalCosts.toFixed(2)}`);
292
+ console.log(` Total Revenue: $${pnl.totalRevenue.toFixed(2)}`);
293
+ console.log(` Total Profit: $${pnl.totalProfit.toFixed(2)}`);
294
+ console.log(` Domains: ${pnl.domainCount}\n`);
295
+ }
296
+ return;
297
+ }
298
+
299
+ if (opts.monthly !== undefined) {
300
+ const { getMonthlyReport } = await import("./core/db.js");
301
+ const months = typeof opts.monthly === "string" ? parseInt(opts.monthly, 10) : 12;
302
+ const report = getMonthlyReport(months);
303
+ if (opts.json) { console.log(JSON.stringify(report, null, 2)); return; }
304
+ console.log(`\nMonthly Report (${months} months):\n`);
305
+ console.log(` ${"Month".padEnd(10)} ${"Costs".padEnd(12)} ${"Revenue".padEnd(12)} ${"Profit".padEnd(12)}`);
306
+ console.log(` ${"─".repeat(10)} ${"─".repeat(12)} ${"─".repeat(12)} ${"─".repeat(12)}`);
307
+ for (const m of report) {
308
+ console.log(` ${m.month.padEnd(10)} $${m.costs.toFixed(2).padStart(10)} $${m.revenue.toFixed(2).padStart(10)} $${m.profit.toFixed(2).padStart(10)}`);
309
+ }
310
+ console.log();
311
+ return;
312
+ }
313
+
314
+ if (opts.pipeline) {
315
+ const { getPipeline } = await import("./core/db.js");
316
+ const pipeline = getPipeline();
317
+ if (opts.json) { console.log(JSON.stringify(pipeline, null, 2)); return; }
318
+ console.log(`\nAcquisition Pipeline (${pipeline.length} domains):\n`);
319
+ if (pipeline.length === 0) { console.log(" Empty. Use --pipeline-add <domain> to add."); }
320
+ for (const p of pipeline) {
321
+ console.log(` ${p.domain.padEnd(30)} ${p.status.padEnd(14)} ${p.priority}${p.max_bid ? ` max: $${p.max_bid}` : ""}${p.notes ? ` ${p.notes}` : ""}`);
322
+ }
323
+ console.log();
324
+ return;
325
+ }
326
+
327
+ if (opts.pipelineAdd) {
328
+ const { addToPipeline } = await import("./core/db.js");
329
+ addToPipeline(opts.pipelineAdd);
330
+ console.log(`Added ${opts.pipelineAdd} to acquisition pipeline`);
331
+ return;
332
+ }
333
+
334
+ if (opts.alerts) {
335
+ const { getUnacknowledgedAlerts } = await import("./core/db.js");
336
+ const alerts = getUnacknowledgedAlerts();
337
+ if (opts.json) { console.log(JSON.stringify(alerts, null, 2)); return; }
338
+ console.log(`\nAlerts (${alerts.length} unacknowledged):\n`);
339
+ if (alerts.length === 0) { console.log(" No alerts."); }
340
+ for (const a of alerts) {
341
+ const icon = a.severity === "critical" ? "!!" : a.severity === "warning" ? "! " : " ";
342
+ console.log(` ${icon} [${a.severity}] ${a.domain}: ${a.message} (${a.created_at})`);
343
+ }
344
+ console.log();
345
+ return;
346
+ }
347
+
348
+ if (opts.dismissAlerts) {
349
+ const { acknowledgeAllAlerts } = await import("./core/db.js");
350
+ acknowledgeAllAlerts();
351
+ console.log("All alerts dismissed");
352
+ return;
353
+ }
354
+
355
+ if (opts.categories) {
356
+ const { getCategories } = await import("./core/db.js");
357
+ const categories = getCategories();
358
+ if (opts.json) { console.log(JSON.stringify(categories, null, 2)); return; }
359
+ console.log(`\nCategories:\n`);
360
+ for (const c of categories) {
361
+ console.log(` ${c.name.padEnd(20)} ${c.count} domain(s)${c.description ? ` — ${c.description}` : ""}`);
362
+ }
363
+ console.log();
364
+ return;
365
+ }
366
+
367
+ if (opts.exportCsv) {
368
+ const { exportPortfolioCSV } = await import("./core/features/portfolio-bulk.js");
369
+ const path = exportPortfolioCSV(opts.exportCsv);
370
+ console.log(`Portfolio exported to ${path}`);
371
+ return;
372
+ }
373
+
374
+ if (opts.exportTax) {
375
+ const year = parseInt(opts.exportTax, 10);
376
+ if (isNaN(year)) { console.error("Usage: --export-tax 2025"); process.exit(1); }
377
+ const { exportTaxCSV } = await import("./core/features/portfolio-bulk.js");
378
+ const path = exportTaxCSV(`tax-${year}.csv`, year);
379
+ console.log(`Tax data for ${year} exported to ${path}`);
380
+ return;
381
+ }
382
+
383
+ if (opts.exportTransactions) {
384
+ const { exportTransactionsCSV } = await import("./core/features/portfolio-bulk.js");
385
+ const path = exportTransactionsCSV(opts.exportTransactions);
386
+ console.log(`Transactions exported to ${path}`);
387
+ return;
388
+ }
389
+
390
+ if (opts.uploadS3) {
391
+ const { isS3Configured, uploadPortfolioExport } = await import("./core/features/s3-export.js");
392
+ if (!isS3Configured()) {
393
+ console.error("S3 not configured. Set S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY in .env");
394
+ process.exit(1);
395
+ }
396
+ const { exportPortfolioCSV } = await import("./core/features/portfolio-bulk.js");
397
+ const csvPath = exportPortfolioCSV("/tmp/ds-portfolio-export.csv");
398
+ const csvContent = await Bun.file(csvPath).text();
399
+ const result = await uploadPortfolioExport(csvContent);
400
+ console.log(`Uploaded to S3: ${result.key}`);
401
+ return;
402
+ }
403
+
404
+ if (opts.dashboard) {
405
+ const { getPortfolioDashboard, getTotalPortfolioValue } = await import("./core/db.js");
406
+ const { estimateAnnualRenewalCost } = await import("./core/features/portfolio-monitor.js");
407
+ const dash = getPortfolioDashboard();
408
+ const annualCost = estimateAnnualRenewalCost();
409
+ if (opts.json) { console.log(JSON.stringify(dash, null, 2)); return; }
410
+ console.log(`\n◆ Portfolio Dashboard`);
411
+ console.log(` Domains: ${dash.totalDomains} | Value: $${dash.totalValue.toFixed(0)} | Annual cost: ~$${annualCost.toFixed(0)}`);
412
+ console.log(` P&L: -$${dash.totalCosts.toFixed(0)} costs + $${dash.totalRevenue.toFixed(0)} revenue = $${dash.totalProfit.toFixed(0)} profit`);
413
+ console.log(` Expiring: ${dash.expiringIn30} in 30d, ${dash.expiringIn90} in 90d`);
414
+ console.log(` Alerts: ${dash.activeAlerts} | Pipeline: ${dash.pipelineCount}`);
415
+ if (Object.keys(dash.byStatus).length > 0) {
416
+ console.log(` Status: ${Object.entries(dash.byStatus).map(([s, c]) => `${s}(${c})`).join(" ")}`);
417
+ }
418
+ console.log();
419
+ return;
420
+ }
421
+
422
+ if (opts.stats) {
423
+ const { getPortfolioStatsDb, closeDb } = await import("./core/db.js");
424
+ const stats = getPortfolioStatsDb();
425
+ if (opts.json) { console.log(JSON.stringify(stats, null, 2)); closeDb(); return; }
426
+ console.log(`\nPortfolio Stats:`);
427
+ console.log(` Domains: ${stats.total}`);
428
+ console.log(` Total spent: $${stats.totalSpent}`);
429
+ console.log(` Expiring (30d): ${stats.expiringIn30}`);
430
+ console.log(` Expiring (90d): ${stats.expiringIn90}`);
431
+ if (Object.keys(stats.byRegistrar).length > 0) {
432
+ console.log(` By registrar:`);
433
+ for (const [reg, count] of Object.entries(stats.byRegistrar)) {
434
+ console.log(` ${reg}: ${count}`);
435
+ }
436
+ }
437
+ console.log();
438
+ closeDb();
439
+ return;
440
+ }
441
+
442
+ // Default: list all portfolio domains
443
+ const { getPortfolioDomains, closeDb } = await import("./core/db.js");
444
+ const domains = getPortfolioDomains();
445
+ if (opts.json) { console.log(JSON.stringify(domains, null, 2)); closeDb(); return; }
446
+ console.log(`\nDomain Portfolio (${domains.length} domains):\n`);
447
+ if (domains.length === 0) { console.log(" Empty. Use --add <domain> to add domains."); }
448
+ for (const d of domains) {
449
+ console.log(` ${d.domain.padEnd(30)} ${(d as any).status?.padEnd(12) || "active".padEnd(12)} ${d.registrar.padEnd(16)} ${d.expiry_date || "no expiry"} $${d.purchase_price}`);
450
+ }
451
+ console.log();
452
+ closeDb();
453
+ });
454
+
455
+ // ─── Config subcommand ───────────────────────────────────
456
+
457
+ program
458
+ .command("config")
459
+ .description("View or edit configuration")
460
+ .option("--path", "Show config file path")
461
+ .option("--show", "Show current config")
462
+ .option("--reset", "Reset to defaults")
463
+ .option("--set <key=value>", "Set a config value (e.g., concurrency=10)")
464
+ .action(async (opts: { path?: boolean; show?: boolean; reset?: boolean; set?: string }) => {
465
+ const { loadConfig, saveConfig, getConfigPath, resetConfig } = await import("./core/features/config.js");
466
+
467
+ if (opts.path) { console.log(getConfigPath()); return; }
468
+ if (opts.reset) { resetConfig(); console.log("Config reset to defaults"); return; }
469
+ if (opts.set) {
470
+ const [key, ...valueParts] = opts.set.split("=");
471
+ const value = valueParts.join("=");
472
+ if (!key || !value) { console.error("Usage: --set key=value"); process.exit(1); }
473
+ const config = loadConfig();
474
+ // Handle nested keys like "notifications.webhookUrl"
475
+ const keys = key.split(".");
476
+ let obj: Record<string, unknown> = config as unknown as Record<string, unknown>;
477
+ for (let i = 0; i < keys.length - 1; i++) {
478
+ const k = keys[i]!;
479
+ if (obj[k] === undefined || typeof obj[k] !== "object" || obj[k] === null) {
480
+ console.error(`Unknown key: ${key}`);
481
+ process.exit(1);
482
+ }
483
+ obj = obj[k] as Record<string, unknown>;
484
+ }
485
+ const lastKey = keys[keys.length - 1]!;
486
+ // Try to preserve type
487
+ const numVal = Number(value);
488
+ if (value === "true") obj[lastKey] = true;
489
+ else if (value === "false") obj[lastKey] = false;
490
+ else if (value === "null") obj[lastKey] = null;
491
+ else if (!isNaN(numVal) && value !== "") obj[lastKey] = numVal;
492
+ else obj[lastKey] = value;
493
+ saveConfig(config);
494
+ console.log(`Set ${key} = ${value}`);
495
+ return;
496
+ }
497
+ // Default: show config
498
+ const config = loadConfig();
499
+ console.log(JSON.stringify(config, null, 2));
500
+ });
501
+
502
+ // ─── Expiring subcommand ────────────────────────────────
503
+
504
+ program
505
+ .command("expiring")
506
+ .description("Browse expiring/dropping domains")
507
+ .option("--tld <tld>", "Filter by TLD (e.g., com, net)")
508
+ .option("--min-age <years>", "Minimum domain age in years")
509
+ .option("-n, --limit <n>", "Max results", "20")
510
+ .option("--api-key <key>", "WhoisFreaks API key")
511
+ .option("--json", "Output as JSON")
512
+ .action(async (opts: { tld?: string; minAge?: string; limit: string; apiKey?: string; json?: boolean }) => {
513
+ const { getExpiringFeed } = await import("./core/features/expiring-feed.js");
514
+ const results = await getExpiringFeed({
515
+ apiKey: opts.apiKey || process.env.WHOISFREAKS_API_KEY,
516
+ tld: opts.tld,
517
+ minAge: opts.minAge ? parseInt(opts.minAge, 10) : undefined,
518
+ limit: parseInt(opts.limit, 10),
519
+ });
520
+ if (opts.json) { console.log(JSON.stringify(results, null, 2)); return; }
521
+ console.log(`\nExpiring Domains (${results.length} found):\n`);
522
+ if (results.length === 0) { console.log(" No results. Provide --api-key or set WHOISFREAKS_API_KEY env var."); }
523
+ for (const d of results) {
524
+ console.log(` ${d.domain} expires: ${d.expiryDate}${d.registrar ? ` [${d.registrar}]` : ""}${d.age ? ` age: ${d.age}` : ""}`);
525
+ }
526
+ console.log();
527
+ });
528
+
529
+ // ─── Drop catch subcommand ──────────────────────────────
530
+
531
+ program
532
+ .command("dropcatch <domain>")
533
+ .description("Monitor an expiring domain and auto-register when it drops")
534
+ .option("--interval <seconds>", "Poll interval in seconds", "30")
535
+ .option("--max-hours <hours>", "Maximum hours to monitor", "24")
536
+ .action(async (domain: string, opts: { interval: string; maxHours: string }) => {
537
+ const { createDropCatcher, formatDropCatchStatus } = await import("./core/features/drop-catch.js");
538
+ const { loadConfigFromEnv } = await import("./core/registrar.js");
539
+ const config = loadConfigFromEnv();
540
+ if (!config?.apiKey) { console.error("Registrar credentials required. Set REGISTRAR_PROVIDER and REGISTRAR_API_KEY."); process.exit(1); }
541
+ const intervalMs = parseInt(opts.interval, 10) * 1000;
542
+ const maxAttempts = Math.ceil((parseInt(opts.maxHours, 10) * 3600000) / intervalMs);
543
+ console.log(`\nDrop Catch: ${domain}`);
544
+ console.log(` Polling every ${opts.interval}s for up to ${opts.maxHours}h (${maxAttempts} attempts)\n`);
545
+ const catcher = createDropCatcher({
546
+ domain, registrarConfig: config, pollIntervalMs: intervalMs, maxAttempts,
547
+ onStatus: (s) => console.log(` ${formatDropCatchStatus(s)}`),
548
+ onSuccess: () => { console.log(`\n SUCCESS — ${domain} registered!\n`); process.exit(0); },
549
+ onFailed: (_d, err) => { console.log(`\n FAILED — ${err}\n`); process.exit(1); },
550
+ });
551
+ await catcher.start();
552
+ });
553
+
554
+ // ─── Recon subcommand ──────────────────────────────────
555
+
556
+ program
557
+ .command("recon <domain>")
558
+ .description("Full security reconnaissance scan — ports, headers, email security, WAF, paths, CORS, certificates, and more")
559
+ .option("--json", "Output as JSON")
560
+ .action(async (domain: string, opts: { json?: boolean }) => {
561
+ await checkDependencies();
562
+ const { whoisLookup, verifyAvailability } = await import("./core/whois.js");
563
+
564
+ if (!opts.json) {
565
+ console.log(`\n\x1b[1m=== RECON: ${domain} ===\x1b[0m\n`);
566
+ }
567
+
568
+ const results = await Promise.allSettled([
569
+ whoisLookup(domain),
570
+ lookupDns(domain),
571
+ httpProbe(domain),
572
+ checkWayback(domain),
573
+ rdapLookup(domain),
574
+ checkSsl(domain),
575
+ checkSocialMedia(domain),
576
+ detectTechStack(domain),
577
+ checkBlacklists(domain),
578
+ estimateBacklinks(domain),
579
+ scanPorts(domain),
580
+ reverseIpLookup(domain),
581
+ lookupAsn(domain),
582
+ checkEmailSecurity(domain),
583
+ checkZoneTransfer(domain),
584
+ queryCertTransparency(domain),
585
+ detectTakeover(domain),
586
+ auditSecurityHeaders(domain),
587
+ detectWaf(domain),
588
+ scanPaths(domain),
589
+ checkCors(domain),
590
+ ]);
591
+
592
+ const val = <T,>(r: PromiseSettledResult<T>): T | null =>
593
+ r.status === "fulfilled" ? r.value : null;
594
+
595
+ const whois = val(results[0]!);
596
+ const dns = val(results[1]!);
597
+ const http = val(results[2]!);
598
+ const wayback = val(results[3]!);
599
+ const rdap = val(results[4]!);
600
+ const ssl = val(results[5]!);
601
+ const social = val(results[6]!);
602
+ const tech = val(results[7]!);
603
+ const blacklist = val(results[8]!);
604
+ const backlinks = val(results[9]!);
605
+ const ports = val(results[10]!) as PortScanResult | null;
606
+ const reverseIp = val(results[11]!) as ReverseIpResult | null;
607
+ const asn = val(results[12]!) as AsnResult | null;
608
+ const emailSec = val(results[13]!) as EmailSecurityResult | null;
609
+ const zoneXfer = val(results[14]!) as ZoneTransferResult | null;
610
+ const certs = val(results[15]!) as CertTransparencyResult | null;
611
+ const takeover = val(results[16]!) as TakeoverResult | null;
612
+ const secHeaders = val(results[17]!) as SecurityHeadersResult | null;
613
+ const waf = val(results[18]!) as WafResult | null;
614
+ const paths = val(results[19]!) as PathScanResult | null;
615
+ const cors = val(results[20]!) as CorsResult | null;
616
+
617
+ if (opts.json) {
618
+ console.log(JSON.stringify({
619
+ domain,
620
+ timestamp: new Date().toISOString(),
621
+ whois, dns, http, wayback, rdap, ssl, social, tech, blacklist, backlinks,
622
+ ports, reverseIp, asn, emailSecurity: emailSec, zoneTransfer: zoneXfer,
623
+ certTransparency: certs, takeover, securityHeaders: secHeaders, waf,
624
+ pathScan: paths, cors,
625
+ }, null, 2));
626
+ return;
627
+ }
628
+
629
+ // --- Text output ---
630
+ const g = "\x1b[32m", r = "\x1b[31m", y = "\x1b[33m", c = "\x1b[36m";
631
+ const b = "\x1b[1m", d = "\x1b[2m", x = "\x1b[0m";
632
+
633
+ // WHOIS
634
+ if (whois) {
635
+ console.log(`${b}WHOIS${x}`);
636
+ if (whois.available) console.log(` ${g}AVAILABLE${x}`);
637
+ else if (whois.expired) console.log(` ${y}EXPIRED${x}`);
638
+ else console.log(` ${r}TAKEN${x}`);
639
+ if (whois.registrar) console.log(` Registrar: ${whois.registrar}`);
640
+ if (whois.createdDate) console.log(` Created: ${whois.createdDate}`);
641
+ if (whois.expiryDate) console.log(` Expires: ${whois.expiryDate}`);
642
+ const age = calculateDomainAge(whois.createdDate);
643
+ if (age) console.log(` Age: ${age}`);
644
+ console.log();
645
+ }
646
+
647
+ // DNS
648
+ if (dns) {
649
+ console.log(`${b}DNS RECORDS${x}`);
650
+ if (dns.a.length) console.log(` A: ${dns.a.join(", ")}`);
651
+ if (dns.aaaa.length) console.log(` AAAA: ${dns.aaaa.join(", ")}`);
652
+ if (dns.mx.length) console.log(` MX: ${dns.mx.join(", ")}`);
653
+ if (dns.txt.length) console.log(` TXT: ${dns.txt.slice(0, 3).join(", ")}`);
654
+ if (dns.cname.length) console.log(` CNAME: ${dns.cname.join(", ")}`);
655
+ console.log();
656
+ }
657
+
658
+ // ASN / Network
659
+ if (asn && !asn.error) {
660
+ console.log(`${b}NETWORK${x}`);
661
+ if (asn.asn) console.log(` ASN: ${asn.asn}${asn.asnName ? ` (${asn.asnName})` : ""}`);
662
+ if (asn.org) console.log(` Org: ${asn.org}`);
663
+ if (asn.country) console.log(` Location: ${asn.city || ""}${asn.city && asn.country ? ", " : ""}${asn.country}`);
664
+ if (asn.isp) console.log(` ISP: ${asn.isp}`);
665
+ console.log();
666
+ }
667
+
668
+ // Port scan
669
+ if (ports && ports.openPorts.length > 0) {
670
+ console.log(`${b}${r}OPEN PORTS (${ports.openPorts.length})${x}`);
671
+ for (const p of ports.openPorts.slice(0, 20)) {
672
+ console.log(` ${y}${String(p.port).padEnd(6)}${x} ${p.service.padEnd(14)} ${d}${p.banner || ""}${x}`);
673
+ }
674
+ if (ports.ip) console.log(` ${d}IP: ${ports.ip} (${ports.scanTime}ms)${x}`);
675
+ console.log();
676
+ }
677
+
678
+ // Reverse IP
679
+ if (reverseIp && reverseIp.sharedDomains.length > 0) {
680
+ console.log(`${b}SHARED HOSTING (${reverseIp.sharedDomains.length} domains on ${reverseIp.ip})${x}`);
681
+ for (const dd of reverseIp.sharedDomains.slice(0, 10)) console.log(` ${dd}`);
682
+ if (reverseIp.sharedDomains.length > 10) console.log(` ${d}+${reverseIp.sharedDomains.length - 10} more${x}`);
683
+ console.log();
684
+ }
685
+
686
+ // SSL
687
+ if (ssl && !ssl.error) {
688
+ console.log(`${b}SSL CERTIFICATE${x}`);
689
+ console.log(` Valid: ${ssl.valid ? `${g}Yes${x}` : `${r}No${x}`}`);
690
+ if (ssl.issuer) console.log(` Issuer: ${ssl.issuer}`);
691
+ if (ssl.daysUntilExpiry !== null) console.log(` Expires: ${ssl.daysUntilExpiry}d`);
692
+ console.log();
693
+ }
694
+
695
+ // Email Security
696
+ if (emailSec) {
697
+ const ec = emailSec.grade <= "B" ? g : r;
698
+ console.log(`${b}EMAIL SECURITY (${ec}${emailSec.grade}${x}${b})${x}`);
699
+ console.log(` SPF: ${emailSec.spf.found ? `${g}Found${x}` : `${r}Missing${x}`}`);
700
+ console.log(` DKIM: ${emailSec.dkim.found ? `${g}Found (${emailSec.dkim.selector})${x}` : `${r}Missing${x}`}`);
701
+ console.log(` DMARC: ${emailSec.dmarc.found ? `${g}p=${emailSec.dmarc.policy || "?"}${x}` : `${r}Missing${x}`}`);
702
+ for (const issue of emailSec.issues.slice(0, 5)) console.log(` ${y}! ${issue}${x}`);
703
+ console.log();
704
+ }
705
+
706
+ // Security Headers
707
+ if (secHeaders && !secHeaders.error) {
708
+ const hc = secHeaders.grade <= "B" ? g : r;
709
+ console.log(`${b}SECURITY HEADERS (${hc}${secHeaders.grade}${x}${b} — ${secHeaders.score}/100)${x}`);
710
+ for (const h of secHeaders.missing.slice(0, 6)) console.log(` ${r}x ${h}${x}`);
711
+ for (const h of secHeaders.headers.filter((h) => h.status === "good").slice(0, 4)) console.log(` ${g}+ ${h.name}${x}`);
712
+ console.log();
713
+ }
714
+
715
+ // WAF
716
+ if (waf) {
717
+ console.log(`${b}WAF${x}`);
718
+ console.log(` ${waf.detected ? `${c}${waf.waf} (${waf.confidence})${x}` : `${d}None detected${x}`}`);
719
+ console.log();
720
+ }
721
+
722
+ // Zone Transfer
723
+ if (zoneXfer && zoneXfer.vulnerable) {
724
+ console.log(`${b}${r}!! ZONE TRANSFER VULNERABLE${x}`);
725
+ for (const ns of zoneXfer.vulnerableNs) console.log(` ${r}${ns}${x}`);
726
+ console.log();
727
+ }
728
+
729
+ // Cert Transparency
730
+ if (certs && certs.subdomains.length > 0) {
731
+ console.log(`${b}CERT TRANSPARENCY (${certs.subdomains.length} subdomains, ${certs.totalCerts} certs)${x}`);
732
+ for (const s of certs.subdomains.slice(0, 15)) console.log(` ${s}`);
733
+ if (certs.subdomains.length > 15) console.log(` ${d}+${certs.subdomains.length - 15} more${x}`);
734
+ console.log();
735
+ }
736
+
737
+ // Takeover
738
+ if (takeover && takeover.vulnerable) {
739
+ console.log(`${b}${r}!! SUBDOMAIN TAKEOVER${x}`);
740
+ for (const f of takeover.findings.filter((f) => f.status === "vulnerable")) {
741
+ console.log(` ${r}${f.subdomain} -> ${f.service}${x}`);
742
+ }
743
+ console.log();
744
+ }
745
+
746
+ // Path Scanner
747
+ if (paths && paths.findings.length > 0) {
748
+ console.log(`${b}${r}EXPOSED PATHS (${paths.findings.length})${x}`);
749
+ for (const f of paths.findings.slice(0, 15)) {
750
+ const fc = f.severity === "critical" ? r : f.severity === "high" ? y : d;
751
+ console.log(` ${fc}${f.severity === "critical" ? "!!" : f.severity === "high" ? "! " : " "} ${f.path} [${f.status}]${x}`);
752
+ }
753
+ console.log();
754
+ }
755
+
756
+ // CORS
757
+ if (cors && cors.vulnerable) {
758
+ console.log(`${b}${r}!! CORS MISCONFIGURATION${x}`);
759
+ for (const f of cors.findings.filter((f) => f.allowed).slice(0, 5)) {
760
+ console.log(` ${r}${f.detail}${x}`);
761
+ }
762
+ console.log();
763
+ }
764
+
765
+ // Tech Stack
766
+ if (tech && tech.technologies.length > 0) {
767
+ console.log(`${b}TECH STACK${x}`);
768
+ if (tech.cms) console.log(` CMS: ${tech.cms}`);
769
+ if (tech.framework) console.log(` Framework: ${tech.framework}`);
770
+ if (tech.cdn) console.log(` CDN: ${tech.cdn}`);
771
+ console.log();
772
+ }
773
+
774
+ // Blacklist
775
+ if (blacklist) {
776
+ if (blacklist.listed) {
777
+ const names = blacklist.lists.filter((l) => l.listed).map((l) => l.name).join(", ");
778
+ console.log(`${b}${r}BLACKLISTED: ${names}${x}`);
779
+ } else {
780
+ console.log(`${b}REPUTATION${x}: ${g}clean (${blacklist.cleanCount}/${blacklist.lists.length})${x}`);
781
+ }
782
+ console.log();
783
+ }
784
+
785
+ // Backlinks
786
+ if (backlinks) {
787
+ const parts: string[] = [];
788
+ if (backlinks.pageRank !== null) parts.push(`PageRank: ${backlinks.pageRank}`);
789
+ if (backlinks.commonCrawlPages !== null) parts.push(`CC pages: ~${backlinks.commonCrawlPages}`);
790
+ if (parts.length > 0) console.log(`${b}AUTHORITY${x}: ${parts.join(", ")}`);
791
+ console.log();
792
+ }
793
+
794
+ // Social
795
+ if (social) {
796
+ const avail = social.filter((s) => s.available && !s.error);
797
+ if (avail.length > 0) {
798
+ console.log(`${b}SOCIAL AVAILABLE${x}: ${avail.map((s) => s.platform).join(", ")}`);
799
+ console.log();
800
+ }
801
+ }
802
+
803
+ console.log(`${d}Scan complete.${x}\n`);
804
+
805
+ // Clean up database connection
806
+ try {
807
+ const { closeDb: closeReconDb } = await import("./core/db.js");
808
+ closeReconDb();
809
+ } catch {}
810
+ });
811
+
812
+ // ─── Database management subcommand ─────────────────────
813
+
814
+ program
815
+ .command("db")
816
+ .description("Database management")
817
+ .option("--stats", "Show database statistics")
818
+ .option("--clear-cache", "Clear all cached scan results")
819
+ .option("--import-legacy", "Import data from legacy JSON files")
820
+ .option("--history <domain>", "Show scan history for a domain")
821
+ .option("--json", "Output as JSON")
822
+ .action(async (opts: { stats?: boolean; clearCache?: boolean; importLegacy?: boolean; history?: string; json?: boolean }) => {
823
+ const { getDbStats, clearCache, importLegacyPortfolio, importLegacySessions, getScanHistory, closeDb } = await import("./core/db.js");
824
+ const { PORTFOLIO_FILE, SESSION_DIR } = await import("./core/paths.js");
825
+
826
+ if (opts.clearCache) {
827
+ const count = clearCache();
828
+ console.log(`Cleared ${count} cached entries`);
829
+ closeDb();
830
+ return;
831
+ }
832
+
833
+ if (opts.importLegacy) {
834
+ const portfolioCount = importLegacyPortfolio(PORTFOLIO_FILE);
835
+ const sessionCount = importLegacySessions(SESSION_DIR);
836
+ console.log(`Imported: ${portfolioCount} portfolio domains, ${sessionCount} sessions`);
837
+ closeDb();
838
+ return;
839
+ }
840
+
841
+ if (opts.history) {
842
+ const history = getScanHistory(opts.history, 20);
843
+ if (opts.json) { console.log(JSON.stringify(history, null, 2)); closeDb(); return; }
844
+ console.log(`\nScan history for ${opts.history} (${history.length} scans):\n`);
845
+ for (const h of history) {
846
+ console.log(` ${h.scanned_at} ${h.status}${h.score ? ` score: ${h.score}` : ""}`);
847
+ }
848
+ console.log();
849
+ closeDb();
850
+ return;
851
+ }
852
+
853
+ // Default: show stats
854
+ const stats = getDbStats();
855
+ if (opts.json) { console.log(JSON.stringify(stats, null, 2)); closeDb(); return; }
856
+ console.log(`\nDatabase Statistics:`);
857
+ console.log(` Domains tracked: ${stats.totalDomains}`);
858
+ console.log(` Total scans: ${stats.totalScans}`);
859
+ console.log(` Sessions: ${stats.totalSessions}`);
860
+ console.log(` Portfolio: ${stats.portfolioSize}`);
861
+ console.log(` WHOIS snapshots: ${stats.whoisSnapshots}`);
862
+ console.log(` Cache entries: ${stats.cacheEntries}`);
863
+ console.log(` Database size: ${(stats.dbSizeBytes / 1024).toFixed(1)} KB`);
864
+ console.log();
865
+ closeDb();
866
+ });
867
+
868
+ // ─── Serve subcommand ──────────────────────────────────
869
+
870
+ program
871
+ .command("serve")
872
+ .description("Start the marketplace server")
873
+ .option("--port <port>", "Port number", "3000")
874
+ .action(async (opts: { port: string }) => {
875
+ process.env.MARKET_PORT = opts.port;
876
+ await import("../marketplace/index.js");
877
+ });
878
+
879
+ // ─── Market subcommand ─────────────────────────────────
880
+
881
+ const market = program
882
+ .command("market")
883
+ .description("Domain marketplace — buy, sell, and trade domains");
884
+
885
+ market
886
+ .command("signup")
887
+ .description("Create a marketplace account")
888
+ .option("--server <url>", "Marketplace server URL", "http://localhost:3000")
889
+ .option("--email <email>", "Email address")
890
+ .option("--password <password>", "Password (min 8 chars)")
891
+ .option("--name <name>", "Display name")
892
+ .action(async (opts: { server: string; email?: string; password?: string; name?: string }) => {
893
+ const { signUp } = await import("./market-client.js");
894
+ let name = opts.name;
895
+ let email = opts.email;
896
+ let password = opts.password;
897
+
898
+ if (!name || !email || !password) {
899
+ const readline = await import("readline");
900
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
901
+ const ask = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
902
+ if (!name) name = await ask("Name: ");
903
+ if (!email) email = await ask("Email: ");
904
+ if (!password) password = await ask("Password (min 8 chars): ");
905
+ rl.close();
906
+ }
907
+
908
+ const result = await signUp(email!, password!, name!, opts.server);
909
+ if (result.success) {
910
+ console.log(`\n✓ Account created. Signed in as ${email}\n`);
911
+ } else {
912
+ console.error(`\n✗ ${result.error}\n`);
913
+ process.exit(1);
914
+ }
915
+ });
916
+
917
+ market
918
+ .command("login")
919
+ .description("Sign in to the marketplace")
920
+ .option("--server <url>", "Marketplace server URL", "http://localhost:3000")
921
+ .option("--email <email>", "Email address")
922
+ .option("--password <password>", "Password")
923
+ .action(async (opts: { server: string; email?: string; password?: string }) => {
924
+ const { signIn } = await import("./market-client.js");
925
+ let email = opts.email;
926
+ let password = opts.password;
927
+
928
+ if (!email || !password) {
929
+ const readline = await import("readline");
930
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
931
+ const ask = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
932
+ if (!email) email = await ask("Email: ");
933
+ if (!password) password = await ask("Password: ");
934
+ rl.close();
935
+ }
936
+
937
+ const result = await signIn(email!, password!, opts.server);
938
+ if (result.success) {
939
+ console.log(`\n✓ Signed in as ${email}\n`);
940
+ } else {
941
+ console.error(`\n✗ ${result.error}\n`);
942
+ process.exit(1);
943
+ }
944
+ });
945
+
946
+ market
947
+ .command("logout")
948
+ .description("Sign out of the marketplace")
949
+ .action(async () => {
950
+ const { signOut } = await import("./market-client.js");
951
+ signOut();
952
+ console.log("Signed out.");
953
+ });
954
+
955
+ market
956
+ .command("whoami")
957
+ .description("Show current auth status")
958
+ .action(async () => {
959
+ const { getAuthInfo, getServerUrl } = await import("./market-client.js");
960
+ const info = getAuthInfo();
961
+ if (info) {
962
+ console.log(`\nSigned in as: ${info.name} (${info.email})`);
963
+ console.log(`Server: ${getServerUrl()}\n`);
964
+ } else {
965
+ console.log("\nNot signed in. Use: domain-sniper market login\n");
966
+ }
967
+ });
968
+
969
+ market
970
+ .command("browse")
971
+ .description("Browse domain listings")
972
+ .option("-q, --query <search>", "Search domains")
973
+ .option("--category <cat>", "Filter by category")
974
+ .option("--min <price>", "Minimum price")
975
+ .option("--max <price>", "Maximum price")
976
+ .option("--verified", "Only verified listings")
977
+ .option("--sort <field>", "Sort: newest, price_asc, price_desc, popular", "newest")
978
+ .option("-n, --limit <n>", "Results per page", "20")
979
+ .option("--json", "Output as JSON")
980
+ .action(async (opts: any) => {
981
+ const { browseListings } = await import("./market-client.js");
982
+ const result = await browseListings({
983
+ search: opts.query, category: opts.category,
984
+ minPrice: opts.min ? parseFloat(opts.min) : undefined,
985
+ maxPrice: opts.max ? parseFloat(opts.max) : undefined,
986
+ verified: opts.verified, sort: opts.sort,
987
+ limit: parseInt(opts.limit, 10),
988
+ });
989
+ if (!result.ok) { console.error("Failed to fetch listings:", result.data?.error); process.exit(1); }
990
+ if (opts.json) { console.log(JSON.stringify(result.data, null, 2)); return; }
991
+
992
+ const { listings, total } = result.data;
993
+ console.log(`\n◆ Domain Marketplace (${total} listings)\n`);
994
+ if (listings.length === 0) { console.log(" No listings found.\n"); return; }
995
+ console.log(` ${"DOMAIN".padEnd(30)} ${"PRICE".padStart(10)} ${"STATUS".padEnd(10)} VERIFIED`);
996
+ console.log(` ${"─".repeat(30)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(8)}`);
997
+ for (const l of listings) {
998
+ console.log(` ${l.domain.padEnd(30)} ${"$" + l.asking_price.toFixed(2).padStart(9)} ${l.status.padEnd(10)} ${l.verified ? "✓" : " "}`);
999
+ }
1000
+ console.log();
1001
+ });
1002
+
1003
+ market
1004
+ .command("list <domain>")
1005
+ .description("List a domain for sale")
1006
+ .requiredOption("-p, --price <amount>", "Asking price")
1007
+ .option("-t, --title <title>", "Listing title")
1008
+ .option("-d, --description <desc>", "Description")
1009
+ .option("--min-offer <amount>", "Minimum acceptable offer")
1010
+ .option("--buy-now", "Enable buy-now at asking price")
1011
+ .option("--category <cat>", "Category")
1012
+ .action(async (domain: string, opts: any) => {
1013
+ const { isLoggedIn, createListingApi } = await import("./market-client.js");
1014
+ if (!isLoggedIn()) { console.error("Not signed in. Use: domain-sniper market login"); process.exit(1); }
1015
+
1016
+ const result = await createListingApi(domain, parseFloat(opts.price), {
1017
+ title: opts.title, description: opts.description,
1018
+ minOffer: opts.minOffer ? parseFloat(opts.minOffer) : undefined,
1019
+ buyNow: opts.buyNow, category: opts.category,
1020
+ });
1021
+ if (!result.ok) { console.error("Failed:", result.data?.error); process.exit(1); }
1022
+
1023
+ const { listing, verification } = result.data;
1024
+ console.log(`\n✓ Listing created (#${listing.id}): ${domain} at $${listing.asking_price}`);
1025
+ console.log(`\nVerify ownership to activate listing:\n`);
1026
+ console.log(`Option 1 — DNS TXT Record:`);
1027
+ console.log(` ${verification.instructions.dns}\n`);
1028
+ console.log(`Option 2 — HTTP File:`);
1029
+ console.log(` ${verification.instructions.http}\n`);
1030
+ console.log(`Option 3 — Meta Tag:`);
1031
+ console.log(` ${verification.instructions.meta}\n`);
1032
+ });
1033
+
1034
+ market
1035
+ .command("verify <domain>")
1036
+ .description("Verify domain ownership for a listing")
1037
+ .action(async (domain: string) => {
1038
+ const { isLoggedIn, getMyListings, verifyListingApi } = await import("./market-client.js");
1039
+ if (!isLoggedIn()) { console.error("Not signed in."); process.exit(1); }
1040
+
1041
+ const myListings = await getMyListings();
1042
+ if (!myListings.ok) { console.error("Failed to fetch listings"); process.exit(1); }
1043
+
1044
+ const listing = myListings.data.find((l: any) => l.domain === domain && !l.verified);
1045
+ if (!listing) { console.error(`No unverified listing found for ${domain}`); process.exit(1); }
1046
+
1047
+ console.log(`\nVerifying ${domain}...`);
1048
+ const result = await verifyListingApi(listing.id);
1049
+ if (result.ok && result.data.verified) {
1050
+ console.log(`✓ Verified via ${result.data.method}! Listing is now active.\n`);
1051
+ } else {
1052
+ console.error(`✗ ${result.data.error || "Verification failed"}`);
1053
+ console.error(` Make sure your DNS TXT record, HTTP file, or meta tag is set up.\n`);
1054
+ process.exit(1);
1055
+ }
1056
+ });
1057
+
1058
+ market
1059
+ .command("offer")
1060
+ .description("Make an offer on a listing")
1061
+ .requiredOption("-l, --listing <id>", "Listing ID")
1062
+ .requiredOption("-a, --amount <price>", "Offer amount")
1063
+ .option("-m, --message <msg>", "Message to seller")
1064
+ .action(async (opts: any) => {
1065
+ const { isLoggedIn, makeOffer } = await import("./market-client.js");
1066
+ if (!isLoggedIn()) { console.error("Not signed in."); process.exit(1); }
1067
+
1068
+ const result = await makeOffer(parseInt(opts.listing, 10), parseFloat(opts.amount), opts.message || "");
1069
+ if (result.ok) {
1070
+ console.log(`\n✓ Offer of $${opts.amount} submitted on listing #${opts.listing}\n`);
1071
+ } else {
1072
+ console.error(`✗ ${result.data?.error || "Failed"}\n`);
1073
+ process.exit(1);
1074
+ }
1075
+ });
1076
+
1077
+ market
1078
+ .command("my-listings")
1079
+ .description("Show your listings")
1080
+ .option("--json", "Output as JSON")
1081
+ .action(async (opts: any) => {
1082
+ const { isLoggedIn, getMyListings } = await import("./market-client.js");
1083
+ if (!isLoggedIn()) { console.error("Not signed in."); process.exit(1); }
1084
+
1085
+ const result = await getMyListings();
1086
+ if (!result.ok) { console.error("Failed"); process.exit(1); }
1087
+ if (opts.json) { console.log(JSON.stringify(result.data, null, 2)); return; }
1088
+
1089
+ const listings = result.data;
1090
+ console.log(`\nYour Listings (${listings.length}):\n`);
1091
+ for (const l of listings) {
1092
+ console.log(` #${l.id} ${l.domain.padEnd(25)} $${l.asking_price} ${l.status} ${l.verified ? "✓ verified" : "unverified"}`);
1093
+ }
1094
+ console.log();
1095
+ });
1096
+
1097
+ market
1098
+ .command("my-offers")
1099
+ .description("Show offers you've made or received")
1100
+ .option("--role <role>", "buyer or seller", "buyer")
1101
+ .option("--json", "Output as JSON")
1102
+ .action(async (opts: any) => {
1103
+ const { isLoggedIn, getMyOffers } = await import("./market-client.js");
1104
+ if (!isLoggedIn()) { console.error("Not signed in."); process.exit(1); }
1105
+
1106
+ const result = await getMyOffers(opts.role);
1107
+ if (!result.ok) { console.error("Failed"); process.exit(1); }
1108
+ if (opts.json) { console.log(JSON.stringify(result.data, null, 2)); return; }
1109
+
1110
+ const offers = result.data;
1111
+ console.log(`\nYour Offers as ${opts.role} (${offers.length}):\n`);
1112
+ for (const o of offers) {
1113
+ console.log(` #${o.id} ${o.domain.padEnd(25)} $${o.amount} ${o.status}`);
1114
+ }
1115
+ console.log();
1116
+ });
1117
+
1118
+ market
1119
+ .command("stats")
1120
+ .description("Show marketplace statistics")
1121
+ .option("--json", "Output as JSON")
1122
+ .action(async (opts: any) => {
1123
+ const { getMarketStatsApi } = await import("./market-client.js");
1124
+ const result = await getMarketStatsApi();
1125
+ if (!result.ok) { console.error("Cannot reach marketplace server"); process.exit(1); }
1126
+ if (opts.json) { console.log(JSON.stringify(result.data, null, 2)); return; }
1127
+
1128
+ const s = result.data;
1129
+ console.log(`\n◆ Marketplace Stats`);
1130
+ console.log(` Total listings: ${s.totalListings}`);
1131
+ console.log(` Active: ${s.activeListings}`);
1132
+ console.log(` Total offers: ${s.totalOffers}`);
1133
+ console.log(` Users: ${s.totalUsers}\n`);
1134
+ });
1135
+
1136
+ // ─── Proxy commands ──────────────────────────────────────
1137
+
1138
+ const proxy = program
1139
+ .command("proxy")
1140
+ .description("HTTP intercepting proxy — capture and analyze traffic");
1141
+
1142
+ proxy
1143
+ .command("start")
1144
+ .description("Start the intercepting proxy")
1145
+ .option("-p, --port <port>", "Proxy port", "8080")
1146
+ .option("--https", "Enable HTTPS interception (requires CA cert)", false)
1147
+ .option("--filter <hosts>", "Only intercept these hosts (comma-separated)")
1148
+ .action(async (opts: { port: string; https: boolean; filter?: string }) => {
1149
+ const { startProxy } = await import("./proxy/server.js");
1150
+ const filterHosts = opts.filter ? opts.filter.split(",").map((h) => h.trim()) : undefined;
1151
+
1152
+ startProxy({
1153
+ port: parseInt(opts.port, 10),
1154
+ httpsInterception: opts.https,
1155
+ filterHosts,
1156
+ onRequest: (entry) => {
1157
+ const statusColor = entry.statusCode && entry.statusCode < 400 ? "\x1b[32m" : "\x1b[31m";
1158
+ console.log(` ${entry.method.padEnd(6)} ${statusColor}${entry.statusCode || "ERR"}\x1b[0m ${entry.host}${entry.url.replace(/^https?:\/\/[^/]+/, "")} (${entry.durationMs}ms, ${entry.size}b)`);
1159
+ },
1160
+ });
1161
+
1162
+ // Keep running
1163
+ console.log("Press Ctrl+C to stop\n");
1164
+ });
1165
+
1166
+ proxy
1167
+ .command("history")
1168
+ .description("Browse intercepted requests")
1169
+ .option("--host <host>", "Filter by host")
1170
+ .option("--method <method>", "Filter by method (GET, POST, etc.)")
1171
+ .option("--search <query>", "Search in URL/body")
1172
+ .option("--flagged", "Show only flagged requests")
1173
+ .option("-n, --limit <n>", "Number of results", "30")
1174
+ .option("--json", "Output as JSON")
1175
+ .action(async (opts: { host?: string; method?: string; search?: string; flagged?: boolean; limit: string; json?: boolean }) => {
1176
+ const { getRequests } = await import("./proxy/db.js");
1177
+ const result = getRequests({
1178
+ host: opts.host, method: opts.method, search: opts.search,
1179
+ flagged: opts.flagged, limit: parseInt(opts.limit, 10),
1180
+ });
1181
+ if (opts.json) { console.log(JSON.stringify(result, null, 2)); return; }
1182
+
1183
+ console.log(`\nIntercepted Requests (${result.total} total):\n`);
1184
+ console.log(` ${"ID".padEnd(6)} ${"METHOD".padEnd(8)} ${"STATUS".padEnd(8)} ${"HOST".padEnd(30)} ${"PATH".padEnd(30)} ${"MS".padEnd(6)}`);
1185
+ console.log(` ${"─".repeat(6)} ${"─".repeat(8)} ${"─".repeat(8)} ${"─".repeat(30)} ${"─".repeat(30)} ${"─".repeat(6)}`);
1186
+ for (const r of result.requests) {
1187
+ const statusColor = r.status_code && r.status_code < 400 ? "\x1b[32m" : r.status_code && r.status_code < 500 ? "\x1b[33m" : "\x1b[31m";
1188
+ console.log(` ${String(r.id).padEnd(6)} ${r.method.padEnd(8)} ${statusColor}${String(r.status_code || "ERR").padEnd(8)}\x1b[0m ${r.host.slice(0, 30).padEnd(30)} ${r.path.slice(0, 30).padEnd(30)} ${String(r.duration_ms).padEnd(6)}`);
1189
+ }
1190
+ console.log();
1191
+ });
1192
+
1193
+ proxy
1194
+ .command("inspect <id>")
1195
+ .description("View full details of an intercepted request")
1196
+ .option("--json", "Output as JSON")
1197
+ .action(async (id: string, opts: { json?: boolean }) => {
1198
+ const { getRequest } = await import("./proxy/db.js");
1199
+ const req = getRequest(parseInt(id, 10));
1200
+ if (!req) { console.error("Request not found"); process.exit(1); }
1201
+
1202
+ if (opts.json) { console.log(JSON.stringify(req, null, 2)); return; }
1203
+
1204
+ console.log(`\n── Request #${req.id} ──────────────────────────────`);
1205
+ console.log(`${req.method} ${req.url}`);
1206
+ console.log(`Host: ${req.host}`);
1207
+ console.log(`Time: ${req.intercepted_at} (${req.duration_ms}ms)\n`);
1208
+
1209
+ console.log("── Request Headers ─────────────────────────────");
1210
+ try {
1211
+ const headers = JSON.parse(req.request_headers) as Record<string, string>;
1212
+ for (const [k, v] of Object.entries(headers)) console.log(` ${k}: ${v}`);
1213
+ } catch {}
1214
+
1215
+ if (req.request_body) {
1216
+ console.log("\n── Request Body ────────────────────────────────");
1217
+ console.log(req.request_body.slice(0, 2000));
1218
+ }
1219
+
1220
+ console.log(`\n── Response (${req.status_code}) ───────────────────────────`);
1221
+ try {
1222
+ const headers = JSON.parse(req.response_headers) as Record<string, string>;
1223
+ for (const [k, v] of Object.entries(headers)) console.log(` ${k}: ${v}`);
1224
+ } catch {}
1225
+
1226
+ if (req.response_body) {
1227
+ console.log("\n── Response Body ───────────────────────────────");
1228
+ console.log(req.response_body.slice(0, 5000));
1229
+ }
1230
+ console.log();
1231
+ });
1232
+
1233
+ proxy
1234
+ .command("replay <id>")
1235
+ .description("Replay an intercepted request")
1236
+ .action(async (id: string) => {
1237
+ const { getRequest } = await import("./proxy/db.js");
1238
+ const req = getRequest(parseInt(id, 10));
1239
+ if (!req) { console.error("Request not found"); process.exit(1); }
1240
+
1241
+ console.log(`\nReplaying: ${req.method} ${req.url}`);
1242
+ const startTime = Date.now();
1243
+ try {
1244
+ const headers = JSON.parse(req.request_headers) as Record<string, string>;
1245
+ const resp = await fetch(req.url, {
1246
+ method: req.method,
1247
+ headers,
1248
+ body: req.method !== "GET" && req.method !== "HEAD" ? req.request_body : undefined,
1249
+ });
1250
+ const body = await resp.text();
1251
+ const duration = Date.now() - startTime;
1252
+ console.log(`Status: ${resp.status} (${duration}ms, ${body.length} bytes)`);
1253
+ console.log(body.slice(0, 2000));
1254
+ } catch (err: unknown) {
1255
+ console.error(`Failed: ${err instanceof Error ? err.message : "unknown"}`);
1256
+ }
1257
+ console.log();
1258
+ });
1259
+
1260
+ proxy
1261
+ .command("clear")
1262
+ .description("Clear intercepted request history")
1263
+ .option("--host <host>", "Only clear requests for this host")
1264
+ .action(async (opts: { host?: string }) => {
1265
+ const { clearRequests } = await import("./proxy/db.js");
1266
+ const count = clearRequests(opts.host);
1267
+ console.log(`Cleared ${count} requests${opts.host ? ` for ${opts.host}` : ""}`);
1268
+ });
1269
+
1270
+ proxy
1271
+ .command("stats")
1272
+ .description("Show proxy statistics")
1273
+ .option("--json", "Output as JSON")
1274
+ .action(async (opts: { json?: boolean }) => {
1275
+ const { getProxyStats, getTopHosts } = await import("./proxy/db.js");
1276
+ const stats = getProxyStats();
1277
+ const topHosts = getTopHosts(5);
1278
+ if (opts.json) { console.log(JSON.stringify({ stats, topHosts }, null, 2)); return; }
1279
+
1280
+ console.log(`\nProxy Statistics:`);
1281
+ console.log(` Total requests: ${stats.totalRequests}`);
1282
+ console.log(` Unique hosts: ${stats.uniqueHosts}`);
1283
+ console.log(` Flagged: ${stats.flagged}`);
1284
+ console.log(` Avg latency: ${stats.avgDuration}ms`);
1285
+ if (topHosts.length > 0) {
1286
+ console.log(`\n Top Hosts:`);
1287
+ for (const h of topHosts) console.log(` ${h.host.padEnd(30)} ${h.count} requests`);
1288
+ }
1289
+ console.log();
1290
+ });
1291
+
1292
+ proxy
1293
+ .command("ca")
1294
+ .description("Manage CA certificate for HTTPS interception")
1295
+ .option("--generate", "Generate CA certificate")
1296
+ .option("--install", "Show installation instructions")
1297
+ .option("--path", "Show CA certificate path")
1298
+ .action(async (opts: { generate?: boolean; install?: boolean; path?: boolean }) => {
1299
+ const { generateCA, getCACertPath, hasCA, getInstallInstructions } = await import("./proxy/ca.js");
1300
+ if (opts.generate) {
1301
+ generateCA();
1302
+ console.log("CA certificate generated.");
1303
+ console.log(`Path: ${getCACertPath()}`);
1304
+ return;
1305
+ }
1306
+ if (opts.path) {
1307
+ console.log(getCACertPath());
1308
+ return;
1309
+ }
1310
+ // Default: show install instructions
1311
+ if (!hasCA()) {
1312
+ console.log("No CA certificate found. Generate one first:");
1313
+ console.log(" domain-sniper proxy ca --generate\n");
1314
+ return;
1315
+ }
1316
+ console.log(getInstallInstructions());
1317
+ });
1318
+
1319
+ // ─── Snipe subcommand ──────────────────────────────────
1320
+
1321
+ const snipeCmd = program
1322
+ .command("snipe")
1323
+ .description("Snipe domains — automatically watch, detect expiry, and register");
1324
+
1325
+ snipeCmd
1326
+ .command("add <domain>")
1327
+ .description("Add a domain to snipe list")
1328
+ .option("--max-price <price>", "Maximum price to pay")
1329
+ .action(async (domain: string, opts: { maxPrice?: string }) => {
1330
+ const { snipeDomain } = await import("./core/features/snipe.js");
1331
+ const { whoisLookup } = await import("./core/whois.js");
1332
+
1333
+ console.log(`\nChecking ${domain}...`);
1334
+ const whois = await whoisLookup(domain);
1335
+
1336
+ snipeDomain(domain, {
1337
+ expiryDate: whois.expiryDate || undefined,
1338
+ maxPrice: opts.maxPrice ? parseFloat(opts.maxPrice) : undefined,
1339
+ });
1340
+
1341
+ if (whois.available) {
1342
+ console.log(` ${domain} is ALREADY AVAILABLE — consider registering now!`);
1343
+ console.log(` Use: domain-sniper snipe run\n`);
1344
+ } else if (whois.expired) {
1345
+ console.log(` ${domain} is EXPIRED — snipe engine will monitor and register when it drops`);
1346
+ if (whois.expiryDate) console.log(` Expiry: ${whois.expiryDate}`);
1347
+ } else {
1348
+ console.log(` ${domain} is currently registered`);
1349
+ if (whois.expiryDate) {
1350
+ const daysLeft = Math.floor((new Date(whois.expiryDate).getTime() - Date.now()) / 86400000);
1351
+ console.log(` Expires: ${whois.expiryDate} (${daysLeft} days)`);
1352
+ }
1353
+ console.log(` Snipe engine will watch for expiry and auto-register when it drops`);
1354
+ }
1355
+ console.log(`\nStart the snipe engine: domain-sniper snipe run\n`);
1356
+ });
1357
+
1358
+ snipeCmd
1359
+ .command("remove <domain>")
1360
+ .description("Remove a domain from snipe list")
1361
+ .action(async (domain: string) => {
1362
+ const { cancelSnipe } = await import("./core/features/snipe.js");
1363
+ cancelSnipe(domain);
1364
+ console.log(`Removed ${domain} from snipe list`);
1365
+ });
1366
+
1367
+ snipeCmd
1368
+ .command("list")
1369
+ .description("Show all snipe targets")
1370
+ .option("--json", "Output as JSON")
1371
+ .action(async (opts: { json?: boolean }) => {
1372
+ const { getSnipeTargets } = await import("./core/features/snipe.js");
1373
+ const { getSnipeStats } = await import("./core/db.js");
1374
+ const targets = getSnipeTargets();
1375
+ const stats = getSnipeStats();
1376
+
1377
+ if (opts.json) { console.log(JSON.stringify({ targets, stats }, null, 2)); return; }
1378
+
1379
+ console.log(`\nSnipe Targets (${stats.total} total — ${stats.watching} watching, ${stats.expiring} expiring, ${stats.dropping} dropping, ${stats.registered} registered):\n`);
1380
+ if (targets.length === 0) { console.log(" No active snipe targets. Add one: domain-sniper snipe add example.com\n"); return; }
1381
+
1382
+ console.log(` ${"DOMAIN".padEnd(30)} ${"STATUS".padEnd(12)} ${"PHASE".padEnd(12)} ${"CHECKS".padEnd(8)} LAST CHECK`);
1383
+ console.log(` ${"─".repeat(30)} ${"─".repeat(12)} ${"─".repeat(12)} ${"─".repeat(8)} ${"─".repeat(20)}`);
1384
+ for (const t of targets) {
1385
+ const statusColor = t.status === "dropping" ? "\x1b[31m" : t.status === "expiring" ? "\x1b[33m" : "\x1b[90m";
1386
+ console.log(` ${t.domain.padEnd(30)} ${statusColor}${t.status.padEnd(12)}\x1b[0m ${t.phase.padEnd(12)} ${String(t.checkCount).padEnd(8)} ${t.lastChecked || "never"}`);
1387
+ }
1388
+ console.log();
1389
+ });
1390
+
1391
+ snipeCmd
1392
+ .command("run")
1393
+ .description("Start the snipe engine (runs in foreground)")
1394
+ .action(async () => {
1395
+ const { createSnipeEngine, getSnipeTargets } = await import("./core/features/snipe.js");
1396
+ const targets = getSnipeTargets();
1397
+
1398
+ if (targets.length === 0) {
1399
+ console.error("No snipe targets. Add one first: domain-sniper snipe add example.com");
1400
+ process.exit(1);
1401
+ }
1402
+
1403
+ console.log(`\n◆ Snipe Engine Starting — ${targets.length} target(s)\n`);
1404
+ for (const t of targets) {
1405
+ console.log(` ${t.domain.padEnd(30)} ${t.status.padEnd(12)} ${t.phase}`);
1406
+ }
1407
+ console.log(`\nPhases: hourly (registered) → frequent/5min (expired) → aggressive/30s (pending delete)`);
1408
+ console.log("Press Ctrl+C to stop\n");
1409
+
1410
+ const engine = createSnipeEngine({
1411
+ onStatusChange: (domain, status, phase, message) => {
1412
+ const ts = new Date().toISOString().slice(11, 19);
1413
+ const color = status === "registered" ? "\x1b[32m" : status === "dropping" ? "\x1b[31m" : status === "expiring" ? "\x1b[33m" : "\x1b[90m";
1414
+ console.log(` ${ts} ${color}[${status}/${phase}]\x1b[0m ${message}`);
1415
+ },
1416
+ onRegistered: (domain) => {
1417
+ console.log(`\n ★ SUCCESS — ${domain} has been REGISTERED!\n`);
1418
+ },
1419
+ onFailed: (domain, error) => {
1420
+ console.log(`\n ✗ FAILED — ${domain}: ${error}\n`);
1421
+ },
1422
+ });
1423
+
1424
+ await engine.start();
1425
+
1426
+ // Keep running
1427
+ process.on("SIGINT", () => {
1428
+ console.log("\nStopping snipe engine...");
1429
+ engine.stop();
1430
+ process.exit(0);
1431
+ });
1432
+ });
1433
+
1434
+ snipeCmd
1435
+ .command("stats")
1436
+ .description("Show snipe statistics")
1437
+ .option("--json", "Output as JSON")
1438
+ .action(async (opts: { json?: boolean }) => {
1439
+ const { getSnipeStats, getAllSnipes } = await import("./core/db.js");
1440
+ const stats = getSnipeStats();
1441
+ const all = getAllSnipes();
1442
+ if (opts.json) { console.log(JSON.stringify({ stats, snipes: all }, null, 2)); return; }
1443
+
1444
+ console.log(`\nSnipe Stats:`);
1445
+ console.log(` Watching: ${stats.watching}`);
1446
+ console.log(` Expiring: ${stats.expiring}`);
1447
+ console.log(` Dropping: ${stats.dropping}`);
1448
+ console.log(` Registered: ${stats.registered}`);
1449
+ console.log(` Failed: ${stats.failed}`);
1450
+ console.log(` Total: ${stats.total}\n`);
1451
+ });
1452
+
1453
+ // ─── Check-update subcommand ────────────────────────────
1454
+
1455
+ program
1456
+ .command("check-update")
1457
+ .description("Check for newer versions")
1458
+ .action(async () => {
1459
+ const { checkForUpdates, formatUpdateMessage } = await import("./core/features/version-check.js");
1460
+ console.log("Checking for updates...");
1461
+ const result = await checkForUpdates();
1462
+ if (result.updateAvailable && result.latest) {
1463
+ console.log(formatUpdateMessage(result.current, result.latest));
1464
+ } else {
1465
+ console.log(`You're on the latest version (${result.current})`);
1466
+ }
1467
+ });
1468
+
1469
+ program.parse();
1470
+
1471
+ // ─── Types ───────────────────────────────────────────────
1472
+
1473
+ interface CliOptions {
1474
+ file?: string;
1475
+ autoRegister: boolean;
1476
+ headless: boolean;
1477
+ json: boolean;
1478
+ concurrency: string;
1479
+ recon: boolean;
1480
+ }
1481
+
1482
+ interface JsonOutputResult {
1483
+ domain: string;
1484
+ status: string;
1485
+ available: boolean;
1486
+ expired: boolean;
1487
+ confidence: string;
1488
+ registrar: string | null;
1489
+ createdDate: string | null;
1490
+ expiryDate: string | null;
1491
+ age: string | null;
1492
+ dns: { a: string[]; aaaa: string[]; mx: string[]; txt: string[]; cname: string[] } | null;
1493
+ http: { status: number | null; server: string | null; parked: boolean; reachable: boolean; redirectUrl: string | null } | null;
1494
+ wayback: { hasHistory: boolean; snapshots: number; firstArchived: string | null; lastArchived: string | null } | null;
1495
+ price: number | null;
1496
+ social: Awaited<ReturnType<typeof checkSocialMedia>> | null;
1497
+ techStack: Awaited<ReturnType<typeof detectTechStack>> | null;
1498
+ blacklist: Awaited<ReturnType<typeof checkBlacklists>> | null;
1499
+ backlinks: Awaited<ReturnType<typeof estimateBacklinks>> | null;
1500
+ portScan?: PortScanResult | null;
1501
+ reverseIp?: ReverseIpResult | null;
1502
+ asn?: AsnResult | null;
1503
+ emailSecurity?: EmailSecurityResult | null;
1504
+ zoneTransfer?: ZoneTransferResult | null;
1505
+ certTransparency?: CertTransparencyResult | null;
1506
+ takeover?: TakeoverResult | null;
1507
+ securityHeaders?: SecurityHeadersResult | null;
1508
+ waf?: WafResult | null;
1509
+ pathScan?: PathScanResult | null;
1510
+ cors?: CorsResult | null;
1511
+ }
1512
+
1513
+ interface JsonOutput {
1514
+ timestamp: string;
1515
+ count: number;
1516
+ results: JsonOutputResult[];
1517
+ }
1518
+
1519
+ // ─── Headless / non-interactive mode ──────────────────────
1520
+
1521
+ async function checkDependencies(): Promise<void> {
1522
+ const { execFile } = await import("child_process");
1523
+ const { promisify } = await import("util");
1524
+ const execFileAsync = promisify(execFile);
1525
+
1526
+ const missing: string[] = [];
1527
+ try { await execFileAsync("which", ["whois"]); } catch { missing.push("whois"); }
1528
+ try { await execFileAsync("which", ["dig"]); } catch { missing.push("dig"); }
1529
+
1530
+ if (missing.length > 0) {
1531
+ console.error(`Missing required tools: ${missing.join(", ")}`);
1532
+ console.error("Install them:");
1533
+ console.error(" macOS: brew install whois bind (for dig)");
1534
+ console.error(" Ubuntu/Debian: apt install whois dnsutils");
1535
+ console.error(" Alpine: apk add whois bind-tools");
1536
+ process.exit(1);
1537
+ }
1538
+ }
1539
+
1540
+ async function runHeadless(domains: string[], options: CliOptions) {
1541
+ await checkDependencies();
1542
+
1543
+ const { whoisLookup, verifyAvailability, parseDomainList } = await import("./core/whois.js");
1544
+ const { loadConfigFromEnv, checkAvailabilityViaRegistrar, registerDomain } = await import("./core/registrar.js");
1545
+ const { readFileSync, existsSync } = await import("fs");
1546
+
1547
+ let domainList = [...domains];
1548
+
1549
+ // Load from file if specified
1550
+ if (options.file) {
1551
+ const filePath = safePath(options.file, [process.cwd()]);
1552
+ if (!existsSync(filePath)) {
1553
+ console.error(`File not found: ${filePath}`);
1554
+ process.exit(1);
1555
+ }
1556
+ const content = readFileSync(filePath, "utf-8");
1557
+ domainList.push(...parseDomainList(content));
1558
+ }
1559
+
1560
+ const rawCount = domainList.length;
1561
+ domainList = sanitizeDomainList(domainList);
1562
+
1563
+ if (domainList.length === 0) {
1564
+ if (rawCount > 0) {
1565
+ console.error(`No valid domains found (${rawCount} input(s) rejected). Domains must be like: example.com`);
1566
+ } else {
1567
+ console.error("No domains specified. Use: domain-sniper example.com or -f domains.txt");
1568
+ }
1569
+ process.exit(1);
1570
+ }
1571
+
1572
+ const config = loadConfigFromEnv();
1573
+ const isJsonMode = options.json;
1574
+
1575
+ // JSON mode: collect results
1576
+ const jsonResults: JsonOutputResult[] = [];
1577
+
1578
+ if (!isJsonMode) {
1579
+ console.log(`\n🔍 Domain Sniper - Checking ${domainList.length} domain(s)...\n`);
1580
+ }
1581
+
1582
+ for (let i = 0; i < domainList.length; i++) {
1583
+ const domain = domainList[i]!;
1584
+ if (!isJsonMode) {
1585
+ process.stdout.write(` Checking ${domain}...`);
1586
+ }
1587
+
1588
+ const whois = await whoisLookup(domain);
1589
+ const verification = await verifyAvailability(domain);
1590
+
1591
+ let status = "TAKEN";
1592
+ let available = false;
1593
+
1594
+ if (whois.available && verification.confidence === "high") {
1595
+ status = "AVAILABLE";
1596
+ available = true;
1597
+ } else if (whois.expired) {
1598
+ status = "EXPIRED";
1599
+ } else if (whois.available) {
1600
+ status = `AVAILABLE (${verification.confidence} confidence)`;
1601
+ available = true;
1602
+ }
1603
+
1604
+ // DNS details
1605
+ let dnsResult: { a: string[]; aaaa: string[]; mx: string[]; txt: string[]; cname: string[] } | null = null;
1606
+ try {
1607
+ dnsResult = await lookupDns(domain);
1608
+ } catch {}
1609
+
1610
+ // HTTP probe
1611
+ let httpResult: { status: number | null; server: string | null; parked: boolean; reachable: boolean; redirectUrl: string | null } | null = null;
1612
+ try {
1613
+ const probe = await httpProbe(domain);
1614
+ httpResult = {
1615
+ status: probe.status,
1616
+ server: probe.server,
1617
+ parked: probe.parked,
1618
+ reachable: probe.reachable,
1619
+ redirectUrl: probe.redirectUrl,
1620
+ };
1621
+ } catch {}
1622
+
1623
+ // Wayback Machine
1624
+ let waybackResult: { hasHistory: boolean; snapshots: number; firstArchived: string | null; lastArchived: string | null } | null = null;
1625
+ try {
1626
+ waybackResult = await checkWayback(domain);
1627
+ } catch {}
1628
+
1629
+ // Domain age
1630
+ const age = calculateDomainAge(whois.createdDate);
1631
+
1632
+ // Registrar price check
1633
+ let price: number | null = null;
1634
+ if (config?.apiKey) {
1635
+ const regCheck = await checkAvailabilityViaRegistrar(domain, config);
1636
+ if (regCheck.price !== undefined) {
1637
+ price = regCheck.price;
1638
+ }
1639
+ }
1640
+
1641
+ // New feature data collection
1642
+ let socialResult: Awaited<ReturnType<typeof checkSocialMedia>> | null = null;
1643
+ try { socialResult = await checkSocialMedia(domain); } catch {}
1644
+
1645
+ let techResult: Awaited<ReturnType<typeof detectTechStack>> | null = null;
1646
+ try { techResult = await detectTechStack(domain); } catch {}
1647
+
1648
+ let blacklistResult: Awaited<ReturnType<typeof checkBlacklists>> | null = null;
1649
+ try { blacklistResult = await checkBlacklists(domain); } catch {}
1650
+
1651
+ let backlinkResult: Awaited<ReturnType<typeof estimateBacklinks>> | null = null;
1652
+ try { backlinkResult = await estimateBacklinks(domain); } catch {}
1653
+
1654
+ // Recon features — only when --recon flag is set
1655
+ let reconData: {
1656
+ portScan: PortScanResult | null;
1657
+ reverseIp: ReverseIpResult | null;
1658
+ asn: AsnResult | null;
1659
+ emailSecurity: EmailSecurityResult | null;
1660
+ zoneTransfer: ZoneTransferResult | null;
1661
+ certTransparency: CertTransparencyResult | null;
1662
+ takeover: TakeoverResult | null;
1663
+ securityHeaders: SecurityHeadersResult | null;
1664
+ waf: WafResult | null;
1665
+ pathScan: PathScanResult | null;
1666
+ cors: CorsResult | null;
1667
+ } | null = null;
1668
+
1669
+ if (options.recon) {
1670
+ const [portsR, reverseIpR, asnR, emailSecR, zoneXferR, certsR, takeoverR, secHeadersR, wafR, pathsR, corsR] = await Promise.all([
1671
+ scanPorts(domain).catch(() => null),
1672
+ reverseIpLookup(domain).catch(() => null),
1673
+ lookupAsn(domain).catch(() => null),
1674
+ checkEmailSecurity(domain).catch(() => null),
1675
+ checkZoneTransfer(domain).catch(() => null),
1676
+ queryCertTransparency(domain).catch(() => null),
1677
+ detectTakeover(domain).catch(() => null),
1678
+ auditSecurityHeaders(domain).catch(() => null),
1679
+ detectWaf(domain).catch(() => null),
1680
+ scanPaths(domain).catch(() => null),
1681
+ checkCors(domain).catch(() => null),
1682
+ ]);
1683
+ reconData = {
1684
+ portScan: portsR,
1685
+ reverseIp: reverseIpR,
1686
+ asn: asnR,
1687
+ emailSecurity: emailSecR,
1688
+ zoneTransfer: zoneXferR,
1689
+ certTransparency: certsR,
1690
+ takeover: takeoverR,
1691
+ securityHeaders: secHeadersR,
1692
+ waf: wafR,
1693
+ pathScan: pathsR,
1694
+ cors: corsR,
1695
+ };
1696
+ }
1697
+
1698
+ if (isJsonMode) {
1699
+ jsonResults.push({
1700
+ domain,
1701
+ status,
1702
+ available,
1703
+ expired: whois.expired,
1704
+ confidence: verification.confidence,
1705
+ registrar: whois.registrar,
1706
+ createdDate: whois.createdDate,
1707
+ expiryDate: whois.expiryDate,
1708
+ age,
1709
+ dns: dnsResult,
1710
+ http: httpResult,
1711
+ wayback: waybackResult,
1712
+ price,
1713
+ social: socialResult,
1714
+ techStack: techResult,
1715
+ blacklist: blacklistResult,
1716
+ backlinks: backlinkResult,
1717
+ ...(reconData || {}),
1718
+ });
1719
+ } else {
1720
+ // Normal text output
1721
+ let color = "\x1b[31m"; // red
1722
+ if (available) {
1723
+ color = "\x1b[32m"; // green
1724
+ } else if (whois.expired) {
1725
+ color = "\x1b[33m"; // yellow
1726
+ }
1727
+
1728
+ console.log(`\r ${color}${status}\x1b[0m ${domain}`);
1729
+
1730
+ // Verification details
1731
+ for (const check of verification.checks) {
1732
+ console.log(` ${check}`);
1733
+ }
1734
+
1735
+ // DNS details
1736
+ if (dnsResult) {
1737
+ if (dnsResult.a.length) console.log(` DNS A: ${dnsResult.a.join(", ")}`);
1738
+ if (dnsResult.mx.length) console.log(` DNS MX: ${dnsResult.mx.join(", ")}`);
1739
+ if (dnsResult.aaaa.length) console.log(` DNS AAAA: ${dnsResult.aaaa.join(", ")}`);
1740
+ }
1741
+
1742
+ // HTTP probe
1743
+ if (httpResult?.reachable) {
1744
+ let httpLine = ` HTTP: ${httpResult.status}`;
1745
+ if (httpResult.parked) httpLine += " (PARKED)";
1746
+ if (httpResult.server) httpLine += ` [${httpResult.server}]`;
1747
+ console.log(httpLine);
1748
+ if (httpResult.redirectUrl) console.log(` Redirect: ${httpResult.redirectUrl}`);
1749
+ }
1750
+
1751
+ // Wayback Machine
1752
+ if (waybackResult?.hasHistory) {
1753
+ console.log(` Wayback: ~${waybackResult.snapshots} snapshots${waybackResult.firstArchived ? ` (${waybackResult.firstArchived}` : ""}${waybackResult.lastArchived ? ` - ${waybackResult.lastArchived})` : waybackResult.firstArchived ? ")" : ""}`);
1754
+ }
1755
+
1756
+ // Domain age
1757
+ if (age) console.log(` Age: ${age}`);
1758
+
1759
+ // Social media
1760
+ if (socialResult) {
1761
+ const avail = socialResult.filter((s) => s.available && !s.error);
1762
+ if (avail.length > 0) {
1763
+ console.log(` Social avail: ${avail.map((s) => s.platform).join(", ")}`);
1764
+ }
1765
+ }
1766
+
1767
+ // Tech stack
1768
+ if (techResult) {
1769
+ const items: string[] = [];
1770
+ if (techResult.cms) items.push(techResult.cms);
1771
+ if (techResult.framework) items.push(techResult.framework);
1772
+ if (techResult.cdn) items.push(techResult.cdn);
1773
+ if (items.length > 0) console.log(` Tech: ${items.join(", ")}`);
1774
+ }
1775
+
1776
+ // Blacklist
1777
+ if (blacklistResult) {
1778
+ if (blacklistResult.listed) {
1779
+ const names = blacklistResult.lists.filter((l) => l.listed).map((l) => l.name).join(", ");
1780
+ console.log(` \x1b[31mBLACKLISTED: ${names}\x1b[0m`);
1781
+ } else {
1782
+ console.log(` Reputation: clean (${blacklistResult.cleanCount}/${blacklistResult.lists.length})`);
1783
+ }
1784
+ }
1785
+
1786
+ // Backlinks
1787
+ if (backlinkResult) {
1788
+ const parts: string[] = [];
1789
+ if (backlinkResult.pageRank !== null) parts.push(`PageRank: ${backlinkResult.pageRank}`);
1790
+ if (backlinkResult.commonCrawlPages !== null) parts.push(`CC pages: ~${backlinkResult.commonCrawlPages}`);
1791
+ if (parts.length > 0) console.log(` Authority: ${parts.join(", ")}`);
1792
+ }
1793
+
1794
+ // Recon data (if --recon was set)
1795
+ if (reconData) {
1796
+ if (reconData.asn && !reconData.asn.error) {
1797
+ const asnParts: string[] = [];
1798
+ if (reconData.asn.asn) asnParts.push(reconData.asn.asn);
1799
+ if (reconData.asn.org) asnParts.push(reconData.asn.org);
1800
+ if (reconData.asn.country) asnParts.push(reconData.asn.country);
1801
+ if (asnParts.length > 0) console.log(` Network: ${asnParts.join(" | ")}`);
1802
+ }
1803
+ if (reconData.portScan && reconData.portScan.openPorts.length > 0) {
1804
+ console.log(` \x1b[31mOpen ports: ${reconData.portScan.openPorts.map((p) => `${p.port}/${p.service}`).join(", ")}\x1b[0m`);
1805
+ }
1806
+ if (reconData.emailSecurity) {
1807
+ console.log(` Email security: ${reconData.emailSecurity.grade} (SPF:${reconData.emailSecurity.spf.found ? "ok" : "missing"} DKIM:${reconData.emailSecurity.dkim.found ? "ok" : "missing"} DMARC:${reconData.emailSecurity.dmarc.found ? "ok" : "missing"})`);
1808
+ }
1809
+ if (reconData.securityHeaders && !reconData.securityHeaders.error) {
1810
+ console.log(` Security headers: ${reconData.securityHeaders.grade} (${reconData.securityHeaders.score}/100)`);
1811
+ }
1812
+ if (reconData.waf?.detected) {
1813
+ console.log(` WAF: ${reconData.waf.waf} (${reconData.waf.confidence})`);
1814
+ }
1815
+ if (reconData.zoneTransfer?.vulnerable) {
1816
+ console.log(` \x1b[31m!! Zone transfer vulnerable: ${reconData.zoneTransfer.vulnerableNs.join(", ")}\x1b[0m`);
1817
+ }
1818
+ if (reconData.takeover?.vulnerable) {
1819
+ const vulnSubs = reconData.takeover.findings.filter((f) => f.status === "vulnerable");
1820
+ console.log(` \x1b[31m!! Subdomain takeover: ${vulnSubs.map((f) => f.subdomain).join(", ")}\x1b[0m`);
1821
+ }
1822
+ if (reconData.pathScan && reconData.pathScan.findings.length > 0) {
1823
+ console.log(` \x1b[31mExposed paths: ${reconData.pathScan.findings.slice(0, 5).map((f) => f.path).join(", ")}\x1b[0m`);
1824
+ }
1825
+ if (reconData.cors?.vulnerable) {
1826
+ console.log(` \x1b[31m!! CORS misconfiguration\x1b[0m`);
1827
+ }
1828
+ if (reconData.certTransparency && reconData.certTransparency.subdomains.length > 0) {
1829
+ console.log(` CT subdomains: ${reconData.certTransparency.subdomains.length} found`);
1830
+ }
1831
+ if (reconData.reverseIp && reconData.reverseIp.sharedDomains.length > 0) {
1832
+ console.log(` Shared hosting: ${reconData.reverseIp.sharedDomains.length} domains on ${reconData.reverseIp.ip}`);
1833
+ }
1834
+ }
1835
+
1836
+ // Registrar check
1837
+ if (config?.apiKey) {
1838
+ const regCheck = await checkAvailabilityViaRegistrar(domain, config);
1839
+ if (regCheck.available) {
1840
+ console.log(` ✓ Registrar (${config.provider}): Available${regCheck.price ? ` - $${regCheck.price}` : ""}`);
1841
+ }
1842
+
1843
+ // Auto-register
1844
+ if (options.autoRegister && (whois.available || whois.expired) && verification.available) {
1845
+ console.log(` ◎ Registering via ${config.provider}...`);
1846
+ const result = await registerDomain(domain, config);
1847
+ if (result.success) {
1848
+ console.log(` ★ ${result.message}`);
1849
+ } else {
1850
+ console.log(` ✗ Registration failed: ${result.error}`);
1851
+ }
1852
+ }
1853
+ }
1854
+
1855
+ console.log();
1856
+
1857
+ // Visual separator between multi-domain results
1858
+ if (i < domainList.length - 1) {
1859
+ console.log(" ───────────────────────────────────");
1860
+ }
1861
+ }
1862
+
1863
+ // Rate limit between lookups
1864
+ if (i < domainList.length - 1) {
1865
+ await new Promise((r) => setTimeout(r, 1500));
1866
+ }
1867
+ }
1868
+
1869
+ if (isJsonMode) {
1870
+ const output: JsonOutput = {
1871
+ timestamp: new Date().toISOString(),
1872
+ count: jsonResults.length,
1873
+ results: jsonResults,
1874
+ };
1875
+ console.log(JSON.stringify(output, null, 2));
1876
+ // Exit with error code if any domain had an error status
1877
+ const hasErrors = jsonResults.some((r) => r.status === "error");
1878
+ if (hasErrors) process.exitCode = 1;
1879
+ } else {
1880
+ console.log("Done!\n");
1881
+ }
1882
+
1883
+ // Clean up database connection
1884
+ try {
1885
+ const { closeDb } = await import("./core/db.js");
1886
+ closeDb();
1887
+ } catch {}
1888
+ }