recker 1.0.8 → 1.0.10-alpha.646ad5f

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.
@@ -1,5 +1,7 @@
1
1
  import readline from 'node:readline';
2
2
  import { promises as dns } from 'node:dns';
3
+ import { promises as fs } from 'node:fs';
4
+ import { join } from 'node:path';
3
5
  import { requireOptional } from '../../utils/optional-require.js';
4
6
  import { createClient } from '../../core/client.js';
5
7
  import { startInteractiveWebSocket } from './websocket.js';
@@ -7,7 +9,8 @@ import { whois, isDomainAvailable } from '../../utils/whois.js';
7
9
  import { inspectTLS } from '../../utils/tls-inspector.js';
8
10
  import { getSecurityRecords } from '../../utils/dns-toolkit.js';
9
11
  import { rdap } from '../../utils/rdap.js';
10
- import pc from '../../utils/colors.js';
12
+ import { ScrapeDocument } from '../../scrape/document.js';
13
+ import colors from '../../utils/colors.js';
11
14
  let highlight;
12
15
  async function initDependencies() {
13
16
  if (!highlight) {
@@ -27,7 +30,11 @@ export class RekShell {
27
30
  baseUrl = '';
28
31
  lastResponse = null;
29
32
  variables = {};
33
+ envVars = {};
34
+ envLoaded = false;
30
35
  initialized = false;
36
+ currentDoc = null;
37
+ currentDocUrl = '';
31
38
  constructor() {
32
39
  this.client = createClient({
33
40
  baseUrl: 'http://localhost',
@@ -47,15 +54,39 @@ export class RekShell {
47
54
  this.initialized = true;
48
55
  }
49
56
  getPrompt() {
50
- const base = this.baseUrl ? pc.cyan(new URL(this.baseUrl).hostname) : pc.gray('rek');
51
- return `${base} ${pc.magenta('›')} `;
57
+ const base = this.baseUrl ? colors.cyan(new URL(this.baseUrl).hostname) : colors.gray('rek');
58
+ return `${base} ${colors.magenta('›')} `;
59
+ }
60
+ getBaseDomain() {
61
+ if (!this.baseUrl)
62
+ return null;
63
+ try {
64
+ return new URL(this.baseUrl).hostname;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ getRootDomain() {
71
+ const hostname = this.getBaseDomain();
72
+ if (!hostname)
73
+ return null;
74
+ const parts = hostname.split('.');
75
+ if (parts.length <= 2)
76
+ return hostname;
77
+ const commonSLDs = ['co', 'com', 'net', 'org', 'gov', 'edu', 'ac'];
78
+ if (parts.length >= 3 && commonSLDs.includes(parts[parts.length - 2])) {
79
+ return parts.slice(-3).join('.');
80
+ }
81
+ return parts.slice(-2).join('.');
52
82
  }
53
83
  completer(line) {
54
84
  const commands = [
55
85
  'get', 'post', 'put', 'delete', 'patch', 'head', 'options',
56
86
  'ws', 'udp', 'load', 'chat', 'ai',
57
87
  'whois', 'tls', 'ssl', 'dns', 'rdap', 'ping',
58
- 'help', 'clear', 'exit', 'set', 'url', 'vars'
88
+ 'scrap', '$', '$text', '$attr', '$html', '$links', '$images', '$scripts', '$css', '$sourcemaps', '$unmap', '$unmap:view', '$unmap:save', '$beautify', '$beautify:save', '$table',
89
+ 'help', 'clear', 'exit', 'set', 'url', 'vars', 'env'
59
90
  ];
60
91
  const hits = commands.filter((c) => c.startsWith(line));
61
92
  return [hits.length ? hits : commands, line];
@@ -63,9 +94,9 @@ export class RekShell {
63
94
  async start() {
64
95
  await this.ensureInitialized();
65
96
  console.clear();
66
- console.log(pc.bold(pc.cyan('Rek Console')));
67
- console.log(pc.gray('Chat with your APIs. Type "help" for magic.'));
68
- console.log(pc.gray('--------------------------------------------\n'));
97
+ console.log(colors.bold(colors.cyan('Rek Console')));
98
+ console.log(colors.gray('Chat with your APIs. Type "help" for magic.'));
99
+ console.log(colors.gray('--------------------------------------------\n'));
69
100
  this.prompt();
70
101
  this.rl.on('line', async (line) => {
71
102
  const input = line.trim();
@@ -75,11 +106,11 @@ export class RekShell {
75
106
  this.prompt();
76
107
  });
77
108
  this.rl.on('SIGINT', () => {
78
- readline.clearLine(process.stdout, 0);
79
- this.prompt();
109
+ console.log('');
110
+ this.rl.close();
80
111
  });
81
112
  this.rl.on('close', () => {
82
- console.log(pc.gray('\nSee ya.'));
113
+ console.log(colors.gray('\nSee ya.'));
83
114
  process.exit(0);
84
115
  });
85
116
  }
@@ -110,7 +141,10 @@ export class RekShell {
110
141
  this.setVariable(parts.slice(1));
111
142
  return;
112
143
  case 'vars':
113
- console.log(this.variables);
144
+ this.showVars();
145
+ return;
146
+ case 'env':
147
+ await this.loadEnvFile(parts[1]);
114
148
  return;
115
149
  case 'load':
116
150
  await this.runLoadTest(parts.slice(1));
@@ -135,6 +169,54 @@ export class RekShell {
135
169
  case 'ping':
136
170
  await this.runPing(parts[1]);
137
171
  return;
172
+ case 'scrap':
173
+ await this.runScrap(parts[1]);
174
+ return;
175
+ case '$':
176
+ await this.runSelect(parts.slice(1).join(' '));
177
+ return;
178
+ case '$text':
179
+ await this.runSelectText(parts.slice(1).join(' '));
180
+ return;
181
+ case '$attr':
182
+ await this.runSelectAttr(parts[1], parts.slice(2).join(' '));
183
+ return;
184
+ case '$html':
185
+ await this.runSelectHtml(parts.slice(1).join(' '));
186
+ return;
187
+ case '$links':
188
+ await this.runSelectLinks(parts[1]);
189
+ return;
190
+ case '$images':
191
+ await this.runSelectImages(parts.slice(1).join(' ') || undefined);
192
+ return;
193
+ case '$scripts':
194
+ await this.runSelectScripts();
195
+ return;
196
+ case '$css':
197
+ await this.runSelectCSS();
198
+ return;
199
+ case '$sourcemaps':
200
+ await this.runSelectSourcemaps();
201
+ return;
202
+ case '$unmap':
203
+ await this.runUnmap(parts.slice(1).join(' '));
204
+ return;
205
+ case '$unmap:view':
206
+ await this.runUnmapView(parts[1] || '');
207
+ return;
208
+ case '$unmap:save':
209
+ await this.runUnmapSave(parts[1] || '');
210
+ return;
211
+ case '$beautify':
212
+ await this.runBeautify(parts.slice(1).join(' '));
213
+ return;
214
+ case '$beautify:save':
215
+ await this.runBeautifySave(parts[1] || '');
216
+ return;
217
+ case '$table':
218
+ await this.runSelectTable(parts.slice(1).join(' '));
219
+ return;
138
220
  }
139
221
  const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
140
222
  let method = 'GET';
@@ -152,12 +234,12 @@ export class RekShell {
152
234
  bodyParts = parts.slice(1);
153
235
  }
154
236
  else {
155
- console.log(pc.red(`Unknown command: ${cmd}`));
237
+ console.log(colors.red(`Unknown command: ${cmd}`));
156
238
  return;
157
239
  }
158
240
  url = this.resolveUrl(url);
159
241
  if (!url) {
160
- console.log(pc.yellow('No URL provided and no Base URL set. Use "url <url>" or provide full URL.'));
242
+ console.log(colors.yellow('No URL provided and no Base URL set. Use "url <url>" or provide full URL.'));
161
243
  return;
162
244
  }
163
245
  const body = {};
@@ -212,7 +294,7 @@ export class RekShell {
212
294
  let targetUrl = '';
213
295
  let users = 50;
214
296
  let duration = 300;
215
- let mode = 'throughput';
297
+ let mode = 'realistic';
216
298
  let http2 = false;
217
299
  let rampUp = 5;
218
300
  for (const arg of args) {
@@ -239,7 +321,7 @@ export class RekShell {
239
321
  }
240
322
  targetUrl = this.resolveUrl(targetUrl);
241
323
  if (!targetUrl) {
242
- console.log(pc.yellow('Target URL required. usage: load <url> users=10 duration=10s ramp=5'));
324
+ console.log(colors.yellow('Target URL required. usage: load <url> users=10 duration=10s ramp=5'));
243
325
  return;
244
326
  }
245
327
  const { startLoadDashboard } = await import('./load-dashboard.js');
@@ -256,7 +338,7 @@ export class RekShell {
256
338
  });
257
339
  }
258
340
  catch (e) {
259
- console.error(pc.red('Load Test Failed: ' + e.message));
341
+ console.error(colors.red('Load Test Failed: ' + e.message));
260
342
  }
261
343
  finally {
262
344
  process.stdout.write('\x1B[?25h');
@@ -271,7 +353,7 @@ export class RekShell {
271
353
  if (!url.startsWith('http'))
272
354
  url = `https://${url}`;
273
355
  this.baseUrl = url;
274
- console.log(pc.gray(`Base URL set to: ${pc.cyan(this.baseUrl)}`));
356
+ console.log(colors.gray(`Base URL set to: ${colors.cyan(this.baseUrl)}`));
275
357
  }
276
358
  setVariable(args) {
277
359
  const [expr] = args;
@@ -279,7 +361,73 @@ export class RekShell {
279
361
  return;
280
362
  const [key, val] = expr.split('=');
281
363
  this.variables[key] = val;
282
- console.log(pc.gray(`Variable $${key} set.`));
364
+ console.log(colors.gray(`Variable $${key} set.`));
365
+ }
366
+ showVars() {
367
+ const hasVars = Object.keys(this.variables).length > 0;
368
+ const hasEnvVars = Object.keys(this.envVars).length > 0;
369
+ if (!hasVars && !hasEnvVars) {
370
+ console.log(colors.gray('No variables set.'));
371
+ console.log(colors.gray('Use "set key=value" to set variables or "env" to load .env file.'));
372
+ return;
373
+ }
374
+ if (hasVars) {
375
+ console.log(colors.bold(colors.yellow('\nSession Variables:')));
376
+ for (const [key, value] of Object.entries(this.variables)) {
377
+ console.log(` ${colors.cyan('$' + key)} = ${colors.green(String(value))}`);
378
+ }
379
+ }
380
+ if (hasEnvVars) {
381
+ console.log(colors.bold(colors.yellow('\nEnvironment Variables (.env):')));
382
+ for (const [key, value] of Object.entries(this.envVars)) {
383
+ const displayValue = key.toLowerCase().includes('key') ||
384
+ key.toLowerCase().includes('secret') ||
385
+ key.toLowerCase().includes('password') ||
386
+ key.toLowerCase().includes('token')
387
+ ? colors.gray('***' + value.slice(-4))
388
+ : colors.green(value);
389
+ console.log(` ${colors.cyan('$' + key)} = ${displayValue}`);
390
+ }
391
+ }
392
+ console.log('');
393
+ }
394
+ async loadEnvFile(filePath) {
395
+ const envPath = filePath || join(process.cwd(), '.env');
396
+ try {
397
+ const content = await fs.readFile(envPath, 'utf-8');
398
+ const lines = content.split('\n');
399
+ let count = 0;
400
+ for (const line of lines) {
401
+ const trimmed = line.trim();
402
+ if (!trimmed || trimmed.startsWith('#'))
403
+ continue;
404
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
405
+ if (match) {
406
+ const [, key, value] = match;
407
+ const cleanKey = key.trim();
408
+ let cleanValue = value.trim();
409
+ if ((cleanValue.startsWith('"') && cleanValue.endsWith('"')) ||
410
+ (cleanValue.startsWith("'") && cleanValue.endsWith("'"))) {
411
+ cleanValue = cleanValue.slice(1, -1);
412
+ }
413
+ this.envVars[cleanKey] = cleanValue;
414
+ process.env[cleanKey] = cleanValue;
415
+ count++;
416
+ }
417
+ }
418
+ this.envLoaded = true;
419
+ console.log(colors.green(`✓ Loaded ${count} variables from ${colors.cyan(envPath)}`));
420
+ console.log(colors.gray('Use "vars" to list all variables.'));
421
+ }
422
+ catch (error) {
423
+ if (error.code === 'ENOENT') {
424
+ console.log(colors.yellow(`No .env file found at ${envPath}`));
425
+ console.log(colors.gray('Create a .env file with KEY=value pairs to use this feature.'));
426
+ }
427
+ else {
428
+ console.log(colors.red(`Error loading .env: ${error.message}`));
429
+ }
430
+ }
283
431
  }
284
432
  resolveVariables(value) {
285
433
  if (value.startsWith('$')) {
@@ -295,7 +443,7 @@ export class RekShell {
295
443
  }
296
444
  return String(current);
297
445
  }
298
- return this.variables[key] || value;
446
+ return this.variables[key] || this.envVars[key] || process.env[key] || value;
299
447
  }
300
448
  return value;
301
449
  }
@@ -328,18 +476,18 @@ export class RekShell {
328
476
  const { UDPTransport } = await import('../../transport/udp.js');
329
477
  const transport = new UDPTransport(url);
330
478
  const msg = Object.keys(body).length ? JSON.stringify(body) : 'ping';
331
- console.log(pc.gray(`UDP packet -> ${url}`));
479
+ console.log(colors.gray(`UDP packet -> ${url}`));
332
480
  const res = await transport.dispatch({
333
481
  url, method: 'GET', headers: new Headers(),
334
482
  body: msg, withHeader: () => ({}), withBody: () => ({})
335
483
  });
336
484
  const text = await res.text();
337
- console.log(pc.green('✔ Sent/Received'));
485
+ console.log(colors.green('✔ Sent/Received'));
338
486
  if (text)
339
487
  console.log(text);
340
488
  return;
341
489
  }
342
- console.log(pc.gray(`${method} ${url}...`));
490
+ console.log(colors.gray(`${method} ${url}...`));
343
491
  try {
344
492
  const hasBody = Object.keys(body).length > 0;
345
493
  const res = await this.client.request(url, {
@@ -348,9 +496,9 @@ export class RekShell {
348
496
  json: hasBody ? body : undefined
349
497
  });
350
498
  const duration = Math.round(performance.now() - startTime);
351
- const statusColor = res.ok ? pc.green : pc.red;
352
- console.log(`${statusColor(pc.bold(res.status))} ${statusColor(res.statusText)} ` +
353
- `${pc.gray(`(${duration}ms)`)}`);
499
+ const statusColor = res.ok ? colors.green : colors.red;
500
+ console.log(`${statusColor(colors.bold(res.status))} ${statusColor(res.statusText)} ` +
501
+ `${colors.gray(`(${duration}ms)`)}`);
354
502
  const text = await res.text();
355
503
  const isJson = res.headers.get('content-type')?.includes('json');
356
504
  if (isJson) {
@@ -370,94 +518,156 @@ export class RekShell {
370
518
  }
371
519
  }
372
520
  catch (error) {
373
- console.error(pc.red(`Error: ${error.message}`));
521
+ console.error(colors.red(`Error: ${error.message}`));
374
522
  }
375
523
  console.log('');
376
524
  }
377
525
  async runWhois(domain) {
378
526
  if (!domain) {
379
- console.log(pc.yellow('Usage: whois <domain>'));
380
- console.log(pc.gray(' Examples: whois google.com | whois 8.8.8.8'));
381
- return;
527
+ domain = this.getRootDomain() || '';
528
+ if (!domain) {
529
+ console.log(colors.yellow('Usage: whois <domain>'));
530
+ console.log(colors.gray(' Examples: whois google.com | whois 8.8.8.8'));
531
+ console.log(colors.gray(' Or set a base URL first: url https://example.com'));
532
+ return;
533
+ }
382
534
  }
383
- console.log(pc.gray(`Looking up ${domain}...`));
384
- const startTime = performance.now();
385
- try {
386
- const result = await whois(domain);
387
- const duration = Math.round(performance.now() - startTime);
388
- console.log(pc.green(`✔ WHOIS lookup completed`) + pc.gray(` (${duration}ms)`));
389
- console.log(pc.gray(`Server: ${result.server}\n`));
390
- const importantFields = [
391
- 'domain name', 'registrar', 'registrar url',
392
- 'creation date', 'registry expiry date', 'updated date',
393
- 'domain status', 'name server', 'dnssec',
394
- 'organization', 'orgname', 'cidr', 'netname', 'country'
395
- ];
396
- for (const field of importantFields) {
397
- const value = result.data[field];
398
- if (value) {
399
- const displayValue = Array.isArray(value) ? value.join(', ') : value;
400
- console.log(` ${pc.cyan(field)}: ${displayValue}`);
535
+ const maxRetries = 3;
536
+ let lastError = null;
537
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
538
+ if (attempt === 1) {
539
+ console.log(colors.gray(`Looking up ${domain}...`));
540
+ }
541
+ else {
542
+ console.log(colors.gray(`Retrying (${attempt}/${maxRetries})...`));
543
+ }
544
+ const startTime = performance.now();
545
+ try {
546
+ const result = await whois(domain);
547
+ const duration = Math.round(performance.now() - startTime);
548
+ console.log(colors.green(`✔ WHOIS lookup completed`) + colors.gray(` (${duration}ms)`));
549
+ console.log(colors.gray(`Server: ${result.server}\n`));
550
+ const importantFields = [
551
+ 'domain name', 'registrar', 'registrar url',
552
+ 'creation date', 'registry expiry date', 'updated date',
553
+ 'domain status', 'name server', 'dnssec',
554
+ 'organization', 'orgname', 'cidr', 'netname', 'country'
555
+ ];
556
+ let foundFields = 0;
557
+ for (const field of importantFields) {
558
+ const value = result.data[field];
559
+ if (value) {
560
+ const displayValue = Array.isArray(value) ? value.join(', ') : value;
561
+ console.log(` ${colors.cyan(field)}: ${displayValue}`);
562
+ foundFields++;
563
+ }
564
+ }
565
+ if (foundFields === 0 && Object.keys(result.data).length > 0) {
566
+ console.log(colors.gray(' (showing all available fields)\n'));
567
+ for (const [key, value] of Object.entries(result.data)) {
568
+ if (value) {
569
+ const displayValue = Array.isArray(value) ? value.join(', ') : String(value);
570
+ console.log(` ${colors.cyan(key)}: ${displayValue}`);
571
+ }
572
+ }
573
+ }
574
+ if (Object.keys(result.data).length === 0 && result.raw) {
575
+ console.log(colors.gray(' (raw response)\n'));
576
+ console.log(colors.white(result.raw.slice(0, 2000)));
401
577
  }
578
+ const available = await isDomainAvailable(domain);
579
+ if (available) {
580
+ console.log(colors.green(`\n✓ Domain appears to be available`));
581
+ }
582
+ this.lastResponse = result.data;
583
+ console.log('');
584
+ return;
402
585
  }
403
- const available = await isDomainAvailable(domain);
404
- if (available) {
405
- console.log(pc.green(`\n✓ Domain appears to be available`));
586
+ catch (error) {
587
+ lastError = error;
588
+ const isRetryable = error.code === 'ECONNRESET' ||
589
+ error.code === 'ECONNREFUSED' ||
590
+ error.code === 'ETIMEDOUT' ||
591
+ error.code === 'ENOTFOUND' ||
592
+ error.message?.includes('timeout') ||
593
+ error.message?.includes('WHOIS query failed');
594
+ if (!isRetryable || attempt === maxRetries) {
595
+ break;
596
+ }
597
+ await new Promise(resolve => setTimeout(resolve, 500 * attempt));
406
598
  }
407
- this.lastResponse = result.data;
408
599
  }
409
- catch (error) {
410
- console.error(pc.red(`WHOIS failed: ${error.message}`));
600
+ const errorMsg = lastError?.message || 'Unknown error';
601
+ const errorCode = lastError?.code;
602
+ console.error(colors.red(`WHOIS failed: ${errorMsg}`));
603
+ if (errorCode) {
604
+ console.error(colors.gray(` Error code: ${errorCode}`));
605
+ }
606
+ if (lastError?.suggestions?.length) {
607
+ console.log(colors.yellow(' Suggestions:'));
608
+ for (const suggestion of lastError.suggestions) {
609
+ console.log(colors.gray(` • ${suggestion}`));
610
+ }
411
611
  }
412
612
  console.log('');
413
613
  }
414
614
  async runTLS(host, port = 443) {
415
615
  if (!host) {
416
- console.log(pc.yellow('Usage: tls <host> [port]'));
417
- console.log(pc.gray(' Examples: tls google.com | tls api.stripe.com 443'));
418
- return;
616
+ host = this.getBaseDomain() || '';
617
+ if (!host) {
618
+ console.log(colors.yellow('Usage: tls <host> [port]'));
619
+ console.log(colors.gray(' Examples: tls google.com | tls api.stripe.com 443'));
620
+ console.log(colors.gray(' Or set a base URL first: url https://example.com'));
621
+ return;
622
+ }
623
+ }
624
+ else {
625
+ host = host.replace(/^https?:\/\//, '').split('/')[0];
419
626
  }
420
- host = host.replace(/^https?:\/\//, '').split('/')[0];
421
- console.log(pc.gray(`Inspecting TLS for ${host}:${port}...`));
627
+ console.log(colors.gray(`Inspecting TLS for ${host}:${port}...`));
422
628
  const startTime = performance.now();
423
629
  try {
424
630
  const info = await inspectTLS(host, port);
425
631
  const duration = Math.round(performance.now() - startTime);
426
- const statusIcon = info.valid ? pc.green('✔') : pc.red('✖');
427
- const statusText = info.valid ? pc.green('Valid') : pc.red('Invalid/Expired');
428
- console.log(`${statusIcon} Certificate ${statusText}` + pc.gray(` (${duration}ms)\n`));
429
- console.log(pc.bold(' Certificate:'));
430
- console.log(` ${pc.cyan('Subject')}: ${info.subject?.CN || info.subject?.O || 'N/A'}`);
431
- console.log(` ${pc.cyan('Issuer')}: ${info.issuer?.CN || info.issuer?.O || 'N/A'}`);
432
- console.log(` ${pc.cyan('Valid From')}: ${info.validFrom.toISOString()}`);
433
- console.log(` ${pc.cyan('Valid To')}: ${info.validTo.toISOString()}`);
434
- const daysColor = info.daysRemaining < 30 ? pc.red : info.daysRemaining < 90 ? pc.yellow : pc.green;
435
- console.log(` ${pc.cyan('Days Remaining')}: ${daysColor(String(info.daysRemaining))}`);
436
- console.log(pc.bold('\n Connection:'));
437
- console.log(` ${pc.cyan('Protocol')}: ${info.protocol || 'N/A'}`);
438
- console.log(` ${pc.cyan('Cipher')}: ${info.cipher?.name || 'N/A'}`);
439
- console.log(` ${pc.cyan('Authorized')}: ${info.authorized ? pc.green('Yes') : pc.red('No')}`);
632
+ const statusIcon = info.valid ? colors.green('✔') : colors.red('✖');
633
+ const statusText = info.valid ? colors.green('Valid') : colors.red('Invalid/Expired');
634
+ console.log(`${statusIcon} Certificate ${statusText}` + colors.gray(` (${duration}ms)\n`));
635
+ console.log(colors.bold(' Certificate:'));
636
+ console.log(` ${colors.cyan('Subject')}: ${info.subject?.CN || info.subject?.O || 'N/A'}`);
637
+ console.log(` ${colors.cyan('Issuer')}: ${info.issuer?.CN || info.issuer?.O || 'N/A'}`);
638
+ console.log(` ${colors.cyan('Valid From')}: ${info.validFrom.toISOString()}`);
639
+ console.log(` ${colors.cyan('Valid To')}: ${info.validTo.toISOString()}`);
640
+ const daysColor = info.daysRemaining < 30 ? colors.red : info.daysRemaining < 90 ? colors.yellow : colors.green;
641
+ console.log(` ${colors.cyan('Days Remaining')}: ${daysColor(String(info.daysRemaining))}`);
642
+ console.log(colors.bold('\n Connection:'));
643
+ console.log(` ${colors.cyan('Protocol')}: ${info.protocol || 'N/A'}`);
644
+ console.log(` ${colors.cyan('Cipher')}: ${info.cipher?.name || 'N/A'}`);
645
+ console.log(` ${colors.cyan('Authorized')}: ${info.authorized ? colors.green('Yes') : colors.red('No')}`);
440
646
  if (info.authorizationError) {
441
- console.log(` ${pc.cyan('Auth Error')}: ${pc.red(String(info.authorizationError))}`);
647
+ console.log(` ${colors.cyan('Auth Error')}: ${colors.red(String(info.authorizationError))}`);
442
648
  }
443
- console.log(pc.bold('\n Fingerprints:'));
444
- console.log(` ${pc.cyan('SHA1')}: ${info.fingerprint}`);
445
- console.log(` ${pc.cyan('SHA256')}: ${info.fingerprint256}`);
446
- console.log(` ${pc.cyan('Serial')}: ${info.serialNumber}`);
649
+ console.log(colors.bold('\n Fingerprints:'));
650
+ console.log(` ${colors.cyan('SHA1')}: ${info.fingerprint}`);
651
+ console.log(` ${colors.cyan('SHA256')}: ${info.fingerprint256}`);
652
+ console.log(` ${colors.cyan('Serial')}: ${info.serialNumber}`);
447
653
  this.lastResponse = info;
448
654
  }
449
655
  catch (error) {
450
- console.error(pc.red(`TLS inspection failed: ${error.message}`));
656
+ console.error(colors.red(`TLS inspection failed: ${error.message}`));
451
657
  }
452
658
  console.log('');
453
659
  }
454
660
  async runDNS(domain) {
455
661
  if (!domain) {
456
- console.log(pc.yellow('Usage: dns <domain>'));
457
- console.log(pc.gray(' Examples: dns google.com | dns github.com'));
458
- return;
662
+ domain = this.getBaseDomain() || '';
663
+ if (!domain) {
664
+ console.log(colors.yellow('Usage: dns <domain>'));
665
+ console.log(colors.gray(' Examples: dns google.com | dns github.com'));
666
+ console.log(colors.gray(' Or set a base URL first: url https://example.com'));
667
+ return;
668
+ }
459
669
  }
460
- console.log(pc.gray(`Resolving DNS for ${domain}...`));
670
+ console.log(colors.gray(`Resolving DNS for ${domain}...`));
461
671
  const startTime = performance.now();
462
672
  try {
463
673
  const [a, aaaa, mx, ns, txt, security] = await Promise.all([
@@ -469,98 +679,108 @@ export class RekShell {
469
679
  getSecurityRecords(domain).catch(() => ({}))
470
680
  ]);
471
681
  const duration = Math.round(performance.now() - startTime);
472
- console.log(pc.green(`✔ DNS resolved`) + pc.gray(` (${duration}ms)\n`));
682
+ console.log(colors.green(`✔ DNS resolved`) + colors.gray(` (${duration}ms)\n`));
473
683
  if (a.length) {
474
- console.log(pc.bold(' A Records (IPv4):'));
475
- a.forEach(ip => console.log(` ${pc.cyan('→')} ${ip}`));
684
+ console.log(colors.bold(' A Records (IPv4):'));
685
+ a.forEach(ip => console.log(` ${colors.cyan('→')} ${ip}`));
476
686
  }
477
687
  if (aaaa.length) {
478
- console.log(pc.bold(' AAAA Records (IPv6):'));
479
- aaaa.forEach(ip => console.log(` ${pc.cyan('→')} ${ip}`));
688
+ console.log(colors.bold(' AAAA Records (IPv6):'));
689
+ aaaa.forEach(ip => console.log(` ${colors.cyan('→')} ${ip}`));
480
690
  }
481
691
  if (ns.length) {
482
- console.log(pc.bold(' NS Records:'));
483
- ns.forEach(n => console.log(` ${pc.cyan('→')} ${n}`));
692
+ console.log(colors.bold(' NS Records:'));
693
+ ns.forEach(n => console.log(` ${colors.cyan('→')} ${n}`));
484
694
  }
485
695
  if (mx.length) {
486
- console.log(pc.bold(' MX Records:'));
696
+ console.log(colors.bold(' MX Records:'));
487
697
  mx.sort((a, b) => a.priority - b.priority)
488
- .forEach(m => console.log(` ${pc.cyan(String(m.priority).padStart(3))} ${m.exchange}`));
698
+ .forEach(m => console.log(` ${colors.cyan(String(m.priority).padStart(3))} ${m.exchange}`));
489
699
  }
490
700
  const sec = security;
491
701
  if (sec.spf?.length) {
492
- console.log(pc.bold(' SPF:'));
493
- console.log(` ${pc.gray(sec.spf[0].slice(0, 80))}${sec.spf[0].length > 80 ? '...' : ''}`);
702
+ console.log(colors.bold(' SPF:'));
703
+ console.log(` ${colors.gray(sec.spf[0].slice(0, 80))}${sec.spf[0].length > 80 ? '...' : ''}`);
494
704
  }
495
705
  if (sec.dmarc) {
496
- console.log(pc.bold(' DMARC:'));
497
- console.log(` ${pc.gray(sec.dmarc.slice(0, 80))}${sec.dmarc.length > 80 ? '...' : ''}`);
706
+ console.log(colors.bold(' DMARC:'));
707
+ console.log(` ${colors.gray(sec.dmarc.slice(0, 80))}${sec.dmarc.length > 80 ? '...' : ''}`);
498
708
  }
499
709
  if (sec.caa?.issue?.length) {
500
- console.log(pc.bold(' CAA:'));
501
- sec.caa.issue.forEach((ca) => console.log(` ${pc.cyan('issue')} ${ca}`));
710
+ console.log(colors.bold(' CAA:'));
711
+ sec.caa.issue.forEach((ca) => console.log(` ${colors.cyan('issue')} ${ca}`));
502
712
  }
503
713
  this.lastResponse = { a, aaaa, mx, ns, txt, security };
504
714
  }
505
715
  catch (error) {
506
- console.error(pc.red(`DNS lookup failed: ${error.message}`));
716
+ console.error(colors.red(`DNS lookup failed: ${error.message}`));
507
717
  }
508
718
  console.log('');
509
719
  }
510
720
  async runRDAP(domain) {
511
721
  if (!domain) {
512
- console.log(pc.yellow('Usage: rdap <domain>'));
513
- console.log(pc.gray(' Examples: rdap google.com | rdap 8.8.8.8'));
514
- return;
722
+ domain = this.getRootDomain() || '';
723
+ if (!domain) {
724
+ console.log(colors.yellow('Usage: rdap <domain>'));
725
+ console.log(colors.gray(' Examples: rdap google.com | rdap 8.8.8.8'));
726
+ console.log(colors.gray(' Or set a base URL first: url https://example.com'));
727
+ return;
728
+ }
515
729
  }
516
- console.log(pc.gray(`RDAP lookup for ${domain}...`));
730
+ console.log(colors.gray(`RDAP lookup for ${domain}...`));
517
731
  const startTime = performance.now();
518
732
  try {
519
733
  const result = await rdap(this.client, domain);
520
734
  const duration = Math.round(performance.now() - startTime);
521
- console.log(pc.green(`✔ RDAP lookup completed`) + pc.gray(` (${duration}ms)\n`));
735
+ console.log(colors.green(`✔ RDAP lookup completed`) + colors.gray(` (${duration}ms)\n`));
522
736
  if (result.status?.length) {
523
- console.log(pc.bold(' Status:'));
524
- result.status.forEach((s) => console.log(` ${pc.cyan('→')} ${s}`));
737
+ console.log(colors.bold(' Status:'));
738
+ result.status.forEach((s) => console.log(` ${colors.cyan('→')} ${s}`));
525
739
  }
526
740
  if (result.events?.length) {
527
- console.log(pc.bold(' Events:'));
741
+ console.log(colors.bold(' Events:'));
528
742
  result.events.forEach((e) => {
529
743
  const date = new Date(e.eventDate).toISOString().split('T')[0];
530
- console.log(` ${pc.cyan(e.eventAction.padEnd(15))} ${date}`);
744
+ console.log(` ${colors.cyan(e.eventAction.padEnd(15))} ${date}`);
531
745
  });
532
746
  }
533
747
  if (result.entities?.length) {
534
- console.log(pc.bold(' Entities:'));
748
+ console.log(colors.bold(' Entities:'));
535
749
  result.entities.forEach((e) => {
536
750
  const roles = e.roles?.join(', ') || 'unknown';
537
- console.log(` ${pc.cyan(roles.padEnd(15))} ${e.handle || 'N/A'}`);
751
+ console.log(` ${colors.cyan(roles.padEnd(15))} ${e.handle || 'N/A'}`);
538
752
  });
539
753
  }
540
754
  if (result.handle) {
541
- console.log(` ${pc.cyan('Handle')}: ${result.handle}`);
755
+ console.log(` ${colors.cyan('Handle')}: ${result.handle}`);
542
756
  }
543
757
  if (result.name) {
544
- console.log(` ${pc.cyan('Name')}: ${result.name}`);
758
+ console.log(` ${colors.cyan('Name')}: ${result.name}`);
545
759
  }
546
760
  if (result.startAddress && result.endAddress) {
547
- console.log(` ${pc.cyan('Range')}: ${result.startAddress} - ${result.endAddress}`);
761
+ console.log(` ${colors.cyan('Range')}: ${result.startAddress} - ${result.endAddress}`);
548
762
  }
549
763
  this.lastResponse = result;
550
764
  }
551
765
  catch (error) {
552
- console.error(pc.red(`RDAP lookup failed: ${error.message}`));
553
- console.log(pc.gray(' Tip: RDAP may not be available for all TLDs. Try "whois" instead.'));
766
+ console.error(colors.red(`RDAP lookup failed: ${error.message}`));
767
+ console.log(colors.gray(' Tip: RDAP may not be available for all TLDs. Try "whois" instead.'));
554
768
  }
555
769
  console.log('');
556
770
  }
557
771
  async runPing(host) {
558
772
  if (!host) {
559
- console.log(pc.yellow('Usage: ping <host>'));
560
- return;
773
+ host = this.getBaseDomain() || '';
774
+ if (!host) {
775
+ console.log(colors.yellow('Usage: ping <host>'));
776
+ console.log(colors.gray(' Or set a base URL first: url https://example.com'));
777
+ return;
778
+ }
561
779
  }
562
- host = host.replace(/^https?:\/\//, '').split('/')[0];
563
- console.log(pc.gray(`Pinging ${host}...`));
780
+ else {
781
+ host = host.replace(/^https?:\/\//, '').split('/')[0];
782
+ }
783
+ console.log(colors.gray(`Pinging ${host}...`));
564
784
  try {
565
785
  const { connect } = await import('node:net');
566
786
  const port = 443;
@@ -568,7 +788,7 @@ export class RekShell {
568
788
  await new Promise((resolve, reject) => {
569
789
  const socket = connect(port, host, () => {
570
790
  const duration = Math.round(performance.now() - startTime);
571
- console.log(pc.green(`✔ ${host}:${port} is reachable`) + pc.gray(` (${duration}ms)`));
791
+ console.log(colors.green(`✔ ${host}:${port} is reachable`) + colors.gray(` (${duration}ms)`));
572
792
  socket.end();
573
793
  resolve();
574
794
  });
@@ -580,50 +800,905 @@ export class RekShell {
580
800
  });
581
801
  }
582
802
  catch (error) {
583
- console.error(pc.red(`✖ ${host} is unreachable: ${error.message}`));
803
+ console.error(colors.red(`✖ ${host} is unreachable: ${error.message}`));
804
+ }
805
+ console.log('');
806
+ }
807
+ async runScrap(url) {
808
+ if (!url) {
809
+ if (!this.baseUrl) {
810
+ console.log(colors.yellow('Usage: scrap <url>'));
811
+ console.log(colors.gray(' Examples: scrap https://news.ycombinator.com'));
812
+ console.log(colors.gray(' Or set a base URL first: url https://example.com'));
813
+ return;
814
+ }
815
+ url = this.baseUrl;
816
+ }
817
+ else if (!url.startsWith('http')) {
818
+ url = this.baseUrl ? `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}` : `https://${url}`;
819
+ }
820
+ console.log(colors.gray(`Fetching ${url}...`));
821
+ const startTime = performance.now();
822
+ try {
823
+ const response = await this.client.get(url);
824
+ const html = await response.text();
825
+ const duration = Math.round(performance.now() - startTime);
826
+ this.currentDoc = await ScrapeDocument.create(html, { baseUrl: url });
827
+ this.currentDocUrl = url;
828
+ const elementCount = this.currentDoc.select('*').length;
829
+ const title = this.currentDoc.selectFirst('title').text() || 'No title';
830
+ const meta = this.currentDoc.meta();
831
+ const og = this.currentDoc.openGraph();
832
+ console.log(colors.green(`✔ Loaded`) + colors.gray(` (${duration}ms)`));
833
+ console.log(` ${colors.cyan('Title')}: ${title}`);
834
+ console.log(` ${colors.cyan('Elements')}: ${elementCount}`);
835
+ console.log(` ${colors.cyan('Size')}: ${(html.length / 1024).toFixed(1)}kb`);
836
+ if (meta.description) {
837
+ const desc = meta.description.length > 100 ? meta.description.slice(0, 100) + '...' : meta.description;
838
+ console.log(` ${colors.cyan('Description')}: ${desc}`);
839
+ }
840
+ const hasOg = og.title || og.description || og.image || og.siteName;
841
+ if (hasOg) {
842
+ console.log(colors.bold('\n OpenGraph:'));
843
+ if (og.siteName)
844
+ console.log(` ${colors.magenta('Site')}: ${og.siteName}`);
845
+ if (og.title && og.title !== title)
846
+ console.log(` ${colors.magenta('Title')}: ${og.title}`);
847
+ if (og.type)
848
+ console.log(` ${colors.magenta('Type')}: ${og.type}`);
849
+ if (og.description) {
850
+ const ogDesc = og.description.length > 80 ? og.description.slice(0, 80) + '...' : og.description;
851
+ console.log(` ${colors.magenta('Description')}: ${ogDesc}`);
852
+ }
853
+ if (og.image) {
854
+ const images = Array.isArray(og.image) ? og.image : [og.image];
855
+ console.log(` ${colors.magenta('Image')}: ${images[0]}`);
856
+ if (images.length > 1)
857
+ console.log(colors.gray(` (+${images.length - 1} more)`));
858
+ }
859
+ if (og.url && og.url !== url)
860
+ console.log(` ${colors.magenta('URL')}: ${og.url}`);
861
+ }
862
+ console.log(colors.gray('\n Use $ <selector> to query, $text, $attr, $links, $images, $scripts, $css, $sourcemaps, $table'));
863
+ }
864
+ catch (error) {
865
+ console.error(colors.red(`Scrape failed: ${error.message}`));
866
+ }
867
+ console.log('');
868
+ }
869
+ async runSelect(selector) {
870
+ if (!this.currentDoc) {
871
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
872
+ return;
873
+ }
874
+ if (!selector) {
875
+ console.log(colors.yellow('Usage: $ <selector>'));
876
+ console.log(colors.gray(' Examples: $ h1 | $ .title | $ a[href*="article"]'));
877
+ return;
878
+ }
879
+ try {
880
+ const elements = this.currentDoc.select(selector);
881
+ const count = elements.length;
882
+ console.log(colors.cyan(`Found ${count} element(s)`));
883
+ if (count > 0 && count <= 10) {
884
+ elements.each((el, i) => {
885
+ const text = el.text().slice(0, 80).replace(/\s+/g, ' ').trim();
886
+ console.log(` ${colors.gray(`${i + 1}.`)} ${text}${text.length >= 80 ? '...' : ''}`);
887
+ });
888
+ }
889
+ else if (count > 10) {
890
+ console.log(colors.gray(' (showing first 10)'));
891
+ let shown = 0;
892
+ elements.each((el, i) => {
893
+ if (shown >= 10)
894
+ return;
895
+ const text = el.text().slice(0, 80).replace(/\s+/g, ' ').trim();
896
+ console.log(` ${colors.gray(`${i + 1}.`)} ${text}${text.length >= 80 ? '...' : ''}`);
897
+ shown++;
898
+ });
899
+ }
900
+ this.lastResponse = { count, selector };
901
+ }
902
+ catch (error) {
903
+ console.error(colors.red(`Query failed: ${error.message}`));
904
+ }
905
+ console.log('');
906
+ }
907
+ async runSelectText(selector) {
908
+ if (!this.currentDoc) {
909
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
910
+ return;
911
+ }
912
+ if (!selector) {
913
+ console.log(colors.yellow('Usage: $text <selector>'));
914
+ return;
915
+ }
916
+ try {
917
+ const elements = this.currentDoc.select(selector);
918
+ const texts = [];
919
+ elements.each((el, i) => {
920
+ const text = el.text().trim();
921
+ if (text) {
922
+ texts.push(text);
923
+ console.log(`${colors.gray(`${i + 1}.`)} ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`);
924
+ }
925
+ });
926
+ this.lastResponse = texts;
927
+ console.log(colors.gray(`\n ${texts.length} text item(s) extracted`));
928
+ }
929
+ catch (error) {
930
+ console.error(colors.red(`Query failed: ${error.message}`));
931
+ }
932
+ console.log('');
933
+ }
934
+ async runSelectAttr(attrName, selector) {
935
+ if (!this.currentDoc) {
936
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
937
+ return;
938
+ }
939
+ if (!attrName || !selector) {
940
+ console.log(colors.yellow('Usage: $attr <attribute> <selector>'));
941
+ console.log(colors.gray(' Examples: $attr href a | $attr src img'));
942
+ return;
943
+ }
944
+ try {
945
+ const elements = this.currentDoc.select(selector);
946
+ const attrs = [];
947
+ elements.each((el, i) => {
948
+ const value = el.attr(attrName);
949
+ if (value) {
950
+ attrs.push(value);
951
+ console.log(`${colors.gray(`${i + 1}.`)} ${value}`);
952
+ }
953
+ });
954
+ this.lastResponse = attrs;
955
+ console.log(colors.gray(`\n ${attrs.length} attribute(s) extracted`));
956
+ }
957
+ catch (error) {
958
+ console.error(colors.red(`Query failed: ${error.message}`));
959
+ }
960
+ console.log('');
961
+ }
962
+ async runSelectHtml(selector) {
963
+ if (!this.currentDoc) {
964
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
965
+ return;
966
+ }
967
+ if (!selector) {
968
+ console.log(colors.yellow('Usage: $html <selector>'));
969
+ return;
970
+ }
971
+ try {
972
+ const element = this.currentDoc.selectFirst(selector);
973
+ const html = element.html();
974
+ if (html) {
975
+ console.log(html.slice(0, 1000));
976
+ if (html.length > 1000) {
977
+ console.log(colors.gray(`\n ... (${html.length} chars total)`));
978
+ }
979
+ this.lastResponse = html;
980
+ }
981
+ else {
982
+ console.log(colors.gray('No element found'));
983
+ }
984
+ }
985
+ catch (error) {
986
+ console.error(colors.red(`Query failed: ${error.message}`));
987
+ }
988
+ console.log('');
989
+ }
990
+ async runSelectLinks(selector) {
991
+ if (!this.currentDoc) {
992
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
993
+ return;
994
+ }
995
+ try {
996
+ const linkSelector = selector || 'a[href]';
997
+ const elements = this.currentDoc.select(linkSelector);
998
+ const links = [];
999
+ elements.each((el, i) => {
1000
+ const href = el.attr('href');
1001
+ const text = el.text().trim().slice(0, 50);
1002
+ if (href) {
1003
+ links.push({ text, href });
1004
+ if (i < 20) {
1005
+ console.log(`${colors.gray(`${i + 1}.`)} ${colors.cyan(text || '(no text)')} ${colors.gray('→')} ${href}`);
1006
+ }
1007
+ }
1008
+ });
1009
+ if (links.length > 20) {
1010
+ console.log(colors.gray(` ... and ${links.length - 20} more links`));
1011
+ }
1012
+ this.lastResponse = links;
1013
+ console.log(colors.gray(`\n ${links.length} link(s) found`));
1014
+ }
1015
+ catch (error) {
1016
+ console.error(colors.red(`Query failed: ${error.message}`));
1017
+ }
1018
+ console.log('');
1019
+ }
1020
+ async runSelectImages(selector) {
1021
+ if (!this.currentDoc) {
1022
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
1023
+ return;
1024
+ }
1025
+ try {
1026
+ const imageExtensions = /\.(png|jpg|jpeg|gif|webp|svg|ico|bmp|tiff|avif)(\?.*)?$/i;
1027
+ const images = [];
1028
+ const scope = selector ? `${selector} ` : '';
1029
+ this.currentDoc.select(`${scope}img[src]`).each((el) => {
1030
+ const src = el.attr('src');
1031
+ if (src)
1032
+ images.push({ type: 'img', src, alt: el.attr('alt') });
1033
+ });
1034
+ this.currentDoc.select(`${scope}source[srcset]`).each((el) => {
1035
+ const srcset = el.attr('srcset');
1036
+ if (srcset) {
1037
+ const src = srcset.split(',')[0].trim().split(' ')[0];
1038
+ if (src)
1039
+ images.push({ type: 'source', src });
1040
+ }
1041
+ });
1042
+ this.currentDoc.select(`${scope}[style*="background"]`).each((el) => {
1043
+ const style = el.attr('style') || '';
1044
+ const matches = style.match(/url\(['"]?([^'"()]+)['"]?\)/gi);
1045
+ if (matches) {
1046
+ matches.forEach(m => {
1047
+ const src = m.replace(/url\(['"]?|['"]?\)/gi, '');
1048
+ if (imageExtensions.test(src))
1049
+ images.push({ type: 'bg', src });
1050
+ });
1051
+ }
1052
+ });
1053
+ if (!selector) {
1054
+ this.currentDoc.select('link[href]').each((el) => {
1055
+ const href = el.attr('href');
1056
+ if (href && imageExtensions.test(href)) {
1057
+ images.push({ type: 'link', src: href });
1058
+ }
1059
+ });
1060
+ this.currentDoc.select('meta[property="og:image"], meta[name="twitter:image"]').each((el) => {
1061
+ const content = el.attr('content');
1062
+ if (content)
1063
+ images.push({ type: 'meta', src: content });
1064
+ });
1065
+ }
1066
+ const uniqueImages = [...new Map(images.map(img => [img.src, img])).values()];
1067
+ uniqueImages.slice(0, 25).forEach((img, i) => {
1068
+ const typeLabel = colors.gray(`[${img.type}]`);
1069
+ const altText = img.alt ? colors.cyan(img.alt.slice(0, 25)) : '';
1070
+ console.log(`${colors.gray(`${i + 1}.`)} ${typeLabel} ${altText} ${img.src.slice(0, 60)}`);
1071
+ });
1072
+ if (uniqueImages.length > 25) {
1073
+ console.log(colors.gray(` ... and ${uniqueImages.length - 25} more images`));
1074
+ }
1075
+ this.lastResponse = uniqueImages;
1076
+ console.log(colors.gray(`\n ${uniqueImages.length} image(s) found`));
1077
+ }
1078
+ catch (error) {
1079
+ console.error(colors.red(`Query failed: ${error.message}`));
1080
+ }
1081
+ console.log('');
1082
+ }
1083
+ async runSelectScripts() {
1084
+ if (!this.currentDoc) {
1085
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
1086
+ return;
1087
+ }
1088
+ try {
1089
+ const scripts = [];
1090
+ this.currentDoc.select('script[src]').each((el) => {
1091
+ const src = el.attr('src');
1092
+ if (src) {
1093
+ scripts.push({
1094
+ type: 'external',
1095
+ src,
1096
+ async: el.attr('async') !== undefined,
1097
+ defer: el.attr('defer') !== undefined
1098
+ });
1099
+ }
1100
+ });
1101
+ this.currentDoc.select('script:not([src])').each((el) => {
1102
+ const content = el.text();
1103
+ if (content.trim()) {
1104
+ scripts.push({
1105
+ type: 'inline',
1106
+ size: content.length
1107
+ });
1108
+ }
1109
+ });
1110
+ let extCount = 0, inlineCount = 0, totalInlineSize = 0;
1111
+ scripts.forEach((script, i) => {
1112
+ if (script.type === 'external') {
1113
+ extCount++;
1114
+ const flags = [
1115
+ script.async ? colors.cyan('async') : '',
1116
+ script.defer ? colors.cyan('defer') : ''
1117
+ ].filter(Boolean).join(' ');
1118
+ if (i < 20) {
1119
+ console.log(`${colors.gray(`${i + 1}.`)} ${colors.green('[ext]')} ${script.src?.slice(0, 70)} ${flags}`);
1120
+ }
1121
+ }
1122
+ else {
1123
+ inlineCount++;
1124
+ totalInlineSize += script.size || 0;
1125
+ if (i < 20) {
1126
+ console.log(`${colors.gray(`${i + 1}.`)} ${colors.yellow('[inline]')} ${((script.size || 0) / 1024).toFixed(1)}kb`);
1127
+ }
1128
+ }
1129
+ });
1130
+ if (scripts.length > 20) {
1131
+ console.log(colors.gray(` ... and ${scripts.length - 20} more scripts`));
1132
+ }
1133
+ this.lastResponse = scripts;
1134
+ console.log(colors.gray(`\n ${extCount} external, ${inlineCount} inline (${(totalInlineSize / 1024).toFixed(1)}kb total)`));
1135
+ }
1136
+ catch (error) {
1137
+ console.error(colors.red(`Query failed: ${error.message}`));
1138
+ }
1139
+ console.log('');
1140
+ }
1141
+ async runSelectCSS() {
1142
+ if (!this.currentDoc) {
1143
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
1144
+ return;
1145
+ }
1146
+ try {
1147
+ const styles = [];
1148
+ this.currentDoc.select('link[rel="stylesheet"]').each((el) => {
1149
+ const href = el.attr('href');
1150
+ if (href) {
1151
+ styles.push({
1152
+ type: 'external',
1153
+ href,
1154
+ media: el.attr('media')
1155
+ });
1156
+ }
1157
+ });
1158
+ this.currentDoc.select('style').each((el) => {
1159
+ const content = el.text();
1160
+ if (content.trim()) {
1161
+ styles.push({
1162
+ type: 'inline',
1163
+ size: content.length,
1164
+ media: el.attr('media')
1165
+ });
1166
+ }
1167
+ });
1168
+ let extCount = 0, inlineCount = 0, totalInlineSize = 0;
1169
+ styles.forEach((style, i) => {
1170
+ if (style.type === 'external') {
1171
+ extCount++;
1172
+ const media = style.media ? colors.cyan(`[${style.media}]`) : '';
1173
+ if (i < 20) {
1174
+ console.log(`${colors.gray(`${i + 1}.`)} ${colors.green('[ext]')} ${style.href?.slice(0, 70)} ${media}`);
1175
+ }
1176
+ }
1177
+ else {
1178
+ inlineCount++;
1179
+ totalInlineSize += style.size || 0;
1180
+ const media = style.media ? colors.cyan(`[${style.media}]`) : '';
1181
+ if (i < 20) {
1182
+ console.log(`${colors.gray(`${i + 1}.`)} ${colors.yellow('[inline]')} ${((style.size || 0) / 1024).toFixed(1)}kb ${media}`);
1183
+ }
1184
+ }
1185
+ });
1186
+ if (styles.length > 20) {
1187
+ console.log(colors.gray(` ... and ${styles.length - 20} more stylesheets`));
1188
+ }
1189
+ this.lastResponse = styles;
1190
+ console.log(colors.gray(`\n ${extCount} external, ${inlineCount} inline (${(totalInlineSize / 1024).toFixed(1)}kb total)`));
1191
+ }
1192
+ catch (error) {
1193
+ console.error(colors.red(`Query failed: ${error.message}`));
1194
+ }
1195
+ console.log('');
1196
+ }
1197
+ async runSelectSourcemaps() {
1198
+ if (!this.currentDoc) {
1199
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
1200
+ return;
1201
+ }
1202
+ try {
1203
+ const sourcemaps = [];
1204
+ const sourceMappingURLPattern = /\/[/*]#\s*sourceMappingURL=([^\s*]+)/gi;
1205
+ this.currentDoc.select('script:not([src])').each((el) => {
1206
+ const content = el.text();
1207
+ const matches = content.matchAll(sourceMappingURLPattern);
1208
+ for (const match of matches) {
1209
+ sourcemaps.push({ type: 'inline-js', url: match[1] });
1210
+ }
1211
+ });
1212
+ this.currentDoc.select('style').each((el) => {
1213
+ const content = el.text();
1214
+ const matches = content.matchAll(sourceMappingURLPattern);
1215
+ for (const match of matches) {
1216
+ sourcemaps.push({ type: 'inline-css', url: match[1] });
1217
+ }
1218
+ });
1219
+ this.currentDoc.select('script[src]').each((el) => {
1220
+ const src = el.attr('src');
1221
+ if (src && !src.endsWith('.map')) {
1222
+ sourcemaps.push({ type: 'js-inferred', url: `${src}.map`, source: src });
1223
+ }
1224
+ });
1225
+ this.currentDoc.select('link[rel="stylesheet"]').each((el) => {
1226
+ const href = el.attr('href');
1227
+ if (href && !href.endsWith('.map')) {
1228
+ sourcemaps.push({ type: 'css-inferred', url: `${href}.map`, source: href });
1229
+ }
1230
+ });
1231
+ this.currentDoc.select('script[src$=".map"], link[href$=".map"]').each((el) => {
1232
+ const url = el.attr('src') || el.attr('href');
1233
+ if (url)
1234
+ sourcemaps.push({ type: 'direct', url });
1235
+ });
1236
+ const uniqueMaps = [...new Map(sourcemaps.map(m => [m.url, m])).values()];
1237
+ const confirmed = uniqueMaps.filter(m => !m.type.includes('inferred'));
1238
+ const inferred = uniqueMaps.filter(m => m.type.includes('inferred'));
1239
+ if (confirmed.length > 0) {
1240
+ console.log(colors.green('Confirmed sourcemaps:'));
1241
+ confirmed.forEach((m, i) => {
1242
+ console.log(`${colors.gray(`${i + 1}.`)} ${colors.cyan(`[${m.type}]`)} ${m.url}`);
1243
+ });
1244
+ }
1245
+ if (inferred.length > 0) {
1246
+ console.log(colors.yellow('\nPotential sourcemaps (inferred):'));
1247
+ inferred.slice(0, 15).forEach((m, i) => {
1248
+ console.log(`${colors.gray(`${i + 1}.`)} ${colors.gray(`[${m.type}]`)} ${m.url.slice(0, 70)}`);
1249
+ });
1250
+ if (inferred.length > 15) {
1251
+ console.log(colors.gray(` ... and ${inferred.length - 15} more`));
1252
+ }
1253
+ }
1254
+ this.lastResponse = uniqueMaps;
1255
+ console.log(colors.gray(`\n ${confirmed.length} confirmed, ${inferred.length} inferred sourcemap(s)`));
1256
+ console.log(colors.gray(` Use $unmap <url> to extract original sources`));
1257
+ }
1258
+ catch (error) {
1259
+ console.error(colors.red(`Query failed: ${error.message}`));
1260
+ }
1261
+ console.log('');
1262
+ }
1263
+ async runUnmap(urlArg) {
1264
+ let mapUrl = urlArg;
1265
+ if (!mapUrl && Array.isArray(this.lastResponse)) {
1266
+ const maps = this.lastResponse;
1267
+ const confirmed = maps.filter(m => !m.type.includes('inferred'));
1268
+ if (confirmed.length > 0) {
1269
+ mapUrl = confirmed[0].url;
1270
+ console.log(colors.gray(`Using: ${mapUrl}`));
1271
+ }
1272
+ else if (maps.length > 0) {
1273
+ mapUrl = maps[0].url;
1274
+ console.log(colors.gray(`Using (inferred): ${mapUrl}`));
1275
+ }
1276
+ }
1277
+ if (!mapUrl) {
1278
+ console.log(colors.yellow('Usage: $unmap <sourcemap-url>'));
1279
+ console.log(colors.gray(' Or run $sourcemaps first to find sourcemaps'));
1280
+ return;
1281
+ }
1282
+ if (!mapUrl.startsWith('http') && this.baseUrl) {
1283
+ const base = new URL(this.baseUrl);
1284
+ mapUrl = new URL(mapUrl, base).toString();
1285
+ }
1286
+ console.log(colors.cyan(`Fetching sourcemap: ${mapUrl}`));
1287
+ try {
1288
+ const response = await this.client.get(mapUrl);
1289
+ const mapData = await response.json();
1290
+ if (!mapData.sources || !Array.isArray(mapData.sources)) {
1291
+ console.log(colors.red('Invalid sourcemap: missing sources array'));
1292
+ return;
1293
+ }
1294
+ console.log(colors.green(`\nSourcemap v${mapData.version || '?'}`));
1295
+ if (mapData.file)
1296
+ console.log(colors.gray(` File: ${mapData.file}`));
1297
+ if (mapData.sourceRoot)
1298
+ console.log(colors.gray(` Root: ${mapData.sourceRoot}`));
1299
+ console.log(colors.gray(` Sources: ${mapData.sources.length}`));
1300
+ if (mapData.names)
1301
+ console.log(colors.gray(` Names: ${mapData.names.length}`));
1302
+ console.log(colors.bold('\nOriginal sources:'));
1303
+ mapData.sources.forEach((source, i) => {
1304
+ const hasContent = mapData.sourcesContent && mapData.sourcesContent[i];
1305
+ const sizeInfo = hasContent
1306
+ ? colors.green(`[${(mapData.sourcesContent[i].length / 1024).toFixed(1)}kb]`)
1307
+ : colors.yellow('[no content]');
1308
+ console.log(`${colors.gray(`${i + 1}.`)} ${sizeInfo} ${source}`);
1309
+ });
1310
+ this.lastResponse = {
1311
+ url: mapUrl,
1312
+ data: mapData,
1313
+ sources: mapData.sources.map((source, i) => ({
1314
+ path: source,
1315
+ content: mapData.sourcesContent?.[i] || null
1316
+ }))
1317
+ };
1318
+ const withContent = mapData.sourcesContent?.filter(c => c).length || 0;
1319
+ console.log(colors.gray(`\n ${withContent}/${mapData.sources.length} sources have embedded content`));
1320
+ if (withContent > 0) {
1321
+ console.log(colors.gray(` Use $unmap:view <index> to view source content`));
1322
+ console.log(colors.gray(` Use $unmap:save <dir> to save all sources to disk`));
1323
+ }
1324
+ }
1325
+ catch (error) {
1326
+ if (error.status === 404) {
1327
+ console.log(colors.yellow(`Sourcemap not found (404): ${mapUrl}`));
1328
+ }
1329
+ else {
1330
+ console.error(colors.red(`Failed to fetch sourcemap: ${error.message}`));
1331
+ }
1332
+ }
1333
+ console.log('');
1334
+ }
1335
+ async runUnmapView(indexStr) {
1336
+ if (!this.lastResponse || !this.lastResponse.sources) {
1337
+ console.log(colors.yellow('No sourcemap loaded. Use $unmap <url> first.'));
1338
+ return;
1339
+ }
1340
+ const index = parseInt(indexStr, 10) - 1;
1341
+ const sources = this.lastResponse.sources;
1342
+ if (isNaN(index) || index < 0 || index >= sources.length) {
1343
+ console.log(colors.yellow(`Invalid index. Use 1-${sources.length}`));
1344
+ return;
1345
+ }
1346
+ const source = sources[index];
1347
+ if (!source.content) {
1348
+ console.log(colors.yellow(`No embedded content for: ${source.path}`));
1349
+ return;
1350
+ }
1351
+ console.log(colors.bold(`\n─── ${source.path} ───\n`));
1352
+ const ext = source.path.split('.').pop()?.toLowerCase();
1353
+ if (['js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs'].includes(ext || '')) {
1354
+ try {
1355
+ console.log(highlight(source.content, { linenos: true }));
1356
+ }
1357
+ catch {
1358
+ console.log(source.content);
1359
+ }
1360
+ }
1361
+ else {
1362
+ console.log(source.content);
1363
+ }
1364
+ console.log(colors.bold(`\n─── end ───\n`));
1365
+ }
1366
+ async runUnmapSave(dir) {
1367
+ if (!this.lastResponse || !this.lastResponse.sources) {
1368
+ console.log(colors.yellow('No sourcemap loaded. Use $unmap <url> first.'));
1369
+ return;
1370
+ }
1371
+ const outputDir = dir || './sourcemap-extracted';
1372
+ const sources = this.lastResponse.sources;
1373
+ const { promises: fs } = await import('node:fs');
1374
+ const path = await import('node:path');
1375
+ let saved = 0, skipped = 0;
1376
+ for (const source of sources) {
1377
+ if (!source.content) {
1378
+ skipped++;
1379
+ continue;
1380
+ }
1381
+ let cleanPath = source.path
1382
+ .replace(/^webpack:\/\/[^/]*\//, '')
1383
+ .replace(/^\.*\//, '')
1384
+ .replace(/^node_modules\//, 'node_modules/');
1385
+ const fullPath = path.join(outputDir, cleanPath);
1386
+ const dirname = path.dirname(fullPath);
1387
+ try {
1388
+ await fs.mkdir(dirname, { recursive: true });
1389
+ await fs.writeFile(fullPath, source.content, 'utf-8');
1390
+ saved++;
1391
+ console.log(colors.green(` ✓ ${cleanPath}`));
1392
+ }
1393
+ catch (err) {
1394
+ console.log(colors.red(` ✗ ${cleanPath}: ${err.message}`));
1395
+ }
1396
+ }
1397
+ console.log(colors.gray(`\n Saved ${saved} files to ${outputDir}`));
1398
+ if (skipped > 0) {
1399
+ console.log(colors.yellow(` Skipped ${skipped} sources without embedded content`));
1400
+ }
1401
+ console.log('');
1402
+ }
1403
+ async runBeautify(urlArg) {
1404
+ if (!urlArg) {
1405
+ console.log(colors.yellow('Usage: $beautify <url-to-js-or-css>'));
1406
+ console.log(colors.gray(' Downloads and formats minified JS/CSS code'));
1407
+ return;
1408
+ }
1409
+ let url = urlArg;
1410
+ if (!url.startsWith('http') && this.baseUrl) {
1411
+ const base = new URL(this.baseUrl);
1412
+ url = new URL(url, base).toString();
1413
+ }
1414
+ console.log(colors.cyan(`Fetching: ${url}`));
1415
+ try {
1416
+ const response = await this.client.get(url);
1417
+ const code = await response.text();
1418
+ const isCSS = url.endsWith('.css') || response.headers.get('content-type')?.includes('css');
1419
+ console.log(colors.gray(` Size: ${(code.length / 1024).toFixed(1)}kb`));
1420
+ const formatted = isCSS ? this.beautifyCSS(code) : this.beautifyJS(code);
1421
+ console.log(colors.bold(`\n─── Beautified ${isCSS ? 'CSS' : 'JS'} ───\n`));
1422
+ try {
1423
+ console.log(highlight(formatted, { linenos: true }));
1424
+ }
1425
+ catch {
1426
+ console.log(formatted);
1427
+ }
1428
+ console.log(colors.bold(`\n─── end ───`));
1429
+ this.lastResponse = { url, original: code, formatted, type: isCSS ? 'css' : 'js' };
1430
+ console.log(colors.gray(`\n Use $beautify:save <file> to save formatted code`));
1431
+ }
1432
+ catch (error) {
1433
+ console.error(colors.red(`Failed to fetch: ${error.message}`));
1434
+ }
1435
+ console.log('');
1436
+ }
1437
+ beautifyJS(code) {
1438
+ let result = '';
1439
+ let indent = 0;
1440
+ let inString = null;
1441
+ let inComment = false;
1442
+ let inLineComment = false;
1443
+ let i = 0;
1444
+ const addNewline = () => {
1445
+ result += '\n' + ' '.repeat(indent);
1446
+ };
1447
+ while (i < code.length) {
1448
+ const char = code[i];
1449
+ const next = code[i + 1];
1450
+ const prev = code[i - 1];
1451
+ if (!inComment && !inLineComment) {
1452
+ if ((char === '"' || char === "'" || char === '`') && prev !== '\\') {
1453
+ if (inString === char) {
1454
+ inString = null;
1455
+ }
1456
+ else if (!inString) {
1457
+ inString = char;
1458
+ }
1459
+ }
1460
+ }
1461
+ if (!inString && !inComment && !inLineComment) {
1462
+ if (char === '/' && next === '*') {
1463
+ inComment = true;
1464
+ result += char;
1465
+ i++;
1466
+ continue;
1467
+ }
1468
+ if (char === '/' && next === '/') {
1469
+ inLineComment = true;
1470
+ result += char;
1471
+ i++;
1472
+ continue;
1473
+ }
1474
+ }
1475
+ if (inComment && char === '*' && next === '/') {
1476
+ result += '*/';
1477
+ inComment = false;
1478
+ i += 2;
1479
+ continue;
1480
+ }
1481
+ if (inLineComment && char === '\n') {
1482
+ inLineComment = false;
1483
+ }
1484
+ if (inString || inComment || inLineComment) {
1485
+ result += char;
1486
+ i++;
1487
+ continue;
1488
+ }
1489
+ if (char === '{') {
1490
+ result += ' {';
1491
+ indent++;
1492
+ addNewline();
1493
+ i++;
1494
+ continue;
1495
+ }
1496
+ if (char === '}') {
1497
+ indent = Math.max(0, indent - 1);
1498
+ addNewline();
1499
+ result += '}';
1500
+ if (next && next !== ';' && next !== ',' && next !== ')' && next !== '\n') {
1501
+ addNewline();
1502
+ }
1503
+ i++;
1504
+ continue;
1505
+ }
1506
+ if (char === ';') {
1507
+ result += ';';
1508
+ if (next && next !== '}' && next !== '\n') {
1509
+ addNewline();
1510
+ }
1511
+ i++;
1512
+ continue;
1513
+ }
1514
+ if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
1515
+ if (result.length > 0 && !/\s$/.test(result)) {
1516
+ result += ' ';
1517
+ }
1518
+ i++;
1519
+ continue;
1520
+ }
1521
+ result += char;
1522
+ i++;
1523
+ }
1524
+ return result.trim();
1525
+ }
1526
+ beautifyCSS(code) {
1527
+ let result = '';
1528
+ let indent = 0;
1529
+ let inString = null;
1530
+ let i = 0;
1531
+ const addNewline = () => {
1532
+ result += '\n' + ' '.repeat(indent);
1533
+ };
1534
+ while (i < code.length) {
1535
+ const char = code[i];
1536
+ const next = code[i + 1];
1537
+ const prev = code[i - 1];
1538
+ if ((char === '"' || char === "'") && prev !== '\\') {
1539
+ if (inString === char) {
1540
+ inString = null;
1541
+ }
1542
+ else if (!inString) {
1543
+ inString = char;
1544
+ }
1545
+ }
1546
+ if (inString) {
1547
+ result += char;
1548
+ i++;
1549
+ continue;
1550
+ }
1551
+ if (char === '{') {
1552
+ result += ' {';
1553
+ indent++;
1554
+ addNewline();
1555
+ i++;
1556
+ continue;
1557
+ }
1558
+ if (char === '}') {
1559
+ indent = Math.max(0, indent - 1);
1560
+ addNewline();
1561
+ result += '}';
1562
+ addNewline();
1563
+ i++;
1564
+ continue;
1565
+ }
1566
+ if (char === ';') {
1567
+ result += ';';
1568
+ addNewline();
1569
+ i++;
1570
+ continue;
1571
+ }
1572
+ if (char === ',' && indent === 0) {
1573
+ result += ',';
1574
+ addNewline();
1575
+ i++;
1576
+ continue;
1577
+ }
1578
+ if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
1579
+ if (result.length > 0 && !/\s$/.test(result)) {
1580
+ result += ' ';
1581
+ }
1582
+ i++;
1583
+ continue;
1584
+ }
1585
+ result += char;
1586
+ i++;
1587
+ }
1588
+ return result.trim();
1589
+ }
1590
+ async runBeautifySave(filename) {
1591
+ if (!this.lastResponse || !this.lastResponse.formatted) {
1592
+ console.log(colors.yellow('No beautified code. Use $beautify <url> first.'));
1593
+ return;
1594
+ }
1595
+ const outputFile = filename || `beautified.${this.lastResponse.type}`;
1596
+ const { promises: fs } = await import('node:fs');
1597
+ try {
1598
+ await fs.writeFile(outputFile, this.lastResponse.formatted, 'utf-8');
1599
+ console.log(colors.green(` ✓ Saved to ${outputFile}`));
1600
+ }
1601
+ catch (err) {
1602
+ console.log(colors.red(` ✗ Failed to save: ${err.message}`));
1603
+ }
1604
+ console.log('');
1605
+ }
1606
+ async runSelectTable(selector) {
1607
+ if (!this.currentDoc) {
1608
+ console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
1609
+ return;
1610
+ }
1611
+ if (!selector) {
1612
+ console.log(colors.yellow('Usage: $table <selector>'));
1613
+ console.log(colors.gray(' Examples: $table table | $table .data-table'));
1614
+ return;
1615
+ }
1616
+ try {
1617
+ const tables = this.currentDoc.tables(selector);
1618
+ if (tables.length === 0) {
1619
+ console.log(colors.gray('No tables found'));
1620
+ return;
1621
+ }
1622
+ tables.forEach((table, tableIndex) => {
1623
+ console.log(colors.bold(`\nTable ${tableIndex + 1}:`));
1624
+ if (table.headers.length > 0) {
1625
+ console.log(colors.cyan(' Headers: ') + table.headers.join(' | '));
1626
+ }
1627
+ console.log(colors.cyan(` Rows: `) + table.rows.length);
1628
+ table.rows.slice(0, 5).forEach((row, i) => {
1629
+ const rowStr = row.map(cell => cell.slice(0, 20)).join(' | ');
1630
+ console.log(` ${colors.gray(`${i + 1}.`)} ${rowStr}`);
1631
+ });
1632
+ if (table.rows.length > 5) {
1633
+ console.log(colors.gray(` ... and ${table.rows.length - 5} more rows`));
1634
+ }
1635
+ });
1636
+ this.lastResponse = tables;
1637
+ }
1638
+ catch (error) {
1639
+ console.error(colors.red(`Query failed: ${error.message}`));
584
1640
  }
585
1641
  console.log('');
586
1642
  }
587
1643
  printHelp() {
588
1644
  console.log(`
589
- ${pc.bold(pc.cyan('Rek Console Help'))}
1645
+ ${colors.bold(colors.cyan('Rek Console Help'))}
1646
+
1647
+ ${colors.bold('Core Commands:')}
1648
+ ${colors.green('url <url>')} Set persistent Base URL.
1649
+ ${colors.green('set <key>=<val>')} Set a session variable.
1650
+ ${colors.green('vars')} List all session and env variables.
1651
+ ${colors.green('env [path]')} Load .env file (default: ./.env).
1652
+ ${colors.green('clear')} Clear the screen.
1653
+ ${colors.green('exit')} Exit the console.
590
1654
 
591
- ${pc.bold('Core Commands:')}
592
- ${pc.green('url <url>')} Set persistent Base URL.
593
- ${pc.green('set <key>=<val>')} Set a session variable.
594
- ${pc.green('vars')} List all session variables.
595
- ${pc.green('clear')} Clear the screen.
596
- ${pc.green('exit')} Exit the console.
1655
+ ${colors.bold('HTTP Requests:')}
1656
+ ${colors.green('<method> <path>')} Execute HTTP request (GET, POST, PUT, DELETE, etc).
1657
+ ${colors.gray('Params:')} ${colors.white('key=value')} (string) or ${colors.white('key:=value')} (typed).
1658
+ ${colors.gray('Headers:')} ${colors.white('Key:Value')}
597
1659
 
598
- ${pc.bold('HTTP Requests:')}
599
- ${pc.green('<method> <path>')} Execute HTTP request (GET, POST, PUT, DELETE, etc).
600
- ${pc.gray('Params:')} ${pc.white('key=value')} (string) or ${pc.white('key:=value')} (typed).
601
- ${pc.gray('Headers:')} ${pc.white('Key:Value')}
1660
+ ${colors.bold('Advanced Tools:')}
1661
+ ${colors.green('load <url>')} Run Load Test.
1662
+ ${colors.gray('Options:')}
1663
+ ${colors.white('users=50')} ${colors.gray('Concurrent users')}
1664
+ ${colors.white('duration=300')} ${colors.gray('Duration in seconds')}
1665
+ ${colors.white('ramp=5')} ${colors.gray('Ramp-up time in seconds')}
1666
+ ${colors.white('mode=realistic')} ${colors.gray('realistic | throughput | stress')}
1667
+ ${colors.white('http2=false')} ${colors.gray('Force HTTP/2')}
602
1668
 
603
- ${pc.bold('Advanced Tools:')}
604
- ${pc.green('load <url>')} Run Load Test.
605
- ${pc.gray('Options:')}
606
- ${pc.white('users=50')} ${pc.gray('Concurrent users')}
607
- ${pc.white('duration=300')} ${pc.gray('Duration in seconds')}
608
- ${pc.white('ramp=5')} ${pc.gray('Ramp-up time in seconds')}
609
- ${pc.white('mode=throughput')}${pc.gray('throughput | stress | realistic')}
610
- ${pc.white('http2=false')} ${pc.gray('Force HTTP/2')}
1669
+ ${colors.green('chat <provider>')} Start AI Chat.
1670
+ ${colors.gray('Providers:')} ${colors.white('openai')}, ${colors.white('anthropic')}
1671
+ ${colors.gray('Arg:')} ${colors.white('model=...')} (optional)
611
1672
 
612
- ${pc.green('chat <provider>')} Start AI Chat.
613
- ${pc.gray('Providers:')} ${pc.white('openai')}, ${pc.white('anthropic')}
614
- ${pc.gray('Arg:')} ${pc.white('model=...')} (optional)
1673
+ ${colors.green('ws <url>')} Start interactive WebSocket session.
1674
+ ${colors.green('udp <url>')} Send UDP packet.
615
1675
 
616
- ${pc.green('ws <url>')} Start interactive WebSocket session.
617
- ${pc.green('udp <url>')} Send UDP packet.
1676
+ ${colors.bold('Network Tools:')}
1677
+ ${colors.green('whois <domain>')} WHOIS lookup (domain or IP).
1678
+ ${colors.green('tls <host> [port]')} Inspect TLS/SSL certificate.
1679
+ ${colors.green('dns <domain>')} Full DNS lookup (A, AAAA, MX, NS, SPF, DMARC).
1680
+ ${colors.green('rdap <domain>')} RDAP lookup (modern WHOIS).
1681
+ ${colors.green('ping <host>')} Quick TCP connectivity check.
618
1682
 
619
- ${pc.bold('Network Tools:')}
620
- ${pc.green('whois <domain>')} WHOIS lookup (domain or IP).
621
- ${pc.green('tls <host> [port]')} Inspect TLS/SSL certificate.
622
- ${pc.green('dns <domain>')} Full DNS lookup (A, AAAA, MX, NS, SPF, DMARC).
623
- ${pc.green('rdap <domain>')} RDAP lookup (modern WHOIS).
624
- ${pc.green('ping <host>')} Quick TCP connectivity check.
1683
+ ${colors.bold('Web Scraping:')}
1684
+ ${colors.green('scrap <url>')} Fetch and parse HTML document.
1685
+ ${colors.green('$ <selector>')} Query elements (CSS selector).
1686
+ ${colors.green('$text <selector>')} Extract text content.
1687
+ ${colors.green('$attr <name> <sel>')} Extract attribute values.
1688
+ ${colors.green('$html <selector>')} Get inner HTML.
1689
+ ${colors.green('$links [selector]')} List all links.
1690
+ ${colors.green('$images [selector]')} List all images (img, bg, og:image, favicon).
1691
+ ${colors.green('$scripts')} List all scripts (external + inline).
1692
+ ${colors.green('$css')} List all stylesheets (external + inline).
1693
+ ${colors.green('$sourcemaps')} Find sourcemaps (confirmed + inferred).
1694
+ ${colors.green('$unmap <url>')} Download and parse sourcemap.
1695
+ ${colors.green('$unmap:view <n>')} View source file by index.
1696
+ ${colors.green('$unmap:save [dir]')} Save all sources to disk.
1697
+ ${colors.green('$beautify <url>')} Format minified JS/CSS code.
1698
+ ${colors.green('$beautify:save [f]')} Save beautified code to file.
1699
+ ${colors.green('$table <selector>')} Extract table as data.
625
1700
 
626
- ${pc.bold('Examples:')}
1701
+ ${colors.bold('Examples:')}
627
1702
  › url httpbin.org
628
1703
  › get /json
629
1704
  › post /post name="Neo" active:=true role:Admin