recker 1.0.39 → 1.0.40-next.b4fa2a1

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