recker 1.0.39-next.ae39611 → 1.0.39

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 CHANGED
@@ -128,7 +128,6 @@ async function main() {
128
128
  .name('rek')
129
129
  .description('The HTTP Client for Humans (and Robots)')
130
130
  .version(version)
131
- .showHelpAfterError(true)
132
131
  .argument('[args...]', 'URL, Method, Headers (Key:Value), Data (key=value)')
133
132
  .option('-v, --verbose', 'Show full request/response details')
134
133
  .option('-q, --quiet', 'Output only response body (no colors, perfect for piping)')
@@ -277,27 +276,7 @@ Error: ${error.message}`));
277
276
  });
278
277
  program
279
278
  .command('completion')
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
- `)
279
+ .description('Generate shell completion script')
301
280
  .action(() => {
302
281
  const script = `
303
282
  ###-begin-rek-completion-###
@@ -345,32 +324,15 @@ complete -F _rek_completions rek
345
324
  program
346
325
  .command('version')
347
326
  .alias('info')
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) {
327
+ .description('Show detailed version information')
328
+ .option('-s, --short', 'Show only version number')
329
+ .option('--format <type>', 'Output format: text or json', 'text')
330
+ .action(async (options) => {
331
+ if (options.short) {
370
332
  console.log(version);
371
333
  return;
372
334
  }
373
- if (formatJson) {
335
+ if (options.format === 'json') {
374
336
  const { getVersionInfo } = await import('../version.js');
375
337
  const info = await getVersionInfo();
376
338
  console.log(JSON.stringify(info, null, 2));
@@ -386,27 +348,6 @@ ${colors.bold(colors.yellow('Options:'))}
386
348
  .alias('repl')
387
349
  .description('Start the interactive Rek Shell')
388
350
  .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
- `)
410
351
  .action(async (options) => {
411
352
  if (options.env !== false) {
412
353
  try {
@@ -424,22 +365,7 @@ ${colors.bold(colors.yellow('Examples:'))}
424
365
  program
425
366
  .command('docs [query...]')
426
367
  .alias('?')
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
- `)
368
+ .description('Search Recker documentation (opens fullscreen panel)')
443
369
  .action(async (queryParts) => {
444
370
  const query = queryParts.join(' ').trim();
445
371
  const { openSearchPanel } = await import('./tui/search-panel.js');
@@ -448,39 +374,8 @@ ${colors.bold(colors.yellow('Examples:'))}
448
374
  program
449
375
  .command('security')
450
376
  .alias('headers')
451
- .alias('grade')
452
- .description('Grade a website\'s security headers (A+ to F)')
377
+ .description('Analyze HTTP response headers for security best practices')
453
378
  .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
- `)
484
379
  .action(async (url) => {
485
380
  if (!url.startsWith('http'))
486
381
  url = `https://${url}`;
@@ -522,30 +417,16 @@ ${colors.bold('Details:')}`);
522
417
  });
523
418
  program
524
419
  .command('seo')
525
- .alias('audit')
526
- .description('Analyze a page\'s SEO health (80+ checks)')
420
+ .description('Analyze page SEO (title, meta, headings, links, images, structured data)')
527
421
  .argument('<url>', 'URL to analyze')
528
- .argument('[args...]', 'Options: all format=json')
422
+ .option('-a, --all', 'Show all checks including passed ones')
423
+ .option('--format <format>', 'Output format: text (default) or json', 'text')
529
424
  .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
-
540
425
  ${colors.bold(colors.yellow('Examples:'))}
541
426
  ${colors.green('$ rek seo example.com')} ${colors.gray('Basic SEO analysis')}
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
427
+ ${colors.green('$ rek seo example.com -a')} ${colors.gray('Show all checks')}
428
+ ${colors.green('$ rek seo example.com --format json')} ${colors.gray('Output as JSON')}
429
+ ${colors.green('$ rek seo example.com --format json | jq')} ${colors.gray('Pipe to jq for processing')}
549
430
 
550
431
  ${colors.bold(colors.yellow('Checks:'))}
551
432
  ${colors.cyan('Title Tag')} Length and presence
@@ -558,11 +439,10 @@ ${colors.bold(colors.yellow('Checks:'))}
558
439
  ${colors.cyan('Structured Data')} JSON-LD presence
559
440
  ${colors.cyan('Technical')} Canonical, viewport, lang
560
441
  `)
561
- .action(async (url, args) => {
442
+ .action(async (url, options) => {
562
443
  if (!url.startsWith('http'))
563
444
  url = `https://${url}`;
564
- const showAll = args.includes('all');
565
- const isJson = args.some(a => a === 'format=json' || a === 'json');
445
+ const isJson = options.format === 'json';
566
446
  const { createClient } = await import('../core/client.js');
567
447
  const { analyzeSeo } = await import('../seo/analyzer.js');
568
448
  if (!isJson) {
@@ -639,11 +519,11 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
639
519
  console.log(` ${colors.gray('Images:')} ${report.images.total} (${report.images.withAlt} with alt, ${report.images.withoutAlt} without)`);
640
520
  console.log('');
641
521
  console.log(`${colors.bold('Checks:')}`);
642
- const checksToShow = showAll
522
+ const checksToShow = options.all
643
523
  ? report.checks
644
524
  : report.checks.filter(c => c.status !== 'pass' && c.status !== 'info');
645
- if (checksToShow.length === 0 && !showAll) {
646
- console.log(colors.green(' All checks passed! Use "all" to see details.'));
525
+ if (checksToShow.length === 0 && !options.all) {
526
+ console.log(colors.green(' All checks passed! Use -a to see details.'));
647
527
  }
648
528
  else {
649
529
  for (const check of checksToShow) {
@@ -679,15 +559,12 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
679
559
  .command('robots')
680
560
  .description('Validate and analyze robots.txt file')
681
561
  .argument('<url>', 'Website URL or direct robots.txt URL')
682
- .argument('[args...]', 'Options: format=json')
562
+ .option('--format <format>', 'Output format: text (default) or json', 'text')
683
563
  .addHelpText('after', `
684
564
  ${colors.bold(colors.yellow('Examples:'))}
685
565
  ${colors.green('$ rek robots example.com')} ${colors.gray('Validate robots.txt')}
686
566
  ${colors.green('$ rek robots example.com/robots.txt')} ${colors.gray('Direct URL')}
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
567
+ ${colors.green('$ rek robots example.com --format json')} ${colors.gray('JSON output')}
691
568
 
692
569
  ${colors.bold(colors.yellow('Checks:'))}
693
570
  ${colors.cyan('Syntax')} Valid robots.txt syntax
@@ -696,14 +573,14 @@ ${colors.bold(colors.yellow('Checks:'))}
696
573
  ${colors.cyan('Crawl-delay')} Aggressive crawl delay
697
574
  ${colors.cyan('AI Bots')} GPTBot, ClaudeBot, Anthropic blocks
698
575
  `)
699
- .action(async (url, args) => {
576
+ .action(async (url, options) => {
700
577
  if (!url.startsWith('http'))
701
578
  url = `https://${url}`;
702
579
  if (!url.includes('robots.txt')) {
703
580
  const urlObj = new URL(url);
704
581
  url = `${urlObj.origin}/robots.txt`;
705
582
  }
706
- const isJson = args.some(a => a === 'format=json' || a === 'json');
583
+ const isJson = options.format === 'json';
707
584
  if (!isJson) {
708
585
  console.log(colors.gray(`Fetching robots.txt from ${url}...`));
709
586
  }
@@ -793,17 +670,14 @@ ${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')
793
670
  .command('sitemap')
794
671
  .description('Validate and analyze sitemap.xml file')
795
672
  .argument('<url>', 'Website URL or direct sitemap URL')
796
- .argument('[args...]', 'Options: discover format=json')
673
+ .option('--format <format>', 'Output format: text (default) or json', 'text')
674
+ .option('--discover', 'Discover all sitemaps via robots.txt')
797
675
  .addHelpText('after', `
798
676
  ${colors.bold(colors.yellow('Examples:'))}
799
677
  ${colors.green('$ rek sitemap example.com')} ${colors.gray('Validate sitemap')}
800
678
  ${colors.green('$ rek sitemap example.com/sitemap.xml')} ${colors.gray('Direct URL')}
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
679
+ ${colors.green('$ rek sitemap example.com --discover')} ${colors.gray('Find all sitemaps')}
680
+ ${colors.green('$ rek sitemap example.com --format json')} ${colors.gray('JSON output')}
807
681
 
808
682
  ${colors.bold(colors.yellow('Checks:'))}
809
683
  ${colors.cyan('Structure')} Valid XML sitemap format
@@ -812,13 +686,12 @@ ${colors.bold(colors.yellow('Checks:'))}
812
686
  ${colors.cyan('URLs')} Valid, no duplicates, same domain
813
687
  ${colors.cyan('Lastmod')} Valid dates, not in future
814
688
  `)
815
- .action(async (url, args) => {
689
+ .action(async (url, options) => {
816
690
  if (!url.startsWith('http'))
817
691
  url = `https://${url}`;
818
- const isJson = args.some(a => a === 'format=json' || a === 'json');
819
- const doDiscover = args.includes('discover');
692
+ const isJson = options.format === 'json';
820
693
  try {
821
- if (doDiscover) {
694
+ if (options.discover) {
822
695
  const { discoverSitemaps } = await import('../seo/validators/sitemap.js');
823
696
  if (!isJson) {
824
697
  console.log(colors.gray(`Discovering sitemaps for ${new URL(url).origin}...`));
@@ -928,17 +801,14 @@ ${colors.gray('Type:')} ${result.parseResult?.type === 'sitemapindex' ? 'Sitemap
928
801
  .command('llms')
929
802
  .description('Validate and analyze llms.txt file (AI/LLM optimization)')
930
803
  .argument('[url]', 'Website URL or direct llms.txt URL')
931
- .argument('[args...]', 'Options: template format=json')
804
+ .option('--format <format>', 'Output format: text (default) or json', 'text')
805
+ .option('--template', 'Generate a template llms.txt file')
932
806
  .addHelpText('after', `
933
807
  ${colors.bold(colors.yellow('Examples:'))}
934
808
  ${colors.green('$ rek llms example.com')} ${colors.gray('Validate llms.txt')}
935
809
  ${colors.green('$ rek llms example.com/llms.txt')} ${colors.gray('Direct URL')}
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
810
+ ${colors.green('$ rek llms example.com --format json')} ${colors.gray('JSON output')}
811
+ ${colors.green('$ rek llms --template > llms.txt')} ${colors.gray('Generate template')}
942
812
 
943
813
  ${colors.bold(colors.yellow('About llms.txt:'))}
944
814
  A proposed standard for providing LLM-friendly content.
@@ -951,10 +821,9 @@ ${colors.bold(colors.yellow('Checks:'))}
951
821
  ${colors.cyan('Description')} Site description block
952
822
  ${colors.cyan('Sections')} Content sections with links
953
823
  `)
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) {
824
+ .action(async (url, options) => {
825
+ const isJson = options.format === 'json';
826
+ if (options.template) {
958
827
  const { generateLlmsTxtTemplate } = await import('../seo/validators/llms-txt.js');
959
828
  const template = generateLlmsTxtTemplate({
960
829
  siteName: 'Your Site Name',
@@ -1076,20 +945,10 @@ ${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')
1076
945
  });
1077
946
  program
1078
947
  .command('spider')
1079
- .alias('crawl')
1080
- .description('Crawl a website and analyze all pages')
948
+ .description('Crawl a website following internal links')
1081
949
  .argument('<url>', 'Starting URL to crawl')
1082
950
  .argument('[args...]', 'Options: depth=N limit=N concurrency=N seo focus=MODE output=file.json')
1083
951
  .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
-
1093
952
  ${colors.bold(colors.yellow('Examples:'))}
1094
953
  ${colors.green('$ rek spider example.com')} ${colors.gray('Crawl with defaults')}
1095
954
  ${colors.green('$ rek spider example.com depth=3 limit=50')} ${colors.gray('Depth 3, max 50 pages')}
@@ -1098,7 +957,6 @@ ${colors.bold(colors.yellow('Examples:'))}
1098
957
  ${colors.green('$ rek spider example.com seo focus=links')} ${colors.gray('Focus on link issues')}
1099
958
  ${colors.green('$ rek spider example.com seo focus=security')} ${colors.gray('Focus on security issues')}
1100
959
  ${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')}
1102
960
 
1103
961
  ${colors.bold(colors.yellow('Options:'))}
1104
962
  ${colors.cyan('depth=N')} Max link depth to follow (default: 5)
@@ -1106,7 +964,6 @@ ${colors.bold(colors.yellow('Options:'))}
1106
964
  ${colors.cyan('concurrency=N')} Parallel requests (default: 5)
1107
965
  ${colors.cyan('seo')} Enable SEO analysis mode
1108
966
  ${colors.cyan('focus=MODE')} Focus analysis on specific area (requires seo)
1109
- ${colors.cyan('format=json')} Output JSON to stdout (for piping)
1110
967
  ${colors.cyan('output=file.json')} Save JSON report to file
1111
968
 
1112
969
  ${colors.bold(colors.yellow('Focus Modes:'))}
@@ -1123,7 +980,6 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1123
980
  let concurrency = 5;
1124
981
  let seoEnabled = false;
1125
982
  let outputFile = '';
1126
- let formatJson = false;
1127
983
  let focusMode = 'all';
1128
984
  const focusCategories = {
1129
985
  links: ['links'],
@@ -1149,9 +1005,6 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1149
1005
  else if (arg.startsWith('output=')) {
1150
1006
  outputFile = arg.split('=')[1] || '';
1151
1007
  }
1152
- else if (arg === 'format=json' || arg === '--format=json') {
1153
- formatJson = true;
1154
- }
1155
1008
  else if (arg.startsWith('focus=')) {
1156
1009
  const mode = arg.split('=')[1] || 'all';
1157
1010
  if (mode in focusCategories) {
@@ -1166,16 +1019,14 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1166
1019
  }
1167
1020
  if (!url.startsWith('http'))
1168
1021
  url = `https://${url}`;
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('');
1022
+ const modeLabel = seoEnabled ? colors.magenta(' + SEO') : '';
1023
+ const focusLabel = focusMode !== 'all' ? colors.cyan(` [focus: ${focusMode}]`) : '';
1024
+ console.log(colors.cyan(`\nSpider starting: ${url}`));
1025
+ console.log(colors.gray(` Depth: ${maxDepth} | Limit: ${maxPages} | Concurrency: ${concurrency}${modeLabel}${focusLabel}`));
1026
+ if (outputFile) {
1027
+ console.log(colors.gray(` Output: ${outputFile}`));
1178
1028
  }
1029
+ console.log('');
1179
1030
  try {
1180
1031
  if (seoEnabled) {
1181
1032
  const { SeoSpider } = await import('../seo/index.js');
@@ -1189,91 +1040,11 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1189
1040
  output: outputFile || undefined,
1190
1041
  focusCategories: focusCategories[focusMode],
1191
1042
  focusMode,
1192
- onProgress: formatJson ? undefined : (progress) => {
1043
+ onProgress: (progress) => {
1193
1044
  process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
1194
1045
  },
1195
1046
  });
1196
1047
  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
- }
1277
1048
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
1278
1049
  console.log(colors.green(`\n✔ SEO Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
1279
1050
  console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
@@ -1453,41 +1224,11 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1453
1224
  concurrency,
1454
1225
  sameDomain: true,
1455
1226
  delay: 100,
1456
- onProgress: formatJson ? undefined : (progress) => {
1227
+ onProgress: (progress) => {
1457
1228
  process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
1458
1229
  },
1459
1230
  });
1460
1231
  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
- }
1491
1232
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
1492
1233
  console.log(colors.green(`\n✔ Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
1493
1234
  console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
@@ -1552,19 +1293,10 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1552
1293
  });
1553
1294
  program
1554
1295
  .command('scrape')
1555
- .alias('extract')
1556
- .description('Extract data from web pages with CSS selectors')
1296
+ .description('Scrape data from a web page using CSS selectors')
1557
1297
  .argument('<url>', 'URL to scrape')
1558
1298
  .argument('[args...]', 'Options: select=SELECTOR, attr=NAME, links, images, meta, tables, scripts, jsonld')
1559
1299
  .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
-
1568
1300
  ${colors.bold(colors.yellow('Examples:'))}
1569
1301
  ${colors.green('$ rek scrape example.com')} ${colors.gray('# Basic page info')}
1570
1302
  ${colors.green('$ rek scrape example.com select="h1"')} ${colors.gray('# Extract h1 text')}
@@ -1762,32 +1494,20 @@ ${colors.bold('Images:')} ${imageCount}
1762
1494
  });
1763
1495
  program
1764
1496
  .command('ai')
1765
- .alias('chat')
1766
- .alias('ask')
1767
- .description('Chat with AI models (OpenAI, Claude, Groq, etc)')
1497
+ .description('Send a single AI prompt (no memory/context)')
1768
1498
  .argument('<preset>', 'AI preset to use (e.g., @openai, @anthropic, @groq)')
1769
- .argument('<prompt...>', 'The prompt and options: "prompt text" model=<model> temperature=<temp> max-tokens=<tokens> wait json env=<path>')
1499
+ .argument('<prompt...>', 'The prompt to send')
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)')
1770
1506
  .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
-
1787
1507
  ${colors.bold(colors.yellow('Examples:'))}
1788
1508
  ${colors.green('$ rek ai @openai "What is the capital of France?"')}
1789
- ${colors.green('$ rek ai @anthropic "Explain quantum computing" model=claude-sonnet-4-20250514')}
1790
- ${colors.green('$ rek ai @groq "Write a haiku" wait')}
1509
+ ${colors.green('$ rek ai @anthropic "Explain quantum computing" -m claude-sonnet-4-20250514')}
1510
+ ${colors.green('$ rek ai @groq "Write a haiku" --wait')}
1791
1511
  ${colors.green('$ rek ai @openai "Translate to Spanish: Hello world"')}
1792
1512
 
1793
1513
  ${colors.bold(colors.yellow('Available AI Presets:'))}
@@ -1809,39 +1529,14 @@ ${colors.bold(colors.yellow('Note:'))}
1809
1529
  This command sends a single prompt without conversation memory.
1810
1530
  For chat with memory, use: ${colors.cyan('rek shell')} then ${colors.cyan('@openai Your message')}
1811
1531
  `)
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);
1532
+ .action(async (preset, promptParts, options) => {
1533
+ if (options.env !== undefined) {
1534
+ await loadEnvFile(options.env);
1840
1535
  }
1841
1536
  else {
1842
1537
  try {
1843
- const envFilePath = join(process.cwd(), '.env');
1844
- await fs.access(envFilePath);
1538
+ const envPath = join(process.cwd(), '.env');
1539
+ await fs.access(envPath);
1845
1540
  await loadEnvFile(true);
1846
1541
  }
1847
1542
  catch {
@@ -1862,7 +1557,7 @@ ${colors.bold(colors.yellow('Note:'))}
1862
1557
  console.log(colors.gray('Use an AI preset like @openai, @anthropic, @groq, etc.'));
1863
1558
  process.exit(1);
1864
1559
  }
1865
- const prompt = actualPromptParts.join(' ');
1560
+ const prompt = promptParts.join(' ');
1866
1561
  if (!prompt.trim()) {
1867
1562
  console.error(colors.red('Error: Prompt is required'));
1868
1563
  process.exit(1);
@@ -1870,16 +1565,16 @@ ${colors.bold(colors.yellow('Note:'))}
1870
1565
  try {
1871
1566
  const { createClient } = await import('../core/client.js');
1872
1567
  const client = createClient(presetConfig);
1873
- if (model) {
1568
+ if (options.model) {
1874
1569
  client.ai.setMemoryConfig({ systemPrompt: undefined });
1875
- client._aiConfig.model = model;
1570
+ client._aiConfig.model = options.model;
1876
1571
  }
1877
- if (!jsonOutput) {
1572
+ if (!options.json) {
1878
1573
  console.log(colors.gray(`Using @${presetName} (${client._aiConfig.model})...\n`));
1879
1574
  }
1880
- if (wait || jsonOutput) {
1575
+ if (options.wait || options.json) {
1881
1576
  const response = await client.ai.prompt(prompt);
1882
- if (jsonOutput) {
1577
+ if (options.json) {
1883
1578
  console.log(JSON.stringify({
1884
1579
  content: response.content,
1885
1580
  model: response.model,
@@ -1915,36 +1610,8 @@ ${colors.bold(colors.yellow('Note:'))}
1915
1610
  });
1916
1611
  program
1917
1612
  .command('ip')
1918
- .alias('geo')
1919
- .alias('geoip')
1920
- .description('Look up geolocation and ISP info for an IP address')
1613
+ .description('Get IP address intelligence using local GeoLite2 database')
1921
1614
  .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
- `)
1948
1615
  .action(async (address) => {
1949
1616
  const { getIpInfo, isGeoIPAvailable } = await import('../mcp/ip-intel.js');
1950
1617
  if (!isGeoIPAvailable()) {
@@ -1983,34 +1650,9 @@ ${colors.bold('Network:')}
1983
1650
  program
1984
1651
  .command('tls')
1985
1652
  .alias('ssl')
1986
- .alias('cert')
1987
1653
  .description('Inspect TLS/SSL certificate of a host')
1988
1654
  .argument('<host>', 'Hostname or IP address')
1989
1655
  .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
- `)
2014
1656
  .action(async (host, port) => {
2015
1657
  const { inspectTLS } = await import('../utils/tls-inspector.js');
2016
1658
  console.log(colors.gray(`Inspecting TLS certificate for ${host}:${port}...`));
@@ -2078,43 +1720,15 @@ ${colors.bold('Fingerprints:')}
2078
1720
  });
2079
1721
  program
2080
1722
  .command('whois')
2081
- .description('Look up domain registration and ownership info')
1723
+ .description('WHOIS lookup for domains and IP addresses')
2082
1724
  .argument('<query>', 'Domain name or IP address')
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) => {
1725
+ .option('-r, --raw', 'Show raw WHOIS response')
1726
+ .action(async (query, options) => {
2112
1727
  const { whois } = await import('../utils/whois.js');
2113
- const raw = args.includes('raw');
2114
1728
  console.log(colors.gray(`Looking up WHOIS for ${query}...`));
2115
1729
  try {
2116
1730
  const result = await whois(query);
2117
- if (raw) {
1731
+ if (options.raw) {
2118
1732
  console.log(result.raw);
2119
1733
  return;
2120
1734
  }
@@ -2149,33 +1763,8 @@ ${colors.bold('Server:')} ${result.server}
2149
1763
  });
2150
1764
  program
2151
1765
  .command('rdap')
2152
- .description('RDAP lookup (modern WHOIS with JSON)')
1766
+ .description('RDAP lookup (modern WHOIS replacement)')
2153
1767
  .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
- `)
2179
1768
  .action(async (domain) => {
2180
1769
  const { rdap } = await import('../utils/rdap.js');
2181
1770
  const { Client } = await import('../core/client.js');
@@ -2227,46 +1816,14 @@ ${colors.bold('Status:')} ${result.status?.join(', ') || 'N/A'}
2227
1816
  });
2228
1817
  program
2229
1818
  .command('ping')
2230
- .description('Test TCP connectivity to host:port')
1819
+ .description('TCP connectivity check to a host')
2231
1820
  .argument('<host>', 'Hostname or IP address')
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) => {
1821
+ .argument('[port]', 'Port number (default: 80 for HTTP, 443 for HTTPS)', '443')
1822
+ .option('-c, --count <n>', 'Number of pings', '4')
1823
+ .action(async (host, port, options) => {
2260
1824
  const net = await import('node:net');
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;
1825
+ const count = parseInt(options.count);
1826
+ const portNum = parseInt(port);
2270
1827
  const results = [];
2271
1828
  console.log(colors.gray(`Pinging ${host}:${portNum}...`));
2272
1829
  console.log('');
@@ -2316,48 +1873,20 @@ ${colors.bold('Statistics:')}
2316
1873
  .command('ls')
2317
1874
  .description('List files in a remote directory')
2318
1875
  .argument('<host>', 'FTP server hostname')
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) => {
1876
+ .argument('[path]', 'Remote path to list', '/')
1877
+ .option('-u, --user <username>', 'Username', 'anonymous')
1878
+ .option('-p, --pass <password>', 'Password', 'anonymous@')
1879
+ .option('-P, --port <port>', 'Port number', '21')
1880
+ .option('--secure', 'Use FTPS (explicit TLS)')
1881
+ .option('--implicit', 'Use implicit FTPS (port 990)')
1882
+ .action(async (host, path, options) => {
2334
1883
  const { createFTP } = await import('../protocols/ftp.js');
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;
1884
+ const secure = options.implicit ? 'implicit' : options.secure ? true : false;
2356
1885
  const client = createFTP({
2357
1886
  host,
2358
- port,
2359
- user,
2360
- password: pass,
1887
+ port: parseInt(options.port),
1888
+ user: options.user,
1889
+ password: options.pass,
2361
1890
  secure,
2362
1891
  });
2363
1892
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -2400,50 +1929,22 @@ ${colors.bold(colors.yellow('Examples:'))}
2400
1929
  .description('Download a file from FTP server')
2401
1930
  .argument('<host>', 'FTP server hostname')
2402
1931
  .argument('<remote>', 'Remote file path')
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) => {
1932
+ .argument('[local]', 'Local file path (default: same filename)')
1933
+ .option('-u, --user <username>', 'Username', 'anonymous')
1934
+ .option('-p, --pass <password>', 'Password', 'anonymous@')
1935
+ .option('-P, --port <port>', 'Port number', '21')
1936
+ .option('--secure', 'Use FTPS (explicit TLS)')
1937
+ .option('--implicit', 'Use implicit FTPS (port 990)')
1938
+ .action(async (host, remote, local, options) => {
2418
1939
  const { createFTP } = await import('../protocols/ftp.js');
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;
1940
+ const path = await import('node:path');
1941
+ const localPath = local || path.basename(remote);
1942
+ const secure = options.implicit ? 'implicit' : options.secure ? true : false;
2442
1943
  const client = createFTP({
2443
1944
  host,
2444
- port,
2445
- user,
2446
- password: pass,
1945
+ port: parseInt(options.port),
1946
+ user: options.user,
1947
+ password: options.pass,
2447
1948
  secure,
2448
1949
  });
2449
1950
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -2483,50 +1984,22 @@ ${colors.bold(colors.yellow('Examples:'))}
2483
1984
  .description('Upload a file to FTP server')
2484
1985
  .argument('<host>', 'FTP server hostname')
2485
1986
  .argument('<local>', 'Local file path')
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) => {
1987
+ .argument('[remote]', 'Remote file path (default: same filename)')
1988
+ .option('-u, --user <username>', 'Username', 'anonymous')
1989
+ .option('-p, --pass <password>', 'Password', 'anonymous@')
1990
+ .option('-P, --port <port>', 'Port number', '21')
1991
+ .option('--secure', 'Use FTPS (explicit TLS)')
1992
+ .option('--implicit', 'Use implicit FTPS (port 990)')
1993
+ .action(async (host, local, remote, options) => {
2501
1994
  const { createFTP } = await import('../protocols/ftp.js');
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;
1995
+ const path = await import('node:path');
1996
+ const remotePath = remote || '/' + path.basename(local);
1997
+ const secure = options.implicit ? 'implicit' : options.secure ? true : false;
2525
1998
  const client = createFTP({
2526
1999
  host,
2527
- port,
2528
- user,
2529
- password: pass,
2000
+ port: parseInt(options.port),
2001
+ user: options.user,
2002
+ password: options.pass,
2530
2003
  secure,
2531
2004
  });
2532
2005
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -2566,44 +2039,19 @@ ${colors.bold(colors.yellow('Examples:'))}
2566
2039
  .description('Delete a file from FTP server')
2567
2040
  .argument('<host>', 'FTP server hostname')
2568
2041
  .argument('<path>', 'Remote file path to delete')
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) => {
2042
+ .option('-u, --user <username>', 'Username', 'anonymous')
2043
+ .option('-p, --pass <password>', 'Password', 'anonymous@')
2044
+ .option('-P, --port <port>', 'Port number', '21')
2045
+ .option('--secure', 'Use FTPS (explicit TLS)')
2046
+ .option('--implicit', 'Use implicit FTPS (port 990)')
2047
+ .action(async (host, remotePath, options) => {
2583
2048
  const { createFTP } = await import('../protocols/ftp.js');
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;
2049
+ const secure = options.implicit ? 'implicit' : options.secure ? true : false;
2602
2050
  const client = createFTP({
2603
2051
  host,
2604
- port,
2605
- user,
2606
- password: pass,
2052
+ port: parseInt(options.port),
2053
+ user: options.user,
2054
+ password: options.pass,
2607
2055
  secure,
2608
2056
  });
2609
2057
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -2634,44 +2082,19 @@ ${colors.bold(colors.yellow('Examples:'))}
2634
2082
  .description('Create a directory on FTP server')
2635
2083
  .argument('<host>', 'FTP server hostname')
2636
2084
  .argument('<path>', 'Remote directory path to create')
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) => {
2085
+ .option('-u, --user <username>', 'Username', 'anonymous')
2086
+ .option('-p, --pass <password>', 'Password', 'anonymous@')
2087
+ .option('-P, --port <port>', 'Port number', '21')
2088
+ .option('--secure', 'Use FTPS (explicit TLS)')
2089
+ .option('--implicit', 'Use implicit FTPS (port 990)')
2090
+ .action(async (host, remotePath, options) => {
2651
2091
  const { createFTP } = await import('../protocols/ftp.js');
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;
2092
+ const secure = options.implicit ? 'implicit' : options.secure ? true : false;
2670
2093
  const client = createFTP({
2671
2094
  host,
2672
- port,
2673
- user,
2674
- password: pass,
2095
+ port: parseInt(options.port),
2096
+ user: options.user,
2097
+ password: options.pass,
2675
2098
  secure,
2676
2099
  });
2677
2100
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -2701,31 +2124,15 @@ ${colors.bold(colors.yellow('Examples:'))}
2701
2124
  .command('telnet')
2702
2125
  .description('Connect to a Telnet server')
2703
2126
  .argument('<host>', 'Hostname or IP address')
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) => {
2127
+ .argument('[port]', 'Port number', '23')
2128
+ .option('-t, --timeout <ms>', 'Connection timeout in ms', '30000')
2129
+ .action(async (host, port, options) => {
2715
2130
  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
- }
2724
2131
  console.log(colors.gray(`Connecting to ${host}:${port}...`));
2725
2132
  const client = createTelnet({
2726
2133
  host,
2727
- port,
2728
- timeout,
2134
+ port: parseInt(port),
2135
+ timeout: parseInt(options.timeout),
2729
2136
  });
2730
2137
  try {
2731
2138
  await client.connect();
@@ -2775,37 +2182,9 @@ ${colors.bold(colors.yellow('Examples:'))}
2775
2182
  });
2776
2183
  dns
2777
2184
  .command('lookup')
2778
- .alias('resolve')
2779
- .description('Look up DNS records (A, MX, TXT, etc)')
2185
+ .description('Perform DNS lookup for any record type')
2780
2186
  .argument('<domain>', 'Domain name to lookup')
2781
2187
  .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
- `)
2809
2188
  .action(async (domain, type) => {
2810
2189
  const { dnsLookup } = await import('../utils/dns-toolkit.js');
2811
2190
  console.log(colors.gray(`Looking up ${type.toUpperCase()} records for ${domain}...`));
@@ -3080,54 +2459,27 @@ ${colors.gray('Status:')} ${statusIcon}
3080
2459
  dns
3081
2460
  .command('generate-dmarc')
3082
2461
  .description('Generate a DMARC record interactively')
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) => {
2462
+ .option('-p, --policy <policy>', 'Policy: none, quarantine, reject', 'none')
2463
+ .option('--subdomain-policy <policy>', 'Subdomain policy')
2464
+ .option('--pct <percent>', 'Percentage of emails to apply policy', '100')
2465
+ .option('--rua <emails>', 'Aggregate report email(s), comma-separated')
2466
+ .option('--ruf <emails>', 'Forensic report email(s), comma-separated')
2467
+ .action(async (options) => {
3099
2468
  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
- }
3117
2469
  const dmarcOptions = {
3118
- policy,
2470
+ policy: options.policy,
3119
2471
  };
3120
- if (subdomainPolicy) {
3121
- dmarcOptions.subdomainPolicy = subdomainPolicy;
2472
+ if (options.subdomainPolicy) {
2473
+ dmarcOptions.subdomainPolicy = options.subdomainPolicy;
3122
2474
  }
3123
- if (pct && pct !== '100') {
3124
- dmarcOptions.percentage = parseInt(pct);
2475
+ if (options.pct && options.pct !== '100') {
2476
+ dmarcOptions.percentage = parseInt(options.pct);
3125
2477
  }
3126
- if (rua) {
3127
- dmarcOptions.aggregateReports = rua.split(',').map((e) => e.trim());
2478
+ if (options.rua) {
2479
+ dmarcOptions.aggregateReports = options.rua.split(',').map((e) => e.trim());
3128
2480
  }
3129
- if (ruf) {
3130
- dmarcOptions.forensicReports = ruf.split(',').map((e) => e.trim());
2481
+ if (options.ruf) {
2482
+ dmarcOptions.forensicReports = options.ruf.split(',').map((e) => e.trim());
3131
2483
  }
3132
2484
  const record = generateDmarc(dmarcOptions);
3133
2485
  console.log(`
@@ -3209,68 +2561,22 @@ ${colors.bold(colors.yellow('Record Types:'))}
3209
2561
  });
3210
2562
  program
3211
2563
  .command('graphql')
3212
- .alias('gql')
3213
- .description('Execute GraphQL queries and mutations')
2564
+ .description('Execute a GraphQL query')
3214
2565
  .argument('<url>', 'GraphQL endpoint URL')
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) => {
2566
+ .option('-q, --query <query>', 'GraphQL query string')
2567
+ .option('-f, --file <file>', 'Path to GraphQL query file')
2568
+ .option('-v, --variables <json>', 'Variables as JSON string')
2569
+ .option('--var-file <file>', 'Path to variables JSON file')
2570
+ .option('-H, --header <header>', 'Add header (can be used multiple times)', (val, prev) => [...prev, val], [])
2571
+ .action(async (url, options) => {
3249
2572
  const { graphql } = await import('../plugins/graphql.js');
3250
2573
  const { createClient } = await import('../core/client.js');
3251
2574
  const fs = await import('node:fs/promises');
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;
2575
+ let query = options.query;
3270
2576
  let variables = {};
3271
- if (queryFile) {
2577
+ if (options.file) {
3272
2578
  try {
3273
- query = await fs.readFile(queryFile, 'utf-8');
2579
+ query = await fs.readFile(options.file, 'utf-8');
3274
2580
  }
3275
2581
  catch (err) {
3276
2582
  console.error(colors.red(`Failed to read query file: ${err.message}`));
@@ -3278,22 +2584,22 @@ ${colors.bold(colors.yellow('Examples:'))}
3278
2584
  }
3279
2585
  }
3280
2586
  if (!query) {
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 } }"'));
2587
+ console.error(colors.red('Error: Query is required. Use --query or --file'));
2588
+ console.log(colors.gray('Example: rek graphql https://api.example.com/graphql -q "query { users { id name } }"'));
3283
2589
  process.exit(1);
3284
2590
  }
3285
- if (variablesStr) {
2591
+ if (options.variables) {
3286
2592
  try {
3287
- variables = JSON.parse(variablesStr);
2593
+ variables = JSON.parse(options.variables);
3288
2594
  }
3289
2595
  catch {
3290
- console.error(colors.red('Invalid JSON in variables='));
2596
+ console.error(colors.red('Invalid JSON in --variables'));
3291
2597
  process.exit(1);
3292
2598
  }
3293
2599
  }
3294
- else if (varFile) {
2600
+ else if (options.varFile) {
3295
2601
  try {
3296
- const content = await fs.readFile(varFile, 'utf-8');
2602
+ const content = await fs.readFile(options.varFile, 'utf-8');
3297
2603
  variables = JSON.parse(content);
3298
2604
  }
3299
2605
  catch (err) {
@@ -3304,7 +2610,7 @@ ${colors.bold(colors.yellow('Examples:'))}
3304
2610
  const headers = {
3305
2611
  'Content-Type': 'application/json',
3306
2612
  };
3307
- for (const h of headerArgs) {
2613
+ for (const h of options.header) {
3308
2614
  const [key, ...valueParts] = h.split(':');
3309
2615
  headers[key.trim()] = valueParts.join(':').trim();
3310
2616
  }
@@ -3468,48 +2774,21 @@ ${colors.bold(colors.yellow('Examples:'))}
3468
2774
  .command('download')
3469
2775
  .description('Download an HLS stream')
3470
2776
  .argument('<url>', 'HLS playlist URL')
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) => {
2777
+ .argument('[output]', 'Output file path', 'output.ts')
2778
+ .option('-q, --quality <quality>', 'Quality: highest, lowest, or resolution (e.g., 720p)')
2779
+ .option('--live', 'Enable live stream mode')
2780
+ .option('-d, --duration <seconds>', 'Duration for live recording in seconds')
2781
+ .option('-c, --concurrency <n>', 'Concurrent segment downloads', '4')
2782
+ .action(async (url, output, options) => {
3487
2783
  const { hls } = await import('../plugins/hls.js');
3488
2784
  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
- }
3506
2785
  const client = new Client();
3507
2786
  console.log(colors.gray(`Downloading HLS stream from ${url}...`));
3508
2787
  console.log(colors.gray(`Output: ${output}`));
3509
2788
  console.log('');
3510
2789
  try {
3511
2790
  const hlsOptions = {
3512
- concurrency,
2791
+ concurrency: parseInt(options.concurrency),
3513
2792
  onProgress: (p) => {
3514
2793
  const segs = p.totalSegments
3515
2794
  ? `${p.downloadedSegments}/${p.totalSegments}`
@@ -3518,17 +2797,17 @@ ${colors.bold(colors.yellow('Examples:'))}
3518
2797
  process.stdout.write(`\r ${colors.cyan(segs)} segments | ${colors.cyan(mb + ' MB')} downloaded`);
3519
2798
  },
3520
2799
  };
3521
- if (quality) {
3522
- if (quality === 'highest' || quality === 'lowest') {
3523
- hlsOptions.quality = quality;
2800
+ if (options.quality) {
2801
+ if (options.quality === 'highest' || options.quality === 'lowest') {
2802
+ hlsOptions.quality = options.quality;
3524
2803
  }
3525
- else if (quality.includes('p')) {
3526
- hlsOptions.quality = { resolution: quality };
2804
+ else if (options.quality.includes('p')) {
2805
+ hlsOptions.quality = { resolution: options.quality };
3527
2806
  }
3528
2807
  }
3529
- if (live) {
3530
- hlsOptions.live = duration
3531
- ? { duration: duration * 1000 }
2808
+ if (options.live) {
2809
+ hlsOptions.live = options.duration
2810
+ ? { duration: parseInt(options.duration) * 1000 }
3532
2811
  : true;
3533
2812
  }
3534
2813
  await hls(client, url, hlsOptions).download(output);
@@ -3701,23 +2980,19 @@ ${colors.bold(colors.yellow('Examples:'))}
3701
2980
  .command('info')
3702
2981
  .description('Show information about a HAR file')
3703
2982
  .argument('<file>', 'HAR file to inspect')
3704
- .argument('[args...]', 'Options: json')
2983
+ .option('--json', 'Output as JSON')
3705
2984
  .addHelpText('after', `
3706
- ${colors.bold(colors.yellow('Options:'))}
3707
- ${colors.cyan('json')} Output as JSON
3708
-
3709
2985
  ${colors.bold(colors.yellow('Examples:'))}
3710
2986
  ${colors.green('$ rek har info api.har')}
3711
- ${colors.green('$ rek har info api.har json')}
2987
+ ${colors.green('$ rek har info api.har --json')}
3712
2988
  `)
3713
- .action(async (file, args) => {
2989
+ .action(async (file, options) => {
3714
2990
  const { promises: fsPromises } = await import('node:fs');
3715
- const jsonOutput = args.includes('json');
3716
2991
  try {
3717
2992
  const content = await fsPromises.readFile(file, 'utf-8');
3718
2993
  const har = JSON.parse(content);
3719
2994
  const entries = har.log?.entries || [];
3720
- if (jsonOutput) {
2995
+ if (options.json) {
3721
2996
  const info = {
3722
2997
  version: har.log?.version,
3723
2998
  creator: har.log?.creator,
@@ -3844,66 +3119,28 @@ ${colors.bold(colors.yellow('Examples:'))}
3844
3119
  const serve = program.command('serve').description('Start mock servers for testing protocols');
3845
3120
  serve
3846
3121
  .command('http')
3847
- .description('Start a mock HTTP server for testing')
3848
- .argument('[args...]', 'Options: port=3000 host=127.0.0.1 echo delay=0 cors')
3122
+ .description('Start a mock HTTP server')
3123
+ .option('-p, --port <number>', 'Port to listen on', '3000')
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)
3849
3128
  .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
-
3873
3129
  ${colors.bold(colors.yellow('Examples:'))}
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
3130
+ ${colors.green('$ rek serve http')} ${colors.gray('Start on port 3000')}
3131
+ ${colors.green('$ rek serve http -p 8080')} ${colors.gray('Start on port 8080')}
3132
+ ${colors.green('$ rek serve http --echo')} ${colors.gray('Echo mode')}
3133
+ ${colors.green('$ rek serve http --delay 500')} ${colors.gray('Add 500ms delay')}
3878
3134
  `)
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
- }
3135
+ .action(async (options) => {
3899
3136
  const { MockHttpServer } = await import('../testing/mock-http-server.js');
3900
3137
  const server = await MockHttpServer.create({
3901
- port,
3902
- host,
3903
- delay,
3904
- cors,
3138
+ port: parseInt(options.port),
3139
+ host: options.host,
3140
+ delay: parseInt(options.delay),
3141
+ cors: options.cors,
3905
3142
  });
3906
- if (echo) {
3143
+ if (options.echo) {
3907
3144
  server.any('/*', (req) => ({
3908
3145
  status: 200,
3909
3146
  body: {
@@ -3920,8 +3157,8 @@ ${colors.bold(colors.yellow('Examples:'))}
3920
3157
  │ ${colors.bold('Recker Mock HTTP Server')} │
3921
3158
  ├─────────────────────────────────────────────┤
3922
3159
  │ URL: ${colors.cyan(server.url.padEnd(37))}│
3923
- │ Mode: ${colors.yellow((echo ? 'Echo' : 'Default').padEnd(36))}│
3924
- │ Delay: ${colors.gray((delay + 'ms').padEnd(35))}│
3160
+ │ Mode: ${colors.yellow((options.echo ? 'Echo' : 'Default').padEnd(36))}│
3161
+ │ Delay: ${colors.gray((options.delay + 'ms').padEnd(35))}│
3925
3162
  ├─────────────────────────────────────────────┤
3926
3163
  │ Press ${colors.bold('Ctrl+C')} to stop │
3927
3164
  └─────────────────────────────────────────────┘
@@ -3939,18 +3176,15 @@ ${colors.bold(colors.yellow('Examples:'))}
3939
3176
  .command('webhook')
3940
3177
  .alias('wh')
3941
3178
  .description('Start a webhook receiver server')
3942
- .argument('[args...]', 'Options: port=3000 host=127.0.0.1 status=204 quiet')
3179
+ .option('-p, --port <number>', 'Port to listen on', '3000')
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)
3943
3183
  .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
-
3950
3184
  ${colors.bold(colors.yellow('Examples:'))}
3951
3185
  ${colors.green('$ rek serve webhook')} ${colors.gray('Start on port 3000')}
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')}
3186
+ ${colors.green('$ rek serve wh -p 8080')} ${colors.gray('Start on port 8080')}
3187
+ ${colors.green('$ rek serve webhook --status 200')} ${colors.gray('Return 200 instead of 204')}
3954
3188
 
3955
3189
  ${colors.bold(colors.yellow('Endpoints:'))}
3956
3190
  * / ${colors.gray('Receive webhook without ID')}
@@ -3959,32 +3193,18 @@ ${colors.bold(colors.yellow('Endpoints:'))}
3959
3193
  ${colors.bold(colors.yellow('Methods:'))}
3960
3194
  ${colors.gray('GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS')}
3961
3195
  `)
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
- }
3196
+ .action(async (options) => {
3977
3197
  const { createWebhookServer } = await import('../testing/mock-http-server.js');
3978
- const status = statusCode;
3198
+ const status = parseInt(options.status);
3979
3199
  if (status !== 200 && status !== 204) {
3980
3200
  console.error(colors.red('Status must be 200 or 204'));
3981
3201
  process.exit(1);
3982
3202
  }
3983
3203
  const server = await createWebhookServer({
3984
- port,
3985
- host,
3204
+ port: parseInt(options.port),
3205
+ host: options.host,
3986
3206
  status,
3987
- log: !quiet,
3207
+ log: !options.quiet,
3988
3208
  });
3989
3209
  console.log(colors.green(`
3990
3210
  ┌─────────────────────────────────────────────┐
@@ -4010,51 +3230,32 @@ ${colors.bold(colors.yellow('Methods:'))}
4010
3230
  .command('websocket')
4011
3231
  .alias('ws')
4012
3232
  .description('Start a mock WebSocket server')
4013
- .argument('[args...]', 'Options: port=8080 host=127.0.0.1 echo noecho delay=0')
3233
+ .option('-p, --port <number>', 'Port to listen on', '8080')
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')
4014
3238
  .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
-
4022
3239
  ${colors.bold(colors.yellow('Examples:'))}
4023
3240
  ${colors.green('$ rek serve websocket')} ${colors.gray('Start on port 8080')}
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')}
3241
+ ${colors.green('$ rek serve ws -p 9000')} ${colors.gray('Start on port 9000')}
3242
+ ${colors.green('$ rek serve ws --no-echo')} ${colors.gray('Disable echo')}
4026
3243
  `)
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
- }
3244
+ .action(async (options) => {
4044
3245
  const { MockWebSocketServer } = await import('../testing/mock-websocket-server.js');
4045
3246
  const server = await MockWebSocketServer.create({
4046
- port,
4047
- host,
4048
- echo,
4049
- delay,
3247
+ port: parseInt(options.port),
3248
+ host: options.host,
3249
+ echo: options.echo,
3250
+ delay: parseInt(options.delay),
4050
3251
  });
4051
3252
  console.log(colors.green(`
4052
3253
  ┌─────────────────────────────────────────────┐
4053
3254
  │ ${colors.bold('Recker Mock WebSocket Server')} │
4054
3255
  ├─────────────────────────────────────────────┤
4055
3256
  │ URL: ${colors.cyan(server.url.padEnd(37))}│
4056
- │ Echo: ${colors.yellow((echo ? 'Enabled' : 'Disabled').padEnd(36))}│
4057
- │ Delay: ${colors.gray((delay + 'ms').padEnd(35))}│
3257
+ │ Echo: ${colors.yellow((options.echo ? 'Enabled' : 'Disabled').padEnd(36))}│
3258
+ │ Delay: ${colors.gray((options.delay + 'ms').padEnd(35))}│
4058
3259
  ├─────────────────────────────────────────────┤
4059
3260
  │ Press ${colors.bold('Ctrl+C')} to stop │
4060
3261
  └─────────────────────────────────────────────┘
@@ -4078,53 +3279,36 @@ ${colors.bold(colors.yellow('Examples:'))}
4078
3279
  serve
4079
3280
  .command('sse')
4080
3281
  .description('Start a mock SSE (Server-Sent Events) server')
4081
- .argument('[args...]', 'Options: port=8081 host=127.0.0.1 path=/events heartbeat=0')
3282
+ .option('-p, --port <number>', 'Port to listen on', '8081')
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')
4082
3286
  .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
-
4089
3287
  ${colors.bold(colors.yellow('Examples:'))}
4090
3288
  ${colors.green('$ rek serve sse')} ${colors.gray('Start on port 8081')}
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')}
3289
+ ${colors.green('$ rek serve sse -p 9000')} ${colors.gray('Start on port 9000')}
3290
+ ${colors.green('$ rek serve sse --heartbeat 5000')} ${colors.gray('Send heartbeat every 5s')}
4093
3291
 
4094
3292
  ${colors.bold(colors.yellow('Interactive Commands:'))}
4095
3293
  Type a message and press Enter to broadcast it to all clients.
4096
3294
  `)
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
- }
3295
+ .action(async (options) => {
4112
3296
  const { MockSSEServer } = await import('../testing/mock-sse-server.js');
4113
3297
  const readline = await import('node:readline');
4114
3298
  const server = await MockSSEServer.create({
4115
- port,
4116
- host,
4117
- path,
3299
+ port: parseInt(options.port),
3300
+ host: options.host,
3301
+ path: options.path,
4118
3302
  });
4119
- if (heartbeat > 0) {
4120
- server.startPeriodicEvents('heartbeat', heartbeat);
3303
+ if (parseInt(options.heartbeat) > 0) {
3304
+ server.startPeriodicEvents('heartbeat', parseInt(options.heartbeat));
4121
3305
  }
4122
3306
  console.log(colors.green(`
4123
3307
  ┌─────────────────────────────────────────────┐
4124
3308
  │ ${colors.bold('Recker Mock SSE Server')} │
4125
3309
  ├─────────────────────────────────────────────┤
4126
3310
  │ URL: ${colors.cyan(server.url.padEnd(37))}│
4127
- │ Heartbeat: ${colors.yellow((heartbeat === 0 ? 'Disabled' : heartbeat + 'ms').padEnd(31))}│
3311
+ │ Heartbeat: ${colors.yellow((options.heartbeat === '0' ? 'Disabled' : options.heartbeat + 'ms').padEnd(31))}│
4128
3312
  ├─────────────────────────────────────────────┤
4129
3313
  │ Type message + Enter to broadcast │
4130
3314
  │ Press ${colors.bold('Ctrl+C')} to stop │
@@ -4156,51 +3340,30 @@ ${colors.bold(colors.yellow('Interactive Commands:'))}
4156
3340
  serve
4157
3341
  .command('hls')
4158
3342
  .description('Start a mock HLS streaming server')
4159
- .argument('[args...]', 'Options: port=8082 host=127.0.0.1 mode=vod segments=10 duration=6 qualities=720p,480p,360p')
3343
+ .option('-p, --port <number>', 'Port to listen on', '8082')
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')
4160
3349
  .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
-
4169
3350
  ${colors.bold(colors.yellow('Examples:'))}
4170
3351
  ${colors.green('$ rek serve hls')} ${colors.gray('Start VOD server')}
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')}
3352
+ ${colors.green('$ rek serve hls --mode live')} ${colors.gray('Start live stream')}
3353
+ ${colors.green('$ rek serve hls --segments 20')} ${colors.gray('VOD with 20 segments')}
3354
+ ${colors.green('$ rek serve hls --qualities 1080p,720p,480p')}
4174
3355
 
4175
3356
  ${colors.bold(colors.yellow('Endpoints:'))}
4176
3357
  ${colors.cyan('/master.m3u8')} Master playlist (multi-quality)
4177
3358
  ${colors.cyan('/playlist.m3u8')} Single quality playlist
4178
3359
  ${colors.cyan('/<quality>/playlist.m3u8')} Quality-specific playlist
4179
3360
  `)
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
- }
3361
+ .action(async (options) => {
4201
3362
  const { MockHlsServer } = await import('../testing/mock-hls-server.js');
4202
3363
  const http = await import('node:http');
4203
- const qualities = qualitiesStr.split(',').map(q => q.trim());
3364
+ const port = parseInt(options.port);
3365
+ const host = options.host;
3366
+ const qualities = options.qualities.split(',').map(q => q.trim());
4204
3367
  const resolutions = ['1920x1080', '1280x720', '854x480', '640x360', '426x240'];
4205
3368
  const bandwidths = [5000000, 2500000, 1400000, 800000, 500000];
4206
3369
  const variants = qualities.map((name, i) => ({
@@ -4211,9 +3374,9 @@ ${colors.bold(colors.yellow('Endpoints:'))}
4211
3374
  const baseUrl = `http://${host}:${port}`;
4212
3375
  const hlsServer = await MockHlsServer.create({
4213
3376
  baseUrl,
4214
- mode: mode,
4215
- segmentCount: segments,
4216
- segmentDuration: duration,
3377
+ mode: options.mode,
3378
+ segmentCount: parseInt(options.segments),
3379
+ segmentDuration: parseInt(options.duration),
4217
3380
  multiQuality: variants.length > 1,
4218
3381
  variants: variants.length > 1 ? variants : undefined,
4219
3382
  });
@@ -4239,9 +3402,9 @@ ${colors.bold(colors.yellow('Endpoints:'))}
4239
3402
  │ ${colors.bold('Recker Mock HLS Server')} │
4240
3403
  ├─────────────────────────────────────────────┤
4241
3404
  │ Master: ${colors.cyan((hlsServer.manifestUrl).padEnd(34))}│
4242
- │ Mode: ${colors.yellow(mode.padEnd(36))}│
4243
- │ Segments: ${colors.gray(String(segments).padEnd(32))}│
4244
- │ Duration: ${colors.gray((duration + 's').padEnd(32))}│
3405
+ │ Mode: ${colors.yellow(options.mode.padEnd(36))}│
3406
+ │ Segments: ${colors.gray(options.segments.padEnd(32))}│
3407
+ │ Duration: ${colors.gray((options.duration + 's').padEnd(32))}│
4245
3408
  │ Qualities: ${colors.cyan(qualities.join(', ').padEnd(31))}│
4246
3409
  ├─────────────────────────────────────────────┤
4247
3410
  │ Press ${colors.bold('Ctrl+C')} to stop │
@@ -4258,46 +3421,30 @@ ${colors.bold(colors.yellow('Endpoints:'))}
4258
3421
  serve
4259
3422
  .command('udp')
4260
3423
  .description('Start a mock UDP server')
4261
- .argument('[args...]', 'Options: port=9000 host=127.0.0.1 echo noecho')
3424
+ .option('-p, --port <number>', 'Port to listen on', '9000')
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')
4262
3428
  .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
-
4269
3429
  ${colors.bold(colors.yellow('Examples:'))}
4270
3430
  ${colors.green('$ rek serve udp')} ${colors.gray('Start on port 9000')}
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')}
3431
+ ${colors.green('$ rek serve udp -p 5353')} ${colors.gray('Start on port 5353')}
3432
+ ${colors.green('$ rek serve udp --no-echo')} ${colors.gray('Disable echo')}
4273
3433
  `)
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
- }
3434
+ .action(async (options) => {
4288
3435
  const { MockUDPServer } = await import('../testing/mock-udp-server.js');
4289
3436
  const server = new MockUDPServer({
4290
- port,
4291
- host,
4292
- echo,
3437
+ port: parseInt(options.port),
3438
+ host: options.host,
3439
+ echo: options.echo,
4293
3440
  });
4294
3441
  await server.start();
4295
3442
  console.log(colors.green(`
4296
3443
  ┌─────────────────────────────────────────────┐
4297
3444
  │ ${colors.bold('Recker Mock UDP Server')} │
4298
3445
  ├─────────────────────────────────────────────┤
4299
- │ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
4300
- │ Echo: ${colors.yellow((echo ? 'Enabled' : 'Disabled').padEnd(36))}│
3446
+ │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
3447
+ │ Echo: ${colors.yellow((options.echo ? 'Enabled' : 'Disabled').padEnd(36))}│
4301
3448
  ├─────────────────────────────────────────────┤
4302
3449
  │ Press ${colors.bold('Ctrl+C')} to stop │
4303
3450
  └─────────────────────────────────────────────┘
@@ -4315,16 +3462,13 @@ ${colors.bold(colors.yellow('Examples:'))}
4315
3462
  serve
4316
3463
  .command('dns')
4317
3464
  .description('Start a mock DNS server')
4318
- .argument('[args...]', 'Options: port=5353 host=127.0.0.1 delay=0')
3465
+ .option('-p, --port <number>', 'Port to listen on', '5353')
3466
+ .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3467
+ .option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
4319
3468
  .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
-
4325
3469
  ${colors.bold(colors.yellow('Examples:'))}
4326
3470
  ${colors.green('$ rek serve dns')} ${colors.gray('Start on port 5353')}
4327
- ${colors.green('$ rek serve dns port=53')} ${colors.gray('Start on standard port (requires root)')}
3471
+ ${colors.green('$ rek serve dns -p 53')} ${colors.gray('Start on standard port (requires root)')}
4328
3472
  ${colors.green('$ dig @127.0.0.1 -p 5353 example.com')} ${colors.gray('Test with dig')}
4329
3473
 
4330
3474
  ${colors.bold(colors.yellow('Default Records:'))}
@@ -4332,32 +3476,21 @@ ${colors.bold(colors.yellow('Default Records:'))}
4332
3476
  ${colors.cyan('example.com')} A, AAAA, NS, MX, TXT records
4333
3477
  ${colors.cyan('test.local')} A: 192.168.1.100
4334
3478
  `)
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
- }
3479
+ .action(async (options) => {
4347
3480
  const { MockDnsServer } = await import('../testing/mock-dns-server.js');
4348
3481
  const server = await MockDnsServer.create({
4349
- port,
4350
- host,
4351
- delay,
3482
+ port: parseInt(options.port),
3483
+ host: options.host,
3484
+ delay: parseInt(options.delay),
4352
3485
  });
4353
3486
  console.log(colors.green(`
4354
3487
  ┌─────────────────────────────────────────────┐
4355
3488
  │ ${colors.bold('Recker Mock DNS Server')} │
4356
3489
  ├─────────────────────────────────────────────┤
4357
- │ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
3490
+ │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
4358
3491
  │ Protocol: ${colors.yellow('UDP'.padEnd(32))}│
4359
3492
  ├─────────────────────────────────────────────┤
4360
- │ Test: dig @${host} -p ${port} example.com │
3493
+ │ Test: dig @${options.host} -p ${options.port} example.com │
4361
3494
  │ Press ${colors.bold('Ctrl+C')} to stop │
4362
3495
  └─────────────────────────────────────────────┘
4363
3496
  `));
@@ -4373,13 +3506,10 @@ ${colors.bold(colors.yellow('Default Records:'))}
4373
3506
  serve
4374
3507
  .command('whois')
4375
3508
  .description('Start a mock WHOIS server')
4376
- .argument('[args...]', 'Options: port=4343 host=127.0.0.1 delay=0')
3509
+ .option('-p, --port <number>', 'Port to listen on', '4343')
3510
+ .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3511
+ .option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
4377
3512
  .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
-
4383
3513
  ${colors.bold(colors.yellow('Examples:'))}
4384
3514
  ${colors.green('$ rek serve whois')} ${colors.gray('Start on port 4343')}
4385
3515
  ${colors.green('$ whois -h 127.0.0.1 -p 4343 example.com')} ${colors.gray('Test with whois')}
@@ -4389,32 +3519,21 @@ ${colors.bold(colors.yellow('Default Domains:'))}
4389
3519
  ${colors.cyan('google.com')} MarkMonitor registrar
4390
3520
  ${colors.cyan('test.local')} Test domain
4391
3521
  `)
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
- }
3522
+ .action(async (options) => {
4404
3523
  const { MockWhoisServer } = await import('../testing/mock-whois-server.js');
4405
3524
  const server = await MockWhoisServer.create({
4406
- port,
4407
- host,
4408
- delay,
3525
+ port: parseInt(options.port),
3526
+ host: options.host,
3527
+ delay: parseInt(options.delay),
4409
3528
  });
4410
3529
  console.log(colors.green(`
4411
3530
  ┌─────────────────────────────────────────────┐
4412
3531
  │ ${colors.bold('Recker Mock WHOIS Server')} │
4413
3532
  ├─────────────────────────────────────────────┤
4414
- │ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
3533
+ │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
4415
3534
  │ Protocol: ${colors.yellow('TCP'.padEnd(32))}│
4416
3535
  ├─────────────────────────────────────────────┤
4417
- │ Test: whois -h ${host} -p ${port} example.com │
3536
+ │ Test: whois -h ${options.host} -p ${options.port} example.com │
4418
3537
  │ Press ${colors.bold('Ctrl+C')} to stop │
4419
3538
  └─────────────────────────────────────────────┘
4420
3539
  `));
@@ -4430,15 +3549,12 @@ ${colors.bold(colors.yellow('Default Domains:'))}
4430
3549
  serve
4431
3550
  .command('telnet')
4432
3551
  .description('Start a mock Telnet server')
4433
- .argument('[args...]', 'Options: port=2323 host=127.0.0.1 echo noecho delay=0')
3552
+ .option('-p, --port <number>', 'Port to listen on', '2323')
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')
4434
3557
  .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
-
4442
3558
  ${colors.bold(colors.yellow('Examples:'))}
4443
3559
  ${colors.green('$ rek serve telnet')} ${colors.gray('Start on port 2323')}
4444
3560
  ${colors.green('$ telnet localhost 2323')} ${colors.gray('Connect to server')}
@@ -4451,38 +3567,22 @@ ${colors.bold(colors.yellow('Built-in Commands:'))}
4451
3567
  ${colors.cyan('ping')} Returns "pong"
4452
3568
  ${colors.cyan('quit')} Disconnect
4453
3569
  `)
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
- }
3570
+ .action(async (options) => {
4471
3571
  const { MockTelnetServer } = await import('../testing/mock-telnet-server.js');
4472
3572
  const server = await MockTelnetServer.create({
4473
- port,
4474
- host,
4475
- echo,
4476
- delay,
3573
+ port: parseInt(options.port),
3574
+ host: options.host,
3575
+ echo: options.echo,
3576
+ delay: parseInt(options.delay),
4477
3577
  });
4478
3578
  console.log(colors.green(`
4479
3579
  ┌─────────────────────────────────────────────┐
4480
3580
  │ ${colors.bold('Recker Mock Telnet Server')} │
4481
3581
  ├─────────────────────────────────────────────┤
4482
- │ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
4483
- │ Echo: ${colors.yellow((echo ? 'Enabled' : 'Disabled').padEnd(36))}│
3582
+ │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
3583
+ │ Echo: ${colors.yellow((options.echo ? 'Enabled' : 'Disabled').padEnd(36))}│
4484
3584
  ├─────────────────────────────────────────────┤
4485
- │ Connect: telnet ${host} ${port} │
3585
+ │ Connect: telnet ${options.host} ${options.port} │
4486
3586
  │ Press ${colors.bold('Ctrl+C')} to stop │
4487
3587
  └─────────────────────────────────────────────┘
4488
3588
  `));
@@ -4504,21 +3604,18 @@ ${colors.bold(colors.yellow('Built-in Commands:'))}
4504
3604
  serve
4505
3605
  .command('ftp')
4506
3606
  .description('Start a mock FTP server')
4507
- .argument('[args...]', 'Options: port=2121 host=127.0.0.1 username=user password=pass anonymous noanonymous delay=0')
3607
+ .option('-p, --port <number>', 'Port to listen on', '2121')
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')
4508
3614
  .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
-
4518
3615
  ${colors.bold(colors.yellow('Examples:'))}
4519
3616
  ${colors.green('$ rek serve ftp')} ${colors.gray('Start on port 2121')}
4520
3617
  ${colors.green('$ ftp localhost 2121')} ${colors.gray('Connect to server')}
4521
- ${colors.green('$ rek serve ftp noanonymous')} ${colors.gray('Require authentication')}
3618
+ ${colors.green('$ rek serve ftp --no-anonymous')} ${colors.gray('Require authentication')}
4522
3619
 
4523
3620
  ${colors.bold(colors.yellow('Default Files:'))}
4524
3621
  ${colors.cyan('/welcome.txt')} Welcome message
@@ -4530,47 +3627,25 @@ ${colors.bold(colors.yellow('Credentials:'))}
4530
3627
  Username: ${colors.cyan('user')} Password: ${colors.cyan('pass')}
4531
3628
  Or use anonymous login with user: ${colors.cyan('anonymous')}
4532
3629
  `)
4533
- .action(async (args) => {
3630
+ .action(async (options) => {
4534
3631
  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
- }
4557
3632
  const server = await MockFtpServer.create({
4558
- port,
4559
- host,
4560
- username,
4561
- password,
4562
- anonymous,
4563
- delay,
3633
+ port: parseInt(options.port),
3634
+ host: options.host,
3635
+ username: options.username,
3636
+ password: options.password,
3637
+ anonymous: options.anonymous,
3638
+ delay: parseInt(options.delay),
4564
3639
  });
4565
3640
  console.log(colors.green(`
4566
3641
  ┌─────────────────────────────────────────────┐
4567
3642
  │ ${colors.bold('Recker Mock FTP Server')} │
4568
3643
  ├─────────────────────────────────────────────┤
4569
- │ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
4570
- │ Anonymous: ${colors.yellow((anonymous ? 'Allowed' : 'Disabled').padEnd(31))}│
4571
- │ User: ${colors.cyan(username.padEnd(36))}│
3644
+ │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
3645
+ │ Anonymous: ${colors.yellow((options.anonymous ? 'Allowed' : 'Disabled').padEnd(31))}│
3646
+ │ User: ${colors.cyan(options.username.padEnd(36))}│
4572
3647
  ├─────────────────────────────────────────────┤
4573
- │ Connect: ftp ${host} ${port} │
3648
+ │ Connect: ftp ${options.host} ${options.port} │
4574
3649
  │ Press ${colors.bold('Ctrl+C')} to stop │
4575
3650
  └─────────────────────────────────────────────┘
4576
3651
  `));
@@ -4592,25 +3667,22 @@ ${colors.bold(colors.yellow('Credentials:'))}
4592
3667
  program
4593
3668
  .command('mcp')
4594
3669
  .description('Start MCP server for AI agents to access Recker documentation')
4595
- .argument('[args...]', 'Options: transport=stdio port=3100 docs=<path> tools=<paths> debug')
3670
+ .option('-t, --transport <mode>', 'Transport mode: stdio, http, sse', 'stdio')
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')
4596
3675
  .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
-
4604
3676
  ${colors.bold(colors.yellow('Transport Modes:'))}
4605
3677
  ${colors.cyan('stdio')} ${colors.gray('(default)')} For Claude Code and other CLI tools
4606
3678
  ${colors.cyan('http')} Simple HTTP POST endpoint
4607
3679
  ${colors.cyan('sse')} HTTP + Server-Sent Events for real-time notifications
4608
3680
 
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')}
3681
+ ${colors.bold(colors.yellow('Usage:'))}
3682
+ ${colors.green('$ rek mcp')} ${colors.gray('Start in stdio mode (for Claude Code)')}
3683
+ ${colors.green('$ rek mcp -t http')} ${colors.gray('Start HTTP server on port 3100')}
3684
+ ${colors.green('$ rek mcp -t sse -p 8080')} ${colors.gray('Start SSE server on custom port')}
3685
+ ${colors.green('$ rek mcp --debug')} ${colors.gray('Enable debug logging')}
4614
3686
 
4615
3687
  ${colors.bold(colors.yellow('Tools provided:'))}
4616
3688
  ${colors.cyan('search_docs')} Search documentation by keyword
@@ -4626,31 +3698,15 @@ ${colors.bold(colors.yellow('Claude Code config (~/.claude.json):'))}
4626
3698
  }
4627
3699
  }`)}
4628
3700
  `)
4629
- .action(async (args) => {
3701
+ .action(async (options) => {
4630
3702
  const { MCPServer } = await import('../mcp/server.js');
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
- }
3703
+ const transport = options.transport;
4648
3704
  const server = new MCPServer({
4649
3705
  transport,
4650
- port,
4651
- docsPath,
4652
- debug,
4653
- toolPaths,
3706
+ port: parseInt(options.port),
3707
+ docsPath: options.docs,
3708
+ debug: options.debug,
3709
+ toolPaths: options.tools,
4654
3710
  });
4655
3711
  if (transport === 'stdio') {
4656
3712
  await server.start();
@@ -4669,7 +3725,7 @@ ${colors.bold(colors.yellow('Claude Code config (~/.claude.json):'))}
4669
3725
  │ ${colors.bold('Recker MCP Server')} │
4670
3726
  ├─────────────────────────────────────────────┤
4671
3727
  │ Transport: ${colors.cyan(transport.padEnd(31))}│
4672
- │ Endpoint: ${colors.cyan(`http://localhost:${port}`.padEnd(32))}│
3728
+ │ Endpoint: ${colors.cyan(`http://localhost:${options.port}`.padEnd(32))}│
4673
3729
  │ Docs indexed: ${colors.yellow(String(server.getDocsCount()).padEnd(28))}│
4674
3730
  ├─────────────────────────────────────────────┤${endpoints}
4675
3731
  ├─────────────────────────────────────────────┤
@@ -5345,39 +4401,21 @@ ${colors.bold(colors.yellow('Examples:'))}
5345
4401
  });
5346
4402
  program
5347
4403
  .command('proxy')
5348
- .description('Route requests through HTTP or SOCKS proxy')
4404
+ .description('Make a request through a proxy')
5349
4405
  .argument('<proxy>', 'Proxy URL (http://host:port or socks5://host:port)')
5350
4406
  .argument('<url>', 'Target URL')
5351
4407
  .argument('[args...]', 'Request arguments: method=x key=value key:=json Header:value')
5352
4408
  .addHelpText('after', `
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
4409
+ ${colors.bold(colors.yellow('Parameters:'))}
4410
+ method=<method> HTTP method (default: GET)
4411
+ key=value String data
4412
+ key:=json JSON data
4413
+ Header:value HTTP headers (Key:Value format)
5371
4414
 
5372
4415
  ${colors.bold(colors.yellow('Examples:'))}
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')}
4416
+ ${colors.green('$ rek proxy http://proxy.example.com:8080 https://api.com/data')}
4417
+ ${colors.green('$ rek proxy socks5://127.0.0.1:1080 https://api.com/users')}
4418
+ ${colors.green('$ rek proxy http://user:pass@proxy.com:3128 api.com/endpoint method=POST data=test')}
5381
4419
  `)
5382
4420
  .action(async (proxy, url, args) => {
5383
4421
  const { createClient } = await import('../core/client.js');
@@ -5436,13 +4474,6 @@ ${colors.bold(colors.yellow('Examples:'))}
5436
4474
  process.exit(1);
5437
4475
  }
5438
4476
  });
5439
- function applyHelpAfterError(cmd) {
5440
- cmd.showHelpAfterError(true);
5441
- for (const subcmd of cmd.commands) {
5442
- applyHelpAfterError(subcmd);
5443
- }
5444
- }
5445
- applyHelpAfterError(program);
5446
4477
  program.parse();
5447
4478
  }
5448
4479
  main().catch((error) => {