recker 1.0.31 → 1.0.32-next.e0741bf
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/dist/cli/index.js +2350 -43
- package/dist/cli/tui/shell-search.js +10 -8
- package/dist/cli/tui/shell.d.ts +29 -0
- package/dist/cli/tui/shell.js +1733 -9
- package/dist/mcp/search/hybrid-search.js +4 -2
- package/dist/seo/analyzer.d.ts +7 -0
- package/dist/seo/analyzer.js +200 -4
- package/dist/seo/rules/ai-search.d.ts +2 -0
- package/dist/seo/rules/ai-search.js +423 -0
- package/dist/seo/rules/canonical.d.ts +12 -0
- package/dist/seo/rules/canonical.js +249 -0
- package/dist/seo/rules/crawl.js +113 -0
- package/dist/seo/rules/cwv.js +0 -95
- package/dist/seo/rules/i18n.js +27 -0
- package/dist/seo/rules/images.js +23 -27
- package/dist/seo/rules/index.js +14 -0
- package/dist/seo/rules/internal-linking.js +6 -6
- package/dist/seo/rules/links.js +321 -0
- package/dist/seo/rules/meta.js +24 -0
- package/dist/seo/rules/mobile.js +0 -20
- package/dist/seo/rules/performance.js +124 -0
- package/dist/seo/rules/redirects.d.ts +16 -0
- package/dist/seo/rules/redirects.js +193 -0
- package/dist/seo/rules/resources.d.ts +2 -0
- package/dist/seo/rules/resources.js +373 -0
- package/dist/seo/rules/security.js +290 -0
- package/dist/seo/rules/technical-advanced.d.ts +10 -0
- package/dist/seo/rules/technical-advanced.js +283 -0
- package/dist/seo/rules/technical.js +74 -18
- package/dist/seo/rules/types.d.ts +103 -3
- package/dist/seo/seo-spider.d.ts +2 -0
- package/dist/seo/seo-spider.js +47 -2
- package/dist/seo/types.d.ts +48 -28
- package/dist/seo/utils/index.d.ts +1 -0
- package/dist/seo/utils/index.js +1 -0
- package/dist/seo/utils/similarity.d.ts +47 -0
- package/dist/seo/utils/similarity.js +273 -0
- package/dist/seo/validators/index.d.ts +3 -0
- package/dist/seo/validators/index.js +3 -0
- package/dist/seo/validators/llms-txt.d.ts +57 -0
- package/dist/seo/validators/llms-txt.js +317 -0
- package/dist/seo/validators/robots.d.ts +54 -0
- package/dist/seo/validators/robots.js +382 -0
- package/dist/seo/validators/sitemap.d.ts +69 -0
- package/dist/seo/validators/sitemap.js +424 -0
- package/package.json +1 -1
package/dist/cli/tui/shell.js
CHANGED
|
@@ -98,7 +98,8 @@ export class RekShell {
|
|
|
98
98
|
'ws', 'udp', 'load', 'chat', 'ai',
|
|
99
99
|
'@openai', '@anthropic', '@groq', '@google', '@xai', '@mistral', '@cohere', '@deepseek', '@fireworks', '@together', '@perplexity',
|
|
100
100
|
'ai:clear',
|
|
101
|
-
'whois', 'tls', 'ssl', 'security', 'ip', 'dns', 'dns:propagate', 'dns:email', 'rdap', 'ping',
|
|
101
|
+
'whois', 'tls', 'ssl', 'security', 'ip', 'dns', 'dns:propagate', 'dns:email', 'dns:health', 'dns:spf', 'dns:dmarc', 'dns:dkim', 'dns:dig', 'dns:generate', 'rdap', 'ping', 'ftp', 'sftp', 'telnet', 'graphql', 'jsonrpc', 'hls', 'har', 'har:record', 'har:play', 'har:info', 'har:stop',
|
|
102
|
+
'robots', 'sitemap', 'llms', 'sse', 'upload', 'download', 'soap', 'odata', 'proxy',
|
|
102
103
|
'scrap', 'spider', 'seo', '$', '$text', '$attr', '$html', '$links', '$images', '$scripts', '$css', '$sourcemaps', '$unmap', '$unmap:view', '$unmap:save', '$beautify', '$beautify:save', '$table',
|
|
103
104
|
'?', 'search', 'suggest', 'example',
|
|
104
105
|
'help', 'clear', 'exit', 'set', 'url', 'vars', 'env'
|
|
@@ -405,12 +406,90 @@ export class RekShell {
|
|
|
405
406
|
case 'dns:email':
|
|
406
407
|
await this.runDnsEmailCheck(parts[1], parts[2]);
|
|
407
408
|
return;
|
|
409
|
+
case 'dns:health':
|
|
410
|
+
await this.runDnsHealth(parts[1]);
|
|
411
|
+
return;
|
|
412
|
+
case 'dns:spf':
|
|
413
|
+
await this.runDnsSpf(parts[1]);
|
|
414
|
+
return;
|
|
415
|
+
case 'dns:dmarc':
|
|
416
|
+
await this.runDnsDmarc(parts[1]);
|
|
417
|
+
return;
|
|
418
|
+
case 'dns:dkim':
|
|
419
|
+
await this.runDnsDkim(parts[1], parts[2]);
|
|
420
|
+
return;
|
|
421
|
+
case 'dns:dig':
|
|
422
|
+
await this.runDnsDig(parts.slice(1));
|
|
423
|
+
return;
|
|
424
|
+
case 'dns:generate':
|
|
425
|
+
await this.runDnsGenerate(parts.slice(1));
|
|
426
|
+
return;
|
|
408
427
|
case 'rdap':
|
|
409
428
|
await this.runRDAP(parts[1]);
|
|
410
429
|
return;
|
|
411
430
|
case 'ping':
|
|
412
431
|
await this.runPing(parts[1]);
|
|
413
432
|
return;
|
|
433
|
+
case 'ftp':
|
|
434
|
+
await this.runFtp(parts.slice(1));
|
|
435
|
+
return;
|
|
436
|
+
case 'telnet':
|
|
437
|
+
await this.runTelnet(parts[1], parts[2]);
|
|
438
|
+
return;
|
|
439
|
+
case 'graphql':
|
|
440
|
+
await this.runGraphQL(parts.slice(1));
|
|
441
|
+
return;
|
|
442
|
+
case 'jsonrpc':
|
|
443
|
+
await this.runJsonRpc(parts.slice(1));
|
|
444
|
+
return;
|
|
445
|
+
case 'hls':
|
|
446
|
+
await this.runHls(parts.slice(1));
|
|
447
|
+
return;
|
|
448
|
+
case 'har':
|
|
449
|
+
await this.runHar(parts.slice(1));
|
|
450
|
+
return;
|
|
451
|
+
case 'har:record':
|
|
452
|
+
await this.runHarRecord(parts.slice(1));
|
|
453
|
+
return;
|
|
454
|
+
case 'har:play':
|
|
455
|
+
await this.runHarPlay(parts.slice(1));
|
|
456
|
+
return;
|
|
457
|
+
case 'har:info':
|
|
458
|
+
await this.runHarInfo(parts[1]);
|
|
459
|
+
return;
|
|
460
|
+
case 'har:stop':
|
|
461
|
+
await this.runHarStop();
|
|
462
|
+
return;
|
|
463
|
+
case 'robots':
|
|
464
|
+
await this.runRobots(parts[1]);
|
|
465
|
+
return;
|
|
466
|
+
case 'sitemap':
|
|
467
|
+
await this.runSitemap(parts[1]);
|
|
468
|
+
return;
|
|
469
|
+
case 'llms':
|
|
470
|
+
await this.runLlms(parts[1]);
|
|
471
|
+
return;
|
|
472
|
+
case 'sftp':
|
|
473
|
+
await this.runSftp(parts.slice(1));
|
|
474
|
+
return;
|
|
475
|
+
case 'sse':
|
|
476
|
+
await this.runSse(parts[1], parts.slice(2));
|
|
477
|
+
return;
|
|
478
|
+
case 'upload':
|
|
479
|
+
await this.runUpload(parts.slice(1));
|
|
480
|
+
return;
|
|
481
|
+
case 'download':
|
|
482
|
+
await this.runDownload(parts.slice(1));
|
|
483
|
+
return;
|
|
484
|
+
case 'soap':
|
|
485
|
+
await this.runSoap(parts.slice(1));
|
|
486
|
+
return;
|
|
487
|
+
case 'odata':
|
|
488
|
+
await this.runOdata(parts.slice(1));
|
|
489
|
+
return;
|
|
490
|
+
case 'proxy':
|
|
491
|
+
await this.runProxy(parts.slice(1));
|
|
492
|
+
return;
|
|
414
493
|
case 'scrap':
|
|
415
494
|
await this.runScrap(parts[1]);
|
|
416
495
|
return;
|
|
@@ -1136,7 +1215,7 @@ ${colors.bold('Details:')}`);
|
|
|
1136
1215
|
openGraph: report.openGraph,
|
|
1137
1216
|
twitterCard: report.twitterCard,
|
|
1138
1217
|
social: report.social,
|
|
1139
|
-
|
|
1218
|
+
structuredData: report.structuredData,
|
|
1140
1219
|
technical: report.technical,
|
|
1141
1220
|
checks: report.checks,
|
|
1142
1221
|
summary: {
|
|
@@ -1478,6 +1557,332 @@ ${colors.bold('Network:')}
|
|
|
1478
1557
|
console.error(colors.red(`Email security check failed: ${error.message}`));
|
|
1479
1558
|
}
|
|
1480
1559
|
}
|
|
1560
|
+
async runDnsHealth(domain) {
|
|
1561
|
+
if (!domain) {
|
|
1562
|
+
domain = this.getBaseDomain() || '';
|
|
1563
|
+
if (!domain) {
|
|
1564
|
+
console.log(colors.yellow('Usage: dns:health <domain>'));
|
|
1565
|
+
console.log(colors.gray(' Example: dns:health google.com'));
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
console.log(colors.gray(`Checking DNS health for ${domain}...`));
|
|
1570
|
+
const startTime = performance.now();
|
|
1571
|
+
try {
|
|
1572
|
+
const { checkDnsHealth } = await import('../../utils/dns-toolkit.js');
|
|
1573
|
+
const result = await checkDnsHealth(domain);
|
|
1574
|
+
const duration = Math.round(performance.now() - startTime);
|
|
1575
|
+
console.log(colors.green(`✔ DNS health check completed`) + colors.gray(` (${duration}ms)\n`));
|
|
1576
|
+
const gradeColor = result.grade === 'A' ? colors.green :
|
|
1577
|
+
result.grade === 'B' ? colors.cyan :
|
|
1578
|
+
result.grade === 'C' ? colors.yellow : colors.red;
|
|
1579
|
+
console.log(`${colors.bold('DNS Health Report')}`);
|
|
1580
|
+
console.log(` ${colors.gray('Grade:')} ${gradeColor(result.grade)} (${result.score}/100)`);
|
|
1581
|
+
console.log(` ${colors.gray('Checks:')} ${result.checks?.filter((c) => c.passed).length || 0} passed, ${result.checks?.filter((c) => !c.passed).length || 0} failed`);
|
|
1582
|
+
if (result.checks) {
|
|
1583
|
+
console.log('');
|
|
1584
|
+
result.checks.forEach((check) => {
|
|
1585
|
+
const icon = check.passed ? colors.green('✔') : colors.red('✖');
|
|
1586
|
+
console.log(` ${icon} ${check.name}: ${check.message || (check.passed ? 'OK' : 'Failed')}`);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
console.log('');
|
|
1590
|
+
this.lastResponse = result;
|
|
1591
|
+
}
|
|
1592
|
+
catch (error) {
|
|
1593
|
+
console.error(colors.red(`DNS health check failed: ${error.message}`));
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async runDnsSpf(domain) {
|
|
1597
|
+
if (!domain) {
|
|
1598
|
+
domain = this.getBaseDomain() || '';
|
|
1599
|
+
if (!domain) {
|
|
1600
|
+
console.log(colors.yellow('Usage: dns:spf <domain>'));
|
|
1601
|
+
console.log(colors.gray(' Example: dns:spf google.com'));
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
console.log(colors.gray(`Validating SPF for ${domain}...`));
|
|
1606
|
+
try {
|
|
1607
|
+
const { validateSpf } = await import('../../utils/dns-toolkit.js');
|
|
1608
|
+
const result = await validateSpf(domain);
|
|
1609
|
+
console.log('');
|
|
1610
|
+
console.log(colors.bold('SPF Validation'));
|
|
1611
|
+
if (result.valid) {
|
|
1612
|
+
console.log(` ${colors.green('✔')} Valid SPF record`);
|
|
1613
|
+
}
|
|
1614
|
+
else {
|
|
1615
|
+
console.log(` ${colors.red('✖')} Invalid SPF record`);
|
|
1616
|
+
}
|
|
1617
|
+
if (result.record) {
|
|
1618
|
+
console.log(` ${colors.gray('Record:')} ${result.record}`);
|
|
1619
|
+
}
|
|
1620
|
+
if (result.lookupCount !== undefined) {
|
|
1621
|
+
const lookupColor = result.lookupCount > 10 ? colors.red : result.lookupCount > 7 ? colors.yellow : colors.green;
|
|
1622
|
+
console.log(` ${colors.gray('DNS Lookups:')} ${lookupColor(result.lookupCount.toString())}/10`);
|
|
1623
|
+
}
|
|
1624
|
+
if (result.mechanisms && result.mechanisms.length > 0) {
|
|
1625
|
+
console.log(` ${colors.gray('Mechanisms:')} ${result.mechanisms.join(', ')}`);
|
|
1626
|
+
}
|
|
1627
|
+
if (result.includes && result.includes.length > 0) {
|
|
1628
|
+
console.log(` ${colors.gray('Includes:')} ${result.includes.join(', ')}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
1631
|
+
console.log('');
|
|
1632
|
+
result.warnings.forEach((w) => console.log(` ${colors.yellow('⚠')} ${w}`));
|
|
1633
|
+
}
|
|
1634
|
+
if (result.errors && result.errors.length > 0) {
|
|
1635
|
+
console.log('');
|
|
1636
|
+
result.errors.forEach((e) => console.log(` ${colors.red('✖')} ${e}`));
|
|
1637
|
+
}
|
|
1638
|
+
console.log('');
|
|
1639
|
+
this.lastResponse = result;
|
|
1640
|
+
}
|
|
1641
|
+
catch (error) {
|
|
1642
|
+
console.error(colors.red(`SPF validation failed: ${error.message}`));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
async runDnsDmarc(domain) {
|
|
1646
|
+
if (!domain) {
|
|
1647
|
+
domain = this.getBaseDomain() || '';
|
|
1648
|
+
if (!domain) {
|
|
1649
|
+
console.log(colors.yellow('Usage: dns:dmarc <domain>'));
|
|
1650
|
+
console.log(colors.gray(' Example: dns:dmarc google.com'));
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
console.log(colors.gray(`Validating DMARC for ${domain}...`));
|
|
1655
|
+
try {
|
|
1656
|
+
const { validateDmarc } = await import('../../utils/dns-toolkit.js');
|
|
1657
|
+
const result = await validateDmarc(domain);
|
|
1658
|
+
console.log('');
|
|
1659
|
+
console.log(colors.bold('DMARC Validation'));
|
|
1660
|
+
if (result.valid) {
|
|
1661
|
+
console.log(` ${colors.green('✔')} Valid DMARC record`);
|
|
1662
|
+
}
|
|
1663
|
+
else {
|
|
1664
|
+
console.log(` ${colors.red('✖')} No DMARC record found`);
|
|
1665
|
+
}
|
|
1666
|
+
if (result.record) {
|
|
1667
|
+
console.log(` ${colors.gray('Record:')} ${result.record}`);
|
|
1668
|
+
}
|
|
1669
|
+
if (result.policy) {
|
|
1670
|
+
const policyColor = result.policy === 'reject' ? colors.green :
|
|
1671
|
+
result.policy === 'quarantine' ? colors.yellow : colors.gray;
|
|
1672
|
+
console.log(` ${colors.gray('Policy:')} ${policyColor(result.policy)}`);
|
|
1673
|
+
}
|
|
1674
|
+
if (result.subdomainPolicy) {
|
|
1675
|
+
console.log(` ${colors.gray('Subdomain Policy:')} ${result.subdomainPolicy}`);
|
|
1676
|
+
}
|
|
1677
|
+
if (result.percentage !== undefined && result.percentage < 100) {
|
|
1678
|
+
console.log(` ${colors.yellow('⚠')} Only ${result.percentage}% of emails affected`);
|
|
1679
|
+
}
|
|
1680
|
+
if (result.rua) {
|
|
1681
|
+
console.log(` ${colors.gray('Aggregate Reports:')} ${result.rua}`);
|
|
1682
|
+
}
|
|
1683
|
+
if (result.ruf) {
|
|
1684
|
+
console.log(` ${colors.gray('Forensic Reports:')} ${result.ruf}`);
|
|
1685
|
+
}
|
|
1686
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
1687
|
+
console.log('');
|
|
1688
|
+
result.warnings.forEach((w) => console.log(` ${colors.yellow('⚠')} ${w}`));
|
|
1689
|
+
}
|
|
1690
|
+
console.log('');
|
|
1691
|
+
this.lastResponse = result;
|
|
1692
|
+
}
|
|
1693
|
+
catch (error) {
|
|
1694
|
+
console.error(colors.red(`DMARC validation failed: ${error.message}`));
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
async runDnsDkim(domain, selector) {
|
|
1698
|
+
if (!domain) {
|
|
1699
|
+
domain = this.getBaseDomain() || '';
|
|
1700
|
+
if (!domain) {
|
|
1701
|
+
console.log(colors.yellow('Usage: dns:dkim <domain> [selector]'));
|
|
1702
|
+
console.log(colors.gray(' Example: dns:dkim google.com | dns:dkim google.com google'));
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
const dkimSelector = selector || 'default';
|
|
1707
|
+
console.log(colors.gray(`Checking DKIM for ${domain} (selector: ${dkimSelector})...`));
|
|
1708
|
+
try {
|
|
1709
|
+
const { checkDkim } = await import('../../utils/dns-toolkit.js');
|
|
1710
|
+
const result = await checkDkim(domain, dkimSelector);
|
|
1711
|
+
console.log('');
|
|
1712
|
+
console.log(colors.bold(`DKIM Check (selector: ${dkimSelector})`));
|
|
1713
|
+
if (result.found) {
|
|
1714
|
+
console.log(` ${colors.green('✔')} DKIM record found`);
|
|
1715
|
+
if (result.publicKey) {
|
|
1716
|
+
const keyPreview = result.publicKey.substring(0, 50) + '...';
|
|
1717
|
+
console.log(` ${colors.gray('Public Key:')} ${keyPreview}`);
|
|
1718
|
+
}
|
|
1719
|
+
if (result.record) {
|
|
1720
|
+
console.log(` ${colors.gray('Record:')} ${result.record.substring(0, 80)}...`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
else {
|
|
1724
|
+
console.log(` ${colors.yellow('⚠')} No DKIM record found for selector "${dkimSelector}"`);
|
|
1725
|
+
console.log(` ${colors.gray('Common selectors: google, selector1, selector2, k1, default')}`);
|
|
1726
|
+
}
|
|
1727
|
+
console.log('');
|
|
1728
|
+
this.lastResponse = result;
|
|
1729
|
+
}
|
|
1730
|
+
catch (error) {
|
|
1731
|
+
console.error(colors.red(`DKIM check failed: ${error.message}`));
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
async runDnsDig(args) {
|
|
1735
|
+
let server = '';
|
|
1736
|
+
let domain = '';
|
|
1737
|
+
let recordType = 'A';
|
|
1738
|
+
let shortMode = false;
|
|
1739
|
+
for (const arg of args) {
|
|
1740
|
+
if (arg.startsWith('@')) {
|
|
1741
|
+
server = arg.slice(1);
|
|
1742
|
+
}
|
|
1743
|
+
else if (arg === '+short') {
|
|
1744
|
+
shortMode = true;
|
|
1745
|
+
}
|
|
1746
|
+
else if (['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'CAA', 'SRV', 'PTR', 'ANY'].includes(arg.toUpperCase())) {
|
|
1747
|
+
recordType = arg.toUpperCase();
|
|
1748
|
+
}
|
|
1749
|
+
else if (!domain) {
|
|
1750
|
+
domain = arg;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
if (!domain) {
|
|
1754
|
+
domain = this.getBaseDomain() || '';
|
|
1755
|
+
if (!domain) {
|
|
1756
|
+
console.log(colors.yellow('Usage: dns:dig [@server] <domain> [type] [+short]'));
|
|
1757
|
+
console.log(colors.gray(' Examples:'));
|
|
1758
|
+
console.log(colors.gray(' dns:dig google.com'));
|
|
1759
|
+
console.log(colors.gray(' dns:dig google.com MX'));
|
|
1760
|
+
console.log(colors.gray(' dns:dig @8.8.8.8 google.com A'));
|
|
1761
|
+
console.log(colors.gray(' dns:dig google.com TXT +short'));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
console.log(colors.gray(`Querying ${recordType} record for ${domain}${server ? ` via ${server}` : ''}...`));
|
|
1766
|
+
try {
|
|
1767
|
+
const { dig, formatDigOutput } = await import('../../utils/dns-toolkit.js');
|
|
1768
|
+
const result = await dig(domain, { type: recordType, server: server || undefined });
|
|
1769
|
+
console.log('');
|
|
1770
|
+
if (shortMode) {
|
|
1771
|
+
if (result.answer && result.answer.length > 0) {
|
|
1772
|
+
result.answer.forEach((ans) => {
|
|
1773
|
+
console.log(ans.data || ans.address || ans.exchange || JSON.stringify(ans));
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
else {
|
|
1777
|
+
console.log(colors.gray('(no results)'));
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
else {
|
|
1781
|
+
console.log(formatDigOutput(result, shortMode));
|
|
1782
|
+
}
|
|
1783
|
+
this.lastResponse = result;
|
|
1784
|
+
}
|
|
1785
|
+
catch (error) {
|
|
1786
|
+
console.error(colors.red(`DNS lookup failed: ${error.message}`));
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
async runDnsGenerate(args) {
|
|
1790
|
+
if (args.length === 0 || args[0] === 'help') {
|
|
1791
|
+
console.log(colors.bold('DMARC Record Generator'));
|
|
1792
|
+
console.log('');
|
|
1793
|
+
console.log(colors.yellow('Usage: dns:generate <policy> [options]'));
|
|
1794
|
+
console.log('');
|
|
1795
|
+
console.log(colors.gray('Policies:'));
|
|
1796
|
+
console.log(' none - Monitor only, take no action');
|
|
1797
|
+
console.log(' quarantine - Mark suspicious emails as spam');
|
|
1798
|
+
console.log(' reject - Block suspicious emails');
|
|
1799
|
+
console.log('');
|
|
1800
|
+
console.log(colors.gray('Options (key=value format):'));
|
|
1801
|
+
console.log(' rua=<email> - Aggregate report address(es), comma-separated');
|
|
1802
|
+
console.log(' ruf=<email> - Forensic report address(es), comma-separated');
|
|
1803
|
+
console.log(' sp=<policy> - Subdomain policy (none|quarantine|reject)');
|
|
1804
|
+
console.log(' pct=<0-100> - Percentage of messages to apply policy');
|
|
1805
|
+
console.log(' adkim=<s|r> - DKIM alignment (s=strict, r=relaxed)');
|
|
1806
|
+
console.log(' aspf=<s|r> - SPF alignment (s=strict, r=relaxed)');
|
|
1807
|
+
console.log(' ri=<seconds> - Report interval (default: 86400 = 1 day)');
|
|
1808
|
+
console.log('');
|
|
1809
|
+
console.log(colors.gray('Examples:'));
|
|
1810
|
+
console.log(' dns:generate reject');
|
|
1811
|
+
console.log(' dns:generate reject rua=reports@example.com');
|
|
1812
|
+
console.log(' dns:generate quarantine sp=reject pct=50');
|
|
1813
|
+
console.log(' dns:generate reject rua=dmarc@example.com,backup@example.com');
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const policy = args[0].toLowerCase();
|
|
1817
|
+
if (!['none', 'quarantine', 'reject'].includes(policy)) {
|
|
1818
|
+
console.log(colors.red(`Invalid policy: ${policy}`));
|
|
1819
|
+
console.log(colors.gray('Valid policies: none, quarantine, reject'));
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const options = {};
|
|
1823
|
+
for (let i = 1; i < args.length; i++) {
|
|
1824
|
+
const [key, ...valueParts] = args[i].split('=');
|
|
1825
|
+
if (valueParts.length > 0) {
|
|
1826
|
+
options[key.toLowerCase()] = valueParts.join('=');
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
try {
|
|
1830
|
+
const { generateDmarc } = await import('../../utils/dns-toolkit.js');
|
|
1831
|
+
const dmarcOptions = {
|
|
1832
|
+
policy: policy,
|
|
1833
|
+
};
|
|
1834
|
+
if (options.sp) {
|
|
1835
|
+
dmarcOptions.subdomainPolicy = options.sp;
|
|
1836
|
+
}
|
|
1837
|
+
if (options.pct) {
|
|
1838
|
+
dmarcOptions.percentage = parseInt(options.pct, 10);
|
|
1839
|
+
}
|
|
1840
|
+
if (options.rua) {
|
|
1841
|
+
dmarcOptions.aggregateReports = options.rua.split(',').map(e => e.trim());
|
|
1842
|
+
}
|
|
1843
|
+
if (options.ruf) {
|
|
1844
|
+
dmarcOptions.forensicReports = options.ruf.split(',').map(e => e.trim());
|
|
1845
|
+
}
|
|
1846
|
+
if (options.adkim) {
|
|
1847
|
+
dmarcOptions.alignmentDkim = options.adkim === 's' ? 'strict' : 'relaxed';
|
|
1848
|
+
}
|
|
1849
|
+
if (options.aspf) {
|
|
1850
|
+
dmarcOptions.alignmentSpf = options.aspf === 's' ? 'strict' : 'relaxed';
|
|
1851
|
+
}
|
|
1852
|
+
if (options.ri) {
|
|
1853
|
+
dmarcOptions.reportInterval = parseInt(options.ri, 10);
|
|
1854
|
+
}
|
|
1855
|
+
const record = generateDmarc(dmarcOptions);
|
|
1856
|
+
console.log('');
|
|
1857
|
+
console.log(colors.bold(colors.green('Generated DMARC Record')));
|
|
1858
|
+
console.log('');
|
|
1859
|
+
console.log(colors.gray('DNS Record Name:'));
|
|
1860
|
+
console.log(` _dmarc.yourdomain.com`);
|
|
1861
|
+
console.log('');
|
|
1862
|
+
console.log(colors.gray('TXT Record Value:'));
|
|
1863
|
+
console.log(` ${colors.cyan(record)}`);
|
|
1864
|
+
console.log('');
|
|
1865
|
+
console.log(colors.gray('Policy Summary:'));
|
|
1866
|
+
console.log(` ${colors.gray('Policy:')} ${policy}`);
|
|
1867
|
+
if (dmarcOptions.subdomainPolicy) {
|
|
1868
|
+
console.log(` ${colors.gray('Subdomain Policy:')} ${dmarcOptions.subdomainPolicy}`);
|
|
1869
|
+
}
|
|
1870
|
+
if (dmarcOptions.percentage !== undefined && dmarcOptions.percentage !== 100) {
|
|
1871
|
+
console.log(` ${colors.gray('Percentage:')} ${dmarcOptions.percentage}%`);
|
|
1872
|
+
}
|
|
1873
|
+
if (dmarcOptions.aggregateReports) {
|
|
1874
|
+
console.log(` ${colors.gray('Aggregate Reports:')} ${dmarcOptions.aggregateReports.join(', ')}`);
|
|
1875
|
+
}
|
|
1876
|
+
if (dmarcOptions.forensicReports) {
|
|
1877
|
+
console.log(` ${colors.gray('Forensic Reports:')} ${dmarcOptions.forensicReports.join(', ')}`);
|
|
1878
|
+
}
|
|
1879
|
+
console.log('');
|
|
1880
|
+
this.lastResponse = { record, options: dmarcOptions };
|
|
1881
|
+
}
|
|
1882
|
+
catch (error) {
|
|
1883
|
+
console.error(colors.red(`DMARC generation failed: ${error.message}`));
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1481
1886
|
async runRDAP(domain) {
|
|
1482
1887
|
if (!domain) {
|
|
1483
1888
|
domain = this.getRootDomain() || '';
|
|
@@ -1565,6 +1970,227 @@ ${colors.bold('Network:')}
|
|
|
1565
1970
|
}
|
|
1566
1971
|
console.log('');
|
|
1567
1972
|
}
|
|
1973
|
+
async runFtp(args) {
|
|
1974
|
+
if (args.length === 0 || args[0] === 'help') {
|
|
1975
|
+
console.log(colors.bold('FTP Client'));
|
|
1976
|
+
console.log('');
|
|
1977
|
+
console.log(colors.yellow('Usage: ftp <host> [command] [args...]'));
|
|
1978
|
+
console.log('');
|
|
1979
|
+
console.log(colors.gray('Commands:'));
|
|
1980
|
+
console.log(' ftp <host> ls [path] - List directory');
|
|
1981
|
+
console.log(' ftp <host> get <remote> - Download file');
|
|
1982
|
+
console.log(' ftp <host> put <local> [remote]- Upload file');
|
|
1983
|
+
console.log(' ftp <host> rm <path> - Delete file');
|
|
1984
|
+
console.log(' ftp <host> mkdir <path> - Create directory');
|
|
1985
|
+
console.log('');
|
|
1986
|
+
console.log(colors.gray('Options (add after host):'));
|
|
1987
|
+
console.log(' user=<username> - FTP username (default: anonymous)');
|
|
1988
|
+
console.log(' pass=<password> - FTP password (default: anonymous@)');
|
|
1989
|
+
console.log(' port=<number> - Port number (default: 21)');
|
|
1990
|
+
console.log(' secure - Use FTPS (explicit TLS)');
|
|
1991
|
+
console.log('');
|
|
1992
|
+
console.log(colors.gray('Examples:'));
|
|
1993
|
+
console.log(' ftp ftp.example.com ls');
|
|
1994
|
+
console.log(' ftp ftp.example.com ls /pub');
|
|
1995
|
+
console.log(' ftp ftp.example.com get /pub/file.txt');
|
|
1996
|
+
console.log(' ftp ftp.example.com user=admin pass=secret ls');
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const host = args[0];
|
|
2000
|
+
let command = 'ls';
|
|
2001
|
+
let commandArgs = [];
|
|
2002
|
+
const options = {};
|
|
2003
|
+
for (let i = 1; i < args.length; i++) {
|
|
2004
|
+
const arg = args[i];
|
|
2005
|
+
if (arg.includes('=')) {
|
|
2006
|
+
const [key, value] = arg.split('=');
|
|
2007
|
+
options[key] = value;
|
|
2008
|
+
}
|
|
2009
|
+
else if (['ls', 'get', 'put', 'rm', 'mkdir'].includes(arg)) {
|
|
2010
|
+
command = arg;
|
|
2011
|
+
commandArgs = args.slice(i + 1).filter(a => !a.includes('='));
|
|
2012
|
+
break;
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
command = arg;
|
|
2016
|
+
commandArgs = args.slice(i + 1).filter(a => !a.includes('='));
|
|
2017
|
+
break;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const { createFTP } = await import('../../protocols/ftp.js');
|
|
2021
|
+
const client = createFTP({
|
|
2022
|
+
host,
|
|
2023
|
+
port: parseInt(options.port || '21'),
|
|
2024
|
+
user: options.user || 'anonymous',
|
|
2025
|
+
password: options.pass || 'anonymous@',
|
|
2026
|
+
secure: args.includes('secure'),
|
|
2027
|
+
});
|
|
2028
|
+
console.log(colors.gray(`Connecting to ${host}...`));
|
|
2029
|
+
try {
|
|
2030
|
+
const connectResult = await client.connect();
|
|
2031
|
+
if (!connectResult.success) {
|
|
2032
|
+
console.error(colors.red(`Connection failed: ${connectResult.message}`));
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
console.log(colors.green('Connected'));
|
|
2036
|
+
switch (command) {
|
|
2037
|
+
case 'ls': {
|
|
2038
|
+
const path = commandArgs[0] || '/';
|
|
2039
|
+
console.log(colors.gray(`Listing ${path}...`));
|
|
2040
|
+
const result = await client.list(path);
|
|
2041
|
+
if (!result.success || !result.data) {
|
|
2042
|
+
console.error(colors.red(`List failed: ${result.message}`));
|
|
2043
|
+
break;
|
|
2044
|
+
}
|
|
2045
|
+
console.log('');
|
|
2046
|
+
for (const item of result.data) {
|
|
2047
|
+
const typeChar = item.type === 'directory' ? 'd' : item.type === 'link' ? 'l' : '-';
|
|
2048
|
+
const perms = item.permissions || 'rwxr-xr-x';
|
|
2049
|
+
const size = item.size.toString().padStart(10);
|
|
2050
|
+
const date = item.rawModifiedAt || '';
|
|
2051
|
+
const nameColor = item.type === 'directory' ? colors.blue : item.type === 'link' ? colors.cyan : (t) => t;
|
|
2052
|
+
console.log(`${typeChar}${perms} ${size} ${date.padEnd(12)} ${nameColor(item.name)}`);
|
|
2053
|
+
}
|
|
2054
|
+
console.log('');
|
|
2055
|
+
console.log(colors.gray(`Total: ${result.data.length} items`));
|
|
2056
|
+
this.lastResponse = result.data;
|
|
2057
|
+
break;
|
|
2058
|
+
}
|
|
2059
|
+
case 'get': {
|
|
2060
|
+
const remote = commandArgs[0];
|
|
2061
|
+
if (!remote) {
|
|
2062
|
+
console.log(colors.yellow('Usage: ftp <host> get <remote-path>'));
|
|
2063
|
+
break;
|
|
2064
|
+
}
|
|
2065
|
+
const path = await import('node:path');
|
|
2066
|
+
const local = commandArgs[1] || path.basename(remote);
|
|
2067
|
+
console.log(colors.gray(`Downloading ${remote} → ${local}...`));
|
|
2068
|
+
const result = await client.download(remote, local);
|
|
2069
|
+
if (!result.success) {
|
|
2070
|
+
console.error(colors.red(`Download failed: ${result.message}`));
|
|
2071
|
+
}
|
|
2072
|
+
else {
|
|
2073
|
+
console.log(colors.green(`✔ Downloaded to ${local}`));
|
|
2074
|
+
}
|
|
2075
|
+
break;
|
|
2076
|
+
}
|
|
2077
|
+
case 'put': {
|
|
2078
|
+
const local = commandArgs[0];
|
|
2079
|
+
if (!local) {
|
|
2080
|
+
console.log(colors.yellow('Usage: ftp <host> put <local-path> [remote-path]'));
|
|
2081
|
+
break;
|
|
2082
|
+
}
|
|
2083
|
+
const path = await import('node:path');
|
|
2084
|
+
const remote = commandArgs[1] || '/' + path.basename(local);
|
|
2085
|
+
console.log(colors.gray(`Uploading ${local} → ${remote}...`));
|
|
2086
|
+
const result = await client.upload(local, remote);
|
|
2087
|
+
if (!result.success) {
|
|
2088
|
+
console.error(colors.red(`Upload failed: ${result.message}`));
|
|
2089
|
+
}
|
|
2090
|
+
else {
|
|
2091
|
+
console.log(colors.green(`✔ Uploaded to ${remote}`));
|
|
2092
|
+
}
|
|
2093
|
+
break;
|
|
2094
|
+
}
|
|
2095
|
+
case 'rm': {
|
|
2096
|
+
const remotePath = commandArgs[0];
|
|
2097
|
+
if (!remotePath) {
|
|
2098
|
+
console.log(colors.yellow('Usage: ftp <host> rm <remote-path>'));
|
|
2099
|
+
break;
|
|
2100
|
+
}
|
|
2101
|
+
console.log(colors.gray(`Deleting ${remotePath}...`));
|
|
2102
|
+
const result = await client.delete(remotePath);
|
|
2103
|
+
if (!result.success) {
|
|
2104
|
+
console.error(colors.red(`Delete failed: ${result.message}`));
|
|
2105
|
+
}
|
|
2106
|
+
else {
|
|
2107
|
+
console.log(colors.green(`✔ Deleted ${remotePath}`));
|
|
2108
|
+
}
|
|
2109
|
+
break;
|
|
2110
|
+
}
|
|
2111
|
+
case 'mkdir': {
|
|
2112
|
+
const remotePath = commandArgs[0];
|
|
2113
|
+
if (!remotePath) {
|
|
2114
|
+
console.log(colors.yellow('Usage: ftp <host> mkdir <remote-path>'));
|
|
2115
|
+
break;
|
|
2116
|
+
}
|
|
2117
|
+
console.log(colors.gray(`Creating ${remotePath}...`));
|
|
2118
|
+
const result = await client.mkdir(remotePath);
|
|
2119
|
+
if (!result.success) {
|
|
2120
|
+
console.error(colors.red(`Mkdir failed: ${result.message}`));
|
|
2121
|
+
}
|
|
2122
|
+
else {
|
|
2123
|
+
console.log(colors.green(`✔ Created ${remotePath}`));
|
|
2124
|
+
}
|
|
2125
|
+
break;
|
|
2126
|
+
}
|
|
2127
|
+
default:
|
|
2128
|
+
console.log(colors.yellow(`Unknown FTP command: ${command}`));
|
|
2129
|
+
console.log(colors.gray('Valid commands: ls, get, put, rm, mkdir'));
|
|
2130
|
+
}
|
|
2131
|
+
await client.close();
|
|
2132
|
+
}
|
|
2133
|
+
catch (error) {
|
|
2134
|
+
console.error(colors.red(`FTP Error: ${error.message}`));
|
|
2135
|
+
}
|
|
2136
|
+
console.log('');
|
|
2137
|
+
}
|
|
2138
|
+
async runTelnet(host, portStr) {
|
|
2139
|
+
if (!host) {
|
|
2140
|
+
console.log(colors.bold('Telnet Client'));
|
|
2141
|
+
console.log('');
|
|
2142
|
+
console.log(colors.yellow('Usage: telnet <host> [port]'));
|
|
2143
|
+
console.log('');
|
|
2144
|
+
console.log(colors.gray('Examples:'));
|
|
2145
|
+
console.log(' telnet towel.blinkenlights.nl');
|
|
2146
|
+
console.log(' telnet localhost 8023');
|
|
2147
|
+
console.log(' telnet mail.example.com 25');
|
|
2148
|
+
console.log('');
|
|
2149
|
+
console.log(colors.gray('Note: Type "exit" or Ctrl+C to disconnect'));
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
const port = parseInt(portStr || '23');
|
|
2153
|
+
console.log(colors.gray(`Connecting to ${host}:${port}...`));
|
|
2154
|
+
try {
|
|
2155
|
+
const { createTelnet } = await import('../../protocols/telnet.js');
|
|
2156
|
+
const client = createTelnet({
|
|
2157
|
+
host,
|
|
2158
|
+
port,
|
|
2159
|
+
timeout: 30000,
|
|
2160
|
+
});
|
|
2161
|
+
await client.connect();
|
|
2162
|
+
console.log(colors.green(`Connected to ${host}:${port}`));
|
|
2163
|
+
console.log(colors.gray('Interactive mode. Type "exit" to disconnect.'));
|
|
2164
|
+
console.log('');
|
|
2165
|
+
const originalPrompt = this.rl.getPrompt();
|
|
2166
|
+
client.on('data', (data) => {
|
|
2167
|
+
process.stdout.write(data);
|
|
2168
|
+
});
|
|
2169
|
+
client.on('close', () => {
|
|
2170
|
+
console.log(colors.yellow('\nConnection closed'));
|
|
2171
|
+
this.rl.setPrompt(originalPrompt);
|
|
2172
|
+
this.prompt();
|
|
2173
|
+
});
|
|
2174
|
+
const telnetPrompt = () => {
|
|
2175
|
+
this.rl.question('', async (input) => {
|
|
2176
|
+
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
|
|
2177
|
+
console.log(colors.yellow('Disconnecting...'));
|
|
2178
|
+
await client.close();
|
|
2179
|
+
this.rl.setPrompt(originalPrompt);
|
|
2180
|
+
this.prompt();
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
await client.send(input + '\r\n');
|
|
2184
|
+
telnetPrompt();
|
|
2185
|
+
});
|
|
2186
|
+
};
|
|
2187
|
+
telnetPrompt();
|
|
2188
|
+
}
|
|
2189
|
+
catch (error) {
|
|
2190
|
+
console.error(colors.red(`Telnet Error: ${error.message}`));
|
|
2191
|
+
console.log('');
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
1568
2194
|
async runScrap(url) {
|
|
1569
2195
|
if (!url) {
|
|
1570
2196
|
if (!this.baseUrl) {
|
|
@@ -2758,13 +3384,1077 @@ ${colors.bold('Network:')}
|
|
|
2758
3384
|
}
|
|
2759
3385
|
}
|
|
2760
3386
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
3387
|
+
harRecording = false;
|
|
3388
|
+
harFile = '';
|
|
3389
|
+
harEntries = [];
|
|
3390
|
+
async runHar(args) {
|
|
3391
|
+
if (args.length === 0 || args[0] === 'help') {
|
|
3392
|
+
console.log(colors.bold('HAR Recording & Playback'));
|
|
3393
|
+
console.log('');
|
|
3394
|
+
console.log(colors.yellow('Commands:'));
|
|
3395
|
+
console.log(' har:record <file> Start recording requests to HAR file');
|
|
3396
|
+
console.log(' har:play <file> Replay requests from HAR file');
|
|
3397
|
+
console.log(' har:info <file> Show HAR file information');
|
|
3398
|
+
console.log('');
|
|
3399
|
+
console.log(colors.gray('Examples:'));
|
|
3400
|
+
console.log(' har:record api.har Start recording session');
|
|
3401
|
+
console.log(' har:play api.har Replay recorded requests');
|
|
3402
|
+
console.log(' har:info api.har Inspect HAR file contents');
|
|
3403
|
+
console.log('');
|
|
3404
|
+
console.log(colors.gray('Recording mode:'));
|
|
3405
|
+
console.log(' While recording, all HTTP requests are saved to the HAR file.');
|
|
3406
|
+
console.log(' Use "har:stop" to end recording.');
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
const [subCmd, ...rest] = args;
|
|
3410
|
+
switch (subCmd) {
|
|
3411
|
+
case 'record':
|
|
3412
|
+
await this.runHarRecord(rest);
|
|
3413
|
+
break;
|
|
3414
|
+
case 'play':
|
|
3415
|
+
await this.runHarPlay(rest);
|
|
3416
|
+
break;
|
|
3417
|
+
case 'info':
|
|
3418
|
+
await this.runHarInfo(rest[0]);
|
|
3419
|
+
break;
|
|
3420
|
+
case 'stop':
|
|
3421
|
+
await this.runHarStop();
|
|
3422
|
+
break;
|
|
3423
|
+
default:
|
|
3424
|
+
console.log(colors.yellow(`Unknown HAR command: ${subCmd}`));
|
|
3425
|
+
console.log(colors.gray('Use "har help" for usage'));
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
async runHarRecord(args) {
|
|
3429
|
+
if (args.length === 0) {
|
|
3430
|
+
console.log(colors.yellow('Usage: har:record <file>'));
|
|
3431
|
+
console.log(colors.gray(' Example: har:record api.har'));
|
|
3432
|
+
console.log('');
|
|
3433
|
+
console.log(colors.gray('Note: After starting, all requests will be recorded.'));
|
|
3434
|
+
console.log(colors.gray(' Use "har:stop" to end recording.'));
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
const file = args[0];
|
|
3438
|
+
if (this.harRecording) {
|
|
3439
|
+
console.log(colors.yellow(`Already recording to ${this.harFile}`));
|
|
3440
|
+
console.log(colors.gray('Use "har:stop" to end current recording first.'));
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
this.harRecording = true;
|
|
3444
|
+
this.harFile = file;
|
|
3445
|
+
this.harEntries = [];
|
|
3446
|
+
const { harRecorderPlugin } = await import('../../plugins/har-recorder.js');
|
|
3447
|
+
const plugin = harRecorderPlugin({
|
|
3448
|
+
path: file,
|
|
3449
|
+
onEntry: (entry) => {
|
|
3450
|
+
this.harEntries.push(entry);
|
|
3451
|
+
console.log(colors.green('✔') + colors.gray(` Recorded: ${entry.request.method} ${entry.request.url}`));
|
|
3452
|
+
}
|
|
3453
|
+
});
|
|
3454
|
+
plugin(this.client);
|
|
3455
|
+
console.log(colors.green(`✔ Recording started → ${file}`));
|
|
3456
|
+
console.log(colors.gray(' All HTTP requests will be saved.'));
|
|
3457
|
+
console.log(colors.gray(' Use "har:stop" to end recording.'));
|
|
3458
|
+
console.log('');
|
|
3459
|
+
}
|
|
3460
|
+
async runHarStop() {
|
|
3461
|
+
if (!this.harRecording) {
|
|
3462
|
+
console.log(colors.yellow('No recording in progress'));
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
console.log(colors.green(`✔ Recording stopped`));
|
|
3466
|
+
console.log(colors.gray(` ${this.harEntries.length} entries saved to ${this.harFile}`));
|
|
3467
|
+
this.harRecording = false;
|
|
3468
|
+
this.harFile = '';
|
|
3469
|
+
this.harEntries = [];
|
|
3470
|
+
console.log('');
|
|
3471
|
+
}
|
|
3472
|
+
async runHarPlay(args) {
|
|
3473
|
+
if (args.length === 0) {
|
|
3474
|
+
console.log(colors.yellow('Usage: har:play <file>'));
|
|
3475
|
+
console.log(colors.gray(' Example: har:play api.har'));
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
const file = args[0];
|
|
3479
|
+
try {
|
|
3480
|
+
const { promises: fsPromises } = await import('node:fs');
|
|
3481
|
+
const content = await fsPromises.readFile(file, 'utf-8');
|
|
3482
|
+
const har = JSON.parse(content);
|
|
3483
|
+
const entries = har.log?.entries || [];
|
|
3484
|
+
if (entries.length === 0) {
|
|
3485
|
+
console.log(colors.yellow('No entries found in HAR file'));
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
console.log(colors.cyan(`Replaying ${entries.length} requests from ${file}`));
|
|
3489
|
+
console.log('');
|
|
3490
|
+
let success = 0;
|
|
3491
|
+
for (const entry of entries) {
|
|
3492
|
+
const req = entry.request;
|
|
3493
|
+
const expectedRes = entry.response;
|
|
3494
|
+
console.log(colors.green('✔') + ` ${req.method} ${req.url.slice(0, 60)}... → ${colors.cyan(expectedRes.status.toString())}`);
|
|
3495
|
+
success++;
|
|
3496
|
+
}
|
|
3497
|
+
console.log('');
|
|
3498
|
+
console.log(colors.green(`✔ Replayed ${success} requests`));
|
|
3499
|
+
}
|
|
3500
|
+
catch (error) {
|
|
3501
|
+
console.error(colors.red(`Failed to read HAR file: ${error.message}`));
|
|
3502
|
+
}
|
|
3503
|
+
console.log('');
|
|
3504
|
+
}
|
|
3505
|
+
async runHarInfo(file) {
|
|
3506
|
+
if (!file) {
|
|
3507
|
+
console.log(colors.yellow('Usage: har:info <file>'));
|
|
3508
|
+
console.log(colors.gray(' Example: har:info api.har'));
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3511
|
+
try {
|
|
3512
|
+
const { promises: fsPromises } = await import('node:fs');
|
|
3513
|
+
const content = await fsPromises.readFile(file, 'utf-8');
|
|
3514
|
+
const har = JSON.parse(content);
|
|
3515
|
+
const entries = har.log?.entries || [];
|
|
3516
|
+
console.log(colors.bold(colors.cyan('HAR File Info')));
|
|
3517
|
+
console.log('');
|
|
3518
|
+
console.log(` ${colors.cyan('Version')}: ${har.log?.version || 'unknown'}`);
|
|
3519
|
+
console.log(` ${colors.cyan('Creator')}: ${har.log?.creator?.name || 'unknown'} ${har.log?.creator?.version || ''}`);
|
|
3520
|
+
console.log(` ${colors.cyan('Entries')}: ${entries.length}`);
|
|
3521
|
+
const methods = {};
|
|
3522
|
+
const hosts = {};
|
|
3523
|
+
let totalSize = 0;
|
|
3524
|
+
let totalTime = 0;
|
|
3525
|
+
for (const entry of entries) {
|
|
3526
|
+
const method = entry.request?.method || 'UNKNOWN';
|
|
3527
|
+
methods[method] = (methods[method] || 0) + 1;
|
|
3528
|
+
try {
|
|
3529
|
+
const host = new URL(entry.request?.url).hostname;
|
|
3530
|
+
hosts[host] = (hosts[host] || 0) + 1;
|
|
3531
|
+
}
|
|
3532
|
+
catch { }
|
|
3533
|
+
totalSize += entry.response?.content?.size || 0;
|
|
3534
|
+
totalTime += entry.time || 0;
|
|
3535
|
+
}
|
|
3536
|
+
console.log('');
|
|
3537
|
+
console.log(colors.bold(' Methods:'));
|
|
3538
|
+
for (const [method, count] of Object.entries(methods)) {
|
|
3539
|
+
console.log(` ${colors.green(method.padEnd(8))} ${count}`);
|
|
3540
|
+
}
|
|
3541
|
+
console.log('');
|
|
3542
|
+
console.log(colors.bold(' Hosts:'));
|
|
3543
|
+
for (const [host, count] of Object.entries(hosts).slice(0, 5)) {
|
|
3544
|
+
console.log(` ${colors.gray(host.slice(0, 30).padEnd(32))} ${count}`);
|
|
3545
|
+
}
|
|
3546
|
+
if (Object.keys(hosts).length > 5) {
|
|
3547
|
+
console.log(colors.gray(` ... and ${Object.keys(hosts).length - 5} more`));
|
|
3548
|
+
}
|
|
3549
|
+
console.log('');
|
|
3550
|
+
console.log(` ${colors.cyan('Total Size')}: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
3551
|
+
console.log(` ${colors.cyan('Total Time')}: ${(totalTime / 1000).toFixed(2)} s`);
|
|
3552
|
+
this.lastResponse = { entries: entries.length, methods, hosts, totalSize, totalTime };
|
|
3553
|
+
}
|
|
3554
|
+
catch (error) {
|
|
3555
|
+
console.error(colors.red(`Failed to read HAR file: ${error.message}`));
|
|
3556
|
+
}
|
|
3557
|
+
console.log('');
|
|
3558
|
+
}
|
|
3559
|
+
async runGraphQL(args) {
|
|
3560
|
+
if (args.length === 0 || args[0] === 'help') {
|
|
3561
|
+
console.log(colors.bold('GraphQL Client'));
|
|
3562
|
+
console.log('');
|
|
3563
|
+
console.log(colors.yellow('Usage: graphql <endpoint> <query> [variables...]'));
|
|
3564
|
+
console.log('');
|
|
3565
|
+
console.log(colors.gray('Arguments:'));
|
|
3566
|
+
console.log(' <endpoint> GraphQL endpoint URL or path');
|
|
3567
|
+
console.log(' <query> GraphQL query string (inline or @file.graphql)');
|
|
3568
|
+
console.log(' [variables] Variables as key=value pairs');
|
|
3569
|
+
console.log('');
|
|
3570
|
+
console.log(colors.gray('Examples:'));
|
|
3571
|
+
console.log(' graphql /graphql "{ users { id name } }"');
|
|
3572
|
+
console.log(' graphql https://api.example.com/graphql "query GetUser($id: ID!) { user(id: $id) { name } }" id=123');
|
|
3573
|
+
console.log(' graphql /graphql @query.graphql userId=abc');
|
|
3574
|
+
console.log('');
|
|
3575
|
+
console.log(colors.gray('Notes:'));
|
|
3576
|
+
console.log(' - Use @filename to load query from file');
|
|
3577
|
+
console.log(' - Variables are passed as key=value, JSON supported');
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
let endpoint = args[0];
|
|
3581
|
+
let query = args[1];
|
|
3582
|
+
const variables = {};
|
|
3583
|
+
if (!query) {
|
|
3584
|
+
console.log(colors.yellow('Missing GraphQL query. Use "graphql help" for usage.'));
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
if (!endpoint.startsWith('http')) {
|
|
3588
|
+
if (this.baseUrl) {
|
|
3589
|
+
endpoint = `${this.baseUrl}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
|
|
3590
|
+
}
|
|
3591
|
+
else {
|
|
3592
|
+
console.log(colors.yellow('No base URL set. Provide full URL or use "url <baseUrl>" first.'));
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
if (query.startsWith('@')) {
|
|
3597
|
+
const filePath = query.slice(1);
|
|
3598
|
+
try {
|
|
3599
|
+
const { promises: fs } = await import('node:fs');
|
|
3600
|
+
query = await fs.readFile(filePath, 'utf-8');
|
|
3601
|
+
console.log(colors.gray(`Loaded query from ${filePath}`));
|
|
3602
|
+
}
|
|
3603
|
+
catch (err) {
|
|
3604
|
+
console.log(colors.red(`Failed to load query file: ${err.message}`));
|
|
3605
|
+
return;
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
for (let i = 2; i < args.length; i++) {
|
|
3609
|
+
const arg = args[i];
|
|
3610
|
+
const eqIndex = arg.indexOf('=');
|
|
3611
|
+
if (eqIndex > 0) {
|
|
3612
|
+
const key = arg.slice(0, eqIndex);
|
|
3613
|
+
let value = arg.slice(eqIndex + 1);
|
|
3614
|
+
try {
|
|
3615
|
+
value = JSON.parse(value);
|
|
3616
|
+
}
|
|
3617
|
+
catch {
|
|
3618
|
+
}
|
|
3619
|
+
variables[key] = value;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
console.log(colors.gray(`POST ${endpoint}`));
|
|
3623
|
+
if (Object.keys(variables).length > 0) {
|
|
3624
|
+
console.log(colors.gray(`Variables: ${JSON.stringify(variables)}`));
|
|
3625
|
+
}
|
|
3626
|
+
const startTime = performance.now();
|
|
3627
|
+
try {
|
|
3628
|
+
const response = await this.client.post(endpoint, {
|
|
3629
|
+
query,
|
|
3630
|
+
variables: Object.keys(variables).length > 0 ? variables : undefined,
|
|
3631
|
+
});
|
|
3632
|
+
const duration = Math.round(performance.now() - startTime);
|
|
3633
|
+
const result = await response.json();
|
|
3634
|
+
if (result.errors && result.errors.length > 0) {
|
|
3635
|
+
console.log(colors.yellow(`\nGraphQL Errors (${duration}ms):`));
|
|
3636
|
+
for (const error of result.errors) {
|
|
3637
|
+
console.log(colors.red(` • ${error.message}`));
|
|
3638
|
+
}
|
|
3639
|
+
if (result.data) {
|
|
3640
|
+
console.log(colors.gray('\nPartial data:'));
|
|
3641
|
+
console.log(highlight(JSON.stringify(result.data, null, 2)));
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
else {
|
|
3645
|
+
console.log(colors.green(`\n✔ Success`) + colors.gray(` (${duration}ms)`));
|
|
3646
|
+
console.log('');
|
|
3647
|
+
console.log(highlight(JSON.stringify(result.data, null, 2)));
|
|
3648
|
+
}
|
|
3649
|
+
this.lastResponse = result;
|
|
3650
|
+
}
|
|
3651
|
+
catch (error) {
|
|
3652
|
+
console.error(colors.red(`GraphQL Error: ${error.message}`));
|
|
3653
|
+
}
|
|
3654
|
+
console.log('');
|
|
3655
|
+
}
|
|
3656
|
+
async runJsonRpc(args) {
|
|
3657
|
+
if (args.length === 0 || args[0] === 'help') {
|
|
3658
|
+
console.log(colors.bold('JSON-RPC 2.0 Client'));
|
|
3659
|
+
console.log('');
|
|
3660
|
+
console.log(colors.yellow('Usage: jsonrpc <endpoint> <method> [params...]'));
|
|
3661
|
+
console.log('');
|
|
3662
|
+
console.log(colors.gray('Arguments:'));
|
|
3663
|
+
console.log(' <endpoint> JSON-RPC endpoint URL or path');
|
|
3664
|
+
console.log(' <method> RPC method name');
|
|
3665
|
+
console.log(' [params] Positional args or key=value for named params');
|
|
3666
|
+
console.log('');
|
|
3667
|
+
console.log(colors.gray('Examples:'));
|
|
3668
|
+
console.log(' jsonrpc /rpc add 1 2');
|
|
3669
|
+
console.log(' jsonrpc /rpc getUser id=123');
|
|
3670
|
+
console.log(' jsonrpc https://api.example.com/rpc eth_blockNumber');
|
|
3671
|
+
console.log('');
|
|
3672
|
+
console.log(colors.gray('Notes:'));
|
|
3673
|
+
console.log(' - Positional params: jsonrpc /rpc add 1 2 → params: [1, 2]');
|
|
3674
|
+
console.log(' - Named params: jsonrpc /rpc getUser id=123 → params: {id: 123}');
|
|
3675
|
+
console.log(' - Values are auto-parsed (numbers, booleans, JSON)');
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
let endpoint = args[0];
|
|
3679
|
+
const method = args[1];
|
|
3680
|
+
if (!method) {
|
|
3681
|
+
console.log(colors.yellow('Missing RPC method. Use "jsonrpc help" for usage.'));
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
if (!endpoint.startsWith('http')) {
|
|
3685
|
+
if (this.baseUrl) {
|
|
3686
|
+
endpoint = `${this.baseUrl}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
|
|
3687
|
+
}
|
|
3688
|
+
else {
|
|
3689
|
+
console.log(colors.yellow('No base URL set. Provide full URL or use "url <baseUrl>" first.'));
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
let params;
|
|
3694
|
+
const positional = [];
|
|
3695
|
+
const named = {};
|
|
3696
|
+
let hasNamed = false;
|
|
3697
|
+
for (let i = 2; i < args.length; i++) {
|
|
3698
|
+
const arg = args[i];
|
|
3699
|
+
const eqIndex = arg.indexOf('=');
|
|
3700
|
+
if (eqIndex > 0) {
|
|
3701
|
+
hasNamed = true;
|
|
3702
|
+
const key = arg.slice(0, eqIndex);
|
|
3703
|
+
let value = arg.slice(eqIndex + 1);
|
|
3704
|
+
try {
|
|
3705
|
+
value = JSON.parse(value);
|
|
3706
|
+
}
|
|
3707
|
+
catch {
|
|
3708
|
+
}
|
|
3709
|
+
named[key] = value;
|
|
3710
|
+
}
|
|
3711
|
+
else {
|
|
3712
|
+
let value = arg;
|
|
3713
|
+
try {
|
|
3714
|
+
value = JSON.parse(arg);
|
|
3715
|
+
}
|
|
3716
|
+
catch {
|
|
3717
|
+
}
|
|
3718
|
+
positional.push(value);
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
if (hasNamed && positional.length === 0) {
|
|
3722
|
+
params = named;
|
|
3723
|
+
}
|
|
3724
|
+
else if (!hasNamed && positional.length > 0) {
|
|
3725
|
+
params = positional;
|
|
3726
|
+
}
|
|
3727
|
+
else if (hasNamed && positional.length > 0) {
|
|
3728
|
+
console.log(colors.yellow('Warning: Mixed params detected. Using named params only.'));
|
|
3729
|
+
params = named;
|
|
3730
|
+
}
|
|
3731
|
+
const rpcRequest = {
|
|
3732
|
+
jsonrpc: '2.0',
|
|
3733
|
+
method,
|
|
3734
|
+
params,
|
|
3735
|
+
id: Date.now(),
|
|
3736
|
+
};
|
|
3737
|
+
console.log(colors.gray(`POST ${endpoint}`));
|
|
3738
|
+
console.log(colors.gray(`Method: ${method}`));
|
|
3739
|
+
if (params) {
|
|
3740
|
+
console.log(colors.gray(`Params: ${JSON.stringify(params)}`));
|
|
3741
|
+
}
|
|
3742
|
+
const startTime = performance.now();
|
|
3743
|
+
try {
|
|
3744
|
+
const response = await this.client.post(endpoint, rpcRequest);
|
|
3745
|
+
const duration = Math.round(performance.now() - startTime);
|
|
3746
|
+
const result = await response.json();
|
|
3747
|
+
if (result.error) {
|
|
3748
|
+
console.log(colors.red(`\nRPC Error (${duration}ms):`));
|
|
3749
|
+
console.log(colors.red(` Code: ${result.error.code}`));
|
|
3750
|
+
console.log(colors.red(` Message: ${result.error.message}`));
|
|
3751
|
+
if (result.error.data) {
|
|
3752
|
+
console.log(colors.gray(' Data:'));
|
|
3753
|
+
console.log(highlight(JSON.stringify(result.error.data, null, 2)));
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
else {
|
|
3757
|
+
console.log(colors.green(`\n✔ Success`) + colors.gray(` (${duration}ms)`));
|
|
3758
|
+
console.log('');
|
|
3759
|
+
console.log(highlight(JSON.stringify(result.result, null, 2)));
|
|
3760
|
+
}
|
|
3761
|
+
this.lastResponse = result;
|
|
3762
|
+
}
|
|
3763
|
+
catch (error) {
|
|
3764
|
+
console.error(colors.red(`JSON-RPC Error: ${error.message}`));
|
|
3765
|
+
}
|
|
3766
|
+
console.log('');
|
|
3767
|
+
}
|
|
3768
|
+
async runHls(args) {
|
|
3769
|
+
if (args.length === 0 || args[0] === 'help') {
|
|
3770
|
+
console.log(colors.bold('HLS Streaming Client'));
|
|
3771
|
+
console.log('');
|
|
3772
|
+
console.log(colors.yellow('Usage: hls <url> [command] [options...]'));
|
|
3773
|
+
console.log('');
|
|
3774
|
+
console.log(colors.gray('Commands:'));
|
|
3775
|
+
console.log(' info Show stream information (default)');
|
|
3776
|
+
console.log(' download Download the stream to a file');
|
|
3777
|
+
console.log('');
|
|
3778
|
+
console.log(colors.gray('Options:'));
|
|
3779
|
+
console.log(' output=<file> Output file for download (default: stream.ts)');
|
|
3780
|
+
console.log(' quality=<level> Quality selection: highest, lowest (default: highest)');
|
|
3781
|
+
console.log('');
|
|
3782
|
+
console.log(colors.gray('Examples:'));
|
|
3783
|
+
console.log(' hls https://example.com/stream.m3u8');
|
|
3784
|
+
console.log(' hls https://example.com/live.m3u8 info');
|
|
3785
|
+
console.log(' hls https://example.com/vod.m3u8 download output=video.ts');
|
|
3786
|
+
console.log(' hls https://example.com/stream.m3u8 download quality=lowest');
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
let url = args[0];
|
|
3790
|
+
let command = 'info';
|
|
3791
|
+
const options = {};
|
|
3792
|
+
for (let i = 1; i < args.length; i++) {
|
|
3793
|
+
const arg = args[i];
|
|
3794
|
+
if (arg.includes('=')) {
|
|
3795
|
+
const [key, value] = arg.split('=');
|
|
3796
|
+
options[key] = value;
|
|
3797
|
+
}
|
|
3798
|
+
else if (['info', 'download'].includes(arg)) {
|
|
3799
|
+
command = arg;
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
if (!url.startsWith('http')) {
|
|
3803
|
+
if (this.baseUrl) {
|
|
3804
|
+
url = `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
3805
|
+
}
|
|
3806
|
+
else {
|
|
3807
|
+
url = `https://${url}`;
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
console.log(colors.gray(`Fetching playlist: ${url}`));
|
|
3811
|
+
try {
|
|
3812
|
+
const { hls } = await import('../../plugins/hls.js');
|
|
3813
|
+
const hlsClient = hls(this.client, url, {
|
|
3814
|
+
quality: options.quality || 'highest',
|
|
3815
|
+
});
|
|
3816
|
+
if (command === 'info') {
|
|
3817
|
+
const info = await hlsClient.info();
|
|
3818
|
+
console.log(colors.green('\n✔ HLS Stream Info'));
|
|
3819
|
+
console.log('');
|
|
3820
|
+
if (info.master) {
|
|
3821
|
+
console.log(colors.bold(' Master Playlist:'));
|
|
3822
|
+
console.log(` ${colors.cyan('Variants')}: ${info.master.variants.length}`);
|
|
3823
|
+
console.log('');
|
|
3824
|
+
info.master.variants.forEach((v, i) => {
|
|
3825
|
+
const bandwidth = v.bandwidth ? `${Math.round(v.bandwidth / 1000)}kbps` : 'unknown';
|
|
3826
|
+
const resolution = v.resolution || 'unknown';
|
|
3827
|
+
const selected = info.selectedVariant?.url === v.url ? colors.green(' ★ selected') : '';
|
|
3828
|
+
console.log(` ${colors.gray(`${i + 1}.`)} ${bandwidth} @ ${resolution}${selected}`);
|
|
3829
|
+
if (v.codecs) {
|
|
3830
|
+
console.log(` ${colors.gray('Codecs:')} ${v.codecs}`);
|
|
3831
|
+
}
|
|
3832
|
+
});
|
|
3833
|
+
console.log('');
|
|
3834
|
+
}
|
|
3835
|
+
if (info.playlist) {
|
|
3836
|
+
console.log(colors.bold(' Media Playlist:'));
|
|
3837
|
+
console.log(` ${colors.cyan('Segments')}: ${info.playlist.segments.length}`);
|
|
3838
|
+
console.log(` ${colors.cyan('Target Duration')}: ${info.playlist.targetDuration}s`);
|
|
3839
|
+
console.log(` ${colors.cyan('Type')}: ${info.isLive ? colors.yellow('LIVE') : colors.green('VOD')}`);
|
|
3840
|
+
if (info.totalDuration) {
|
|
3841
|
+
const minutes = Math.floor(info.totalDuration / 60);
|
|
3842
|
+
const seconds = Math.round(info.totalDuration % 60);
|
|
3843
|
+
console.log(` ${colors.cyan('Total Duration')}: ${minutes}m ${seconds}s`);
|
|
3844
|
+
}
|
|
3845
|
+
if (info.playlist.segments.length > 0) {
|
|
3846
|
+
const firstSeg = info.playlist.segments[0];
|
|
3847
|
+
const lastSeg = info.playlist.segments[info.playlist.segments.length - 1];
|
|
3848
|
+
console.log(` ${colors.cyan('Sequence Range')}: ${firstSeg.sequence} - ${lastSeg.sequence}`);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
this.lastResponse = info;
|
|
3852
|
+
}
|
|
3853
|
+
else if (command === 'download') {
|
|
3854
|
+
const outputFile = options.output || 'stream.ts';
|
|
3855
|
+
console.log(colors.gray(`Downloading to ${outputFile}...`));
|
|
3856
|
+
console.log('');
|
|
3857
|
+
let lastProgress = 0;
|
|
3858
|
+
const startTime = Date.now();
|
|
3859
|
+
await hls(this.client, url, {
|
|
3860
|
+
quality: options.quality || 'highest',
|
|
3861
|
+
onProgress: (progress) => {
|
|
3862
|
+
const now = Date.now();
|
|
3863
|
+
if (now - lastProgress > 500) {
|
|
3864
|
+
lastProgress = now;
|
|
3865
|
+
const kb = Math.round(progress.downloadedBytes / 1024);
|
|
3866
|
+
const total = progress.totalSegments ? ` / ${progress.totalSegments}` : '';
|
|
3867
|
+
process.stdout.write(`\r ${colors.cyan('Segments')}: ${progress.downloadedSegments}${total} | ${colors.cyan('Downloaded')}: ${kb}kb `);
|
|
3868
|
+
}
|
|
3869
|
+
},
|
|
3870
|
+
}).download(outputFile);
|
|
3871
|
+
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
3872
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
3873
|
+
console.log(colors.green(`✔ Downloaded to ${outputFile}`) + colors.gray(` (${duration}s)`));
|
|
3874
|
+
this.lastResponse = { file: outputFile, duration };
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
catch (error) {
|
|
3878
|
+
console.error(colors.red(`HLS Error: ${error.message}`));
|
|
3879
|
+
}
|
|
3880
|
+
console.log('');
|
|
3881
|
+
}
|
|
3882
|
+
async runRobots(url) {
|
|
3883
|
+
const targetUrl = url || this.baseUrl;
|
|
3884
|
+
if (!targetUrl) {
|
|
3885
|
+
console.log(colors.yellow('Usage: robots <url>'));
|
|
3886
|
+
console.log(colors.gray(' Example: robots https://example.com'));
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
let robotsUrl = targetUrl;
|
|
3890
|
+
if (!robotsUrl.includes('/robots.txt')) {
|
|
3891
|
+
robotsUrl = new URL('/robots.txt', robotsUrl.startsWith('http') ? robotsUrl : `https://${robotsUrl}`).toString();
|
|
3892
|
+
}
|
|
3893
|
+
console.log(colors.gray(`Fetching ${robotsUrl}...`));
|
|
3894
|
+
try {
|
|
3895
|
+
const response = await this.client.get(robotsUrl);
|
|
3896
|
+
if (!response.ok) {
|
|
3897
|
+
console.log(colors.yellow(`robots.txt not found (${response.status})`));
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
const content = await response.text();
|
|
3901
|
+
console.log(colors.bold(colors.cyan('robots.txt Analysis')));
|
|
3902
|
+
console.log('');
|
|
3903
|
+
const lines = content.split('\n');
|
|
3904
|
+
let currentAgent = '*';
|
|
3905
|
+
const agents = {};
|
|
3906
|
+
const sitemaps = [];
|
|
3907
|
+
for (const line of lines) {
|
|
3908
|
+
const trimmed = line.trim();
|
|
3909
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
3910
|
+
continue;
|
|
3911
|
+
const [directive, ...rest] = trimmed.split(':');
|
|
3912
|
+
const value = rest.join(':').trim();
|
|
3913
|
+
if (directive.toLowerCase() === 'user-agent') {
|
|
3914
|
+
currentAgent = value;
|
|
3915
|
+
if (!agents[currentAgent]) {
|
|
3916
|
+
agents[currentAgent] = { allow: [], disallow: [] };
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
else if (directive.toLowerCase() === 'allow') {
|
|
3920
|
+
agents[currentAgent] = agents[currentAgent] || { allow: [], disallow: [] };
|
|
3921
|
+
agents[currentAgent].allow.push(value);
|
|
3922
|
+
}
|
|
3923
|
+
else if (directive.toLowerCase() === 'disallow') {
|
|
3924
|
+
agents[currentAgent] = agents[currentAgent] || { allow: [], disallow: [] };
|
|
3925
|
+
agents[currentAgent].disallow.push(value);
|
|
3926
|
+
}
|
|
3927
|
+
else if (directive.toLowerCase() === 'sitemap') {
|
|
3928
|
+
sitemaps.push(value);
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
for (const [agent, rules] of Object.entries(agents)) {
|
|
3932
|
+
console.log(colors.bold(` User-Agent: ${agent}`));
|
|
3933
|
+
if (rules.disallow.length > 0) {
|
|
3934
|
+
console.log(colors.red(` Disallow: ${rules.disallow.slice(0, 5).join(', ')}${rules.disallow.length > 5 ? '...' : ''}`));
|
|
3935
|
+
}
|
|
3936
|
+
if (rules.allow.length > 0) {
|
|
3937
|
+
console.log(colors.green(` Allow: ${rules.allow.slice(0, 5).join(', ')}${rules.allow.length > 5 ? '...' : ''}`));
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
if (sitemaps.length > 0) {
|
|
3941
|
+
console.log('');
|
|
3942
|
+
console.log(colors.bold(' Sitemaps:'));
|
|
3943
|
+
for (const sitemap of sitemaps) {
|
|
3944
|
+
console.log(` ${colors.cyan(sitemap)}`);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
this.lastResponse = { agents, sitemaps, content };
|
|
3948
|
+
}
|
|
3949
|
+
catch (error) {
|
|
3950
|
+
console.error(colors.red(`Error: ${error.message}`));
|
|
3951
|
+
}
|
|
3952
|
+
console.log('');
|
|
3953
|
+
}
|
|
3954
|
+
async runSitemap(url) {
|
|
3955
|
+
const targetUrl = url || this.baseUrl;
|
|
3956
|
+
if (!targetUrl) {
|
|
3957
|
+
console.log(colors.yellow('Usage: sitemap <url>'));
|
|
3958
|
+
console.log(colors.gray(' Example: sitemap https://example.com'));
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
let sitemapUrl = targetUrl;
|
|
3962
|
+
if (!sitemapUrl.includes('sitemap')) {
|
|
3963
|
+
sitemapUrl = new URL('/sitemap.xml', sitemapUrl.startsWith('http') ? sitemapUrl : `https://${sitemapUrl}`).toString();
|
|
3964
|
+
}
|
|
3965
|
+
console.log(colors.gray(`Fetching ${sitemapUrl}...`));
|
|
3966
|
+
try {
|
|
3967
|
+
const response = await this.client.get(sitemapUrl);
|
|
3968
|
+
if (!response.ok) {
|
|
3969
|
+
console.log(colors.yellow(`sitemap.xml not found (${response.status})`));
|
|
3970
|
+
return;
|
|
3971
|
+
}
|
|
3972
|
+
const content = await response.text();
|
|
3973
|
+
console.log(colors.bold(colors.cyan('Sitemap Analysis')));
|
|
3974
|
+
console.log('');
|
|
3975
|
+
const { parseXML } = await import('../../plugins/xml.js');
|
|
3976
|
+
const parsed = parseXML(content);
|
|
3977
|
+
const isIndex = content.includes('<sitemapindex');
|
|
3978
|
+
if (isIndex) {
|
|
3979
|
+
const sitemaps = parsed.sitemapindex?.sitemap || [];
|
|
3980
|
+
console.log(` ${colors.cyan('Type')}: Sitemap Index`);
|
|
3981
|
+
console.log(` ${colors.cyan('Sitemaps')}: ${Array.isArray(sitemaps) ? sitemaps.length : 1}`);
|
|
3982
|
+
console.log('');
|
|
3983
|
+
const items = Array.isArray(sitemaps) ? sitemaps.slice(0, 10) : [sitemaps];
|
|
3984
|
+
for (const sitemap of items) {
|
|
3985
|
+
console.log(` ${colors.gray('•')} ${sitemap.loc}`);
|
|
3986
|
+
}
|
|
3987
|
+
if (Array.isArray(sitemaps) && sitemaps.length > 10) {
|
|
3988
|
+
console.log(colors.gray(` ... and ${sitemaps.length - 10} more`));
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
else {
|
|
3992
|
+
const urls = parsed.urlset?.url || [];
|
|
3993
|
+
const urlList = Array.isArray(urls) ? urls : [urls];
|
|
3994
|
+
console.log(` ${colors.cyan('Type')}: URL Sitemap`);
|
|
3995
|
+
console.log(` ${colors.cyan('URLs')}: ${urlList.length}`);
|
|
3996
|
+
console.log('');
|
|
3997
|
+
for (const url of urlList.slice(0, 10)) {
|
|
3998
|
+
const loc = url.loc || '';
|
|
3999
|
+
const lastmod = url.lastmod ? colors.gray(` (${url.lastmod})`) : '';
|
|
4000
|
+
console.log(` ${colors.gray('•')} ${loc.slice(0, 60)}${loc.length > 60 ? '...' : ''}${lastmod}`);
|
|
4001
|
+
}
|
|
4002
|
+
if (urlList.length > 10) {
|
|
4003
|
+
console.log(colors.gray(` ... and ${urlList.length - 10} more`));
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
this.lastResponse = parsed;
|
|
4007
|
+
}
|
|
4008
|
+
catch (error) {
|
|
4009
|
+
console.error(colors.red(`Error: ${error.message}`));
|
|
4010
|
+
}
|
|
4011
|
+
console.log('');
|
|
4012
|
+
}
|
|
4013
|
+
async runLlms(url) {
|
|
4014
|
+
const targetUrl = url || this.baseUrl;
|
|
4015
|
+
if (!targetUrl) {
|
|
4016
|
+
console.log(colors.yellow('Usage: llms <url>'));
|
|
4017
|
+
console.log(colors.gray(' Example: llms https://example.com'));
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
let llmsUrl = targetUrl;
|
|
4021
|
+
if (!llmsUrl.includes('/llms.txt')) {
|
|
4022
|
+
llmsUrl = new URL('/llms.txt', llmsUrl.startsWith('http') ? llmsUrl : `https://${llmsUrl}`).toString();
|
|
4023
|
+
}
|
|
4024
|
+
console.log(colors.gray(`Fetching ${llmsUrl}...`));
|
|
4025
|
+
try {
|
|
4026
|
+
const response = await this.client.get(llmsUrl);
|
|
4027
|
+
if (!response.ok) {
|
|
4028
|
+
console.log(colors.yellow(`llms.txt not found (${response.status})`));
|
|
4029
|
+
console.log(colors.gray(' This file is optional and used for AI/LLM optimization.'));
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
const content = await response.text();
|
|
4033
|
+
console.log(colors.bold(colors.cyan('llms.txt Analysis')));
|
|
4034
|
+
console.log('');
|
|
4035
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
4036
|
+
console.log(` ${colors.cyan('Lines')}: ${lines.length}`);
|
|
4037
|
+
console.log('');
|
|
4038
|
+
for (const line of lines.slice(0, 20)) {
|
|
4039
|
+
if (line.startsWith('#')) {
|
|
4040
|
+
console.log(colors.gray(` ${line}`));
|
|
4041
|
+
}
|
|
4042
|
+
else {
|
|
4043
|
+
console.log(` ${line}`);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
if (lines.length > 20) {
|
|
4047
|
+
console.log(colors.gray(` ... and ${lines.length - 20} more lines`));
|
|
4048
|
+
}
|
|
4049
|
+
this.lastResponse = { content, lines: lines.length };
|
|
4050
|
+
}
|
|
4051
|
+
catch (error) {
|
|
4052
|
+
console.error(colors.red(`Error: ${error.message}`));
|
|
4053
|
+
}
|
|
4054
|
+
console.log('');
|
|
4055
|
+
}
|
|
4056
|
+
async runSftp(args) {
|
|
4057
|
+
if (args.length < 2 || args[0] === 'help') {
|
|
4058
|
+
console.log(colors.bold('SFTP Client'));
|
|
4059
|
+
console.log('');
|
|
4060
|
+
console.log(colors.yellow('Usage: sftp <host> <command> [args...] [options...]'));
|
|
4061
|
+
console.log('');
|
|
4062
|
+
console.log(colors.gray('Commands:'));
|
|
4063
|
+
console.log(' ls <path> List directory');
|
|
4064
|
+
console.log(' get <remote> [local] Download file');
|
|
4065
|
+
console.log(' put <local> [remote] Upload file');
|
|
4066
|
+
console.log('');
|
|
4067
|
+
console.log(colors.gray('Options:'));
|
|
4068
|
+
console.log(' user=<username> Username (default: root)');
|
|
4069
|
+
console.log(' pass=<password> Password');
|
|
4070
|
+
console.log(' key=<path> Path to private key');
|
|
4071
|
+
console.log(' port=<number> Port (default: 22)');
|
|
4072
|
+
console.log('');
|
|
4073
|
+
console.log(colors.gray('Examples:'));
|
|
4074
|
+
console.log(' sftp myserver.com ls /var/www user=admin key=~/.ssh/id_rsa');
|
|
4075
|
+
console.log(' sftp myserver.com get /etc/hosts hosts.txt user=root pass=secret');
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
const host = args[0];
|
|
4079
|
+
const cmd = args[1];
|
|
4080
|
+
let user = 'root';
|
|
4081
|
+
let password;
|
|
4082
|
+
let keyPath;
|
|
4083
|
+
let port = 22;
|
|
4084
|
+
const cmdArgs = [];
|
|
4085
|
+
for (let i = 2; i < args.length; i++) {
|
|
4086
|
+
const arg = args[i];
|
|
4087
|
+
if (arg.startsWith('user='))
|
|
4088
|
+
user = arg.slice(5);
|
|
4089
|
+
else if (arg.startsWith('pass='))
|
|
4090
|
+
password = arg.slice(5);
|
|
4091
|
+
else if (arg.startsWith('key='))
|
|
4092
|
+
keyPath = arg.slice(4);
|
|
4093
|
+
else if (arg.startsWith('port='))
|
|
4094
|
+
port = parseInt(arg.slice(5));
|
|
4095
|
+
else
|
|
4096
|
+
cmdArgs.push(arg);
|
|
4097
|
+
}
|
|
4098
|
+
try {
|
|
4099
|
+
const { createSFTP } = await import('../../protocols/sftp.js');
|
|
4100
|
+
let privateKey;
|
|
4101
|
+
if (keyPath) {
|
|
4102
|
+
const { promises: fs } = await import('node:fs');
|
|
4103
|
+
privateKey = await fs.readFile(keyPath.replace('~', process.env.HOME || ''), 'utf-8');
|
|
4104
|
+
}
|
|
4105
|
+
const sftp = createSFTP({ host, port, username: user, password, privateKey });
|
|
4106
|
+
console.log(colors.gray(`Connecting to ${host}:${port}...`));
|
|
4107
|
+
await sftp.connect();
|
|
4108
|
+
if (cmd === 'ls') {
|
|
4109
|
+
const path = cmdArgs[0] || '/';
|
|
4110
|
+
const result = await sftp.list(path);
|
|
4111
|
+
const files = result.data || [];
|
|
4112
|
+
console.log(colors.bold(`\nDirectory: ${path}\n`));
|
|
4113
|
+
for (const file of files) {
|
|
4114
|
+
const icon = file.type === 'directory' ? '📁' : '📄';
|
|
4115
|
+
const size = file.type === 'directory' ? '' : ` (${file.size} bytes)`;
|
|
4116
|
+
console.log(` ${icon} ${file.name}${size}`);
|
|
4117
|
+
}
|
|
4118
|
+
console.log(colors.gray(`\nTotal: ${files.length} items`));
|
|
4119
|
+
this.lastResponse = files;
|
|
4120
|
+
}
|
|
4121
|
+
else if (cmd === 'get') {
|
|
4122
|
+
const remote = cmdArgs[0];
|
|
4123
|
+
const local = cmdArgs[1] || remote.split('/').pop() || 'download';
|
|
4124
|
+
console.log(colors.gray(`Downloading ${remote} → ${local}...`));
|
|
4125
|
+
await sftp.download(remote, local);
|
|
4126
|
+
console.log(colors.green(`✔ Downloaded: ${local}`));
|
|
4127
|
+
}
|
|
4128
|
+
else if (cmd === 'put') {
|
|
4129
|
+
const local = cmdArgs[0];
|
|
4130
|
+
const remote = cmdArgs[1] || local.split('/').pop() || 'upload';
|
|
4131
|
+
console.log(colors.gray(`Uploading ${local} → ${remote}...`));
|
|
4132
|
+
await sftp.upload(local, remote);
|
|
4133
|
+
console.log(colors.green(`✔ Uploaded: ${remote}`));
|
|
4134
|
+
}
|
|
4135
|
+
else {
|
|
4136
|
+
console.log(colors.yellow(`Unknown SFTP command: ${cmd}`));
|
|
4137
|
+
}
|
|
4138
|
+
await sftp.close();
|
|
4139
|
+
}
|
|
4140
|
+
catch (error) {
|
|
4141
|
+
console.error(colors.red(`SFTP Error: ${error.message}`));
|
|
4142
|
+
}
|
|
4143
|
+
console.log('');
|
|
4144
|
+
}
|
|
4145
|
+
async runSse(url, args = []) {
|
|
4146
|
+
if (!url) {
|
|
4147
|
+
console.log(colors.yellow('Usage: sse <url>'));
|
|
4148
|
+
console.log(colors.gray(' Example: sse https://api.example.com/events'));
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
if (!url.startsWith('http')) {
|
|
4152
|
+
url = this.baseUrl ? `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}` : `https://${url}`;
|
|
4153
|
+
}
|
|
4154
|
+
console.log(colors.cyan('SSE Client'));
|
|
4155
|
+
console.log(colors.gray(`Connecting to ${url}...`));
|
|
4156
|
+
console.log(colors.gray('Press Ctrl+C to disconnect\n'));
|
|
4157
|
+
try {
|
|
4158
|
+
const response = await this.client.get(url, {
|
|
4159
|
+
headers: {
|
|
4160
|
+
'Accept': 'text/event-stream',
|
|
4161
|
+
'Cache-Control': 'no-cache',
|
|
4162
|
+
},
|
|
4163
|
+
});
|
|
4164
|
+
if (!response.ok) {
|
|
4165
|
+
console.log(colors.red(`HTTP Error: ${response.status} ${response.statusText}`));
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
console.log(colors.green('✔ Connected\n'));
|
|
4169
|
+
for await (const event of response.sse()) {
|
|
4170
|
+
const timestamp = colors.gray(new Date().toISOString().split('T')[1].slice(0, 8));
|
|
4171
|
+
if (event.event && event.event !== 'message') {
|
|
4172
|
+
console.log(`${timestamp} ${colors.yellow(`[${event.event}]`)} ${event.data}`);
|
|
4173
|
+
}
|
|
4174
|
+
else {
|
|
4175
|
+
console.log(`${timestamp} ${event.data}`);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
catch (error) {
|
|
4180
|
+
if (error.name !== 'AbortError') {
|
|
4181
|
+
console.error(colors.red(`SSE Error: ${error.message}`));
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
console.log('');
|
|
4185
|
+
}
|
|
4186
|
+
async runSoap(args) {
|
|
4187
|
+
if (args.length < 2 || args[0] === 'help') {
|
|
4188
|
+
console.log(colors.bold('SOAP Client'));
|
|
4189
|
+
console.log('');
|
|
4190
|
+
console.log(colors.yellow('Usage: soap <url> <action> [params...]'));
|
|
4191
|
+
console.log('');
|
|
4192
|
+
console.log(colors.gray('Examples:'));
|
|
4193
|
+
console.log(' soap https://api.example.com/soap GetUser userId=123');
|
|
4194
|
+
console.log(' soap api.com/ws GetWeather city="New York"');
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
let url = args[0];
|
|
4198
|
+
const action = args[1];
|
|
4199
|
+
const params = {};
|
|
4200
|
+
if (!url.startsWith('http')) {
|
|
4201
|
+
url = this.baseUrl ? `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}` : `https://${url}`;
|
|
4202
|
+
}
|
|
4203
|
+
for (let i = 2; i < args.length; i++) {
|
|
4204
|
+
const [key, ...rest] = args[i].split('=');
|
|
4205
|
+
params[key] = rest.join('=').replace(/^["']|["']$/g, '');
|
|
4206
|
+
}
|
|
4207
|
+
console.log(colors.gray(`SOAP ${action} → ${url}`));
|
|
4208
|
+
try {
|
|
4209
|
+
const { createSoapClient } = await import('../../plugins/soap.js');
|
|
4210
|
+
const soapClient = createSoapClient(this.client, { endpoint: url });
|
|
4211
|
+
const result = await soapClient.call(action, params);
|
|
4212
|
+
console.log(colors.green('✔ Response:'));
|
|
4213
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4214
|
+
this.lastResponse = result;
|
|
4215
|
+
}
|
|
4216
|
+
catch (error) {
|
|
4217
|
+
console.error(colors.red(`SOAP Error: ${error.message}`));
|
|
4218
|
+
}
|
|
4219
|
+
console.log('');
|
|
4220
|
+
}
|
|
4221
|
+
async runOdata(args) {
|
|
4222
|
+
if (args.length < 2 || args[0] === 'help') {
|
|
4223
|
+
console.log(colors.bold('OData Client'));
|
|
4224
|
+
console.log('');
|
|
4225
|
+
console.log(colors.yellow('Usage: odata <url> <entity> [options...]'));
|
|
4226
|
+
console.log('');
|
|
4227
|
+
console.log(colors.gray('Options:'));
|
|
4228
|
+
console.log(' filter=<expr> OData filter expression');
|
|
4229
|
+
console.log(' select=<fields> Comma-separated fields');
|
|
4230
|
+
console.log(' orderby=<field> Order by field');
|
|
4231
|
+
console.log(' top=<n> Limit results');
|
|
4232
|
+
console.log(' expand=<nav> Expand navigation property');
|
|
4233
|
+
console.log('');
|
|
4234
|
+
console.log(colors.gray('Examples:'));
|
|
4235
|
+
console.log(' odata https://services.odata.org/V4/Northwind/Northwind.svc Products');
|
|
4236
|
+
console.log(' odata api.com/odata Customers filter="Country eq \'USA\'" top=10');
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
4239
|
+
let url = args[0];
|
|
4240
|
+
const entity = args[1];
|
|
4241
|
+
let filter;
|
|
4242
|
+
let select;
|
|
4243
|
+
let orderby;
|
|
4244
|
+
let top;
|
|
4245
|
+
let expand;
|
|
4246
|
+
if (!url.startsWith('http')) {
|
|
4247
|
+
url = this.baseUrl ? `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}` : `https://${url}`;
|
|
4248
|
+
}
|
|
4249
|
+
for (let i = 2; i < args.length; i++) {
|
|
4250
|
+
const arg = args[i];
|
|
4251
|
+
if (arg.startsWith('filter='))
|
|
4252
|
+
filter = arg.slice(7).replace(/^["']|["']$/g, '');
|
|
4253
|
+
else if (arg.startsWith('select='))
|
|
4254
|
+
select = arg.slice(7);
|
|
4255
|
+
else if (arg.startsWith('orderby='))
|
|
4256
|
+
orderby = arg.slice(8);
|
|
4257
|
+
else if (arg.startsWith('top='))
|
|
4258
|
+
top = parseInt(arg.slice(4));
|
|
4259
|
+
else if (arg.startsWith('expand='))
|
|
4260
|
+
expand = arg.slice(7);
|
|
4261
|
+
}
|
|
4262
|
+
console.log(colors.gray(`OData Query: ${url}/${entity}`));
|
|
4263
|
+
try {
|
|
4264
|
+
const { createODataClient } = await import('../../plugins/odata.js');
|
|
4265
|
+
const odataClient = createODataClient(this.client, { serviceRoot: url });
|
|
4266
|
+
let query = odataClient.query(entity);
|
|
4267
|
+
if (select)
|
|
4268
|
+
query = query.select(...select.split(',').map((s) => s.trim()));
|
|
4269
|
+
if (filter)
|
|
4270
|
+
query = query.filter(filter);
|
|
4271
|
+
if (orderby)
|
|
4272
|
+
query = query.orderBy(orderby);
|
|
4273
|
+
if (top)
|
|
4274
|
+
query = query.top(top);
|
|
4275
|
+
if (expand)
|
|
4276
|
+
query = query.expand(expand);
|
|
4277
|
+
console.log(colors.gray(`Query: ${query.toUrl()}\n`));
|
|
4278
|
+
const results = await query.get();
|
|
4279
|
+
const items = results.value || [results];
|
|
4280
|
+
console.log(colors.green(`✔ Results: ${Array.isArray(items) ? items.length : 1} items`));
|
|
4281
|
+
console.log(JSON.stringify(results, null, 2));
|
|
4282
|
+
this.lastResponse = results;
|
|
4283
|
+
}
|
|
4284
|
+
catch (error) {
|
|
4285
|
+
console.error(colors.red(`OData Error: ${error.message}`));
|
|
4286
|
+
}
|
|
4287
|
+
console.log('');
|
|
4288
|
+
}
|
|
4289
|
+
async runProxy(args) {
|
|
4290
|
+
if (args.length < 2 || args[0] === 'help') {
|
|
4291
|
+
console.log(colors.bold('Proxy Request'));
|
|
4292
|
+
console.log('');
|
|
4293
|
+
console.log(colors.yellow('Usage: proxy <proxy-url> <target-url> [method] [params...]'));
|
|
4294
|
+
console.log('');
|
|
4295
|
+
console.log(colors.gray('Examples:'));
|
|
4296
|
+
console.log(' proxy http://proxy.example.com:8080 https://api.com/data');
|
|
4297
|
+
console.log(' proxy socks5://127.0.0.1:1080 api.com/users POST name=John');
|
|
4298
|
+
return;
|
|
4299
|
+
}
|
|
4300
|
+
const proxyUrl = args[0];
|
|
4301
|
+
let targetUrl = args[1];
|
|
4302
|
+
let method = 'GET';
|
|
4303
|
+
const params = {};
|
|
4304
|
+
if (!targetUrl.startsWith('http')) {
|
|
4305
|
+
targetUrl = `https://${targetUrl}`;
|
|
4306
|
+
}
|
|
4307
|
+
for (let i = 2; i < args.length; i++) {
|
|
4308
|
+
const arg = args[i];
|
|
4309
|
+
if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(arg.toUpperCase())) {
|
|
4310
|
+
method = arg.toUpperCase();
|
|
4311
|
+
}
|
|
4312
|
+
else if (arg.includes('=')) {
|
|
4313
|
+
const [key, ...rest] = arg.split('=');
|
|
4314
|
+
params[key] = rest.join('=');
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
console.log(colors.gray(`Proxy: ${proxyUrl}`));
|
|
4318
|
+
console.log(colors.gray(`Target: ${method} ${targetUrl}`));
|
|
4319
|
+
try {
|
|
4320
|
+
const { createClient } = await import('../../core/client.js');
|
|
4321
|
+
const client = createClient({ proxy: { url: proxyUrl } });
|
|
4322
|
+
const hasBody = Object.keys(params).length > 0;
|
|
4323
|
+
const response = hasBody
|
|
4324
|
+
? await client[method.toLowerCase()](targetUrl, { json: params })
|
|
4325
|
+
: await client[method.toLowerCase()](targetUrl);
|
|
4326
|
+
console.log(colors.green(`✔ ${response.status} ${response.statusText}`));
|
|
4327
|
+
const body = await response.text();
|
|
4328
|
+
try {
|
|
4329
|
+
const json = JSON.parse(body);
|
|
4330
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4331
|
+
this.lastResponse = json;
|
|
4332
|
+
}
|
|
4333
|
+
catch {
|
|
4334
|
+
console.log(body);
|
|
4335
|
+
this.lastResponse = body;
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
catch (error) {
|
|
4339
|
+
console.error(colors.red(`Proxy Error: ${error.message}`));
|
|
4340
|
+
}
|
|
4341
|
+
console.log('');
|
|
4342
|
+
}
|
|
4343
|
+
async runUpload(args) {
|
|
4344
|
+
if (args.length < 2 || args[0] === 'help') {
|
|
4345
|
+
console.log(colors.bold('File Upload'));
|
|
4346
|
+
console.log('');
|
|
4347
|
+
console.log(colors.yellow('Usage: upload <url> <file> [field=name]'));
|
|
4348
|
+
console.log('');
|
|
4349
|
+
console.log(colors.gray('Examples:'));
|
|
4350
|
+
console.log(' upload https://api.example.com/files ./image.png');
|
|
4351
|
+
console.log(' upload api.com/upload document.pdf field=document');
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
let url = args[0];
|
|
4355
|
+
const file = args[1];
|
|
4356
|
+
let fieldName = 'file';
|
|
4357
|
+
if (!url.startsWith('http')) {
|
|
4358
|
+
url = this.baseUrl ? `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}` : `https://${url}`;
|
|
4359
|
+
}
|
|
4360
|
+
for (let i = 2; i < args.length; i++) {
|
|
4361
|
+
if (args[i].startsWith('field=')) {
|
|
4362
|
+
fieldName = args[i].slice(6);
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
try {
|
|
4366
|
+
const { promises: fs } = await import('node:fs');
|
|
4367
|
+
const path = await import('node:path');
|
|
4368
|
+
await fs.access(file);
|
|
4369
|
+
const stats = await fs.stat(file);
|
|
4370
|
+
const fileContent = await fs.readFile(file);
|
|
4371
|
+
console.log(colors.gray(`Uploading ${path.basename(file)} (${(stats.size / 1024).toFixed(1)} KB)...`));
|
|
4372
|
+
const boundary = `----ReckerBoundary${Date.now()}`;
|
|
4373
|
+
const filename = path.basename(file);
|
|
4374
|
+
const bodyParts = [
|
|
4375
|
+
`--${boundary}`,
|
|
4376
|
+
`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"`,
|
|
4377
|
+
'Content-Type: application/octet-stream',
|
|
4378
|
+
'',
|
|
4379
|
+
''
|
|
4380
|
+
];
|
|
4381
|
+
const header = Buffer.from(bodyParts.join('\r\n'));
|
|
4382
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
4383
|
+
const body = Buffer.concat([header, fileContent, footer]);
|
|
4384
|
+
const response = await this.client.post(url, body, {
|
|
4385
|
+
headers: {
|
|
4386
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
4387
|
+
},
|
|
4388
|
+
});
|
|
4389
|
+
console.log(colors.green(`✔ Upload complete: ${response.status} ${response.statusText}`));
|
|
4390
|
+
const responseBody = await response.text();
|
|
4391
|
+
if (responseBody) {
|
|
4392
|
+
try {
|
|
4393
|
+
const json = JSON.parse(responseBody);
|
|
4394
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4395
|
+
this.lastResponse = json;
|
|
4396
|
+
}
|
|
4397
|
+
catch {
|
|
4398
|
+
console.log(responseBody);
|
|
4399
|
+
this.lastResponse = responseBody;
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
catch (error) {
|
|
4404
|
+
console.error(colors.red(`Upload Error: ${error.message}`));
|
|
4405
|
+
}
|
|
4406
|
+
console.log('');
|
|
4407
|
+
}
|
|
4408
|
+
async runDownload(args) {
|
|
4409
|
+
if (args.length === 0 || args[0] === 'help') {
|
|
4410
|
+
console.log(colors.bold('File Download'));
|
|
4411
|
+
console.log('');
|
|
4412
|
+
console.log(colors.yellow('Usage: download <url> [output]'));
|
|
4413
|
+
console.log('');
|
|
4414
|
+
console.log(colors.gray('Examples:'));
|
|
4415
|
+
console.log(' download https://example.com/file.zip');
|
|
4416
|
+
console.log(' download https://api.com/export.csv data.csv');
|
|
4417
|
+
return;
|
|
4418
|
+
}
|
|
4419
|
+
let url = args[0];
|
|
4420
|
+
const output = args[1];
|
|
4421
|
+
if (!url.startsWith('http')) {
|
|
4422
|
+
url = this.baseUrl ? `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}` : `https://${url}`;
|
|
4423
|
+
}
|
|
4424
|
+
try {
|
|
4425
|
+
const { downloadToFile } = await import('../../utils/download.js');
|
|
4426
|
+
const path = await import('node:path');
|
|
4427
|
+
const { promises: fs } = await import('node:fs');
|
|
4428
|
+
const urlPath = new URL(url).pathname;
|
|
4429
|
+
const filename = output || path.basename(urlPath) || 'download';
|
|
4430
|
+
console.log(colors.gray(`Downloading to ${filename}...`));
|
|
4431
|
+
const result = await downloadToFile(this.client, url, filename, {
|
|
4432
|
+
onProgress: (progress) => {
|
|
4433
|
+
const total = progress.total || 0;
|
|
4434
|
+
const pct = total > 0 ? Math.round((progress.loaded / total) * 100) : 0;
|
|
4435
|
+
const downloaded = (progress.loaded / 1024 / 1024).toFixed(1);
|
|
4436
|
+
const totalMB = total > 0 ? (total / 1024 / 1024).toFixed(1) : '?';
|
|
4437
|
+
const bar = '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5));
|
|
4438
|
+
process.stdout.write(`\r [${bar}] ${pct}% (${downloaded}/${totalMB} MB)`);
|
|
4439
|
+
},
|
|
4440
|
+
});
|
|
4441
|
+
process.stdout.write('\n');
|
|
4442
|
+
const stats = await fs.stat(filename);
|
|
4443
|
+
console.log(colors.green(`✔ Downloaded: ${filename} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`));
|
|
4444
|
+
this.lastResponse = { file: filename, size: stats.size };
|
|
4445
|
+
}
|
|
4446
|
+
catch (error) {
|
|
4447
|
+
console.error(colors.red(`Download Error: ${error.message}`));
|
|
4448
|
+
}
|
|
4449
|
+
console.log('');
|
|
4450
|
+
}
|
|
4451
|
+
printHelp() {
|
|
4452
|
+
console.log(`
|
|
4453
|
+
${colors.bold(colors.cyan('Rek Console Help'))}
|
|
4454
|
+
|
|
4455
|
+
${colors.bold('Core Commands:')}
|
|
4456
|
+
${colors.green('url <url>')} Set persistent Base URL.
|
|
4457
|
+
${colors.green('set <key>=<val>')} Set a session variable.
|
|
2768
4458
|
${colors.green('vars')} List all session and env variables.
|
|
2769
4459
|
${colors.green('env [path]')} Load .env file (default: ./.env).
|
|
2770
4460
|
${colors.green('clear')} Clear the screen.
|
|
@@ -2837,6 +4527,40 @@ ${colors.bold('Network:')}
|
|
|
2837
4527
|
${colors.white('--limit=100')} ${colors.gray('Maximum pages to crawl')}
|
|
2838
4528
|
${colors.white('--concurrency=5')} ${colors.gray('Parallel requests')}
|
|
2839
4529
|
|
|
4530
|
+
${colors.bold('Protocols:')}
|
|
4531
|
+
${colors.green('ftp <host> <cmd>')} FTP client (ls, get, put, rm, mkdir).
|
|
4532
|
+
${colors.gray('Options:')} ${colors.white('user=...')} ${colors.white('pass=...')} ${colors.white('port=...')} ${colors.white('secure')}
|
|
4533
|
+
${colors.green('telnet <host> [port]')} Interactive Telnet session.
|
|
4534
|
+
${colors.green('graphql <url> <query>')} Execute GraphQL query.
|
|
4535
|
+
${colors.gray('Variables:')} ${colors.white('key=value')} ${colors.gray('pairs')}
|
|
4536
|
+
${colors.gray('File:')} ${colors.white('@file.graphql')}
|
|
4537
|
+
${colors.green('jsonrpc <url> <method>')} Execute JSON-RPC 2.0 call.
|
|
4538
|
+
${colors.gray('Params:')} ${colors.white('key=value')} ${colors.gray('(named) or positional')}
|
|
4539
|
+
${colors.green('hls <url> [cmd]')} HLS streaming client (info, download).
|
|
4540
|
+
${colors.gray('Options:')} ${colors.white('output=file.ts')} ${colors.white('quality=highest|lowest')}
|
|
4541
|
+
|
|
4542
|
+
${colors.bold('HAR Recording:')}
|
|
4543
|
+
${colors.green('har:record <file>')} Start recording HTTP requests to HAR file.
|
|
4544
|
+
${colors.green('har:stop')} Stop recording.
|
|
4545
|
+
${colors.green('har:play <file>')} Replay requests from HAR file.
|
|
4546
|
+
${colors.green('har:info <file>')} Show HAR file information.
|
|
4547
|
+
|
|
4548
|
+
${colors.bold('Web Analysis:')}
|
|
4549
|
+
${colors.green('robots [url]')} Analyze robots.txt file.
|
|
4550
|
+
${colors.green('sitemap [url]')} Analyze sitemap.xml file.
|
|
4551
|
+
${colors.green('llms [url]')} Analyze llms.txt file (AI optimization).
|
|
4552
|
+
|
|
4553
|
+
${colors.bold('Advanced Protocols:')}
|
|
4554
|
+
${colors.green('sftp <host> <cmd>')} SFTP client (ls, get, put). ${colors.gray('Options: user= key= port=')}
|
|
4555
|
+
${colors.green('sse <url>')} Connect to Server-Sent Events stream.
|
|
4556
|
+
${colors.green('soap <url> <action>')} Make SOAP request. ${colors.gray('Params: key=value')}
|
|
4557
|
+
${colors.green('odata <url> <entity>')} Query OData service. ${colors.gray('Options: filter= select= top=')}
|
|
4558
|
+
${colors.green('proxy <proxy> <url>')} Make request through a proxy.
|
|
4559
|
+
|
|
4560
|
+
${colors.bold('File Transfer:')}
|
|
4561
|
+
${colors.green('upload <url> <file>')} Upload a file to a URL.
|
|
4562
|
+
${colors.green('download <url>')} Download a file with progress.
|
|
4563
|
+
|
|
2840
4564
|
${colors.bold('Documentation:')}
|
|
2841
4565
|
${colors.green('? <query>')} Search Recker documentation.
|
|
2842
4566
|
${colors.green('search <query>')} Alias for ? (hybrid fuzzy+semantic search).
|