recker 1.0.39 → 1.0.40
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 +1330 -361
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -128,6 +128,7 @@ async function main() {
|
|
|
128
128
|
.name('rek')
|
|
129
129
|
.description('The HTTP Client for Humans (and Robots)')
|
|
130
130
|
.version(version)
|
|
131
|
+
.showHelpAfterError(true)
|
|
131
132
|
.argument('[args...]', 'URL, Method, Headers (Key:Value), Data (key=value)')
|
|
132
133
|
.option('-v, --verbose', 'Show full request/response details')
|
|
133
134
|
.option('-q, --quiet', 'Output only response body (no colors, perfect for piping)')
|
|
@@ -276,7 +277,27 @@ Error: ${error.message}`));
|
|
|
276
277
|
});
|
|
277
278
|
program
|
|
278
279
|
.command('completion')
|
|
279
|
-
.description('Generate shell completion script')
|
|
280
|
+
.description('Generate shell auto-completion script')
|
|
281
|
+
.addHelpText('after', `
|
|
282
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
283
|
+
Generates a shell completion script for bash/zsh that enables tab-completion
|
|
284
|
+
for rek commands, options, HTTP methods, and API presets (@github, @openai, etc).
|
|
285
|
+
|
|
286
|
+
Once installed, pressing TAB will auto-complete commands and show suggestions,
|
|
287
|
+
making the CLI much faster to use.
|
|
288
|
+
|
|
289
|
+
${colors.bold(colors.yellow('Installation:'))}
|
|
290
|
+
${colors.cyan('# Bash (add to ~/.bashrc)')}
|
|
291
|
+
${colors.green('$ rek completion >> ~/.bashrc')}
|
|
292
|
+
${colors.green('$ source ~/.bashrc')}
|
|
293
|
+
|
|
294
|
+
${colors.cyan('# Zsh (add to ~/.zshrc)')}
|
|
295
|
+
${colors.green('$ rek completion >> ~/.zshrc')}
|
|
296
|
+
${colors.green('$ source ~/.zshrc')}
|
|
297
|
+
|
|
298
|
+
${colors.cyan('# One-time use (current session only)')}
|
|
299
|
+
${colors.green('$ source <(rek completion)')}
|
|
300
|
+
`)
|
|
280
301
|
.action(() => {
|
|
281
302
|
const script = `
|
|
282
303
|
###-begin-rek-completion-###
|
|
@@ -324,15 +345,32 @@ complete -F _rek_completions rek
|
|
|
324
345
|
program
|
|
325
346
|
.command('version')
|
|
326
347
|
.alias('info')
|
|
327
|
-
.description('Show
|
|
328
|
-
.
|
|
329
|
-
.
|
|
330
|
-
|
|
331
|
-
|
|
348
|
+
.description('Show version and environment information')
|
|
349
|
+
.argument('[args...]', 'Options: short format=json')
|
|
350
|
+
.addHelpText('after', `
|
|
351
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
352
|
+
Displays the installed Recker version along with your Node.js version,
|
|
353
|
+
operating system, and architecture. Useful for debugging, reporting issues,
|
|
354
|
+
or verifying which version is running in production environments.
|
|
355
|
+
|
|
356
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
357
|
+
${colors.green('$ rek version')} Show full version info
|
|
358
|
+
${colors.green('$ rek version short')} Just the version number (for scripts)
|
|
359
|
+
${colors.green('$ rek version format=json')} Machine-readable JSON output
|
|
360
|
+
${colors.green('$ rek info')} Alias for version
|
|
361
|
+
|
|
362
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
363
|
+
${colors.cyan('short')} Show only version number
|
|
364
|
+
${colors.cyan('format=json')} Output as JSON
|
|
365
|
+
`)
|
|
366
|
+
.action(async (args) => {
|
|
367
|
+
const isShort = args.includes('short');
|
|
368
|
+
const formatJson = args.some(a => a === 'format=json' || a === 'json');
|
|
369
|
+
if (isShort) {
|
|
332
370
|
console.log(version);
|
|
333
371
|
return;
|
|
334
372
|
}
|
|
335
|
-
if (
|
|
373
|
+
if (formatJson) {
|
|
336
374
|
const { getVersionInfo } = await import('../version.js');
|
|
337
375
|
const info = await getVersionInfo();
|
|
338
376
|
console.log(JSON.stringify(info, null, 2));
|
|
@@ -348,6 +386,27 @@ complete -F _rek_completions rek
|
|
|
348
386
|
.alias('repl')
|
|
349
387
|
.description('Start the interactive Rek Shell')
|
|
350
388
|
.option('-e, --env [path]', 'Load .env file (auto-loads from cwd by default)')
|
|
389
|
+
.addHelpText('after', `
|
|
390
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
391
|
+
Launches an interactive REPL (Read-Eval-Print Loop) for exploring APIs.
|
|
392
|
+
The shell provides auto-completion, command history, and a rich set of
|
|
393
|
+
built-in commands for HTTP requests, DNS lookups, WHOIS queries, and more.
|
|
394
|
+
|
|
395
|
+
Perfect for API exploration, debugging, and quick prototyping without
|
|
396
|
+
writing scripts. Environment variables from .env are loaded automatically.
|
|
397
|
+
|
|
398
|
+
${colors.bold(colors.yellow('Shell Commands:'))}
|
|
399
|
+
get/post/put/delete <url> Make HTTP requests
|
|
400
|
+
whois <domain> WHOIS lookup
|
|
401
|
+
dns <domain> DNS resolution
|
|
402
|
+
tls <domain> TLS certificate inspection
|
|
403
|
+
help Show all available commands
|
|
404
|
+
|
|
405
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
406
|
+
${colors.green('$ rek shell')} Start interactive shell
|
|
407
|
+
${colors.green('$ rek shell -e .env.local')} Load custom env file
|
|
408
|
+
${colors.green('$ rek repl')} Alias for shell
|
|
409
|
+
`)
|
|
351
410
|
.action(async (options) => {
|
|
352
411
|
if (options.env !== false) {
|
|
353
412
|
try {
|
|
@@ -365,7 +424,22 @@ complete -F _rek_completions rek
|
|
|
365
424
|
program
|
|
366
425
|
.command('docs [query...]')
|
|
367
426
|
.alias('?')
|
|
368
|
-
.description('Search Recker documentation
|
|
427
|
+
.description('Search Recker documentation')
|
|
428
|
+
.addHelpText('after', `
|
|
429
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
430
|
+
Opens a fullscreen interactive panel to search Recker's documentation.
|
|
431
|
+
Uses fuzzy search to find relevant docs about HTTP clients, plugins,
|
|
432
|
+
authentication, caching, and all other features.
|
|
433
|
+
|
|
434
|
+
The search is powered by semantic embeddings for accurate results.
|
|
435
|
+
Navigate with arrow keys, press Enter to view, Esc to close.
|
|
436
|
+
|
|
437
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
438
|
+
${colors.green('$ rek docs')} Open documentation browser
|
|
439
|
+
${colors.green('$ rek docs retry')} Search for retry-related docs
|
|
440
|
+
${colors.green('$ rek docs "rate limit"')} Search for rate limiting
|
|
441
|
+
${colors.green('$ rek ? oauth')} Quick search with ? alias
|
|
442
|
+
`)
|
|
369
443
|
.action(async (queryParts) => {
|
|
370
444
|
const query = queryParts.join(' ').trim();
|
|
371
445
|
const { openSearchPanel } = await import('./tui/search-panel.js');
|
|
@@ -374,8 +448,39 @@ complete -F _rek_completions rek
|
|
|
374
448
|
program
|
|
375
449
|
.command('security')
|
|
376
450
|
.alias('headers')
|
|
377
|
-
.
|
|
451
|
+
.alias('grade')
|
|
452
|
+
.description('Grade a website\'s security headers (A+ to F)')
|
|
378
453
|
.argument('<url>', 'URL to analyze')
|
|
454
|
+
.addHelpText('after', `
|
|
455
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
456
|
+
Fetches a URL and analyzes its HTTP response headers for security best
|
|
457
|
+
practices. Assigns a grade from A+ to F based on the presence and correct
|
|
458
|
+
configuration of security headers.
|
|
459
|
+
|
|
460
|
+
Checks for HSTS, CSP, X-Frame-Options, X-Content-Type-Options, and other
|
|
461
|
+
important security headers. Great for security audits, DevSecOps pipelines,
|
|
462
|
+
or verifying your site's security configuration.
|
|
463
|
+
|
|
464
|
+
${colors.bold(colors.yellow('Headers Analyzed:'))}
|
|
465
|
+
- Strict-Transport-Security (HSTS)
|
|
466
|
+
- Content-Security-Policy (CSP)
|
|
467
|
+
- X-Frame-Options (clickjacking protection)
|
|
468
|
+
- X-Content-Type-Options (MIME sniffing)
|
|
469
|
+
- Referrer-Policy
|
|
470
|
+
- Permissions-Policy
|
|
471
|
+
- X-XSS-Protection (legacy)
|
|
472
|
+
|
|
473
|
+
${colors.bold(colors.yellow('Grade Scale:'))}
|
|
474
|
+
${colors.green('A+/A/A-')} Excellent - all critical headers present
|
|
475
|
+
${colors.blue('B+/B/B-')} Good - most headers present
|
|
476
|
+
${colors.yellow('C+/C/C-')} Fair - some headers missing
|
|
477
|
+
${colors.red('D/F')} Poor - critical headers missing
|
|
478
|
+
|
|
479
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
480
|
+
${colors.green('$ rek security github.com')} Grade GitHub's headers
|
|
481
|
+
${colors.green('$ rek headers api.stripe.com')} Using headers alias
|
|
482
|
+
${colors.green('$ rek grade mysite.com')} Using grade alias
|
|
483
|
+
`)
|
|
379
484
|
.action(async (url) => {
|
|
380
485
|
if (!url.startsWith('http'))
|
|
381
486
|
url = `https://${url}`;
|
|
@@ -417,16 +522,30 @@ ${colors.bold('Details:')}`);
|
|
|
417
522
|
});
|
|
418
523
|
program
|
|
419
524
|
.command('seo')
|
|
420
|
-
.
|
|
525
|
+
.alias('audit')
|
|
526
|
+
.description('Analyze a page\'s SEO health (80+ checks)')
|
|
421
527
|
.argument('<url>', 'URL to analyze')
|
|
422
|
-
.
|
|
423
|
-
.option('--format <format>', 'Output format: text (default) or json', 'text')
|
|
528
|
+
.argument('[args...]', 'Options: all format=json')
|
|
424
529
|
.addHelpText('after', `
|
|
530
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
531
|
+
Performs a comprehensive SEO audit on a single page. Analyzes title, meta
|
|
532
|
+
description, headings hierarchy, images, links, structured data, OpenGraph
|
|
533
|
+
tags, Twitter cards, and technical SEO factors.
|
|
534
|
+
|
|
535
|
+
Returns a score with detailed recommendations. Use format=json for
|
|
536
|
+
integration with CI/CD pipelines or automated monitoring.
|
|
537
|
+
|
|
538
|
+
For full site audits, use ${colors.cyan('rek spider <url> seo')} instead.
|
|
539
|
+
|
|
425
540
|
${colors.bold(colors.yellow('Examples:'))}
|
|
426
541
|
${colors.green('$ rek seo example.com')} ${colors.gray('Basic SEO analysis')}
|
|
427
|
-
${colors.green('$ rek seo example.com
|
|
428
|
-
${colors.green('$ rek seo example.com
|
|
429
|
-
${colors.green('$ rek seo example.com
|
|
542
|
+
${colors.green('$ rek seo example.com all')} ${colors.gray('Show all checks')}
|
|
543
|
+
${colors.green('$ rek seo example.com format=json')} ${colors.gray('Output as JSON')}
|
|
544
|
+
${colors.green('$ rek seo example.com format=json | jq')} ${colors.gray('Pipe to jq for processing')}
|
|
545
|
+
|
|
546
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
547
|
+
${colors.cyan('all')} Show all checks including passed ones
|
|
548
|
+
${colors.cyan('format=json')} Output as JSON
|
|
430
549
|
|
|
431
550
|
${colors.bold(colors.yellow('Checks:'))}
|
|
432
551
|
${colors.cyan('Title Tag')} Length and presence
|
|
@@ -439,10 +558,11 @@ ${colors.bold(colors.yellow('Checks:'))}
|
|
|
439
558
|
${colors.cyan('Structured Data')} JSON-LD presence
|
|
440
559
|
${colors.cyan('Technical')} Canonical, viewport, lang
|
|
441
560
|
`)
|
|
442
|
-
.action(async (url,
|
|
561
|
+
.action(async (url, args) => {
|
|
443
562
|
if (!url.startsWith('http'))
|
|
444
563
|
url = `https://${url}`;
|
|
445
|
-
const
|
|
564
|
+
const showAll = args.includes('all');
|
|
565
|
+
const isJson = args.some(a => a === 'format=json' || a === 'json');
|
|
446
566
|
const { createClient } = await import('../core/client.js');
|
|
447
567
|
const { analyzeSeo } = await import('../seo/analyzer.js');
|
|
448
568
|
if (!isJson) {
|
|
@@ -519,11 +639,11 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
|
|
|
519
639
|
console.log(` ${colors.gray('Images:')} ${report.images.total} (${report.images.withAlt} with alt, ${report.images.withoutAlt} without)`);
|
|
520
640
|
console.log('');
|
|
521
641
|
console.log(`${colors.bold('Checks:')}`);
|
|
522
|
-
const checksToShow =
|
|
642
|
+
const checksToShow = showAll
|
|
523
643
|
? report.checks
|
|
524
644
|
: report.checks.filter(c => c.status !== 'pass' && c.status !== 'info');
|
|
525
|
-
if (checksToShow.length === 0 && !
|
|
526
|
-
console.log(colors.green(' All checks passed! Use
|
|
645
|
+
if (checksToShow.length === 0 && !showAll) {
|
|
646
|
+
console.log(colors.green(' All checks passed! Use "all" to see details.'));
|
|
527
647
|
}
|
|
528
648
|
else {
|
|
529
649
|
for (const check of checksToShow) {
|
|
@@ -559,12 +679,15 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
|
|
|
559
679
|
.command('robots')
|
|
560
680
|
.description('Validate and analyze robots.txt file')
|
|
561
681
|
.argument('<url>', 'Website URL or direct robots.txt URL')
|
|
562
|
-
.
|
|
682
|
+
.argument('[args...]', 'Options: format=json')
|
|
563
683
|
.addHelpText('after', `
|
|
564
684
|
${colors.bold(colors.yellow('Examples:'))}
|
|
565
685
|
${colors.green('$ rek robots example.com')} ${colors.gray('Validate robots.txt')}
|
|
566
686
|
${colors.green('$ rek robots example.com/robots.txt')} ${colors.gray('Direct URL')}
|
|
567
|
-
${colors.green('$ rek robots example.com
|
|
687
|
+
${colors.green('$ rek robots example.com format=json')} ${colors.gray('JSON output')}
|
|
688
|
+
|
|
689
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
690
|
+
${colors.cyan('format=json')} Output as JSON
|
|
568
691
|
|
|
569
692
|
${colors.bold(colors.yellow('Checks:'))}
|
|
570
693
|
${colors.cyan('Syntax')} Valid robots.txt syntax
|
|
@@ -573,14 +696,14 @@ ${colors.bold(colors.yellow('Checks:'))}
|
|
|
573
696
|
${colors.cyan('Crawl-delay')} Aggressive crawl delay
|
|
574
697
|
${colors.cyan('AI Bots')} GPTBot, ClaudeBot, Anthropic blocks
|
|
575
698
|
`)
|
|
576
|
-
.action(async (url,
|
|
699
|
+
.action(async (url, args) => {
|
|
577
700
|
if (!url.startsWith('http'))
|
|
578
701
|
url = `https://${url}`;
|
|
579
702
|
if (!url.includes('robots.txt')) {
|
|
580
703
|
const urlObj = new URL(url);
|
|
581
704
|
url = `${urlObj.origin}/robots.txt`;
|
|
582
705
|
}
|
|
583
|
-
const isJson =
|
|
706
|
+
const isJson = args.some(a => a === 'format=json' || a === 'json');
|
|
584
707
|
if (!isJson) {
|
|
585
708
|
console.log(colors.gray(`Fetching robots.txt from ${url}...`));
|
|
586
709
|
}
|
|
@@ -670,14 +793,17 @@ ${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')
|
|
|
670
793
|
.command('sitemap')
|
|
671
794
|
.description('Validate and analyze sitemap.xml file')
|
|
672
795
|
.argument('<url>', 'Website URL or direct sitemap URL')
|
|
673
|
-
.
|
|
674
|
-
.option('--discover', 'Discover all sitemaps via robots.txt')
|
|
796
|
+
.argument('[args...]', 'Options: discover format=json')
|
|
675
797
|
.addHelpText('after', `
|
|
676
798
|
${colors.bold(colors.yellow('Examples:'))}
|
|
677
799
|
${colors.green('$ rek sitemap example.com')} ${colors.gray('Validate sitemap')}
|
|
678
800
|
${colors.green('$ rek sitemap example.com/sitemap.xml')} ${colors.gray('Direct URL')}
|
|
679
|
-
${colors.green('$ rek sitemap example.com
|
|
680
|
-
${colors.green('$ rek sitemap example.com
|
|
801
|
+
${colors.green('$ rek sitemap example.com discover')} ${colors.gray('Find all sitemaps')}
|
|
802
|
+
${colors.green('$ rek sitemap example.com format=json')} ${colors.gray('JSON output')}
|
|
803
|
+
|
|
804
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
805
|
+
${colors.cyan('discover')} Discover all sitemaps via robots.txt
|
|
806
|
+
${colors.cyan('format=json')} Output as JSON
|
|
681
807
|
|
|
682
808
|
${colors.bold(colors.yellow('Checks:'))}
|
|
683
809
|
${colors.cyan('Structure')} Valid XML sitemap format
|
|
@@ -686,12 +812,13 @@ ${colors.bold(colors.yellow('Checks:'))}
|
|
|
686
812
|
${colors.cyan('URLs')} Valid, no duplicates, same domain
|
|
687
813
|
${colors.cyan('Lastmod')} Valid dates, not in future
|
|
688
814
|
`)
|
|
689
|
-
.action(async (url,
|
|
815
|
+
.action(async (url, args) => {
|
|
690
816
|
if (!url.startsWith('http'))
|
|
691
817
|
url = `https://${url}`;
|
|
692
|
-
const isJson =
|
|
818
|
+
const isJson = args.some(a => a === 'format=json' || a === 'json');
|
|
819
|
+
const doDiscover = args.includes('discover');
|
|
693
820
|
try {
|
|
694
|
-
if (
|
|
821
|
+
if (doDiscover) {
|
|
695
822
|
const { discoverSitemaps } = await import('../seo/validators/sitemap.js');
|
|
696
823
|
if (!isJson) {
|
|
697
824
|
console.log(colors.gray(`Discovering sitemaps for ${new URL(url).origin}...`));
|
|
@@ -801,14 +928,17 @@ ${colors.gray('Type:')} ${result.parseResult?.type === 'sitemapindex' ? 'Sitemap
|
|
|
801
928
|
.command('llms')
|
|
802
929
|
.description('Validate and analyze llms.txt file (AI/LLM optimization)')
|
|
803
930
|
.argument('[url]', 'Website URL or direct llms.txt URL')
|
|
804
|
-
.
|
|
805
|
-
.option('--template', 'Generate a template llms.txt file')
|
|
931
|
+
.argument('[args...]', 'Options: template format=json')
|
|
806
932
|
.addHelpText('after', `
|
|
807
933
|
${colors.bold(colors.yellow('Examples:'))}
|
|
808
934
|
${colors.green('$ rek llms example.com')} ${colors.gray('Validate llms.txt')}
|
|
809
935
|
${colors.green('$ rek llms example.com/llms.txt')} ${colors.gray('Direct URL')}
|
|
810
|
-
${colors.green('$ rek llms example.com
|
|
811
|
-
${colors.green('$ rek llms
|
|
936
|
+
${colors.green('$ rek llms example.com format=json')} ${colors.gray('JSON output')}
|
|
937
|
+
${colors.green('$ rek llms template > llms.txt')} ${colors.gray('Generate template')}
|
|
938
|
+
|
|
939
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
940
|
+
${colors.cyan('template')} Generate a template llms.txt file
|
|
941
|
+
${colors.cyan('format=json')} Output as JSON
|
|
812
942
|
|
|
813
943
|
${colors.bold(colors.yellow('About llms.txt:'))}
|
|
814
944
|
A proposed standard for providing LLM-friendly content.
|
|
@@ -821,9 +951,10 @@ ${colors.bold(colors.yellow('Checks:'))}
|
|
|
821
951
|
${colors.cyan('Description')} Site description block
|
|
822
952
|
${colors.cyan('Sections')} Content sections with links
|
|
823
953
|
`)
|
|
824
|
-
.action(async (url,
|
|
825
|
-
const isJson =
|
|
826
|
-
|
|
954
|
+
.action(async (url, args) => {
|
|
955
|
+
const isJson = args.some(a => a === 'format=json' || a === 'json');
|
|
956
|
+
const isTemplate = args.includes('template') || url === 'template';
|
|
957
|
+
if (isTemplate) {
|
|
827
958
|
const { generateLlmsTxtTemplate } = await import('../seo/validators/llms-txt.js');
|
|
828
959
|
const template = generateLlmsTxtTemplate({
|
|
829
960
|
siteName: 'Your Site Name',
|
|
@@ -945,10 +1076,20 @@ ${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')
|
|
|
945
1076
|
});
|
|
946
1077
|
program
|
|
947
1078
|
.command('spider')
|
|
948
|
-
.
|
|
1079
|
+
.alias('crawl')
|
|
1080
|
+
.description('Crawl a website and analyze all pages')
|
|
949
1081
|
.argument('<url>', 'Starting URL to crawl')
|
|
950
1082
|
.argument('[args...]', 'Options: depth=N limit=N concurrency=N seo focus=MODE output=file.json')
|
|
951
1083
|
.addHelpText('after', `
|
|
1084
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
1085
|
+
Crawls a website starting from the given URL, following internal links up to
|
|
1086
|
+
a specified depth. Discovers all pages, collects metadata, and optionally
|
|
1087
|
+
performs comprehensive SEO analysis.
|
|
1088
|
+
|
|
1089
|
+
The crawler respects robots.txt, handles JavaScript-rendered content, and
|
|
1090
|
+
provides detailed reports on site structure, broken links, and SEO issues.
|
|
1091
|
+
Perfect for site audits, migration planning, or competitive analysis.
|
|
1092
|
+
|
|
952
1093
|
${colors.bold(colors.yellow('Examples:'))}
|
|
953
1094
|
${colors.green('$ rek spider example.com')} ${colors.gray('Crawl with defaults')}
|
|
954
1095
|
${colors.green('$ rek spider example.com depth=3 limit=50')} ${colors.gray('Depth 3, max 50 pages')}
|
|
@@ -957,6 +1098,7 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
957
1098
|
${colors.green('$ rek spider example.com seo focus=links')} ${colors.gray('Focus on link issues')}
|
|
958
1099
|
${colors.green('$ rek spider example.com seo focus=security')} ${colors.gray('Focus on security issues')}
|
|
959
1100
|
${colors.green('$ rek spider example.com seo focus=duplicates')} ${colors.gray('Focus on duplicate content')}
|
|
1101
|
+
${colors.green('$ rek spider example.com seo format=json')} ${colors.gray('JSON output to stdout')}
|
|
960
1102
|
|
|
961
1103
|
${colors.bold(colors.yellow('Options:'))}
|
|
962
1104
|
${colors.cyan('depth=N')} Max link depth to follow (default: 5)
|
|
@@ -964,6 +1106,7 @@ ${colors.bold(colors.yellow('Options:'))}
|
|
|
964
1106
|
${colors.cyan('concurrency=N')} Parallel requests (default: 5)
|
|
965
1107
|
${colors.cyan('seo')} Enable SEO analysis mode
|
|
966
1108
|
${colors.cyan('focus=MODE')} Focus analysis on specific area (requires seo)
|
|
1109
|
+
${colors.cyan('format=json')} Output JSON to stdout (for piping)
|
|
967
1110
|
${colors.cyan('output=file.json')} Save JSON report to file
|
|
968
1111
|
|
|
969
1112
|
${colors.bold(colors.yellow('Focus Modes:'))}
|
|
@@ -980,6 +1123,7 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
|
|
|
980
1123
|
let concurrency = 5;
|
|
981
1124
|
let seoEnabled = false;
|
|
982
1125
|
let outputFile = '';
|
|
1126
|
+
let formatJson = false;
|
|
983
1127
|
let focusMode = 'all';
|
|
984
1128
|
const focusCategories = {
|
|
985
1129
|
links: ['links'],
|
|
@@ -1005,6 +1149,9 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
|
|
|
1005
1149
|
else if (arg.startsWith('output=')) {
|
|
1006
1150
|
outputFile = arg.split('=')[1] || '';
|
|
1007
1151
|
}
|
|
1152
|
+
else if (arg === 'format=json' || arg === '--format=json') {
|
|
1153
|
+
formatJson = true;
|
|
1154
|
+
}
|
|
1008
1155
|
else if (arg.startsWith('focus=')) {
|
|
1009
1156
|
const mode = arg.split('=')[1] || 'all';
|
|
1010
1157
|
if (mode in focusCategories) {
|
|
@@ -1019,14 +1166,16 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
|
|
|
1019
1166
|
}
|
|
1020
1167
|
if (!url.startsWith('http'))
|
|
1021
1168
|
url = `https://${url}`;
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1169
|
+
if (!formatJson) {
|
|
1170
|
+
const modeLabel = seoEnabled ? colors.magenta(' + SEO') : '';
|
|
1171
|
+
const focusLabel = focusMode !== 'all' ? colors.cyan(` [focus: ${focusMode}]`) : '';
|
|
1172
|
+
console.log(colors.cyan(`\nSpider starting: ${url}`));
|
|
1173
|
+
console.log(colors.gray(` Depth: ${maxDepth} | Limit: ${maxPages} | Concurrency: ${concurrency}${modeLabel}${focusLabel}`));
|
|
1174
|
+
if (outputFile) {
|
|
1175
|
+
console.log(colors.gray(` Output: ${outputFile}`));
|
|
1176
|
+
}
|
|
1177
|
+
console.log('');
|
|
1028
1178
|
}
|
|
1029
|
-
console.log('');
|
|
1030
1179
|
try {
|
|
1031
1180
|
if (seoEnabled) {
|
|
1032
1181
|
const { SeoSpider } = await import('../seo/index.js');
|
|
@@ -1040,11 +1189,91 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
|
|
|
1040
1189
|
output: outputFile || undefined,
|
|
1041
1190
|
focusCategories: focusCategories[focusMode],
|
|
1042
1191
|
focusMode,
|
|
1043
|
-
onProgress: (progress) => {
|
|
1192
|
+
onProgress: formatJson ? undefined : (progress) => {
|
|
1044
1193
|
process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
|
|
1045
1194
|
},
|
|
1046
1195
|
});
|
|
1047
1196
|
const result = await seoSpider.crawl(url);
|
|
1197
|
+
if (formatJson) {
|
|
1198
|
+
const responseTimes = result.pages.filter(p => p.duration > 0).map(p => p.duration);
|
|
1199
|
+
const avgResponseTime = responseTimes.length > 0
|
|
1200
|
+
? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
|
|
1201
|
+
: 0;
|
|
1202
|
+
const statusCounts = {};
|
|
1203
|
+
for (const page of result.pages) {
|
|
1204
|
+
const key = page.status?.toString() || 'error';
|
|
1205
|
+
statusCounts[key] = (statusCounts[key] || 0) + 1;
|
|
1206
|
+
}
|
|
1207
|
+
let totalInternalLinks = 0;
|
|
1208
|
+
let totalExternalLinks = 0;
|
|
1209
|
+
let totalImages = 0;
|
|
1210
|
+
let imagesWithoutAlt = 0;
|
|
1211
|
+
for (const page of result.pages) {
|
|
1212
|
+
if (page.seoReport) {
|
|
1213
|
+
totalInternalLinks += page.seoReport.links?.internal || 0;
|
|
1214
|
+
totalExternalLinks += page.seoReport.links?.external || 0;
|
|
1215
|
+
totalImages += page.seoReport.images?.total || 0;
|
|
1216
|
+
imagesWithoutAlt += page.seoReport.images?.withoutAlt || 0;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const jsonOutput = {
|
|
1220
|
+
startUrl: url,
|
|
1221
|
+
crawledAt: new Date().toISOString(),
|
|
1222
|
+
duration: result.duration,
|
|
1223
|
+
config: {
|
|
1224
|
+
maxDepth,
|
|
1225
|
+
maxPages,
|
|
1226
|
+
concurrency,
|
|
1227
|
+
focusMode,
|
|
1228
|
+
},
|
|
1229
|
+
summary: {
|
|
1230
|
+
totalPages: result.pages.length,
|
|
1231
|
+
uniqueUrls: result.visited.size,
|
|
1232
|
+
avgSeoScore: result.summary.avgScore,
|
|
1233
|
+
avgResponseTime,
|
|
1234
|
+
pagesWithErrors: result.summary.pagesWithErrors,
|
|
1235
|
+
pagesWithWarnings: result.summary.pagesWithWarnings,
|
|
1236
|
+
duplicateTitles: result.summary.duplicateTitles,
|
|
1237
|
+
duplicateDescriptions: result.summary.duplicateDescriptions,
|
|
1238
|
+
duplicateH1s: result.summary.duplicateH1s,
|
|
1239
|
+
orphanPages: result.summary.orphanPages,
|
|
1240
|
+
},
|
|
1241
|
+
content: {
|
|
1242
|
+
totalInternalLinks,
|
|
1243
|
+
totalExternalLinks,
|
|
1244
|
+
totalImages,
|
|
1245
|
+
imagesWithoutAlt,
|
|
1246
|
+
},
|
|
1247
|
+
httpStatus: statusCounts,
|
|
1248
|
+
siteWideIssues: result.siteWideIssues.map(issue => ({
|
|
1249
|
+
type: issue.type,
|
|
1250
|
+
severity: issue.severity,
|
|
1251
|
+
message: issue.message,
|
|
1252
|
+
value: issue.value,
|
|
1253
|
+
affectedUrls: issue.affectedUrls,
|
|
1254
|
+
})),
|
|
1255
|
+
pages: result.pages.map(page => ({
|
|
1256
|
+
url: page.url,
|
|
1257
|
+
status: page.status,
|
|
1258
|
+
depth: page.depth,
|
|
1259
|
+
duration: page.duration,
|
|
1260
|
+
title: page.title,
|
|
1261
|
+
error: page.error,
|
|
1262
|
+
seo: page.seoReport ? {
|
|
1263
|
+
score: page.seoReport.score,
|
|
1264
|
+
grade: page.seoReport.grade,
|
|
1265
|
+
title: page.seoReport.title,
|
|
1266
|
+
metaDescription: page.seoReport.metaDescription,
|
|
1267
|
+
headings: page.seoReport.headings,
|
|
1268
|
+
links: page.seoReport.links,
|
|
1269
|
+
images: page.seoReport.images,
|
|
1270
|
+
checks: page.seoReport.checks,
|
|
1271
|
+
} : null,
|
|
1272
|
+
})),
|
|
1273
|
+
};
|
|
1274
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1048
1277
|
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
1049
1278
|
console.log(colors.green(`\n✔ SEO Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
|
|
1050
1279
|
console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
|
|
@@ -1224,11 +1453,41 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
|
|
|
1224
1453
|
concurrency,
|
|
1225
1454
|
sameDomain: true,
|
|
1226
1455
|
delay: 100,
|
|
1227
|
-
onProgress: (progress) => {
|
|
1456
|
+
onProgress: formatJson ? undefined : (progress) => {
|
|
1228
1457
|
process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
|
|
1229
1458
|
},
|
|
1230
1459
|
});
|
|
1231
1460
|
const result = await spider.crawl(url);
|
|
1461
|
+
if (formatJson) {
|
|
1462
|
+
const jsonOutput = {
|
|
1463
|
+
startUrl: result.startUrl,
|
|
1464
|
+
crawledAt: new Date().toISOString(),
|
|
1465
|
+
duration: result.duration,
|
|
1466
|
+
config: {
|
|
1467
|
+
maxDepth,
|
|
1468
|
+
maxPages,
|
|
1469
|
+
concurrency,
|
|
1470
|
+
},
|
|
1471
|
+
summary: {
|
|
1472
|
+
totalPages: result.pages.length,
|
|
1473
|
+
successCount: result.pages.filter(p => !p.error).length,
|
|
1474
|
+
errorCount: result.errors.length,
|
|
1475
|
+
uniqueUrls: result.visited.size,
|
|
1476
|
+
},
|
|
1477
|
+
pages: result.pages.map(p => ({
|
|
1478
|
+
url: p.url,
|
|
1479
|
+
status: p.status,
|
|
1480
|
+
title: p.title,
|
|
1481
|
+
depth: p.depth,
|
|
1482
|
+
linksCount: p.links.length,
|
|
1483
|
+
duration: p.duration,
|
|
1484
|
+
error: p.error,
|
|
1485
|
+
})),
|
|
1486
|
+
errors: result.errors,
|
|
1487
|
+
};
|
|
1488
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1232
1491
|
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
1233
1492
|
console.log(colors.green(`\n✔ Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
|
|
1234
1493
|
console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
|
|
@@ -1293,10 +1552,19 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
|
|
|
1293
1552
|
});
|
|
1294
1553
|
program
|
|
1295
1554
|
.command('scrape')
|
|
1296
|
-
.
|
|
1555
|
+
.alias('extract')
|
|
1556
|
+
.description('Extract data from web pages with CSS selectors')
|
|
1297
1557
|
.argument('<url>', 'URL to scrape')
|
|
1298
1558
|
.argument('[args...]', 'Options: select=SELECTOR, attr=NAME, links, images, meta, tables, scripts, jsonld')
|
|
1299
1559
|
.addHelpText('after', `
|
|
1560
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
1561
|
+
Fetches a web page and extracts data using CSS selectors. Can extract
|
|
1562
|
+
text content, specific attributes, links, images, meta tags, tables,
|
|
1563
|
+
scripts, and JSON-LD structured data.
|
|
1564
|
+
|
|
1565
|
+
Perfect for quick data extraction, competitive research, price monitoring,
|
|
1566
|
+
or building datasets. Outputs clean, structured data ready for processing.
|
|
1567
|
+
|
|
1300
1568
|
${colors.bold(colors.yellow('Examples:'))}
|
|
1301
1569
|
${colors.green('$ rek scrape example.com')} ${colors.gray('# Basic page info')}
|
|
1302
1570
|
${colors.green('$ rek scrape example.com select="h1"')} ${colors.gray('# Extract h1 text')}
|
|
@@ -1494,20 +1762,32 @@ ${colors.bold('Images:')} ${imageCount}
|
|
|
1494
1762
|
});
|
|
1495
1763
|
program
|
|
1496
1764
|
.command('ai')
|
|
1497
|
-
.
|
|
1765
|
+
.alias('chat')
|
|
1766
|
+
.alias('ask')
|
|
1767
|
+
.description('Chat with AI models (OpenAI, Claude, Groq, etc)')
|
|
1498
1768
|
.argument('<preset>', 'AI preset to use (e.g., @openai, @anthropic, @groq)')
|
|
1499
|
-
.argument('<prompt...>', 'The prompt
|
|
1500
|
-
.option('-m, --model <model>', 'Override default model')
|
|
1501
|
-
.option('-t, --temperature <temp>', 'Temperature (0-1)', '0.7')
|
|
1502
|
-
.option('--max-tokens <tokens>', 'Max tokens in response', '2048')
|
|
1503
|
-
.option('-w, --wait', 'Wait for full response (disable streaming)')
|
|
1504
|
-
.option('-j, --json', 'Output raw JSON response')
|
|
1505
|
-
.option('-e, --env [path]', 'Load .env file (auto-loads from cwd if exists)')
|
|
1769
|
+
.argument('<prompt...>', 'The prompt and options: "prompt text" model=<model> temperature=<temp> max-tokens=<tokens> wait json env=<path>')
|
|
1506
1770
|
.addHelpText('after', `
|
|
1771
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
1772
|
+
Sends a prompt to an AI language model and streams the response back.
|
|
1773
|
+
Supports all major AI providers including OpenAI, Anthropic, Google, Groq,
|
|
1774
|
+
Mistral, and more. API keys are loaded from environment variables.
|
|
1775
|
+
|
|
1776
|
+
Each provider uses sensible defaults but you can override the model,
|
|
1777
|
+
temperature, and max tokens. Responses stream in real-time by default.
|
|
1778
|
+
|
|
1779
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
1780
|
+
${colors.cyan('model=<model>')} Override default model
|
|
1781
|
+
${colors.cyan('temperature=<temp>')} Temperature (0-1, default: 0.7)
|
|
1782
|
+
${colors.cyan('max-tokens=<tokens>')} Max tokens in response (default: 2048)
|
|
1783
|
+
${colors.cyan('wait')} Wait for full response (disable streaming)
|
|
1784
|
+
${colors.cyan('json')} Output raw JSON response
|
|
1785
|
+
${colors.cyan('env=<path>')} Load .env file (auto-loads from cwd if exists)
|
|
1786
|
+
|
|
1507
1787
|
${colors.bold(colors.yellow('Examples:'))}
|
|
1508
1788
|
${colors.green('$ rek ai @openai "What is the capital of France?"')}
|
|
1509
|
-
${colors.green('$ rek ai @anthropic "Explain quantum computing"
|
|
1510
|
-
${colors.green('$ rek ai @groq "Write a haiku"
|
|
1789
|
+
${colors.green('$ rek ai @anthropic "Explain quantum computing" model=claude-sonnet-4-20250514')}
|
|
1790
|
+
${colors.green('$ rek ai @groq "Write a haiku" wait')}
|
|
1511
1791
|
${colors.green('$ rek ai @openai "Translate to Spanish: Hello world"')}
|
|
1512
1792
|
|
|
1513
1793
|
${colors.bold(colors.yellow('Available AI Presets:'))}
|
|
@@ -1529,14 +1809,39 @@ ${colors.bold(colors.yellow('Note:'))}
|
|
|
1529
1809
|
This command sends a single prompt without conversation memory.
|
|
1530
1810
|
For chat with memory, use: ${colors.cyan('rek shell')} then ${colors.cyan('@openai Your message')}
|
|
1531
1811
|
`)
|
|
1532
|
-
.action(async (preset, promptParts
|
|
1533
|
-
|
|
1534
|
-
|
|
1812
|
+
.action(async (preset, promptParts) => {
|
|
1813
|
+
let model;
|
|
1814
|
+
let temperature = '0.7';
|
|
1815
|
+
let maxTokens = '2048';
|
|
1816
|
+
let wait = false;
|
|
1817
|
+
let jsonOutput = false;
|
|
1818
|
+
let envPath;
|
|
1819
|
+
const actualPromptParts = [];
|
|
1820
|
+
for (const part of promptParts) {
|
|
1821
|
+
if (part.startsWith('model='))
|
|
1822
|
+
model = part.split('=')[1];
|
|
1823
|
+
else if (part.startsWith('temperature='))
|
|
1824
|
+
temperature = part.split('=')[1];
|
|
1825
|
+
else if (part.startsWith('max-tokens='))
|
|
1826
|
+
maxTokens = part.split('=')[1];
|
|
1827
|
+
else if (part === 'wait')
|
|
1828
|
+
wait = true;
|
|
1829
|
+
else if (part === 'json')
|
|
1830
|
+
jsonOutput = true;
|
|
1831
|
+
else if (part.startsWith('env='))
|
|
1832
|
+
envPath = part.split('=')[1];
|
|
1833
|
+
else if (part === 'env')
|
|
1834
|
+
envPath = true;
|
|
1835
|
+
else
|
|
1836
|
+
actualPromptParts.push(part);
|
|
1837
|
+
}
|
|
1838
|
+
if (envPath !== undefined) {
|
|
1839
|
+
await loadEnvFile(envPath);
|
|
1535
1840
|
}
|
|
1536
1841
|
else {
|
|
1537
1842
|
try {
|
|
1538
|
-
const
|
|
1539
|
-
await fs.access(
|
|
1843
|
+
const envFilePath = join(process.cwd(), '.env');
|
|
1844
|
+
await fs.access(envFilePath);
|
|
1540
1845
|
await loadEnvFile(true);
|
|
1541
1846
|
}
|
|
1542
1847
|
catch {
|
|
@@ -1557,7 +1862,7 @@ ${colors.bold(colors.yellow('Note:'))}
|
|
|
1557
1862
|
console.log(colors.gray('Use an AI preset like @openai, @anthropic, @groq, etc.'));
|
|
1558
1863
|
process.exit(1);
|
|
1559
1864
|
}
|
|
1560
|
-
const prompt =
|
|
1865
|
+
const prompt = actualPromptParts.join(' ');
|
|
1561
1866
|
if (!prompt.trim()) {
|
|
1562
1867
|
console.error(colors.red('Error: Prompt is required'));
|
|
1563
1868
|
process.exit(1);
|
|
@@ -1565,16 +1870,16 @@ ${colors.bold(colors.yellow('Note:'))}
|
|
|
1565
1870
|
try {
|
|
1566
1871
|
const { createClient } = await import('../core/client.js');
|
|
1567
1872
|
const client = createClient(presetConfig);
|
|
1568
|
-
if (
|
|
1873
|
+
if (model) {
|
|
1569
1874
|
client.ai.setMemoryConfig({ systemPrompt: undefined });
|
|
1570
|
-
client._aiConfig.model =
|
|
1875
|
+
client._aiConfig.model = model;
|
|
1571
1876
|
}
|
|
1572
|
-
if (!
|
|
1877
|
+
if (!jsonOutput) {
|
|
1573
1878
|
console.log(colors.gray(`Using @${presetName} (${client._aiConfig.model})...\n`));
|
|
1574
1879
|
}
|
|
1575
|
-
if (
|
|
1880
|
+
if (wait || jsonOutput) {
|
|
1576
1881
|
const response = await client.ai.prompt(prompt);
|
|
1577
|
-
if (
|
|
1882
|
+
if (jsonOutput) {
|
|
1578
1883
|
console.log(JSON.stringify({
|
|
1579
1884
|
content: response.content,
|
|
1580
1885
|
model: response.model,
|
|
@@ -1610,8 +1915,36 @@ ${colors.bold(colors.yellow('Note:'))}
|
|
|
1610
1915
|
});
|
|
1611
1916
|
program
|
|
1612
1917
|
.command('ip')
|
|
1613
|
-
.
|
|
1918
|
+
.alias('geo')
|
|
1919
|
+
.alias('geoip')
|
|
1920
|
+
.description('Look up geolocation and ISP info for an IP address')
|
|
1614
1921
|
.argument('<address>', 'IP address to lookup')
|
|
1922
|
+
.addHelpText('after', `
|
|
1923
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
1924
|
+
Looks up geographic location and network information for an IP address
|
|
1925
|
+
using the MaxMind GeoLite2 database (downloaded automatically on first use).
|
|
1926
|
+
|
|
1927
|
+
Shows city, region, country, coordinates, timezone, ISP/organization, and
|
|
1928
|
+
whether it's a bogon (private/reserved) address. Works offline after the
|
|
1929
|
+
initial database download.
|
|
1930
|
+
|
|
1931
|
+
${colors.bold(colors.yellow('Information Displayed:'))}
|
|
1932
|
+
- City, region, country
|
|
1933
|
+
- Geographic coordinates (lat/long)
|
|
1934
|
+
- Timezone
|
|
1935
|
+
- ISP/Organization name
|
|
1936
|
+
- ASN (Autonomous System Number)
|
|
1937
|
+
|
|
1938
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
1939
|
+
${colors.green('$ rek ip 8.8.8.8')} Google DNS
|
|
1940
|
+
${colors.green('$ rek geo 1.1.1.1')} Cloudflare DNS
|
|
1941
|
+
${colors.green('$ rek geoip 151.101.1.140')} GitHub's IP
|
|
1942
|
+
${colors.green('$ rek ip 192.168.1.1')} Shows "Bogon/Private IP"
|
|
1943
|
+
|
|
1944
|
+
${colors.bold(colors.yellow('Note:'))}
|
|
1945
|
+
The GeoLite2 database (~70MB) is downloaded on first use and cached
|
|
1946
|
+
in ~/.cache/recker/. Updates automatically when stale.
|
|
1947
|
+
`)
|
|
1615
1948
|
.action(async (address) => {
|
|
1616
1949
|
const { getIpInfo, isGeoIPAvailable } = await import('../mcp/ip-intel.js');
|
|
1617
1950
|
if (!isGeoIPAvailable()) {
|
|
@@ -1650,9 +1983,34 @@ ${colors.bold('Network:')}
|
|
|
1650
1983
|
program
|
|
1651
1984
|
.command('tls')
|
|
1652
1985
|
.alias('ssl')
|
|
1986
|
+
.alias('cert')
|
|
1653
1987
|
.description('Inspect TLS/SSL certificate of a host')
|
|
1654
1988
|
.argument('<host>', 'Hostname or IP address')
|
|
1655
1989
|
.argument('[port]', 'Port number (default: 443)', '443')
|
|
1990
|
+
.addHelpText('after', `
|
|
1991
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
1992
|
+
Connects to a server and inspects its TLS/SSL certificate. Shows the
|
|
1993
|
+
certificate issuer, validity dates, days until expiration, subject
|
|
1994
|
+
alternative names (SANs), and whether the certificate is trusted.
|
|
1995
|
+
|
|
1996
|
+
Useful for debugging SSL issues, checking certificate expiration before
|
|
1997
|
+
it causes outages, or verifying a site's security configuration.
|
|
1998
|
+
|
|
1999
|
+
${colors.bold(colors.yellow('Information Displayed:'))}
|
|
2000
|
+
- Certificate validity status (valid/expired)
|
|
2001
|
+
- Trust status (CA-signed or self-signed)
|
|
2002
|
+
- Days remaining until expiration
|
|
2003
|
+
- Issuer (Certificate Authority)
|
|
2004
|
+
- Subject (domain name)
|
|
2005
|
+
- Serial number and fingerprints
|
|
2006
|
+
- Subject Alternative Names (SANs)
|
|
2007
|
+
|
|
2008
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2009
|
+
${colors.green('$ rek tls google.com')} Inspect Google's cert
|
|
2010
|
+
${colors.green('$ rek ssl api.stripe.com')} Using ssl alias
|
|
2011
|
+
${colors.green('$ rek cert example.com 8443')} Custom port
|
|
2012
|
+
${colors.green('$ rek tls 192.168.1.1 443')} Check IP directly
|
|
2013
|
+
`)
|
|
1656
2014
|
.action(async (host, port) => {
|
|
1657
2015
|
const { inspectTLS } = await import('../utils/tls-inspector.js');
|
|
1658
2016
|
console.log(colors.gray(`Inspecting TLS certificate for ${host}:${port}...`));
|
|
@@ -1720,15 +2078,43 @@ ${colors.bold('Fingerprints:')}
|
|
|
1720
2078
|
});
|
|
1721
2079
|
program
|
|
1722
2080
|
.command('whois')
|
|
1723
|
-
.description('
|
|
2081
|
+
.description('Look up domain registration and ownership info')
|
|
1724
2082
|
.argument('<query>', 'Domain name or IP address')
|
|
1725
|
-
.
|
|
1726
|
-
.
|
|
2083
|
+
.argument('[args...]', 'Options: raw')
|
|
2084
|
+
.addHelpText('after', `
|
|
2085
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
2086
|
+
Queries WHOIS servers to retrieve domain registration information.
|
|
2087
|
+
Shows registrar, creation/expiration dates, nameservers, and registrant
|
|
2088
|
+
contact information (when available, many use privacy protection).
|
|
2089
|
+
|
|
2090
|
+
Also works with IP addresses to find network ownership information.
|
|
2091
|
+
|
|
2092
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
2093
|
+
${colors.cyan('raw')} Show raw WHOIS response
|
|
2094
|
+
|
|
2095
|
+
${colors.bold(colors.yellow('Information Displayed:'))}
|
|
2096
|
+
- Domain status (active, expired, pending delete)
|
|
2097
|
+
- Registrar name
|
|
2098
|
+
- Creation/update/expiration dates
|
|
2099
|
+
- Name servers
|
|
2100
|
+
- Registrant, admin, tech contacts (if public)
|
|
2101
|
+
|
|
2102
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2103
|
+
${colors.green('$ rek whois github.com')} Domain registration info
|
|
2104
|
+
${colors.green('$ rek whois google.com raw')} Raw WHOIS response
|
|
2105
|
+
${colors.green('$ rek whois 8.8.8.8')} IP address ownership
|
|
2106
|
+
${colors.green('$ rek whois example.co.uk')} ccTLD domains work too
|
|
2107
|
+
|
|
2108
|
+
${colors.bold(colors.yellow('See also:'))}
|
|
2109
|
+
${colors.cyan('rek rdap <domain>')} RDAP (modern WHOIS replacement)
|
|
2110
|
+
`)
|
|
2111
|
+
.action(async (query, args) => {
|
|
1727
2112
|
const { whois } = await import('../utils/whois.js');
|
|
2113
|
+
const raw = args.includes('raw');
|
|
1728
2114
|
console.log(colors.gray(`Looking up WHOIS for ${query}...`));
|
|
1729
2115
|
try {
|
|
1730
2116
|
const result = await whois(query);
|
|
1731
|
-
if (
|
|
2117
|
+
if (raw) {
|
|
1732
2118
|
console.log(result.raw);
|
|
1733
2119
|
return;
|
|
1734
2120
|
}
|
|
@@ -1763,8 +2149,33 @@ ${colors.bold('Server:')} ${result.server}
|
|
|
1763
2149
|
});
|
|
1764
2150
|
program
|
|
1765
2151
|
.command('rdap')
|
|
1766
|
-
.description('RDAP lookup (modern WHOIS
|
|
2152
|
+
.description('RDAP lookup (modern WHOIS with JSON)')
|
|
1767
2153
|
.argument('<domain>', 'Domain name to lookup')
|
|
2154
|
+
.addHelpText('after', `
|
|
2155
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
2156
|
+
Performs an RDAP (Registration Data Access Protocol) lookup for a domain.
|
|
2157
|
+
RDAP is the modern, standardized replacement for WHOIS that returns
|
|
2158
|
+
structured JSON data instead of unstructured text.
|
|
2159
|
+
|
|
2160
|
+
RDAP provides better data consistency, supports internationalized domain
|
|
2161
|
+
names (IDN), and follows HTTP redirects to find authoritative servers.
|
|
2162
|
+
All major TLDs now support RDAP.
|
|
2163
|
+
|
|
2164
|
+
${colors.bold(colors.yellow('Advantages over WHOIS:'))}
|
|
2165
|
+
- Structured JSON output (machine-readable)
|
|
2166
|
+
- Better internationalization support
|
|
2167
|
+
- Standardized by IETF (RFC 7480-7484)
|
|
2168
|
+
- Bootstrap mechanism for TLD discovery
|
|
2169
|
+
- Rate limiting with proper error codes
|
|
2170
|
+
|
|
2171
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2172
|
+
${colors.green('$ rek rdap github.com')} Domain registration info
|
|
2173
|
+
${colors.green('$ rek rdap google.co.uk')} ccTLD domains
|
|
2174
|
+
${colors.green('$ rek rdap cloudflare.com')} Check registrar info
|
|
2175
|
+
|
|
2176
|
+
${colors.bold(colors.yellow('See also:'))}
|
|
2177
|
+
${colors.cyan('rek whois <domain>')} Traditional WHOIS lookup
|
|
2178
|
+
`)
|
|
1768
2179
|
.action(async (domain) => {
|
|
1769
2180
|
const { rdap } = await import('../utils/rdap.js');
|
|
1770
2181
|
const { Client } = await import('../core/client.js');
|
|
@@ -1816,14 +2227,46 @@ ${colors.bold('Status:')} ${result.status?.join(', ') || 'N/A'}
|
|
|
1816
2227
|
});
|
|
1817
2228
|
program
|
|
1818
2229
|
.command('ping')
|
|
1819
|
-
.description('TCP connectivity
|
|
2230
|
+
.description('Test TCP connectivity to host:port')
|
|
1820
2231
|
.argument('<host>', 'Hostname or IP address')
|
|
1821
|
-
.argument('[
|
|
1822
|
-
.
|
|
1823
|
-
|
|
2232
|
+
.argument('[args...]', 'Port and options: [port] count=4')
|
|
2233
|
+
.addHelpText('after', `
|
|
2234
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
2235
|
+
Tests TCP connectivity to a host and port, measuring connection latency.
|
|
2236
|
+
Unlike ICMP ping, this actually establishes TCP connections, so it works
|
|
2237
|
+
through most firewalls and accurately tests if a service is reachable.
|
|
2238
|
+
|
|
2239
|
+
Shows individual response times and calculates min/avg/max/stddev stats.
|
|
2240
|
+
Perfect for testing if a server is up, measuring network latency, or
|
|
2241
|
+
debugging connectivity issues.
|
|
2242
|
+
|
|
2243
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
2244
|
+
${colors.cyan('[port]')} Port number (default: 443)
|
|
2245
|
+
${colors.cyan('count=<n>')} Number of pings (default: 4)
|
|
2246
|
+
|
|
2247
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2248
|
+
${colors.green('$ rek ping google.com')} Test HTTPS (port 443)
|
|
2249
|
+
${colors.green('$ rek ping google.com 80')} Test HTTP (port 80)
|
|
2250
|
+
${colors.green('$ rek ping db.server.com 5432')} Test PostgreSQL port
|
|
2251
|
+
${colors.green('$ rek ping redis.local 6379 count=10')} 10 pings to Redis
|
|
2252
|
+
|
|
2253
|
+
${colors.bold(colors.yellow('Output:'))}
|
|
2254
|
+
Shows response time for each attempt, then summary statistics:
|
|
2255
|
+
- min/avg/max response times
|
|
2256
|
+
- standard deviation
|
|
2257
|
+
- packet loss percentage
|
|
2258
|
+
`)
|
|
2259
|
+
.action(async (host, args) => {
|
|
1824
2260
|
const net = await import('node:net');
|
|
1825
|
-
|
|
1826
|
-
|
|
2261
|
+
let port = 443;
|
|
2262
|
+
let count = 4;
|
|
2263
|
+
for (const arg of args) {
|
|
2264
|
+
if (arg.startsWith('count='))
|
|
2265
|
+
count = parseInt(arg.split('=')[1]);
|
|
2266
|
+
else if (!arg.includes('=') && /^\d+$/.test(arg))
|
|
2267
|
+
port = parseInt(arg);
|
|
2268
|
+
}
|
|
2269
|
+
const portNum = port;
|
|
1827
2270
|
const results = [];
|
|
1828
2271
|
console.log(colors.gray(`Pinging ${host}:${portNum}...`));
|
|
1829
2272
|
console.log('');
|
|
@@ -1873,20 +2316,48 @@ ${colors.bold('Statistics:')}
|
|
|
1873
2316
|
.command('ls')
|
|
1874
2317
|
.description('List files in a remote directory')
|
|
1875
2318
|
.argument('<host>', 'FTP server hostname')
|
|
1876
|
-
.argument('[
|
|
1877
|
-
.
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2319
|
+
.argument('[args...]', 'Path and options: [path] user=anonymous pass=anonymous@ port=21 secure implicit')
|
|
2320
|
+
.addHelpText('after', `
|
|
2321
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
2322
|
+
${colors.cyan('user=<username>')} Username (default: anonymous)
|
|
2323
|
+
${colors.cyan('pass=<password>')} Password (default: anonymous@)
|
|
2324
|
+
${colors.cyan('port=<port>')} Port number (default: 21)
|
|
2325
|
+
${colors.cyan('secure')} Use FTPS (explicit TLS)
|
|
2326
|
+
${colors.cyan('implicit')} Use implicit FTPS (port 990)
|
|
2327
|
+
|
|
2328
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2329
|
+
${colors.green('$ rek ftp ls ftp.example.com')} ${colors.gray('List root')}
|
|
2330
|
+
${colors.green('$ rek ftp ls ftp.example.com /pub')} ${colors.gray('List /pub directory')}
|
|
2331
|
+
${colors.green('$ rek ftp ls ftp.example.com user=admin pass=secret')} ${colors.gray('With credentials')}
|
|
2332
|
+
`)
|
|
2333
|
+
.action(async (host, args) => {
|
|
1883
2334
|
const { createFTP } = await import('../protocols/ftp.js');
|
|
1884
|
-
|
|
2335
|
+
let path = '/';
|
|
2336
|
+
let user = 'anonymous';
|
|
2337
|
+
let pass = 'anonymous@';
|
|
2338
|
+
let port = 21;
|
|
2339
|
+
let secureOption = false;
|
|
2340
|
+
let implicitOption = false;
|
|
2341
|
+
for (const arg of args) {
|
|
2342
|
+
if (arg.startsWith('user='))
|
|
2343
|
+
user = arg.split('=')[1];
|
|
2344
|
+
else if (arg.startsWith('pass='))
|
|
2345
|
+
pass = arg.split('=')[1];
|
|
2346
|
+
else if (arg.startsWith('port='))
|
|
2347
|
+
port = parseInt(arg.split('=')[1]);
|
|
2348
|
+
else if (arg === 'secure')
|
|
2349
|
+
secureOption = true;
|
|
2350
|
+
else if (arg === 'implicit')
|
|
2351
|
+
implicitOption = true;
|
|
2352
|
+
else if (!arg.includes('='))
|
|
2353
|
+
path = arg;
|
|
2354
|
+
}
|
|
2355
|
+
const secure = implicitOption ? 'implicit' : secureOption ? true : false;
|
|
1885
2356
|
const client = createFTP({
|
|
1886
2357
|
host,
|
|
1887
|
-
port
|
|
1888
|
-
user
|
|
1889
|
-
password:
|
|
2358
|
+
port,
|
|
2359
|
+
user,
|
|
2360
|
+
password: pass,
|
|
1890
2361
|
secure,
|
|
1891
2362
|
});
|
|
1892
2363
|
console.log(colors.gray(`Connecting to ${host}...`));
|
|
@@ -1929,22 +2400,50 @@ ${colors.bold('Statistics:')}
|
|
|
1929
2400
|
.description('Download a file from FTP server')
|
|
1930
2401
|
.argument('<host>', 'FTP server hostname')
|
|
1931
2402
|
.argument('<remote>', 'Remote file path')
|
|
1932
|
-
.argument('[
|
|
1933
|
-
.
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2403
|
+
.argument('[args...]', 'Local path and options: [local] user=anonymous pass=anonymous@ port=21 secure implicit')
|
|
2404
|
+
.addHelpText('after', `
|
|
2405
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
2406
|
+
${colors.cyan('user=<username>')} Username (default: anonymous)
|
|
2407
|
+
${colors.cyan('pass=<password>')} Password (default: anonymous@)
|
|
2408
|
+
${colors.cyan('port=<port>')} Port number (default: 21)
|
|
2409
|
+
${colors.cyan('secure')} Use FTPS (explicit TLS)
|
|
2410
|
+
${colors.cyan('implicit')} Use implicit FTPS (port 990)
|
|
2411
|
+
|
|
2412
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2413
|
+
${colors.green('$ rek ftp get ftp.example.com /pub/file.zip')} ${colors.gray('Download file')}
|
|
2414
|
+
${colors.green('$ rek ftp get ftp.example.com /pub/file.zip myfile.zip')} ${colors.gray('Save as different name')}
|
|
2415
|
+
${colors.green('$ rek ftp get ftp.example.com /data.csv user=admin pass=secret')} ${colors.gray('With credentials')}
|
|
2416
|
+
`)
|
|
2417
|
+
.action(async (host, remote, args) => {
|
|
1939
2418
|
const { createFTP } = await import('../protocols/ftp.js');
|
|
1940
|
-
const
|
|
1941
|
-
|
|
1942
|
-
|
|
2419
|
+
const pathMod = await import('node:path');
|
|
2420
|
+
let local;
|
|
2421
|
+
let user = 'anonymous';
|
|
2422
|
+
let pass = 'anonymous@';
|
|
2423
|
+
let port = 21;
|
|
2424
|
+
let secureOption = false;
|
|
2425
|
+
let implicitOption = false;
|
|
2426
|
+
for (const arg of args) {
|
|
2427
|
+
if (arg.startsWith('user='))
|
|
2428
|
+
user = arg.split('=')[1];
|
|
2429
|
+
else if (arg.startsWith('pass='))
|
|
2430
|
+
pass = arg.split('=')[1];
|
|
2431
|
+
else if (arg.startsWith('port='))
|
|
2432
|
+
port = parseInt(arg.split('=')[1]);
|
|
2433
|
+
else if (arg === 'secure')
|
|
2434
|
+
secureOption = true;
|
|
2435
|
+
else if (arg === 'implicit')
|
|
2436
|
+
implicitOption = true;
|
|
2437
|
+
else if (!arg.includes('='))
|
|
2438
|
+
local = arg;
|
|
2439
|
+
}
|
|
2440
|
+
const localPath = local || pathMod.basename(remote);
|
|
2441
|
+
const secure = implicitOption ? 'implicit' : secureOption ? true : false;
|
|
1943
2442
|
const client = createFTP({
|
|
1944
2443
|
host,
|
|
1945
|
-
port
|
|
1946
|
-
user
|
|
1947
|
-
password:
|
|
2444
|
+
port,
|
|
2445
|
+
user,
|
|
2446
|
+
password: pass,
|
|
1948
2447
|
secure,
|
|
1949
2448
|
});
|
|
1950
2449
|
console.log(colors.gray(`Connecting to ${host}...`));
|
|
@@ -1984,22 +2483,50 @@ ${colors.bold('Statistics:')}
|
|
|
1984
2483
|
.description('Upload a file to FTP server')
|
|
1985
2484
|
.argument('<host>', 'FTP server hostname')
|
|
1986
2485
|
.argument('<local>', 'Local file path')
|
|
1987
|
-
.argument('[
|
|
1988
|
-
.
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2486
|
+
.argument('[args...]', 'Remote path and options: [remote] user=anonymous pass=anonymous@ port=21 secure implicit')
|
|
2487
|
+
.addHelpText('after', `
|
|
2488
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
2489
|
+
${colors.cyan('user=<username>')} Username (default: anonymous)
|
|
2490
|
+
${colors.cyan('pass=<password>')} Password (default: anonymous@)
|
|
2491
|
+
${colors.cyan('port=<port>')} Port number (default: 21)
|
|
2492
|
+
${colors.cyan('secure')} Use FTPS (explicit TLS)
|
|
2493
|
+
${colors.cyan('implicit')} Use implicit FTPS (port 990)
|
|
2494
|
+
|
|
2495
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2496
|
+
${colors.green('$ rek ftp put ftp.example.com myfile.zip')} ${colors.gray('Upload file')}
|
|
2497
|
+
${colors.green('$ rek ftp put ftp.example.com myfile.zip /uploads/data.zip')} ${colors.gray('Upload with path')}
|
|
2498
|
+
${colors.green('$ rek ftp put ftp.example.com data.csv user=admin pass=secret')} ${colors.gray('With credentials')}
|
|
2499
|
+
`)
|
|
2500
|
+
.action(async (host, local, args) => {
|
|
1994
2501
|
const { createFTP } = await import('../protocols/ftp.js');
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
2502
|
+
const pathMod = await import('node:path');
|
|
2503
|
+
let remote;
|
|
2504
|
+
let user = 'anonymous';
|
|
2505
|
+
let pass = 'anonymous@';
|
|
2506
|
+
let port = 21;
|
|
2507
|
+
let secureOption = false;
|
|
2508
|
+
let implicitOption = false;
|
|
2509
|
+
for (const arg of args) {
|
|
2510
|
+
if (arg.startsWith('user='))
|
|
2511
|
+
user = arg.split('=')[1];
|
|
2512
|
+
else if (arg.startsWith('pass='))
|
|
2513
|
+
pass = arg.split('=')[1];
|
|
2514
|
+
else if (arg.startsWith('port='))
|
|
2515
|
+
port = parseInt(arg.split('=')[1]);
|
|
2516
|
+
else if (arg === 'secure')
|
|
2517
|
+
secureOption = true;
|
|
2518
|
+
else if (arg === 'implicit')
|
|
2519
|
+
implicitOption = true;
|
|
2520
|
+
else if (!arg.includes('='))
|
|
2521
|
+
remote = arg;
|
|
2522
|
+
}
|
|
2523
|
+
const remotePath = remote || '/' + pathMod.basename(local);
|
|
2524
|
+
const secure = implicitOption ? 'implicit' : secureOption ? true : false;
|
|
1998
2525
|
const client = createFTP({
|
|
1999
2526
|
host,
|
|
2000
|
-
port
|
|
2001
|
-
user
|
|
2002
|
-
password:
|
|
2527
|
+
port,
|
|
2528
|
+
user,
|
|
2529
|
+
password: pass,
|
|
2003
2530
|
secure,
|
|
2004
2531
|
});
|
|
2005
2532
|
console.log(colors.gray(`Connecting to ${host}...`));
|
|
@@ -2039,19 +2566,44 @@ ${colors.bold('Statistics:')}
|
|
|
2039
2566
|
.description('Delete a file from FTP server')
|
|
2040
2567
|
.argument('<host>', 'FTP server hostname')
|
|
2041
2568
|
.argument('<path>', 'Remote file path to delete')
|
|
2042
|
-
.
|
|
2043
|
-
.
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2569
|
+
.argument('[args...]', 'Options: user=anonymous pass=anonymous@ port=21 secure implicit')
|
|
2570
|
+
.addHelpText('after', `
|
|
2571
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
2572
|
+
${colors.cyan('user=<username>')} Username (default: anonymous)
|
|
2573
|
+
${colors.cyan('pass=<password>')} Password (default: anonymous@)
|
|
2574
|
+
${colors.cyan('port=<port>')} Port number (default: 21)
|
|
2575
|
+
${colors.cyan('secure')} Use FTPS (explicit TLS)
|
|
2576
|
+
${colors.cyan('implicit')} Use implicit FTPS (port 990)
|
|
2577
|
+
|
|
2578
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2579
|
+
${colors.green('$ rek ftp rm ftp.example.com /uploads/old.zip')} ${colors.gray('Delete file')}
|
|
2580
|
+
${colors.green('$ rek ftp rm ftp.example.com /data.txt user=admin pass=secret')} ${colors.gray('With credentials')}
|
|
2581
|
+
`)
|
|
2582
|
+
.action(async (host, remotePath, args) => {
|
|
2048
2583
|
const { createFTP } = await import('../protocols/ftp.js');
|
|
2049
|
-
|
|
2584
|
+
let user = 'anonymous';
|
|
2585
|
+
let pass = 'anonymous@';
|
|
2586
|
+
let port = 21;
|
|
2587
|
+
let secureOption = false;
|
|
2588
|
+
let implicitOption = false;
|
|
2589
|
+
for (const arg of args) {
|
|
2590
|
+
if (arg.startsWith('user='))
|
|
2591
|
+
user = arg.split('=')[1];
|
|
2592
|
+
else if (arg.startsWith('pass='))
|
|
2593
|
+
pass = arg.split('=')[1];
|
|
2594
|
+
else if (arg.startsWith('port='))
|
|
2595
|
+
port = parseInt(arg.split('=')[1]);
|
|
2596
|
+
else if (arg === 'secure')
|
|
2597
|
+
secureOption = true;
|
|
2598
|
+
else if (arg === 'implicit')
|
|
2599
|
+
implicitOption = true;
|
|
2600
|
+
}
|
|
2601
|
+
const secure = implicitOption ? 'implicit' : secureOption ? true : false;
|
|
2050
2602
|
const client = createFTP({
|
|
2051
2603
|
host,
|
|
2052
|
-
port
|
|
2053
|
-
user
|
|
2054
|
-
password:
|
|
2604
|
+
port,
|
|
2605
|
+
user,
|
|
2606
|
+
password: pass,
|
|
2055
2607
|
secure,
|
|
2056
2608
|
});
|
|
2057
2609
|
console.log(colors.gray(`Connecting to ${host}...`));
|
|
@@ -2082,19 +2634,44 @@ ${colors.bold('Statistics:')}
|
|
|
2082
2634
|
.description('Create a directory on FTP server')
|
|
2083
2635
|
.argument('<host>', 'FTP server hostname')
|
|
2084
2636
|
.argument('<path>', 'Remote directory path to create')
|
|
2085
|
-
.
|
|
2086
|
-
.
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2637
|
+
.argument('[args...]', 'Options: user=anonymous pass=anonymous@ port=21 secure implicit')
|
|
2638
|
+
.addHelpText('after', `
|
|
2639
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
2640
|
+
${colors.cyan('user=<username>')} Username (default: anonymous)
|
|
2641
|
+
${colors.cyan('pass=<password>')} Password (default: anonymous@)
|
|
2642
|
+
${colors.cyan('port=<port>')} Port number (default: 21)
|
|
2643
|
+
${colors.cyan('secure')} Use FTPS (explicit TLS)
|
|
2644
|
+
${colors.cyan('implicit')} Use implicit FTPS (port 990)
|
|
2645
|
+
|
|
2646
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2647
|
+
${colors.green('$ rek ftp mkdir ftp.example.com /uploads/new-folder')} ${colors.gray('Create directory')}
|
|
2648
|
+
${colors.green('$ rek ftp mkdir ftp.example.com /data user=admin pass=secret')} ${colors.gray('With credentials')}
|
|
2649
|
+
`)
|
|
2650
|
+
.action(async (host, remotePath, args) => {
|
|
2091
2651
|
const { createFTP } = await import('../protocols/ftp.js');
|
|
2092
|
-
|
|
2652
|
+
let user = 'anonymous';
|
|
2653
|
+
let pass = 'anonymous@';
|
|
2654
|
+
let port = 21;
|
|
2655
|
+
let secureOption = false;
|
|
2656
|
+
let implicitOption = false;
|
|
2657
|
+
for (const arg of args) {
|
|
2658
|
+
if (arg.startsWith('user='))
|
|
2659
|
+
user = arg.split('=')[1];
|
|
2660
|
+
else if (arg.startsWith('pass='))
|
|
2661
|
+
pass = arg.split('=')[1];
|
|
2662
|
+
else if (arg.startsWith('port='))
|
|
2663
|
+
port = parseInt(arg.split('=')[1]);
|
|
2664
|
+
else if (arg === 'secure')
|
|
2665
|
+
secureOption = true;
|
|
2666
|
+
else if (arg === 'implicit')
|
|
2667
|
+
implicitOption = true;
|
|
2668
|
+
}
|
|
2669
|
+
const secure = implicitOption ? 'implicit' : secureOption ? true : false;
|
|
2093
2670
|
const client = createFTP({
|
|
2094
2671
|
host,
|
|
2095
|
-
port
|
|
2096
|
-
user
|
|
2097
|
-
password:
|
|
2672
|
+
port,
|
|
2673
|
+
user,
|
|
2674
|
+
password: pass,
|
|
2098
2675
|
secure,
|
|
2099
2676
|
});
|
|
2100
2677
|
console.log(colors.gray(`Connecting to ${host}...`));
|
|
@@ -2124,15 +2701,31 @@ ${colors.bold('Statistics:')}
|
|
|
2124
2701
|
.command('telnet')
|
|
2125
2702
|
.description('Connect to a Telnet server')
|
|
2126
2703
|
.argument('<host>', 'Hostname or IP address')
|
|
2127
|
-
.argument('[
|
|
2128
|
-
.
|
|
2129
|
-
|
|
2704
|
+
.argument('[args...]', 'Port and options: [port] timeout=30000')
|
|
2705
|
+
.addHelpText('after', `
|
|
2706
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
2707
|
+
${colors.cyan('[port]')} Port number (default: 23)
|
|
2708
|
+
${colors.cyan('timeout=<ms>')} Connection timeout in ms (default: 30000)
|
|
2709
|
+
|
|
2710
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2711
|
+
${colors.green('$ rek telnet mail.example.com 25')} ${colors.gray('Connect to SMTP server')}
|
|
2712
|
+
${colors.green('$ rek telnet host.example.com timeout=60000')} ${colors.gray('With custom timeout')}
|
|
2713
|
+
`)
|
|
2714
|
+
.action(async (host, args) => {
|
|
2130
2715
|
const { createTelnet } = await import('../protocols/telnet.js');
|
|
2716
|
+
let port = 23;
|
|
2717
|
+
let timeout = 30000;
|
|
2718
|
+
for (const arg of args) {
|
|
2719
|
+
if (arg.startsWith('timeout='))
|
|
2720
|
+
timeout = parseInt(arg.split('=')[1]);
|
|
2721
|
+
else if (!arg.includes('=') && /^\d+$/.test(arg))
|
|
2722
|
+
port = parseInt(arg);
|
|
2723
|
+
}
|
|
2131
2724
|
console.log(colors.gray(`Connecting to ${host}:${port}...`));
|
|
2132
2725
|
const client = createTelnet({
|
|
2133
2726
|
host,
|
|
2134
|
-
port
|
|
2135
|
-
timeout
|
|
2727
|
+
port,
|
|
2728
|
+
timeout,
|
|
2136
2729
|
});
|
|
2137
2730
|
try {
|
|
2138
2731
|
await client.connect();
|
|
@@ -2182,9 +2775,37 @@ ${colors.bold('Statistics:')}
|
|
|
2182
2775
|
});
|
|
2183
2776
|
dns
|
|
2184
2777
|
.command('lookup')
|
|
2185
|
-
.
|
|
2778
|
+
.alias('resolve')
|
|
2779
|
+
.description('Look up DNS records (A, MX, TXT, etc)')
|
|
2186
2780
|
.argument('<domain>', 'Domain name to lookup')
|
|
2187
2781
|
.argument('[type]', 'Record type (A, AAAA, CNAME, MX, NS, TXT, SOA, CAA, SRV, ANY)', 'A')
|
|
2782
|
+
.addHelpText('after', `
|
|
2783
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
2784
|
+
Queries DNS servers to resolve domain records. Returns IP addresses (A/AAAA),
|
|
2785
|
+
mail servers (MX), name servers (NS), text records (TXT), and more.
|
|
2786
|
+
|
|
2787
|
+
Uses your system's configured DNS resolvers. For advanced queries with
|
|
2788
|
+
custom nameservers, use ${colors.cyan('rek dns dig')} instead.
|
|
2789
|
+
|
|
2790
|
+
${colors.bold(colors.yellow('Record Types:'))}
|
|
2791
|
+
A IPv4 address
|
|
2792
|
+
AAAA IPv6 address
|
|
2793
|
+
CNAME Canonical name (alias)
|
|
2794
|
+
MX Mail exchange servers
|
|
2795
|
+
NS Name servers
|
|
2796
|
+
TXT Text records (SPF, DKIM, etc)
|
|
2797
|
+
SOA Start of Authority
|
|
2798
|
+
CAA Certificate Authority Authorization
|
|
2799
|
+
SRV Service location
|
|
2800
|
+
ANY All available records
|
|
2801
|
+
|
|
2802
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
2803
|
+
${colors.green('$ rek dns lookup google.com')} A records (default)
|
|
2804
|
+
${colors.green('$ rek dns lookup google.com MX')} Mail servers
|
|
2805
|
+
${colors.green('$ rek dns lookup google.com TXT')} Text records
|
|
2806
|
+
${colors.green('$ rek dns lookup google.com ANY')} All records
|
|
2807
|
+
${colors.green('$ rek dns resolve github.com AAAA')} IPv6 addresses
|
|
2808
|
+
`)
|
|
2188
2809
|
.action(async (domain, type) => {
|
|
2189
2810
|
const { dnsLookup } = await import('../utils/dns-toolkit.js');
|
|
2190
2811
|
console.log(colors.gray(`Looking up ${type.toUpperCase()} records for ${domain}...`));
|
|
@@ -2459,27 +3080,54 @@ ${colors.gray('Status:')} ${statusIcon}
|
|
|
2459
3080
|
dns
|
|
2460
3081
|
.command('generate-dmarc')
|
|
2461
3082
|
.description('Generate a DMARC record interactively')
|
|
2462
|
-
.
|
|
2463
|
-
.
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
3083
|
+
.argument('[args...]', 'Options: policy=none subdomain-policy=<policy> pct=100 rua=<emails> ruf=<emails>')
|
|
3084
|
+
.addHelpText('after', `
|
|
3085
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
3086
|
+
${colors.cyan('policy=<policy>')} Policy: none, quarantine, reject (default: none)
|
|
3087
|
+
${colors.cyan('subdomain-policy=<policy>')} Subdomain policy
|
|
3088
|
+
${colors.cyan('pct=<percent>')} Percentage of emails to apply policy (default: 100)
|
|
3089
|
+
${colors.cyan('rua=<emails>')} Aggregate report email(s), comma-separated
|
|
3090
|
+
${colors.cyan('ruf=<emails>')} Forensic report email(s), comma-separated
|
|
3091
|
+
|
|
3092
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
3093
|
+
${colors.green('$ rek dns generate-dmarc')} ${colors.gray('Generate with defaults')}
|
|
3094
|
+
${colors.green('$ rek dns generate-dmarc policy=quarantine')} ${colors.gray('Set quarantine policy')}
|
|
3095
|
+
${colors.green('$ rek dns generate-dmarc policy=reject pct=50')} ${colors.gray('Reject 50% of failures')}
|
|
3096
|
+
${colors.green('$ rek dns generate-dmarc rua=admin@example.com')} ${colors.gray('Send reports to email')}
|
|
3097
|
+
`)
|
|
3098
|
+
.action(async (args) => {
|
|
2468
3099
|
const { generateDmarc } = await import('../utils/dns-toolkit.js');
|
|
3100
|
+
let policy = 'none';
|
|
3101
|
+
let subdomainPolicy;
|
|
3102
|
+
let pct = '100';
|
|
3103
|
+
let rua;
|
|
3104
|
+
let ruf;
|
|
3105
|
+
for (const arg of args) {
|
|
3106
|
+
if (arg.startsWith('policy='))
|
|
3107
|
+
policy = arg.split('=')[1];
|
|
3108
|
+
else if (arg.startsWith('subdomain-policy='))
|
|
3109
|
+
subdomainPolicy = arg.split('=')[1];
|
|
3110
|
+
else if (arg.startsWith('pct='))
|
|
3111
|
+
pct = arg.split('=')[1];
|
|
3112
|
+
else if (arg.startsWith('rua='))
|
|
3113
|
+
rua = arg.split('=')[1];
|
|
3114
|
+
else if (arg.startsWith('ruf='))
|
|
3115
|
+
ruf = arg.split('=')[1];
|
|
3116
|
+
}
|
|
2469
3117
|
const dmarcOptions = {
|
|
2470
|
-
policy
|
|
3118
|
+
policy,
|
|
2471
3119
|
};
|
|
2472
|
-
if (
|
|
2473
|
-
dmarcOptions.subdomainPolicy =
|
|
3120
|
+
if (subdomainPolicy) {
|
|
3121
|
+
dmarcOptions.subdomainPolicy = subdomainPolicy;
|
|
2474
3122
|
}
|
|
2475
|
-
if (
|
|
2476
|
-
dmarcOptions.percentage = parseInt(
|
|
3123
|
+
if (pct && pct !== '100') {
|
|
3124
|
+
dmarcOptions.percentage = parseInt(pct);
|
|
2477
3125
|
}
|
|
2478
|
-
if (
|
|
2479
|
-
dmarcOptions.aggregateReports =
|
|
3126
|
+
if (rua) {
|
|
3127
|
+
dmarcOptions.aggregateReports = rua.split(',').map((e) => e.trim());
|
|
2480
3128
|
}
|
|
2481
|
-
if (
|
|
2482
|
-
dmarcOptions.forensicReports =
|
|
3129
|
+
if (ruf) {
|
|
3130
|
+
dmarcOptions.forensicReports = ruf.split(',').map((e) => e.trim());
|
|
2483
3131
|
}
|
|
2484
3132
|
const record = generateDmarc(dmarcOptions);
|
|
2485
3133
|
console.log(`
|
|
@@ -2561,22 +3209,68 @@ ${colors.bold(colors.yellow('Record Types:'))}
|
|
|
2561
3209
|
});
|
|
2562
3210
|
program
|
|
2563
3211
|
.command('graphql')
|
|
2564
|
-
.
|
|
3212
|
+
.alias('gql')
|
|
3213
|
+
.description('Execute GraphQL queries and mutations')
|
|
2565
3214
|
.argument('<url>', 'GraphQL endpoint URL')
|
|
2566
|
-
.
|
|
2567
|
-
.
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
3215
|
+
.argument('[args...]', 'Options: query=<query> file=<file> variables=<json> var-file=<file> Header:Value')
|
|
3216
|
+
.addHelpText('after', `
|
|
3217
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
3218
|
+
Execute GraphQL queries and mutations against any GraphQL endpoint.
|
|
3219
|
+
Supports inline queries, query files (.graphql), and variables from
|
|
3220
|
+
JSON files or inline. Perfect for testing GraphQL APIs quickly.
|
|
3221
|
+
|
|
3222
|
+
Results are displayed as formatted JSON. Headers can be added for
|
|
3223
|
+
authentication (Bearer tokens, API keys, etc).
|
|
3224
|
+
|
|
3225
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
3226
|
+
${colors.cyan('query=<query>')} Inline GraphQL query string
|
|
3227
|
+
${colors.cyan('file=<file>')} Load query from .graphql file
|
|
3228
|
+
${colors.cyan('variables=<json>')} Inline variables as JSON
|
|
3229
|
+
${colors.cyan('var-file=<file>')} Load variables from JSON file
|
|
3230
|
+
${colors.cyan('Header:Value')} Add header (can use multiple times)
|
|
3231
|
+
|
|
3232
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
3233
|
+
${colors.green('$ rek graphql https://api.github.com/graphql query="{ viewer { login } }"')}
|
|
3234
|
+
${colors.gray(' Simple query')}
|
|
3235
|
+
|
|
3236
|
+
${colors.green('$ rek gql https://api.spacex.land/graphql query="{ rockets { name } }"')}
|
|
3237
|
+
${colors.gray(' Using the gql alias')}
|
|
3238
|
+
|
|
3239
|
+
${colors.green('$ rek graphql api.example.com/graphql file=query.graphql variables=\'{"id": 123}\'')}
|
|
3240
|
+
${colors.gray(' Query from file with variables')}
|
|
3241
|
+
|
|
3242
|
+
${colors.green('$ rek graphql api.com/graphql query="query User($id: ID!) { user(id: $id) { name } }" var-file=vars.json')}
|
|
3243
|
+
${colors.gray(' Parameterized query with variable file')}
|
|
3244
|
+
|
|
3245
|
+
${colors.green('$ rek graphql api.com/graphql query="{ me { id } }" Authorization:"Bearer token123"')}
|
|
3246
|
+
${colors.gray(' With authentication header')}
|
|
3247
|
+
`)
|
|
3248
|
+
.action(async (url, args) => {
|
|
2572
3249
|
const { graphql } = await import('../plugins/graphql.js');
|
|
2573
3250
|
const { createClient } = await import('../core/client.js');
|
|
2574
3251
|
const fs = await import('node:fs/promises');
|
|
2575
|
-
let
|
|
3252
|
+
let queryStr;
|
|
3253
|
+
let queryFile;
|
|
3254
|
+
let variablesStr;
|
|
3255
|
+
let varFile;
|
|
3256
|
+
const headerArgs = [];
|
|
3257
|
+
for (const arg of args) {
|
|
3258
|
+
if (arg.startsWith('query='))
|
|
3259
|
+
queryStr = arg.slice(6);
|
|
3260
|
+
else if (arg.startsWith('file='))
|
|
3261
|
+
queryFile = arg.slice(5);
|
|
3262
|
+
else if (arg.startsWith('variables='))
|
|
3263
|
+
variablesStr = arg.slice(10);
|
|
3264
|
+
else if (arg.startsWith('var-file='))
|
|
3265
|
+
varFile = arg.slice(9);
|
|
3266
|
+
else if (arg.includes(':'))
|
|
3267
|
+
headerArgs.push(arg);
|
|
3268
|
+
}
|
|
3269
|
+
let query = queryStr;
|
|
2576
3270
|
let variables = {};
|
|
2577
|
-
if (
|
|
3271
|
+
if (queryFile) {
|
|
2578
3272
|
try {
|
|
2579
|
-
query = await fs.readFile(
|
|
3273
|
+
query = await fs.readFile(queryFile, 'utf-8');
|
|
2580
3274
|
}
|
|
2581
3275
|
catch (err) {
|
|
2582
3276
|
console.error(colors.red(`Failed to read query file: ${err.message}`));
|
|
@@ -2584,22 +3278,22 @@ ${colors.bold(colors.yellow('Record Types:'))}
|
|
|
2584
3278
|
}
|
|
2585
3279
|
}
|
|
2586
3280
|
if (!query) {
|
|
2587
|
-
console.error(colors.red('Error: Query is required. Use
|
|
2588
|
-
console.log(colors.gray('Example: rek graphql https://api.example.com/graphql
|
|
3281
|
+
console.error(colors.red('Error: Query is required. Use query= or file='));
|
|
3282
|
+
console.log(colors.gray('Example: rek graphql https://api.example.com/graphql query="query { users { id name } }"'));
|
|
2589
3283
|
process.exit(1);
|
|
2590
3284
|
}
|
|
2591
|
-
if (
|
|
3285
|
+
if (variablesStr) {
|
|
2592
3286
|
try {
|
|
2593
|
-
variables = JSON.parse(
|
|
3287
|
+
variables = JSON.parse(variablesStr);
|
|
2594
3288
|
}
|
|
2595
3289
|
catch {
|
|
2596
|
-
console.error(colors.red('Invalid JSON in
|
|
3290
|
+
console.error(colors.red('Invalid JSON in variables='));
|
|
2597
3291
|
process.exit(1);
|
|
2598
3292
|
}
|
|
2599
3293
|
}
|
|
2600
|
-
else if (
|
|
3294
|
+
else if (varFile) {
|
|
2601
3295
|
try {
|
|
2602
|
-
const content = await fs.readFile(
|
|
3296
|
+
const content = await fs.readFile(varFile, 'utf-8');
|
|
2603
3297
|
variables = JSON.parse(content);
|
|
2604
3298
|
}
|
|
2605
3299
|
catch (err) {
|
|
@@ -2610,7 +3304,7 @@ ${colors.bold(colors.yellow('Record Types:'))}
|
|
|
2610
3304
|
const headers = {
|
|
2611
3305
|
'Content-Type': 'application/json',
|
|
2612
3306
|
};
|
|
2613
|
-
for (const h of
|
|
3307
|
+
for (const h of headerArgs) {
|
|
2614
3308
|
const [key, ...valueParts] = h.split(':');
|
|
2615
3309
|
headers[key.trim()] = valueParts.join(':').trim();
|
|
2616
3310
|
}
|
|
@@ -2774,21 +3468,48 @@ ${colors.bold(colors.yellow('Record Types:'))}
|
|
|
2774
3468
|
.command('download')
|
|
2775
3469
|
.description('Download an HLS stream')
|
|
2776
3470
|
.argument('<url>', 'HLS playlist URL')
|
|
2777
|
-
.argument('[
|
|
2778
|
-
.
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
3471
|
+
.argument('[args...]', 'Output and options: [output] quality=highest live duration=<seconds> concurrency=4')
|
|
3472
|
+
.addHelpText('after', `
|
|
3473
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
3474
|
+
${colors.cyan('[output]')} Output file path (default: output.ts)
|
|
3475
|
+
${colors.cyan('quality=<quality>')} Quality: highest, lowest, or resolution (e.g., 720p)
|
|
3476
|
+
${colors.cyan('live')} Enable live stream mode
|
|
3477
|
+
${colors.cyan('duration=<seconds>')} Duration for live recording in seconds
|
|
3478
|
+
${colors.cyan('concurrency=<n>')} Concurrent segment downloads (default: 4)
|
|
3479
|
+
|
|
3480
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
3481
|
+
${colors.green('$ rek hls download https://example.com/stream.m3u8')} ${colors.gray('Download stream')}
|
|
3482
|
+
${colors.green('$ rek hls download https://example.com/stream.m3u8 video.ts')} ${colors.gray('Custom output')}
|
|
3483
|
+
${colors.green('$ rek hls download https://example.com/stream.m3u8 quality=720p')} ${colors.gray('Select quality')}
|
|
3484
|
+
${colors.green('$ rek hls download https://example.com/live.m3u8 live duration=60')} ${colors.gray('Record live stream')}
|
|
3485
|
+
`)
|
|
3486
|
+
.action(async (url, args) => {
|
|
2783
3487
|
const { hls } = await import('../plugins/hls.js');
|
|
2784
3488
|
const { Client } = await import('../core/client.js');
|
|
3489
|
+
let output = 'output.ts';
|
|
3490
|
+
let quality;
|
|
3491
|
+
let live = false;
|
|
3492
|
+
let duration;
|
|
3493
|
+
let concurrency = 4;
|
|
3494
|
+
for (const arg of args) {
|
|
3495
|
+
if (arg.startsWith('quality='))
|
|
3496
|
+
quality = arg.split('=')[1];
|
|
3497
|
+
else if (arg === 'live')
|
|
3498
|
+
live = true;
|
|
3499
|
+
else if (arg.startsWith('duration='))
|
|
3500
|
+
duration = parseInt(arg.split('=')[1]);
|
|
3501
|
+
else if (arg.startsWith('concurrency='))
|
|
3502
|
+
concurrency = parseInt(arg.split('=')[1]);
|
|
3503
|
+
else if (!arg.includes('='))
|
|
3504
|
+
output = arg;
|
|
3505
|
+
}
|
|
2785
3506
|
const client = new Client();
|
|
2786
3507
|
console.log(colors.gray(`Downloading HLS stream from ${url}...`));
|
|
2787
3508
|
console.log(colors.gray(`Output: ${output}`));
|
|
2788
3509
|
console.log('');
|
|
2789
3510
|
try {
|
|
2790
3511
|
const hlsOptions = {
|
|
2791
|
-
concurrency
|
|
3512
|
+
concurrency,
|
|
2792
3513
|
onProgress: (p) => {
|
|
2793
3514
|
const segs = p.totalSegments
|
|
2794
3515
|
? `${p.downloadedSegments}/${p.totalSegments}`
|
|
@@ -2797,17 +3518,17 @@ ${colors.bold(colors.yellow('Record Types:'))}
|
|
|
2797
3518
|
process.stdout.write(`\r ${colors.cyan(segs)} segments | ${colors.cyan(mb + ' MB')} downloaded`);
|
|
2798
3519
|
},
|
|
2799
3520
|
};
|
|
2800
|
-
if (
|
|
2801
|
-
if (
|
|
2802
|
-
hlsOptions.quality =
|
|
3521
|
+
if (quality) {
|
|
3522
|
+
if (quality === 'highest' || quality === 'lowest') {
|
|
3523
|
+
hlsOptions.quality = quality;
|
|
2803
3524
|
}
|
|
2804
|
-
else if (
|
|
2805
|
-
hlsOptions.quality = { resolution:
|
|
3525
|
+
else if (quality.includes('p')) {
|
|
3526
|
+
hlsOptions.quality = { resolution: quality };
|
|
2806
3527
|
}
|
|
2807
3528
|
}
|
|
2808
|
-
if (
|
|
2809
|
-
hlsOptions.live =
|
|
2810
|
-
? { duration:
|
|
3529
|
+
if (live) {
|
|
3530
|
+
hlsOptions.live = duration
|
|
3531
|
+
? { duration: duration * 1000 }
|
|
2811
3532
|
: true;
|
|
2812
3533
|
}
|
|
2813
3534
|
await hls(client, url, hlsOptions).download(output);
|
|
@@ -2980,19 +3701,23 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
2980
3701
|
.command('info')
|
|
2981
3702
|
.description('Show information about a HAR file')
|
|
2982
3703
|
.argument('<file>', 'HAR file to inspect')
|
|
2983
|
-
.
|
|
3704
|
+
.argument('[args...]', 'Options: json')
|
|
2984
3705
|
.addHelpText('after', `
|
|
3706
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
3707
|
+
${colors.cyan('json')} Output as JSON
|
|
3708
|
+
|
|
2985
3709
|
${colors.bold(colors.yellow('Examples:'))}
|
|
2986
3710
|
${colors.green('$ rek har info api.har')}
|
|
2987
|
-
${colors.green('$ rek har info api.har
|
|
3711
|
+
${colors.green('$ rek har info api.har json')}
|
|
2988
3712
|
`)
|
|
2989
|
-
.action(async (file,
|
|
3713
|
+
.action(async (file, args) => {
|
|
2990
3714
|
const { promises: fsPromises } = await import('node:fs');
|
|
3715
|
+
const jsonOutput = args.includes('json');
|
|
2991
3716
|
try {
|
|
2992
3717
|
const content = await fsPromises.readFile(file, 'utf-8');
|
|
2993
3718
|
const har = JSON.parse(content);
|
|
2994
3719
|
const entries = har.log?.entries || [];
|
|
2995
|
-
if (
|
|
3720
|
+
if (jsonOutput) {
|
|
2996
3721
|
const info = {
|
|
2997
3722
|
version: har.log?.version,
|
|
2998
3723
|
creator: har.log?.creator,
|
|
@@ -3119,28 +3844,66 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
3119
3844
|
const serve = program.command('serve').description('Start mock servers for testing protocols');
|
|
3120
3845
|
serve
|
|
3121
3846
|
.command('http')
|
|
3122
|
-
.description('Start a mock HTTP server')
|
|
3123
|
-
.
|
|
3124
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3125
|
-
.option('--echo', 'Echo request body back in response')
|
|
3126
|
-
.option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
|
|
3127
|
-
.option('--cors', 'Enable CORS', true)
|
|
3847
|
+
.description('Start a mock HTTP server for testing')
|
|
3848
|
+
.argument('[args...]', 'Options: port=3000 host=127.0.0.1 echo delay=0 cors')
|
|
3128
3849
|
.addHelpText('after', `
|
|
3850
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
3851
|
+
Starts a local mock HTTP server for testing HTTP clients, webhooks, or APIs.
|
|
3852
|
+
Provides useful built-in endpoints for testing various HTTP scenarios.
|
|
3853
|
+
|
|
3854
|
+
Supports echo mode (returns the request back), configurable delays for
|
|
3855
|
+
testing timeouts, and CORS for browser-based testing. Perfect for
|
|
3856
|
+
integration tests, webhook development, or API prototyping.
|
|
3857
|
+
|
|
3858
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
3859
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 3000)
|
|
3860
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
3861
|
+
${colors.cyan('echo')} Echo request body back in response
|
|
3862
|
+
${colors.cyan('delay=<ms>')} Add delay to responses in ms (default: 0)
|
|
3863
|
+
${colors.cyan('cors')} Enable CORS (default: true)
|
|
3864
|
+
|
|
3865
|
+
${colors.bold(colors.yellow('Built-in Endpoints:'))}
|
|
3866
|
+
GET / Health check, returns { ok: true }
|
|
3867
|
+
GET /json Sample JSON response
|
|
3868
|
+
GET /delay/:ms Delayed response
|
|
3869
|
+
POST /echo Echo request body back
|
|
3870
|
+
* /status/:code Return specific HTTP status code
|
|
3871
|
+
GET /headers Return request headers
|
|
3872
|
+
|
|
3129
3873
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3130
|
-
${colors.green('$ rek serve http')}
|
|
3131
|
-
${colors.green('$ rek serve http
|
|
3132
|
-
${colors.green('$ rek serve http
|
|
3133
|
-
${colors.green('$ rek serve http
|
|
3874
|
+
${colors.green('$ rek serve http')} Start on port 3000
|
|
3875
|
+
${colors.green('$ rek serve http port=8080')} Start on port 8080
|
|
3876
|
+
${colors.green('$ rek serve http echo')} Echo mode (all routes)
|
|
3877
|
+
${colors.green('$ rek serve http delay=500')} Add 500ms delay to all responses
|
|
3134
3878
|
`)
|
|
3135
|
-
.action(async (
|
|
3879
|
+
.action(async (args) => {
|
|
3880
|
+
let port = 3000;
|
|
3881
|
+
let host = '127.0.0.1';
|
|
3882
|
+
let echo = false;
|
|
3883
|
+
let delay = 0;
|
|
3884
|
+
let cors = true;
|
|
3885
|
+
for (const arg of args) {
|
|
3886
|
+
if (arg.startsWith('port='))
|
|
3887
|
+
port = parseInt(arg.split('=')[1]);
|
|
3888
|
+
else if (arg.startsWith('host='))
|
|
3889
|
+
host = arg.split('=')[1];
|
|
3890
|
+
else if (arg === 'echo')
|
|
3891
|
+
echo = true;
|
|
3892
|
+
else if (arg.startsWith('delay='))
|
|
3893
|
+
delay = parseInt(arg.split('=')[1]);
|
|
3894
|
+
else if (arg === 'cors')
|
|
3895
|
+
cors = true;
|
|
3896
|
+
else if (arg === 'nocors')
|
|
3897
|
+
cors = false;
|
|
3898
|
+
}
|
|
3136
3899
|
const { MockHttpServer } = await import('../testing/mock-http-server.js');
|
|
3137
3900
|
const server = await MockHttpServer.create({
|
|
3138
|
-
port
|
|
3139
|
-
host
|
|
3140
|
-
delay
|
|
3141
|
-
cors
|
|
3901
|
+
port,
|
|
3902
|
+
host,
|
|
3903
|
+
delay,
|
|
3904
|
+
cors,
|
|
3142
3905
|
});
|
|
3143
|
-
if (
|
|
3906
|
+
if (echo) {
|
|
3144
3907
|
server.any('/*', (req) => ({
|
|
3145
3908
|
status: 200,
|
|
3146
3909
|
body: {
|
|
@@ -3157,8 +3920,8 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
3157
3920
|
│ ${colors.bold('Recker Mock HTTP Server')} │
|
|
3158
3921
|
├─────────────────────────────────────────────┤
|
|
3159
3922
|
│ URL: ${colors.cyan(server.url.padEnd(37))}│
|
|
3160
|
-
│ Mode: ${colors.yellow((
|
|
3161
|
-
│ Delay: ${colors.gray((
|
|
3923
|
+
│ Mode: ${colors.yellow((echo ? 'Echo' : 'Default').padEnd(36))}│
|
|
3924
|
+
│ Delay: ${colors.gray((delay + 'ms').padEnd(35))}│
|
|
3162
3925
|
├─────────────────────────────────────────────┤
|
|
3163
3926
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
3164
3927
|
└─────────────────────────────────────────────┘
|
|
@@ -3176,15 +3939,18 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
3176
3939
|
.command('webhook')
|
|
3177
3940
|
.alias('wh')
|
|
3178
3941
|
.description('Start a webhook receiver server')
|
|
3179
|
-
.
|
|
3180
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3181
|
-
.option('-s, --status <code>', 'Response status code (200 or 204)', '204')
|
|
3182
|
-
.option('-q, --quiet', 'Disable logging', false)
|
|
3942
|
+
.argument('[args...]', 'Options: port=3000 host=127.0.0.1 status=204 quiet')
|
|
3183
3943
|
.addHelpText('after', `
|
|
3944
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
3945
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 3000)
|
|
3946
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
3947
|
+
${colors.cyan('status=<code>')} Response status code 200 or 204 (default: 204)
|
|
3948
|
+
${colors.cyan('quiet')} Disable logging
|
|
3949
|
+
|
|
3184
3950
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3185
3951
|
${colors.green('$ rek serve webhook')} ${colors.gray('Start on port 3000')}
|
|
3186
|
-
${colors.green('$ rek serve wh
|
|
3187
|
-
${colors.green('$ rek serve webhook
|
|
3952
|
+
${colors.green('$ rek serve wh port=8080')} ${colors.gray('Start on port 8080')}
|
|
3953
|
+
${colors.green('$ rek serve webhook status=200')} ${colors.gray('Return 200 instead of 204')}
|
|
3188
3954
|
|
|
3189
3955
|
${colors.bold(colors.yellow('Endpoints:'))}
|
|
3190
3956
|
* / ${colors.gray('Receive webhook without ID')}
|
|
@@ -3193,18 +3959,32 @@ ${colors.bold(colors.yellow('Endpoints:'))}
|
|
|
3193
3959
|
${colors.bold(colors.yellow('Methods:'))}
|
|
3194
3960
|
${colors.gray('GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS')}
|
|
3195
3961
|
`)
|
|
3196
|
-
.action(async (
|
|
3962
|
+
.action(async (args) => {
|
|
3963
|
+
let port = 3000;
|
|
3964
|
+
let host = '127.0.0.1';
|
|
3965
|
+
let statusCode = 204;
|
|
3966
|
+
let quiet = false;
|
|
3967
|
+
for (const arg of args) {
|
|
3968
|
+
if (arg.startsWith('port='))
|
|
3969
|
+
port = parseInt(arg.split('=')[1]);
|
|
3970
|
+
else if (arg.startsWith('host='))
|
|
3971
|
+
host = arg.split('=')[1];
|
|
3972
|
+
else if (arg.startsWith('status='))
|
|
3973
|
+
statusCode = parseInt(arg.split('=')[1]);
|
|
3974
|
+
else if (arg === 'quiet')
|
|
3975
|
+
quiet = true;
|
|
3976
|
+
}
|
|
3197
3977
|
const { createWebhookServer } = await import('../testing/mock-http-server.js');
|
|
3198
|
-
const status =
|
|
3978
|
+
const status = statusCode;
|
|
3199
3979
|
if (status !== 200 && status !== 204) {
|
|
3200
3980
|
console.error(colors.red('Status must be 200 or 204'));
|
|
3201
3981
|
process.exit(1);
|
|
3202
3982
|
}
|
|
3203
3983
|
const server = await createWebhookServer({
|
|
3204
|
-
port
|
|
3205
|
-
host
|
|
3984
|
+
port,
|
|
3985
|
+
host,
|
|
3206
3986
|
status,
|
|
3207
|
-
log: !
|
|
3987
|
+
log: !quiet,
|
|
3208
3988
|
});
|
|
3209
3989
|
console.log(colors.green(`
|
|
3210
3990
|
┌─────────────────────────────────────────────┐
|
|
@@ -3230,32 +4010,51 @@ ${colors.bold(colors.yellow('Methods:'))}
|
|
|
3230
4010
|
.command('websocket')
|
|
3231
4011
|
.alias('ws')
|
|
3232
4012
|
.description('Start a mock WebSocket server')
|
|
3233
|
-
.
|
|
3234
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3235
|
-
.option('--echo', 'Echo messages back (default: true)', true)
|
|
3236
|
-
.option('--no-echo', 'Disable echo mode')
|
|
3237
|
-
.option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
|
|
4013
|
+
.argument('[args...]', 'Options: port=8080 host=127.0.0.1 echo noecho delay=0')
|
|
3238
4014
|
.addHelpText('after', `
|
|
4015
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4016
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 8080)
|
|
4017
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
4018
|
+
${colors.cyan('echo')} Echo messages back (default)
|
|
4019
|
+
${colors.cyan('noecho')} Disable echo mode
|
|
4020
|
+
${colors.cyan('delay=<ms>')} Add delay to responses in ms (default: 0)
|
|
4021
|
+
|
|
3239
4022
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3240
4023
|
${colors.green('$ rek serve websocket')} ${colors.gray('Start on port 8080')}
|
|
3241
|
-
${colors.green('$ rek serve ws
|
|
3242
|
-
${colors.green('$ rek serve ws
|
|
4024
|
+
${colors.green('$ rek serve ws port=9000')} ${colors.gray('Start on port 9000')}
|
|
4025
|
+
${colors.green('$ rek serve ws noecho')} ${colors.gray('Disable echo')}
|
|
3243
4026
|
`)
|
|
3244
|
-
.action(async (
|
|
4027
|
+
.action(async (args) => {
|
|
4028
|
+
let port = 8080;
|
|
4029
|
+
let host = '127.0.0.1';
|
|
4030
|
+
let echo = true;
|
|
4031
|
+
let delay = 0;
|
|
4032
|
+
for (const arg of args) {
|
|
4033
|
+
if (arg.startsWith('port='))
|
|
4034
|
+
port = parseInt(arg.split('=')[1]);
|
|
4035
|
+
else if (arg.startsWith('host='))
|
|
4036
|
+
host = arg.split('=')[1];
|
|
4037
|
+
else if (arg === 'echo')
|
|
4038
|
+
echo = true;
|
|
4039
|
+
else if (arg === 'noecho')
|
|
4040
|
+
echo = false;
|
|
4041
|
+
else if (arg.startsWith('delay='))
|
|
4042
|
+
delay = parseInt(arg.split('=')[1]);
|
|
4043
|
+
}
|
|
3245
4044
|
const { MockWebSocketServer } = await import('../testing/mock-websocket-server.js');
|
|
3246
4045
|
const server = await MockWebSocketServer.create({
|
|
3247
|
-
port
|
|
3248
|
-
host
|
|
3249
|
-
echo
|
|
3250
|
-
delay
|
|
4046
|
+
port,
|
|
4047
|
+
host,
|
|
4048
|
+
echo,
|
|
4049
|
+
delay,
|
|
3251
4050
|
});
|
|
3252
4051
|
console.log(colors.green(`
|
|
3253
4052
|
┌─────────────────────────────────────────────┐
|
|
3254
4053
|
│ ${colors.bold('Recker Mock WebSocket Server')} │
|
|
3255
4054
|
├─────────────────────────────────────────────┤
|
|
3256
4055
|
│ URL: ${colors.cyan(server.url.padEnd(37))}│
|
|
3257
|
-
│ Echo: ${colors.yellow((
|
|
3258
|
-
│ Delay: ${colors.gray((
|
|
4056
|
+
│ Echo: ${colors.yellow((echo ? 'Enabled' : 'Disabled').padEnd(36))}│
|
|
4057
|
+
│ Delay: ${colors.gray((delay + 'ms').padEnd(35))}│
|
|
3259
4058
|
├─────────────────────────────────────────────┤
|
|
3260
4059
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
3261
4060
|
└─────────────────────────────────────────────┘
|
|
@@ -3279,36 +4078,53 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
3279
4078
|
serve
|
|
3280
4079
|
.command('sse')
|
|
3281
4080
|
.description('Start a mock SSE (Server-Sent Events) server')
|
|
3282
|
-
.
|
|
3283
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3284
|
-
.option('--path <string>', 'SSE endpoint path', '/events')
|
|
3285
|
-
.option('--heartbeat <ms>', 'Send heartbeat every N ms (0 = disabled)', '0')
|
|
4081
|
+
.argument('[args...]', 'Options: port=8081 host=127.0.0.1 path=/events heartbeat=0')
|
|
3286
4082
|
.addHelpText('after', `
|
|
4083
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4084
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 8081)
|
|
4085
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
4086
|
+
${colors.cyan('path=<string>')} SSE endpoint path (default: /events)
|
|
4087
|
+
${colors.cyan('heartbeat=<ms>')} Send heartbeat every N ms (0 = disabled)
|
|
4088
|
+
|
|
3287
4089
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3288
4090
|
${colors.green('$ rek serve sse')} ${colors.gray('Start on port 8081')}
|
|
3289
|
-
${colors.green('$ rek serve sse
|
|
3290
|
-
${colors.green('$ rek serve sse
|
|
4091
|
+
${colors.green('$ rek serve sse port=9000')} ${colors.gray('Start on port 9000')}
|
|
4092
|
+
${colors.green('$ rek serve sse heartbeat=5000')} ${colors.gray('Send heartbeat every 5s')}
|
|
3291
4093
|
|
|
3292
4094
|
${colors.bold(colors.yellow('Interactive Commands:'))}
|
|
3293
4095
|
Type a message and press Enter to broadcast it to all clients.
|
|
3294
4096
|
`)
|
|
3295
|
-
.action(async (
|
|
4097
|
+
.action(async (args) => {
|
|
4098
|
+
let port = 8081;
|
|
4099
|
+
let host = '127.0.0.1';
|
|
4100
|
+
let path = '/events';
|
|
4101
|
+
let heartbeat = 0;
|
|
4102
|
+
for (const arg of args) {
|
|
4103
|
+
if (arg.startsWith('port='))
|
|
4104
|
+
port = parseInt(arg.split('=')[1]);
|
|
4105
|
+
else if (arg.startsWith('host='))
|
|
4106
|
+
host = arg.split('=')[1];
|
|
4107
|
+
else if (arg.startsWith('path='))
|
|
4108
|
+
path = arg.split('=')[1];
|
|
4109
|
+
else if (arg.startsWith('heartbeat='))
|
|
4110
|
+
heartbeat = parseInt(arg.split('=')[1]);
|
|
4111
|
+
}
|
|
3296
4112
|
const { MockSSEServer } = await import('../testing/mock-sse-server.js');
|
|
3297
4113
|
const readline = await import('node:readline');
|
|
3298
4114
|
const server = await MockSSEServer.create({
|
|
3299
|
-
port
|
|
3300
|
-
host
|
|
3301
|
-
path
|
|
4115
|
+
port,
|
|
4116
|
+
host,
|
|
4117
|
+
path,
|
|
3302
4118
|
});
|
|
3303
|
-
if (
|
|
3304
|
-
server.startPeriodicEvents('heartbeat',
|
|
4119
|
+
if (heartbeat > 0) {
|
|
4120
|
+
server.startPeriodicEvents('heartbeat', heartbeat);
|
|
3305
4121
|
}
|
|
3306
4122
|
console.log(colors.green(`
|
|
3307
4123
|
┌─────────────────────────────────────────────┐
|
|
3308
4124
|
│ ${colors.bold('Recker Mock SSE Server')} │
|
|
3309
4125
|
├─────────────────────────────────────────────┤
|
|
3310
4126
|
│ URL: ${colors.cyan(server.url.padEnd(37))}│
|
|
3311
|
-
│ Heartbeat: ${colors.yellow((
|
|
4127
|
+
│ Heartbeat: ${colors.yellow((heartbeat === 0 ? 'Disabled' : heartbeat + 'ms').padEnd(31))}│
|
|
3312
4128
|
├─────────────────────────────────────────────┤
|
|
3313
4129
|
│ Type message + Enter to broadcast │
|
|
3314
4130
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
@@ -3340,30 +4156,51 @@ ${colors.bold(colors.yellow('Interactive Commands:'))}
|
|
|
3340
4156
|
serve
|
|
3341
4157
|
.command('hls')
|
|
3342
4158
|
.description('Start a mock HLS streaming server')
|
|
3343
|
-
.
|
|
3344
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3345
|
-
.option('--mode <type>', 'Stream mode: vod, live, event', 'vod')
|
|
3346
|
-
.option('--segments <number>', 'Number of segments (VOD mode)', '10')
|
|
3347
|
-
.option('--duration <seconds>', 'Segment duration in seconds', '6')
|
|
3348
|
-
.option('--qualities <list>', 'Comma-separated quality variants', '720p,480p,360p')
|
|
4159
|
+
.argument('[args...]', 'Options: port=8082 host=127.0.0.1 mode=vod segments=10 duration=6 qualities=720p,480p,360p')
|
|
3349
4160
|
.addHelpText('after', `
|
|
4161
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4162
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 8082)
|
|
4163
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
4164
|
+
${colors.cyan('mode=<type>')} Stream mode: vod, live, event (default: vod)
|
|
4165
|
+
${colors.cyan('segments=<n>')} Number of segments for VOD (default: 10)
|
|
4166
|
+
${colors.cyan('duration=<sec>')} Segment duration in seconds (default: 6)
|
|
4167
|
+
${colors.cyan('qualities=<list>')} Comma-separated quality variants (default: 720p,480p,360p)
|
|
4168
|
+
|
|
3350
4169
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3351
4170
|
${colors.green('$ rek serve hls')} ${colors.gray('Start VOD server')}
|
|
3352
|
-
${colors.green('$ rek serve hls
|
|
3353
|
-
${colors.green('$ rek serve hls
|
|
3354
|
-
${colors.green('$ rek serve hls
|
|
4171
|
+
${colors.green('$ rek serve hls mode=live')} ${colors.gray('Start live stream')}
|
|
4172
|
+
${colors.green('$ rek serve hls segments=20')} ${colors.gray('VOD with 20 segments')}
|
|
4173
|
+
${colors.green('$ rek serve hls qualities=1080p,720p,480p')}
|
|
3355
4174
|
|
|
3356
4175
|
${colors.bold(colors.yellow('Endpoints:'))}
|
|
3357
4176
|
${colors.cyan('/master.m3u8')} Master playlist (multi-quality)
|
|
3358
4177
|
${colors.cyan('/playlist.m3u8')} Single quality playlist
|
|
3359
4178
|
${colors.cyan('/<quality>/playlist.m3u8')} Quality-specific playlist
|
|
3360
4179
|
`)
|
|
3361
|
-
.action(async (
|
|
4180
|
+
.action(async (args) => {
|
|
4181
|
+
let port = 8082;
|
|
4182
|
+
let host = '127.0.0.1';
|
|
4183
|
+
let mode = 'vod';
|
|
4184
|
+
let segments = 10;
|
|
4185
|
+
let duration = 6;
|
|
4186
|
+
let qualitiesStr = '720p,480p,360p';
|
|
4187
|
+
for (const arg of args) {
|
|
4188
|
+
if (arg.startsWith('port='))
|
|
4189
|
+
port = parseInt(arg.split('=')[1]);
|
|
4190
|
+
else if (arg.startsWith('host='))
|
|
4191
|
+
host = arg.split('=')[1];
|
|
4192
|
+
else if (arg.startsWith('mode='))
|
|
4193
|
+
mode = arg.split('=')[1];
|
|
4194
|
+
else if (arg.startsWith('segments='))
|
|
4195
|
+
segments = parseInt(arg.split('=')[1]);
|
|
4196
|
+
else if (arg.startsWith('duration='))
|
|
4197
|
+
duration = parseInt(arg.split('=')[1]);
|
|
4198
|
+
else if (arg.startsWith('qualities='))
|
|
4199
|
+
qualitiesStr = arg.split('=')[1];
|
|
4200
|
+
}
|
|
3362
4201
|
const { MockHlsServer } = await import('../testing/mock-hls-server.js');
|
|
3363
4202
|
const http = await import('node:http');
|
|
3364
|
-
const
|
|
3365
|
-
const host = options.host;
|
|
3366
|
-
const qualities = options.qualities.split(',').map(q => q.trim());
|
|
4203
|
+
const qualities = qualitiesStr.split(',').map(q => q.trim());
|
|
3367
4204
|
const resolutions = ['1920x1080', '1280x720', '854x480', '640x360', '426x240'];
|
|
3368
4205
|
const bandwidths = [5000000, 2500000, 1400000, 800000, 500000];
|
|
3369
4206
|
const variants = qualities.map((name, i) => ({
|
|
@@ -3374,9 +4211,9 @@ ${colors.bold(colors.yellow('Endpoints:'))}
|
|
|
3374
4211
|
const baseUrl = `http://${host}:${port}`;
|
|
3375
4212
|
const hlsServer = await MockHlsServer.create({
|
|
3376
4213
|
baseUrl,
|
|
3377
|
-
mode:
|
|
3378
|
-
segmentCount:
|
|
3379
|
-
segmentDuration:
|
|
4214
|
+
mode: mode,
|
|
4215
|
+
segmentCount: segments,
|
|
4216
|
+
segmentDuration: duration,
|
|
3380
4217
|
multiQuality: variants.length > 1,
|
|
3381
4218
|
variants: variants.length > 1 ? variants : undefined,
|
|
3382
4219
|
});
|
|
@@ -3402,9 +4239,9 @@ ${colors.bold(colors.yellow('Endpoints:'))}
|
|
|
3402
4239
|
│ ${colors.bold('Recker Mock HLS Server')} │
|
|
3403
4240
|
├─────────────────────────────────────────────┤
|
|
3404
4241
|
│ Master: ${colors.cyan((hlsServer.manifestUrl).padEnd(34))}│
|
|
3405
|
-
│ Mode: ${colors.yellow(
|
|
3406
|
-
│ Segments: ${colors.gray(
|
|
3407
|
-
│ Duration: ${colors.gray((
|
|
4242
|
+
│ Mode: ${colors.yellow(mode.padEnd(36))}│
|
|
4243
|
+
│ Segments: ${colors.gray(String(segments).padEnd(32))}│
|
|
4244
|
+
│ Duration: ${colors.gray((duration + 's').padEnd(32))}│
|
|
3408
4245
|
│ Qualities: ${colors.cyan(qualities.join(', ').padEnd(31))}│
|
|
3409
4246
|
├─────────────────────────────────────────────┤
|
|
3410
4247
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
@@ -3421,30 +4258,46 @@ ${colors.bold(colors.yellow('Endpoints:'))}
|
|
|
3421
4258
|
serve
|
|
3422
4259
|
.command('udp')
|
|
3423
4260
|
.description('Start a mock UDP server')
|
|
3424
|
-
.
|
|
3425
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3426
|
-
.option('--echo', 'Echo messages back (default: true)', true)
|
|
3427
|
-
.option('--no-echo', 'Disable echo mode')
|
|
4261
|
+
.argument('[args...]', 'Options: port=9000 host=127.0.0.1 echo noecho')
|
|
3428
4262
|
.addHelpText('after', `
|
|
4263
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4264
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 9000)
|
|
4265
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
4266
|
+
${colors.cyan('echo')} Echo messages back (default)
|
|
4267
|
+
${colors.cyan('noecho')} Disable echo mode
|
|
4268
|
+
|
|
3429
4269
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3430
4270
|
${colors.green('$ rek serve udp')} ${colors.gray('Start on port 9000')}
|
|
3431
|
-
${colors.green('$ rek serve udp
|
|
3432
|
-
${colors.green('$ rek serve udp
|
|
4271
|
+
${colors.green('$ rek serve udp port=5353')} ${colors.gray('Start on port 5353')}
|
|
4272
|
+
${colors.green('$ rek serve udp noecho')} ${colors.gray('Disable echo')}
|
|
3433
4273
|
`)
|
|
3434
|
-
.action(async (
|
|
4274
|
+
.action(async (args) => {
|
|
4275
|
+
let port = 9000;
|
|
4276
|
+
let host = '127.0.0.1';
|
|
4277
|
+
let echo = true;
|
|
4278
|
+
for (const arg of args) {
|
|
4279
|
+
if (arg.startsWith('port='))
|
|
4280
|
+
port = parseInt(arg.split('=')[1]);
|
|
4281
|
+
else if (arg.startsWith('host='))
|
|
4282
|
+
host = arg.split('=')[1];
|
|
4283
|
+
else if (arg === 'echo')
|
|
4284
|
+
echo = true;
|
|
4285
|
+
else if (arg === 'noecho')
|
|
4286
|
+
echo = false;
|
|
4287
|
+
}
|
|
3435
4288
|
const { MockUDPServer } = await import('../testing/mock-udp-server.js');
|
|
3436
4289
|
const server = new MockUDPServer({
|
|
3437
|
-
port
|
|
3438
|
-
host
|
|
3439
|
-
echo
|
|
4290
|
+
port,
|
|
4291
|
+
host,
|
|
4292
|
+
echo,
|
|
3440
4293
|
});
|
|
3441
4294
|
await server.start();
|
|
3442
4295
|
console.log(colors.green(`
|
|
3443
4296
|
┌─────────────────────────────────────────────┐
|
|
3444
4297
|
│ ${colors.bold('Recker Mock UDP Server')} │
|
|
3445
4298
|
├─────────────────────────────────────────────┤
|
|
3446
|
-
│ Address: ${colors.cyan(`${
|
|
3447
|
-
│ Echo: ${colors.yellow((
|
|
4299
|
+
│ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
|
|
4300
|
+
│ Echo: ${colors.yellow((echo ? 'Enabled' : 'Disabled').padEnd(36))}│
|
|
3448
4301
|
├─────────────────────────────────────────────┤
|
|
3449
4302
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
3450
4303
|
└─────────────────────────────────────────────┘
|
|
@@ -3462,13 +4315,16 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
3462
4315
|
serve
|
|
3463
4316
|
.command('dns')
|
|
3464
4317
|
.description('Start a mock DNS server')
|
|
3465
|
-
.
|
|
3466
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3467
|
-
.option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
|
|
4318
|
+
.argument('[args...]', 'Options: port=5353 host=127.0.0.1 delay=0')
|
|
3468
4319
|
.addHelpText('after', `
|
|
4320
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4321
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 5353)
|
|
4322
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
4323
|
+
${colors.cyan('delay=<ms>')} Add delay to responses in ms (default: 0)
|
|
4324
|
+
|
|
3469
4325
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3470
4326
|
${colors.green('$ rek serve dns')} ${colors.gray('Start on port 5353')}
|
|
3471
|
-
${colors.green('$ rek serve dns
|
|
4327
|
+
${colors.green('$ rek serve dns port=53')} ${colors.gray('Start on standard port (requires root)')}
|
|
3472
4328
|
${colors.green('$ dig @127.0.0.1 -p 5353 example.com')} ${colors.gray('Test with dig')}
|
|
3473
4329
|
|
|
3474
4330
|
${colors.bold(colors.yellow('Default Records:'))}
|
|
@@ -3476,21 +4332,32 @@ ${colors.bold(colors.yellow('Default Records:'))}
|
|
|
3476
4332
|
${colors.cyan('example.com')} A, AAAA, NS, MX, TXT records
|
|
3477
4333
|
${colors.cyan('test.local')} A: 192.168.1.100
|
|
3478
4334
|
`)
|
|
3479
|
-
.action(async (
|
|
4335
|
+
.action(async (args) => {
|
|
4336
|
+
let port = 5353;
|
|
4337
|
+
let host = '127.0.0.1';
|
|
4338
|
+
let delay = 0;
|
|
4339
|
+
for (const arg of args) {
|
|
4340
|
+
if (arg.startsWith('port='))
|
|
4341
|
+
port = parseInt(arg.split('=')[1]);
|
|
4342
|
+
else if (arg.startsWith('host='))
|
|
4343
|
+
host = arg.split('=')[1];
|
|
4344
|
+
else if (arg.startsWith('delay='))
|
|
4345
|
+
delay = parseInt(arg.split('=')[1]);
|
|
4346
|
+
}
|
|
3480
4347
|
const { MockDnsServer } = await import('../testing/mock-dns-server.js');
|
|
3481
4348
|
const server = await MockDnsServer.create({
|
|
3482
|
-
port
|
|
3483
|
-
host
|
|
3484
|
-
delay
|
|
4349
|
+
port,
|
|
4350
|
+
host,
|
|
4351
|
+
delay,
|
|
3485
4352
|
});
|
|
3486
4353
|
console.log(colors.green(`
|
|
3487
4354
|
┌─────────────────────────────────────────────┐
|
|
3488
4355
|
│ ${colors.bold('Recker Mock DNS Server')} │
|
|
3489
4356
|
├─────────────────────────────────────────────┤
|
|
3490
|
-
│ Address: ${colors.cyan(`${
|
|
4357
|
+
│ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
|
|
3491
4358
|
│ Protocol: ${colors.yellow('UDP'.padEnd(32))}│
|
|
3492
4359
|
├─────────────────────────────────────────────┤
|
|
3493
|
-
│ Test: dig @${
|
|
4360
|
+
│ Test: dig @${host} -p ${port} example.com │
|
|
3494
4361
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
3495
4362
|
└─────────────────────────────────────────────┘
|
|
3496
4363
|
`));
|
|
@@ -3506,10 +4373,13 @@ ${colors.bold(colors.yellow('Default Records:'))}
|
|
|
3506
4373
|
serve
|
|
3507
4374
|
.command('whois')
|
|
3508
4375
|
.description('Start a mock WHOIS server')
|
|
3509
|
-
.
|
|
3510
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3511
|
-
.option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
|
|
4376
|
+
.argument('[args...]', 'Options: port=4343 host=127.0.0.1 delay=0')
|
|
3512
4377
|
.addHelpText('after', `
|
|
4378
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4379
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 4343)
|
|
4380
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
4381
|
+
${colors.cyan('delay=<ms>')} Add delay to responses in ms (default: 0)
|
|
4382
|
+
|
|
3513
4383
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3514
4384
|
${colors.green('$ rek serve whois')} ${colors.gray('Start on port 4343')}
|
|
3515
4385
|
${colors.green('$ whois -h 127.0.0.1 -p 4343 example.com')} ${colors.gray('Test with whois')}
|
|
@@ -3519,21 +4389,32 @@ ${colors.bold(colors.yellow('Default Domains:'))}
|
|
|
3519
4389
|
${colors.cyan('google.com')} MarkMonitor registrar
|
|
3520
4390
|
${colors.cyan('test.local')} Test domain
|
|
3521
4391
|
`)
|
|
3522
|
-
.action(async (
|
|
4392
|
+
.action(async (args) => {
|
|
4393
|
+
let port = 4343;
|
|
4394
|
+
let host = '127.0.0.1';
|
|
4395
|
+
let delay = 0;
|
|
4396
|
+
for (const arg of args) {
|
|
4397
|
+
if (arg.startsWith('port='))
|
|
4398
|
+
port = parseInt(arg.split('=')[1]);
|
|
4399
|
+
else if (arg.startsWith('host='))
|
|
4400
|
+
host = arg.split('=')[1];
|
|
4401
|
+
else if (arg.startsWith('delay='))
|
|
4402
|
+
delay = parseInt(arg.split('=')[1]);
|
|
4403
|
+
}
|
|
3523
4404
|
const { MockWhoisServer } = await import('../testing/mock-whois-server.js');
|
|
3524
4405
|
const server = await MockWhoisServer.create({
|
|
3525
|
-
port
|
|
3526
|
-
host
|
|
3527
|
-
delay
|
|
4406
|
+
port,
|
|
4407
|
+
host,
|
|
4408
|
+
delay,
|
|
3528
4409
|
});
|
|
3529
4410
|
console.log(colors.green(`
|
|
3530
4411
|
┌─────────────────────────────────────────────┐
|
|
3531
4412
|
│ ${colors.bold('Recker Mock WHOIS Server')} │
|
|
3532
4413
|
├─────────────────────────────────────────────┤
|
|
3533
|
-
│ Address: ${colors.cyan(`${
|
|
4414
|
+
│ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
|
|
3534
4415
|
│ Protocol: ${colors.yellow('TCP'.padEnd(32))}│
|
|
3535
4416
|
├─────────────────────────────────────────────┤
|
|
3536
|
-
│ Test: whois -h ${
|
|
4417
|
+
│ Test: whois -h ${host} -p ${port} example.com │
|
|
3537
4418
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
3538
4419
|
└─────────────────────────────────────────────┘
|
|
3539
4420
|
`));
|
|
@@ -3549,12 +4430,15 @@ ${colors.bold(colors.yellow('Default Domains:'))}
|
|
|
3549
4430
|
serve
|
|
3550
4431
|
.command('telnet')
|
|
3551
4432
|
.description('Start a mock Telnet server')
|
|
3552
|
-
.
|
|
3553
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3554
|
-
.option('--echo', 'Echo input back (default: true)', true)
|
|
3555
|
-
.option('--no-echo', 'Disable echo mode')
|
|
3556
|
-
.option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
|
|
4433
|
+
.argument('[args...]', 'Options: port=2323 host=127.0.0.1 echo noecho delay=0')
|
|
3557
4434
|
.addHelpText('after', `
|
|
4435
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4436
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 2323)
|
|
4437
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
4438
|
+
${colors.cyan('echo')} Echo input back (default)
|
|
4439
|
+
${colors.cyan('noecho')} Disable echo mode
|
|
4440
|
+
${colors.cyan('delay=<ms>')} Add delay to responses in ms (default: 0)
|
|
4441
|
+
|
|
3558
4442
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3559
4443
|
${colors.green('$ rek serve telnet')} ${colors.gray('Start on port 2323')}
|
|
3560
4444
|
${colors.green('$ telnet localhost 2323')} ${colors.gray('Connect to server')}
|
|
@@ -3567,22 +4451,38 @@ ${colors.bold(colors.yellow('Built-in Commands:'))}
|
|
|
3567
4451
|
${colors.cyan('ping')} Returns "pong"
|
|
3568
4452
|
${colors.cyan('quit')} Disconnect
|
|
3569
4453
|
`)
|
|
3570
|
-
.action(async (
|
|
4454
|
+
.action(async (args) => {
|
|
4455
|
+
let port = 2323;
|
|
4456
|
+
let host = '127.0.0.1';
|
|
4457
|
+
let echo = true;
|
|
4458
|
+
let delay = 0;
|
|
4459
|
+
for (const arg of args) {
|
|
4460
|
+
if (arg.startsWith('port='))
|
|
4461
|
+
port = parseInt(arg.split('=')[1]);
|
|
4462
|
+
else if (arg.startsWith('host='))
|
|
4463
|
+
host = arg.split('=')[1];
|
|
4464
|
+
else if (arg === 'echo')
|
|
4465
|
+
echo = true;
|
|
4466
|
+
else if (arg === 'noecho')
|
|
4467
|
+
echo = false;
|
|
4468
|
+
else if (arg.startsWith('delay='))
|
|
4469
|
+
delay = parseInt(arg.split('=')[1]);
|
|
4470
|
+
}
|
|
3571
4471
|
const { MockTelnetServer } = await import('../testing/mock-telnet-server.js');
|
|
3572
4472
|
const server = await MockTelnetServer.create({
|
|
3573
|
-
port
|
|
3574
|
-
host
|
|
3575
|
-
echo
|
|
3576
|
-
delay
|
|
4473
|
+
port,
|
|
4474
|
+
host,
|
|
4475
|
+
echo,
|
|
4476
|
+
delay,
|
|
3577
4477
|
});
|
|
3578
4478
|
console.log(colors.green(`
|
|
3579
4479
|
┌─────────────────────────────────────────────┐
|
|
3580
4480
|
│ ${colors.bold('Recker Mock Telnet Server')} │
|
|
3581
4481
|
├─────────────────────────────────────────────┤
|
|
3582
|
-
│ Address: ${colors.cyan(`${
|
|
3583
|
-
│ Echo: ${colors.yellow((
|
|
4482
|
+
│ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
|
|
4483
|
+
│ Echo: ${colors.yellow((echo ? 'Enabled' : 'Disabled').padEnd(36))}│
|
|
3584
4484
|
├─────────────────────────────────────────────┤
|
|
3585
|
-
│ Connect: telnet ${
|
|
4485
|
+
│ Connect: telnet ${host} ${port} │
|
|
3586
4486
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
3587
4487
|
└─────────────────────────────────────────────┘
|
|
3588
4488
|
`));
|
|
@@ -3604,18 +4504,21 @@ ${colors.bold(colors.yellow('Built-in Commands:'))}
|
|
|
3604
4504
|
serve
|
|
3605
4505
|
.command('ftp')
|
|
3606
4506
|
.description('Start a mock FTP server')
|
|
3607
|
-
.
|
|
3608
|
-
.option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
|
|
3609
|
-
.option('-u, --username <user>', 'Username for auth', 'user')
|
|
3610
|
-
.option('--password <pass>', 'Password for auth', 'pass')
|
|
3611
|
-
.option('--anonymous', 'Allow anonymous login (default: true)', true)
|
|
3612
|
-
.option('--no-anonymous', 'Disable anonymous login')
|
|
3613
|
-
.option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
|
|
4507
|
+
.argument('[args...]', 'Options: port=2121 host=127.0.0.1 username=user password=pass anonymous noanonymous delay=0')
|
|
3614
4508
|
.addHelpText('after', `
|
|
4509
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4510
|
+
${colors.cyan('port=<number>')} Port to listen on (default: 2121)
|
|
4511
|
+
${colors.cyan('host=<string>')} Host to bind to (default: 127.0.0.1)
|
|
4512
|
+
${colors.cyan('username=<user>')} Username for auth (default: user)
|
|
4513
|
+
${colors.cyan('password=<pass>')} Password for auth (default: pass)
|
|
4514
|
+
${colors.cyan('anonymous')} Allow anonymous login (default)
|
|
4515
|
+
${colors.cyan('noanonymous')} Disable anonymous login
|
|
4516
|
+
${colors.cyan('delay=<ms>')} Add delay to responses (default: 0)
|
|
4517
|
+
|
|
3615
4518
|
${colors.bold(colors.yellow('Examples:'))}
|
|
3616
4519
|
${colors.green('$ rek serve ftp')} ${colors.gray('Start on port 2121')}
|
|
3617
4520
|
${colors.green('$ ftp localhost 2121')} ${colors.gray('Connect to server')}
|
|
3618
|
-
${colors.green('$ rek serve ftp
|
|
4521
|
+
${colors.green('$ rek serve ftp noanonymous')} ${colors.gray('Require authentication')}
|
|
3619
4522
|
|
|
3620
4523
|
${colors.bold(colors.yellow('Default Files:'))}
|
|
3621
4524
|
${colors.cyan('/welcome.txt')} Welcome message
|
|
@@ -3627,25 +4530,47 @@ ${colors.bold(colors.yellow('Credentials:'))}
|
|
|
3627
4530
|
Username: ${colors.cyan('user')} Password: ${colors.cyan('pass')}
|
|
3628
4531
|
Or use anonymous login with user: ${colors.cyan('anonymous')}
|
|
3629
4532
|
`)
|
|
3630
|
-
.action(async (
|
|
4533
|
+
.action(async (args) => {
|
|
3631
4534
|
const { MockFtpServer } = await import('../testing/mock-ftp-server.js');
|
|
4535
|
+
let port = 2121;
|
|
4536
|
+
let host = '127.0.0.1';
|
|
4537
|
+
let username = 'user';
|
|
4538
|
+
let password = 'pass';
|
|
4539
|
+
let anonymous = true;
|
|
4540
|
+
let delay = 0;
|
|
4541
|
+
for (const arg of args) {
|
|
4542
|
+
if (arg.startsWith('port='))
|
|
4543
|
+
port = parseInt(arg.split('=')[1]);
|
|
4544
|
+
else if (arg.startsWith('host='))
|
|
4545
|
+
host = arg.split('=')[1];
|
|
4546
|
+
else if (arg.startsWith('username='))
|
|
4547
|
+
username = arg.split('=')[1];
|
|
4548
|
+
else if (arg.startsWith('password='))
|
|
4549
|
+
password = arg.split('=')[1];
|
|
4550
|
+
else if (arg === 'anonymous')
|
|
4551
|
+
anonymous = true;
|
|
4552
|
+
else if (arg === 'noanonymous')
|
|
4553
|
+
anonymous = false;
|
|
4554
|
+
else if (arg.startsWith('delay='))
|
|
4555
|
+
delay = parseInt(arg.split('=')[1]);
|
|
4556
|
+
}
|
|
3632
4557
|
const server = await MockFtpServer.create({
|
|
3633
|
-
port
|
|
3634
|
-
host
|
|
3635
|
-
username
|
|
3636
|
-
password
|
|
3637
|
-
anonymous
|
|
3638
|
-
delay
|
|
4558
|
+
port,
|
|
4559
|
+
host,
|
|
4560
|
+
username,
|
|
4561
|
+
password,
|
|
4562
|
+
anonymous,
|
|
4563
|
+
delay,
|
|
3639
4564
|
});
|
|
3640
4565
|
console.log(colors.green(`
|
|
3641
4566
|
┌─────────────────────────────────────────────┐
|
|
3642
4567
|
│ ${colors.bold('Recker Mock FTP Server')} │
|
|
3643
4568
|
├─────────────────────────────────────────────┤
|
|
3644
|
-
│ Address: ${colors.cyan(`${
|
|
3645
|
-
│ Anonymous: ${colors.yellow((
|
|
3646
|
-
│ User: ${colors.cyan(
|
|
4569
|
+
│ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
|
|
4570
|
+
│ Anonymous: ${colors.yellow((anonymous ? 'Allowed' : 'Disabled').padEnd(31))}│
|
|
4571
|
+
│ User: ${colors.cyan(username.padEnd(36))}│
|
|
3647
4572
|
├─────────────────────────────────────────────┤
|
|
3648
|
-
│ Connect: ftp ${
|
|
4573
|
+
│ Connect: ftp ${host} ${port} │
|
|
3649
4574
|
│ Press ${colors.bold('Ctrl+C')} to stop │
|
|
3650
4575
|
└─────────────────────────────────────────────┘
|
|
3651
4576
|
`));
|
|
@@ -3667,22 +4592,25 @@ ${colors.bold(colors.yellow('Credentials:'))}
|
|
|
3667
4592
|
program
|
|
3668
4593
|
.command('mcp')
|
|
3669
4594
|
.description('Start MCP server for AI agents to access Recker documentation')
|
|
3670
|
-
.
|
|
3671
|
-
.option('-p, --port <number>', 'Server port (for http/sse modes)', '3100')
|
|
3672
|
-
.option('-d, --docs <path>', 'Path to documentation folder')
|
|
3673
|
-
.option('-T, --tools <paths...>', 'Paths to external tool modules to load')
|
|
3674
|
-
.option('--debug', 'Enable debug logging')
|
|
4595
|
+
.argument('[args...]', 'Options: transport=stdio port=3100 docs=<path> tools=<paths> debug')
|
|
3675
4596
|
.addHelpText('after', `
|
|
4597
|
+
${colors.bold(colors.yellow('Options:'))}
|
|
4598
|
+
${colors.cyan('transport=<mode>')} Transport mode: stdio, http, sse (default: stdio)
|
|
4599
|
+
${colors.cyan('port=<number>')} Server port for http/sse modes (default: 3100)
|
|
4600
|
+
${colors.cyan('docs=<path>')} Path to documentation folder
|
|
4601
|
+
${colors.cyan('tools=<paths>')} Paths to external tool modules (comma-separated)
|
|
4602
|
+
${colors.cyan('debug')} Enable debug logging
|
|
4603
|
+
|
|
3676
4604
|
${colors.bold(colors.yellow('Transport Modes:'))}
|
|
3677
4605
|
${colors.cyan('stdio')} ${colors.gray('(default)')} For Claude Code and other CLI tools
|
|
3678
4606
|
${colors.cyan('http')} Simple HTTP POST endpoint
|
|
3679
4607
|
${colors.cyan('sse')} HTTP + Server-Sent Events for real-time notifications
|
|
3680
4608
|
|
|
3681
|
-
${colors.bold(colors.yellow('
|
|
3682
|
-
${colors.green('$ rek mcp')}
|
|
3683
|
-
${colors.green('$ rek mcp
|
|
3684
|
-
${colors.green('$ rek mcp
|
|
3685
|
-
${colors.green('$ rek mcp
|
|
4609
|
+
${colors.bold(colors.yellow('Examples:'))}
|
|
4610
|
+
${colors.green('$ rek mcp')} ${colors.gray('Start in stdio mode (for Claude Code)')}
|
|
4611
|
+
${colors.green('$ rek mcp transport=http')} ${colors.gray('Start HTTP server on port 3100')}
|
|
4612
|
+
${colors.green('$ rek mcp transport=sse port=8080')} ${colors.gray('Start SSE server on custom port')}
|
|
4613
|
+
${colors.green('$ rek mcp debug')} ${colors.gray('Enable debug logging')}
|
|
3686
4614
|
|
|
3687
4615
|
${colors.bold(colors.yellow('Tools provided:'))}
|
|
3688
4616
|
${colors.cyan('search_docs')} Search documentation by keyword
|
|
@@ -3698,15 +4626,31 @@ ${colors.bold(colors.yellow('Claude Code config (~/.claude.json):'))}
|
|
|
3698
4626
|
}
|
|
3699
4627
|
}`)}
|
|
3700
4628
|
`)
|
|
3701
|
-
.action(async (
|
|
4629
|
+
.action(async (args) => {
|
|
3702
4630
|
const { MCPServer } = await import('../mcp/server.js');
|
|
3703
|
-
|
|
4631
|
+
let transport = 'stdio';
|
|
4632
|
+
let port = 3100;
|
|
4633
|
+
let docsPath;
|
|
4634
|
+
let debug = false;
|
|
4635
|
+
let toolPaths;
|
|
4636
|
+
for (const arg of args) {
|
|
4637
|
+
if (arg.startsWith('transport='))
|
|
4638
|
+
transport = arg.split('=')[1];
|
|
4639
|
+
else if (arg.startsWith('port='))
|
|
4640
|
+
port = parseInt(arg.split('=')[1]);
|
|
4641
|
+
else if (arg.startsWith('docs='))
|
|
4642
|
+
docsPath = arg.split('=')[1];
|
|
4643
|
+
else if (arg.startsWith('tools='))
|
|
4644
|
+
toolPaths = arg.split('=')[1].split(',');
|
|
4645
|
+
else if (arg === 'debug')
|
|
4646
|
+
debug = true;
|
|
4647
|
+
}
|
|
3704
4648
|
const server = new MCPServer({
|
|
3705
4649
|
transport,
|
|
3706
|
-
port
|
|
3707
|
-
docsPath
|
|
3708
|
-
debug
|
|
3709
|
-
toolPaths
|
|
4650
|
+
port,
|
|
4651
|
+
docsPath,
|
|
4652
|
+
debug,
|
|
4653
|
+
toolPaths,
|
|
3710
4654
|
});
|
|
3711
4655
|
if (transport === 'stdio') {
|
|
3712
4656
|
await server.start();
|
|
@@ -3725,7 +4669,7 @@ ${colors.bold(colors.yellow('Claude Code config (~/.claude.json):'))}
|
|
|
3725
4669
|
│ ${colors.bold('Recker MCP Server')} │
|
|
3726
4670
|
├─────────────────────────────────────────────┤
|
|
3727
4671
|
│ Transport: ${colors.cyan(transport.padEnd(31))}│
|
|
3728
|
-
│ Endpoint: ${colors.cyan(`http://localhost:${
|
|
4672
|
+
│ Endpoint: ${colors.cyan(`http://localhost:${port}`.padEnd(32))}│
|
|
3729
4673
|
│ Docs indexed: ${colors.yellow(String(server.getDocsCount()).padEnd(28))}│
|
|
3730
4674
|
├─────────────────────────────────────────────┤${endpoints}
|
|
3731
4675
|
├─────────────────────────────────────────────┤
|
|
@@ -4401,21 +5345,39 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
4401
5345
|
});
|
|
4402
5346
|
program
|
|
4403
5347
|
.command('proxy')
|
|
4404
|
-
.description('
|
|
5348
|
+
.description('Route requests through HTTP or SOCKS proxy')
|
|
4405
5349
|
.argument('<proxy>', 'Proxy URL (http://host:port or socks5://host:port)')
|
|
4406
5350
|
.argument('<url>', 'Target URL')
|
|
4407
5351
|
.argument('[args...]', 'Request arguments: method=x key=value key:=json Header:value')
|
|
4408
5352
|
.addHelpText('after', `
|
|
4409
|
-
${colors.bold(colors.
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
5353
|
+
${colors.bold(colors.blue('What it does:'))}
|
|
5354
|
+
Routes HTTP requests through a proxy server. Supports HTTP, HTTPS, and SOCKS5
|
|
5355
|
+
proxies. Useful for bypassing geo-restrictions, debugging traffic, accessing
|
|
5356
|
+
internal networks, or anonymizing requests.
|
|
5357
|
+
|
|
5358
|
+
The proxy URL can include authentication (user:pass@host:port).
|
|
5359
|
+
All standard rek request options (headers, body, method) work normally.
|
|
5360
|
+
|
|
5361
|
+
${colors.bold(colors.yellow('Supported Proxy Types:'))}
|
|
5362
|
+
http://host:port HTTP proxy
|
|
5363
|
+
https://host:port HTTPS proxy
|
|
5364
|
+
socks5://host:port SOCKS5 proxy (Tor, SSH tunnels)
|
|
5365
|
+
|
|
5366
|
+
${colors.bold(colors.yellow('Request Syntax:'))}
|
|
5367
|
+
method=<method> HTTP method (default: GET)
|
|
5368
|
+
key=value String data (form/JSON body)
|
|
5369
|
+
key:=json JSON value (numbers, booleans, objects)
|
|
5370
|
+
Header:value HTTP headers
|
|
4414
5371
|
|
|
4415
5372
|
${colors.bold(colors.yellow('Examples:'))}
|
|
4416
|
-
${colors.green('$ rek proxy http://proxy.example.com:8080
|
|
4417
|
-
${colors.
|
|
4418
|
-
|
|
5373
|
+
${colors.green('$ rek proxy http://proxy.example.com:8080 api.com/data')}
|
|
5374
|
+
${colors.gray(' Simple GET through HTTP proxy')}
|
|
5375
|
+
|
|
5376
|
+
${colors.green('$ rek proxy socks5://127.0.0.1:9050 api.com/users')}
|
|
5377
|
+
${colors.gray(' Route through Tor (SOCKS5 on port 9050)')}
|
|
5378
|
+
|
|
5379
|
+
${colors.green('$ rek proxy http://user:pass@proxy.com:3128 api.com method=POST name="John"')}
|
|
5380
|
+
${colors.gray(' POST with authentication')}
|
|
4419
5381
|
`)
|
|
4420
5382
|
.action(async (proxy, url, args) => {
|
|
4421
5383
|
const { createClient } = await import('../core/client.js');
|
|
@@ -4474,6 +5436,13 @@ ${colors.bold(colors.yellow('Examples:'))}
|
|
|
4474
5436
|
process.exit(1);
|
|
4475
5437
|
}
|
|
4476
5438
|
});
|
|
5439
|
+
function applyHelpAfterError(cmd) {
|
|
5440
|
+
cmd.showHelpAfterError(true);
|
|
5441
|
+
for (const subcmd of cmd.commands) {
|
|
5442
|
+
applyHelpAfterError(subcmd);
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
applyHelpAfterError(program);
|
|
4477
5446
|
program.parse();
|
|
4478
5447
|
}
|
|
4479
5448
|
main().catch((error) => {
|