recker 1.0.37 → 1.0.39-next.8bbc11c

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
@@ -5,6 +5,7 @@ import { join } from 'node:path';
5
5
  import colors from '../utils/colors.js';
6
6
  import { formatColumns } from '../utils/columns.js';
7
7
  import { summarizeErrors, formatErrorSummary } from './helpers.js';
8
+ import { getVersion, formatVersionInfo } from '../version.js';
8
9
  async function readStdin() {
9
10
  if (process.stdin.isTTY) {
10
11
  return null;
@@ -69,13 +70,7 @@ async function main() {
69
70
  const { handleRequest } = await import('./handler.js');
70
71
  const { resolvePreset } = await import('./presets.js');
71
72
  const presets = await import('../presets/index.js');
72
- let version = '0.0.0';
73
- try {
74
- const pkg = await import('../../package.json', { with: { type: 'json' } });
75
- version = pkg.default?.version || '0.0.0';
76
- }
77
- catch {
78
- }
73
+ const version = await getVersion();
79
74
  function parseMixedArgs(args, hasPreset = false) {
80
75
  const headers = {};
81
76
  const data = {};
@@ -133,6 +128,7 @@ async function main() {
133
128
  .name('rek')
134
129
  .description('The HTTP Client for Humans (and Robots)')
135
130
  .version(version)
131
+ .showHelpAfterError(true)
136
132
  .argument('[args...]', 'URL, Method, Headers (Key:Value), Data (key=value)')
137
133
  .option('-v, --verbose', 'Show full request/response details')
138
134
  .option('-q, --quiet', 'Output only response body (no colors, perfect for piping)')
@@ -281,7 +277,27 @@ Error: ${error.message}`));
281
277
  });
282
278
  program
283
279
  .command('completion')
284
- .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
+ `)
285
301
  .action(() => {
286
302
  const script = `
287
303
  ###-begin-rek-completion-###
@@ -326,12 +342,71 @@ complete -F _rek_completions rek
326
342
  `;
327
343
  console.log(script);
328
344
  });
345
+ program
346
+ .command('version')
347
+ .alias('info')
348
+ .description('Show version and environment information')
349
+ .argument('[args...]', 'Options: short format=json')
350
+ .addHelpText('after', `
351
+ ${colors.bold(colors.blue('What it does:'))}
352
+ Displays the installed Recker version along with your Node.js version,
353
+ operating system, and architecture. Useful for debugging, reporting issues,
354
+ or verifying which version is running in production environments.
355
+
356
+ ${colors.bold(colors.yellow('Examples:'))}
357
+ ${colors.green('$ rek version')} Show full version info
358
+ ${colors.green('$ rek version short')} Just the version number (for scripts)
359
+ ${colors.green('$ rek version format=json')} Machine-readable JSON output
360
+ ${colors.green('$ rek info')} Alias for version
361
+
362
+ ${colors.bold(colors.yellow('Options:'))}
363
+ ${colors.cyan('short')} Show only version number
364
+ ${colors.cyan('format=json')} Output as JSON
365
+ `)
366
+ .action(async (args) => {
367
+ const isShort = args.includes('short');
368
+ const formatJson = args.some(a => a === 'format=json' || a === 'json');
369
+ if (isShort) {
370
+ console.log(version);
371
+ return;
372
+ }
373
+ if (formatJson) {
374
+ const { getVersionInfo } = await import('../version.js');
375
+ const info = await getVersionInfo();
376
+ console.log(JSON.stringify(info, null, 2));
377
+ return;
378
+ }
379
+ const versionInfo = await formatVersionInfo(true);
380
+ console.log(colors.bold(colors.cyan('recker')) + ' ' + colors.green(version));
381
+ console.log(colors.gray(versionInfo));
382
+ });
329
383
  program
330
384
  .command('shell')
331
385
  .alias('interactive')
332
386
  .alias('repl')
333
387
  .description('Start the interactive Rek Shell')
334
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
+ `)
335
410
  .action(async (options) => {
336
411
  if (options.env !== false) {
337
412
  try {
@@ -349,7 +424,22 @@ complete -F _rek_completions rek
349
424
  program
350
425
  .command('docs [query...]')
351
426
  .alias('?')
352
- .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
+ `)
353
443
  .action(async (queryParts) => {
354
444
  const query = queryParts.join(' ').trim();
355
445
  const { openSearchPanel } = await import('./tui/search-panel.js');
@@ -358,8 +448,39 @@ complete -F _rek_completions rek
358
448
  program
359
449
  .command('security')
360
450
  .alias('headers')
361
- .description('Analyze HTTP response headers for security best practices')
451
+ .alias('grade')
452
+ .description('Grade a website\'s security headers (A+ to F)')
362
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
+ `)
363
484
  .action(async (url) => {
364
485
  if (!url.startsWith('http'))
365
486
  url = `https://${url}`;
@@ -401,16 +522,30 @@ ${colors.bold('Details:')}`);
401
522
  });
402
523
  program
403
524
  .command('seo')
404
- .description('Analyze page SEO (title, meta, headings, links, images, structured data)')
525
+ .alias('audit')
526
+ .description('Analyze a page\'s SEO health (80+ checks)')
405
527
  .argument('<url>', 'URL to analyze')
406
- .option('-a, --all', 'Show all checks including passed ones')
407
- .option('--format <format>', 'Output format: text (default) or json', 'text')
528
+ .argument('[args...]', 'Options: all format=json')
408
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
+
409
540
  ${colors.bold(colors.yellow('Examples:'))}
410
541
  ${colors.green('$ rek seo example.com')} ${colors.gray('Basic SEO analysis')}
411
- ${colors.green('$ rek seo example.com -a')} ${colors.gray('Show all checks')}
412
- ${colors.green('$ rek seo example.com --format json')} ${colors.gray('Output as JSON')}
413
- ${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
414
549
 
415
550
  ${colors.bold(colors.yellow('Checks:'))}
416
551
  ${colors.cyan('Title Tag')} Length and presence
@@ -423,10 +558,11 @@ ${colors.bold(colors.yellow('Checks:'))}
423
558
  ${colors.cyan('Structured Data')} JSON-LD presence
424
559
  ${colors.cyan('Technical')} Canonical, viewport, lang
425
560
  `)
426
- .action(async (url, options) => {
561
+ .action(async (url, args) => {
427
562
  if (!url.startsWith('http'))
428
563
  url = `https://${url}`;
429
- const isJson = options.format === 'json';
564
+ const showAll = args.includes('all');
565
+ const isJson = args.some(a => a === 'format=json' || a === 'json');
430
566
  const { createClient } = await import('../core/client.js');
431
567
  const { analyzeSeo } = await import('../seo/analyzer.js');
432
568
  if (!isJson) {
@@ -503,11 +639,11 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
503
639
  console.log(` ${colors.gray('Images:')} ${report.images.total} (${report.images.withAlt} with alt, ${report.images.withoutAlt} without)`);
504
640
  console.log('');
505
641
  console.log(`${colors.bold('Checks:')}`);
506
- const checksToShow = options.all
642
+ const checksToShow = showAll
507
643
  ? report.checks
508
644
  : report.checks.filter(c => c.status !== 'pass' && c.status !== 'info');
509
- if (checksToShow.length === 0 && !options.all) {
510
- 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.'));
511
647
  }
512
648
  else {
513
649
  for (const check of checksToShow) {
@@ -543,12 +679,15 @@ ${colors.gray('Grade:')} ${gradeColor(colors.bold(report.grade))} ${colors.gray
543
679
  .command('robots')
544
680
  .description('Validate and analyze robots.txt file')
545
681
  .argument('<url>', 'Website URL or direct robots.txt URL')
546
- .option('--format <format>', 'Output format: text (default) or json', 'text')
682
+ .argument('[args...]', 'Options: format=json')
547
683
  .addHelpText('after', `
548
684
  ${colors.bold(colors.yellow('Examples:'))}
549
685
  ${colors.green('$ rek robots example.com')} ${colors.gray('Validate robots.txt')}
550
686
  ${colors.green('$ rek robots example.com/robots.txt')} ${colors.gray('Direct URL')}
551
- ${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
552
691
 
553
692
  ${colors.bold(colors.yellow('Checks:'))}
554
693
  ${colors.cyan('Syntax')} Valid robots.txt syntax
@@ -557,14 +696,14 @@ ${colors.bold(colors.yellow('Checks:'))}
557
696
  ${colors.cyan('Crawl-delay')} Aggressive crawl delay
558
697
  ${colors.cyan('AI Bots')} GPTBot, ClaudeBot, Anthropic blocks
559
698
  `)
560
- .action(async (url, options) => {
699
+ .action(async (url, args) => {
561
700
  if (!url.startsWith('http'))
562
701
  url = `https://${url}`;
563
702
  if (!url.includes('robots.txt')) {
564
703
  const urlObj = new URL(url);
565
704
  url = `${urlObj.origin}/robots.txt`;
566
705
  }
567
- const isJson = options.format === 'json';
706
+ const isJson = args.some(a => a === 'format=json' || a === 'json');
568
707
  if (!isJson) {
569
708
  console.log(colors.gray(`Fetching robots.txt from ${url}...`));
570
709
  }
@@ -654,14 +793,17 @@ ${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')
654
793
  .command('sitemap')
655
794
  .description('Validate and analyze sitemap.xml file')
656
795
  .argument('<url>', 'Website URL or direct sitemap URL')
657
- .option('--format <format>', 'Output format: text (default) or json', 'text')
658
- .option('--discover', 'Discover all sitemaps via robots.txt')
796
+ .argument('[args...]', 'Options: discover format=json')
659
797
  .addHelpText('after', `
660
798
  ${colors.bold(colors.yellow('Examples:'))}
661
799
  ${colors.green('$ rek sitemap example.com')} ${colors.gray('Validate sitemap')}
662
800
  ${colors.green('$ rek sitemap example.com/sitemap.xml')} ${colors.gray('Direct URL')}
663
- ${colors.green('$ rek sitemap example.com --discover')} ${colors.gray('Find all sitemaps')}
664
- ${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
665
807
 
666
808
  ${colors.bold(colors.yellow('Checks:'))}
667
809
  ${colors.cyan('Structure')} Valid XML sitemap format
@@ -670,12 +812,13 @@ ${colors.bold(colors.yellow('Checks:'))}
670
812
  ${colors.cyan('URLs')} Valid, no duplicates, same domain
671
813
  ${colors.cyan('Lastmod')} Valid dates, not in future
672
814
  `)
673
- .action(async (url, options) => {
815
+ .action(async (url, args) => {
674
816
  if (!url.startsWith('http'))
675
817
  url = `https://${url}`;
676
- const isJson = options.format === 'json';
818
+ const isJson = args.some(a => a === 'format=json' || a === 'json');
819
+ const doDiscover = args.includes('discover');
677
820
  try {
678
- if (options.discover) {
821
+ if (doDiscover) {
679
822
  const { discoverSitemaps } = await import('../seo/validators/sitemap.js');
680
823
  if (!isJson) {
681
824
  console.log(colors.gray(`Discovering sitemaps for ${new URL(url).origin}...`));
@@ -785,14 +928,17 @@ ${colors.gray('Type:')} ${result.parseResult?.type === 'sitemapindex' ? 'Sitemap
785
928
  .command('llms')
786
929
  .description('Validate and analyze llms.txt file (AI/LLM optimization)')
787
930
  .argument('[url]', 'Website URL or direct llms.txt URL')
788
- .option('--format <format>', 'Output format: text (default) or json', 'text')
789
- .option('--template', 'Generate a template llms.txt file')
931
+ .argument('[args...]', 'Options: template format=json')
790
932
  .addHelpText('after', `
791
933
  ${colors.bold(colors.yellow('Examples:'))}
792
934
  ${colors.green('$ rek llms example.com')} ${colors.gray('Validate llms.txt')}
793
935
  ${colors.green('$ rek llms example.com/llms.txt')} ${colors.gray('Direct URL')}
794
- ${colors.green('$ rek llms example.com --format json')} ${colors.gray('JSON output')}
795
- ${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
796
942
 
797
943
  ${colors.bold(colors.yellow('About llms.txt:'))}
798
944
  A proposed standard for providing LLM-friendly content.
@@ -805,9 +951,10 @@ ${colors.bold(colors.yellow('Checks:'))}
805
951
  ${colors.cyan('Description')} Site description block
806
952
  ${colors.cyan('Sections')} Content sections with links
807
953
  `)
808
- .action(async (url, options) => {
809
- const isJson = options.format === 'json';
810
- 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) {
811
958
  const { generateLlmsTxtTemplate } = await import('../seo/validators/llms-txt.js');
812
959
  const template = generateLlmsTxtTemplate({
813
960
  siteName: 'Your Site Name',
@@ -929,10 +1076,20 @@ ${colors.gray('Valid:')} ${result.valid ? colors.green('Yes') : colors.red('No')
929
1076
  });
930
1077
  program
931
1078
  .command('spider')
932
- .description('Crawl a website following internal links')
1079
+ .alias('crawl')
1080
+ .description('Crawl a website and analyze all pages')
933
1081
  .argument('<url>', 'Starting URL to crawl')
934
1082
  .argument('[args...]', 'Options: depth=N limit=N concurrency=N seo focus=MODE output=file.json')
935
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
+
936
1093
  ${colors.bold(colors.yellow('Examples:'))}
937
1094
  ${colors.green('$ rek spider example.com')} ${colors.gray('Crawl with defaults')}
938
1095
  ${colors.green('$ rek spider example.com depth=3 limit=50')} ${colors.gray('Depth 3, max 50 pages')}
@@ -941,6 +1098,7 @@ ${colors.bold(colors.yellow('Examples:'))}
941
1098
  ${colors.green('$ rek spider example.com seo focus=links')} ${colors.gray('Focus on link issues')}
942
1099
  ${colors.green('$ rek spider example.com seo focus=security')} ${colors.gray('Focus on security issues')}
943
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')}
944
1102
 
945
1103
  ${colors.bold(colors.yellow('Options:'))}
946
1104
  ${colors.cyan('depth=N')} Max link depth to follow (default: 5)
@@ -948,6 +1106,7 @@ ${colors.bold(colors.yellow('Options:'))}
948
1106
  ${colors.cyan('concurrency=N')} Parallel requests (default: 5)
949
1107
  ${colors.cyan('seo')} Enable SEO analysis mode
950
1108
  ${colors.cyan('focus=MODE')} Focus analysis on specific area (requires seo)
1109
+ ${colors.cyan('format=json')} Output JSON to stdout (for piping)
951
1110
  ${colors.cyan('output=file.json')} Save JSON report to file
952
1111
 
953
1112
  ${colors.bold(colors.yellow('Focus Modes:'))}
@@ -964,6 +1123,7 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
964
1123
  let concurrency = 5;
965
1124
  let seoEnabled = false;
966
1125
  let outputFile = '';
1126
+ let formatJson = false;
967
1127
  let focusMode = 'all';
968
1128
  const focusCategories = {
969
1129
  links: ['links'],
@@ -989,6 +1149,9 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
989
1149
  else if (arg.startsWith('output=')) {
990
1150
  outputFile = arg.split('=')[1] || '';
991
1151
  }
1152
+ else if (arg === 'format=json' || arg === '--format=json') {
1153
+ formatJson = true;
1154
+ }
992
1155
  else if (arg.startsWith('focus=')) {
993
1156
  const mode = arg.split('=')[1] || 'all';
994
1157
  if (mode in focusCategories) {
@@ -1003,14 +1166,16 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1003
1166
  }
1004
1167
  if (!url.startsWith('http'))
1005
1168
  url = `https://${url}`;
1006
- const modeLabel = seoEnabled ? colors.magenta(' + SEO') : '';
1007
- const focusLabel = focusMode !== 'all' ? colors.cyan(` [focus: ${focusMode}]`) : '';
1008
- console.log(colors.cyan(`\nSpider starting: ${url}`));
1009
- console.log(colors.gray(` Depth: ${maxDepth} | Limit: ${maxPages} | Concurrency: ${concurrency}${modeLabel}${focusLabel}`));
1010
- if (outputFile) {
1011
- 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('');
1012
1178
  }
1013
- console.log('');
1014
1179
  try {
1015
1180
  if (seoEnabled) {
1016
1181
  const { SeoSpider } = await import('../seo/index.js');
@@ -1024,11 +1189,91 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1024
1189
  output: outputFile || undefined,
1025
1190
  focusCategories: focusCategories[focusMode],
1026
1191
  focusMode,
1027
- onProgress: (progress) => {
1192
+ onProgress: formatJson ? undefined : (progress) => {
1028
1193
  process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
1029
1194
  },
1030
1195
  });
1031
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
+ }
1032
1277
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
1033
1278
  console.log(colors.green(`\n✔ SEO Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
1034
1279
  console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
@@ -1208,11 +1453,41 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1208
1453
  concurrency,
1209
1454
  sameDomain: true,
1210
1455
  delay: 100,
1211
- onProgress: (progress) => {
1456
+ onProgress: formatJson ? undefined : (progress) => {
1212
1457
  process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
1213
1458
  },
1214
1459
  });
1215
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
+ }
1216
1491
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
1217
1492
  console.log(colors.green(`\n✔ Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
1218
1493
  console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
@@ -1277,10 +1552,19 @@ ${colors.bold(colors.yellow('Focus Modes:'))}
1277
1552
  });
1278
1553
  program
1279
1554
  .command('scrape')
1280
- .description('Scrape data from a web page using CSS selectors')
1555
+ .alias('extract')
1556
+ .description('Extract data from web pages with CSS selectors')
1281
1557
  .argument('<url>', 'URL to scrape')
1282
1558
  .argument('[args...]', 'Options: select=SELECTOR, attr=NAME, links, images, meta, tables, scripts, jsonld')
1283
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
+
1284
1568
  ${colors.bold(colors.yellow('Examples:'))}
1285
1569
  ${colors.green('$ rek scrape example.com')} ${colors.gray('# Basic page info')}
1286
1570
  ${colors.green('$ rek scrape example.com select="h1"')} ${colors.gray('# Extract h1 text')}
@@ -1478,20 +1762,32 @@ ${colors.bold('Images:')} ${imageCount}
1478
1762
  });
1479
1763
  program
1480
1764
  .command('ai')
1481
- .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)')
1482
1768
  .argument('<preset>', 'AI preset to use (e.g., @openai, @anthropic, @groq)')
1483
- .argument('<prompt...>', 'The prompt to send')
1484
- .option('-m, --model <model>', 'Override default model')
1485
- .option('-t, --temperature <temp>', 'Temperature (0-1)', '0.7')
1486
- .option('--max-tokens <tokens>', 'Max tokens in response', '2048')
1487
- .option('-w, --wait', 'Wait for full response (disable streaming)')
1488
- .option('-j, --json', 'Output raw JSON response')
1489
- .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>')
1490
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
+
1491
1787
  ${colors.bold(colors.yellow('Examples:'))}
1492
1788
  ${colors.green('$ rek ai @openai "What is the capital of France?"')}
1493
- ${colors.green('$ rek ai @anthropic "Explain quantum computing" -m claude-sonnet-4-20250514')}
1494
- ${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')}
1495
1791
  ${colors.green('$ rek ai @openai "Translate to Spanish: Hello world"')}
1496
1792
 
1497
1793
  ${colors.bold(colors.yellow('Available AI Presets:'))}
@@ -1513,14 +1809,39 @@ ${colors.bold(colors.yellow('Note:'))}
1513
1809
  This command sends a single prompt without conversation memory.
1514
1810
  For chat with memory, use: ${colors.cyan('rek shell')} then ${colors.cyan('@openai Your message')}
1515
1811
  `)
1516
- .action(async (preset, promptParts, options) => {
1517
- if (options.env !== undefined) {
1518
- 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);
1519
1840
  }
1520
1841
  else {
1521
1842
  try {
1522
- const envPath = join(process.cwd(), '.env');
1523
- await fs.access(envPath);
1843
+ const envFilePath = join(process.cwd(), '.env');
1844
+ await fs.access(envFilePath);
1524
1845
  await loadEnvFile(true);
1525
1846
  }
1526
1847
  catch {
@@ -1541,7 +1862,7 @@ ${colors.bold(colors.yellow('Note:'))}
1541
1862
  console.log(colors.gray('Use an AI preset like @openai, @anthropic, @groq, etc.'));
1542
1863
  process.exit(1);
1543
1864
  }
1544
- const prompt = promptParts.join(' ');
1865
+ const prompt = actualPromptParts.join(' ');
1545
1866
  if (!prompt.trim()) {
1546
1867
  console.error(colors.red('Error: Prompt is required'));
1547
1868
  process.exit(1);
@@ -1549,16 +1870,16 @@ ${colors.bold(colors.yellow('Note:'))}
1549
1870
  try {
1550
1871
  const { createClient } = await import('../core/client.js');
1551
1872
  const client = createClient(presetConfig);
1552
- if (options.model) {
1873
+ if (model) {
1553
1874
  client.ai.setMemoryConfig({ systemPrompt: undefined });
1554
- client._aiConfig.model = options.model;
1875
+ client._aiConfig.model = model;
1555
1876
  }
1556
- if (!options.json) {
1877
+ if (!jsonOutput) {
1557
1878
  console.log(colors.gray(`Using @${presetName} (${client._aiConfig.model})...\n`));
1558
1879
  }
1559
- if (options.wait || options.json) {
1880
+ if (wait || jsonOutput) {
1560
1881
  const response = await client.ai.prompt(prompt);
1561
- if (options.json) {
1882
+ if (jsonOutput) {
1562
1883
  console.log(JSON.stringify({
1563
1884
  content: response.content,
1564
1885
  model: response.model,
@@ -1594,8 +1915,36 @@ ${colors.bold(colors.yellow('Note:'))}
1594
1915
  });
1595
1916
  program
1596
1917
  .command('ip')
1597
- .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')
1598
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
+ `)
1599
1948
  .action(async (address) => {
1600
1949
  const { getIpInfo, isGeoIPAvailable } = await import('../mcp/ip-intel.js');
1601
1950
  if (!isGeoIPAvailable()) {
@@ -1634,9 +1983,34 @@ ${colors.bold('Network:')}
1634
1983
  program
1635
1984
  .command('tls')
1636
1985
  .alias('ssl')
1986
+ .alias('cert')
1637
1987
  .description('Inspect TLS/SSL certificate of a host')
1638
1988
  .argument('<host>', 'Hostname or IP address')
1639
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
+ `)
1640
2014
  .action(async (host, port) => {
1641
2015
  const { inspectTLS } = await import('../utils/tls-inspector.js');
1642
2016
  console.log(colors.gray(`Inspecting TLS certificate for ${host}:${port}...`));
@@ -1704,15 +2078,43 @@ ${colors.bold('Fingerprints:')}
1704
2078
  });
1705
2079
  program
1706
2080
  .command('whois')
1707
- .description('WHOIS lookup for domains and IP addresses')
2081
+ .description('Look up domain registration and ownership info')
1708
2082
  .argument('<query>', 'Domain name or IP address')
1709
- .option('-r, --raw', 'Show raw WHOIS response')
1710
- .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) => {
1711
2112
  const { whois } = await import('../utils/whois.js');
2113
+ const raw = args.includes('raw');
1712
2114
  console.log(colors.gray(`Looking up WHOIS for ${query}...`));
1713
2115
  try {
1714
2116
  const result = await whois(query);
1715
- if (options.raw) {
2117
+ if (raw) {
1716
2118
  console.log(result.raw);
1717
2119
  return;
1718
2120
  }
@@ -1747,8 +2149,33 @@ ${colors.bold('Server:')} ${result.server}
1747
2149
  });
1748
2150
  program
1749
2151
  .command('rdap')
1750
- .description('RDAP lookup (modern WHOIS replacement)')
2152
+ .description('RDAP lookup (modern WHOIS with JSON)')
1751
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
+ `)
1752
2179
  .action(async (domain) => {
1753
2180
  const { rdap } = await import('../utils/rdap.js');
1754
2181
  const { Client } = await import('../core/client.js');
@@ -1800,14 +2227,46 @@ ${colors.bold('Status:')} ${result.status?.join(', ') || 'N/A'}
1800
2227
  });
1801
2228
  program
1802
2229
  .command('ping')
1803
- .description('TCP connectivity check to a host')
2230
+ .description('Test TCP connectivity to host:port')
1804
2231
  .argument('<host>', 'Hostname or IP address')
1805
- .argument('[port]', 'Port number (default: 80 for HTTP, 443 for HTTPS)', '443')
1806
- .option('-c, --count <n>', 'Number of pings', '4')
1807
- .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) => {
1808
2260
  const net = await import('node:net');
1809
- const count = parseInt(options.count);
1810
- 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;
1811
2270
  const results = [];
1812
2271
  console.log(colors.gray(`Pinging ${host}:${portNum}...`));
1813
2272
  console.log('');
@@ -1857,20 +2316,48 @@ ${colors.bold('Statistics:')}
1857
2316
  .command('ls')
1858
2317
  .description('List files in a remote directory')
1859
2318
  .argument('<host>', 'FTP server hostname')
1860
- .argument('[path]', 'Remote path to list', '/')
1861
- .option('-u, --user <username>', 'Username', 'anonymous')
1862
- .option('-p, --pass <password>', 'Password', 'anonymous@')
1863
- .option('-P, --port <port>', 'Port number', '21')
1864
- .option('--secure', 'Use FTPS (explicit TLS)')
1865
- .option('--implicit', 'Use implicit FTPS (port 990)')
1866
- .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) => {
1867
2334
  const { createFTP } = await import('../protocols/ftp.js');
1868
- 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;
1869
2356
  const client = createFTP({
1870
2357
  host,
1871
- port: parseInt(options.port),
1872
- user: options.user,
1873
- password: options.pass,
2358
+ port,
2359
+ user,
2360
+ password: pass,
1874
2361
  secure,
1875
2362
  });
1876
2363
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -1913,22 +2400,50 @@ ${colors.bold('Statistics:')}
1913
2400
  .description('Download a file from FTP server')
1914
2401
  .argument('<host>', 'FTP server hostname')
1915
2402
  .argument('<remote>', 'Remote file path')
1916
- .argument('[local]', 'Local file path (default: same filename)')
1917
- .option('-u, --user <username>', 'Username', 'anonymous')
1918
- .option('-p, --pass <password>', 'Password', 'anonymous@')
1919
- .option('-P, --port <port>', 'Port number', '21')
1920
- .option('--secure', 'Use FTPS (explicit TLS)')
1921
- .option('--implicit', 'Use implicit FTPS (port 990)')
1922
- .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) => {
1923
2418
  const { createFTP } = await import('../protocols/ftp.js');
1924
- const path = await import('node:path');
1925
- const localPath = local || path.basename(remote);
1926
- 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;
1927
2442
  const client = createFTP({
1928
2443
  host,
1929
- port: parseInt(options.port),
1930
- user: options.user,
1931
- password: options.pass,
2444
+ port,
2445
+ user,
2446
+ password: pass,
1932
2447
  secure,
1933
2448
  });
1934
2449
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -1968,22 +2483,50 @@ ${colors.bold('Statistics:')}
1968
2483
  .description('Upload a file to FTP server')
1969
2484
  .argument('<host>', 'FTP server hostname')
1970
2485
  .argument('<local>', 'Local file path')
1971
- .argument('[remote]', 'Remote file path (default: same filename)')
1972
- .option('-u, --user <username>', 'Username', 'anonymous')
1973
- .option('-p, --pass <password>', 'Password', 'anonymous@')
1974
- .option('-P, --port <port>', 'Port number', '21')
1975
- .option('--secure', 'Use FTPS (explicit TLS)')
1976
- .option('--implicit', 'Use implicit FTPS (port 990)')
1977
- .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) => {
1978
2501
  const { createFTP } = await import('../protocols/ftp.js');
1979
- const path = await import('node:path');
1980
- const remotePath = remote || '/' + path.basename(local);
1981
- 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;
1982
2525
  const client = createFTP({
1983
2526
  host,
1984
- port: parseInt(options.port),
1985
- user: options.user,
1986
- password: options.pass,
2527
+ port,
2528
+ user,
2529
+ password: pass,
1987
2530
  secure,
1988
2531
  });
1989
2532
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -2023,19 +2566,44 @@ ${colors.bold('Statistics:')}
2023
2566
  .description('Delete a file from FTP server')
2024
2567
  .argument('<host>', 'FTP server hostname')
2025
2568
  .argument('<path>', 'Remote file path to delete')
2026
- .option('-u, --user <username>', 'Username', 'anonymous')
2027
- .option('-p, --pass <password>', 'Password', 'anonymous@')
2028
- .option('-P, --port <port>', 'Port number', '21')
2029
- .option('--secure', 'Use FTPS (explicit TLS)')
2030
- .option('--implicit', 'Use implicit FTPS (port 990)')
2031
- .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) => {
2032
2583
  const { createFTP } = await import('../protocols/ftp.js');
2033
- 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;
2034
2602
  const client = createFTP({
2035
2603
  host,
2036
- port: parseInt(options.port),
2037
- user: options.user,
2038
- password: options.pass,
2604
+ port,
2605
+ user,
2606
+ password: pass,
2039
2607
  secure,
2040
2608
  });
2041
2609
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -2066,19 +2634,44 @@ ${colors.bold('Statistics:')}
2066
2634
  .description('Create a directory on FTP server')
2067
2635
  .argument('<host>', 'FTP server hostname')
2068
2636
  .argument('<path>', 'Remote directory path to create')
2069
- .option('-u, --user <username>', 'Username', 'anonymous')
2070
- .option('-p, --pass <password>', 'Password', 'anonymous@')
2071
- .option('-P, --port <port>', 'Port number', '21')
2072
- .option('--secure', 'Use FTPS (explicit TLS)')
2073
- .option('--implicit', 'Use implicit FTPS (port 990)')
2074
- .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) => {
2075
2651
  const { createFTP } = await import('../protocols/ftp.js');
2076
- 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;
2077
2670
  const client = createFTP({
2078
2671
  host,
2079
- port: parseInt(options.port),
2080
- user: options.user,
2081
- password: options.pass,
2672
+ port,
2673
+ user,
2674
+ password: pass,
2082
2675
  secure,
2083
2676
  });
2084
2677
  console.log(colors.gray(`Connecting to ${host}...`));
@@ -2108,15 +2701,31 @@ ${colors.bold('Statistics:')}
2108
2701
  .command('telnet')
2109
2702
  .description('Connect to a Telnet server')
2110
2703
  .argument('<host>', 'Hostname or IP address')
2111
- .argument('[port]', 'Port number', '23')
2112
- .option('-t, --timeout <ms>', 'Connection timeout in ms', '30000')
2113
- .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) => {
2114
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
+ }
2115
2724
  console.log(colors.gray(`Connecting to ${host}:${port}...`));
2116
2725
  const client = createTelnet({
2117
2726
  host,
2118
- port: parseInt(port),
2119
- timeout: parseInt(options.timeout),
2727
+ port,
2728
+ timeout,
2120
2729
  });
2121
2730
  try {
2122
2731
  await client.connect();
@@ -2166,9 +2775,37 @@ ${colors.bold('Statistics:')}
2166
2775
  });
2167
2776
  dns
2168
2777
  .command('lookup')
2169
- .description('Perform DNS lookup for any record type')
2778
+ .alias('resolve')
2779
+ .description('Look up DNS records (A, MX, TXT, etc)')
2170
2780
  .argument('<domain>', 'Domain name to lookup')
2171
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
+ `)
2172
2809
  .action(async (domain, type) => {
2173
2810
  const { dnsLookup } = await import('../utils/dns-toolkit.js');
2174
2811
  console.log(colors.gray(`Looking up ${type.toUpperCase()} records for ${domain}...`));
@@ -2443,27 +3080,54 @@ ${colors.gray('Status:')} ${statusIcon}
2443
3080
  dns
2444
3081
  .command('generate-dmarc')
2445
3082
  .description('Generate a DMARC record interactively')
2446
- .option('-p, --policy <policy>', 'Policy: none, quarantine, reject', 'none')
2447
- .option('--subdomain-policy <policy>', 'Subdomain policy')
2448
- .option('--pct <percent>', 'Percentage of emails to apply policy', '100')
2449
- .option('--rua <emails>', 'Aggregate report email(s), comma-separated')
2450
- .option('--ruf <emails>', 'Forensic report email(s), comma-separated')
2451
- .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) => {
2452
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
+ }
2453
3117
  const dmarcOptions = {
2454
- policy: options.policy,
3118
+ policy,
2455
3119
  };
2456
- if (options.subdomainPolicy) {
2457
- dmarcOptions.subdomainPolicy = options.subdomainPolicy;
3120
+ if (subdomainPolicy) {
3121
+ dmarcOptions.subdomainPolicy = subdomainPolicy;
2458
3122
  }
2459
- if (options.pct && options.pct !== '100') {
2460
- dmarcOptions.percentage = parseInt(options.pct);
3123
+ if (pct && pct !== '100') {
3124
+ dmarcOptions.percentage = parseInt(pct);
2461
3125
  }
2462
- if (options.rua) {
2463
- dmarcOptions.aggregateReports = options.rua.split(',').map((e) => e.trim());
3126
+ if (rua) {
3127
+ dmarcOptions.aggregateReports = rua.split(',').map((e) => e.trim());
2464
3128
  }
2465
- if (options.ruf) {
2466
- dmarcOptions.forensicReports = options.ruf.split(',').map((e) => e.trim());
3129
+ if (ruf) {
3130
+ dmarcOptions.forensicReports = ruf.split(',').map((e) => e.trim());
2467
3131
  }
2468
3132
  const record = generateDmarc(dmarcOptions);
2469
3133
  console.log(`
@@ -2545,22 +3209,68 @@ ${colors.bold(colors.yellow('Record Types:'))}
2545
3209
  });
2546
3210
  program
2547
3211
  .command('graphql')
2548
- .description('Execute a GraphQL query')
3212
+ .alias('gql')
3213
+ .description('Execute GraphQL queries and mutations')
2549
3214
  .argument('<url>', 'GraphQL endpoint URL')
2550
- .option('-q, --query <query>', 'GraphQL query string')
2551
- .option('-f, --file <file>', 'Path to GraphQL query file')
2552
- .option('-v, --variables <json>', 'Variables as JSON string')
2553
- .option('--var-file <file>', 'Path to variables JSON file')
2554
- .option('-H, --header <header>', 'Add header (can be used multiple times)', (val, prev) => [...prev, val], [])
2555
- .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) => {
2556
3249
  const { graphql } = await import('../plugins/graphql.js');
2557
3250
  const { createClient } = await import('../core/client.js');
2558
3251
  const fs = await import('node:fs/promises');
2559
- 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;
2560
3270
  let variables = {};
2561
- if (options.file) {
3271
+ if (queryFile) {
2562
3272
  try {
2563
- query = await fs.readFile(options.file, 'utf-8');
3273
+ query = await fs.readFile(queryFile, 'utf-8');
2564
3274
  }
2565
3275
  catch (err) {
2566
3276
  console.error(colors.red(`Failed to read query file: ${err.message}`));
@@ -2568,22 +3278,22 @@ ${colors.bold(colors.yellow('Record Types:'))}
2568
3278
  }
2569
3279
  }
2570
3280
  if (!query) {
2571
- console.error(colors.red('Error: Query is required. Use --query or --file'));
2572
- 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 } }"'));
2573
3283
  process.exit(1);
2574
3284
  }
2575
- if (options.variables) {
3285
+ if (variablesStr) {
2576
3286
  try {
2577
- variables = JSON.parse(options.variables);
3287
+ variables = JSON.parse(variablesStr);
2578
3288
  }
2579
3289
  catch {
2580
- console.error(colors.red('Invalid JSON in --variables'));
3290
+ console.error(colors.red('Invalid JSON in variables='));
2581
3291
  process.exit(1);
2582
3292
  }
2583
3293
  }
2584
- else if (options.varFile) {
3294
+ else if (varFile) {
2585
3295
  try {
2586
- const content = await fs.readFile(options.varFile, 'utf-8');
3296
+ const content = await fs.readFile(varFile, 'utf-8');
2587
3297
  variables = JSON.parse(content);
2588
3298
  }
2589
3299
  catch (err) {
@@ -2594,7 +3304,7 @@ ${colors.bold(colors.yellow('Record Types:'))}
2594
3304
  const headers = {
2595
3305
  'Content-Type': 'application/json',
2596
3306
  };
2597
- for (const h of options.header) {
3307
+ for (const h of headerArgs) {
2598
3308
  const [key, ...valueParts] = h.split(':');
2599
3309
  headers[key.trim()] = valueParts.join(':').trim();
2600
3310
  }
@@ -2758,21 +3468,48 @@ ${colors.bold(colors.yellow('Record Types:'))}
2758
3468
  .command('download')
2759
3469
  .description('Download an HLS stream')
2760
3470
  .argument('<url>', 'HLS playlist URL')
2761
- .argument('[output]', 'Output file path', 'output.ts')
2762
- .option('-q, --quality <quality>', 'Quality: highest, lowest, or resolution (e.g., 720p)')
2763
- .option('--live', 'Enable live stream mode')
2764
- .option('-d, --duration <seconds>', 'Duration for live recording in seconds')
2765
- .option('-c, --concurrency <n>', 'Concurrent segment downloads', '4')
2766
- .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) => {
2767
3487
  const { hls } = await import('../plugins/hls.js');
2768
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
+ }
2769
3506
  const client = new Client();
2770
3507
  console.log(colors.gray(`Downloading HLS stream from ${url}...`));
2771
3508
  console.log(colors.gray(`Output: ${output}`));
2772
3509
  console.log('');
2773
3510
  try {
2774
3511
  const hlsOptions = {
2775
- concurrency: parseInt(options.concurrency),
3512
+ concurrency,
2776
3513
  onProgress: (p) => {
2777
3514
  const segs = p.totalSegments
2778
3515
  ? `${p.downloadedSegments}/${p.totalSegments}`
@@ -2781,17 +3518,17 @@ ${colors.bold(colors.yellow('Record Types:'))}
2781
3518
  process.stdout.write(`\r ${colors.cyan(segs)} segments | ${colors.cyan(mb + ' MB')} downloaded`);
2782
3519
  },
2783
3520
  };
2784
- if (options.quality) {
2785
- if (options.quality === 'highest' || options.quality === 'lowest') {
2786
- hlsOptions.quality = options.quality;
3521
+ if (quality) {
3522
+ if (quality === 'highest' || quality === 'lowest') {
3523
+ hlsOptions.quality = quality;
2787
3524
  }
2788
- else if (options.quality.includes('p')) {
2789
- hlsOptions.quality = { resolution: options.quality };
3525
+ else if (quality.includes('p')) {
3526
+ hlsOptions.quality = { resolution: quality };
2790
3527
  }
2791
3528
  }
2792
- if (options.live) {
2793
- hlsOptions.live = options.duration
2794
- ? { duration: parseInt(options.duration) * 1000 }
3529
+ if (live) {
3530
+ hlsOptions.live = duration
3531
+ ? { duration: duration * 1000 }
2795
3532
  : true;
2796
3533
  }
2797
3534
  await hls(client, url, hlsOptions).download(output);
@@ -2964,19 +3701,23 @@ ${colors.bold(colors.yellow('Examples:'))}
2964
3701
  .command('info')
2965
3702
  .description('Show information about a HAR file')
2966
3703
  .argument('<file>', 'HAR file to inspect')
2967
- .option('--json', 'Output as JSON')
3704
+ .argument('[args...]', 'Options: json')
2968
3705
  .addHelpText('after', `
3706
+ ${colors.bold(colors.yellow('Options:'))}
3707
+ ${colors.cyan('json')} Output as JSON
3708
+
2969
3709
  ${colors.bold(colors.yellow('Examples:'))}
2970
3710
  ${colors.green('$ rek har info api.har')}
2971
- ${colors.green('$ rek har info api.har --json')}
3711
+ ${colors.green('$ rek har info api.har json')}
2972
3712
  `)
2973
- .action(async (file, options) => {
3713
+ .action(async (file, args) => {
2974
3714
  const { promises: fsPromises } = await import('node:fs');
3715
+ const jsonOutput = args.includes('json');
2975
3716
  try {
2976
3717
  const content = await fsPromises.readFile(file, 'utf-8');
2977
3718
  const har = JSON.parse(content);
2978
3719
  const entries = har.log?.entries || [];
2979
- if (options.json) {
3720
+ if (jsonOutput) {
2980
3721
  const info = {
2981
3722
  version: har.log?.version,
2982
3723
  creator: har.log?.creator,
@@ -3103,28 +3844,66 @@ ${colors.bold(colors.yellow('Examples:'))}
3103
3844
  const serve = program.command('serve').description('Start mock servers for testing protocols');
3104
3845
  serve
3105
3846
  .command('http')
3106
- .description('Start a mock HTTP server')
3107
- .option('-p, --port <number>', 'Port to listen on', '3000')
3108
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3109
- .option('--echo', 'Echo request body back in response')
3110
- .option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
3111
- .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')
3112
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
+
3113
3873
  ${colors.bold(colors.yellow('Examples:'))}
3114
- ${colors.green('$ rek serve http')} ${colors.gray('Start on port 3000')}
3115
- ${colors.green('$ rek serve http -p 8080')} ${colors.gray('Start on port 8080')}
3116
- ${colors.green('$ rek serve http --echo')} ${colors.gray('Echo mode')}
3117
- ${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
3118
3878
  `)
3119
- .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
+ }
3120
3899
  const { MockHttpServer } = await import('../testing/mock-http-server.js');
3121
3900
  const server = await MockHttpServer.create({
3122
- port: parseInt(options.port),
3123
- host: options.host,
3124
- delay: parseInt(options.delay),
3125
- cors: options.cors,
3901
+ port,
3902
+ host,
3903
+ delay,
3904
+ cors,
3126
3905
  });
3127
- if (options.echo) {
3906
+ if (echo) {
3128
3907
  server.any('/*', (req) => ({
3129
3908
  status: 200,
3130
3909
  body: {
@@ -3141,8 +3920,8 @@ ${colors.bold(colors.yellow('Examples:'))}
3141
3920
  │ ${colors.bold('Recker Mock HTTP Server')} │
3142
3921
  ├─────────────────────────────────────────────┤
3143
3922
  │ URL: ${colors.cyan(server.url.padEnd(37))}│
3144
- │ Mode: ${colors.yellow((options.echo ? 'Echo' : 'Default').padEnd(36))}│
3145
- │ 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))}│
3146
3925
  ├─────────────────────────────────────────────┤
3147
3926
  │ Press ${colors.bold('Ctrl+C')} to stop │
3148
3927
  └─────────────────────────────────────────────┘
@@ -3160,15 +3939,18 @@ ${colors.bold(colors.yellow('Examples:'))}
3160
3939
  .command('webhook')
3161
3940
  .alias('wh')
3162
3941
  .description('Start a webhook receiver server')
3163
- .option('-p, --port <number>', 'Port to listen on', '3000')
3164
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3165
- .option('-s, --status <code>', 'Response status code (200 or 204)', '204')
3166
- .option('-q, --quiet', 'Disable logging', false)
3942
+ .argument('[args...]', 'Options: port=3000 host=127.0.0.1 status=204 quiet')
3167
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
+
3168
3950
  ${colors.bold(colors.yellow('Examples:'))}
3169
3951
  ${colors.green('$ rek serve webhook')} ${colors.gray('Start on port 3000')}
3170
- ${colors.green('$ rek serve wh -p 8080')} ${colors.gray('Start on port 8080')}
3171
- ${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')}
3172
3954
 
3173
3955
  ${colors.bold(colors.yellow('Endpoints:'))}
3174
3956
  * / ${colors.gray('Receive webhook without ID')}
@@ -3177,18 +3959,32 @@ ${colors.bold(colors.yellow('Endpoints:'))}
3177
3959
  ${colors.bold(colors.yellow('Methods:'))}
3178
3960
  ${colors.gray('GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS')}
3179
3961
  `)
3180
- .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
+ }
3181
3977
  const { createWebhookServer } = await import('../testing/mock-http-server.js');
3182
- const status = parseInt(options.status);
3978
+ const status = statusCode;
3183
3979
  if (status !== 200 && status !== 204) {
3184
3980
  console.error(colors.red('Status must be 200 or 204'));
3185
3981
  process.exit(1);
3186
3982
  }
3187
3983
  const server = await createWebhookServer({
3188
- port: parseInt(options.port),
3189
- host: options.host,
3984
+ port,
3985
+ host,
3190
3986
  status,
3191
- log: !options.quiet,
3987
+ log: !quiet,
3192
3988
  });
3193
3989
  console.log(colors.green(`
3194
3990
  ┌─────────────────────────────────────────────┐
@@ -3214,32 +4010,51 @@ ${colors.bold(colors.yellow('Methods:'))}
3214
4010
  .command('websocket')
3215
4011
  .alias('ws')
3216
4012
  .description('Start a mock WebSocket server')
3217
- .option('-p, --port <number>', 'Port to listen on', '8080')
3218
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3219
- .option('--echo', 'Echo messages back (default: true)', true)
3220
- .option('--no-echo', 'Disable echo mode')
3221
- .option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
4013
+ .argument('[args...]', 'Options: port=8080 host=127.0.0.1 echo noecho delay=0')
3222
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
+
3223
4022
  ${colors.bold(colors.yellow('Examples:'))}
3224
4023
  ${colors.green('$ rek serve websocket')} ${colors.gray('Start on port 8080')}
3225
- ${colors.green('$ rek serve ws -p 9000')} ${colors.gray('Start on port 9000')}
3226
- ${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')}
3227
4026
  `)
3228
- .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
+ }
3229
4044
  const { MockWebSocketServer } = await import('../testing/mock-websocket-server.js');
3230
4045
  const server = await MockWebSocketServer.create({
3231
- port: parseInt(options.port),
3232
- host: options.host,
3233
- echo: options.echo,
3234
- delay: parseInt(options.delay),
4046
+ port,
4047
+ host,
4048
+ echo,
4049
+ delay,
3235
4050
  });
3236
4051
  console.log(colors.green(`
3237
4052
  ┌─────────────────────────────────────────────┐
3238
4053
  │ ${colors.bold('Recker Mock WebSocket Server')} │
3239
4054
  ├─────────────────────────────────────────────┤
3240
4055
  │ URL: ${colors.cyan(server.url.padEnd(37))}│
3241
- │ Echo: ${colors.yellow((options.echo ? 'Enabled' : 'Disabled').padEnd(36))}│
3242
- │ 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))}│
3243
4058
  ├─────────────────────────────────────────────┤
3244
4059
  │ Press ${colors.bold('Ctrl+C')} to stop │
3245
4060
  └─────────────────────────────────────────────┘
@@ -3263,36 +4078,53 @@ ${colors.bold(colors.yellow('Examples:'))}
3263
4078
  serve
3264
4079
  .command('sse')
3265
4080
  .description('Start a mock SSE (Server-Sent Events) server')
3266
- .option('-p, --port <number>', 'Port to listen on', '8081')
3267
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3268
- .option('--path <string>', 'SSE endpoint path', '/events')
3269
- .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')
3270
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
+
3271
4089
  ${colors.bold(colors.yellow('Examples:'))}
3272
4090
  ${colors.green('$ rek serve sse')} ${colors.gray('Start on port 8081')}
3273
- ${colors.green('$ rek serve sse -p 9000')} ${colors.gray('Start on port 9000')}
3274
- ${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')}
3275
4093
 
3276
4094
  ${colors.bold(colors.yellow('Interactive Commands:'))}
3277
4095
  Type a message and press Enter to broadcast it to all clients.
3278
4096
  `)
3279
- .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
+ }
3280
4112
  const { MockSSEServer } = await import('../testing/mock-sse-server.js');
3281
4113
  const readline = await import('node:readline');
3282
4114
  const server = await MockSSEServer.create({
3283
- port: parseInt(options.port),
3284
- host: options.host,
3285
- path: options.path,
4115
+ port,
4116
+ host,
4117
+ path,
3286
4118
  });
3287
- if (parseInt(options.heartbeat) > 0) {
3288
- server.startPeriodicEvents('heartbeat', parseInt(options.heartbeat));
4119
+ if (heartbeat > 0) {
4120
+ server.startPeriodicEvents('heartbeat', heartbeat);
3289
4121
  }
3290
4122
  console.log(colors.green(`
3291
4123
  ┌─────────────────────────────────────────────┐
3292
4124
  │ ${colors.bold('Recker Mock SSE Server')} │
3293
4125
  ├─────────────────────────────────────────────┤
3294
4126
  │ URL: ${colors.cyan(server.url.padEnd(37))}│
3295
- │ Heartbeat: ${colors.yellow((options.heartbeat === '0' ? 'Disabled' : options.heartbeat + 'ms').padEnd(31))}│
4127
+ │ Heartbeat: ${colors.yellow((heartbeat === 0 ? 'Disabled' : heartbeat + 'ms').padEnd(31))}│
3296
4128
  ├─────────────────────────────────────────────┤
3297
4129
  │ Type message + Enter to broadcast │
3298
4130
  │ Press ${colors.bold('Ctrl+C')} to stop │
@@ -3324,30 +4156,51 @@ ${colors.bold(colors.yellow('Interactive Commands:'))}
3324
4156
  serve
3325
4157
  .command('hls')
3326
4158
  .description('Start a mock HLS streaming server')
3327
- .option('-p, --port <number>', 'Port to listen on', '8082')
3328
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3329
- .option('--mode <type>', 'Stream mode: vod, live, event', 'vod')
3330
- .option('--segments <number>', 'Number of segments (VOD mode)', '10')
3331
- .option('--duration <seconds>', 'Segment duration in seconds', '6')
3332
- .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')
3333
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
+
3334
4169
  ${colors.bold(colors.yellow('Examples:'))}
3335
4170
  ${colors.green('$ rek serve hls')} ${colors.gray('Start VOD server')}
3336
- ${colors.green('$ rek serve hls --mode live')} ${colors.gray('Start live stream')}
3337
- ${colors.green('$ rek serve hls --segments 20')} ${colors.gray('VOD with 20 segments')}
3338
- ${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')}
3339
4174
 
3340
4175
  ${colors.bold(colors.yellow('Endpoints:'))}
3341
4176
  ${colors.cyan('/master.m3u8')} Master playlist (multi-quality)
3342
4177
  ${colors.cyan('/playlist.m3u8')} Single quality playlist
3343
4178
  ${colors.cyan('/<quality>/playlist.m3u8')} Quality-specific playlist
3344
4179
  `)
3345
- .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
+ }
3346
4201
  const { MockHlsServer } = await import('../testing/mock-hls-server.js');
3347
4202
  const http = await import('node:http');
3348
- const port = parseInt(options.port);
3349
- const host = options.host;
3350
- const qualities = options.qualities.split(',').map(q => q.trim());
4203
+ const qualities = qualitiesStr.split(',').map(q => q.trim());
3351
4204
  const resolutions = ['1920x1080', '1280x720', '854x480', '640x360', '426x240'];
3352
4205
  const bandwidths = [5000000, 2500000, 1400000, 800000, 500000];
3353
4206
  const variants = qualities.map((name, i) => ({
@@ -3358,9 +4211,9 @@ ${colors.bold(colors.yellow('Endpoints:'))}
3358
4211
  const baseUrl = `http://${host}:${port}`;
3359
4212
  const hlsServer = await MockHlsServer.create({
3360
4213
  baseUrl,
3361
- mode: options.mode,
3362
- segmentCount: parseInt(options.segments),
3363
- segmentDuration: parseInt(options.duration),
4214
+ mode: mode,
4215
+ segmentCount: segments,
4216
+ segmentDuration: duration,
3364
4217
  multiQuality: variants.length > 1,
3365
4218
  variants: variants.length > 1 ? variants : undefined,
3366
4219
  });
@@ -3386,9 +4239,9 @@ ${colors.bold(colors.yellow('Endpoints:'))}
3386
4239
  │ ${colors.bold('Recker Mock HLS Server')} │
3387
4240
  ├─────────────────────────────────────────────┤
3388
4241
  │ Master: ${colors.cyan((hlsServer.manifestUrl).padEnd(34))}│
3389
- │ Mode: ${colors.yellow(options.mode.padEnd(36))}│
3390
- │ Segments: ${colors.gray(options.segments.padEnd(32))}│
3391
- │ 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))}│
3392
4245
  │ Qualities: ${colors.cyan(qualities.join(', ').padEnd(31))}│
3393
4246
  ├─────────────────────────────────────────────┤
3394
4247
  │ Press ${colors.bold('Ctrl+C')} to stop │
@@ -3405,30 +4258,46 @@ ${colors.bold(colors.yellow('Endpoints:'))}
3405
4258
  serve
3406
4259
  .command('udp')
3407
4260
  .description('Start a mock UDP server')
3408
- .option('-p, --port <number>', 'Port to listen on', '9000')
3409
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3410
- .option('--echo', 'Echo messages back (default: true)', true)
3411
- .option('--no-echo', 'Disable echo mode')
4261
+ .argument('[args...]', 'Options: port=9000 host=127.0.0.1 echo noecho')
3412
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
+
3413
4269
  ${colors.bold(colors.yellow('Examples:'))}
3414
4270
  ${colors.green('$ rek serve udp')} ${colors.gray('Start on port 9000')}
3415
- ${colors.green('$ rek serve udp -p 5353')} ${colors.gray('Start on port 5353')}
3416
- ${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')}
3417
4273
  `)
3418
- .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
+ }
3419
4288
  const { MockUDPServer } = await import('../testing/mock-udp-server.js');
3420
4289
  const server = new MockUDPServer({
3421
- port: parseInt(options.port),
3422
- host: options.host,
3423
- echo: options.echo,
4290
+ port,
4291
+ host,
4292
+ echo,
3424
4293
  });
3425
4294
  await server.start();
3426
4295
  console.log(colors.green(`
3427
4296
  ┌─────────────────────────────────────────────┐
3428
4297
  │ ${colors.bold('Recker Mock UDP Server')} │
3429
4298
  ├─────────────────────────────────────────────┤
3430
- │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
3431
- │ 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))}│
3432
4301
  ├─────────────────────────────────────────────┤
3433
4302
  │ Press ${colors.bold('Ctrl+C')} to stop │
3434
4303
  └─────────────────────────────────────────────┘
@@ -3446,13 +4315,16 @@ ${colors.bold(colors.yellow('Examples:'))}
3446
4315
  serve
3447
4316
  .command('dns')
3448
4317
  .description('Start a mock DNS server')
3449
- .option('-p, --port <number>', 'Port to listen on', '5353')
3450
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3451
- .option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
4318
+ .argument('[args...]', 'Options: port=5353 host=127.0.0.1 delay=0')
3452
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
+
3453
4325
  ${colors.bold(colors.yellow('Examples:'))}
3454
4326
  ${colors.green('$ rek serve dns')} ${colors.gray('Start on port 5353')}
3455
- ${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)')}
3456
4328
  ${colors.green('$ dig @127.0.0.1 -p 5353 example.com')} ${colors.gray('Test with dig')}
3457
4329
 
3458
4330
  ${colors.bold(colors.yellow('Default Records:'))}
@@ -3460,21 +4332,32 @@ ${colors.bold(colors.yellow('Default Records:'))}
3460
4332
  ${colors.cyan('example.com')} A, AAAA, NS, MX, TXT records
3461
4333
  ${colors.cyan('test.local')} A: 192.168.1.100
3462
4334
  `)
3463
- .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
+ }
3464
4347
  const { MockDnsServer } = await import('../testing/mock-dns-server.js');
3465
4348
  const server = await MockDnsServer.create({
3466
- port: parseInt(options.port),
3467
- host: options.host,
3468
- delay: parseInt(options.delay),
4349
+ port,
4350
+ host,
4351
+ delay,
3469
4352
  });
3470
4353
  console.log(colors.green(`
3471
4354
  ┌─────────────────────────────────────────────┐
3472
4355
  │ ${colors.bold('Recker Mock DNS Server')} │
3473
4356
  ├─────────────────────────────────────────────┤
3474
- │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
4357
+ │ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
3475
4358
  │ Protocol: ${colors.yellow('UDP'.padEnd(32))}│
3476
4359
  ├─────────────────────────────────────────────┤
3477
- │ Test: dig @${options.host} -p ${options.port} example.com │
4360
+ │ Test: dig @${host} -p ${port} example.com │
3478
4361
  │ Press ${colors.bold('Ctrl+C')} to stop │
3479
4362
  └─────────────────────────────────────────────┘
3480
4363
  `));
@@ -3490,10 +4373,13 @@ ${colors.bold(colors.yellow('Default Records:'))}
3490
4373
  serve
3491
4374
  .command('whois')
3492
4375
  .description('Start a mock WHOIS server')
3493
- .option('-p, --port <number>', 'Port to listen on', '4343')
3494
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3495
- .option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
4376
+ .argument('[args...]', 'Options: port=4343 host=127.0.0.1 delay=0')
3496
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
+
3497
4383
  ${colors.bold(colors.yellow('Examples:'))}
3498
4384
  ${colors.green('$ rek serve whois')} ${colors.gray('Start on port 4343')}
3499
4385
  ${colors.green('$ whois -h 127.0.0.1 -p 4343 example.com')} ${colors.gray('Test with whois')}
@@ -3503,21 +4389,32 @@ ${colors.bold(colors.yellow('Default Domains:'))}
3503
4389
  ${colors.cyan('google.com')} MarkMonitor registrar
3504
4390
  ${colors.cyan('test.local')} Test domain
3505
4391
  `)
3506
- .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
+ }
3507
4404
  const { MockWhoisServer } = await import('../testing/mock-whois-server.js');
3508
4405
  const server = await MockWhoisServer.create({
3509
- port: parseInt(options.port),
3510
- host: options.host,
3511
- delay: parseInt(options.delay),
4406
+ port,
4407
+ host,
4408
+ delay,
3512
4409
  });
3513
4410
  console.log(colors.green(`
3514
4411
  ┌─────────────────────────────────────────────┐
3515
4412
  │ ${colors.bold('Recker Mock WHOIS Server')} │
3516
4413
  ├─────────────────────────────────────────────┤
3517
- │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
4414
+ │ Address: ${colors.cyan(`${host}:${port}`.padEnd(33))}│
3518
4415
  │ Protocol: ${colors.yellow('TCP'.padEnd(32))}│
3519
4416
  ├─────────────────────────────────────────────┤
3520
- │ Test: whois -h ${options.host} -p ${options.port} example.com │
4417
+ │ Test: whois -h ${host} -p ${port} example.com │
3521
4418
  │ Press ${colors.bold('Ctrl+C')} to stop │
3522
4419
  └─────────────────────────────────────────────┘
3523
4420
  `));
@@ -3533,12 +4430,15 @@ ${colors.bold(colors.yellow('Default Domains:'))}
3533
4430
  serve
3534
4431
  .command('telnet')
3535
4432
  .description('Start a mock Telnet server')
3536
- .option('-p, --port <number>', 'Port to listen on', '2323')
3537
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3538
- .option('--echo', 'Echo input back (default: true)', true)
3539
- .option('--no-echo', 'Disable echo mode')
3540
- .option('--delay <ms>', 'Add delay to responses (milliseconds)', '0')
4433
+ .argument('[args...]', 'Options: port=2323 host=127.0.0.1 echo noecho delay=0')
3541
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
+
3542
4442
  ${colors.bold(colors.yellow('Examples:'))}
3543
4443
  ${colors.green('$ rek serve telnet')} ${colors.gray('Start on port 2323')}
3544
4444
  ${colors.green('$ telnet localhost 2323')} ${colors.gray('Connect to server')}
@@ -3551,22 +4451,38 @@ ${colors.bold(colors.yellow('Built-in Commands:'))}
3551
4451
  ${colors.cyan('ping')} Returns "pong"
3552
4452
  ${colors.cyan('quit')} Disconnect
3553
4453
  `)
3554
- .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
+ }
3555
4471
  const { MockTelnetServer } = await import('../testing/mock-telnet-server.js');
3556
4472
  const server = await MockTelnetServer.create({
3557
- port: parseInt(options.port),
3558
- host: options.host,
3559
- echo: options.echo,
3560
- delay: parseInt(options.delay),
4473
+ port,
4474
+ host,
4475
+ echo,
4476
+ delay,
3561
4477
  });
3562
4478
  console.log(colors.green(`
3563
4479
  ┌─────────────────────────────────────────────┐
3564
4480
  │ ${colors.bold('Recker Mock Telnet Server')} │
3565
4481
  ├─────────────────────────────────────────────┤
3566
- │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
3567
- │ 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))}│
3568
4484
  ├─────────────────────────────────────────────┤
3569
- │ Connect: telnet ${options.host} ${options.port} │
4485
+ │ Connect: telnet ${host} ${port} │
3570
4486
  │ Press ${colors.bold('Ctrl+C')} to stop │
3571
4487
  └─────────────────────────────────────────────┘
3572
4488
  `));
@@ -3588,18 +4504,21 @@ ${colors.bold(colors.yellow('Built-in Commands:'))}
3588
4504
  serve
3589
4505
  .command('ftp')
3590
4506
  .description('Start a mock FTP server')
3591
- .option('-p, --port <number>', 'Port to listen on', '2121')
3592
- .option('-h, --host <string>', 'Host to bind to', '127.0.0.1')
3593
- .option('-u, --username <user>', 'Username for auth', 'user')
3594
- .option('--password <pass>', 'Password for auth', 'pass')
3595
- .option('--anonymous', 'Allow anonymous login (default: true)', true)
3596
- .option('--no-anonymous', 'Disable anonymous login')
3597
- .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')
3598
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
+
3599
4518
  ${colors.bold(colors.yellow('Examples:'))}
3600
4519
  ${colors.green('$ rek serve ftp')} ${colors.gray('Start on port 2121')}
3601
4520
  ${colors.green('$ ftp localhost 2121')} ${colors.gray('Connect to server')}
3602
- ${colors.green('$ rek serve ftp --no-anonymous')} ${colors.gray('Require authentication')}
4521
+ ${colors.green('$ rek serve ftp noanonymous')} ${colors.gray('Require authentication')}
3603
4522
 
3604
4523
  ${colors.bold(colors.yellow('Default Files:'))}
3605
4524
  ${colors.cyan('/welcome.txt')} Welcome message
@@ -3611,25 +4530,47 @@ ${colors.bold(colors.yellow('Credentials:'))}
3611
4530
  Username: ${colors.cyan('user')} Password: ${colors.cyan('pass')}
3612
4531
  Or use anonymous login with user: ${colors.cyan('anonymous')}
3613
4532
  `)
3614
- .action(async (options) => {
4533
+ .action(async (args) => {
3615
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
+ }
3616
4557
  const server = await MockFtpServer.create({
3617
- port: parseInt(options.port),
3618
- host: options.host,
3619
- username: options.username,
3620
- password: options.password,
3621
- anonymous: options.anonymous,
3622
- delay: parseInt(options.delay),
4558
+ port,
4559
+ host,
4560
+ username,
4561
+ password,
4562
+ anonymous,
4563
+ delay,
3623
4564
  });
3624
4565
  console.log(colors.green(`
3625
4566
  ┌─────────────────────────────────────────────┐
3626
4567
  │ ${colors.bold('Recker Mock FTP Server')} │
3627
4568
  ├─────────────────────────────────────────────┤
3628
- │ Address: ${colors.cyan(`${options.host}:${options.port}`.padEnd(33))}│
3629
- │ Anonymous: ${colors.yellow((options.anonymous ? 'Allowed' : 'Disabled').padEnd(31))}│
3630
- │ 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))}│
3631
4572
  ├─────────────────────────────────────────────┤
3632
- │ Connect: ftp ${options.host} ${options.port} │
4573
+ │ Connect: ftp ${host} ${port} │
3633
4574
  │ Press ${colors.bold('Ctrl+C')} to stop │
3634
4575
  └─────────────────────────────────────────────┘
3635
4576
  `));
@@ -3651,22 +4592,25 @@ ${colors.bold(colors.yellow('Credentials:'))}
3651
4592
  program
3652
4593
  .command('mcp')
3653
4594
  .description('Start MCP server for AI agents to access Recker documentation')
3654
- .option('-t, --transport <mode>', 'Transport mode: stdio, http, sse', 'stdio')
3655
- .option('-p, --port <number>', 'Server port (for http/sse modes)', '3100')
3656
- .option('-d, --docs <path>', 'Path to documentation folder')
3657
- .option('-T, --tools <paths...>', 'Paths to external tool modules to load')
3658
- .option('--debug', 'Enable debug logging')
4595
+ .argument('[args...]', 'Options: transport=stdio port=3100 docs=<path> tools=<paths> debug')
3659
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
+
3660
4604
  ${colors.bold(colors.yellow('Transport Modes:'))}
3661
4605
  ${colors.cyan('stdio')} ${colors.gray('(default)')} For Claude Code and other CLI tools
3662
4606
  ${colors.cyan('http')} Simple HTTP POST endpoint
3663
4607
  ${colors.cyan('sse')} HTTP + Server-Sent Events for real-time notifications
3664
4608
 
3665
- ${colors.bold(colors.yellow('Usage:'))}
3666
- ${colors.green('$ rek mcp')} ${colors.gray('Start in stdio mode (for Claude Code)')}
3667
- ${colors.green('$ rek mcp -t http')} ${colors.gray('Start HTTP server on port 3100')}
3668
- ${colors.green('$ rek mcp -t sse -p 8080')} ${colors.gray('Start SSE server on custom port')}
3669
- ${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')}
3670
4614
 
3671
4615
  ${colors.bold(colors.yellow('Tools provided:'))}
3672
4616
  ${colors.cyan('search_docs')} Search documentation by keyword
@@ -3682,15 +4626,31 @@ ${colors.bold(colors.yellow('Claude Code config (~/.claude.json):'))}
3682
4626
  }
3683
4627
  }`)}
3684
4628
  `)
3685
- .action(async (options) => {
4629
+ .action(async (args) => {
3686
4630
  const { MCPServer } = await import('../mcp/server.js');
3687
- 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
+ }
3688
4648
  const server = new MCPServer({
3689
4649
  transport,
3690
- port: parseInt(options.port),
3691
- docsPath: options.docs,
3692
- debug: options.debug,
3693
- toolPaths: options.tools,
4650
+ port,
4651
+ docsPath,
4652
+ debug,
4653
+ toolPaths,
3694
4654
  });
3695
4655
  if (transport === 'stdio') {
3696
4656
  await server.start();
@@ -3709,7 +4669,7 @@ ${colors.bold(colors.yellow('Claude Code config (~/.claude.json):'))}
3709
4669
  │ ${colors.bold('Recker MCP Server')} │
3710
4670
  ├─────────────────────────────────────────────┤
3711
4671
  │ Transport: ${colors.cyan(transport.padEnd(31))}│
3712
- │ Endpoint: ${colors.cyan(`http://localhost:${options.port}`.padEnd(32))}│
4672
+ │ Endpoint: ${colors.cyan(`http://localhost:${port}`.padEnd(32))}│
3713
4673
  │ Docs indexed: ${colors.yellow(String(server.getDocsCount()).padEnd(28))}│
3714
4674
  ├─────────────────────────────────────────────┤${endpoints}
3715
4675
  ├─────────────────────────────────────────────┤
@@ -4385,21 +5345,39 @@ ${colors.bold(colors.yellow('Examples:'))}
4385
5345
  });
4386
5346
  program
4387
5347
  .command('proxy')
4388
- .description('Make a request through a proxy')
5348
+ .description('Route requests through HTTP or SOCKS proxy')
4389
5349
  .argument('<proxy>', 'Proxy URL (http://host:port or socks5://host:port)')
4390
5350
  .argument('<url>', 'Target URL')
4391
5351
  .argument('[args...]', 'Request arguments: method=x key=value key:=json Header:value')
4392
5352
  .addHelpText('after', `
4393
- ${colors.bold(colors.yellow('Parameters:'))}
4394
- method=<method> HTTP method (default: GET)
4395
- key=value String data
4396
- key:=json JSON data
4397
- 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
4398
5371
 
4399
5372
  ${colors.bold(colors.yellow('Examples:'))}
4400
- ${colors.green('$ rek proxy http://proxy.example.com:8080 https://api.com/data')}
4401
- ${colors.green('$ rek proxy socks5://127.0.0.1:1080 https://api.com/users')}
4402
- ${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')}
4403
5381
  `)
4404
5382
  .action(async (proxy, url, args) => {
4405
5383
  const { createClient } = await import('../core/client.js');
@@ -4458,6 +5436,13 @@ ${colors.bold(colors.yellow('Examples:'))}
4458
5436
  process.exit(1);
4459
5437
  }
4460
5438
  });
5439
+ function applyHelpAfterError(cmd) {
5440
+ cmd.showHelpAfterError(true);
5441
+ for (const subcmd of cmd.commands) {
5442
+ applyHelpAfterError(subcmd);
5443
+ }
5444
+ }
5445
+ applyHelpAfterError(program);
4461
5446
  program.parse();
4462
5447
  }
4463
5448
  main().catch((error) => {