recker 1.0.8 → 1.0.10
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/handler.d.ts.map +1 -1
- package/dist/cli/handler.js +17 -3
- package/dist/cli/index.js +81 -2
- package/dist/cli/tui/shell.d.ts +26 -0
- package/dist/cli/tui/shell.d.ts.map +1 -1
- package/dist/cli/tui/shell.js +1226 -151
- package/dist/utils/whois.d.ts.map +1 -1
- package/dist/utils/whois.js +17 -2
- package/package.json +4 -7
package/dist/cli/tui/shell.js
CHANGED
|
@@ -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
|
|
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 ?
|
|
51
|
-
return `${base} ${
|
|
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
|
-
'
|
|
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(
|
|
67
|
-
console.log(
|
|
68
|
-
console.log(
|
|
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
|
-
|
|
79
|
-
this.
|
|
109
|
+
console.log('');
|
|
110
|
+
this.rl.close();
|
|
80
111
|
});
|
|
81
112
|
this.rl.on('close', () => {
|
|
82
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 = '
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
485
|
+
console.log(colors.green('✔ Sent/Received'));
|
|
338
486
|
if (text)
|
|
339
487
|
console.log(text);
|
|
340
488
|
return;
|
|
341
489
|
}
|
|
342
|
-
console.log(
|
|
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 ?
|
|
352
|
-
console.log(`${statusColor(
|
|
353
|
-
`${
|
|
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(
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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 ?
|
|
427
|
-
const statusText = info.valid ?
|
|
428
|
-
console.log(`${statusIcon} Certificate ${statusText}` +
|
|
429
|
-
console.log(
|
|
430
|
-
console.log(` ${
|
|
431
|
-
console.log(` ${
|
|
432
|
-
console.log(` ${
|
|
433
|
-
console.log(` ${
|
|
434
|
-
const daysColor = info.daysRemaining < 30 ?
|
|
435
|
-
console.log(` ${
|
|
436
|
-
console.log(
|
|
437
|
-
console.log(` ${
|
|
438
|
-
console.log(` ${
|
|
439
|
-
console.log(` ${
|
|
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(` ${
|
|
647
|
+
console.log(` ${colors.cyan('Auth Error')}: ${colors.red(String(info.authorizationError))}`);
|
|
442
648
|
}
|
|
443
|
-
console.log(
|
|
444
|
-
console.log(` ${
|
|
445
|
-
console.log(` ${
|
|
446
|
-
console.log(` ${
|
|
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(
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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(
|
|
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(
|
|
682
|
+
console.log(colors.green(`✔ DNS resolved`) + colors.gray(` (${duration}ms)\n`));
|
|
473
683
|
if (a.length) {
|
|
474
|
-
console.log(
|
|
475
|
-
a.forEach(ip => console.log(` ${
|
|
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(
|
|
479
|
-
aaaa.forEach(ip => console.log(` ${
|
|
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(
|
|
483
|
-
ns.forEach(n => console.log(` ${
|
|
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(
|
|
696
|
+
console.log(colors.bold(' MX Records:'));
|
|
487
697
|
mx.sort((a, b) => a.priority - b.priority)
|
|
488
|
-
.forEach(m => console.log(` ${
|
|
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(
|
|
493
|
-
console.log(` ${
|
|
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(
|
|
497
|
-
console.log(` ${
|
|
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(
|
|
501
|
-
sec.caa.issue.forEach((ca) => console.log(` ${
|
|
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(
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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(
|
|
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(
|
|
735
|
+
console.log(colors.green(`✔ RDAP lookup completed`) + colors.gray(` (${duration}ms)\n`));
|
|
522
736
|
if (result.status?.length) {
|
|
523
|
-
console.log(
|
|
524
|
-
result.status.forEach((s) => console.log(` ${
|
|
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(
|
|
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(` ${
|
|
744
|
+
console.log(` ${colors.cyan(e.eventAction.padEnd(15))} ${date}`);
|
|
531
745
|
});
|
|
532
746
|
}
|
|
533
747
|
if (result.entities?.length) {
|
|
534
|
-
console.log(
|
|
748
|
+
console.log(colors.bold(' Entities:'));
|
|
535
749
|
result.entities.forEach((e) => {
|
|
536
750
|
const roles = e.roles?.join(', ') || 'unknown';
|
|
537
|
-
console.log(` ${
|
|
751
|
+
console.log(` ${colors.cyan(roles.padEnd(15))} ${e.handle || 'N/A'}`);
|
|
538
752
|
});
|
|
539
753
|
}
|
|
540
754
|
if (result.handle) {
|
|
541
|
-
console.log(` ${
|
|
755
|
+
console.log(` ${colors.cyan('Handle')}: ${result.handle}`);
|
|
542
756
|
}
|
|
543
757
|
if (result.name) {
|
|
544
|
-
console.log(` ${
|
|
758
|
+
console.log(` ${colors.cyan('Name')}: ${result.name}`);
|
|
545
759
|
}
|
|
546
760
|
if (result.startAddress && result.endAddress) {
|
|
547
|
-
console.log(` ${
|
|
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(
|
|
553
|
-
console.log(
|
|
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
|
-
|
|
560
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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(
|
|
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(
|
|
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
|
-
${
|
|
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
|
-
${
|
|
592
|
-
${
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
${
|
|
599
|
-
${
|
|
600
|
-
${
|
|
601
|
-
${
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
${
|
|
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
|
-
${
|
|
613
|
-
|
|
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
|
-
|
|
617
|
-
${
|
|
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
|
-
${
|
|
620
|
-
${
|
|
621
|
-
${
|
|
622
|
-
${
|
|
623
|
-
${
|
|
624
|
-
${
|
|
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
|
-
${
|
|
1701
|
+
${colors.bold('Examples:')}
|
|
627
1702
|
› url httpbin.org
|
|
628
1703
|
› get /json
|
|
629
1704
|
› post /post name="Neo" active:=true role:Admin
|