recker 1.0.30 → 1.0.32-next.02f2bae
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 +2653 -197
- 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/index.js
CHANGED
|
@@ -450,7 +450,7 @@ ${colors.bold(colors.yellow('Checks:'))}
|
|
|
450
450
|
images: report.images,
|
|
451
451
|
openGraph: report.social.openGraph,
|
|
452
452
|
twitterCard: report.social.twitterCard,
|
|
453
|
-
|
|
453
|
+
structuredData: report.structuredData,
|
|
454
454
|
technical: report.technical,
|
|
455
455
|
checks: report.checks,
|
|
456
456
|
summary: {
|
|
@@ -482,6 +482,18 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
|
|
|
482
482
|
if (report.metaDescription) {
|
|
483
483
|
console.log(`${colors.bold('Description:')} ${colors.gray(report.metaDescription.text.slice(0, 80))}${report.metaDescription.text.length > 80 ? '...' : ''}`);
|
|
484
484
|
}
|
|
485
|
+
if (report.openGraph) {
|
|
486
|
+
console.log(`${colors.bold('OpenGraph:')} ${report.openGraph.title ? colors.green('✔') : colors.red('✖')} title, ${report.openGraph.description ? colors.green('✔') : colors.red('✖')} description, ${report.openGraph.image ? colors.green('✔') : colors.red('✖')} image`);
|
|
487
|
+
}
|
|
488
|
+
if (report.twitterCard) {
|
|
489
|
+
console.log(`${colors.bold('Twitter Card:')} ${report.twitterCard.card || 'none'} ${report.twitterCard.title ? colors.green('✔') : colors.red('✖')} title, ${report.twitterCard.image ? colors.green('✔') : colors.red('✖')} image`);
|
|
490
|
+
}
|
|
491
|
+
if (report.structuredData.count > 0) {
|
|
492
|
+
console.log(`${colors.bold('Structured Data:')} ${report.structuredData.count} schema(s) - ${report.structuredData.types.join(', ')}`);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
console.log(`${colors.bold('Structured Data:')} ${colors.yellow('None detected')}`);
|
|
496
|
+
}
|
|
485
497
|
console.log('');
|
|
486
498
|
console.log(`${colors.bold('Content Metrics:')}`);
|
|
487
499
|
console.log(` ${colors.gray('Words:')} ${report.content.wordCount} ${colors.gray('Reading time:')} ~${report.content.readingTimeMinutes} min`);
|
|
@@ -520,10 +532,6 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
|
|
|
520
532
|
}
|
|
521
533
|
}
|
|
522
534
|
console.log('');
|
|
523
|
-
if (report.jsonLd.count > 0) {
|
|
524
|
-
console.log(`${colors.bold('Structured Data:')} ${report.jsonLd.types.join(', ') || 'Present'}`);
|
|
525
|
-
console.log('');
|
|
526
|
-
}
|
|
527
535
|
}
|
|
528
536
|
catch (error) {
|
|
529
537
|
console.error(colors.red(`SEO analysis failed: ${error.message}`));
|
|
@@ -531,139 +539,904 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
|
|
|
531
539
|
}
|
|
532
540
|
});
|
|
533
541
|
program
|
|
534
|
-
.command('
|
|
535
|
-
.description('
|
|
536
|
-
.argument('<url>', '
|
|
537
|
-
.option('-d, --depth <n>', 'Maximum depth to crawl', '5')
|
|
538
|
-
.option('-l, --limit <n>', 'Maximum pages to crawl', '100')
|
|
539
|
-
.option('-c, --concurrency <n>', 'Concurrent requests', '5')
|
|
540
|
-
.option('--delay <ms>', 'Delay between requests in ms', '100')
|
|
542
|
+
.command('robots')
|
|
543
|
+
.description('Validate and analyze robots.txt file')
|
|
544
|
+
.argument('<url>', 'Website URL or direct robots.txt URL')
|
|
541
545
|
.option('--format <format>', 'Output format: text (default) or json', 'text')
|
|
542
|
-
.option('-o, --output <file>', 'Write JSON results to file')
|
|
543
546
|
.addHelpText('after', `
|
|
544
547
|
${colors.bold(colors.yellow('Examples:'))}
|
|
545
|
-
${colors.green('$ rek
|
|
546
|
-
${colors.green('$ rek
|
|
547
|
-
${colors.green('$ rek
|
|
548
|
-
${colors.green('$ rek spider example.com -o results.json')} ${colors.gray('Save to file')}
|
|
548
|
+
${colors.green('$ rek robots example.com')} ${colors.gray('Validate robots.txt')}
|
|
549
|
+
${colors.green('$ rek robots example.com/robots.txt')} ${colors.gray('Direct URL')}
|
|
550
|
+
${colors.green('$ rek robots example.com --format json')} ${colors.gray('JSON output')}
|
|
549
551
|
|
|
550
|
-
${colors.bold(colors.yellow('
|
|
551
|
-
${colors.cyan('
|
|
552
|
-
${colors.cyan('
|
|
553
|
-
${colors.cyan('
|
|
554
|
-
${colors.cyan('
|
|
552
|
+
${colors.bold(colors.yellow('Checks:'))}
|
|
553
|
+
${colors.cyan('Syntax')} Valid robots.txt syntax
|
|
554
|
+
${colors.cyan('User-Agent blocks')} Defined crawl rules
|
|
555
|
+
${colors.cyan('Sitemap')} Sitemap directive present
|
|
556
|
+
${colors.cyan('Crawl-delay')} Aggressive crawl delay
|
|
557
|
+
${colors.cyan('AI Bots')} GPTBot, ClaudeBot, Anthropic blocks
|
|
555
558
|
`)
|
|
556
559
|
.action(async (url, options) => {
|
|
557
560
|
if (!url.startsWith('http'))
|
|
558
561
|
url = `https://${url}`;
|
|
562
|
+
if (!url.includes('robots.txt')) {
|
|
563
|
+
const urlObj = new URL(url);
|
|
564
|
+
url = `${urlObj.origin}/robots.txt`;
|
|
565
|
+
}
|
|
559
566
|
const isJson = options.format === 'json';
|
|
560
|
-
const maxDepth = parseInt(options.depth || '5', 10);
|
|
561
|
-
const maxPages = parseInt(options.limit || '100', 10);
|
|
562
|
-
const concurrency = parseInt(options.concurrency || '5', 10);
|
|
563
|
-
const delay = parseInt(options.delay || '100', 10);
|
|
564
|
-
const { Spider } = await import('../scrape/spider.js');
|
|
565
567
|
if (!isJson) {
|
|
566
|
-
console.log(colors.gray(
|
|
567
|
-
console.log(colors.gray(` Depth: ${maxDepth}, Limit: ${maxPages}, Concurrency: ${concurrency}\n`));
|
|
568
|
+
console.log(colors.gray(`Fetching robots.txt from ${url}...`));
|
|
568
569
|
}
|
|
569
570
|
try {
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
571
|
+
const { fetchAndValidateRobotsTxt } = await import('../seo/validators/robots.js');
|
|
572
|
+
const result = await fetchAndValidateRobotsTxt(url);
|
|
573
|
+
if (isJson) {
|
|
574
|
+
console.log(JSON.stringify(result, null, 2));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
console.log(`
|
|
578
|
+
${colors.bold(colors.cyan('🤖 Robots.txt Analysis'))}
|
|
579
|
+
${colors.gray('URL:')} ${url}
|
|
580
|
+
${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')}
|
|
581
|
+
`);
|
|
582
|
+
if (result.parseResult) {
|
|
583
|
+
const { parseResult } = result;
|
|
584
|
+
if (parseResult.userAgentBlocks.length > 0) {
|
|
585
|
+
console.log(colors.bold('User-Agent Blocks:'));
|
|
586
|
+
for (const block of parseResult.userAgentBlocks.slice(0, 5)) {
|
|
587
|
+
const agents = block.userAgents.join(', ');
|
|
588
|
+
const allowCount = block.rules.filter(r => r.type === 'allow').length;
|
|
589
|
+
const disallowCount = block.rules.filter(r => r.type === 'disallow').length;
|
|
590
|
+
console.log(` ${colors.cyan(agents)}`);
|
|
591
|
+
console.log(` ${colors.green(`Allow: ${allowCount}`)} | ${colors.red(`Disallow: ${disallowCount}`)}`);
|
|
579
592
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
593
|
+
if (parseResult.userAgentBlocks.length > 5) {
|
|
594
|
+
console.log(colors.gray(` ... and ${parseResult.userAgentBlocks.length - 5} more blocks`));
|
|
595
|
+
}
|
|
596
|
+
console.log('');
|
|
597
|
+
}
|
|
598
|
+
if (parseResult.sitemaps.length > 0) {
|
|
599
|
+
console.log(colors.bold('Sitemaps:'));
|
|
600
|
+
for (const sitemap of parseResult.sitemaps.slice(0, 3)) {
|
|
601
|
+
console.log(` ${colors.gray('→')} ${sitemap}`);
|
|
602
|
+
}
|
|
603
|
+
if (parseResult.sitemaps.length > 3) {
|
|
604
|
+
console.log(colors.gray(` ... and ${parseResult.sitemaps.length - 3} more`));
|
|
605
|
+
}
|
|
606
|
+
console.log('');
|
|
607
|
+
}
|
|
608
|
+
const aiAgents = ['gptbot', 'chatgpt-user', 'claudebot', 'claude-web', 'anthropic-ai', 'ccbot'];
|
|
609
|
+
const blockedAiBots = [];
|
|
610
|
+
for (const block of parseResult.userAgentBlocks) {
|
|
611
|
+
for (const agent of block.userAgents) {
|
|
612
|
+
if (aiAgents.includes(agent.toLowerCase())) {
|
|
613
|
+
const hasBlockAll = block.rules.some(r => r.type === 'disallow' && (r.path === '/' || r.path === '/*'));
|
|
614
|
+
if (hasBlockAll) {
|
|
615
|
+
blockedAiBots.push(agent);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (blockedAiBots.length > 0) {
|
|
621
|
+
console.log(colors.bold('AI Bots Blocked:'));
|
|
622
|
+
for (const bot of blockedAiBots) {
|
|
623
|
+
console.log(` ${colors.red('✗')} ${bot}`);
|
|
624
|
+
}
|
|
625
|
+
console.log('');
|
|
626
|
+
}
|
|
586
627
|
}
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
628
|
+
if (result.issues.length > 0) {
|
|
629
|
+
console.log(colors.bold('Issues:'));
|
|
630
|
+
for (const issue of result.issues) {
|
|
631
|
+
const icon = issue.type === 'error' ? colors.red('✗')
|
|
632
|
+
: issue.type === 'warning' ? colors.yellow('⚠')
|
|
633
|
+
: colors.gray('ℹ');
|
|
634
|
+
console.log(` ${icon} ${issue.message}`);
|
|
635
|
+
}
|
|
636
|
+
console.log('');
|
|
637
|
+
}
|
|
638
|
+
const errorCount = result.issues.filter(i => i.type === 'error').length;
|
|
639
|
+
const warningCount = result.issues.filter(i => i.type === 'warning').length;
|
|
640
|
+
if (errorCount === 0 && warningCount === 0) {
|
|
641
|
+
console.log(colors.green('✔ No issues found'));
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
console.log(`${colors.red(`${errorCount} errors`)} | ${colors.yellow(`${warningCount} warnings`)}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
console.error(colors.red(`Robots.txt analysis failed: ${error.message}`));
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
program
|
|
653
|
+
.command('sitemap')
|
|
654
|
+
.description('Validate and analyze sitemap.xml file')
|
|
655
|
+
.argument('<url>', 'Website URL or direct sitemap URL')
|
|
656
|
+
.option('--format <format>', 'Output format: text (default) or json', 'text')
|
|
657
|
+
.option('--discover', 'Discover all sitemaps via robots.txt')
|
|
658
|
+
.addHelpText('after', `
|
|
659
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
660
|
+
${colors.green('$ rek sitemap example.com')} ${colors.gray('Validate sitemap')}
|
|
661
|
+
${colors.green('$ rek sitemap example.com/sitemap.xml')} ${colors.gray('Direct URL')}
|
|
662
|
+
${colors.green('$ rek sitemap example.com --discover')} ${colors.gray('Find all sitemaps')}
|
|
663
|
+
${colors.green('$ rek sitemap example.com --format json')} ${colors.gray('JSON output')}
|
|
664
|
+
|
|
665
|
+
${colors.bold(colors.yellow('Checks:'))}
|
|
666
|
+
${colors.cyan('Structure')} Valid XML sitemap format
|
|
667
|
+
${colors.cyan('URL Count')} Within 50,000 URL limit
|
|
668
|
+
${colors.cyan('File Size')} Within 50MB limit
|
|
669
|
+
${colors.cyan('URLs')} Valid, no duplicates, same domain
|
|
670
|
+
${colors.cyan('Lastmod')} Valid dates, not in future
|
|
671
|
+
`)
|
|
672
|
+
.action(async (url, options) => {
|
|
673
|
+
if (!url.startsWith('http'))
|
|
674
|
+
url = `https://${url}`;
|
|
675
|
+
const isJson = options.format === 'json';
|
|
676
|
+
try {
|
|
677
|
+
if (options.discover) {
|
|
678
|
+
const { discoverSitemaps } = await import('../seo/validators/sitemap.js');
|
|
679
|
+
if (!isJson) {
|
|
680
|
+
console.log(colors.gray(`Discovering sitemaps for ${new URL(url).origin}...`));
|
|
612
681
|
}
|
|
682
|
+
const sitemaps = await discoverSitemaps(url);
|
|
613
683
|
if (isJson) {
|
|
614
|
-
console.log(JSON.stringify(
|
|
684
|
+
console.log(JSON.stringify({ url, sitemaps }, null, 2));
|
|
615
685
|
return;
|
|
616
686
|
}
|
|
687
|
+
console.log(`
|
|
688
|
+
${colors.bold(colors.cyan('🗺️ Sitemap Discovery'))}
|
|
689
|
+
${colors.gray('Site:')} ${new URL(url).origin}
|
|
690
|
+
${colors.gray('Found:')} ${sitemaps.length} sitemap(s)
|
|
691
|
+
`);
|
|
692
|
+
if (sitemaps.length > 0) {
|
|
693
|
+
for (const sitemap of sitemaps) {
|
|
694
|
+
console.log(` ${colors.gray('→')} ${sitemap}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
console.log(colors.yellow(' No sitemaps found in robots.txt or common locations'));
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
617
701
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
console.log('');
|
|
622
|
-
const successCount = result.pages.filter(p => !p.error).length;
|
|
623
|
-
console.log(`${colors.bold('Summary:')}`);
|
|
624
|
-
console.log(` ${colors.green('✓')} Pages crawled: ${colors.bold(String(successCount))}`);
|
|
625
|
-
console.log(` ${colors.red('✗')} Errors: ${colors.bold(String(result.errors.length))}`);
|
|
626
|
-
console.log(` ${colors.gray('○')} Unique URLs: ${result.visited.size}`);
|
|
627
|
-
console.log('');
|
|
628
|
-
const byDepth = new Map();
|
|
629
|
-
for (const page of result.pages) {
|
|
630
|
-
byDepth.set(page.depth, (byDepth.get(page.depth) || 0) + 1);
|
|
702
|
+
if (!url.includes('sitemap')) {
|
|
703
|
+
const urlObj = new URL(url);
|
|
704
|
+
url = `${urlObj.origin}/sitemap.xml`;
|
|
631
705
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
console.log(` ${colors.gray(`Depth ${depth}:`)} ${count} pages`);
|
|
706
|
+
if (!isJson) {
|
|
707
|
+
console.log(colors.gray(`Fetching sitemap from ${url}...`));
|
|
635
708
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
709
|
+
const { fetchAndValidateSitemap } = await import('../seo/validators/sitemap.js');
|
|
710
|
+
const result = await fetchAndValidateSitemap(url);
|
|
711
|
+
if (isJson) {
|
|
712
|
+
console.log(JSON.stringify(result, null, 2));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
console.log(`
|
|
716
|
+
${colors.bold(colors.cyan('🗺️ Sitemap Analysis'))}
|
|
717
|
+
${colors.gray('URL:')} ${url}
|
|
718
|
+
${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')}
|
|
719
|
+
${colors.gray('Type:')} ${result.parseResult?.type === 'sitemapindex' ? 'Sitemap Index' : 'URL Set'}
|
|
720
|
+
`);
|
|
721
|
+
if (result.parseResult) {
|
|
722
|
+
const { parseResult } = result;
|
|
723
|
+
if (parseResult.type === 'sitemapindex') {
|
|
724
|
+
console.log(colors.bold(`Sitemaps: ${parseResult.sitemaps?.length || 0}`));
|
|
725
|
+
for (const sm of (parseResult.sitemaps || []).slice(0, 5)) {
|
|
726
|
+
console.log(` ${colors.gray('→')} ${sm.loc}`);
|
|
727
|
+
if (sm.lastmod) {
|
|
728
|
+
console.log(` ${colors.gray(`Last modified: ${sm.lastmod}`)}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if ((parseResult.sitemaps?.length || 0) > 5) {
|
|
732
|
+
console.log(colors.gray(` ... and ${parseResult.sitemaps.length - 5} more`));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
console.log(colors.bold(`URLs: ${parseResult.urls?.length || 0}`));
|
|
737
|
+
const sampleUrls = (parseResult.urls || []).slice(0, 5);
|
|
738
|
+
for (const entry of sampleUrls) {
|
|
739
|
+
const path = new URL(entry.loc).pathname;
|
|
740
|
+
console.log(` ${colors.gray('→')} ${path}`);
|
|
741
|
+
}
|
|
742
|
+
if ((parseResult.urls?.length || 0) > 5) {
|
|
743
|
+
console.log(colors.gray(` ... and ${parseResult.urls.length - 5} more URLs`));
|
|
744
|
+
}
|
|
745
|
+
const urlsWithLastmod = (parseResult.urls || []).filter(u => u.lastmod).length;
|
|
746
|
+
const urlsWithPriority = (parseResult.urls || []).filter(u => u.priority !== undefined).length;
|
|
747
|
+
const urlsWithChangefreq = (parseResult.urls || []).filter(u => u.changefreq).length;
|
|
748
|
+
console.log('');
|
|
749
|
+
console.log(colors.bold('Statistics:'));
|
|
750
|
+
console.log(` ${colors.gray('With lastmod:')} ${urlsWithLastmod} (${((urlsWithLastmod / (parseResult.urls?.length || 1)) * 100).toFixed(0)}%)`);
|
|
751
|
+
console.log(` ${colors.gray('With priority:')} ${urlsWithPriority} (${((urlsWithPriority / (parseResult.urls?.length || 1)) * 100).toFixed(0)}%)`);
|
|
752
|
+
console.log(` ${colors.gray('With changefreq:')} ${urlsWithChangefreq} (${((urlsWithChangefreq / (parseResult.urls?.length || 1)) * 100).toFixed(0)}%)`);
|
|
753
|
+
}
|
|
754
|
+
console.log('');
|
|
755
|
+
}
|
|
756
|
+
if (result.issues.length > 0) {
|
|
757
|
+
console.log(colors.bold('Issues:'));
|
|
758
|
+
for (const issue of result.issues.slice(0, 10)) {
|
|
759
|
+
const icon = issue.type === 'error' ? colors.red('✗')
|
|
760
|
+
: issue.type === 'warning' ? colors.yellow('⚠')
|
|
761
|
+
: colors.gray('ℹ');
|
|
762
|
+
console.log(` ${icon} ${issue.message}`);
|
|
763
|
+
}
|
|
764
|
+
if (result.issues.length > 10) {
|
|
765
|
+
console.log(colors.gray(` ... and ${result.issues.length - 10} more issues`));
|
|
766
|
+
}
|
|
767
|
+
console.log('');
|
|
768
|
+
}
|
|
769
|
+
const errorCount = result.issues.filter(i => i.type === 'error').length;
|
|
770
|
+
const warningCount = result.issues.filter(i => i.type === 'warning').length;
|
|
771
|
+
if (errorCount === 0 && warningCount === 0) {
|
|
772
|
+
console.log(colors.green('✔ No issues found'));
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
console.log(`${colors.red(`${errorCount} errors`)} | ${colors.yellow(`${warningCount} warnings`)}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
console.error(colors.red(`Sitemap analysis failed: ${error.message}`));
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
program
|
|
784
|
+
.command('llms')
|
|
785
|
+
.description('Validate and analyze llms.txt file (AI/LLM optimization)')
|
|
786
|
+
.argument('[url]', 'Website URL or direct llms.txt URL')
|
|
787
|
+
.option('--format <format>', 'Output format: text (default) or json', 'text')
|
|
788
|
+
.option('--template', 'Generate a template llms.txt file')
|
|
789
|
+
.addHelpText('after', `
|
|
790
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
791
|
+
${colors.green('$ rek llms example.com')} ${colors.gray('Validate llms.txt')}
|
|
792
|
+
${colors.green('$ rek llms example.com/llms.txt')} ${colors.gray('Direct URL')}
|
|
793
|
+
${colors.green('$ rek llms example.com --format json')} ${colors.gray('JSON output')}
|
|
794
|
+
${colors.green('$ rek llms --template > llms.txt')} ${colors.gray('Generate template')}
|
|
795
|
+
|
|
796
|
+
${colors.bold(colors.yellow('About llms.txt:'))}
|
|
797
|
+
A proposed standard for providing LLM-friendly content.
|
|
798
|
+
Similar to robots.txt but for AI/LLM crawlers.
|
|
799
|
+
Learn more: ${colors.cyan('https://llmstxt.org')}
|
|
800
|
+
|
|
801
|
+
${colors.bold(colors.yellow('Checks:'))}
|
|
802
|
+
${colors.cyan('Structure')} Valid llms.txt format
|
|
803
|
+
${colors.cyan('Site Name')} Primary heading present
|
|
804
|
+
${colors.cyan('Description')} Site description block
|
|
805
|
+
${colors.cyan('Sections')} Content sections with links
|
|
806
|
+
`)
|
|
807
|
+
.action(async (url, options) => {
|
|
808
|
+
const isJson = options.format === 'json';
|
|
809
|
+
if (options.template) {
|
|
810
|
+
const { generateLlmsTxtTemplate } = await import('../seo/validators/llms-txt.js');
|
|
811
|
+
const template = generateLlmsTxtTemplate({
|
|
812
|
+
siteName: 'Your Site Name',
|
|
813
|
+
siteDescription: 'A brief description of your website and what it offers.',
|
|
814
|
+
sections: [
|
|
815
|
+
{
|
|
816
|
+
title: 'Documentation',
|
|
817
|
+
links: [
|
|
818
|
+
{ text: 'Getting Started', url: '/docs/getting-started' },
|
|
819
|
+
{ text: 'API Reference', url: '/docs/api' },
|
|
820
|
+
],
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
title: 'Resources',
|
|
824
|
+
links: [
|
|
825
|
+
{ text: 'Blog', url: '/blog' },
|
|
826
|
+
{ text: 'FAQ', url: '/faq' },
|
|
827
|
+
],
|
|
828
|
+
},
|
|
829
|
+
],
|
|
830
|
+
});
|
|
831
|
+
console.log(template);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (!url) {
|
|
835
|
+
console.error(colors.red('URL is required (use --template to generate a template)'));
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
if (!url.startsWith('http'))
|
|
839
|
+
url = `https://${url}`;
|
|
840
|
+
if (!url.includes('llms.txt')) {
|
|
841
|
+
const urlObj = new URL(url);
|
|
842
|
+
url = `${urlObj.origin}/llms.txt`;
|
|
843
|
+
}
|
|
844
|
+
if (!isJson) {
|
|
845
|
+
console.log(colors.gray(`Fetching llms.txt from ${url}...`));
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const { fetchAndValidateLlmsTxt } = await import('../seo/validators/llms-txt.js');
|
|
849
|
+
const result = await fetchAndValidateLlmsTxt(url);
|
|
850
|
+
if (isJson) {
|
|
851
|
+
console.log(JSON.stringify(result, null, 2));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (!result.exists) {
|
|
855
|
+
console.log(`
|
|
856
|
+
${colors.bold(colors.cyan('📄 llms.txt Analysis'))}
|
|
857
|
+
${colors.gray('URL:')} ${url}
|
|
858
|
+
${colors.red('Status:')} File not found
|
|
859
|
+
|
|
860
|
+
${colors.yellow('Recommendation:')}
|
|
861
|
+
Consider creating an llms.txt file to help AI/LLM systems
|
|
862
|
+
better understand your site's content and structure.
|
|
863
|
+
|
|
864
|
+
Use ${colors.cyan('rek llms --template')} to generate a starting template.
|
|
865
|
+
Learn more: ${colors.cyan('https://llmstxt.org')}
|
|
866
|
+
`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
console.log(`
|
|
870
|
+
${colors.bold(colors.cyan('📄 llms.txt Analysis'))}
|
|
871
|
+
${colors.gray('URL:')} ${url}
|
|
872
|
+
${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')}
|
|
873
|
+
`);
|
|
874
|
+
if (result.parseResult) {
|
|
875
|
+
const { parseResult } = result;
|
|
876
|
+
if (parseResult.siteName) {
|
|
877
|
+
console.log(`${colors.bold('Site Name:')} ${parseResult.siteName}`);
|
|
644
878
|
}
|
|
645
|
-
if (
|
|
646
|
-
|
|
879
|
+
if (parseResult.siteDescription) {
|
|
880
|
+
const desc = parseResult.siteDescription.length > 100
|
|
881
|
+
? parseResult.siteDescription.slice(0, 97) + '...'
|
|
882
|
+
: parseResult.siteDescription;
|
|
883
|
+
console.log(`${colors.bold('Description:')} ${colors.gray(desc)}`);
|
|
647
884
|
}
|
|
648
885
|
console.log('');
|
|
886
|
+
if (parseResult.sections.length > 0) {
|
|
887
|
+
console.log(colors.bold(`Sections: ${parseResult.sections.length}`));
|
|
888
|
+
for (const section of parseResult.sections) {
|
|
889
|
+
const linkCount = parseResult.links.filter(l => true).length;
|
|
890
|
+
console.log(` ${colors.cyan('##')} ${section.title}`);
|
|
891
|
+
}
|
|
892
|
+
console.log('');
|
|
893
|
+
}
|
|
894
|
+
if (parseResult.links.length > 0) {
|
|
895
|
+
console.log(colors.bold(`Links: ${parseResult.links.length}`));
|
|
896
|
+
for (const link of parseResult.links.slice(0, 5)) {
|
|
897
|
+
console.log(` ${colors.gray('→')} [${link.text}](${link.url})`);
|
|
898
|
+
}
|
|
899
|
+
if (parseResult.links.length > 5) {
|
|
900
|
+
console.log(colors.gray(` ... and ${parseResult.links.length - 5} more links`));
|
|
901
|
+
}
|
|
902
|
+
console.log('');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (result.issues.length > 0) {
|
|
906
|
+
console.log(colors.bold('Issues:'));
|
|
907
|
+
for (const issue of result.issues) {
|
|
908
|
+
const icon = issue.type === 'error' ? colors.red('✗')
|
|
909
|
+
: issue.type === 'warning' ? colors.yellow('⚠')
|
|
910
|
+
: colors.gray('ℹ');
|
|
911
|
+
console.log(` ${icon} ${issue.message}`);
|
|
912
|
+
}
|
|
913
|
+
console.log('');
|
|
914
|
+
}
|
|
915
|
+
const errorCount = result.issues.filter(i => i.type === 'error').length;
|
|
916
|
+
const warningCount = result.issues.filter(i => i.type === 'warning').length;
|
|
917
|
+
if (errorCount === 0 && warningCount === 0 && result.valid) {
|
|
918
|
+
console.log(colors.green('✔ Valid llms.txt file'));
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
console.log(`${colors.red(`${errorCount} errors`)} | ${colors.yellow(`${warningCount} warnings`)}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
console.error(colors.red(`llms.txt analysis failed: ${error.message}`));
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
program
|
|
930
|
+
.command('spider')
|
|
931
|
+
.description('Crawl a website following internal links')
|
|
932
|
+
.argument('<url>', 'Starting URL to crawl')
|
|
933
|
+
.argument('[args...]', 'Options: depth=N limit=N concurrency=N seo focus=MODE output=file.json')
|
|
934
|
+
.addHelpText('after', `
|
|
935
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
936
|
+
${colors.green('$ rek spider example.com')} ${colors.gray('Crawl with defaults')}
|
|
937
|
+
${colors.green('$ rek spider example.com depth=3 limit=50')} ${colors.gray('Depth 3, max 50 pages')}
|
|
938
|
+
${colors.green('$ rek spider example.com seo')} ${colors.gray('Crawl + SEO analysis')}
|
|
939
|
+
${colors.green('$ rek spider example.com seo output=report.json')} ${colors.gray('SEO with JSON export')}
|
|
940
|
+
${colors.green('$ rek spider example.com seo focus=links')} ${colors.gray('Focus on link issues')}
|
|
941
|
+
${colors.green('$ rek spider example.com seo focus=security')} ${colors.gray('Focus on security issues')}
|
|
942
|
+
${colors.green('$ rek spider example.com seo focus=duplicates')} ${colors.gray('Focus on duplicate content')}
|
|
943
|
+
|
|
944
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
945
|
+
${colors.cyan('depth=N')} Max link depth to follow (default: 5)
|
|
946
|
+
${colors.cyan('limit=N')} Max pages to crawl (default: 100)
|
|
947
|
+
${colors.cyan('concurrency=N')} Parallel requests (default: 5)
|
|
948
|
+
${colors.cyan('seo')} Enable SEO analysis mode
|
|
949
|
+
${colors.cyan('focus=MODE')} Focus analysis on specific area (requires seo)
|
|
950
|
+
${colors.cyan('output=file.json')} Save JSON report to file
|
|
951
|
+
|
|
952
|
+
${colors.bold(colors.yellow('Focus Modes:'))}
|
|
953
|
+
${colors.cyan('links')} Internal/external links, broken links, anchor text
|
|
954
|
+
${colors.cyan('duplicates')} Duplicate titles, descriptions, content (85% similarity)
|
|
955
|
+
${colors.cyan('security')} SSL/TLS, HTTPS, form security, headers
|
|
956
|
+
${colors.cyan('ai')} AI/LLM optimization, llms.txt, robots.txt AI bots
|
|
957
|
+
${colors.cyan('resources')} JS/CSS optimization, image compression, caching
|
|
958
|
+
${colors.cyan('all')} Run all focus modes (default)
|
|
959
|
+
`)
|
|
960
|
+
.action(async (url, args) => {
|
|
961
|
+
let maxDepth = 5;
|
|
962
|
+
let maxPages = 100;
|
|
963
|
+
let concurrency = 5;
|
|
964
|
+
let seoEnabled = false;
|
|
965
|
+
let outputFile = '';
|
|
966
|
+
let focusMode = 'all';
|
|
967
|
+
const focusCategories = {
|
|
968
|
+
links: ['links'],
|
|
969
|
+
duplicates: ['title', 'meta', 'content'],
|
|
970
|
+
security: ['security'],
|
|
971
|
+
ai: ['ai-search'],
|
|
972
|
+
resources: ['resources', 'performance'],
|
|
973
|
+
all: [],
|
|
974
|
+
};
|
|
975
|
+
for (const arg of args) {
|
|
976
|
+
if (arg.startsWith('depth=')) {
|
|
977
|
+
maxDepth = parseInt(arg.split('=')[1]) || 5;
|
|
978
|
+
}
|
|
979
|
+
else if (arg.startsWith('limit=')) {
|
|
980
|
+
maxPages = parseInt(arg.split('=')[1]) || 100;
|
|
981
|
+
}
|
|
982
|
+
else if (arg.startsWith('concurrency=')) {
|
|
983
|
+
concurrency = parseInt(arg.split('=')[1]) || 5;
|
|
984
|
+
}
|
|
985
|
+
else if (arg === 'seo') {
|
|
986
|
+
seoEnabled = true;
|
|
987
|
+
}
|
|
988
|
+
else if (arg.startsWith('output=')) {
|
|
989
|
+
outputFile = arg.split('=')[1] || '';
|
|
990
|
+
}
|
|
991
|
+
else if (arg.startsWith('focus=')) {
|
|
992
|
+
const mode = arg.split('=')[1] || 'all';
|
|
993
|
+
if (mode in focusCategories) {
|
|
994
|
+
focusMode = mode;
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
console.error(colors.red(`Invalid focus mode: ${mode}`));
|
|
998
|
+
console.error(colors.gray(`Valid modes: ${Object.keys(focusCategories).join(', ')}`));
|
|
999
|
+
process.exit(1);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (!url.startsWith('http'))
|
|
1004
|
+
url = `https://${url}`;
|
|
1005
|
+
const modeLabel = seoEnabled ? colors.magenta(' + SEO') : '';
|
|
1006
|
+
const focusLabel = focusMode !== 'all' ? colors.cyan(` [focus: ${focusMode}]`) : '';
|
|
1007
|
+
console.log(colors.cyan(`\nSpider starting: ${url}`));
|
|
1008
|
+
console.log(colors.gray(` Depth: ${maxDepth} | Limit: ${maxPages} | Concurrency: ${concurrency}${modeLabel}${focusLabel}`));
|
|
1009
|
+
if (outputFile) {
|
|
1010
|
+
console.log(colors.gray(` Output: ${outputFile}`));
|
|
1011
|
+
}
|
|
1012
|
+
console.log('');
|
|
1013
|
+
try {
|
|
1014
|
+
if (seoEnabled) {
|
|
1015
|
+
const { SeoSpider } = await import('../seo/index.js');
|
|
1016
|
+
const seoSpider = new SeoSpider({
|
|
1017
|
+
maxDepth,
|
|
1018
|
+
maxPages,
|
|
1019
|
+
concurrency,
|
|
1020
|
+
sameDomain: true,
|
|
1021
|
+
delay: 100,
|
|
1022
|
+
seo: true,
|
|
1023
|
+
output: outputFile || undefined,
|
|
1024
|
+
focusCategories: focusCategories[focusMode],
|
|
1025
|
+
focusMode,
|
|
1026
|
+
onProgress: (progress) => {
|
|
1027
|
+
process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
1030
|
+
const result = await seoSpider.crawl(url);
|
|
1031
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
1032
|
+
console.log(colors.green(`\n✔ SEO Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
|
|
1033
|
+
console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
|
|
1034
|
+
console.log(` ${colors.cyan('Unique URLs')}: ${result.visited.size}`);
|
|
1035
|
+
console.log(` ${colors.cyan('Avg SEO Score')}: ${result.summary.avgScore}/100`);
|
|
1036
|
+
const responseTimes = result.pages.filter(p => p.duration > 0).map(p => p.duration);
|
|
1037
|
+
const avgResponseTime = responseTimes.length > 0
|
|
1038
|
+
? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
|
|
1039
|
+
: 0;
|
|
1040
|
+
const minResponseTime = responseTimes.length > 0 ? Math.min(...responseTimes) : 0;
|
|
1041
|
+
const maxResponseTime = responseTimes.length > 0 ? Math.max(...responseTimes) : 0;
|
|
1042
|
+
const reqPerSec = result.duration > 0 ? (result.pages.length / (result.duration / 1000)).toFixed(1) : '0';
|
|
1043
|
+
const statusCounts = new Map();
|
|
1044
|
+
for (const page of result.pages) {
|
|
1045
|
+
const status = page.status || 0;
|
|
1046
|
+
statusCounts.set(status, (statusCounts.get(status) || 0) + 1);
|
|
1047
|
+
}
|
|
1048
|
+
let totalInternalLinks = 0;
|
|
1049
|
+
let totalExternalLinks = 0;
|
|
1050
|
+
let totalImages = 0;
|
|
1051
|
+
let imagesWithoutAlt = 0;
|
|
1052
|
+
let pagesWithoutTitle = 0;
|
|
1053
|
+
let pagesWithoutDescription = 0;
|
|
1054
|
+
for (const page of result.pages) {
|
|
1055
|
+
if (page.seoReport) {
|
|
1056
|
+
totalInternalLinks += page.seoReport.links?.internal || 0;
|
|
1057
|
+
totalExternalLinks += page.seoReport.links?.external || 0;
|
|
1058
|
+
totalImages += page.seoReport.images?.total || 0;
|
|
1059
|
+
imagesWithoutAlt += page.seoReport.images?.withoutAlt || 0;
|
|
1060
|
+
if (!page.seoReport.title?.text)
|
|
1061
|
+
pagesWithoutTitle++;
|
|
1062
|
+
if (!page.seoReport.metaDescription?.text)
|
|
1063
|
+
pagesWithoutDescription++;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
console.log(colors.bold('\n Performance:'));
|
|
1067
|
+
console.log(` ${colors.gray('Avg Response:')} ${avgResponseTime}ms`);
|
|
1068
|
+
console.log(` ${colors.gray('Min/Max:')} ${minResponseTime}ms / ${maxResponseTime}ms`);
|
|
1069
|
+
console.log(` ${colors.gray('Throughput:')} ${reqPerSec} req/s`);
|
|
1070
|
+
console.log(colors.bold('\n HTTP Status:'));
|
|
1071
|
+
const sortedStatuses = Array.from(statusCounts.entries()).sort((a, b) => b[1] - a[1]);
|
|
1072
|
+
for (const [status, count] of sortedStatuses.slice(0, 5)) {
|
|
1073
|
+
const statusLabel = status === 0 ? 'Error' : status.toString();
|
|
1074
|
+
const statusColor = status >= 400 || status === 0 ? colors.red :
|
|
1075
|
+
status >= 300 ? colors.yellow : colors.green;
|
|
1076
|
+
const pct = ((count / result.pages.length) * 100).toFixed(0);
|
|
1077
|
+
console.log(` ${statusColor(statusLabel.padEnd(5))} ${count.toString().padStart(3)} (${pct}%)`);
|
|
1078
|
+
}
|
|
1079
|
+
console.log(colors.bold('\n Content:'));
|
|
1080
|
+
console.log(` ${colors.gray('Internal links:')} ${totalInternalLinks.toLocaleString()}`);
|
|
1081
|
+
console.log(` ${colors.gray('External links:')} ${totalExternalLinks.toLocaleString()}`);
|
|
1082
|
+
console.log(` ${colors.gray('Images:')} ${totalImages.toLocaleString()} (${imagesWithoutAlt} missing alt)`);
|
|
1083
|
+
console.log(` ${colors.gray('Missing title:')} ${pagesWithoutTitle}`);
|
|
1084
|
+
console.log(` ${colors.gray('Missing desc:')} ${pagesWithoutDescription}`);
|
|
1085
|
+
console.log(colors.bold('\n SEO Summary:'));
|
|
1086
|
+
const { summary } = result;
|
|
1087
|
+
console.log(` ${colors.red('✗')} Pages with errors: ${summary.pagesWithErrors}`);
|
|
1088
|
+
console.log(` ${colors.yellow('⚠')} Pages with warnings: ${summary.pagesWithWarnings}`);
|
|
1089
|
+
console.log(` ${colors.magenta('⚐')} Duplicate titles: ${summary.duplicateTitles}`);
|
|
1090
|
+
console.log(` ${colors.magenta('⚐')} Duplicate descriptions:${summary.duplicateDescriptions}`);
|
|
1091
|
+
console.log(` ${colors.magenta('⚐')} Duplicate H1s: ${summary.duplicateH1s}`);
|
|
1092
|
+
console.log(` ${colors.gray('○')} Orphan pages: ${summary.orphanPages}`);
|
|
1093
|
+
if (result.siteWideIssues.length > 0) {
|
|
1094
|
+
console.log(colors.bold('\n Site-Wide Issues:'));
|
|
1095
|
+
for (const issue of result.siteWideIssues.slice(0, 10)) {
|
|
1096
|
+
const icon = issue.severity === 'error' ? colors.red('✗') :
|
|
1097
|
+
issue.severity === 'warning' ? colors.yellow('⚠') : colors.gray('○');
|
|
1098
|
+
console.log(` ${icon} ${issue.message}`);
|
|
1099
|
+
if (issue.value) {
|
|
1100
|
+
const truncatedValue = issue.value.length > 50 ? issue.value.slice(0, 47) + '...' : issue.value;
|
|
1101
|
+
console.log(` ${colors.gray(`"${truncatedValue}"`)}`);
|
|
1102
|
+
}
|
|
1103
|
+
const uniquePaths = [...new Set(issue.affectedUrls.map(u => new URL(u).pathname))];
|
|
1104
|
+
if (uniquePaths.length <= 3) {
|
|
1105
|
+
for (const path of uniquePaths) {
|
|
1106
|
+
console.log(` ${colors.gray('→')} ${path}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
console.log(` ${colors.gray(`→ ${uniquePaths.length} pages affected`)}`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (result.siteWideIssues.length > 10) {
|
|
1114
|
+
console.log(colors.gray(` ... and ${result.siteWideIssues.length - 10} more issues`));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const pagesWithScores = result.pages
|
|
1118
|
+
.filter(p => p.seoReport)
|
|
1119
|
+
.sort((a, b) => (a.seoReport?.score || 0) - (b.seoReport?.score || 0));
|
|
1120
|
+
const seenPaths = new Set();
|
|
1121
|
+
const uniquePages = pagesWithScores.filter(page => {
|
|
1122
|
+
const path = new URL(page.url).pathname;
|
|
1123
|
+
if (seenPaths.has(path))
|
|
1124
|
+
return false;
|
|
1125
|
+
seenPaths.add(path);
|
|
1126
|
+
return true;
|
|
1127
|
+
});
|
|
1128
|
+
if (uniquePages.length > 0) {
|
|
1129
|
+
console.log(colors.bold('\n Pages by SEO Score:'));
|
|
1130
|
+
const worstPages = uniquePages.slice(0, 5);
|
|
1131
|
+
for (const page of worstPages) {
|
|
1132
|
+
const score = page.seoReport?.score || 0;
|
|
1133
|
+
const grade = page.seoReport?.grade || '?';
|
|
1134
|
+
const path = new URL(page.url).pathname;
|
|
1135
|
+
const scoreColor = score >= 80 ? colors.green : score >= 60 ? colors.yellow : colors.red;
|
|
1136
|
+
console.log(` ${scoreColor(`${score.toString().padStart(3)}`)} ${colors.gray(`[${grade}]`)} ${path.slice(0, 50)}`);
|
|
1137
|
+
}
|
|
1138
|
+
if (uniquePages.length > 5) {
|
|
1139
|
+
console.log(colors.gray(` ... and ${uniquePages.length - 5} more pages`));
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (outputFile) {
|
|
1143
|
+
console.log(colors.green(`\n Report saved to: ${outputFile}`));
|
|
1144
|
+
}
|
|
649
1145
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1146
|
+
else {
|
|
1147
|
+
const { Spider } = await import('../scrape/spider.js');
|
|
1148
|
+
const spider = new Spider({
|
|
1149
|
+
maxDepth,
|
|
1150
|
+
maxPages,
|
|
1151
|
+
concurrency,
|
|
1152
|
+
sameDomain: true,
|
|
1153
|
+
delay: 100,
|
|
1154
|
+
onProgress: (progress) => {
|
|
1155
|
+
process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
const result = await spider.crawl(url);
|
|
1159
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
1160
|
+
console.log(colors.green(`\n✔ Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
|
|
1161
|
+
console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
|
|
1162
|
+
console.log(` ${colors.cyan('Unique URLs')}: ${result.visited.size}`);
|
|
1163
|
+
console.log(` ${colors.cyan('Errors')}: ${result.errors.length}`);
|
|
1164
|
+
const byDepth = new Map();
|
|
1165
|
+
for (const page of result.pages) {
|
|
1166
|
+
byDepth.set(page.depth, (byDepth.get(page.depth) || 0) + 1);
|
|
656
1167
|
}
|
|
657
|
-
|
|
658
|
-
|
|
1168
|
+
console.log(colors.bold('\n Pages by depth:'));
|
|
1169
|
+
for (const [depth, count] of Array.from(byDepth.entries()).sort((a, b) => a[0] - b[0])) {
|
|
1170
|
+
const bar = '█'.repeat(Math.min(count, 40));
|
|
1171
|
+
console.log(` ${colors.gray(`d${depth}:`)} ${bar} ${count}`);
|
|
1172
|
+
}
|
|
1173
|
+
const topPages = [...result.pages]
|
|
1174
|
+
.filter(p => !p.error)
|
|
1175
|
+
.sort((a, b) => b.links.length - a.links.length)
|
|
1176
|
+
.slice(0, 10);
|
|
1177
|
+
if (topPages.length > 0) {
|
|
1178
|
+
console.log(colors.bold('\n Top pages by outgoing links:'));
|
|
1179
|
+
for (const page of topPages) {
|
|
1180
|
+
const title = page.title.slice(0, 40) || new URL(page.url).pathname;
|
|
1181
|
+
console.log(` ${colors.cyan(page.links.length.toString().padStart(3))} ${title}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const formatError = (error) => {
|
|
1185
|
+
const statusMatch = error.match(/status code (\d{3})/i);
|
|
1186
|
+
if (statusMatch) {
|
|
1187
|
+
return `HTTP ${statusMatch[1]}`;
|
|
1188
|
+
}
|
|
1189
|
+
return error.length > 50 ? error.slice(0, 47) + '...' : error;
|
|
1190
|
+
};
|
|
1191
|
+
if (result.errors.length > 0 && result.errors.length <= 10) {
|
|
1192
|
+
console.log(colors.bold('\n Errors:'));
|
|
1193
|
+
for (const err of result.errors) {
|
|
1194
|
+
const path = new URL(err.url).pathname;
|
|
1195
|
+
console.log(` ${colors.red('✗')} ${path.padEnd(30)} → ${formatError(err.error)}`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
else if (result.errors.length > 10) {
|
|
1199
|
+
console.log(colors.bold('\n Errors:'));
|
|
1200
|
+
for (const err of result.errors.slice(0, 5)) {
|
|
1201
|
+
const path = new URL(err.url).pathname;
|
|
1202
|
+
console.log(` ${colors.red('✗')} ${path.padEnd(30)} → ${formatError(err.error)}`);
|
|
1203
|
+
}
|
|
1204
|
+
console.log(colors.gray(` ... and ${result.errors.length - 5} more errors`));
|
|
1205
|
+
}
|
|
1206
|
+
if (outputFile) {
|
|
1207
|
+
const jsonOutput = {
|
|
1208
|
+
startUrl: result.startUrl,
|
|
1209
|
+
crawledAt: new Date().toISOString(),
|
|
1210
|
+
duration: result.duration,
|
|
1211
|
+
summary: {
|
|
1212
|
+
totalPages: result.pages.length,
|
|
1213
|
+
successCount: result.pages.filter(p => !p.error).length,
|
|
1214
|
+
errorCount: result.errors.length,
|
|
1215
|
+
uniqueUrls: result.visited.size,
|
|
1216
|
+
},
|
|
1217
|
+
pages: result.pages.map(p => ({
|
|
1218
|
+
url: p.url,
|
|
1219
|
+
status: p.status,
|
|
1220
|
+
title: p.title,
|
|
1221
|
+
depth: p.depth,
|
|
1222
|
+
linksCount: p.links.length,
|
|
1223
|
+
duration: p.duration,
|
|
1224
|
+
error: p.error,
|
|
1225
|
+
})),
|
|
1226
|
+
errors: result.errors,
|
|
1227
|
+
};
|
|
1228
|
+
await fs.writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
|
|
1229
|
+
console.log(colors.green(`\n Report saved to: ${outputFile}`));
|
|
659
1230
|
}
|
|
660
1231
|
}
|
|
1232
|
+
console.log('');
|
|
661
1233
|
}
|
|
662
1234
|
catch (error) {
|
|
663
1235
|
console.error(colors.red(`\nSpider failed: ${error.message}`));
|
|
664
1236
|
process.exit(1);
|
|
665
1237
|
}
|
|
666
1238
|
});
|
|
1239
|
+
program
|
|
1240
|
+
.command('scrape')
|
|
1241
|
+
.description('Scrape data from a web page using CSS selectors')
|
|
1242
|
+
.argument('<url>', 'URL to scrape')
|
|
1243
|
+
.argument('[args...]', 'Options: select=SELECTOR, attr=NAME, links, images, meta, tables, scripts, jsonld')
|
|
1244
|
+
.addHelpText('after', `
|
|
1245
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
1246
|
+
${colors.green('$ rek scrape example.com')} ${colors.gray('# Basic page info')}
|
|
1247
|
+
${colors.green('$ rek scrape example.com select="h1"')} ${colors.gray('# Extract h1 text')}
|
|
1248
|
+
${colors.green('$ rek scrape example.com select="a" attr=href')} ${colors.gray('# Extract link hrefs')}
|
|
1249
|
+
${colors.green('$ rek scrape example.com links')} ${colors.gray('# All links')}
|
|
1250
|
+
${colors.green('$ rek scrape example.com images')} ${colors.gray('# All images')}
|
|
1251
|
+
${colors.green('$ rek scrape example.com meta')} ${colors.gray('# Meta tags')}
|
|
1252
|
+
${colors.green('$ rek scrape example.com tables')} ${colors.gray('# All tables as JSON')}
|
|
1253
|
+
${colors.green('$ rek scrape example.com scripts')} ${colors.gray('# All scripts')}
|
|
1254
|
+
${colors.green('$ rek scrape example.com jsonld')} ${colors.gray('# JSON-LD structured data')}
|
|
1255
|
+
|
|
1256
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
1257
|
+
${colors.cyan('select=SELECTOR')} CSS selector to extract elements
|
|
1258
|
+
${colors.cyan('attr=NAME')} Extract specific attribute (use with select)
|
|
1259
|
+
${colors.cyan('links')} Extract all links with text and href
|
|
1260
|
+
${colors.cyan('images')} Extract all images with src and alt
|
|
1261
|
+
${colors.cyan('meta')} Extract all meta tags
|
|
1262
|
+
${colors.cyan('tables')} Extract tables as structured JSON
|
|
1263
|
+
${colors.cyan('scripts')} Extract all script sources
|
|
1264
|
+
${colors.cyan('jsonld')} Extract JSON-LD structured data
|
|
1265
|
+
`)
|
|
1266
|
+
.action(async (url, args) => {
|
|
1267
|
+
const { ScrapeDocument } = await import('../scrape/document.js');
|
|
1268
|
+
const { Client } = await import('../core/client.js');
|
|
1269
|
+
let selector = '';
|
|
1270
|
+
let attrName = '';
|
|
1271
|
+
let extractLinks = false;
|
|
1272
|
+
let extractImages = false;
|
|
1273
|
+
let extractMeta = false;
|
|
1274
|
+
let extractTables = false;
|
|
1275
|
+
let extractScripts = false;
|
|
1276
|
+
let extractJsonLd = false;
|
|
1277
|
+
for (const arg of args) {
|
|
1278
|
+
if (arg.startsWith('select=')) {
|
|
1279
|
+
selector = arg.slice(7);
|
|
1280
|
+
}
|
|
1281
|
+
else if (arg.startsWith('attr=')) {
|
|
1282
|
+
attrName = arg.slice(5);
|
|
1283
|
+
}
|
|
1284
|
+
else if (arg === 'links') {
|
|
1285
|
+
extractLinks = true;
|
|
1286
|
+
}
|
|
1287
|
+
else if (arg === 'images') {
|
|
1288
|
+
extractImages = true;
|
|
1289
|
+
}
|
|
1290
|
+
else if (arg === 'meta') {
|
|
1291
|
+
extractMeta = true;
|
|
1292
|
+
}
|
|
1293
|
+
else if (arg === 'tables') {
|
|
1294
|
+
extractTables = true;
|
|
1295
|
+
}
|
|
1296
|
+
else if (arg === 'scripts') {
|
|
1297
|
+
extractScripts = true;
|
|
1298
|
+
}
|
|
1299
|
+
else if (arg === 'jsonld') {
|
|
1300
|
+
extractJsonLd = true;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
1304
|
+
url = `https://${url}`;
|
|
1305
|
+
}
|
|
1306
|
+
console.log(colors.gray(`Fetching ${url}...`));
|
|
1307
|
+
try {
|
|
1308
|
+
const client = new Client();
|
|
1309
|
+
const response = await client.get(url);
|
|
1310
|
+
const html = await response.text();
|
|
1311
|
+
const doc = await ScrapeDocument.create(html, { baseUrl: url });
|
|
1312
|
+
if (!selector && !extractLinks && !extractImages && !extractMeta && !extractTables && !extractScripts && !extractJsonLd) {
|
|
1313
|
+
const title = doc.text('title') || 'N/A';
|
|
1314
|
+
const description = doc.attr('meta[name="description"]', 'content') || 'N/A';
|
|
1315
|
+
const h1 = doc.text('h1') || 'N/A';
|
|
1316
|
+
const linkCount = doc.links().length;
|
|
1317
|
+
const imageCount = doc.images().length;
|
|
1318
|
+
console.log(`
|
|
1319
|
+
${colors.bold(colors.cyan('📄 Page Info'))}
|
|
1320
|
+
|
|
1321
|
+
${colors.bold('Title:')} ${title}
|
|
1322
|
+
${colors.bold('Description:')} ${description.slice(0, 100)}${description.length > 100 ? '...' : ''}
|
|
1323
|
+
${colors.bold('H1:')} ${h1}
|
|
1324
|
+
${colors.bold('Links:')} ${linkCount}
|
|
1325
|
+
${colors.bold('Images:')} ${imageCount}
|
|
1326
|
+
`);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (selector) {
|
|
1330
|
+
if (attrName) {
|
|
1331
|
+
const values = doc.attrs(selector, attrName);
|
|
1332
|
+
console.log(`\n${colors.bold(`Found ${values.length} values for "${attrName}" in "${selector}"`)}\n`);
|
|
1333
|
+
values.slice(0, 50).forEach((value, i) => {
|
|
1334
|
+
if (value) {
|
|
1335
|
+
console.log(`${colors.gray(`${i + 1}.`)} ${value}`);
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
if (values.length > 50) {
|
|
1339
|
+
console.log(colors.gray(`\n... and ${values.length - 50} more`));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
const texts = doc.texts(selector);
|
|
1344
|
+
console.log(`\n${colors.bold(`Found ${texts.length} elements matching "${selector}"`)}\n`);
|
|
1345
|
+
texts.slice(0, 50).forEach((text, i) => {
|
|
1346
|
+
const trimmed = text.trim();
|
|
1347
|
+
if (trimmed) {
|
|
1348
|
+
console.log(`${colors.gray(`${i + 1}.`)} ${trimmed.slice(0, 200)}`);
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
if (texts.length > 50) {
|
|
1352
|
+
console.log(colors.gray(`\n... and ${texts.length - 50} more`));
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (extractLinks) {
|
|
1358
|
+
const links = doc.links();
|
|
1359
|
+
console.log(`\n${colors.bold(`Found ${links.length} links`)}\n`);
|
|
1360
|
+
links.slice(0, 50).forEach((link, i) => {
|
|
1361
|
+
const text = (link.text || '').trim().slice(0, 50) || '[no text]';
|
|
1362
|
+
console.log(`${colors.gray(`${i + 1}.`)} ${colors.cyan(text)}`);
|
|
1363
|
+
console.log(` ${colors.gray(link.href)}`);
|
|
1364
|
+
});
|
|
1365
|
+
if (links.length > 50) {
|
|
1366
|
+
console.log(colors.gray(`\n... and ${links.length - 50} more`));
|
|
1367
|
+
}
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (extractImages) {
|
|
1371
|
+
const images = doc.images();
|
|
1372
|
+
console.log(`\n${colors.bold(`Found ${images.length} images`)}\n`);
|
|
1373
|
+
images.slice(0, 30).forEach((img, i) => {
|
|
1374
|
+
const alt = img.alt || '[no alt]';
|
|
1375
|
+
console.log(`${colors.gray(`${i + 1}.`)} ${colors.cyan(alt.slice(0, 50))}`);
|
|
1376
|
+
console.log(` ${colors.gray(img.src)}`);
|
|
1377
|
+
});
|
|
1378
|
+
if (images.length > 30) {
|
|
1379
|
+
console.log(colors.gray(`\n... and ${images.length - 30} more`));
|
|
1380
|
+
}
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (extractMeta) {
|
|
1384
|
+
const meta = doc.meta();
|
|
1385
|
+
const entries = Object.entries(meta);
|
|
1386
|
+
console.log(`\n${colors.bold(`Found ${entries.length} meta entries`)}\n`);
|
|
1387
|
+
entries.forEach(([name, content]) => {
|
|
1388
|
+
if (name && content) {
|
|
1389
|
+
const value = String(content);
|
|
1390
|
+
console.log(`${colors.cyan(name)}: ${value.slice(0, 100)}${value.length > 100 ? '...' : ''}`);
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (extractTables) {
|
|
1396
|
+
const tables = doc.tables();
|
|
1397
|
+
console.log(`\n${colors.bold(`Found ${tables.length} tables`)}\n`);
|
|
1398
|
+
tables.slice(0, 5).forEach((table, tableIndex) => {
|
|
1399
|
+
console.log(`${colors.bold(`Table ${tableIndex + 1}:`)} ${table.rows?.length || 0} rows`);
|
|
1400
|
+
console.log(JSON.stringify((table.rows || []).slice(0, 10), null, 2));
|
|
1401
|
+
if ((table.rows?.length || 0) > 10) {
|
|
1402
|
+
console.log(colors.gray(`... and ${(table.rows?.length || 0) - 10} more rows`));
|
|
1403
|
+
}
|
|
1404
|
+
console.log('');
|
|
1405
|
+
});
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (extractScripts) {
|
|
1409
|
+
const scripts = doc.scripts();
|
|
1410
|
+
const external = scripts.filter(s => s.src);
|
|
1411
|
+
const inline = scripts.filter(s => !s.src);
|
|
1412
|
+
console.log(`\n${colors.bold(`Found ${external.length} external scripts, ${inline.length} inline`)}\n`);
|
|
1413
|
+
if (external.length > 0) {
|
|
1414
|
+
console.log(colors.bold('External Scripts:'));
|
|
1415
|
+
external.slice(0, 20).forEach((script, i) => {
|
|
1416
|
+
console.log(`${colors.gray(`${i + 1}.`)} ${script.src}`);
|
|
1417
|
+
});
|
|
1418
|
+
if (external.length > 20) {
|
|
1419
|
+
console.log(colors.gray(`... and ${external.length - 20} more`));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
if (extractJsonLd) {
|
|
1425
|
+
const jsonld = doc.jsonLd();
|
|
1426
|
+
console.log(`\n${colors.bold(`Found ${jsonld.length} JSON-LD blocks`)}\n`);
|
|
1427
|
+
jsonld.forEach((data, i) => {
|
|
1428
|
+
console.log(`${colors.bold(`Block ${i + 1}:`)} ${data['@type'] || 'Unknown type'}`);
|
|
1429
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1430
|
+
console.log('');
|
|
1431
|
+
});
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
console.error(colors.red(`Scrape failed: ${error.message}`));
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
667
1440
|
program
|
|
668
1441
|
.command('ai')
|
|
669
1442
|
.description('Send a single AI prompt (no memory/context)')
|
|
@@ -790,103 +1563,553 @@ ${colors.bold(colors.yellow('Note:'))}
|
|
|
790
1563
|
console.log(colors.gray(`Downloading GeoLite2 database...`));
|
|
791
1564
|
}
|
|
792
1565
|
try {
|
|
793
|
-
const info = await getIpInfo(address);
|
|
794
|
-
if (info.bogon) {
|
|
795
|
-
console.log(colors.yellow(`\n⚠ ${address} is a Bogon/Private IP.`));
|
|
796
|
-
console.log(colors.gray(` Type: ${info.bogonType}`));
|
|
797
|
-
return;
|
|
1566
|
+
const info = await getIpInfo(address);
|
|
1567
|
+
if (info.bogon) {
|
|
1568
|
+
console.log(colors.yellow(`\n⚠ ${address} is a Bogon/Private IP.`));
|
|
1569
|
+
console.log(colors.gray(` Type: ${info.bogonType}`));
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
console.log(`
|
|
1573
|
+
${colors.bold(colors.cyan('🌍 IP Intelligence Report'))}
|
|
1574
|
+
|
|
1575
|
+
${colors.bold('Location:')}
|
|
1576
|
+
${colors.gray('City:')} ${info.city || 'N/A'}
|
|
1577
|
+
${colors.gray('Region:')} ${info.region || 'N/A'}
|
|
1578
|
+
${colors.gray('Country:')} ${info.country || 'N/A'} ${info.countryCode ? `(${info.countryCode})` : ''}
|
|
1579
|
+
${colors.gray('Continent:')} ${info.continent || 'N/A'}
|
|
1580
|
+
${colors.gray('Timezone:')} ${info.timezone || 'N/A'}
|
|
1581
|
+
${colors.gray('Coords:')} ${info.loc ? colors.cyan(info.loc) : 'N/A'}
|
|
1582
|
+
${colors.gray('Accuracy:')} ${info.accuracy ? `~${info.accuracy} km` : 'N/A'}
|
|
1583
|
+
|
|
1584
|
+
${colors.bold('Network:')}
|
|
1585
|
+
${colors.gray('IP:')} ${info.ip}
|
|
1586
|
+
${colors.gray('Type:')} ${info.isIPv6 ? 'IPv6' : 'IPv4'}
|
|
1587
|
+
${colors.gray('Postal:')} ${info.postal || 'N/A'}
|
|
1588
|
+
`);
|
|
1589
|
+
}
|
|
1590
|
+
catch (err) {
|
|
1591
|
+
console.error(colors.red(`IP Lookup Failed: ${err.message}`));
|
|
1592
|
+
process.exit(1);
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
program
|
|
1596
|
+
.command('tls')
|
|
1597
|
+
.alias('ssl')
|
|
1598
|
+
.description('Inspect TLS/SSL certificate of a host')
|
|
1599
|
+
.argument('<host>', 'Hostname or IP address')
|
|
1600
|
+
.argument('[port]', 'Port number (default: 443)', '443')
|
|
1601
|
+
.action(async (host, port) => {
|
|
1602
|
+
const { inspectTLS } = await import('../utils/tls-inspector.js');
|
|
1603
|
+
console.log(colors.gray(`Inspecting TLS certificate for ${host}:${port}...`));
|
|
1604
|
+
try {
|
|
1605
|
+
const info = await inspectTLS(host, parseInt(port));
|
|
1606
|
+
let daysColor = colors.green;
|
|
1607
|
+
if (info.daysRemaining < 30)
|
|
1608
|
+
daysColor = colors.red;
|
|
1609
|
+
else if (info.daysRemaining < 90)
|
|
1610
|
+
daysColor = colors.yellow;
|
|
1611
|
+
const validIcon = info.valid ? colors.green('✔ Valid') : colors.red('✖ Expired');
|
|
1612
|
+
const authIcon = info.authorized ? colors.green('✔ Trusted') : colors.yellow('⚠ Self-signed/Untrusted');
|
|
1613
|
+
console.log(`
|
|
1614
|
+
${colors.bold(colors.cyan('🔒 TLS Certificate Report'))}
|
|
1615
|
+
|
|
1616
|
+
${colors.bold('Status:')}
|
|
1617
|
+
${validIcon}
|
|
1618
|
+
${authIcon}
|
|
1619
|
+
${colors.gray('Days Remaining:')} ${daysColor(info.daysRemaining.toString())}
|
|
1620
|
+
|
|
1621
|
+
${colors.bold('Certificate:')}
|
|
1622
|
+
${colors.gray('Subject:')} ${info.subject?.CN || info.subject?.O || 'N/A'}
|
|
1623
|
+
${colors.gray('Issuer:')} ${info.issuer?.CN || info.issuer?.O || 'N/A'}
|
|
1624
|
+
${colors.gray('Valid From:')} ${info.validFrom.toISOString().split('T')[0]}
|
|
1625
|
+
${colors.gray('Valid To:')} ${info.validTo.toISOString().split('T')[0]}
|
|
1626
|
+
${colors.gray('Serial:')} ${info.serialNumber}
|
|
1627
|
+
|
|
1628
|
+
${colors.bold('Security:')}
|
|
1629
|
+
${colors.gray('Protocol:')} ${info.protocol || 'N/A'}
|
|
1630
|
+
${colors.gray('Cipher:')} ${info.cipher?.name || 'N/A'}
|
|
1631
|
+
${colors.gray('Key:')} ${info.pubkey ? `${info.pubkey.algo.toUpperCase()} ${info.pubkey.size}-bit` : 'N/A'}
|
|
1632
|
+
|
|
1633
|
+
${colors.bold('Fingerprints:')}
|
|
1634
|
+
${colors.gray('SHA-1:')} ${info.fingerprint}
|
|
1635
|
+
${colors.gray('SHA-256:')} ${info.fingerprint256?.slice(0, 40)}...
|
|
1636
|
+
`);
|
|
1637
|
+
if (info.altNames && info.altNames.length > 0) {
|
|
1638
|
+
console.log(`${colors.bold('Subject Alternative Names:')}`);
|
|
1639
|
+
info.altNames.slice(0, 10).forEach(san => {
|
|
1640
|
+
console.log(` ${colors.gray('•')} ${san}`);
|
|
1641
|
+
});
|
|
1642
|
+
if (info.altNames.length > 10) {
|
|
1643
|
+
console.log(` ${colors.gray(`... and ${info.altNames.length - 10} more`)}`);
|
|
1644
|
+
}
|
|
1645
|
+
console.log('');
|
|
1646
|
+
}
|
|
1647
|
+
if (info.extKeyUsage && info.extKeyUsage.length > 0) {
|
|
1648
|
+
console.log(`${colors.bold('Extended Key Usage:')}`);
|
|
1649
|
+
info.extKeyUsage.forEach(oid => {
|
|
1650
|
+
const oidNames = {
|
|
1651
|
+
'1.3.6.1.5.5.7.3.1': 'Server Authentication',
|
|
1652
|
+
'1.3.6.1.5.5.7.3.2': 'Client Authentication',
|
|
1653
|
+
'1.3.6.1.5.5.7.3.3': 'Code Signing',
|
|
1654
|
+
'1.3.6.1.5.5.7.3.4': 'Email Protection',
|
|
1655
|
+
};
|
|
1656
|
+
console.log(` ${colors.gray('•')} ${oidNames[oid] || oid}`);
|
|
1657
|
+
});
|
|
1658
|
+
console.log('');
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
catch (err) {
|
|
1662
|
+
console.error(colors.red(`TLS Inspection Failed: ${err.message}`));
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
program
|
|
1667
|
+
.command('whois')
|
|
1668
|
+
.description('WHOIS lookup for domains and IP addresses')
|
|
1669
|
+
.argument('<query>', 'Domain name or IP address')
|
|
1670
|
+
.option('-r, --raw', 'Show raw WHOIS response')
|
|
1671
|
+
.action(async (query, options) => {
|
|
1672
|
+
const { whois } = await import('../utils/whois.js');
|
|
1673
|
+
console.log(colors.gray(`Looking up WHOIS for ${query}...`));
|
|
1674
|
+
try {
|
|
1675
|
+
const result = await whois(query);
|
|
1676
|
+
if (options.raw) {
|
|
1677
|
+
console.log(result.raw);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
console.log(`
|
|
1681
|
+
${colors.bold(colors.cyan('📋 WHOIS Report'))}
|
|
1682
|
+
|
|
1683
|
+
${colors.bold('Query:')} ${result.query}
|
|
1684
|
+
${colors.bold('Server:')} ${result.server}
|
|
1685
|
+
`);
|
|
1686
|
+
if (result.data && Object.keys(result.data).length > 0) {
|
|
1687
|
+
console.log(colors.bold('Parsed Data:'));
|
|
1688
|
+
const importantKeys = ['Domain Name', 'Registrar', 'Creation Date', 'Expiration Date', 'Updated Date', 'Name Server', 'Status'];
|
|
1689
|
+
for (const key of importantKeys) {
|
|
1690
|
+
const value = result.data[key];
|
|
1691
|
+
if (value) {
|
|
1692
|
+
if (Array.isArray(value)) {
|
|
1693
|
+
console.log(` ${colors.cyan(key)}:`);
|
|
1694
|
+
value.forEach((v) => console.log(` ${colors.gray('•')} ${v}`));
|
|
1695
|
+
}
|
|
1696
|
+
else {
|
|
1697
|
+
console.log(` ${colors.cyan(key)}: ${value}`);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
console.log('');
|
|
1703
|
+
}
|
|
1704
|
+
catch (err) {
|
|
1705
|
+
console.error(colors.red(`WHOIS Lookup Failed: ${err.message}`));
|
|
1706
|
+
process.exit(1);
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
program
|
|
1710
|
+
.command('rdap')
|
|
1711
|
+
.description('RDAP lookup (modern WHOIS replacement)')
|
|
1712
|
+
.argument('<domain>', 'Domain name to lookup')
|
|
1713
|
+
.action(async (domain) => {
|
|
1714
|
+
const { rdap } = await import('../utils/rdap.js');
|
|
1715
|
+
const { Client } = await import('../core/client.js');
|
|
1716
|
+
console.log(colors.gray(`Looking up RDAP for ${domain}...`));
|
|
1717
|
+
try {
|
|
1718
|
+
const client = new Client();
|
|
1719
|
+
const result = await rdap(client, domain);
|
|
1720
|
+
console.log(`
|
|
1721
|
+
${colors.bold(colors.cyan('📋 RDAP Report'))}
|
|
1722
|
+
|
|
1723
|
+
${colors.bold('Domain:')} ${result.ldhName || domain}
|
|
1724
|
+
${colors.bold('Handle:')} ${result.handle || 'N/A'}
|
|
1725
|
+
${colors.bold('Status:')} ${result.status?.join(', ') || 'N/A'}
|
|
1726
|
+
`);
|
|
1727
|
+
if (result.events && result.events.length > 0) {
|
|
1728
|
+
console.log(`${colors.bold('Events:')}`);
|
|
1729
|
+
result.events.forEach((event) => {
|
|
1730
|
+
const date = event.eventDate ? new Date(event.eventDate).toISOString().split('T')[0] : 'N/A';
|
|
1731
|
+
console.log(` ${colors.gray(event.eventAction + ':')} ${date}`);
|
|
1732
|
+
});
|
|
1733
|
+
console.log('');
|
|
1734
|
+
}
|
|
1735
|
+
if (result.nameservers && result.nameservers.length > 0) {
|
|
1736
|
+
console.log(`${colors.bold('Name Servers:')}`);
|
|
1737
|
+
result.nameservers.forEach((ns) => {
|
|
1738
|
+
console.log(` ${colors.gray('•')} ${ns.ldhName}`);
|
|
1739
|
+
});
|
|
1740
|
+
console.log('');
|
|
1741
|
+
}
|
|
1742
|
+
if (result.entities && result.entities.length > 0) {
|
|
1743
|
+
console.log(`${colors.bold('Entities:')}`);
|
|
1744
|
+
result.entities.slice(0, 5).forEach((entity) => {
|
|
1745
|
+
const roles = entity.roles?.join(', ') || 'N/A';
|
|
1746
|
+
console.log(` ${colors.gray(roles + ':')} ${entity.handle || 'Unknown'}`);
|
|
1747
|
+
});
|
|
1748
|
+
console.log('');
|
|
1749
|
+
}
|
|
1750
|
+
if (result.links && result.links.length > 0) {
|
|
1751
|
+
const selfLink = result.links.find((l) => l.rel === 'self');
|
|
1752
|
+
if (selfLink) {
|
|
1753
|
+
console.log(`${colors.gray('Source:')} ${selfLink.href}`);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
catch (err) {
|
|
1758
|
+
console.error(colors.red(`RDAP Lookup Failed: ${err.message}`));
|
|
1759
|
+
process.exit(1);
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
program
|
|
1763
|
+
.command('ping')
|
|
1764
|
+
.description('TCP connectivity check to a host')
|
|
1765
|
+
.argument('<host>', 'Hostname or IP address')
|
|
1766
|
+
.argument('[port]', 'Port number (default: 80 for HTTP, 443 for HTTPS)', '443')
|
|
1767
|
+
.option('-c, --count <n>', 'Number of pings', '4')
|
|
1768
|
+
.action(async (host, port, options) => {
|
|
1769
|
+
const net = await import('node:net');
|
|
1770
|
+
const count = parseInt(options.count);
|
|
1771
|
+
const portNum = parseInt(port);
|
|
1772
|
+
const results = [];
|
|
1773
|
+
console.log(colors.gray(`Pinging ${host}:${portNum}...`));
|
|
1774
|
+
console.log('');
|
|
1775
|
+
for (let i = 0; i < count; i++) {
|
|
1776
|
+
const start = performance.now();
|
|
1777
|
+
try {
|
|
1778
|
+
await new Promise((resolve, reject) => {
|
|
1779
|
+
const socket = net.connect(portNum, host, () => {
|
|
1780
|
+
socket.destroy();
|
|
1781
|
+
resolve();
|
|
1782
|
+
});
|
|
1783
|
+
socket.setTimeout(5000);
|
|
1784
|
+
socket.on('timeout', () => {
|
|
1785
|
+
socket.destroy();
|
|
1786
|
+
reject(new Error('Timeout'));
|
|
1787
|
+
});
|
|
1788
|
+
socket.on('error', reject);
|
|
1789
|
+
});
|
|
1790
|
+
const elapsed = performance.now() - start;
|
|
1791
|
+
results.push(elapsed);
|
|
1792
|
+
console.log(`${colors.green('✔')} Connected to ${host}:${portNum} - ${colors.cyan(elapsed.toFixed(2) + 'ms')}`);
|
|
1793
|
+
}
|
|
1794
|
+
catch (err) {
|
|
1795
|
+
console.log(`${colors.red('✖')} Failed to connect: ${err.message}`);
|
|
1796
|
+
}
|
|
1797
|
+
if (i < count - 1) {
|
|
1798
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (results.length > 0) {
|
|
1802
|
+
const avg = results.reduce((a, b) => a + b, 0) / results.length;
|
|
1803
|
+
const min = Math.min(...results);
|
|
1804
|
+
const max = Math.max(...results);
|
|
1805
|
+
console.log(`
|
|
1806
|
+
${colors.bold('Statistics:')}
|
|
1807
|
+
${colors.gray('Sent:')} ${count}
|
|
1808
|
+
${colors.gray('Received:')} ${results.length}
|
|
1809
|
+
${colors.gray('Lost:')} ${count - results.length} (${((count - results.length) / count * 100).toFixed(0)}%)
|
|
1810
|
+
${colors.gray('Min:')} ${min.toFixed(2)}ms
|
|
1811
|
+
${colors.gray('Avg:')} ${avg.toFixed(2)}ms
|
|
1812
|
+
${colors.gray('Max:')} ${max.toFixed(2)}ms
|
|
1813
|
+
`);
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
const ftpCmd = program.command('ftp').description('FTP client operations');
|
|
1817
|
+
ftpCmd
|
|
1818
|
+
.command('ls')
|
|
1819
|
+
.description('List files in a remote directory')
|
|
1820
|
+
.argument('<host>', 'FTP server hostname')
|
|
1821
|
+
.argument('[path]', 'Remote path to list', '/')
|
|
1822
|
+
.option('-u, --user <username>', 'Username', 'anonymous')
|
|
1823
|
+
.option('-p, --pass <password>', 'Password', 'anonymous@')
|
|
1824
|
+
.option('-P, --port <port>', 'Port number', '21')
|
|
1825
|
+
.option('--secure', 'Use FTPS (explicit TLS)')
|
|
1826
|
+
.option('--implicit', 'Use implicit FTPS (port 990)')
|
|
1827
|
+
.action(async (host, path, options) => {
|
|
1828
|
+
const { createFTP } = await import('../protocols/ftp.js');
|
|
1829
|
+
const secure = options.implicit ? 'implicit' : options.secure ? true : false;
|
|
1830
|
+
const client = createFTP({
|
|
1831
|
+
host,
|
|
1832
|
+
port: parseInt(options.port),
|
|
1833
|
+
user: options.user,
|
|
1834
|
+
password: options.pass,
|
|
1835
|
+
secure,
|
|
1836
|
+
});
|
|
1837
|
+
console.log(colors.gray(`Connecting to ${host}...`));
|
|
1838
|
+
try {
|
|
1839
|
+
const connectResult = await client.connect();
|
|
1840
|
+
if (!connectResult.success) {
|
|
1841
|
+
console.error(colors.red(`Connection failed: ${connectResult.message}`));
|
|
1842
|
+
process.exit(1);
|
|
1843
|
+
}
|
|
1844
|
+
console.log(colors.green('Connected'));
|
|
1845
|
+
console.log(colors.gray(`Listing ${path}...`));
|
|
1846
|
+
const result = await client.list(path);
|
|
1847
|
+
if (!result.success || !result.data) {
|
|
1848
|
+
console.error(colors.red(`List failed: ${result.message}`));
|
|
1849
|
+
await client.close();
|
|
1850
|
+
process.exit(1);
|
|
1851
|
+
}
|
|
1852
|
+
console.log('');
|
|
1853
|
+
console.log(colors.bold(`Contents of ${path}:`));
|
|
1854
|
+
console.log('');
|
|
1855
|
+
for (const item of result.data) {
|
|
1856
|
+
const typeChar = item.type === 'directory' ? 'd' : item.type === 'link' ? 'l' : '-';
|
|
1857
|
+
const perms = item.permissions || 'rwxr-xr-x';
|
|
1858
|
+
const size = item.size.toString().padStart(10);
|
|
1859
|
+
const date = item.rawModifiedAt || '';
|
|
1860
|
+
const nameColor = item.type === 'directory' ? colors.blue : item.type === 'link' ? colors.cyan : colors.white;
|
|
1861
|
+
console.log(`${typeChar}${perms} ${size} ${date.padEnd(12)} ${nameColor(item.name)}`);
|
|
1862
|
+
}
|
|
1863
|
+
console.log('');
|
|
1864
|
+
console.log(colors.gray(`Total: ${result.data.length} items`));
|
|
1865
|
+
await client.close();
|
|
1866
|
+
}
|
|
1867
|
+
catch (err) {
|
|
1868
|
+
console.error(colors.red(`FTP Error: ${err.message}`));
|
|
1869
|
+
process.exit(1);
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
ftpCmd
|
|
1873
|
+
.command('get')
|
|
1874
|
+
.description('Download a file from FTP server')
|
|
1875
|
+
.argument('<host>', 'FTP server hostname')
|
|
1876
|
+
.argument('<remote>', 'Remote file path')
|
|
1877
|
+
.argument('[local]', 'Local file path (default: same filename)')
|
|
1878
|
+
.option('-u, --user <username>', 'Username', 'anonymous')
|
|
1879
|
+
.option('-p, --pass <password>', 'Password', 'anonymous@')
|
|
1880
|
+
.option('-P, --port <port>', 'Port number', '21')
|
|
1881
|
+
.option('--secure', 'Use FTPS (explicit TLS)')
|
|
1882
|
+
.option('--implicit', 'Use implicit FTPS (port 990)')
|
|
1883
|
+
.action(async (host, remote, local, options) => {
|
|
1884
|
+
const { createFTP } = await import('../protocols/ftp.js');
|
|
1885
|
+
const path = await import('node:path');
|
|
1886
|
+
const localPath = local || path.basename(remote);
|
|
1887
|
+
const secure = options.implicit ? 'implicit' : options.secure ? true : false;
|
|
1888
|
+
const client = createFTP({
|
|
1889
|
+
host,
|
|
1890
|
+
port: parseInt(options.port),
|
|
1891
|
+
user: options.user,
|
|
1892
|
+
password: options.pass,
|
|
1893
|
+
secure,
|
|
1894
|
+
});
|
|
1895
|
+
console.log(colors.gray(`Connecting to ${host}...`));
|
|
1896
|
+
try {
|
|
1897
|
+
const connectResult = await client.connect();
|
|
1898
|
+
if (!connectResult.success) {
|
|
1899
|
+
console.error(colors.red(`Connection failed: ${connectResult.message}`));
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1902
|
+
console.log(colors.green('Connected'));
|
|
1903
|
+
console.log(colors.gray(`Downloading ${remote} → ${localPath}...`));
|
|
1904
|
+
let lastProgress = 0;
|
|
1905
|
+
client.progress((p) => {
|
|
1906
|
+
const mb = (p.bytesOverall / 1024 / 1024).toFixed(2);
|
|
1907
|
+
if (p.bytesOverall - lastProgress > 100000) {
|
|
1908
|
+
process.stdout.write(`\r ${colors.cyan(mb + ' MB')} downloaded...`);
|
|
1909
|
+
lastProgress = p.bytesOverall;
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
const result = await client.download(remote, localPath);
|
|
1913
|
+
console.log('');
|
|
1914
|
+
if (!result.success) {
|
|
1915
|
+
console.error(colors.red(`Download failed: ${result.message}`));
|
|
1916
|
+
await client.close();
|
|
1917
|
+
process.exit(1);
|
|
1918
|
+
}
|
|
1919
|
+
console.log(colors.green(`✔ Downloaded to ${localPath}`));
|
|
1920
|
+
await client.close();
|
|
1921
|
+
}
|
|
1922
|
+
catch (err) {
|
|
1923
|
+
console.error(colors.red(`FTP Error: ${err.message}`));
|
|
1924
|
+
process.exit(1);
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
ftpCmd
|
|
1928
|
+
.command('put')
|
|
1929
|
+
.description('Upload a file to FTP server')
|
|
1930
|
+
.argument('<host>', 'FTP server hostname')
|
|
1931
|
+
.argument('<local>', 'Local file path')
|
|
1932
|
+
.argument('[remote]', 'Remote file path (default: same filename)')
|
|
1933
|
+
.option('-u, --user <username>', 'Username', 'anonymous')
|
|
1934
|
+
.option('-p, --pass <password>', 'Password', 'anonymous@')
|
|
1935
|
+
.option('-P, --port <port>', 'Port number', '21')
|
|
1936
|
+
.option('--secure', 'Use FTPS (explicit TLS)')
|
|
1937
|
+
.option('--implicit', 'Use implicit FTPS (port 990)')
|
|
1938
|
+
.action(async (host, local, remote, options) => {
|
|
1939
|
+
const { createFTP } = await import('../protocols/ftp.js');
|
|
1940
|
+
const path = await import('node:path');
|
|
1941
|
+
const remotePath = remote || '/' + path.basename(local);
|
|
1942
|
+
const secure = options.implicit ? 'implicit' : options.secure ? true : false;
|
|
1943
|
+
const client = createFTP({
|
|
1944
|
+
host,
|
|
1945
|
+
port: parseInt(options.port),
|
|
1946
|
+
user: options.user,
|
|
1947
|
+
password: options.pass,
|
|
1948
|
+
secure,
|
|
1949
|
+
});
|
|
1950
|
+
console.log(colors.gray(`Connecting to ${host}...`));
|
|
1951
|
+
try {
|
|
1952
|
+
const connectResult = await client.connect();
|
|
1953
|
+
if (!connectResult.success) {
|
|
1954
|
+
console.error(colors.red(`Connection failed: ${connectResult.message}`));
|
|
1955
|
+
process.exit(1);
|
|
1956
|
+
}
|
|
1957
|
+
console.log(colors.green('Connected'));
|
|
1958
|
+
console.log(colors.gray(`Uploading ${local} → ${remotePath}...`));
|
|
1959
|
+
let lastProgress = 0;
|
|
1960
|
+
client.progress((p) => {
|
|
1961
|
+
const mb = (p.bytesOverall / 1024 / 1024).toFixed(2);
|
|
1962
|
+
if (p.bytesOverall - lastProgress > 100000) {
|
|
1963
|
+
process.stdout.write(`\r ${colors.cyan(mb + ' MB')} uploaded...`);
|
|
1964
|
+
lastProgress = p.bytesOverall;
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
const result = await client.upload(local, remotePath);
|
|
1968
|
+
console.log('');
|
|
1969
|
+
if (!result.success) {
|
|
1970
|
+
console.error(colors.red(`Upload failed: ${result.message}`));
|
|
1971
|
+
await client.close();
|
|
1972
|
+
process.exit(1);
|
|
1973
|
+
}
|
|
1974
|
+
console.log(colors.green(`✔ Uploaded to ${remotePath}`));
|
|
1975
|
+
await client.close();
|
|
1976
|
+
}
|
|
1977
|
+
catch (err) {
|
|
1978
|
+
console.error(colors.red(`FTP Error: ${err.message}`));
|
|
1979
|
+
process.exit(1);
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
ftpCmd
|
|
1983
|
+
.command('rm')
|
|
1984
|
+
.description('Delete a file from FTP server')
|
|
1985
|
+
.argument('<host>', 'FTP server hostname')
|
|
1986
|
+
.argument('<path>', 'Remote file path to delete')
|
|
1987
|
+
.option('-u, --user <username>', 'Username', 'anonymous')
|
|
1988
|
+
.option('-p, --pass <password>', 'Password', 'anonymous@')
|
|
1989
|
+
.option('-P, --port <port>', 'Port number', '21')
|
|
1990
|
+
.option('--secure', 'Use FTPS (explicit TLS)')
|
|
1991
|
+
.option('--implicit', 'Use implicit FTPS (port 990)')
|
|
1992
|
+
.action(async (host, remotePath, options) => {
|
|
1993
|
+
const { createFTP } = await import('../protocols/ftp.js');
|
|
1994
|
+
const secure = options.implicit ? 'implicit' : options.secure ? true : false;
|
|
1995
|
+
const client = createFTP({
|
|
1996
|
+
host,
|
|
1997
|
+
port: parseInt(options.port),
|
|
1998
|
+
user: options.user,
|
|
1999
|
+
password: options.pass,
|
|
2000
|
+
secure,
|
|
2001
|
+
});
|
|
2002
|
+
console.log(colors.gray(`Connecting to ${host}...`));
|
|
2003
|
+
try {
|
|
2004
|
+
const connectResult = await client.connect();
|
|
2005
|
+
if (!connectResult.success) {
|
|
2006
|
+
console.error(colors.red(`Connection failed: ${connectResult.message}`));
|
|
2007
|
+
process.exit(1);
|
|
2008
|
+
}
|
|
2009
|
+
console.log(colors.green('Connected'));
|
|
2010
|
+
console.log(colors.gray(`Deleting ${remotePath}...`));
|
|
2011
|
+
const result = await client.delete(remotePath);
|
|
2012
|
+
if (!result.success) {
|
|
2013
|
+
console.error(colors.red(`Delete failed: ${result.message}`));
|
|
2014
|
+
await client.close();
|
|
2015
|
+
process.exit(1);
|
|
2016
|
+
}
|
|
2017
|
+
console.log(colors.green(`✔ Deleted ${remotePath}`));
|
|
2018
|
+
await client.close();
|
|
2019
|
+
}
|
|
2020
|
+
catch (err) {
|
|
2021
|
+
console.error(colors.red(`FTP Error: ${err.message}`));
|
|
2022
|
+
process.exit(1);
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
ftpCmd
|
|
2026
|
+
.command('mkdir')
|
|
2027
|
+
.description('Create a directory on FTP server')
|
|
2028
|
+
.argument('<host>', 'FTP server hostname')
|
|
2029
|
+
.argument('<path>', 'Remote directory path to create')
|
|
2030
|
+
.option('-u, --user <username>', 'Username', 'anonymous')
|
|
2031
|
+
.option('-p, --pass <password>', 'Password', 'anonymous@')
|
|
2032
|
+
.option('-P, --port <port>', 'Port number', '21')
|
|
2033
|
+
.option('--secure', 'Use FTPS (explicit TLS)')
|
|
2034
|
+
.option('--implicit', 'Use implicit FTPS (port 990)')
|
|
2035
|
+
.action(async (host, remotePath, options) => {
|
|
2036
|
+
const { createFTP } = await import('../protocols/ftp.js');
|
|
2037
|
+
const secure = options.implicit ? 'implicit' : options.secure ? true : false;
|
|
2038
|
+
const client = createFTP({
|
|
2039
|
+
host,
|
|
2040
|
+
port: parseInt(options.port),
|
|
2041
|
+
user: options.user,
|
|
2042
|
+
password: options.pass,
|
|
2043
|
+
secure,
|
|
2044
|
+
});
|
|
2045
|
+
console.log(colors.gray(`Connecting to ${host}...`));
|
|
2046
|
+
try {
|
|
2047
|
+
const connectResult = await client.connect();
|
|
2048
|
+
if (!connectResult.success) {
|
|
2049
|
+
console.error(colors.red(`Connection failed: ${connectResult.message}`));
|
|
2050
|
+
process.exit(1);
|
|
798
2051
|
}
|
|
799
|
-
console.log(
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
${colors.gray('Accuracy:')} ${info.accuracy ? `~${info.accuracy} km` : 'N/A'}
|
|
810
|
-
|
|
811
|
-
${colors.bold('Network:')}
|
|
812
|
-
${colors.gray('IP:')} ${info.ip}
|
|
813
|
-
${colors.gray('Type:')} ${info.isIPv6 ? 'IPv6' : 'IPv4'}
|
|
814
|
-
${colors.gray('Postal:')} ${info.postal || 'N/A'}
|
|
815
|
-
`);
|
|
2052
|
+
console.log(colors.green('Connected'));
|
|
2053
|
+
console.log(colors.gray(`Creating ${remotePath}...`));
|
|
2054
|
+
const result = await client.mkdir(remotePath);
|
|
2055
|
+
if (!result.success) {
|
|
2056
|
+
console.error(colors.red(`Mkdir failed: ${result.message}`));
|
|
2057
|
+
await client.close();
|
|
2058
|
+
process.exit(1);
|
|
2059
|
+
}
|
|
2060
|
+
console.log(colors.green(`✔ Created ${remotePath}`));
|
|
2061
|
+
await client.close();
|
|
816
2062
|
}
|
|
817
2063
|
catch (err) {
|
|
818
|
-
console.error(colors.red(`
|
|
2064
|
+
console.error(colors.red(`FTP Error: ${err.message}`));
|
|
819
2065
|
process.exit(1);
|
|
820
2066
|
}
|
|
821
2067
|
});
|
|
822
2068
|
program
|
|
823
|
-
.command('
|
|
824
|
-
.
|
|
825
|
-
.description('Inspect TLS/SSL certificate of a host')
|
|
2069
|
+
.command('telnet')
|
|
2070
|
+
.description('Connect to a Telnet server')
|
|
826
2071
|
.argument('<host>', 'Hostname or IP address')
|
|
827
|
-
.argument('[port]', 'Port number
|
|
828
|
-
.
|
|
829
|
-
|
|
830
|
-
|
|
2072
|
+
.argument('[port]', 'Port number', '23')
|
|
2073
|
+
.option('-t, --timeout <ms>', 'Connection timeout in ms', '30000')
|
|
2074
|
+
.action(async (host, port, options) => {
|
|
2075
|
+
const { createTelnet } = await import('../protocols/telnet.js');
|
|
2076
|
+
console.log(colors.gray(`Connecting to ${host}:${port}...`));
|
|
2077
|
+
const client = createTelnet({
|
|
2078
|
+
host,
|
|
2079
|
+
port: parseInt(port),
|
|
2080
|
+
timeout: parseInt(options.timeout),
|
|
2081
|
+
});
|
|
831
2082
|
try {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const validIcon = info.valid ? colors.green('✔ Valid') : colors.red('✖ Expired');
|
|
839
|
-
const authIcon = info.authorized ? colors.green('✔ Trusted') : colors.yellow('⚠ Self-signed/Untrusted');
|
|
840
|
-
console.log(`
|
|
841
|
-
${colors.bold(colors.cyan('🔒 TLS Certificate Report'))}
|
|
842
|
-
|
|
843
|
-
${colors.bold('Status:')}
|
|
844
|
-
${validIcon}
|
|
845
|
-
${authIcon}
|
|
846
|
-
${colors.gray('Days Remaining:')} ${daysColor(info.daysRemaining.toString())}
|
|
847
|
-
|
|
848
|
-
${colors.bold('Certificate:')}
|
|
849
|
-
${colors.gray('Subject:')} ${info.subject?.CN || info.subject?.O || 'N/A'}
|
|
850
|
-
${colors.gray('Issuer:')} ${info.issuer?.CN || info.issuer?.O || 'N/A'}
|
|
851
|
-
${colors.gray('Valid From:')} ${info.validFrom.toISOString().split('T')[0]}
|
|
852
|
-
${colors.gray('Valid To:')} ${info.validTo.toISOString().split('T')[0]}
|
|
853
|
-
${colors.gray('Serial:')} ${info.serialNumber}
|
|
854
|
-
|
|
855
|
-
${colors.bold('Security:')}
|
|
856
|
-
${colors.gray('Protocol:')} ${info.protocol || 'N/A'}
|
|
857
|
-
${colors.gray('Cipher:')} ${info.cipher?.name || 'N/A'}
|
|
858
|
-
${colors.gray('Key:')} ${info.pubkey ? `${info.pubkey.algo.toUpperCase()} ${info.pubkey.size}-bit` : 'N/A'}
|
|
859
|
-
|
|
860
|
-
${colors.bold('Fingerprints:')}
|
|
861
|
-
${colors.gray('SHA-1:')} ${info.fingerprint}
|
|
862
|
-
${colors.gray('SHA-256:')} ${info.fingerprint256?.slice(0, 40)}...
|
|
863
|
-
`);
|
|
864
|
-
if (info.altNames && info.altNames.length > 0) {
|
|
865
|
-
console.log(`${colors.bold('Subject Alternative Names:')}`);
|
|
866
|
-
info.altNames.slice(0, 10).forEach(san => {
|
|
867
|
-
console.log(` ${colors.gray('•')} ${san}`);
|
|
868
|
-
});
|
|
869
|
-
if (info.altNames.length > 10) {
|
|
870
|
-
console.log(` ${colors.gray(`... and ${info.altNames.length - 10} more`)}`);
|
|
871
|
-
}
|
|
872
|
-
console.log('');
|
|
873
|
-
}
|
|
874
|
-
if (info.extKeyUsage && info.extKeyUsage.length > 0) {
|
|
875
|
-
console.log(`${colors.bold('Extended Key Usage:')}`);
|
|
876
|
-
info.extKeyUsage.forEach(oid => {
|
|
877
|
-
const oidNames = {
|
|
878
|
-
'1.3.6.1.5.5.7.3.1': 'Server Authentication',
|
|
879
|
-
'1.3.6.1.5.5.7.3.2': 'Client Authentication',
|
|
880
|
-
'1.3.6.1.5.5.7.3.3': 'Code Signing',
|
|
881
|
-
'1.3.6.1.5.5.7.3.4': 'Email Protection',
|
|
882
|
-
};
|
|
883
|
-
console.log(` ${colors.gray('•')} ${oidNames[oid] || oid}`);
|
|
884
|
-
});
|
|
885
|
-
console.log('');
|
|
2083
|
+
await client.connect();
|
|
2084
|
+
console.log(colors.green(`Connected to ${host}:${port}`));
|
|
2085
|
+
console.log(colors.gray('Type your commands. Press Ctrl+C to exit.'));
|
|
2086
|
+
console.log('');
|
|
2087
|
+
if (process.stdin.isTTY) {
|
|
2088
|
+
process.stdin.setRawMode(true);
|
|
886
2089
|
}
|
|
2090
|
+
process.stdin.resume();
|
|
2091
|
+
process.stdin.on('data', async (data) => {
|
|
2092
|
+
if (data[0] === 0x03) {
|
|
2093
|
+
console.log(colors.yellow('\nDisconnecting...'));
|
|
2094
|
+
await client.close();
|
|
2095
|
+
process.exit(0);
|
|
2096
|
+
}
|
|
2097
|
+
await client.send(data.toString());
|
|
2098
|
+
});
|
|
2099
|
+
client.on('data', (data) => {
|
|
2100
|
+
process.stdout.write(data);
|
|
2101
|
+
});
|
|
2102
|
+
client.on('close', () => {
|
|
2103
|
+
console.log(colors.yellow('\nConnection closed'));
|
|
2104
|
+
process.exit(0);
|
|
2105
|
+
});
|
|
2106
|
+
client.on('error', (err) => {
|
|
2107
|
+
console.error(colors.red(`Error: ${err.message}`));
|
|
2108
|
+
process.exit(1);
|
|
2109
|
+
});
|
|
887
2110
|
}
|
|
888
2111
|
catch (err) {
|
|
889
|
-
console.error(colors.red(`
|
|
2112
|
+
console.error(colors.red(`Telnet Error: ${err.message}`));
|
|
890
2113
|
process.exit(1);
|
|
891
2114
|
}
|
|
892
2115
|
});
|
|
@@ -1266,18 +2489,519 @@ ${colors.bold(colors.yellow('Record Types:'))}
|
|
|
1266
2489
|
domain = arg;
|
|
1267
2490
|
}
|
|
1268
2491
|
}
|
|
1269
|
-
if (!domain) {
|
|
1270
|
-
console.error(colors.red('Error: Domain/IP is required'));
|
|
1271
|
-
console.log(colors.gray('Usage: rek dig example.com [TYPE]'));
|
|
1272
|
-
console.log(colors.gray(' rek dig -x 8.8.8.8'));
|
|
2492
|
+
if (!domain) {
|
|
2493
|
+
console.error(colors.red('Error: Domain/IP is required'));
|
|
2494
|
+
console.log(colors.gray('Usage: rek dig example.com [TYPE]'));
|
|
2495
|
+
console.log(colors.gray(' rek dig -x 8.8.8.8'));
|
|
2496
|
+
process.exit(1);
|
|
2497
|
+
}
|
|
2498
|
+
try {
|
|
2499
|
+
const result = await dig(domain, { server, type, reverse, short });
|
|
2500
|
+
console.log(formatDigOutput(result, short));
|
|
2501
|
+
}
|
|
2502
|
+
catch (err) {
|
|
2503
|
+
console.error(colors.red(`dig: ${err.message}`));
|
|
2504
|
+
process.exit(1);
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
program
|
|
2508
|
+
.command('graphql')
|
|
2509
|
+
.description('Execute a GraphQL query')
|
|
2510
|
+
.argument('<url>', 'GraphQL endpoint URL')
|
|
2511
|
+
.option('-q, --query <query>', 'GraphQL query string')
|
|
2512
|
+
.option('-f, --file <file>', 'Path to GraphQL query file')
|
|
2513
|
+
.option('-v, --variables <json>', 'Variables as JSON string')
|
|
2514
|
+
.option('--var-file <file>', 'Path to variables JSON file')
|
|
2515
|
+
.option('-H, --header <header>', 'Add header (can be used multiple times)', (val, prev) => [...prev, val], [])
|
|
2516
|
+
.action(async (url, options) => {
|
|
2517
|
+
const { graphql } = await import('../plugins/graphql.js');
|
|
2518
|
+
const { createClient } = await import('../core/client.js');
|
|
2519
|
+
const fs = await import('node:fs/promises');
|
|
2520
|
+
let query = options.query;
|
|
2521
|
+
let variables = {};
|
|
2522
|
+
if (options.file) {
|
|
2523
|
+
try {
|
|
2524
|
+
query = await fs.readFile(options.file, 'utf-8');
|
|
2525
|
+
}
|
|
2526
|
+
catch (err) {
|
|
2527
|
+
console.error(colors.red(`Failed to read query file: ${err.message}`));
|
|
2528
|
+
process.exit(1);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
if (!query) {
|
|
2532
|
+
console.error(colors.red('Error: Query is required. Use --query or --file'));
|
|
2533
|
+
console.log(colors.gray('Example: rek graphql https://api.example.com/graphql -q "query { users { id name } }"'));
|
|
2534
|
+
process.exit(1);
|
|
2535
|
+
}
|
|
2536
|
+
if (options.variables) {
|
|
2537
|
+
try {
|
|
2538
|
+
variables = JSON.parse(options.variables);
|
|
2539
|
+
}
|
|
2540
|
+
catch {
|
|
2541
|
+
console.error(colors.red('Invalid JSON in --variables'));
|
|
2542
|
+
process.exit(1);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
else if (options.varFile) {
|
|
2546
|
+
try {
|
|
2547
|
+
const content = await fs.readFile(options.varFile, 'utf-8');
|
|
2548
|
+
variables = JSON.parse(content);
|
|
2549
|
+
}
|
|
2550
|
+
catch (err) {
|
|
2551
|
+
console.error(colors.red(`Failed to read variables file: ${err.message}`));
|
|
2552
|
+
process.exit(1);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
const headers = {
|
|
2556
|
+
'Content-Type': 'application/json',
|
|
2557
|
+
};
|
|
2558
|
+
for (const h of options.header) {
|
|
2559
|
+
const [key, ...valueParts] = h.split(':');
|
|
2560
|
+
headers[key.trim()] = valueParts.join(':').trim();
|
|
2561
|
+
}
|
|
2562
|
+
console.log(colors.gray(`Executing GraphQL query against ${url}...`));
|
|
2563
|
+
try {
|
|
2564
|
+
const client = createClient({ baseUrl: url, headers });
|
|
2565
|
+
const result = await graphql(client, query, variables);
|
|
2566
|
+
console.log('');
|
|
2567
|
+
console.log(colors.bold(colors.green('Response:')));
|
|
2568
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2569
|
+
}
|
|
2570
|
+
catch (err) {
|
|
2571
|
+
console.error(colors.red(`GraphQL Error: ${err.message}`));
|
|
2572
|
+
if (err.errors) {
|
|
2573
|
+
console.log(colors.bold(colors.red('GraphQL Errors:')));
|
|
2574
|
+
for (const e of err.errors) {
|
|
2575
|
+
console.log(` ${colors.red('•')} ${e.message}`);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
program
|
|
2582
|
+
.command('jsonrpc')
|
|
2583
|
+
.description('Execute a JSON-RPC 2.0 call')
|
|
2584
|
+
.argument('<url>', 'JSON-RPC endpoint URL')
|
|
2585
|
+
.argument('<method>', 'Method name to call')
|
|
2586
|
+
.argument('[params...]', 'Method parameters (JSON values)')
|
|
2587
|
+
.option('-n, --named', 'Use named parameters (key=value format)')
|
|
2588
|
+
.option('-b, --batch <methods>', 'Batch multiple methods (comma-separated)')
|
|
2589
|
+
.option('-H, --header <header>', 'Add header (can be used multiple times)', (val, prev) => [...prev, val], [])
|
|
2590
|
+
.action(async (url, method, params, options) => {
|
|
2591
|
+
const { createJsonRpcClient } = await import('../plugins/jsonrpc.js');
|
|
2592
|
+
const { createClient } = await import('../core/client.js');
|
|
2593
|
+
const headers = {
|
|
2594
|
+
'Content-Type': 'application/json',
|
|
2595
|
+
};
|
|
2596
|
+
for (const h of options.header) {
|
|
2597
|
+
const [key, ...valueParts] = h.split(':');
|
|
2598
|
+
headers[key.trim()] = valueParts.join(':').trim();
|
|
2599
|
+
}
|
|
2600
|
+
const client = createClient({ baseUrl: url, headers });
|
|
2601
|
+
const rpc = createJsonRpcClient(client, { endpoint: '' });
|
|
2602
|
+
console.log(colors.gray(`Calling ${method} on ${url}...`));
|
|
2603
|
+
try {
|
|
2604
|
+
let rpcParams;
|
|
2605
|
+
if (options.named) {
|
|
2606
|
+
rpcParams = {};
|
|
2607
|
+
for (const p of params) {
|
|
2608
|
+
const [key, ...valueParts] = p.split('=');
|
|
2609
|
+
const value = valueParts.join('=');
|
|
2610
|
+
try {
|
|
2611
|
+
rpcParams[key] = JSON.parse(value);
|
|
2612
|
+
}
|
|
2613
|
+
catch {
|
|
2614
|
+
rpcParams[key] = value;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
rpcParams = params.map((p) => {
|
|
2620
|
+
try {
|
|
2621
|
+
return JSON.parse(p);
|
|
2622
|
+
}
|
|
2623
|
+
catch {
|
|
2624
|
+
return p;
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
const result = await rpc.call(method, rpcParams);
|
|
2629
|
+
console.log('');
|
|
2630
|
+
console.log(colors.bold(colors.green('Result:')));
|
|
2631
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2632
|
+
}
|
|
2633
|
+
catch (err) {
|
|
2634
|
+
console.error(colors.red(`JSON-RPC Error: ${err.message}`));
|
|
2635
|
+
if (err.code) {
|
|
2636
|
+
console.log(colors.gray(`Error code: ${err.code}`));
|
|
2637
|
+
}
|
|
2638
|
+
if (err.data) {
|
|
2639
|
+
console.log(colors.gray(`Error data: ${JSON.stringify(err.data)}`));
|
|
2640
|
+
}
|
|
2641
|
+
process.exit(1);
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
const hlsCmd = program.command('hls').description('HLS streaming operations');
|
|
2645
|
+
hlsCmd
|
|
2646
|
+
.command('info')
|
|
2647
|
+
.description('Get information about an HLS stream')
|
|
2648
|
+
.argument('<url>', 'HLS playlist URL')
|
|
2649
|
+
.action(async (url) => {
|
|
2650
|
+
const { Client } = await import('../core/client.js');
|
|
2651
|
+
const client = new Client();
|
|
2652
|
+
console.log(colors.gray(`Fetching playlist from ${url}...`));
|
|
2653
|
+
try {
|
|
2654
|
+
const res = await client.get(url);
|
|
2655
|
+
const content = await res.text();
|
|
2656
|
+
const lines = content.split('\n').map(l => l.trim()).filter(Boolean);
|
|
2657
|
+
if (!lines[0]?.startsWith('#EXTM3U')) {
|
|
2658
|
+
console.error(colors.red('Not a valid HLS playlist'));
|
|
2659
|
+
process.exit(1);
|
|
2660
|
+
}
|
|
2661
|
+
const isMaster = lines.some(l => l.startsWith('#EXT-X-STREAM-INF'));
|
|
2662
|
+
console.log('');
|
|
2663
|
+
console.log(colors.bold(colors.cyan('HLS Stream Info')));
|
|
2664
|
+
console.log(`${colors.gray('URL:')} ${url}`);
|
|
2665
|
+
console.log(`${colors.gray('Type:')} ${isMaster ? 'Master Playlist' : 'Media Playlist'}`);
|
|
2666
|
+
console.log('');
|
|
2667
|
+
if (isMaster) {
|
|
2668
|
+
console.log(colors.bold('Available Qualities:'));
|
|
2669
|
+
let i = 0;
|
|
2670
|
+
for (let j = 0; j < lines.length; j++) {
|
|
2671
|
+
if (lines[j].startsWith('#EXT-X-STREAM-INF')) {
|
|
2672
|
+
const bandwidth = lines[j].match(/BANDWIDTH=(\d+)/)?.[1];
|
|
2673
|
+
const resolution = lines[j].match(/RESOLUTION=([^,]+)/)?.[1];
|
|
2674
|
+
const codecs = lines[j].match(/CODECS="([^"]+)"/)?.[1];
|
|
2675
|
+
const variantUrl = lines[j + 1];
|
|
2676
|
+
const bw = bandwidth ? `${Math.round(parseInt(bandwidth) / 1000)}kbps` : 'N/A';
|
|
2677
|
+
console.log(` ${colors.green(String(i + 1))}. ${resolution || 'Unknown'} - ${bw}`);
|
|
2678
|
+
if (codecs) {
|
|
2679
|
+
console.log(` ${colors.gray('Codecs:')} ${codecs}`);
|
|
2680
|
+
}
|
|
2681
|
+
i++;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
else {
|
|
2686
|
+
const segments = lines.filter(l => !l.startsWith('#') && l.length > 0);
|
|
2687
|
+
const targetDuration = lines.find(l => l.startsWith('#EXT-X-TARGETDURATION'))?.split(':')[1];
|
|
2688
|
+
const endList = lines.some(l => l === '#EXT-X-ENDLIST');
|
|
2689
|
+
const mediaSequence = lines.find(l => l.startsWith('#EXT-X-MEDIA-SEQUENCE'))?.split(':')[1];
|
|
2690
|
+
console.log(`${colors.gray('Segments:')} ${segments.length}`);
|
|
2691
|
+
if (targetDuration) {
|
|
2692
|
+
console.log(`${colors.gray('Target Duration:')} ${targetDuration}s`);
|
|
2693
|
+
}
|
|
2694
|
+
if (mediaSequence) {
|
|
2695
|
+
console.log(`${colors.gray('Media Sequence:')} ${mediaSequence}`);
|
|
2696
|
+
}
|
|
2697
|
+
console.log(`${colors.gray('Type:')} ${endList ? 'VOD' : 'Live'}`);
|
|
2698
|
+
let totalDuration = 0;
|
|
2699
|
+
for (const line of lines) {
|
|
2700
|
+
if (line.startsWith('#EXTINF:')) {
|
|
2701
|
+
const duration = parseFloat(line.split(':')[1].split(',')[0]);
|
|
2702
|
+
totalDuration += duration;
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
if (totalDuration > 0) {
|
|
2706
|
+
const minutes = Math.floor(totalDuration / 60);
|
|
2707
|
+
const seconds = Math.round(totalDuration % 60);
|
|
2708
|
+
console.log(`${colors.gray('Total Duration:')} ${minutes}m ${seconds}s`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
console.log('');
|
|
2712
|
+
}
|
|
2713
|
+
catch (err) {
|
|
2714
|
+
console.error(colors.red(`HLS Error: ${err.message}`));
|
|
2715
|
+
process.exit(1);
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
hlsCmd
|
|
2719
|
+
.command('download')
|
|
2720
|
+
.description('Download an HLS stream')
|
|
2721
|
+
.argument('<url>', 'HLS playlist URL')
|
|
2722
|
+
.argument('[output]', 'Output file path', 'output.ts')
|
|
2723
|
+
.option('-q, --quality <quality>', 'Quality: highest, lowest, or resolution (e.g., 720p)')
|
|
2724
|
+
.option('--live', 'Enable live stream mode')
|
|
2725
|
+
.option('-d, --duration <seconds>', 'Duration for live recording in seconds')
|
|
2726
|
+
.option('-c, --concurrency <n>', 'Concurrent segment downloads', '4')
|
|
2727
|
+
.action(async (url, output, options) => {
|
|
2728
|
+
const { hls } = await import('../plugins/hls.js');
|
|
2729
|
+
const { Client } = await import('../core/client.js');
|
|
2730
|
+
const client = new Client();
|
|
2731
|
+
console.log(colors.gray(`Downloading HLS stream from ${url}...`));
|
|
2732
|
+
console.log(colors.gray(`Output: ${output}`));
|
|
2733
|
+
console.log('');
|
|
2734
|
+
try {
|
|
2735
|
+
const hlsOptions = {
|
|
2736
|
+
concurrency: parseInt(options.concurrency),
|
|
2737
|
+
onProgress: (p) => {
|
|
2738
|
+
const segs = p.totalSegments
|
|
2739
|
+
? `${p.downloadedSegments}/${p.totalSegments}`
|
|
2740
|
+
: `${p.downloadedSegments}`;
|
|
2741
|
+
const mb = (p.downloadedBytes / 1024 / 1024).toFixed(2);
|
|
2742
|
+
process.stdout.write(`\r ${colors.cyan(segs)} segments | ${colors.cyan(mb + ' MB')} downloaded`);
|
|
2743
|
+
},
|
|
2744
|
+
};
|
|
2745
|
+
if (options.quality) {
|
|
2746
|
+
if (options.quality === 'highest' || options.quality === 'lowest') {
|
|
2747
|
+
hlsOptions.quality = options.quality;
|
|
2748
|
+
}
|
|
2749
|
+
else if (options.quality.includes('p')) {
|
|
2750
|
+
hlsOptions.quality = { resolution: options.quality };
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
if (options.live) {
|
|
2754
|
+
hlsOptions.live = options.duration
|
|
2755
|
+
? { duration: parseInt(options.duration) * 1000 }
|
|
2756
|
+
: true;
|
|
2757
|
+
}
|
|
2758
|
+
await hls(client, url, hlsOptions).download(output);
|
|
2759
|
+
console.log('');
|
|
2760
|
+
console.log(colors.green(`✔ Download complete: ${output}`));
|
|
2761
|
+
}
|
|
2762
|
+
catch (err) {
|
|
2763
|
+
console.log('');
|
|
2764
|
+
console.error(colors.red(`HLS Download Error: ${err.message}`));
|
|
2765
|
+
process.exit(1);
|
|
2766
|
+
}
|
|
2767
|
+
});
|
|
2768
|
+
const harCmd = program.command('har').description('HAR recording and playback');
|
|
2769
|
+
harCmd
|
|
2770
|
+
.command('record')
|
|
2771
|
+
.description('Record HTTP requests to HAR file')
|
|
2772
|
+
.argument('<file>', 'Output HAR file path')
|
|
2773
|
+
.argument('[url]', 'URL to request (optional, starts recording session)')
|
|
2774
|
+
.option('-a, --append', 'Append to existing HAR file')
|
|
2775
|
+
.addHelpText('after', `
|
|
2776
|
+
${colors.bold(colors.yellow('Usage:'))}
|
|
2777
|
+
Record a single request:
|
|
2778
|
+
${colors.green('$ rek har record output.har https://api.example.com/users')}
|
|
2779
|
+
|
|
2780
|
+
Start recording session (shell mode):
|
|
2781
|
+
${colors.green('$ rek har record output.har')}
|
|
2782
|
+
${colors.gray('Then use shell commands to make requests')}
|
|
2783
|
+
|
|
2784
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2785
|
+
${colors.green('$ rek har record api.har https://api.github.com/users/octocat')}
|
|
2786
|
+
${colors.green('$ rek har record session.har --append')}
|
|
2787
|
+
`)
|
|
2788
|
+
.action(async (file, url, options) => {
|
|
2789
|
+
const { createClient } = await import('../core/client.js');
|
|
2790
|
+
const { harRecorderPlugin } = await import('../plugins/har-recorder.js');
|
|
2791
|
+
const { promises: fsPromises } = await import('node:fs');
|
|
2792
|
+
let existingEntries = [];
|
|
2793
|
+
if (options.append) {
|
|
2794
|
+
try {
|
|
2795
|
+
const existing = await fsPromises.readFile(file, 'utf-8');
|
|
2796
|
+
const har = JSON.parse(existing);
|
|
2797
|
+
existingEntries = har.log?.entries || [];
|
|
2798
|
+
console.log(colors.gray(`Appending to existing HAR with ${existingEntries.length} entries`));
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
const client = createClient();
|
|
2804
|
+
const plugin = harRecorderPlugin({
|
|
2805
|
+
path: file,
|
|
2806
|
+
onEntry: (entry) => {
|
|
2807
|
+
console.log(colors.green('✔') + colors.gray(` Recorded: ${entry.request.method} ${entry.request.url}`));
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
plugin(client);
|
|
2811
|
+
if (url) {
|
|
2812
|
+
if (!url.startsWith('http')) {
|
|
2813
|
+
url = `https://${url}`;
|
|
2814
|
+
}
|
|
2815
|
+
console.log(colors.gray(`Recording request to ${url}...`));
|
|
2816
|
+
try {
|
|
2817
|
+
const response = await client.get(url);
|
|
2818
|
+
console.log(colors.green(`✔ Response: ${response.status} ${response.statusText}`));
|
|
2819
|
+
console.log(colors.gray(`Saved to ${file}`));
|
|
2820
|
+
}
|
|
2821
|
+
catch (error) {
|
|
2822
|
+
console.error(colors.red(`Request failed: ${error.message}`));
|
|
2823
|
+
process.exit(1);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
else {
|
|
2827
|
+
console.log(colors.cyan('HAR Recording Session'));
|
|
2828
|
+
console.log(colors.gray(`Recording to: ${file}`));
|
|
2829
|
+
console.log(colors.gray('Enter URLs to record, or "exit" to quit'));
|
|
2830
|
+
console.log('');
|
|
2831
|
+
const readline = await import('node:readline');
|
|
2832
|
+
const rl = readline.createInterface({
|
|
2833
|
+
input: process.stdin,
|
|
2834
|
+
output: process.stdout,
|
|
2835
|
+
});
|
|
2836
|
+
const prompt = () => {
|
|
2837
|
+
rl.question(colors.cyan('har> '), async (input) => {
|
|
2838
|
+
const line = input.trim();
|
|
2839
|
+
if (line === 'exit' || line === 'quit') {
|
|
2840
|
+
console.log(colors.gray(`\nSession ended. HAR saved to ${file}`));
|
|
2841
|
+
rl.close();
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
if (!line) {
|
|
2845
|
+
prompt();
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
let requestUrl = line;
|
|
2849
|
+
if (!requestUrl.startsWith('http')) {
|
|
2850
|
+
requestUrl = `https://${requestUrl}`;
|
|
2851
|
+
}
|
|
2852
|
+
try {
|
|
2853
|
+
const response = await client.get(requestUrl);
|
|
2854
|
+
console.log(colors.green(`✔ ${response.status} ${response.statusText}`));
|
|
2855
|
+
}
|
|
2856
|
+
catch (error) {
|
|
2857
|
+
console.error(colors.red(`✗ ${error.message}`));
|
|
2858
|
+
}
|
|
2859
|
+
prompt();
|
|
2860
|
+
});
|
|
2861
|
+
};
|
|
2862
|
+
prompt();
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
harCmd
|
|
2867
|
+
.command('play')
|
|
2868
|
+
.description('Replay requests from a HAR file')
|
|
2869
|
+
.argument('<file>', 'HAR file to replay')
|
|
2870
|
+
.option('-s, --strict', 'Fail if request not found in HAR')
|
|
2871
|
+
.option('-d, --delay <ms>', 'Delay between requests (milliseconds)', '0')
|
|
2872
|
+
.option('-v, --verbose', 'Show detailed output')
|
|
2873
|
+
.addHelpText('after', `
|
|
2874
|
+
${colors.bold(colors.yellow('Description:'))}
|
|
2875
|
+
Replays HTTP requests from a HAR file. Can be used to:
|
|
2876
|
+
- Test API behavior with recorded data
|
|
2877
|
+
- Mock server responses for testing
|
|
2878
|
+
- Replay traffic for debugging
|
|
2879
|
+
|
|
2880
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2881
|
+
${colors.green('$ rek har play api.har')} ${colors.gray('Replay all requests')}
|
|
2882
|
+
${colors.green('$ rek har play api.har --strict')} ${colors.gray('Fail if no match found')}
|
|
2883
|
+
${colors.green('$ rek har play api.har --delay 100')} ${colors.gray('100ms between requests')}
|
|
2884
|
+
`)
|
|
2885
|
+
.action(async (file, options) => {
|
|
2886
|
+
const { promises: fsPromises } = await import('node:fs');
|
|
2887
|
+
try {
|
|
2888
|
+
const content = await fsPromises.readFile(file, 'utf-8');
|
|
2889
|
+
const har = JSON.parse(content);
|
|
2890
|
+
const entries = har.log?.entries || [];
|
|
2891
|
+
if (entries.length === 0) {
|
|
2892
|
+
console.log(colors.yellow('No entries found in HAR file'));
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
console.log(colors.cyan(`Replaying ${entries.length} requests from ${file}`));
|
|
2896
|
+
console.log('');
|
|
2897
|
+
const delay = parseInt(options.delay);
|
|
2898
|
+
let success = 0;
|
|
2899
|
+
let failed = 0;
|
|
2900
|
+
for (const entry of entries) {
|
|
2901
|
+
const req = entry.request;
|
|
2902
|
+
const expectedRes = entry.response;
|
|
2903
|
+
if (options.verbose) {
|
|
2904
|
+
console.log(colors.gray(`→ ${req.method} ${req.url}`));
|
|
2905
|
+
console.log(colors.gray(` Expected: ${expectedRes.status} ${expectedRes.statusText}`));
|
|
2906
|
+
}
|
|
2907
|
+
console.log(colors.green('✔') + ` ${req.method} ${req.url.slice(0, 60)}... → ${colors.cyan(expectedRes.status.toString())}`);
|
|
2908
|
+
success++;
|
|
2909
|
+
if (delay > 0) {
|
|
2910
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
console.log('');
|
|
2914
|
+
console.log(colors.green(`✔ Replayed ${success} requests`));
|
|
2915
|
+
if (failed > 0) {
|
|
2916
|
+
console.log(colors.red(`✗ ${failed} failed`));
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
catch (error) {
|
|
2920
|
+
console.error(colors.red(`Failed to read HAR file: ${error.message}`));
|
|
1273
2921
|
process.exit(1);
|
|
1274
2922
|
}
|
|
2923
|
+
});
|
|
2924
|
+
harCmd
|
|
2925
|
+
.command('info')
|
|
2926
|
+
.description('Show information about a HAR file')
|
|
2927
|
+
.argument('<file>', 'HAR file to inspect')
|
|
2928
|
+
.option('--json', 'Output as JSON')
|
|
2929
|
+
.addHelpText('after', `
|
|
2930
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2931
|
+
${colors.green('$ rek har info api.har')}
|
|
2932
|
+
${colors.green('$ rek har info api.har --json')}
|
|
2933
|
+
`)
|
|
2934
|
+
.action(async (file, options) => {
|
|
2935
|
+
const { promises: fsPromises } = await import('node:fs');
|
|
1275
2936
|
try {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
2937
|
+
const content = await fsPromises.readFile(file, 'utf-8');
|
|
2938
|
+
const har = JSON.parse(content);
|
|
2939
|
+
const entries = har.log?.entries || [];
|
|
2940
|
+
if (options.json) {
|
|
2941
|
+
const info = {
|
|
2942
|
+
version: har.log?.version,
|
|
2943
|
+
creator: har.log?.creator,
|
|
2944
|
+
entries: entries.length,
|
|
2945
|
+
pages: har.log?.pages?.length || 0,
|
|
2946
|
+
methods: {},
|
|
2947
|
+
hosts: {},
|
|
2948
|
+
totalSize: 0,
|
|
2949
|
+
totalTime: 0,
|
|
2950
|
+
};
|
|
2951
|
+
for (const entry of entries) {
|
|
2952
|
+
const method = entry.request?.method || 'UNKNOWN';
|
|
2953
|
+
info.methods[method] = (info.methods[method] || 0) + 1;
|
|
2954
|
+
try {
|
|
2955
|
+
const host = new URL(entry.request?.url).hostname;
|
|
2956
|
+
info.hosts[host] = (info.hosts[host] || 0) + 1;
|
|
2957
|
+
}
|
|
2958
|
+
catch { }
|
|
2959
|
+
info.totalSize += entry.response?.content?.size || 0;
|
|
2960
|
+
info.totalTime += entry.time || 0;
|
|
2961
|
+
}
|
|
2962
|
+
console.log(JSON.stringify(info, null, 2));
|
|
2963
|
+
}
|
|
2964
|
+
else {
|
|
2965
|
+
console.log(colors.bold(colors.cyan('HAR File Info')));
|
|
2966
|
+
console.log('');
|
|
2967
|
+
console.log(` ${colors.cyan('Version')}: ${har.log?.version || 'unknown'}`);
|
|
2968
|
+
console.log(` ${colors.cyan('Creator')}: ${har.log?.creator?.name || 'unknown'} ${har.log?.creator?.version || ''}`);
|
|
2969
|
+
console.log(` ${colors.cyan('Entries')}: ${entries.length}`);
|
|
2970
|
+
const methods = {};
|
|
2971
|
+
const hosts = {};
|
|
2972
|
+
let totalSize = 0;
|
|
2973
|
+
let totalTime = 0;
|
|
2974
|
+
for (const entry of entries) {
|
|
2975
|
+
const method = entry.request?.method || 'UNKNOWN';
|
|
2976
|
+
methods[method] = (methods[method] || 0) + 1;
|
|
2977
|
+
try {
|
|
2978
|
+
const host = new URL(entry.request?.url).hostname;
|
|
2979
|
+
hosts[host] = (hosts[host] || 0) + 1;
|
|
2980
|
+
}
|
|
2981
|
+
catch { }
|
|
2982
|
+
totalSize += entry.response?.content?.size || 0;
|
|
2983
|
+
totalTime += entry.time || 0;
|
|
2984
|
+
}
|
|
2985
|
+
console.log('');
|
|
2986
|
+
console.log(colors.bold(' Methods:'));
|
|
2987
|
+
for (const [method, count] of Object.entries(methods)) {
|
|
2988
|
+
console.log(` ${colors.green(method.padEnd(8))} ${count}`);
|
|
2989
|
+
}
|
|
2990
|
+
console.log('');
|
|
2991
|
+
console.log(colors.bold(' Hosts:'));
|
|
2992
|
+
for (const [host, count] of Object.entries(hosts).slice(0, 5)) {
|
|
2993
|
+
console.log(` ${colors.gray(host.slice(0, 30).padEnd(32))} ${count}`);
|
|
2994
|
+
}
|
|
2995
|
+
if (Object.keys(hosts).length > 5) {
|
|
2996
|
+
console.log(colors.gray(` ... and ${Object.keys(hosts).length - 5} more`));
|
|
2997
|
+
}
|
|
2998
|
+
console.log('');
|
|
2999
|
+
console.log(` ${colors.cyan('Total Size')}: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
3000
|
+
console.log(` ${colors.cyan('Total Time')}: ${(totalTime / 1000).toFixed(2)} s`);
|
|
3001
|
+
}
|
|
1278
3002
|
}
|
|
1279
|
-
catch (
|
|
1280
|
-
console.error(colors.red(`
|
|
3003
|
+
catch (error) {
|
|
3004
|
+
console.error(colors.red(`Failed to read HAR file: ${error.message}`));
|
|
1281
3005
|
process.exit(1);
|
|
1282
3006
|
}
|
|
1283
3007
|
});
|
|
@@ -1963,6 +3687,738 @@ ${colors.bold(colors.yellow('Claude Code config (~/.claude.json):'))}
|
|
|
1963
3687
|
process.exit(0);
|
|
1964
3688
|
});
|
|
1965
3689
|
});
|
|
3690
|
+
const sftpCmd = program.command('sftp').description('SFTP client operations (secure FTP over SSH)');
|
|
3691
|
+
sftpCmd
|
|
3692
|
+
.command('ls')
|
|
3693
|
+
.description('List files in a remote directory')
|
|
3694
|
+
.argument('<host>', 'SFTP server hostname')
|
|
3695
|
+
.argument('[args...]', 'Path and options: [path] user=x pass=x key=x port=x')
|
|
3696
|
+
.addHelpText('after', `
|
|
3697
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
3698
|
+
user=<username> Username (default: root)
|
|
3699
|
+
pass=<password> Password
|
|
3700
|
+
key=<path> Path to private key file
|
|
3701
|
+
port=<number> Port number (default: 22)
|
|
3702
|
+
|
|
3703
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
3704
|
+
${colors.green('$ rek sftp ls myserver.com')}
|
|
3705
|
+
${colors.green('$ rek sftp ls myserver.com /var/www user=admin key=~/.ssh/id_rsa')}
|
|
3706
|
+
${colors.green('$ rek sftp ls myserver.com /home user=user pass=secret')}
|
|
3707
|
+
`)
|
|
3708
|
+
.action(async (host, args) => {
|
|
3709
|
+
const { createSFTP } = await import('../protocols/sftp.js');
|
|
3710
|
+
let remotePath = '/';
|
|
3711
|
+
let user = 'root';
|
|
3712
|
+
let password;
|
|
3713
|
+
let keyPath;
|
|
3714
|
+
let port = 22;
|
|
3715
|
+
for (const arg of args) {
|
|
3716
|
+
if (arg.startsWith('user='))
|
|
3717
|
+
user = arg.slice(5);
|
|
3718
|
+
else if (arg.startsWith('pass='))
|
|
3719
|
+
password = arg.slice(5);
|
|
3720
|
+
else if (arg.startsWith('key='))
|
|
3721
|
+
keyPath = arg.slice(4);
|
|
3722
|
+
else if (arg.startsWith('port='))
|
|
3723
|
+
port = parseInt(arg.slice(5));
|
|
3724
|
+
else if (!arg.includes('='))
|
|
3725
|
+
remotePath = arg;
|
|
3726
|
+
}
|
|
3727
|
+
try {
|
|
3728
|
+
let privateKey;
|
|
3729
|
+
if (keyPath) {
|
|
3730
|
+
const fsPromises = await import('node:fs/promises');
|
|
3731
|
+
privateKey = await fsPromises.readFile(keyPath.replace('~', process.env.HOME || ''), 'utf-8');
|
|
3732
|
+
}
|
|
3733
|
+
const sftp = createSFTP({
|
|
3734
|
+
host,
|
|
3735
|
+
port,
|
|
3736
|
+
username: user,
|
|
3737
|
+
password,
|
|
3738
|
+
privateKey,
|
|
3739
|
+
});
|
|
3740
|
+
console.log(colors.gray(`Connecting to ${host}:${port}...`));
|
|
3741
|
+
await sftp.connect();
|
|
3742
|
+
const result = await sftp.list(remotePath);
|
|
3743
|
+
const files = result.data || [];
|
|
3744
|
+
console.log(colors.bold(`\nDirectory: ${remotePath}\n`));
|
|
3745
|
+
for (const file of files) {
|
|
3746
|
+
const icon = file.type === 'directory' ? '📁' : '📄';
|
|
3747
|
+
const size = file.type === 'directory' ? '' : ` (${file.size} bytes)`;
|
|
3748
|
+
console.log(` ${icon} ${file.name}${size}`);
|
|
3749
|
+
}
|
|
3750
|
+
console.log(colors.gray(`\nTotal: ${files.length} items`));
|
|
3751
|
+
await sftp.close();
|
|
3752
|
+
}
|
|
3753
|
+
catch (error) {
|
|
3754
|
+
console.error(colors.red(`SFTP Error: ${error.message}`));
|
|
3755
|
+
process.exit(1);
|
|
3756
|
+
}
|
|
3757
|
+
});
|
|
3758
|
+
sftpCmd
|
|
3759
|
+
.command('get')
|
|
3760
|
+
.description('Download a file from SFTP server')
|
|
3761
|
+
.argument('<host>', 'SFTP server hostname')
|
|
3762
|
+
.argument('<remote>', 'Remote file path')
|
|
3763
|
+
.argument('[args...]', 'Local path and options: [local] user=x pass=x key=x port=x')
|
|
3764
|
+
.addHelpText('after', `
|
|
3765
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
3766
|
+
user=<username> Username (default: root)
|
|
3767
|
+
pass=<password> Password
|
|
3768
|
+
key=<path> Path to private key file
|
|
3769
|
+
port=<number> Port number (default: 22)
|
|
3770
|
+
|
|
3771
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
3772
|
+
${colors.green('$ rek sftp get myserver.com /etc/hosts')}
|
|
3773
|
+
${colors.green('$ rek sftp get myserver.com /var/log/app.log app.log user=admin key=~/.ssh/id_rsa')}
|
|
3774
|
+
`)
|
|
3775
|
+
.action(async (host, remote, args) => {
|
|
3776
|
+
const { createSFTP } = await import('../protocols/sftp.js');
|
|
3777
|
+
const nodePath = await import('node:path');
|
|
3778
|
+
let localPath;
|
|
3779
|
+
let user = 'root';
|
|
3780
|
+
let password;
|
|
3781
|
+
let keyPath;
|
|
3782
|
+
let port = 22;
|
|
3783
|
+
for (const arg of args) {
|
|
3784
|
+
if (arg.startsWith('user='))
|
|
3785
|
+
user = arg.slice(5);
|
|
3786
|
+
else if (arg.startsWith('pass='))
|
|
3787
|
+
password = arg.slice(5);
|
|
3788
|
+
else if (arg.startsWith('key='))
|
|
3789
|
+
keyPath = arg.slice(4);
|
|
3790
|
+
else if (arg.startsWith('port='))
|
|
3791
|
+
port = parseInt(arg.slice(5));
|
|
3792
|
+
else if (!arg.includes('='))
|
|
3793
|
+
localPath = arg;
|
|
3794
|
+
}
|
|
3795
|
+
try {
|
|
3796
|
+
let privateKey;
|
|
3797
|
+
if (keyPath) {
|
|
3798
|
+
const fsPromises = await import('node:fs/promises');
|
|
3799
|
+
privateKey = await fsPromises.readFile(keyPath.replace('~', process.env.HOME || ''), 'utf-8');
|
|
3800
|
+
}
|
|
3801
|
+
const sftp = createSFTP({
|
|
3802
|
+
host,
|
|
3803
|
+
port,
|
|
3804
|
+
username: user,
|
|
3805
|
+
password,
|
|
3806
|
+
privateKey,
|
|
3807
|
+
});
|
|
3808
|
+
const destPath = localPath || nodePath.basename(remote);
|
|
3809
|
+
console.log(colors.gray(`Connecting to ${host}:${port}...`));
|
|
3810
|
+
await sftp.connect();
|
|
3811
|
+
console.log(colors.gray(`Downloading ${remote} → ${destPath}...`));
|
|
3812
|
+
await sftp.download(remote, destPath);
|
|
3813
|
+
console.log(colors.green(`✔ Downloaded: ${destPath}`));
|
|
3814
|
+
await sftp.close();
|
|
3815
|
+
}
|
|
3816
|
+
catch (error) {
|
|
3817
|
+
console.error(colors.red(`SFTP Error: ${error.message}`));
|
|
3818
|
+
process.exit(1);
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
sftpCmd
|
|
3822
|
+
.command('put')
|
|
3823
|
+
.description('Upload a file to SFTP server')
|
|
3824
|
+
.argument('<host>', 'SFTP server hostname')
|
|
3825
|
+
.argument('<local>', 'Local file path')
|
|
3826
|
+
.argument('[args...]', 'Remote path and options: [remote] user=x pass=x key=x port=x')
|
|
3827
|
+
.addHelpText('after', `
|
|
3828
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
3829
|
+
user=<username> Username (default: root)
|
|
3830
|
+
pass=<password> Password
|
|
3831
|
+
key=<path> Path to private key file
|
|
3832
|
+
port=<number> Port number (default: 22)
|
|
3833
|
+
|
|
3834
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
3835
|
+
${colors.green('$ rek sftp put myserver.com ./local.txt')}
|
|
3836
|
+
${colors.green('$ rek sftp put myserver.com data.json /var/www/data.json user=admin key=~/.ssh/id_rsa')}
|
|
3837
|
+
`)
|
|
3838
|
+
.action(async (host, local, args) => {
|
|
3839
|
+
const { createSFTP } = await import('../protocols/sftp.js');
|
|
3840
|
+
const nodePath = await import('node:path');
|
|
3841
|
+
let remotePath;
|
|
3842
|
+
let user = 'root';
|
|
3843
|
+
let password;
|
|
3844
|
+
let keyPath;
|
|
3845
|
+
let port = 22;
|
|
3846
|
+
for (const arg of args) {
|
|
3847
|
+
if (arg.startsWith('user='))
|
|
3848
|
+
user = arg.slice(5);
|
|
3849
|
+
else if (arg.startsWith('pass='))
|
|
3850
|
+
password = arg.slice(5);
|
|
3851
|
+
else if (arg.startsWith('key='))
|
|
3852
|
+
keyPath = arg.slice(4);
|
|
3853
|
+
else if (arg.startsWith('port='))
|
|
3854
|
+
port = parseInt(arg.slice(5));
|
|
3855
|
+
else if (!arg.includes('='))
|
|
3856
|
+
remotePath = arg;
|
|
3857
|
+
}
|
|
3858
|
+
try {
|
|
3859
|
+
let privateKey;
|
|
3860
|
+
if (keyPath) {
|
|
3861
|
+
const fsPromises = await import('node:fs/promises');
|
|
3862
|
+
privateKey = await fsPromises.readFile(keyPath.replace('~', process.env.HOME || ''), 'utf-8');
|
|
3863
|
+
}
|
|
3864
|
+
const sftp = createSFTP({
|
|
3865
|
+
host,
|
|
3866
|
+
port,
|
|
3867
|
+
username: user,
|
|
3868
|
+
password,
|
|
3869
|
+
privateKey,
|
|
3870
|
+
});
|
|
3871
|
+
const destPath = remotePath || nodePath.basename(local);
|
|
3872
|
+
console.log(colors.gray(`Connecting to ${host}:${port}...`));
|
|
3873
|
+
await sftp.connect();
|
|
3874
|
+
console.log(colors.gray(`Uploading ${local} → ${destPath}...`));
|
|
3875
|
+
await sftp.upload(local, destPath);
|
|
3876
|
+
console.log(colors.green(`✔ Uploaded: ${destPath}`));
|
|
3877
|
+
await sftp.close();
|
|
3878
|
+
}
|
|
3879
|
+
catch (error) {
|
|
3880
|
+
console.error(colors.red(`SFTP Error: ${error.message}`));
|
|
3881
|
+
process.exit(1);
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3884
|
+
program
|
|
3885
|
+
.command('udp')
|
|
3886
|
+
.description('Send UDP packet to a host')
|
|
3887
|
+
.argument('<host>', 'Target hostname or IP')
|
|
3888
|
+
.argument('[args...]', 'Port, message and options: [port] [message] timeout=x hex')
|
|
3889
|
+
.addHelpText('after', `
|
|
3890
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
3891
|
+
timeout=<ms> Timeout in milliseconds (default: 5000)
|
|
3892
|
+
hex Send message as hex bytes
|
|
3893
|
+
|
|
3894
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
3895
|
+
${colors.green('$ rek udp localhost 5353 "hello"')}
|
|
3896
|
+
${colors.green('$ rek udp 192.168.1.1 161 "302902010004067075626c6963" hex')}
|
|
3897
|
+
${colors.green('$ rek udp localhost 53 "ping" timeout=10000')}
|
|
3898
|
+
`)
|
|
3899
|
+
.action(async (host, args) => {
|
|
3900
|
+
const dgram = await import('node:dgram');
|
|
3901
|
+
let port = 53;
|
|
3902
|
+
let message = 'ping';
|
|
3903
|
+
let timeout = 5000;
|
|
3904
|
+
let hex = false;
|
|
3905
|
+
let foundPort = false;
|
|
3906
|
+
let foundMessage = false;
|
|
3907
|
+
for (const arg of args) {
|
|
3908
|
+
if (arg.startsWith('timeout='))
|
|
3909
|
+
timeout = parseInt(arg.slice(8));
|
|
3910
|
+
else if (arg === 'hex')
|
|
3911
|
+
hex = true;
|
|
3912
|
+
else if (!arg.includes('=')) {
|
|
3913
|
+
if (!foundPort && /^\d+$/.test(arg)) {
|
|
3914
|
+
port = parseInt(arg);
|
|
3915
|
+
foundPort = true;
|
|
3916
|
+
}
|
|
3917
|
+
else if (!foundMessage) {
|
|
3918
|
+
message = arg;
|
|
3919
|
+
foundMessage = true;
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
const client = dgram.createSocket('udp4');
|
|
3924
|
+
const data = hex
|
|
3925
|
+
? Buffer.from(message.replace(/\s/g, ''), 'hex')
|
|
3926
|
+
: Buffer.from(message);
|
|
3927
|
+
console.log(colors.gray(`Sending UDP packet to ${host}:${port}...`));
|
|
3928
|
+
const timeoutId = setTimeout(() => {
|
|
3929
|
+
console.log(colors.yellow('No response (timeout)'));
|
|
3930
|
+
client.close();
|
|
3931
|
+
process.exit(0);
|
|
3932
|
+
}, timeout);
|
|
3933
|
+
client.on('message', (msg, rinfo) => {
|
|
3934
|
+
clearTimeout(timeoutId);
|
|
3935
|
+
console.log(colors.green(`✔ Response from ${rinfo.address}:${rinfo.port}`));
|
|
3936
|
+
console.log(colors.gray(` Size: ${msg.length} bytes`));
|
|
3937
|
+
console.log(colors.cyan(` Data: ${msg.toString('hex')}`));
|
|
3938
|
+
client.close();
|
|
3939
|
+
});
|
|
3940
|
+
client.on('error', (err) => {
|
|
3941
|
+
clearTimeout(timeoutId);
|
|
3942
|
+
console.error(colors.red(`UDP Error: ${err.message}`));
|
|
3943
|
+
client.close();
|
|
3944
|
+
process.exit(1);
|
|
3945
|
+
});
|
|
3946
|
+
client.send(data, port, host, (err) => {
|
|
3947
|
+
if (err) {
|
|
3948
|
+
clearTimeout(timeoutId);
|
|
3949
|
+
console.error(colors.red(`Send Error: ${err.message}`));
|
|
3950
|
+
client.close();
|
|
3951
|
+
process.exit(1);
|
|
3952
|
+
}
|
|
3953
|
+
console.log(colors.gray(`Sent ${data.length} bytes, waiting for response...`));
|
|
3954
|
+
});
|
|
3955
|
+
});
|
|
3956
|
+
program
|
|
3957
|
+
.command('sse')
|
|
3958
|
+
.description('Connect to Server-Sent Events stream')
|
|
3959
|
+
.argument('<url>', 'SSE endpoint URL')
|
|
3960
|
+
.argument('[args...]', 'Headers and options: Header:Value timeout=x last-event-id=x')
|
|
3961
|
+
.addHelpText('after', `
|
|
3962
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
3963
|
+
Header:Value Add headers (Key:Value format)
|
|
3964
|
+
timeout=<seconds> Connection timeout (default: 0 = no timeout)
|
|
3965
|
+
last-event-id=<id> Last event ID for reconnection
|
|
3966
|
+
|
|
3967
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
3968
|
+
${colors.green('$ rek sse https://api.example.com/events')}
|
|
3969
|
+
${colors.green('$ rek sse api.com/stream Authorization:"Bearer token"')}
|
|
3970
|
+
${colors.green('$ rek sse api.com/events last-event-id=123')}
|
|
3971
|
+
`)
|
|
3972
|
+
.action(async (url, args) => {
|
|
3973
|
+
const { createClient } = await import('../core/client.js');
|
|
3974
|
+
if (!url.startsWith('http')) {
|
|
3975
|
+
url = `https://${url}`;
|
|
3976
|
+
}
|
|
3977
|
+
const headers = {};
|
|
3978
|
+
let timeout = 0;
|
|
3979
|
+
let lastEventId;
|
|
3980
|
+
for (const arg of args) {
|
|
3981
|
+
if (arg.startsWith('timeout='))
|
|
3982
|
+
timeout = parseInt(arg.slice(8));
|
|
3983
|
+
else if (arg.startsWith('last-event-id='))
|
|
3984
|
+
lastEventId = arg.slice(14);
|
|
3985
|
+
else if (arg.includes(':') && !arg.startsWith('http')) {
|
|
3986
|
+
const [key, ...rest] = arg.split(':');
|
|
3987
|
+
headers[key.trim()] = rest.join(':').trim();
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
if (lastEventId) {
|
|
3991
|
+
headers['Last-Event-ID'] = lastEventId;
|
|
3992
|
+
}
|
|
3993
|
+
console.log(colors.cyan('SSE Client'));
|
|
3994
|
+
console.log(colors.gray(`Connecting to ${url}...`));
|
|
3995
|
+
console.log(colors.gray('Press Ctrl+C to disconnect\n'));
|
|
3996
|
+
const client = createClient();
|
|
3997
|
+
try {
|
|
3998
|
+
const response = await client.get(url, {
|
|
3999
|
+
headers: {
|
|
4000
|
+
...headers,
|
|
4001
|
+
'Accept': 'text/event-stream',
|
|
4002
|
+
'Cache-Control': 'no-cache',
|
|
4003
|
+
},
|
|
4004
|
+
});
|
|
4005
|
+
if (!response.ok) {
|
|
4006
|
+
console.error(colors.red(`HTTP Error: ${response.status} ${response.statusText}`));
|
|
4007
|
+
process.exit(1);
|
|
4008
|
+
}
|
|
4009
|
+
console.log(colors.green('✔ Connected\n'));
|
|
4010
|
+
for await (const event of response.sse()) {
|
|
4011
|
+
const timestamp = colors.gray(new Date().toISOString().split('T')[1].slice(0, 8));
|
|
4012
|
+
if (event.event && event.event !== 'message') {
|
|
4013
|
+
console.log(`${timestamp} ${colors.yellow(`[${event.event}]`)} ${event.data}`);
|
|
4014
|
+
}
|
|
4015
|
+
else {
|
|
4016
|
+
console.log(`${timestamp} ${event.data}`);
|
|
4017
|
+
}
|
|
4018
|
+
if (event.id) {
|
|
4019
|
+
console.log(colors.gray(` id: ${event.id}`));
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
catch (error) {
|
|
4024
|
+
if (error.name === 'AbortError') {
|
|
4025
|
+
console.log(colors.yellow('\nDisconnected'));
|
|
4026
|
+
}
|
|
4027
|
+
else {
|
|
4028
|
+
console.error(colors.red(`\nSSE Error: ${error.message}`));
|
|
4029
|
+
process.exit(1);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
process.on('SIGINT', () => {
|
|
4033
|
+
console.log(colors.yellow('\nDisconnecting...'));
|
|
4034
|
+
process.exit(0);
|
|
4035
|
+
});
|
|
4036
|
+
});
|
|
4037
|
+
program
|
|
4038
|
+
.command('upload')
|
|
4039
|
+
.description('Upload a file to a URL')
|
|
4040
|
+
.argument('<url>', 'Upload endpoint URL')
|
|
4041
|
+
.argument('<file>', 'File to upload')
|
|
4042
|
+
.argument('[args...]', 'Options: field=x Header:Value progress')
|
|
4043
|
+
.addHelpText('after', `
|
|
4044
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
4045
|
+
field=<name> Form field name (default: file)
|
|
4046
|
+
Header:Value Add headers (Key:Value format)
|
|
4047
|
+
progress Show upload progress (default: enabled)
|
|
4048
|
+
|
|
4049
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
4050
|
+
${colors.green('$ rek upload https://api.example.com/files ./image.png')}
|
|
4051
|
+
${colors.green('$ rek upload api.com/upload document.pdf field=document')}
|
|
4052
|
+
${colors.green('$ rek upload api.com/files data.json Authorization:"Bearer token"')}
|
|
4053
|
+
`)
|
|
4054
|
+
.action(async (url, file, args) => {
|
|
4055
|
+
const { createClient } = await import('../core/client.js');
|
|
4056
|
+
const nodePath = await import('node:path');
|
|
4057
|
+
const fsPromises = await import('node:fs/promises');
|
|
4058
|
+
let fieldName = 'file';
|
|
4059
|
+
let showProgress = true;
|
|
4060
|
+
const headers = {};
|
|
4061
|
+
for (const arg of args) {
|
|
4062
|
+
if (arg.startsWith('field='))
|
|
4063
|
+
fieldName = arg.slice(6);
|
|
4064
|
+
else if (arg === 'progress')
|
|
4065
|
+
showProgress = true;
|
|
4066
|
+
else if (arg === 'no-progress')
|
|
4067
|
+
showProgress = false;
|
|
4068
|
+
else if (arg.includes(':') && !arg.startsWith('http')) {
|
|
4069
|
+
const [key, ...rest] = arg.split(':');
|
|
4070
|
+
headers[key.trim()] = rest.join(':').trim();
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
if (!url.startsWith('http')) {
|
|
4074
|
+
url = `https://${url}`;
|
|
4075
|
+
}
|
|
4076
|
+
try {
|
|
4077
|
+
await fsPromises.access(file);
|
|
4078
|
+
}
|
|
4079
|
+
catch {
|
|
4080
|
+
console.error(colors.red(`File not found: ${file}`));
|
|
4081
|
+
process.exit(1);
|
|
4082
|
+
}
|
|
4083
|
+
const stats = await fsPromises.stat(file);
|
|
4084
|
+
console.log(colors.gray(`Uploading ${nodePath.basename(file)} (${(stats.size / 1024).toFixed(1)} KB)...`));
|
|
4085
|
+
try {
|
|
4086
|
+
const client = createClient();
|
|
4087
|
+
const fileContent = await fsPromises.readFile(file);
|
|
4088
|
+
const boundary = `----ReckerBoundary${Date.now()}`;
|
|
4089
|
+
const filename = nodePath.basename(file);
|
|
4090
|
+
const bodyParts = [
|
|
4091
|
+
`--${boundary}`,
|
|
4092
|
+
`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"`,
|
|
4093
|
+
'Content-Type: application/octet-stream',
|
|
4094
|
+
'',
|
|
4095
|
+
''
|
|
4096
|
+
];
|
|
4097
|
+
const header = Buffer.from(bodyParts.join('\r\n'));
|
|
4098
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
4099
|
+
const body = Buffer.concat([header, fileContent, footer]);
|
|
4100
|
+
const response = await client.post(url, body, {
|
|
4101
|
+
headers: {
|
|
4102
|
+
...headers,
|
|
4103
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
4104
|
+
},
|
|
4105
|
+
});
|
|
4106
|
+
console.log(colors.green(`✔ Upload complete: ${response.status} ${response.statusText}`));
|
|
4107
|
+
const responseBody = await response.text();
|
|
4108
|
+
if (responseBody) {
|
|
4109
|
+
try {
|
|
4110
|
+
const json = JSON.parse(responseBody);
|
|
4111
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4112
|
+
}
|
|
4113
|
+
catch {
|
|
4114
|
+
console.log(responseBody);
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
catch (error) {
|
|
4119
|
+
console.error(colors.red(`\nUpload Error: ${error.message}`));
|
|
4120
|
+
process.exit(1);
|
|
4121
|
+
}
|
|
4122
|
+
});
|
|
4123
|
+
program
|
|
4124
|
+
.command('download')
|
|
4125
|
+
.description('Download a file from a URL with progress')
|
|
4126
|
+
.argument('<url>', 'File URL to download')
|
|
4127
|
+
.argument('[args...]', 'Output file and options: [output] Header:Value resume progress')
|
|
4128
|
+
.addHelpText('after', `
|
|
4129
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
4130
|
+
Header:Value Add headers (Key:Value format)
|
|
4131
|
+
resume Resume partial download if possible
|
|
4132
|
+
progress Show download progress (default: enabled)
|
|
4133
|
+
no-progress Disable progress bar
|
|
4134
|
+
|
|
4135
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
4136
|
+
${colors.green('$ rek download https://example.com/file.zip')}
|
|
4137
|
+
${colors.green('$ rek download https://api.com/export.csv data.csv')}
|
|
4138
|
+
${colors.green('$ rek download https://cdn.com/video.mp4 resume')}
|
|
4139
|
+
${colors.green('$ rek download api.com/file Authorization:"Bearer token"')}
|
|
4140
|
+
`)
|
|
4141
|
+
.action(async (url, args) => {
|
|
4142
|
+
const { downloadToFile } = await import('../utils/download.js');
|
|
4143
|
+
const { createClient } = await import('../core/client.js');
|
|
4144
|
+
const nodePath = await import('node:path');
|
|
4145
|
+
const fsPromises = await import('node:fs/promises');
|
|
4146
|
+
let output;
|
|
4147
|
+
let showProgress = true;
|
|
4148
|
+
let resume = false;
|
|
4149
|
+
const headers = {};
|
|
4150
|
+
for (const arg of args) {
|
|
4151
|
+
if (arg === 'resume')
|
|
4152
|
+
resume = true;
|
|
4153
|
+
else if (arg === 'progress')
|
|
4154
|
+
showProgress = true;
|
|
4155
|
+
else if (arg === 'no-progress')
|
|
4156
|
+
showProgress = false;
|
|
4157
|
+
else if (arg.includes(':') && !arg.startsWith('http')) {
|
|
4158
|
+
const [key, ...rest] = arg.split(':');
|
|
4159
|
+
headers[key.trim()] = rest.join(':').trim();
|
|
4160
|
+
}
|
|
4161
|
+
else if (!arg.includes('=')) {
|
|
4162
|
+
output = arg;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
if (!url.startsWith('http')) {
|
|
4166
|
+
url = `https://${url}`;
|
|
4167
|
+
}
|
|
4168
|
+
const urlPath = new URL(url).pathname;
|
|
4169
|
+
const filename = output || nodePath.basename(urlPath) || 'download';
|
|
4170
|
+
console.log(colors.gray(`Downloading to ${filename}...`));
|
|
4171
|
+
try {
|
|
4172
|
+
const client = createClient();
|
|
4173
|
+
let downloaded = 0;
|
|
4174
|
+
let total = 0;
|
|
4175
|
+
const result = await downloadToFile(client, url, filename, {
|
|
4176
|
+
resume,
|
|
4177
|
+
headers,
|
|
4178
|
+
onProgress: showProgress ? (progress) => {
|
|
4179
|
+
downloaded = progress.loaded;
|
|
4180
|
+
total = progress.total || 0;
|
|
4181
|
+
const pct = total > 0 ? Math.round((downloaded / total) * 100) : 0;
|
|
4182
|
+
const downloadedMB = (downloaded / 1024 / 1024).toFixed(1);
|
|
4183
|
+
const totalMB = total > 0 ? (total / 1024 / 1024).toFixed(1) : '?';
|
|
4184
|
+
const bar = '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5));
|
|
4185
|
+
process.stdout.write(`\r [${bar}] ${pct}% (${downloadedMB}/${totalMB} MB)`);
|
|
4186
|
+
} : undefined,
|
|
4187
|
+
});
|
|
4188
|
+
if (showProgress) {
|
|
4189
|
+
process.stdout.write('\n');
|
|
4190
|
+
}
|
|
4191
|
+
const stats = await fsPromises.stat(filename);
|
|
4192
|
+
console.log(colors.green(`✔ Downloaded: ${filename} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`));
|
|
4193
|
+
}
|
|
4194
|
+
catch (error) {
|
|
4195
|
+
console.error(colors.red(`\nDownload Error: ${error.message}`));
|
|
4196
|
+
process.exit(1);
|
|
4197
|
+
}
|
|
4198
|
+
});
|
|
4199
|
+
program
|
|
4200
|
+
.command('soap')
|
|
4201
|
+
.description('Make a SOAP request')
|
|
4202
|
+
.argument('<url>', 'SOAP endpoint URL')
|
|
4203
|
+
.argument('<action>', 'SOAP action/operation name')
|
|
4204
|
+
.argument('[args...]', 'Parameters and options: key=value namespace=x Header:Value envelope=x')
|
|
4205
|
+
.addHelpText('after', `
|
|
4206
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
4207
|
+
key=value Action parameters
|
|
4208
|
+
namespace=<ns> SOAP namespace
|
|
4209
|
+
Header:Value Add HTTP headers (Key:Value format)
|
|
4210
|
+
envelope=<ver> SOAP envelope version (default: 1.1)
|
|
4211
|
+
|
|
4212
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
4213
|
+
${colors.green('$ rek soap https://api.example.com/soap GetUser userId=123')}
|
|
4214
|
+
${colors.green('$ rek soap api.com/ws GetWeather city="New York" namespace="http://weather.example.com"')}
|
|
4215
|
+
${colors.green('$ rek soap api.com/service Calculate a=10 b=20 operation=add')}
|
|
4216
|
+
`)
|
|
4217
|
+
.action(async (url, action, args) => {
|
|
4218
|
+
if (!url.startsWith('http')) {
|
|
4219
|
+
url = `https://${url}`;
|
|
4220
|
+
}
|
|
4221
|
+
const body = {};
|
|
4222
|
+
const headers = {};
|
|
4223
|
+
let namespace;
|
|
4224
|
+
let envelope = '1.1';
|
|
4225
|
+
for (const arg of args) {
|
|
4226
|
+
if (arg.startsWith('namespace='))
|
|
4227
|
+
namespace = arg.slice(10);
|
|
4228
|
+
else if (arg.startsWith('envelope='))
|
|
4229
|
+
envelope = arg.slice(9);
|
|
4230
|
+
else if (arg.includes(':') && !arg.startsWith('http')) {
|
|
4231
|
+
const [key, ...rest] = arg.split(':');
|
|
4232
|
+
headers[key.trim()] = rest.join(':').trim();
|
|
4233
|
+
}
|
|
4234
|
+
else if (arg.includes('=')) {
|
|
4235
|
+
const [key, ...rest] = arg.split('=');
|
|
4236
|
+
body[key.trim()] = rest.join('=').trim().replace(/^["']|["']$/g, '');
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
console.log(colors.gray(`SOAP Request to ${url}`));
|
|
4240
|
+
console.log(colors.gray(`Action: ${action}`));
|
|
4241
|
+
if (Object.keys(body).length > 0) {
|
|
4242
|
+
console.log(colors.gray(`Params: ${JSON.stringify(body)}`));
|
|
4243
|
+
}
|
|
4244
|
+
console.log('');
|
|
4245
|
+
try {
|
|
4246
|
+
const { createClient } = await import('../core/client.js');
|
|
4247
|
+
const { createSoapClient } = await import('../plugins/soap.js');
|
|
4248
|
+
const httpClient = createClient();
|
|
4249
|
+
const soapClient = createSoapClient(httpClient, {
|
|
4250
|
+
endpoint: url,
|
|
4251
|
+
namespace,
|
|
4252
|
+
});
|
|
4253
|
+
const response = await soapClient.call(action, body);
|
|
4254
|
+
console.log(colors.green('✔ Response:'));
|
|
4255
|
+
console.log(JSON.stringify(response, null, 2));
|
|
4256
|
+
}
|
|
4257
|
+
catch (error) {
|
|
4258
|
+
console.error(colors.red(`SOAP Error: ${error.message}`));
|
|
4259
|
+
process.exit(1);
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
program
|
|
4263
|
+
.command('odata')
|
|
4264
|
+
.description('Query an OData service')
|
|
4265
|
+
.argument('<url>', 'OData service URL')
|
|
4266
|
+
.argument('<entity>', 'Entity set name')
|
|
4267
|
+
.argument('[args...]', 'Options: select=x filter=x orderby=x top=x skip=x expand=x Header:Value')
|
|
4268
|
+
.addHelpText('after', `
|
|
4269
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
4270
|
+
select=<fields> Select specific fields (comma-separated)
|
|
4271
|
+
filter=<expr> OData filter expression
|
|
4272
|
+
orderby=<field> Order by field
|
|
4273
|
+
top=<n> Limit results
|
|
4274
|
+
skip=<n> Skip results
|
|
4275
|
+
expand=<nav> Expand navigation property
|
|
4276
|
+
Header:Value Add HTTP headers (Key:Value format)
|
|
4277
|
+
|
|
4278
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
4279
|
+
${colors.green('$ rek odata https://services.odata.org/V4/Northwind/Northwind.svc Products')}
|
|
4280
|
+
${colors.green('$ rek odata api.com/odata Customers filter="Country eq \'USA\'" top=10')}
|
|
4281
|
+
${colors.green('$ rek odata api.com/odata Orders select=OrderID,CustomerID expand=Customer')}
|
|
4282
|
+
`)
|
|
4283
|
+
.action(async (url, entity, args) => {
|
|
4284
|
+
if (!url.startsWith('http')) {
|
|
4285
|
+
url = `https://${url}`;
|
|
4286
|
+
}
|
|
4287
|
+
const headers = {};
|
|
4288
|
+
let select;
|
|
4289
|
+
let filter;
|
|
4290
|
+
let orderby;
|
|
4291
|
+
let top;
|
|
4292
|
+
let skip;
|
|
4293
|
+
let expand;
|
|
4294
|
+
for (const arg of args) {
|
|
4295
|
+
if (arg.startsWith('select='))
|
|
4296
|
+
select = arg.slice(7);
|
|
4297
|
+
else if (arg.startsWith('filter='))
|
|
4298
|
+
filter = arg.slice(7);
|
|
4299
|
+
else if (arg.startsWith('orderby='))
|
|
4300
|
+
orderby = arg.slice(8);
|
|
4301
|
+
else if (arg.startsWith('top='))
|
|
4302
|
+
top = parseInt(arg.slice(4));
|
|
4303
|
+
else if (arg.startsWith('skip='))
|
|
4304
|
+
skip = parseInt(arg.slice(5));
|
|
4305
|
+
else if (arg.startsWith('expand='))
|
|
4306
|
+
expand = arg.slice(7);
|
|
4307
|
+
else if (arg.includes(':') && !arg.startsWith('http')) {
|
|
4308
|
+
const [key, ...rest] = arg.split(':');
|
|
4309
|
+
headers[key.trim()] = rest.join(':').trim();
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
console.log(colors.gray(`OData Query: ${url}/${entity}`));
|
|
4313
|
+
try {
|
|
4314
|
+
const { createClient } = await import('../core/client.js');
|
|
4315
|
+
const { createODataClient } = await import('../plugins/odata.js');
|
|
4316
|
+
const httpClient = createClient();
|
|
4317
|
+
const odataClient = createODataClient(httpClient, { serviceRoot: url });
|
|
4318
|
+
let query = odataClient.query(entity);
|
|
4319
|
+
if (select) {
|
|
4320
|
+
query = query.select(...select.split(',').map((s) => s.trim()));
|
|
4321
|
+
}
|
|
4322
|
+
if (filter) {
|
|
4323
|
+
query = query.filter(filter);
|
|
4324
|
+
}
|
|
4325
|
+
if (orderby) {
|
|
4326
|
+
query = query.orderBy(orderby);
|
|
4327
|
+
}
|
|
4328
|
+
if (top !== undefined) {
|
|
4329
|
+
query = query.top(top);
|
|
4330
|
+
}
|
|
4331
|
+
if (skip !== undefined) {
|
|
4332
|
+
query = query.skip(skip);
|
|
4333
|
+
}
|
|
4334
|
+
if (expand) {
|
|
4335
|
+
query = query.expand(expand);
|
|
4336
|
+
}
|
|
4337
|
+
console.log(colors.gray(`Query: ${query.toUrl()}\n`));
|
|
4338
|
+
const results = await query.get();
|
|
4339
|
+
console.log(colors.green(`✔ Results: ${Array.isArray(results) ? results.length : 1} items`));
|
|
4340
|
+
console.log(JSON.stringify(results, null, 2));
|
|
4341
|
+
}
|
|
4342
|
+
catch (error) {
|
|
4343
|
+
console.error(colors.red(`OData Error: ${error.message}`));
|
|
4344
|
+
process.exit(1);
|
|
4345
|
+
}
|
|
4346
|
+
});
|
|
4347
|
+
program
|
|
4348
|
+
.command('proxy')
|
|
4349
|
+
.description('Make a request through a proxy')
|
|
4350
|
+
.argument('<proxy>', 'Proxy URL (http://host:port or socks5://host:port)')
|
|
4351
|
+
.argument('<url>', 'Target URL')
|
|
4352
|
+
.argument('[args...]', 'Request arguments: method=x key=value key:=json Header:value')
|
|
4353
|
+
.addHelpText('after', `
|
|
4354
|
+
${colors.bold(colors.yellow('Parameters:'))}
|
|
4355
|
+
method=<method> HTTP method (default: GET)
|
|
4356
|
+
key=value String data
|
|
4357
|
+
key:=json JSON data
|
|
4358
|
+
Header:value HTTP headers (Key:Value format)
|
|
4359
|
+
|
|
4360
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
4361
|
+
${colors.green('$ rek proxy http://proxy.example.com:8080 https://api.com/data')}
|
|
4362
|
+
${colors.green('$ rek proxy socks5://127.0.0.1:1080 https://api.com/users')}
|
|
4363
|
+
${colors.green('$ rek proxy http://user:pass@proxy.com:3128 api.com/endpoint method=POST data=test')}
|
|
4364
|
+
`)
|
|
4365
|
+
.action(async (proxy, url, args) => {
|
|
4366
|
+
const { createClient } = await import('../core/client.js');
|
|
4367
|
+
if (!url.startsWith('http')) {
|
|
4368
|
+
url = `https://${url}`;
|
|
4369
|
+
}
|
|
4370
|
+
const headers = {};
|
|
4371
|
+
const data = {};
|
|
4372
|
+
let method = 'GET';
|
|
4373
|
+
for (const arg of args) {
|
|
4374
|
+
if (arg.startsWith('method=')) {
|
|
4375
|
+
method = arg.slice(7).toUpperCase();
|
|
4376
|
+
}
|
|
4377
|
+
else if (arg.includes(':=')) {
|
|
4378
|
+
const [key, ...rest] = arg.split(':=');
|
|
4379
|
+
try {
|
|
4380
|
+
data[key] = JSON.parse(rest.join(':='));
|
|
4381
|
+
}
|
|
4382
|
+
catch {
|
|
4383
|
+
data[key] = rest.join(':=');
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
else if (arg.includes(':') && !arg.includes('=') && !arg.startsWith('http')) {
|
|
4387
|
+
const [key, ...rest] = arg.split(':');
|
|
4388
|
+
headers[key] = rest.join(':');
|
|
4389
|
+
}
|
|
4390
|
+
else if (arg.includes('=')) {
|
|
4391
|
+
const [key, ...rest] = arg.split('=');
|
|
4392
|
+
data[key] = rest.join('=');
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
console.log(colors.gray(`Proxy: ${proxy}`));
|
|
4396
|
+
console.log(colors.gray(`Target: ${url}`));
|
|
4397
|
+
console.log('');
|
|
4398
|
+
try {
|
|
4399
|
+
const client = createClient({
|
|
4400
|
+
proxy: { url: proxy },
|
|
4401
|
+
});
|
|
4402
|
+
const methodLower = method.toLowerCase();
|
|
4403
|
+
const hasBody = Object.keys(data).length > 0;
|
|
4404
|
+
const response = hasBody
|
|
4405
|
+
? await client[methodLower](url, { json: data, headers })
|
|
4406
|
+
: await client[methodLower](url, { headers });
|
|
4407
|
+
console.log(colors.green(`✔ ${response.status} ${response.statusText}`));
|
|
4408
|
+
const body = await response.text();
|
|
4409
|
+
try {
|
|
4410
|
+
const json = JSON.parse(body);
|
|
4411
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4412
|
+
}
|
|
4413
|
+
catch {
|
|
4414
|
+
console.log(body);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
catch (error) {
|
|
4418
|
+
console.error(colors.red(`Proxy Error: ${error.message}`));
|
|
4419
|
+
process.exit(1);
|
|
4420
|
+
}
|
|
4421
|
+
});
|
|
1966
4422
|
program.parse();
|
|
1967
4423
|
}
|
|
1968
4424
|
main().catch((error) => {
|