domsniper 0.1.3 → 0.2.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 +36 -17
- package/package.json +1 -1
- package/src/app.tsx +668 -169
- package/src/core/features/domain-suggest.ts +41 -0
- package/src/core/features/grouping.ts +94 -0
- package/src/core/features/history.ts +163 -0
- package/src/core/validate.ts +83 -0
- package/src/core/whois.ts +9 -5
- package/src/index.tsx +88 -13
package/src/app.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { readFileSync, existsSync } from "fs";
|
|
|
9
9
|
import { theme, borders, statusStyle, type DomainStatus } from "./core/theme.js";
|
|
10
10
|
import type { DomainEntry } from "./core/types.js";
|
|
11
11
|
import { createEmptyEntry } from "./core/types.js";
|
|
12
|
-
import { sanitizeDomainList, safePath } from "./core/validate.js";
|
|
12
|
+
import { sanitizeDomainList, safePath, detectTldTypo } from "./core/validate.js";
|
|
13
13
|
import { lookupDns } from "./core/features/dns-details.js";
|
|
14
14
|
import { httpProbe } from "./core/features/http-probe.js";
|
|
15
15
|
import { checkWayback } from "./core/features/wayback.js";
|
|
@@ -20,14 +20,14 @@ import { scoreDomain, scoreGrade } from "./core/features/scoring.js";
|
|
|
20
20
|
import { exportToCSV, exportToJSON } from "./core/features/export.js";
|
|
21
21
|
import { DomainWatcher, formatInterval } from "./core/features/watch.js";
|
|
22
22
|
import { saveSession, loadSession, listSessions } from "./core/features/session.js";
|
|
23
|
-
import { filterDomains, nextStatus,
|
|
23
|
+
import { filterDomains, nextStatus, DEFAULT_FILTER, type FilterConfig, type FilterStatus, type SortField } from "./core/features/filter.js";
|
|
24
24
|
import { rdapLookup } from "./core/features/rdap.js";
|
|
25
25
|
import { checkSsl } from "./core/features/ssl-check.js";
|
|
26
26
|
import { discoverSubdomains, getActiveSubdomains } from "./core/features/subdomain-discovery.js";
|
|
27
27
|
import { checkMarketplaces } from "./core/features/marketplace.js";
|
|
28
28
|
import { sendWebhook } from "./core/features/webhooks.js";
|
|
29
29
|
import { loadConfig } from "./core/features/config.js";
|
|
30
|
-
import { generateSuggestions } from "./core/features/domain-suggest.js";
|
|
30
|
+
import { generateSuggestions, generateScoredSuggestions } from "./core/features/domain-suggest.js";
|
|
31
31
|
import { addToPortfolio } from "./core/features/portfolio.js";
|
|
32
32
|
import { snipeDomain, getSnipeTargets, cancelSnipe } from "./core/features/snipe.js";
|
|
33
33
|
import {
|
|
@@ -52,14 +52,16 @@ import { auditSecurityHeaders } from "./core/features/security-headers.js";
|
|
|
52
52
|
import { detectWaf } from "./core/features/waf-detect.js";
|
|
53
53
|
import { scanPaths } from "./core/features/path-scanner.js";
|
|
54
54
|
import { checkCors } from "./core/features/cors-check.js";
|
|
55
|
-
import {
|
|
55
|
+
import { groupDomains, shouldShowGroups, type DomainGroup } from "./core/features/grouping.js";
|
|
56
|
+
import { getTimeline, loadHistoryScan, type HistoryTimeline } from "./core/features/history.js";
|
|
57
|
+
import { upsertDomain, saveScan, getCached, setCache, clearCache, getScanHistory, getLatestScan, getDomainByName, createSession as dbCreateSession, updateSessionCount, getDbStats, getAllDomains, getPortfolioExpiring } from "./core/db.js";
|
|
56
58
|
import { getPortfolioDashboard, getUnacknowledgedAlerts, acknowledgeAllAlerts, getMonthlyReport, addTransaction, updatePortfolioStatus, getCategories, getSnipeStats, type PortfolioStatus, type TransactionType } from "./core/db.js";
|
|
57
59
|
import { generateRenewalCalendar, estimateAnnualRenewalCost } from "./core/features/portfolio-monitor.js";
|
|
58
60
|
|
|
59
61
|
// ─── Types ────────────────────────────────────────────────
|
|
60
62
|
|
|
61
63
|
type Mode = "idle" | "input" | "scanning" | "done" | "watching";
|
|
62
|
-
type InputMode = "domain" | "file" | "expand" | "variations" | "export" | "load";
|
|
64
|
+
type InputMode = "domain" | "file" | "expand" | "variations" | "export" | "load" | "suggest";
|
|
63
65
|
|
|
64
66
|
interface AppProps {
|
|
65
67
|
initialDomains?: string[];
|
|
@@ -90,16 +92,20 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
90
92
|
const [showPortfolio, setShowPortfolio] = useState(false);
|
|
91
93
|
const [filter, setFilter] = useState<FilterConfig>({ ...DEFAULT_FILTER });
|
|
92
94
|
const [logs, setLogs] = useState<{ id: number; time: string; msg: string; fg: string }[]>([
|
|
93
|
-
{ id: 0, time: ts(), msg: "
|
|
95
|
+
{ id: 0, time: ts(), msg: "domsniper v0.2.0 — recon mode on", fg: theme.textMuted },
|
|
94
96
|
{ id: 1, time: ts(), msg: "Press ? for all commands", fg: theme.textMuted },
|
|
95
97
|
]);
|
|
96
98
|
const [registrarConfig] = useState<RegistrarConfig | null>(loadConfigFromEnv());
|
|
97
|
-
const [reconMode, setReconMode] = useState(
|
|
99
|
+
const [reconMode, setReconMode] = useState(true);
|
|
100
|
+
const [showActionMenu, setShowActionMenu] = useState(false);
|
|
98
101
|
const [watcher, setWatcher] = useState<DomainWatcher | null>(null);
|
|
99
102
|
const [watchCycle, setWatchCycle] = useState(0);
|
|
100
103
|
const [intelTab, setIntelTab] = useState<IntelTab>("overview");
|
|
101
104
|
const [scanProgress, setScanProgress] = useState<{ current: number; total: number } | null>(null);
|
|
102
105
|
const [confirmBulkRegister, setConfirmBulkRegister] = useState(false);
|
|
106
|
+
const [showGroups, setShowGroups] = useState(false);
|
|
107
|
+
const [showHistory, setShowHistory] = useState(false);
|
|
108
|
+
const [historyIndex, setHistoryIndex] = useState(0);
|
|
103
109
|
|
|
104
110
|
// Marketplace state
|
|
105
111
|
const [showMarket, setShowMarket] = useState(false);
|
|
@@ -263,13 +269,30 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
263
269
|
|
|
264
270
|
const processAllDomains = useCallback(async (domainList: string[], append = false) => {
|
|
265
271
|
if (processingRef.current) return;
|
|
272
|
+
|
|
273
|
+
// Deduplicate
|
|
274
|
+
const uniqueList = [...new Set(domainList)];
|
|
275
|
+
|
|
276
|
+
// If appending, also remove domains already in the list
|
|
277
|
+
const existingDomains = append ? new Set(domains.map((d) => d.domain)) : new Set<string>();
|
|
278
|
+
const newDomains = uniqueList.filter((d) => !existingDomains.has(d));
|
|
279
|
+
|
|
280
|
+
if (newDomains.length === 0) {
|
|
281
|
+
log("All domains already in list", theme.textMuted);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (newDomains.length < domainList.length) {
|
|
286
|
+
log(`Skipped ${domainList.length - newDomains.length} duplicate(s)`, theme.textDisabled);
|
|
287
|
+
}
|
|
288
|
+
|
|
266
289
|
processingRef.current = true;
|
|
267
290
|
setMode("scanning");
|
|
268
291
|
|
|
269
292
|
let sessionId: number | undefined;
|
|
270
293
|
try { sessionId = dbCreateSession(); } catch {}
|
|
271
294
|
|
|
272
|
-
const entries: DomainEntry[] =
|
|
295
|
+
const entries: DomainEntry[] = newDomains.map((d) => createEmptyEntry(d));
|
|
273
296
|
|
|
274
297
|
if (append) {
|
|
275
298
|
setDomains((prev: DomainEntry[]) => [...prev, ...entries]);
|
|
@@ -278,8 +301,8 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
278
301
|
}
|
|
279
302
|
|
|
280
303
|
const startIdx = append ? domainsCountRef.current : 0;
|
|
281
|
-
log(`━━━ Scanning ${
|
|
282
|
-
setScanProgress({ current: 0, total:
|
|
304
|
+
log(`━━━ Scanning ${newDomains.length} domain${newDomains.length > 1 ? "s" : ""} ━━━`, theme.info);
|
|
305
|
+
setScanProgress({ current: 0, total: newDomains.length });
|
|
283
306
|
|
|
284
307
|
// Concurrent pool
|
|
285
308
|
const CONCURRENCY = 5;
|
|
@@ -295,7 +318,7 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
295
318
|
return u;
|
|
296
319
|
});
|
|
297
320
|
|
|
298
|
-
const result = await processDomain(
|
|
321
|
+
const result = await processDomain(newDomains[i]!);
|
|
299
322
|
setDomains((prev: DomainEntry[]) => { const u = [...prev]; u[globalIdx] = result; return u; });
|
|
300
323
|
setScanProgress((prev) => prev ? { ...prev, current: prev.current + 1 } : null);
|
|
301
324
|
|
|
@@ -306,13 +329,13 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
306
329
|
return u;
|
|
307
330
|
});
|
|
308
331
|
try {
|
|
309
|
-
const regResult = await registerDomain(
|
|
332
|
+
const regResult = await registerDomain(newDomains[i]!, registrarConfig);
|
|
310
333
|
setDomains((prev: DomainEntry[]) => {
|
|
311
334
|
const u = [...prev];
|
|
312
335
|
if (u[globalIdx]) u[globalIdx] = { ...u[globalIdx]!, status: regResult.success ? "registered" : u[globalIdx]!.status, registration: regResult };
|
|
313
336
|
return u;
|
|
314
337
|
});
|
|
315
|
-
if (regResult.success) log(`★ REG'd ${
|
|
338
|
+
if (regResult.success) log(`★ REG'd ${newDomains[i]}`, theme.secondary);
|
|
316
339
|
} catch (err: unknown) {
|
|
317
340
|
log(`REG failed: ${err instanceof Error ? err.message : "unknown"}`, theme.error);
|
|
318
341
|
}
|
|
@@ -324,18 +347,18 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
324
347
|
}
|
|
325
348
|
|
|
326
349
|
const workers = Array.from(
|
|
327
|
-
{ length: Math.min(CONCURRENCY,
|
|
350
|
+
{ length: Math.min(CONCURRENCY, newDomains.length) },
|
|
328
351
|
() => worker()
|
|
329
352
|
);
|
|
330
353
|
await Promise.all(workers);
|
|
331
354
|
|
|
332
|
-
try { if (sessionId) updateSessionCount(sessionId,
|
|
355
|
+
try { if (sessionId) updateSessionCount(sessionId, newDomains.length); } catch {}
|
|
333
356
|
|
|
334
357
|
processingRef.current = false;
|
|
335
358
|
setScanProgress(null);
|
|
336
359
|
setMode("done");
|
|
337
360
|
log("━━━ Scan complete ━━━", theme.info);
|
|
338
|
-
}, [autoRegister, registrarConfig, processDomain, log]);
|
|
361
|
+
}, [autoRegister, registrarConfig, processDomain, log, domains]);
|
|
339
362
|
|
|
340
363
|
// ─── Marketplace loaders ────────────────────────────────
|
|
341
364
|
|
|
@@ -403,6 +426,10 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
403
426
|
// ─── Filtered domains ──────────────────────────────────
|
|
404
427
|
|
|
405
428
|
const filteredDomains = useMemo(() => filterDomains(domains, filter), [domains, filter]);
|
|
429
|
+
const domainGroups = useMemo(() => {
|
|
430
|
+
if (!showGroups || filteredDomains.length < 4) return null;
|
|
431
|
+
return groupDomains(filteredDomains);
|
|
432
|
+
}, [filteredDomains, showGroups]);
|
|
406
433
|
const selected = filteredDomains[selectedIndex] || null;
|
|
407
434
|
|
|
408
435
|
// ─── Keyboard ───────────────────────────────────────────
|
|
@@ -419,12 +446,186 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
419
446
|
if (showHelp && key === "escape") { setShowHelp(false); return; }
|
|
420
447
|
if (showHelp) return; // Consume all keys while help is shown
|
|
421
448
|
|
|
449
|
+
// ── History panel ──
|
|
450
|
+
if (showHistory && selected) {
|
|
451
|
+
if (key === "escape") { setShowHistory(false); return; }
|
|
452
|
+
if (key === "up" || key === "k") { setHistoryIndex((i) => Math.max(0, i - 1)); return; }
|
|
453
|
+
if (key === "down" || key === "j") { setHistoryIndex((i) => i + 1); return; }
|
|
454
|
+
|
|
455
|
+
// Load selected historical scan
|
|
456
|
+
if (key === "return") {
|
|
457
|
+
const timeline = getTimeline(selected.domain, 20);
|
|
458
|
+
const entry = timeline.entries[historyIndex];
|
|
459
|
+
if (entry) {
|
|
460
|
+
const scan = loadHistoryScan(entry.id);
|
|
461
|
+
if (scan) {
|
|
462
|
+
const idx = domains.indexOf(selected);
|
|
463
|
+
if (idx >= 0) {
|
|
464
|
+
setDomains((prev: DomainEntry[]) => {
|
|
465
|
+
const u = [...prev];
|
|
466
|
+
u[idx] = { ...scan, tagged: prev[idx]!.tagged };
|
|
467
|
+
return u;
|
|
468
|
+
});
|
|
469
|
+
log(`Loaded scan from ${entry.scannedAt}`, theme.info);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
setShowHistory(false);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return; // Consume all keys while history is open
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── Action menu mode ──
|
|
481
|
+
if (showActionMenu && selected) {
|
|
482
|
+
const idx = domains.indexOf(selected);
|
|
483
|
+
|
|
484
|
+
if (key === "escape") { setShowActionMenu(false); return; }
|
|
485
|
+
|
|
486
|
+
// Rescan
|
|
487
|
+
if (key === "return") {
|
|
488
|
+
setShowActionMenu(false);
|
|
489
|
+
clearCache(selected.domain);
|
|
490
|
+
if (idx >= 0) {
|
|
491
|
+
setDomains((prev: DomainEntry[]) => { const u = [...prev]; u[idx] = { ...createEmptyEntry(selected.domain), status: "checking" }; return u; });
|
|
492
|
+
processDomain(selected.domain).then((result) => { setDomains((prev: DomainEntry[]) => { const u = [...prev]; u[idx] = result; return u; }); });
|
|
493
|
+
}
|
|
494
|
+
log(`Rescanning ${selected.domain}...`, theme.info);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Register
|
|
499
|
+
if (key === "r" && (selected.status === "available" || selected.status === "expired") && registrarConfig?.apiKey) {
|
|
500
|
+
setShowActionMenu(false);
|
|
501
|
+
void handleRegister(idx);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Snipe
|
|
506
|
+
if (key === "z") {
|
|
507
|
+
setShowActionMenu(false);
|
|
508
|
+
if (selected.status === "taken" || selected.status === "expired") {
|
|
509
|
+
snipeDomain(selected.domain, { expiryDate: selected.whois?.expiryDate || undefined });
|
|
510
|
+
log(`Sniping ${selected.domain}`, theme.warning);
|
|
511
|
+
} else if (selected.status === "available") {
|
|
512
|
+
log(`Already available — press r to register`, theme.primary);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Add to portfolio
|
|
518
|
+
if (key === "p") {
|
|
519
|
+
setShowActionMenu(false);
|
|
520
|
+
try {
|
|
521
|
+
addToPortfolio(selected.domain, {
|
|
522
|
+
registrar: selected.whois?.registrar || selected.rdap?.registrar || "unknown",
|
|
523
|
+
expiryDate: selected.whois?.expiryDate || selected.rdap?.expiryDate || "",
|
|
524
|
+
});
|
|
525
|
+
log(`Added ${selected.domain} to portfolio`, theme.info);
|
|
526
|
+
} catch (err: unknown) {
|
|
527
|
+
log(`Portfolio: ${err instanceof Error ? err.message : "failed"}`, theme.error);
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Suggest alternatives
|
|
533
|
+
if (key === "d") {
|
|
534
|
+
setShowActionMenu(false);
|
|
535
|
+
const name = selected.domain.split(".")[0] || "";
|
|
536
|
+
const existingDomains = new Set(domains.map((d) => d.domain));
|
|
537
|
+
const suggestions = generateScoredSuggestions(name, ["com", "io", "dev", "app", "co"], 20);
|
|
538
|
+
const newSuggestions = suggestions.filter((s) => !existingDomains.has(s.domain));
|
|
539
|
+
if (newSuggestions.length > 0) {
|
|
540
|
+
log(`Generated ${newSuggestions.length} suggestions from "${name}" (top: ${newSuggestions[0]!.domain} ${newSuggestions[0]!.grade})`, theme.info);
|
|
541
|
+
processAllDomains(newSuggestions.map((s) => s.domain), true);
|
|
542
|
+
setShowGroups(true);
|
|
543
|
+
} else {
|
|
544
|
+
log(`All suggestions for "${name}" already in list`, theme.textMuted);
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Variations
|
|
550
|
+
if (key === "v") {
|
|
551
|
+
setShowActionMenu(false);
|
|
552
|
+
const vars = generateVariations(selected.domain);
|
|
553
|
+
log(`Generated ${vars.length} variations of ${selected.domain}`, theme.info);
|
|
554
|
+
if (vars.length > 0) { processAllDomains(vars, true); setShowGroups(true); }
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// TLD expansion
|
|
559
|
+
if (key === "e") {
|
|
560
|
+
setShowActionMenu(false);
|
|
561
|
+
const name = selected.domain.split(".")[0] || "";
|
|
562
|
+
const expanded = expandTlds(name, "popular");
|
|
563
|
+
log(`Expanded "${name}" into ${expanded.length} TLD variants`, theme.info);
|
|
564
|
+
if (expanded.length > 0) { processAllDomains(expanded, domains.length > 0); setShowGroups(true); }
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Export
|
|
569
|
+
if (key === "x") {
|
|
570
|
+
setShowActionMenu(false);
|
|
571
|
+
setInputMode("export");
|
|
572
|
+
setMode("input");
|
|
573
|
+
setInputValue("");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Watch
|
|
578
|
+
if (key === "w") {
|
|
579
|
+
setShowActionMenu(false);
|
|
580
|
+
setDomains((prev: DomainEntry[]) => {
|
|
581
|
+
const u = [...prev];
|
|
582
|
+
if (idx >= 0) u[idx] = { ...u[idx]!, tagged: true };
|
|
583
|
+
return u;
|
|
584
|
+
});
|
|
585
|
+
log(`Tagged ${selected.domain} for watching`, theme.info);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Clear cache
|
|
590
|
+
if (key === "c") {
|
|
591
|
+
setShowActionMenu(false);
|
|
592
|
+
const count = clearCache(selected.domain);
|
|
593
|
+
log(`Cleared cache for ${selected.domain} (${count} entries)`, theme.info);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// History panel
|
|
598
|
+
if (key === "h") {
|
|
599
|
+
setShowActionMenu(false);
|
|
600
|
+
setShowHistory(true);
|
|
601
|
+
setHistoryIndex(0);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Load previous
|
|
606
|
+
if (key === "l") {
|
|
607
|
+
setShowActionMenu(false);
|
|
608
|
+
try {
|
|
609
|
+
const prevScan = getLatestScan(selected.domain);
|
|
610
|
+
if (prevScan && idx >= 0) {
|
|
611
|
+
setDomains((prev: DomainEntry[]) => { const u = [...prev]; u[idx] = { ...prevScan, tagged: prev[idx]!.tagged }; return u; });
|
|
612
|
+
log(`Loaded last saved scan for ${selected.domain}`, theme.info);
|
|
613
|
+
} else {
|
|
614
|
+
log(`No saved scan for ${selected.domain}`, theme.textMuted);
|
|
615
|
+
}
|
|
616
|
+
} catch { log("Could not load scan", theme.error); }
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return; // Consume all other keys while menu is open
|
|
621
|
+
}
|
|
622
|
+
|
|
422
623
|
// Toggle portfolio dashboard
|
|
423
|
-
if (key === "
|
|
624
|
+
if (key === "b" && mode !== "input") { setShowPortfolio((v) => !v); return; }
|
|
424
625
|
if (showPortfolio && key === "escape") { setShowPortfolio(false); return; }
|
|
425
626
|
|
|
426
627
|
// ── Marketplace toggle ──
|
|
427
|
-
if (key === "
|
|
628
|
+
if (key === "m" && mode !== "input") {
|
|
428
629
|
if (!showMarket) {
|
|
429
630
|
setShowMarket(true);
|
|
430
631
|
setMarketView("browse");
|
|
@@ -544,7 +745,7 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
544
745
|
}
|
|
545
746
|
|
|
546
747
|
// Cancel bulk register on any other key
|
|
547
|
-
if (confirmBulkRegister && key !== "
|
|
748
|
+
if (confirmBulkRegister && key !== "a") {
|
|
548
749
|
setConfirmBulkRegister(false);
|
|
549
750
|
}
|
|
550
751
|
|
|
@@ -557,10 +758,11 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
557
758
|
// Generate variations for selected domain
|
|
558
759
|
const vars = generateVariations(selected.domain);
|
|
559
760
|
log(`Generated ${vars.length} variations of ${selected.domain}`, theme.info);
|
|
560
|
-
if (vars.length > 0) processAllDomains(vars, true);
|
|
761
|
+
if (vars.length > 0) { processAllDomains(vars, true); setShowGroups(true); }
|
|
561
762
|
return;
|
|
562
763
|
}
|
|
563
764
|
if (key === "x") { setInputMode("export"); setMode("input"); setInputValue(""); log("Enter export path (.csv or .json)...", theme.textMuted); return; }
|
|
765
|
+
if (key === "d" && !selected && !showActionMenu && !showMarket && !showPortfolio) { setInputMode("suggest"); setMode("input"); setInputValue(""); log("Enter keyword for domain suggestions...", theme.textMuted); return; }
|
|
564
766
|
}
|
|
565
767
|
|
|
566
768
|
// ── Input mode ──
|
|
@@ -575,20 +777,44 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
575
777
|
if (key === "n" && !ctrl) {
|
|
576
778
|
setReconMode((v) => {
|
|
577
779
|
const newVal = !v;
|
|
578
|
-
|
|
780
|
+
if (domains.length > 0) {
|
|
781
|
+
log(newVal ? "Recon mode ON — press Enter on a domain to rescan with recon" : "Recon mode OFF (fast scan)", newVal ? theme.warning : theme.textMuted);
|
|
782
|
+
} else {
|
|
783
|
+
log(newVal ? "Recon mode ON — next scan will include security checks" : "Recon mode OFF (fast scan)", newVal ? theme.warning : theme.textMuted);
|
|
784
|
+
}
|
|
579
785
|
return newVal;
|
|
580
786
|
});
|
|
581
787
|
return;
|
|
582
788
|
}
|
|
583
789
|
|
|
584
|
-
// Snipe selected domain (
|
|
585
|
-
if (key === "
|
|
586
|
-
if (selected.status === "
|
|
790
|
+
// Snipe selected domain (merged with drop catch for expired domains)
|
|
791
|
+
if (key === "z" && selected) {
|
|
792
|
+
if (selected.status === "expired" || selected.whois?.expired) {
|
|
793
|
+
// Expired domain — start snipe + drop catch
|
|
794
|
+
snipeDomain(selected.domain, {
|
|
795
|
+
expiryDate: selected.whois?.expiryDate || undefined,
|
|
796
|
+
});
|
|
797
|
+
log(`Sniping ${selected.domain} — expired, will auto-register when it drops`, theme.warning);
|
|
798
|
+
log(`Run 'domain-sniper snipe run' to start the engine`, theme.textDisabled);
|
|
799
|
+
// Also start drop catch if registrar is configured
|
|
800
|
+
if (registrarConfig?.apiKey) {
|
|
801
|
+
const catcher = createDropCatcher({
|
|
802
|
+
domain: selected.domain,
|
|
803
|
+
registrarConfig: registrarConfig!,
|
|
804
|
+
pollIntervalMs: 30000,
|
|
805
|
+
maxAttempts: 2880,
|
|
806
|
+
onStatus: (status: DropCatchStatus) => log(formatDropCatchStatus(status), status.phase === "success" ? theme.primary : status.phase === "failed" ? theme.error : theme.info),
|
|
807
|
+
onSuccess: (d: string) => log(`DROP CAUGHT: ${d}!`, theme.primary),
|
|
808
|
+
onFailed: (d: string, err: string) => log(`Drop catch failed for ${d}: ${err}`, theme.error),
|
|
809
|
+
});
|
|
810
|
+
catcher.start();
|
|
811
|
+
log(`Drop catch started for ${selected.domain} (polling every 30s)`, theme.info);
|
|
812
|
+
}
|
|
813
|
+
} else if (selected.status === "taken") {
|
|
587
814
|
snipeDomain(selected.domain, {
|
|
588
815
|
expiryDate: selected.whois?.expiryDate || undefined,
|
|
589
816
|
});
|
|
590
|
-
|
|
591
|
-
log(`Sniping ${selected.domain} — ${selected.status === "expired" ? "expired, checking every 5 min" : "watching for expiry"}`, theme.warning);
|
|
817
|
+
log(`Sniping ${selected.domain} — watching for expiry`, theme.info);
|
|
592
818
|
log(`Run 'domain-sniper snipe run' to start the engine`, theme.textDisabled);
|
|
593
819
|
} else if (selected.status === "available") {
|
|
594
820
|
log(`${selected.domain} is already available — press r to register now`, theme.primary);
|
|
@@ -604,9 +830,23 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
604
830
|
else if (key === "down" || key === "j" || (ctrl && key === "n")) setSelectedIndex((i: number) => Math.min(filteredDomains.length - 1, i + 1));
|
|
605
831
|
else if (key === "pageup") setSelectedIndex((i: number) => Math.max(0, i - 10));
|
|
606
832
|
else if (key === "pagedown") setSelectedIndex((i: number) => Math.min(filteredDomains.length - 1, i + 10));
|
|
607
|
-
else if (key === "home"
|
|
833
|
+
else if (key === "home") setSelectedIndex(0);
|
|
834
|
+
else if (key === "g") {
|
|
835
|
+
if (shouldShowGroups(filteredDomains)) {
|
|
836
|
+
setShowGroups((v) => !v);
|
|
837
|
+
log(showGroups ? "Flat list view" : "Grouped by base name", theme.textMuted);
|
|
838
|
+
} else {
|
|
839
|
+
log("Not enough domains to group", theme.textMuted);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
608
842
|
else if (key === "end") setSelectedIndex(Math.max(0, filteredDomains.length - 1));
|
|
609
843
|
|
|
844
|
+
// History panel (global shortcut)
|
|
845
|
+
else if (key === "h" && selected && !showActionMenu && !showMarket && !showPortfolio && !showHistory) {
|
|
846
|
+
setShowHistory(true);
|
|
847
|
+
setHistoryIndex(0);
|
|
848
|
+
}
|
|
849
|
+
|
|
610
850
|
// Register (Issue 5: feedback on all statuses)
|
|
611
851
|
else if (key === "r" && selected) {
|
|
612
852
|
if ((selected.status === "available" || selected.status === "expired") && registrarConfig?.apiKey) {
|
|
@@ -618,16 +858,16 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
618
858
|
}
|
|
619
859
|
}
|
|
620
860
|
|
|
621
|
-
// Bulk register tagged (
|
|
622
|
-
else if (key === "
|
|
861
|
+
// Bulk register tagged (two-step confirmation)
|
|
862
|
+
else if (key === "a") {
|
|
623
863
|
const tagged = domains.filter((d) => d.tagged && (d.status === "available" || d.status === "expired"));
|
|
624
864
|
if (!registrarConfig?.apiKey) {
|
|
625
865
|
log("No registrar configured", theme.warning);
|
|
626
866
|
} else if (tagged.length === 0) {
|
|
627
|
-
log("Tag domains with SPACE first, then
|
|
867
|
+
log("Tag domains with SPACE first, then a to bulk register", theme.warning);
|
|
628
868
|
} else if (!confirmBulkRegister) {
|
|
629
869
|
setConfirmBulkRegister(true);
|
|
630
|
-
log(`⚠ CONFIRM: Press
|
|
870
|
+
log(`⚠ CONFIRM: Press a again to register ${tagged.length} domain(s) via ${registrarConfig.provider}`, theme.warning);
|
|
631
871
|
setTimeout(() => setConfirmBulkRegister(false), 5000);
|
|
632
872
|
} else {
|
|
633
873
|
setConfirmBulkRegister(false);
|
|
@@ -654,17 +894,23 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
654
894
|
setSelectedIndex(0);
|
|
655
895
|
}
|
|
656
896
|
|
|
657
|
-
// Sort: cycle field
|
|
897
|
+
// Sort: cycle field + order (o cycles: field asc -> field desc -> next field asc -> ...)
|
|
658
898
|
else if (key === "o") {
|
|
659
|
-
setFilter((f: FilterConfig) =>
|
|
899
|
+
setFilter((f: FilterConfig) => {
|
|
900
|
+
const fields: SortField[] = ["domain", "status", "score", "expiry", "price"];
|
|
901
|
+
const currentIdx = fields.indexOf(f.sort);
|
|
902
|
+
if (f.order === "asc") {
|
|
903
|
+
// Switch to desc of same field
|
|
904
|
+
return { ...f, order: "desc" as const };
|
|
905
|
+
} else {
|
|
906
|
+
// Move to next field, reset to asc
|
|
907
|
+
const nextField = fields[(currentIdx + 1) % fields.length]!;
|
|
908
|
+
return { ...f, sort: nextField, order: "asc" as const };
|
|
909
|
+
}
|
|
910
|
+
});
|
|
660
911
|
setSelectedIndex(0);
|
|
661
912
|
}
|
|
662
913
|
|
|
663
|
-
// Sort: toggle order
|
|
664
|
-
else if (key === "O") {
|
|
665
|
-
setFilter((f: FilterConfig) => ({ ...f, order: f.order === "asc" ? "desc" : "asc" }));
|
|
666
|
-
}
|
|
667
|
-
|
|
668
914
|
// Watch mode
|
|
669
915
|
else if (key === "w") {
|
|
670
916
|
if (watcher?.running) {
|
|
@@ -692,38 +938,21 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
692
938
|
}
|
|
693
939
|
}
|
|
694
940
|
|
|
695
|
-
// Domain suggestions
|
|
941
|
+
// Domain suggestions — scored, multi-TLD
|
|
696
942
|
else if (key === "d" && selected) {
|
|
697
943
|
const name = selected.domain.split(".")[0] || "";
|
|
698
|
-
const suggestions = generateSuggestions(name);
|
|
699
944
|
const existingDomains = new Set(domains.map((d) => d.domain));
|
|
700
|
-
const
|
|
945
|
+
const suggestions = generateScoredSuggestions(name, ["com", "io", "dev", "app", "co"], 20);
|
|
946
|
+
const newSuggestions = suggestions.filter((s) => !existingDomains.has(s.domain));
|
|
701
947
|
if (newSuggestions.length > 0) {
|
|
702
|
-
log(`Generated ${newSuggestions.length}
|
|
948
|
+
log(`Generated ${newSuggestions.length} suggestions from "${name}" (top: ${newSuggestions[0]!.domain} ${newSuggestions[0]!.grade})`, theme.info);
|
|
703
949
|
processAllDomains(newSuggestions.map((s) => s.domain), true);
|
|
950
|
+
setShowGroups(true);
|
|
704
951
|
} else {
|
|
705
952
|
log(`All suggestions for "${name}" already in list`, theme.textMuted);
|
|
706
953
|
}
|
|
707
954
|
}
|
|
708
955
|
|
|
709
|
-
// Drop catch mode
|
|
710
|
-
else if (key === "D" && selected && registrarConfig?.apiKey) {
|
|
711
|
-
if (selected.status === "expired" || selected.whois?.expired) {
|
|
712
|
-
const catcher = createDropCatcher({
|
|
713
|
-
domain: selected.domain,
|
|
714
|
-
registrarConfig: registrarConfig!,
|
|
715
|
-
pollIntervalMs: 30000,
|
|
716
|
-
maxAttempts: 2880,
|
|
717
|
-
onStatus: (status: DropCatchStatus) => log(formatDropCatchStatus(status), status.phase === "success" ? theme.primary : status.phase === "failed" ? theme.error : theme.info),
|
|
718
|
-
onSuccess: (d: string) => log(`DROP CAUGHT: ${d}!`, theme.primary),
|
|
719
|
-
onFailed: (d: string, err: string) => log(`Drop catch failed for ${d}: ${err}`, theme.error),
|
|
720
|
-
});
|
|
721
|
-
catcher.start();
|
|
722
|
-
log(`Drop catch started for ${selected.domain} (polling every 30s)`, theme.info);
|
|
723
|
-
} else {
|
|
724
|
-
log("Drop catch requires an expired domain", theme.warning);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
956
|
|
|
728
957
|
// Add to portfolio
|
|
729
958
|
else if (key === "p" && selected) {
|
|
@@ -745,6 +974,15 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
745
974
|
log(`Cleared cache for ${selected.domain} (${count} entries)`, theme.info);
|
|
746
975
|
}
|
|
747
976
|
|
|
977
|
+
// Context menu
|
|
978
|
+
else if (key === "return" && selected && mode !== "scanning" && !showMarket && !showPortfolio) {
|
|
979
|
+
if (showActionMenu) {
|
|
980
|
+
setShowActionMenu(false);
|
|
981
|
+
} else {
|
|
982
|
+
setShowActionMenu(true);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
748
986
|
// Show scan history for selected domain
|
|
749
987
|
else if (key === "h" && selected) {
|
|
750
988
|
const history = getScanHistory(selected.domain, 5);
|
|
@@ -758,6 +996,29 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
758
996
|
}
|
|
759
997
|
}
|
|
760
998
|
|
|
999
|
+
// Load previous scan result
|
|
1000
|
+
else if (key === "l" && selected) {
|
|
1001
|
+
try {
|
|
1002
|
+
const prevScan = getLatestScan(selected.domain);
|
|
1003
|
+
if (prevScan) {
|
|
1004
|
+
// Replace the current entry with the historical scan
|
|
1005
|
+
const idx = domains.indexOf(selected);
|
|
1006
|
+
if (idx >= 0) {
|
|
1007
|
+
setDomains((prev: DomainEntry[]) => {
|
|
1008
|
+
const u = [...prev];
|
|
1009
|
+
u[idx] = { ...prevScan, tagged: prev[idx]!.tagged };
|
|
1010
|
+
return u;
|
|
1011
|
+
});
|
|
1012
|
+
log(`Loaded last saved scan for ${selected.domain}`, theme.info);
|
|
1013
|
+
}
|
|
1014
|
+
} else {
|
|
1015
|
+
log(`No saved scan history for ${selected.domain}`, theme.textMuted);
|
|
1016
|
+
}
|
|
1017
|
+
} catch {
|
|
1018
|
+
log("Could not load scan history", theme.error);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
761
1022
|
// Save session
|
|
762
1023
|
else if (ctrl && key === "s") {
|
|
763
1024
|
const path = saveSession(domains);
|
|
@@ -859,7 +1120,7 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
859
1120
|
} else if (inputMode === "expand") {
|
|
860
1121
|
const expanded = expandTlds(v, "popular");
|
|
861
1122
|
log(`Expanded "${v}" into ${expanded.length} TLD variants`, theme.info);
|
|
862
|
-
if (expanded.length > 0) processAllDomains(expanded, domains.length > 0);
|
|
1123
|
+
if (expanded.length > 0) { processAllDomains(expanded, domains.length > 0); setShowGroups(true); }
|
|
863
1124
|
else { setMode(domains.length > 0 ? "done" : "idle"); }
|
|
864
1125
|
} else if (inputMode === "export") {
|
|
865
1126
|
try {
|
|
@@ -874,6 +1135,23 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
874
1135
|
setMode("done");
|
|
875
1136
|
log(`Loaded session: ${session.id} (${session.domains.length} domains)`, theme.info);
|
|
876
1137
|
} else { log("Session not found", theme.error); setMode(domains.length > 0 ? "done" : "idle"); }
|
|
1138
|
+
} else if (inputMode === "suggest") {
|
|
1139
|
+
const suggestions = generateScoredSuggestions(v, ["com", "io", "dev", "app", "co"], 20);
|
|
1140
|
+
if (suggestions.length > 0) {
|
|
1141
|
+
const existingDomains = new Set(domains.map((d) => d.domain));
|
|
1142
|
+
const newSuggestions = suggestions.filter((s) => !existingDomains.has(s.domain));
|
|
1143
|
+
if (newSuggestions.length > 0) {
|
|
1144
|
+
log(`Generated ${newSuggestions.length} suggestions for "${v}" (top: ${newSuggestions[0]!.domain} ${newSuggestions[0]!.grade})`, theme.info);
|
|
1145
|
+
processAllDomains(newSuggestions.map((s) => s.domain), domains.length > 0);
|
|
1146
|
+
setShowGroups(true);
|
|
1147
|
+
} else {
|
|
1148
|
+
log(`All suggestions for "${v}" already in list`, theme.textMuted);
|
|
1149
|
+
setMode(domains.length > 0 ? "done" : "idle");
|
|
1150
|
+
}
|
|
1151
|
+
} else {
|
|
1152
|
+
log("No suggestions generated", theme.warning);
|
|
1153
|
+
setMode(domains.length > 0 ? "done" : "idle");
|
|
1154
|
+
}
|
|
877
1155
|
} else if (inputMode === "domain") {
|
|
878
1156
|
if (existsSync(v)) {
|
|
879
1157
|
const content = readFileSync(v, "utf-8");
|
|
@@ -881,8 +1159,22 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
881
1159
|
if (list.length > 0) { log(`Loaded ${list.length} from ${v}`, theme.info); processAllDomains(list, domains.length > 0); }
|
|
882
1160
|
} else {
|
|
883
1161
|
const list = v.split(/[,\s]+/).map((d: string) => d.trim().toLowerCase()).filter(Boolean);
|
|
884
|
-
|
|
885
|
-
|
|
1162
|
+
|
|
1163
|
+
// Auto-detect and warn about TLD typos
|
|
1164
|
+
const corrected: string[] = [];
|
|
1165
|
+
for (const d of list) {
|
|
1166
|
+
const suggestion = detectTldTypo(d);
|
|
1167
|
+
if (suggestion) {
|
|
1168
|
+
log(`Auto-corrected: "${d}" → "${suggestion}"`, theme.warning);
|
|
1169
|
+
corrected.push(suggestion);
|
|
1170
|
+
} else {
|
|
1171
|
+
corrected.push(d);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const validated = sanitizeDomainList(corrected);
|
|
1176
|
+
const unique = [...new Set(validated)];
|
|
1177
|
+
if (unique.length > 0) processAllDomains(unique, domains.length > 0);
|
|
886
1178
|
else log("No valid domains entered", theme.warning);
|
|
887
1179
|
}
|
|
888
1180
|
}
|
|
@@ -925,8 +1217,8 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
925
1217
|
const dLine = (w: number) => "═".repeat(Math.max(1, w - 2));
|
|
926
1218
|
const logPanelH = Math.max(4, Math.floor((height - 5) * 0.28));
|
|
927
1219
|
|
|
928
|
-
const inputLabel = marketInputMode === "search" ? "SEARCH" : marketInputMode === "list-price" ? "PRICE" : marketInputMode === "offer-amount" ? "OFFER" : inputMode === "file" ? "FILE" : inputMode === "expand" ? "EXPAND" : inputMode === "export" ? "EXPORT" : inputMode === "load" ? "LOAD" : "SCAN";
|
|
929
|
-
const inputPlaceholder = marketInputMode === "search" ? "Search domains..." : marketInputMode === "list-price" ? `Asking price for ${marketListDomain}` : marketInputMode === "offer-amount" ? "Your offer amount" : inputMode === "file" ? "/path/to/domains.txt" : inputMode === "expand" ? "coolstartup" : inputMode === "export" ? "results.csv or results.json" : inputMode === "load" ? "session-id or path" : "domains or /path/to/file";
|
|
1220
|
+
const inputLabel = marketInputMode === "search" ? "SEARCH" : marketInputMode === "list-price" ? "PRICE" : marketInputMode === "offer-amount" ? "OFFER" : inputMode === "suggest" ? "SUGGEST" : inputMode === "file" ? "FILE" : inputMode === "expand" ? "EXPAND" : inputMode === "export" ? "EXPORT" : inputMode === "load" ? "LOAD" : "SCAN";
|
|
1221
|
+
const inputPlaceholder = marketInputMode === "search" ? "Search domains..." : marketInputMode === "list-price" ? `Asking price for ${marketListDomain}` : marketInputMode === "offer-amount" ? "Your offer amount" : inputMode === "suggest" ? "Enter a keyword (e.g., startup, cloud)" : inputMode === "file" ? "/path/to/domains.txt" : inputMode === "expand" ? "coolstartup" : inputMode === "export" ? "results.csv or results.json" : inputMode === "load" ? "session-id or path" : "domains or /path/to/file";
|
|
930
1222
|
|
|
931
1223
|
// ─── Render ─────────────────────────────────────────────
|
|
932
1224
|
|
|
@@ -1008,7 +1300,7 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
1008
1300
|
)}
|
|
1009
1301
|
{marketUnread > 0 && <text content={`✉ ${marketUnread}`} fg={theme.warning} />}
|
|
1010
1302
|
<box flexGrow={1} />
|
|
1011
|
-
<text content="(
|
|
1303
|
+
<text content="(m to close)" fg={theme.textDisabled} />
|
|
1012
1304
|
</box>
|
|
1013
1305
|
<text content={hLine(width)} fg={theme.borderSubtle} paddingLeft={1} />
|
|
1014
1306
|
|
|
@@ -1091,7 +1383,7 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
1091
1383
|
<box backgroundColor={theme.textDisabled}><text content=" ⏎ " fg={theme.background} /></box><text content="view" fg={theme.textMuted} />
|
|
1092
1384
|
{isLoggedIn() && selected && (<><box backgroundColor={theme.textDisabled}><text content=" l " fg={theme.background} /></box><text content="list domain" fg={theme.textMuted} /></>)}
|
|
1093
1385
|
<box backgroundColor={theme.textDisabled}><text content=" r " fg={theme.background} /></box><text content="refresh" fg={theme.textMuted} />
|
|
1094
|
-
<box backgroundColor={theme.textDisabled}><text content="
|
|
1386
|
+
<box backgroundColor={theme.textDisabled}><text content=" m " fg={theme.background} /></box><text content="close" fg={theme.textMuted} />
|
|
1095
1387
|
</box>
|
|
1096
1388
|
</box>
|
|
1097
1389
|
</box>
|
|
@@ -1112,7 +1404,7 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
1112
1404
|
<box backgroundColor={theme.primary}><text content=" PORTFOLIO DASHBOARD " fg={theme.background} /></box>
|
|
1113
1405
|
<text content={`${dash.totalDomains} domains`} fg={theme.text} />
|
|
1114
1406
|
<text content={`$${dash.totalValue.toFixed(0)} value`} fg={theme.primary} />
|
|
1115
|
-
<text content="(Press
|
|
1407
|
+
<text content="(Press b to close)" fg={theme.textDisabled} />
|
|
1116
1408
|
</box>
|
|
1117
1409
|
|
|
1118
1410
|
<text content="" />
|
|
@@ -1228,29 +1520,68 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
1228
1520
|
{/* ─── LEFT PANEL ─── */}
|
|
1229
1521
|
<box width={sidebarW} flexDirection="column" minHeight={0}>
|
|
1230
1522
|
<box flexDirection="row" justifyContent="space-between" paddingLeft={1} paddingRight={1} flexShrink={0}>
|
|
1231
|
-
<
|
|
1523
|
+
<box flexDirection="row" gap={1}>
|
|
1524
|
+
<text content={` TARGETS ${filteredDomains.length !== stats.total ? `(${filteredDomains.length}/${stats.total})` : stats.total > 0 ? `(${stats.total})` : ""}`} fg={theme.primary} />
|
|
1525
|
+
{showGroups && <text content="grouped" fg={theme.textDisabled} />}
|
|
1526
|
+
</box>
|
|
1232
1527
|
{stats.checking > 0 && <text content={`◆ ${stats.checking}`} fg={theme.warning} />}
|
|
1233
1528
|
</box>
|
|
1234
1529
|
<text content={hLine(sidebarW)} fg={theme.borderSubtle} paddingLeft={1} />
|
|
1235
1530
|
|
|
1236
1531
|
{filteredDomains.length > 0 ? (
|
|
1237
1532
|
<scrollbox flexGrow={1} paddingLeft={1} minHeight={0} scrollbarOptions={{ visible: true }}>
|
|
1238
|
-
{
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1533
|
+
{showGroups && domainGroups ? (
|
|
1534
|
+
<>
|
|
1535
|
+
{domainGroups.map((group) => (
|
|
1536
|
+
<box key={group.baseName} flexDirection="column">
|
|
1537
|
+
{group.total >= 2 && (
|
|
1538
|
+
<box flexDirection="row" gap={1} paddingTop={1}>
|
|
1539
|
+
<text content={`▸ ${group.baseName}`} fg={theme.secondary} />
|
|
1540
|
+
<text content={`(${group.available > 0 ? `${group.available} avail` : ""}${group.expired > 0 ? ` ${group.expired} exp` : ""}${group.taken > 0 ? ` ${group.taken} tkn` : ""})`} fg={theme.textDisabled} />
|
|
1541
|
+
</box>
|
|
1542
|
+
)}
|
|
1543
|
+
{group.domains.map((domainName) => {
|
|
1544
|
+
const entry = filteredDomains.find((d) => d.domain === domainName);
|
|
1545
|
+
if (!entry) return null;
|
|
1546
|
+
const i = filteredDomains.indexOf(entry);
|
|
1547
|
+
const active = selectedIndex === i;
|
|
1548
|
+
const ss = statusStyle(entry.status);
|
|
1549
|
+
const cached = domainScores.get(entry.domain);
|
|
1550
|
+
return (
|
|
1551
|
+
<box key={entry.domain} flexDirection="row" backgroundColor={active ? theme.primaryDim : "transparent"} paddingLeft={group.total >= 2 ? 2 : 1} gap={1}
|
|
1552
|
+
onMouseDown={() => setSelectedIndex(i)}>
|
|
1553
|
+
<text content={entry.tagged ? "◉" : " "} fg={entry.tagged ? theme.info : "transparent"} />
|
|
1554
|
+
<text content={ss.icon} fg={ss.fg} />
|
|
1555
|
+
<text content={entry.domain} fg={active ? theme.text : theme.textSecondary} />
|
|
1556
|
+
<box flexGrow={1} />
|
|
1557
|
+
{cached && <text content={cached.grade.grade} fg={cached.grade.color} />}
|
|
1558
|
+
{entry.status === "checking" && <text content="···" fg={theme.warning} />}
|
|
1559
|
+
</box>
|
|
1560
|
+
);
|
|
1561
|
+
})}
|
|
1562
|
+
</box>
|
|
1563
|
+
))}
|
|
1564
|
+
</>
|
|
1565
|
+
) : (
|
|
1566
|
+
<>
|
|
1567
|
+
{filteredDomains.map((entry: DomainEntry, i: number) => {
|
|
1568
|
+
const active = selectedIndex === i;
|
|
1569
|
+
const ss = statusStyle(entry.status);
|
|
1570
|
+
const cached = domainScores.get(entry.domain);
|
|
1571
|
+
const gr = cached?.grade ?? scoreGrade(0);
|
|
1572
|
+
return (
|
|
1573
|
+
<box key={entry.domain} flexDirection="row" backgroundColor={active ? theme.primaryDim : "transparent"} paddingLeft={1} gap={1} onMouseDown={() => setSelectedIndex(i)}>
|
|
1574
|
+
<text content={entry.tagged ? "◉" : " "} fg={entry.tagged ? theme.info : "transparent"} />
|
|
1575
|
+
<text content={ss.icon} fg={ss.fg} />
|
|
1576
|
+
<text content={entry.domain} fg={active ? theme.text : theme.textSecondary} />
|
|
1577
|
+
<box flexGrow={1} />
|
|
1578
|
+
<text content={gr.grade} fg={gr.color} />
|
|
1579
|
+
{entry.status === "checking" && <text content="···" fg={theme.warning} />}
|
|
1580
|
+
</box>
|
|
1581
|
+
);
|
|
1582
|
+
})}
|
|
1583
|
+
</>
|
|
1584
|
+
)}
|
|
1254
1585
|
</scrollbox>
|
|
1255
1586
|
) : (
|
|
1256
1587
|
<box flexGrow={1} alignItems="center" justifyContent="center" minHeight={0}>
|
|
@@ -1298,66 +1629,221 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
1298
1629
|
{showHelp ? (
|
|
1299
1630
|
<scrollbox flexGrow={1} paddingLeft={2} paddingRight={2} minHeight={0} scrollbarOptions={{ visible: false }}>
|
|
1300
1631
|
<box flexDirection="column" gap={0} paddingTop={1}>
|
|
1301
|
-
<text content="
|
|
1302
|
-
<text content="" />
|
|
1303
|
-
<text content="Navigation" fg={theme.secondary} />
|
|
1304
|
-
<text content=" ↑/k ↓/j Move selection" fg={theme.textSecondary} />
|
|
1305
|
-
<text content=" PgUp PgDn Jump 10" fg={theme.textSecondary} />
|
|
1306
|
-
<text content=" g / Home First" fg={theme.textSecondary} />
|
|
1307
|
-
<text content=" End Last" fg={theme.textSecondary} />
|
|
1308
|
-
<text content=" Tab / ` Cycle INTEL tabs" fg={theme.textSecondary} />
|
|
1632
|
+
<text content="GLOBAL SHORTCUTS" fg={theme.primary} />
|
|
1309
1633
|
<text content="" />
|
|
1310
|
-
<text content="
|
|
1311
|
-
<text content=" / i Enter domains" fg={theme.textSecondary} />
|
|
1634
|
+
<text content=" / i Scan domains" fg={theme.textSecondary} />
|
|
1312
1635
|
<text content=" f Load from file" fg={theme.textSecondary} />
|
|
1313
1636
|
<text content=" e TLD expansion" fg={theme.textSecondary} />
|
|
1314
|
-
<text content="
|
|
1315
|
-
<text content="" />
|
|
1316
|
-
<text content="
|
|
1317
|
-
<text content="
|
|
1318
|
-
<text content="
|
|
1319
|
-
<text content="
|
|
1320
|
-
<text content=" d Suggest similar domains" fg={theme.textSecondary} />
|
|
1321
|
-
<text content=" p Add to portfolio" fg={theme.textSecondary} />
|
|
1322
|
-
<text content=" S Snipe domain (auto-register when it drops)" fg={theme.textSecondary} />
|
|
1323
|
-
<text content=" D Drop catch (expired only)" fg={theme.textSecondary} />
|
|
1324
|
-
<text content=" c Clear cache for selected" fg={theme.textSecondary} />
|
|
1325
|
-
<text content=" h Show scan history" fg={theme.textSecondary} />
|
|
1326
|
-
<text content=" w Watch tagged (1h)" fg={theme.textSecondary} />
|
|
1327
|
-
<text content="" />
|
|
1328
|
-
<text content="Filter & Sort" fg={theme.secondary} />
|
|
1637
|
+
<text content=" d Suggest domains from keyword" fg={theme.textSecondary} />
|
|
1638
|
+
<text content=" Space Tag / untag" fg={theme.textSecondary} />
|
|
1639
|
+
<text content=" Tab Cycle intel tabs" fg={theme.textSecondary} />
|
|
1640
|
+
<text content=" n Toggle recon mode" fg={theme.textSecondary} />
|
|
1641
|
+
<text content=" b Portfolio dashboard" fg={theme.textSecondary} />
|
|
1642
|
+
<text content=" m Marketplace" fg={theme.textSecondary} />
|
|
1329
1643
|
<text content=" s Cycle status filter" fg={theme.textSecondary} />
|
|
1330
|
-
<text content=" o Cycle sort
|
|
1331
|
-
<text content="
|
|
1644
|
+
<text content=" o Cycle sort" fg={theme.textSecondary} />
|
|
1645
|
+
<text content=" g Toggle group view" fg={theme.textSecondary} />
|
|
1646
|
+
<text content=" q Quit" fg={theme.textSecondary} />
|
|
1647
|
+
<text content=" ? This help" fg={theme.textSecondary} />
|
|
1332
1648
|
<text content="" />
|
|
1333
|
-
<text content="
|
|
1334
|
-
<text content=" n Toggle recon mode" fg={theme.textSecondary} />
|
|
1335
|
-
<text content=" Enables port scan, WAF, headers," fg={theme.textDisabled} />
|
|
1336
|
-
<text content=" CORS, zone transfer, takeover detect" fg={theme.textDisabled} />
|
|
1337
|
-
<text content=" (rescan required after toggling)" fg={theme.textDisabled} />
|
|
1649
|
+
<text content="DOMAIN ACTIONS (Enter to open menu)" fg={theme.primary} />
|
|
1338
1650
|
<text content="" />
|
|
1339
|
-
<text content="
|
|
1340
|
-
<text content="
|
|
1341
|
-
<text content="
|
|
1342
|
-
<text content="
|
|
1343
|
-
<text content="
|
|
1344
|
-
<text content="
|
|
1345
|
-
<text content="
|
|
1346
|
-
<text content="
|
|
1651
|
+
<text content=" r Register" fg={theme.textSecondary} />
|
|
1652
|
+
<text content=" z Snipe" fg={theme.textSecondary} />
|
|
1653
|
+
<text content=" p Add to portfolio" fg={theme.textSecondary} />
|
|
1654
|
+
<text content=" d Suggest similar" fg={theme.textSecondary} />
|
|
1655
|
+
<text content=" v Variations" fg={theme.textSecondary} />
|
|
1656
|
+
<text content=" w Tag for watch" fg={theme.textSecondary} />
|
|
1657
|
+
<text content=" h History timeline (with deltas)" fg={theme.textSecondary} />
|
|
1658
|
+
<text content=" c Clear cache" fg={theme.textSecondary} />
|
|
1659
|
+
<text content=" x Export" fg={theme.textSecondary} />
|
|
1660
|
+
<text content=" Enter Rescan" fg={theme.textSecondary} />
|
|
1347
1661
|
<text content="" />
|
|
1348
|
-
<text content="
|
|
1349
|
-
<text content="
|
|
1350
|
-
<text content="
|
|
1662
|
+
<text content="Navigation" fg={theme.secondary} />
|
|
1663
|
+
<text content=" ↑/k ↓/j Move selection" fg={theme.textSecondary} />
|
|
1664
|
+
<text content=" PgUp PgDn Jump 10" fg={theme.textSecondary} />
|
|
1665
|
+
<text content=" Home First End Last" fg={theme.textSecondary} />
|
|
1351
1666
|
<text content="" />
|
|
1352
1667
|
<text content="Session" fg={theme.secondary} />
|
|
1353
1668
|
<text content=" Ctrl+S Save session" fg={theme.textSecondary} />
|
|
1354
1669
|
<text content=" Ctrl+L Load session" fg={theme.textSecondary} />
|
|
1355
|
-
<text content="
|
|
1670
|
+
<text content=" a Bulk register tagged (2x confirm)" fg={theme.textSecondary} />
|
|
1356
1671
|
<text content="" />
|
|
1357
|
-
<text content="
|
|
1358
|
-
<text content="
|
|
1672
|
+
<text content="Marketplace (when open)" fg={theme.secondary} />
|
|
1673
|
+
<text content=" / 1 2 3 Search / Browse / Mine / Offers" fg={theme.textSecondary} />
|
|
1674
|
+
<text content=" l List domain for sale" fg={theme.textSecondary} />
|
|
1675
|
+
<text content=" o Make offer (detail view)" fg={theme.textSecondary} />
|
|
1359
1676
|
</box>
|
|
1360
1677
|
</scrollbox>
|
|
1678
|
+
) : showHistory && selected ? (
|
|
1679
|
+
<scrollbox flexGrow={1} paddingLeft={1} paddingRight={1} minHeight={0} scrollbarOptions={{ visible: true }}>
|
|
1680
|
+
{(() => {
|
|
1681
|
+
const timeline = getTimeline(selected.domain, 20);
|
|
1682
|
+
const maxIdx = Math.max(0, timeline.entries.length - 1);
|
|
1683
|
+
const safeIdx = Math.min(historyIndex, maxIdx);
|
|
1684
|
+
|
|
1685
|
+
return (
|
|
1686
|
+
<box flexDirection="column" gap={0} paddingTop={1}>
|
|
1687
|
+
{/* Header */}
|
|
1688
|
+
<box flexDirection="row" gap={2} paddingLeft={1}>
|
|
1689
|
+
<text content="HISTORY" fg={theme.secondary} />
|
|
1690
|
+
<text content={selected.domain} fg={theme.text} />
|
|
1691
|
+
<text content={`(${timeline.totalScans} scans)`} fg={theme.textDisabled} />
|
|
1692
|
+
</box>
|
|
1693
|
+
|
|
1694
|
+
{timeline.firstSeen && (
|
|
1695
|
+
<box flexDirection="row" gap={2} paddingLeft={1}>
|
|
1696
|
+
<text content={`First: ${timeline.firstSeen}`} fg={theme.textDisabled} />
|
|
1697
|
+
<text content={`Latest: ${timeline.lastSeen}`} fg={theme.textDisabled} />
|
|
1698
|
+
</box>
|
|
1699
|
+
)}
|
|
1700
|
+
|
|
1701
|
+
<text content="" />
|
|
1702
|
+
|
|
1703
|
+
{/* Status changes summary */}
|
|
1704
|
+
{timeline.statusChanges.length > 0 && (
|
|
1705
|
+
<box flexDirection="column" paddingLeft={1}>
|
|
1706
|
+
<text content="STATUS CHANGES" fg={theme.warning} />
|
|
1707
|
+
{timeline.statusChanges.map((sc, i) => (
|
|
1708
|
+
<box key={i} flexDirection="row" gap={1} paddingLeft={1}>
|
|
1709
|
+
<text content={sc.from} fg={theme.error} />
|
|
1710
|
+
<text content={"\u2192"} fg={theme.textDisabled} />
|
|
1711
|
+
<text content={sc.to} fg={theme.primary} />
|
|
1712
|
+
<text content={sc.at} fg={theme.textDisabled} />
|
|
1713
|
+
</box>
|
|
1714
|
+
))}
|
|
1715
|
+
<text content="" />
|
|
1716
|
+
</box>
|
|
1717
|
+
)}
|
|
1718
|
+
|
|
1719
|
+
{/* Timeline entries */}
|
|
1720
|
+
<text content=" SCANS (Enter to load, Esc to close)" fg={theme.textMuted} paddingLeft={1} />
|
|
1721
|
+
<text content="" />
|
|
1722
|
+
|
|
1723
|
+
{timeline.entries.length === 0 ? (
|
|
1724
|
+
<box paddingLeft={2} paddingTop={1}>
|
|
1725
|
+
<text content="No scan history for this domain" fg={theme.textDisabled} />
|
|
1726
|
+
<text content="Scan this domain first to build history" fg={theme.textDisabled} />
|
|
1727
|
+
</box>
|
|
1728
|
+
) : (
|
|
1729
|
+
timeline.entries.map((entry, i) => {
|
|
1730
|
+
const active = safeIdx === i;
|
|
1731
|
+
const statusColor = entry.status === "available" ? theme.primary : entry.status === "expired" ? theme.warning : entry.status === "error" ? theme.error : theme.textSecondary;
|
|
1732
|
+
const deltas = i < timeline.deltas.length ? timeline.deltas[i]! : [];
|
|
1733
|
+
|
|
1734
|
+
return (
|
|
1735
|
+
<box key={entry.id} flexDirection="column">
|
|
1736
|
+
{/* Scan entry row */}
|
|
1737
|
+
<box flexDirection="row" gap={1} paddingLeft={1} backgroundColor={active ? theme.primaryDim : "transparent"}>
|
|
1738
|
+
<text content={active ? "\u25b8" : " "} fg={theme.primary} />
|
|
1739
|
+
<text content={entry.scannedAt} fg={active ? theme.text : theme.textDisabled} />
|
|
1740
|
+
<text content={entry.status} fg={statusColor} />
|
|
1741
|
+
{entry.score !== null && <text content={`(${entry.score})`} fg={theme.textDisabled} />}
|
|
1742
|
+
</box>
|
|
1743
|
+
|
|
1744
|
+
{/* Deltas (changes from this scan to the next) */}
|
|
1745
|
+
{deltas.length > 0 && (
|
|
1746
|
+
<box flexDirection="column" paddingLeft={3}>
|
|
1747
|
+
{deltas.map((d, j) => (
|
|
1748
|
+
<box key={j} flexDirection="row" gap={1}>
|
|
1749
|
+
<text content={d.severity === "critical" ? "!!" : d.severity === "warning" ? " !" : " "} fg={d.severity === "critical" ? theme.error : d.severity === "warning" ? theme.warning : theme.textDisabled} />
|
|
1750
|
+
<text content={d.field} fg={theme.textMuted} />
|
|
1751
|
+
<text content={d.from} fg={theme.error} />
|
|
1752
|
+
<text content={"\u2192"} fg={theme.textDisabled} />
|
|
1753
|
+
<text content={d.to} fg={theme.primary} />
|
|
1754
|
+
</box>
|
|
1755
|
+
))}
|
|
1756
|
+
</box>
|
|
1757
|
+
)}
|
|
1758
|
+
</box>
|
|
1759
|
+
);
|
|
1760
|
+
})
|
|
1761
|
+
)}
|
|
1762
|
+
</box>
|
|
1763
|
+
);
|
|
1764
|
+
})()}
|
|
1765
|
+
</scrollbox>
|
|
1766
|
+
) : showActionMenu && selected ? (
|
|
1767
|
+
<box flexGrow={1} paddingLeft={2} paddingTop={1} minHeight={0} flexDirection="column">
|
|
1768
|
+
{/* Menu header */}
|
|
1769
|
+
<box flexDirection="row" gap={1}>
|
|
1770
|
+
<text content={selected.domain} fg={theme.text} />
|
|
1771
|
+
<text content={`(${statusStyle(selected.status).label})`} fg={statusStyle(selected.status).fg} />
|
|
1772
|
+
</box>
|
|
1773
|
+
<text content="" />
|
|
1774
|
+
|
|
1775
|
+
{/* Available actions — context-dependent */}
|
|
1776
|
+
{(selected.status === "available" || selected.status === "expired") && registrarConfig?.apiKey && (
|
|
1777
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1778
|
+
<box backgroundColor={theme.primaryDim}><text content=" r " fg={theme.primary} /></box>
|
|
1779
|
+
<text content="Register this domain" fg={theme.textSecondary} />
|
|
1780
|
+
</box>
|
|
1781
|
+
)}
|
|
1782
|
+
|
|
1783
|
+
{(selected.status === "taken" || selected.status === "expired") && (
|
|
1784
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1785
|
+
<box backgroundColor={theme.warningDim}><text content=" z " fg={theme.warning} /></box>
|
|
1786
|
+
<text content="Snipe (auto-register when it drops)" fg={theme.textSecondary} />
|
|
1787
|
+
</box>
|
|
1788
|
+
)}
|
|
1789
|
+
|
|
1790
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1791
|
+
<box backgroundColor={theme.secondaryDim}><text content=" p " fg={theme.secondary} /></box>
|
|
1792
|
+
<text content="Add to portfolio" fg={theme.textSecondary} />
|
|
1793
|
+
</box>
|
|
1794
|
+
|
|
1795
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1796
|
+
<box backgroundColor={theme.infoDim}><text content=" d " fg={theme.info} /></box>
|
|
1797
|
+
<text content="Suggest similar domains" fg={theme.textSecondary} />
|
|
1798
|
+
</box>
|
|
1799
|
+
|
|
1800
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1801
|
+
<box backgroundColor={theme.infoDim}><text content=" v " fg={theme.info} /></box>
|
|
1802
|
+
<text content="Generate variations" fg={theme.textSecondary} />
|
|
1803
|
+
</box>
|
|
1804
|
+
|
|
1805
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1806
|
+
<box backgroundColor={theme.infoDim}><text content=" e " fg={theme.info} /></box>
|
|
1807
|
+
<text content="Check other TLDs" fg={theme.textSecondary} />
|
|
1808
|
+
</box>
|
|
1809
|
+
|
|
1810
|
+
<text content="" />
|
|
1811
|
+
|
|
1812
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1813
|
+
<box backgroundColor={theme.textDisabled}><text content=" w " fg={theme.text} /></box>
|
|
1814
|
+
<text content="Tag for watching" fg={theme.textSecondary} />
|
|
1815
|
+
</box>
|
|
1816
|
+
|
|
1817
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1818
|
+
<box backgroundColor={theme.textDisabled}><text content=" h " fg={theme.text} /></box>
|
|
1819
|
+
<text content="History timeline (with deltas)" fg={theme.textSecondary} />
|
|
1820
|
+
</box>
|
|
1821
|
+
|
|
1822
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1823
|
+
<box backgroundColor={theme.textDisabled}><text content=" l " fg={theme.text} /></box>
|
|
1824
|
+
<text content="Load previous scan" fg={theme.textSecondary} />
|
|
1825
|
+
</box>
|
|
1826
|
+
|
|
1827
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1828
|
+
<box backgroundColor={theme.textDisabled}><text content=" c " fg={theme.text} /></box>
|
|
1829
|
+
<text content="Clear cache" fg={theme.textSecondary} />
|
|
1830
|
+
</box>
|
|
1831
|
+
|
|
1832
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1833
|
+
<box backgroundColor={theme.textDisabled}><text content=" x " fg={theme.text} /></box>
|
|
1834
|
+
<text content="Export results" fg={theme.textSecondary} />
|
|
1835
|
+
</box>
|
|
1836
|
+
|
|
1837
|
+
<text content="" />
|
|
1838
|
+
|
|
1839
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
1840
|
+
<box backgroundColor={theme.primaryDim}><text content=" ⏎ " fg={theme.primary} /></box>
|
|
1841
|
+
<text content="Rescan domain" fg={theme.textSecondary} />
|
|
1842
|
+
</box>
|
|
1843
|
+
|
|
1844
|
+
<text content="" />
|
|
1845
|
+
<text content=" Press a key to act, Esc to cancel" fg={theme.textDisabled} />
|
|
1846
|
+
</box>
|
|
1361
1847
|
) : selected ? (
|
|
1362
1848
|
<scrollbox flexGrow={1} paddingLeft={1} paddingRight={1} minHeight={0} scrollbarOptions={{ visible: true }}>
|
|
1363
1849
|
<box flexDirection="column" gap={0} paddingRight={1}>
|
|
@@ -1405,15 +1891,15 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
1405
1891
|
<box paddingLeft={2} paddingTop={0} flexShrink={0}>
|
|
1406
1892
|
<text content={
|
|
1407
1893
|
selected.status === "available" && registrarConfig?.apiKey
|
|
1408
|
-
? "\u2192 Press r to register | p to add to portfolio |
|
|
1894
|
+
? "\u2192 Press r to register | p to add to portfolio | m then l to list for sale"
|
|
1409
1895
|
: selected.status === "available"
|
|
1410
|
-
? "\u2192 Press p to add to portfolio |
|
|
1896
|
+
? "\u2192 Press p to add to portfolio | m then l to list for sale"
|
|
1411
1897
|
: selected.status === "expired"
|
|
1412
|
-
? "\u2192 Press
|
|
1898
|
+
? "\u2192 Press z to snipe (auto drop catch) | w to watch for availability"
|
|
1413
1899
|
: selected.status === "taken"
|
|
1414
1900
|
? "\u2192 Press d for alternatives | v for variations | Tab for more intel"
|
|
1415
1901
|
: selected.status === "registered"
|
|
1416
|
-
? "\u2192 Press p to add to portfolio |
|
|
1902
|
+
? "\u2192 Press p to add to portfolio | m then l to list for sale"
|
|
1417
1903
|
: selected.status === "error"
|
|
1418
1904
|
? "\u2192 Press c to clear cache and rescan"
|
|
1419
1905
|
: ""
|
|
@@ -1880,7 +2366,7 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
1880
2366
|
<text content="" />
|
|
1881
2367
|
<box flexDirection="row" gap={1} justifyContent="center">
|
|
1882
2368
|
<text content="◆" fg={theme.primary} />
|
|
1883
|
-
<text content="DOMAIN SNIPER v0.
|
|
2369
|
+
<text content="DOMAIN SNIPER v0.2.0" fg={theme.primary} />
|
|
1884
2370
|
</box>
|
|
1885
2371
|
<box justifyContent="center">
|
|
1886
2372
|
<text content="Domain Intelligence & Security Recon" fg={theme.textDisabled} />
|
|
@@ -1977,8 +2463,8 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
1977
2463
|
<box flexDirection="row" gap={1}><box backgroundColor={theme.primaryDim}><text content=" ? " fg={theme.primary} /></box><text content="help" fg={theme.textSecondary} /></box>
|
|
1978
2464
|
</box>
|
|
1979
2465
|
<box flexDirection="row" justifyContent="center" gap={3}>
|
|
1980
|
-
<box flexDirection="row" gap={1}><box backgroundColor={theme.secondaryDim}><text content="
|
|
1981
|
-
<box flexDirection="row" gap={1}><box backgroundColor={theme.secondaryDim}><text content="
|
|
2466
|
+
<box flexDirection="row" gap={1}><box backgroundColor={theme.secondaryDim}><text content=" m " fg={theme.secondary} /></box><text content="market" fg={theme.textSecondary} /></box>
|
|
2467
|
+
<box flexDirection="row" gap={1}><box backgroundColor={theme.secondaryDim}><text content=" b " fg={theme.secondary} /></box><text content="portfolio" fg={theme.textSecondary} /></box>
|
|
1982
2468
|
<box flexDirection="row" gap={1}><box backgroundColor={theme.accentDim}><text content=" n " fg={theme.accent} /></box><text content="recon" fg={theme.textSecondary} /></box>
|
|
1983
2469
|
<box flexDirection="row" gap={1}><box backgroundColor={theme.errorDim}><text content=" q " fg={theme.error} /></box><text content="quit" fg={theme.textSecondary} /></box>
|
|
1984
2470
|
</box>
|
|
@@ -2022,43 +2508,56 @@ export function App({ initialDomains, batchFile, autoRegister = false }: AppProp
|
|
|
2022
2508
|
<text content={dLine(width)} fg={theme.border} paddingLeft={1} />
|
|
2023
2509
|
<box flexDirection="row" justifyContent="space-between" paddingLeft={1} paddingRight={1}>
|
|
2024
2510
|
<box flexDirection="row" gap={1}>
|
|
2025
|
-
{
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
<text content={h.label} fg={h.key === "n" && reconMode ? theme.error : h.key === "P" && showPortfolio ? theme.primary : h.key === "M" && showMarket ? theme.secondary : theme.textMuted} />
|
|
2049
|
-
</box>
|
|
2050
|
-
))}
|
|
2051
|
-
</>
|
|
2052
|
-
);
|
|
2053
|
-
})()
|
|
2054
|
-
) : (
|
|
2511
|
+
{showHistory ? (
|
|
2512
|
+
<>
|
|
2513
|
+
<text content={"\u2191\u2193 navigate"} fg={theme.textMuted} />
|
|
2514
|
+
<box backgroundColor={theme.primaryDim}><text content={" \u23ce "} fg={theme.primary} /></box><text content="load scan" fg={theme.textMuted} />
|
|
2515
|
+
<box backgroundColor={theme.textDisabled}><text content=" esc " fg={theme.background} /></box><text content="close" fg={theme.textMuted} />
|
|
2516
|
+
</>
|
|
2517
|
+
) : showActionMenu ? (
|
|
2518
|
+
<>
|
|
2519
|
+
<text content="Press a key or" fg={theme.textMuted} />
|
|
2520
|
+
<box backgroundColor={theme.textDisabled}><text content=" esc " fg={theme.background} /></box>
|
|
2521
|
+
<text content="cancel" fg={theme.textMuted} />
|
|
2522
|
+
</>
|
|
2523
|
+
) : showMarket ? (
|
|
2524
|
+
<>
|
|
2525
|
+
<box backgroundColor={theme.textDisabled}><text content=" 1 " fg={theme.background} /></box><text content="browse" fg={theme.textMuted} />
|
|
2526
|
+
<box backgroundColor={theme.textDisabled}><text content=" / " fg={theme.background} /></box><text content="search" fg={theme.textMuted} />
|
|
2527
|
+
<box backgroundColor={theme.textDisabled}><text content=" m " fg={theme.background} /></box><text content="close" fg={theme.textMuted} />
|
|
2528
|
+
</>
|
|
2529
|
+
) : showPortfolio ? (
|
|
2530
|
+
<>
|
|
2531
|
+
<box backgroundColor={theme.textDisabled}><text content=" b " fg={theme.background} /></box><text content="close" fg={theme.textMuted} />
|
|
2532
|
+
</>
|
|
2533
|
+
) : mode === "input" ? (
|
|
2055
2534
|
<>
|
|
2056
2535
|
<box backgroundColor={theme.textDisabled}><text content=" ⏎ " fg={theme.background} /></box><text content="submit" fg={theme.textMuted} />
|
|
2057
2536
|
<box backgroundColor={theme.textDisabled}><text content=" esc " fg={theme.background} /></box><text content="cancel" fg={theme.textMuted} />
|
|
2058
2537
|
</>
|
|
2538
|
+
) : mode === "idle" ? (
|
|
2539
|
+
<>
|
|
2540
|
+
<box backgroundColor={theme.primaryDim} onMouseDown={() => { setInputMode("domain"); setMode("input"); setInputValue(""); }}><text content=" / " fg={theme.primary} /></box><text content="scan" fg={theme.textMuted} />
|
|
2541
|
+
<box backgroundColor={theme.primaryDim} onMouseDown={() => { setInputMode("expand"); setMode("input"); setInputValue(""); }}><text content=" e " fg={theme.primary} /></box><text content="expand" fg={theme.textMuted} />
|
|
2542
|
+
<box backgroundColor={theme.primaryDim} onMouseDown={() => { setInputMode("file"); setMode("input"); setInputValue(""); }}><text content=" f " fg={theme.primary} /></box><text content="file" fg={theme.textMuted} />
|
|
2543
|
+
<box backgroundColor={theme.textDisabled} onMouseDown={() => setShowHelp(true)}><text content=" ? " fg={theme.background} /></box><text content="help" fg={theme.textMuted} />
|
|
2544
|
+
</>
|
|
2545
|
+
) : selected ? (
|
|
2546
|
+
<>
|
|
2547
|
+
<box backgroundColor={theme.primaryDim} onMouseDown={() => setShowActionMenu(true)}><text content=" ⏎ " fg={theme.primary} /></box><text content="actions" fg={theme.textMuted} />
|
|
2548
|
+
<box backgroundColor={theme.textDisabled}><text content=" ␣ " fg={theme.background} /></box><text content="tag" fg={theme.textMuted} />
|
|
2549
|
+
<box backgroundColor={theme.textDisabled} onMouseDown={() => { setInputMode("domain"); setMode("input"); setInputValue(""); }}><text content=" / " fg={theme.background} /></box><text content="scan" fg={theme.textMuted} />
|
|
2550
|
+
<box backgroundColor={theme.textDisabled}><text content=" Tab " fg={theme.background} /></box><text content="tabs" fg={theme.textMuted} />
|
|
2551
|
+
<box backgroundColor={theme.textDisabled} onMouseDown={() => setShowHelp(true)}><text content=" ? " fg={theme.background} /></box><text content="all" fg={theme.textMuted} />
|
|
2552
|
+
</>
|
|
2553
|
+
) : (
|
|
2554
|
+
<>
|
|
2555
|
+
<box backgroundColor={theme.textDisabled}><text content=" / " fg={theme.background} /></box><text content="scan" fg={theme.textMuted} />
|
|
2556
|
+
<box backgroundColor={theme.textDisabled}><text content=" ? " fg={theme.background} /></box><text content="help" fg={theme.textMuted} />
|
|
2557
|
+
</>
|
|
2059
2558
|
)}
|
|
2060
2559
|
</box>
|
|
2061
|
-
<text content={stats.total > 0 ? `${stats.available + stats.expired}/${stats.total} actionable` : "v0.
|
|
2560
|
+
<text content={stats.total > 0 ? `${stats.available + stats.expired}/${stats.total} actionable` : "v0.2.0"} fg={theme.textDisabled} />
|
|
2062
2561
|
</box>
|
|
2063
2562
|
</box>
|
|
2064
2563
|
</box>
|