recker 1.0.9 → 1.0.10-alpha.bf846c8

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';
@@ -28,6 +30,8 @@ export class RekShell {
28
30
  baseUrl = '';
29
31
  lastResponse = null;
30
32
  variables = {};
33
+ envVars = {};
34
+ envLoaded = false;
31
35
  initialized = false;
32
36
  currentDoc = null;
33
37
  currentDocUrl = '';
@@ -81,8 +85,8 @@ export class RekShell {
81
85
  'get', 'post', 'put', 'delete', 'patch', 'head', 'options',
82
86
  'ws', 'udp', 'load', 'chat', 'ai',
83
87
  'whois', 'tls', 'ssl', 'dns', 'rdap', 'ping',
84
- 'scrap', '$', '$text', '$attr', '$html', '$links', '$images', '$table',
85
- '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'
86
90
  ];
87
91
  const hits = commands.filter((c) => c.startsWith(line));
88
92
  return [hits.length ? hits : commands, line];
@@ -137,7 +141,10 @@ export class RekShell {
137
141
  this.setVariable(parts.slice(1));
138
142
  return;
139
143
  case 'vars':
140
- console.log(this.variables);
144
+ this.showVars();
145
+ return;
146
+ case 'env':
147
+ await this.loadEnvFile(parts[1]);
141
148
  return;
142
149
  case 'load':
143
150
  await this.runLoadTest(parts.slice(1));
@@ -181,7 +188,31 @@ export class RekShell {
181
188
  await this.runSelectLinks(parts[1]);
182
189
  return;
183
190
  case '$images':
184
- await this.runSelectImages(parts[1]);
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] || '');
185
216
  return;
186
217
  case '$table':
187
218
  await this.runSelectTable(parts.slice(1).join(' '));
@@ -332,6 +363,72 @@ export class RekShell {
332
363
  this.variables[key] = val;
333
364
  console.log(colors.gray(`Variable $${key} set.`));
334
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
+ }
431
+ }
335
432
  resolveVariables(value) {
336
433
  if (value.startsWith('$')) {
337
434
  const key = value.slice(1);
@@ -346,7 +443,7 @@ export class RekShell {
346
443
  }
347
444
  return String(current);
348
445
  }
349
- return this.variables[key] || value;
446
+ return this.variables[key] || this.envVars[key] || process.env[key] || value;
350
447
  }
351
448
  return value;
352
449
  }
@@ -726,15 +823,43 @@ export class RekShell {
726
823
  const response = await this.client.get(url);
727
824
  const html = await response.text();
728
825
  const duration = Math.round(performance.now() - startTime);
729
- this.currentDoc = await ScrapeDocument.create(html);
826
+ this.currentDoc = await ScrapeDocument.create(html, { baseUrl: url });
730
827
  this.currentDocUrl = url;
731
828
  const elementCount = this.currentDoc.select('*').length;
732
829
  const title = this.currentDoc.selectFirst('title').text() || 'No title';
830
+ const meta = this.currentDoc.meta();
831
+ const og = this.currentDoc.openGraph();
733
832
  console.log(colors.green(`✔ Loaded`) + colors.gray(` (${duration}ms)`));
734
833
  console.log(` ${colors.cyan('Title')}: ${title}`);
735
834
  console.log(` ${colors.cyan('Elements')}: ${elementCount}`);
736
835
  console.log(` ${colors.cyan('Size')}: ${(html.length / 1024).toFixed(1)}kb`);
737
- console.log(colors.gray('\n Use $ <selector> to query, $text, $attr, $links, $images, $table'));
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'));
738
863
  }
739
864
  catch (error) {
740
865
  console.error(colors.red(`Scrape failed: ${error.message}`));
@@ -898,30 +1023,586 @@ export class RekShell {
898
1023
  return;
899
1024
  }
900
1025
  try {
901
- const imgSelector = selector || 'img[src]';
902
- const elements = this.currentDoc.select(imgSelector);
1026
+ const imageExtensions = /\.(png|jpg|jpeg|gif|webp|svg|ico|bmp|tiff|avif)(\?.*)?$/i;
903
1027
  const images = [];
904
- elements.each((el, i) => {
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) => {
905
1091
  const src = el.attr('src');
906
- const alt = el.attr('alt') || '';
907
1092
  if (src) {
908
- images.push({ alt, 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}]`) : '';
909
1173
  if (i < 20) {
910
- console.log(`${colors.gray(`${i + 1}.`)} ${colors.cyan(alt.slice(0, 30) || '(no alt)')} ${colors.gray('→')} ${src.slice(0, 60)}`);
1174
+ console.log(`${colors.gray(`${i + 1}.`)} ${colors.green('[ext]')} ${style.href?.slice(0, 70)} ${media}`);
911
1175
  }
912
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
+ }
913
1218
  });
914
- if (images.length > 20) {
915
- console.log(colors.gray(` ... and ${images.length - 20} more images`));
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
+ }
916
1253
  }
917
- this.lastResponse = images;
918
- console.log(colors.gray(`\n ${images.length} image(s) found`));
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`));
919
1257
  }
920
1258
  catch (error) {
921
1259
  console.error(colors.red(`Query failed: ${error.message}`));
922
1260
  }
923
1261
  console.log('');
924
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
+ }
925
1606
  async runSelectTable(selector) {
926
1607
  if (!this.currentDoc) {
927
1608
  console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
@@ -966,7 +1647,8 @@ export class RekShell {
966
1647
  ${colors.bold('Core Commands:')}
967
1648
  ${colors.green('url <url>')} Set persistent Base URL.
968
1649
  ${colors.green('set <key>=<val>')} Set a session variable.
969
- ${colors.green('vars')} List all session variables.
1650
+ ${colors.green('vars')} List all session and env variables.
1651
+ ${colors.green('env [path]')} Load .env file (default: ./.env).
970
1652
  ${colors.green('clear')} Clear the screen.
971
1653
  ${colors.green('exit')} Exit the console.
972
1654
 
@@ -1005,7 +1687,15 @@ export class RekShell {
1005
1687
  ${colors.green('$attr <name> <sel>')} Extract attribute values.
1006
1688
  ${colors.green('$html <selector>')} Get inner HTML.
1007
1689
  ${colors.green('$links [selector]')} List all links.
1008
- ${colors.green('$images [selector]')} List all images.
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.
1009
1699
  ${colors.green('$table <selector>')} Extract table as data.
1010
1700
 
1011
1701
  ${colors.bold('Examples:')}