cli-browser 1.0.0 → 1.1.0
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/README.md +15 -3
- package/package.json +1 -2
- package/src/browser.js +321 -47
- package/src/index.js +33 -11
- package/src/ui.js +9 -8
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
```
|
|
4
4
|
██████╗██╗ ██╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗███████╗██████╗
|
|
5
5
|
██╔════╝██║ ██║ ██╔══██╗██╔══██╗██╔═══██╗██║ ██║██╔════╝██╔════╝██╔══██╗
|
|
6
|
-
██║ ██║ ██║ ██████╔╝██████╔╝██║ ██║██║ █╗
|
|
7
|
-
██║ ██║ ██║ ██╔══██╗██╔══██╗██║
|
|
8
|
-
╚██████╗███████╗██║ ██████╔╝██║
|
|
6
|
+
██║ ██║ ██║ ██████╔╝██████╔╝██║ ██║██║ █╗ ██║███████╗█████╗ ██████╔╝
|
|
7
|
+
██║ ██║ ██║ ██╔══██╗██╔══██╗██║ ██║██║███╗██║╚════██║██╔══╝ ██╔══██╗
|
|
8
|
+
╚██████╗███████╗██║ ██████╔╝██║ ██║╚██████╔╝╚███╔███╔╝███████║███████╗██║ ██║
|
|
9
9
|
╚═════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚══════╝╚═╝ ╚═╝
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -89,6 +89,11 @@ open https://example.com # Open URL directly
|
|
|
89
89
|
link 3 # Follow link #3 on page
|
|
90
90
|
reader # Reader mode (clean view)
|
|
91
91
|
info # Page metadata
|
|
92
|
+
source # View page HTML source
|
|
93
|
+
links # List all links on current page
|
|
94
|
+
find <text> # Find text on current page
|
|
95
|
+
top # Scroll to top of page
|
|
96
|
+
page 2 # Go to search results page 2
|
|
92
97
|
```
|
|
93
98
|
|
|
94
99
|
### Navigation
|
|
@@ -149,6 +154,13 @@ cookies # View page cookies
|
|
|
149
154
|
scan example.com # Full security scan
|
|
150
155
|
robots # View robots.txt
|
|
151
156
|
sitemap # View sitemap.xml
|
|
157
|
+
whois example.com # DNS lookup
|
|
158
|
+
resolve bit.ly/short # Follow redirects to final URL
|
|
159
|
+
extract links # Extract all links
|
|
160
|
+
extract images # Extract all images
|
|
161
|
+
extract emails # Extract email addresses
|
|
162
|
+
extract headings # Extract headings
|
|
163
|
+
extract scripts # Extract scripts
|
|
152
164
|
```
|
|
153
165
|
|
|
154
166
|
### Downloads
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-browser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A powerful Terminal Internet browser — search, browse, navigate, and explore the web entirely from your CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"cheerio": "^1.0.0",
|
|
33
33
|
"cli-table3": "^0.6.5",
|
|
34
34
|
"conf": "^13.0.0",
|
|
35
|
-
"ora": "^8.0.0",
|
|
36
35
|
"boxen": "^8.0.0",
|
|
37
36
|
"figures": "^6.0.0",
|
|
38
37
|
"wrap-ansi": "^9.0.0",
|
package/src/browser.js
CHANGED
|
@@ -14,21 +14,34 @@ import plugins from './plugins.js';
|
|
|
14
14
|
import sessions from './sessions.js';
|
|
15
15
|
import { theme, THEMES } from './themes.js';
|
|
16
16
|
import * as ui from './ui.js';
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
// Safe loading indicator that doesn't break readline
|
|
19
|
+
function showLoading(msg) {
|
|
20
|
+
process.stdout.write(theme.muted(` ⏳ ${msg}`));
|
|
21
|
+
return {
|
|
22
|
+
stop: () => {
|
|
23
|
+
process.stdout.clearLine?.(0);
|
|
24
|
+
process.stdout.cursorTo?.(0);
|
|
25
|
+
},
|
|
26
|
+
fail: (errMsg) => {
|
|
27
|
+
process.stdout.clearLine?.(0);
|
|
28
|
+
process.stdout.cursorTo?.(0);
|
|
29
|
+
if (errMsg) ui.error(errMsg);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
18
33
|
|
|
19
34
|
class Browser {
|
|
20
35
|
constructor() {
|
|
21
36
|
this.lastSearchResults = [];
|
|
22
37
|
this.currentPage = null;
|
|
23
38
|
this.running = true;
|
|
39
|
+
this.rl = null;
|
|
40
|
+
this.commandHistory = [];
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
async fetchPage(url) {
|
|
27
|
-
const
|
|
28
|
-
text: theme.muted('Loading...'),
|
|
29
|
-
spinner: 'dots',
|
|
30
|
-
color: 'cyan',
|
|
31
|
-
}).start();
|
|
44
|
+
const loader = showLoading(`Loading ${url.slice(0, 60)}...`);
|
|
32
45
|
|
|
33
46
|
try {
|
|
34
47
|
const res = await axios.get(url, {
|
|
@@ -38,7 +51,7 @@ class Browser {
|
|
|
38
51
|
validateStatus: (status) => status < 500,
|
|
39
52
|
});
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
loader.stop();
|
|
42
55
|
stats.incrementPageVisit();
|
|
43
56
|
history.add({ type: 'page', title: '', url });
|
|
44
57
|
|
|
@@ -49,7 +62,7 @@ class Browser {
|
|
|
49
62
|
url: res.request?.res?.responseUrl || url,
|
|
50
63
|
};
|
|
51
64
|
} catch (err) {
|
|
52
|
-
|
|
65
|
+
loader.stop();
|
|
53
66
|
throw err;
|
|
54
67
|
}
|
|
55
68
|
}
|
|
@@ -218,6 +231,32 @@ class Browser {
|
|
|
218
231
|
case 'ver':
|
|
219
232
|
return ui.info('CLI Browser v1.0.0');
|
|
220
233
|
|
|
234
|
+
// === NEW FEATURES ===
|
|
235
|
+
case 'source':
|
|
236
|
+
case 'src':
|
|
237
|
+
return this.handleSource();
|
|
238
|
+
|
|
239
|
+
case 'links':
|
|
240
|
+
return this.handleLinks();
|
|
241
|
+
|
|
242
|
+
case 'find':
|
|
243
|
+
return this.handleFind(rawArgs);
|
|
244
|
+
|
|
245
|
+
case 'top':
|
|
246
|
+
return this.handleTop();
|
|
247
|
+
|
|
248
|
+
case 'whois':
|
|
249
|
+
return await this.handleWhois(rawArgs);
|
|
250
|
+
|
|
251
|
+
case 'page':
|
|
252
|
+
return await this.handlePage(args);
|
|
253
|
+
|
|
254
|
+
case 'extract':
|
|
255
|
+
return this.handleExtract(args[0]);
|
|
256
|
+
|
|
257
|
+
case 'resolve':
|
|
258
|
+
return await this.handleResolve(rawArgs);
|
|
259
|
+
|
|
221
260
|
default:
|
|
222
261
|
// If it looks like a URL
|
|
223
262
|
if (trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.includes('.') && !trimmed.includes(' ')) {
|
|
@@ -271,15 +310,11 @@ class Browser {
|
|
|
271
310
|
return await this.handleMultiSearch(query);
|
|
272
311
|
}
|
|
273
312
|
|
|
274
|
-
const
|
|
275
|
-
text: theme.muted(`Searching "${query}"...`),
|
|
276
|
-
spinner: 'dots',
|
|
277
|
-
color: 'cyan',
|
|
278
|
-
}).start();
|
|
313
|
+
const loader = showLoading(`Searching "${query}"...`);
|
|
279
314
|
|
|
280
315
|
try {
|
|
281
316
|
const data = await search.search(query, { engine, type, time });
|
|
282
|
-
|
|
317
|
+
loader.stop();
|
|
283
318
|
|
|
284
319
|
stats.incrementSearch();
|
|
285
320
|
history.add({ type: 'search', query, title: `Search: ${query}` });
|
|
@@ -320,21 +355,17 @@ class Browser {
|
|
|
320
355
|
ui.info('Type "open <number>" to visit a result');
|
|
321
356
|
|
|
322
357
|
} catch (err) {
|
|
323
|
-
|
|
358
|
+
loader.stop();
|
|
324
359
|
ui.error(`Search failed: ${err.message}`);
|
|
325
360
|
}
|
|
326
361
|
}
|
|
327
362
|
|
|
328
363
|
async handleMultiSearch(query) {
|
|
329
|
-
const
|
|
330
|
-
text: theme.muted(`Multi-searching "${query}"...`),
|
|
331
|
-
spinner: 'dots',
|
|
332
|
-
color: 'cyan',
|
|
333
|
-
}).start();
|
|
364
|
+
const loader = showLoading(`Multi-searching "${query}"...`);
|
|
334
365
|
|
|
335
366
|
try {
|
|
336
367
|
const results = await search.multiSearch(query);
|
|
337
|
-
|
|
368
|
+
loader.stop();
|
|
338
369
|
|
|
339
370
|
stats.incrementSearch();
|
|
340
371
|
ui.header(`Multi-Search: "${query}"`);
|
|
@@ -348,7 +379,7 @@ class Browser {
|
|
|
348
379
|
}
|
|
349
380
|
}
|
|
350
381
|
} catch (err) {
|
|
351
|
-
|
|
382
|
+
loader.stop();
|
|
352
383
|
ui.error(`Multi-search failed: ${err.message}`);
|
|
353
384
|
}
|
|
354
385
|
}
|
|
@@ -356,15 +387,11 @@ class Browser {
|
|
|
356
387
|
async handleImageSearch(query) {
|
|
357
388
|
if (!query) return ui.error('Usage: image <query>');
|
|
358
389
|
|
|
359
|
-
const
|
|
360
|
-
text: theme.muted(`Searching images "${query}"...`),
|
|
361
|
-
spinner: 'dots',
|
|
362
|
-
color: 'cyan',
|
|
363
|
-
}).start();
|
|
390
|
+
const loader = showLoading(`Searching images "${query}"...`);
|
|
364
391
|
|
|
365
392
|
try {
|
|
366
393
|
const data = await search.imageSearch(query);
|
|
367
|
-
|
|
394
|
+
loader.stop();
|
|
368
395
|
|
|
369
396
|
stats.incrementSearch();
|
|
370
397
|
ui.header(`Image Results: "${query}"`);
|
|
@@ -380,7 +407,7 @@ class Browser {
|
|
|
380
407
|
this.lastSearchResults = data.results;
|
|
381
408
|
ui.info('Type "open <number>" to view source page');
|
|
382
409
|
} catch (err) {
|
|
383
|
-
|
|
410
|
+
loader.stop();
|
|
384
411
|
ui.error(`Image search failed: ${err.message}`);
|
|
385
412
|
}
|
|
386
413
|
}
|
|
@@ -731,15 +758,11 @@ class Browser {
|
|
|
731
758
|
|
|
732
759
|
if (!target.startsWith('http')) target = 'https://' + target;
|
|
733
760
|
|
|
734
|
-
const
|
|
735
|
-
text: theme.muted(`Scanning ${target}...`),
|
|
736
|
-
spinner: 'dots',
|
|
737
|
-
color: 'cyan',
|
|
738
|
-
}).start();
|
|
761
|
+
const loader = showLoading(`Scanning ${target}...`);
|
|
739
762
|
|
|
740
763
|
try {
|
|
741
764
|
const data = await developer.scan(target);
|
|
742
|
-
|
|
765
|
+
loader.stop();
|
|
743
766
|
|
|
744
767
|
ui.header(`Scan: ${target}`);
|
|
745
768
|
|
|
@@ -771,7 +794,7 @@ class Browser {
|
|
|
771
794
|
});
|
|
772
795
|
}
|
|
773
796
|
} catch (err) {
|
|
774
|
-
|
|
797
|
+
loader.stop();
|
|
775
798
|
ui.error(err.message);
|
|
776
799
|
}
|
|
777
800
|
}
|
|
@@ -780,14 +803,14 @@ class Browser {
|
|
|
780
803
|
const tab = tabs.getCurrentTab();
|
|
781
804
|
if (!tab.url) return ui.error('Open a page first');
|
|
782
805
|
|
|
783
|
-
const
|
|
806
|
+
const loader = showLoading('Fetching robots.txt...');
|
|
784
807
|
try {
|
|
785
808
|
const content = await developer.getRobots(tab.url);
|
|
786
|
-
|
|
809
|
+
loader.stop();
|
|
787
810
|
ui.header('robots.txt');
|
|
788
811
|
console.log(theme.secondary(content));
|
|
789
812
|
} catch (err) {
|
|
790
|
-
|
|
813
|
+
loader.stop();
|
|
791
814
|
ui.error(err.message);
|
|
792
815
|
}
|
|
793
816
|
}
|
|
@@ -796,10 +819,10 @@ class Browser {
|
|
|
796
819
|
const tab = tabs.getCurrentTab();
|
|
797
820
|
if (!tab.url) return ui.error('Open a page first');
|
|
798
821
|
|
|
799
|
-
const
|
|
822
|
+
const loader = showLoading('Fetching sitemap.xml...');
|
|
800
823
|
try {
|
|
801
824
|
const data = await developer.getSitemap(tab.url);
|
|
802
|
-
|
|
825
|
+
loader.stop();
|
|
803
826
|
ui.header('Sitemap');
|
|
804
827
|
if (Array.isArray(data)) {
|
|
805
828
|
data.slice(0, 30).forEach((url, i) => {
|
|
@@ -812,7 +835,7 @@ class Browser {
|
|
|
812
835
|
console.log(theme.secondary(data));
|
|
813
836
|
}
|
|
814
837
|
} catch (err) {
|
|
815
|
-
|
|
838
|
+
loader.stop();
|
|
816
839
|
ui.error(err.message);
|
|
817
840
|
}
|
|
818
841
|
}
|
|
@@ -824,14 +847,14 @@ class Browser {
|
|
|
824
847
|
if (!url) return ui.error('Usage: download <url>');
|
|
825
848
|
if (!url.startsWith('http')) url = 'https://' + url;
|
|
826
849
|
|
|
827
|
-
const
|
|
850
|
+
const loader = showLoading('Downloading...');
|
|
828
851
|
try {
|
|
829
852
|
const entry = await downloads.download(url);
|
|
830
|
-
|
|
853
|
+
loader.stop();
|
|
831
854
|
ui.success(`Downloaded: ${entry.filename} (${downloads.formatSize(entry.downloaded)})`);
|
|
832
855
|
ui.info(`Saved to: ${entry.filepath}`);
|
|
833
856
|
} catch (err) {
|
|
834
|
-
|
|
857
|
+
loader.stop();
|
|
835
858
|
ui.error(`Download failed: ${err.message}`);
|
|
836
859
|
}
|
|
837
860
|
}
|
|
@@ -1123,6 +1146,11 @@ class Browser {
|
|
|
1123
1146
|
['link <number>', 'Follow a link on current page'],
|
|
1124
1147
|
['reader', 'Reader mode (clean view)'],
|
|
1125
1148
|
['info', 'Page information'],
|
|
1149
|
+
['source', 'View page HTML source'],
|
|
1150
|
+
['links', 'List all links on page'],
|
|
1151
|
+
['find <text>', 'Find text on current page'],
|
|
1152
|
+
['top', 'Scroll to top of page'],
|
|
1153
|
+
['page <n>', 'Go to search results page N'],
|
|
1126
1154
|
]);
|
|
1127
1155
|
|
|
1128
1156
|
ui.helpBox('Navigation', [
|
|
@@ -1165,6 +1193,9 @@ class Browser {
|
|
|
1165
1193
|
['scan <url>', 'Security scan'],
|
|
1166
1194
|
['robots', 'View robots.txt'],
|
|
1167
1195
|
['sitemap', 'View sitemap'],
|
|
1196
|
+
['whois <domain>', 'DNS lookup'],
|
|
1197
|
+
['resolve <url>', 'Follow redirects to final URL'],
|
|
1198
|
+
['extract <type>', 'Extract links/images/emails/headings/scripts'],
|
|
1168
1199
|
]);
|
|
1169
1200
|
|
|
1170
1201
|
ui.helpBox('Tools', [
|
|
@@ -1249,9 +1280,8 @@ class Browser {
|
|
|
1249
1280
|
handleExit() {
|
|
1250
1281
|
stats.saveTotalTime();
|
|
1251
1282
|
console.log();
|
|
1252
|
-
ui.info('Saving session...');
|
|
1253
1283
|
const s = stats.getStats();
|
|
1254
|
-
|
|
1284
|
+
console.log(theme.muted(` Session: ${s.searchCount} searches, ${s.pagesVisited} pages, ${s.sessionTime}`));
|
|
1255
1285
|
console.log();
|
|
1256
1286
|
ui.success('Goodbye! Thanks for using CLI Browser.');
|
|
1257
1287
|
console.log();
|
|
@@ -1259,6 +1289,249 @@ class Browser {
|
|
|
1259
1289
|
process.exit(0);
|
|
1260
1290
|
}
|
|
1261
1291
|
|
|
1292
|
+
// ==================
|
|
1293
|
+
// NEW: SOURCE VIEW
|
|
1294
|
+
// ==================
|
|
1295
|
+
handleSource() {
|
|
1296
|
+
const tab = tabs.getCurrentTab();
|
|
1297
|
+
if (!tab.html) return ui.warn('No page loaded');
|
|
1298
|
+
|
|
1299
|
+
ui.header('Page Source');
|
|
1300
|
+
const html = typeof tab.html === 'string' ? tab.html : String(tab.html);
|
|
1301
|
+
// Show first 3000 chars of source
|
|
1302
|
+
const source = html.slice(0, 3000);
|
|
1303
|
+
source.split('\n').forEach(line => {
|
|
1304
|
+
console.log(theme.muted(' │ ') + theme.secondary(line));
|
|
1305
|
+
});
|
|
1306
|
+
if (html.length > 3000) {
|
|
1307
|
+
console.log(theme.muted(` ... (${html.length - 3000} more characters)`));
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// ==================
|
|
1312
|
+
// NEW: LIST ALL LINKS
|
|
1313
|
+
// ==================
|
|
1314
|
+
handleLinks() {
|
|
1315
|
+
const tab = tabs.getCurrentTab();
|
|
1316
|
+
if (!tab.links || tab.links.length === 0) {
|
|
1317
|
+
return ui.warn('No links on current page');
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
ui.header(`Links (${tab.links.length} total)`);
|
|
1321
|
+
tab.links.forEach((link, i) => {
|
|
1322
|
+
console.log(
|
|
1323
|
+
theme.accent(` [${i + 1}] `) +
|
|
1324
|
+
theme.link(link.text || 'Untitled') +
|
|
1325
|
+
'\n ' + theme.url(link.url)
|
|
1326
|
+
);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// ==================
|
|
1331
|
+
// NEW: FIND TEXT ON PAGE
|
|
1332
|
+
// ==================
|
|
1333
|
+
handleFind(query) {
|
|
1334
|
+
if (!query) return ui.error('Usage: find <text>');
|
|
1335
|
+
|
|
1336
|
+
const tab = tabs.getCurrentTab();
|
|
1337
|
+
if (!tab.content) return ui.warn('No page loaded');
|
|
1338
|
+
|
|
1339
|
+
const text = tab.content.replace(/\x1B\[[0-9;]*m/g, ''); // strip ANSI
|
|
1340
|
+
const lines = text.split('\n');
|
|
1341
|
+
const matches = [];
|
|
1342
|
+
|
|
1343
|
+
lines.forEach((line, i) => {
|
|
1344
|
+
if (line.toLowerCase().includes(query.toLowerCase())) {
|
|
1345
|
+
matches.push({ line: i + 1, text: line.trim() });
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
ui.header(`Find: "${query}"`);
|
|
1350
|
+
if (matches.length === 0) {
|
|
1351
|
+
return ui.info('No matches found');
|
|
1352
|
+
}
|
|
1353
|
+
ui.info(`${matches.length} match${matches.length > 1 ? 'es' : ''} found`);
|
|
1354
|
+
console.log();
|
|
1355
|
+
|
|
1356
|
+
matches.slice(0, 20).forEach(m => {
|
|
1357
|
+
const highlighted = m.text.replace(
|
|
1358
|
+
new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'),
|
|
1359
|
+
(match) => theme.highlight(match)
|
|
1360
|
+
);
|
|
1361
|
+
console.log(theme.muted(` L${m.line}: `) + highlighted.slice(0, 120));
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
if (matches.length > 20) {
|
|
1365
|
+
console.log(theme.muted(` ... and ${matches.length - 20} more matches`));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// ==================
|
|
1370
|
+
// NEW: SCROLL TO TOP
|
|
1371
|
+
// ==================
|
|
1372
|
+
handleTop() {
|
|
1373
|
+
const tab = tabs.getCurrentTab();
|
|
1374
|
+
if (!tab.content) return ui.warn('No page loaded');
|
|
1375
|
+
|
|
1376
|
+
ui.statusBar(tabs.getAllTabs(), tabs.getActiveIndex(), tab.url);
|
|
1377
|
+
// Show first portion of page
|
|
1378
|
+
const lines = tab.content.split('\n').slice(0, 40);
|
|
1379
|
+
console.log(lines.join('\n'));
|
|
1380
|
+
console.log(theme.muted('\n ... use "find <text>" to search within the page'));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// ==================
|
|
1384
|
+
// NEW: WHOIS LOOKUP
|
|
1385
|
+
// ==================
|
|
1386
|
+
async handleWhois(domain) {
|
|
1387
|
+
if (!domain) {
|
|
1388
|
+
const tab = tabs.getCurrentTab();
|
|
1389
|
+
if (!tab.url) return ui.error('Usage: whois <domain>');
|
|
1390
|
+
try {
|
|
1391
|
+
domain = new URL(tab.url).hostname;
|
|
1392
|
+
} catch {
|
|
1393
|
+
return ui.error('Invalid URL');
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const loader = showLoading(`Looking up ${domain}...`);
|
|
1398
|
+
try {
|
|
1399
|
+
// Use a public API for whois
|
|
1400
|
+
const res = await axios.get(`https://dns.google/resolve?name=${domain}&type=A`, {
|
|
1401
|
+
timeout: 10000,
|
|
1402
|
+
});
|
|
1403
|
+
loader.stop();
|
|
1404
|
+
|
|
1405
|
+
ui.header(`DNS Lookup: ${domain}`);
|
|
1406
|
+
if (res.data.Answer) {
|
|
1407
|
+
res.data.Answer.forEach(a => {
|
|
1408
|
+
ui.keyValue(a.name || 'Record', `${a.data} (TTL: ${a.TTL}s, Type: ${a.type})`);
|
|
1409
|
+
});
|
|
1410
|
+
} else {
|
|
1411
|
+
ui.info('No DNS records found');
|
|
1412
|
+
}
|
|
1413
|
+
if (res.data.Status !== undefined) {
|
|
1414
|
+
ui.keyValue('Status', res.data.Status === 0 ? 'OK (NOERROR)' : `Code ${res.data.Status}`);
|
|
1415
|
+
}
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
loader.stop();
|
|
1418
|
+
ui.error(`Lookup failed: ${err.message}`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// ==================
|
|
1423
|
+
// NEW: SEARCH PAGINATION
|
|
1424
|
+
// ==================
|
|
1425
|
+
async handlePage(args) {
|
|
1426
|
+
const pageNum = parseInt(args[0]);
|
|
1427
|
+
if (!pageNum || pageNum < 1) return ui.error('Usage: page <number>');
|
|
1428
|
+
|
|
1429
|
+
// Re-run last search with different page
|
|
1430
|
+
const lastEntry = history.getSession().filter(e => e.type === 'search').pop();
|
|
1431
|
+
if (!lastEntry) return ui.warn('No previous search to paginate');
|
|
1432
|
+
|
|
1433
|
+
const loader = showLoading(`Loading page ${pageNum}...`);
|
|
1434
|
+
try {
|
|
1435
|
+
const data = await search.search(lastEntry.query, { page: pageNum });
|
|
1436
|
+
loader.stop();
|
|
1437
|
+
|
|
1438
|
+
if (data.results.length === 0) {
|
|
1439
|
+
return ui.warn('No more results');
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
ui.header(`Search Results: "${lastEntry.query}" (Page ${pageNum})`);
|
|
1443
|
+
console.log();
|
|
1444
|
+
data.results.slice(0, config.get('pageSize')).forEach((r, i) => {
|
|
1445
|
+
ui.searchResult(i + 1, r);
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
this.lastSearchResults = data.results;
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
loader.stop();
|
|
1451
|
+
ui.error(`Pagination failed: ${err.message}`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// ==================
|
|
1456
|
+
// NEW: EXTRACT ELEMENTS
|
|
1457
|
+
// ==================
|
|
1458
|
+
handleExtract(type) {
|
|
1459
|
+
const tab = tabs.getCurrentTab();
|
|
1460
|
+
if (!tab.html) return ui.warn('No page loaded');
|
|
1461
|
+
|
|
1462
|
+
const validTypes = ['links', 'images', 'headings', 'emails', 'scripts'];
|
|
1463
|
+
if (!type || !validTypes.includes(type)) {
|
|
1464
|
+
return ui.error(`Usage: extract <${validTypes.join('|')}>`);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const cheerio = renderer.constructor; // We'll use the renderer's existing parsing
|
|
1468
|
+
ui.header(`Extract: ${type}`);
|
|
1469
|
+
|
|
1470
|
+
switch (type) {
|
|
1471
|
+
case 'links':
|
|
1472
|
+
this.handleLinks();
|
|
1473
|
+
break;
|
|
1474
|
+
case 'images': {
|
|
1475
|
+
const images = renderer.getImages();
|
|
1476
|
+
if (!images.length) return ui.info('No images found');
|
|
1477
|
+
images.forEach((img, i) => {
|
|
1478
|
+
console.log(theme.accent(` [${i + 1}] `) + theme.muted(img.alt || 'Image'));
|
|
1479
|
+
console.log(' ' + theme.url(img.src));
|
|
1480
|
+
});
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
case 'headings': {
|
|
1484
|
+
// Re-parse for headings
|
|
1485
|
+
const text = tab.content.replace(/\x1B\[[0-9;]*m/g, '');
|
|
1486
|
+
const headingLines = text.split('\n').filter(l => l.includes('══') || l.includes('▸'));
|
|
1487
|
+
headingLines.forEach(h => console.log(theme.secondary(' ' + h.trim())));
|
|
1488
|
+
if (!headingLines.length) ui.info('No headings found');
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
case 'emails': {
|
|
1492
|
+
const html = typeof tab.html === 'string' ? tab.html : '';
|
|
1493
|
+
const emails = [...new Set(html.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [])];
|
|
1494
|
+
if (!emails.length) return ui.info('No emails found');
|
|
1495
|
+
emails.forEach((e, i) => console.log(theme.accent(` [${i + 1}] `) + theme.secondary(e)));
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
case 'scripts': {
|
|
1499
|
+
const data = developer.inspect(tab.html, tab.url);
|
|
1500
|
+
if (!data.scripts.length) return ui.info('No scripts found');
|
|
1501
|
+
data.scripts.forEach((s, i) => console.log(theme.accent(` [${i + 1}] `) + theme.url(s)));
|
|
1502
|
+
break;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// ==================
|
|
1508
|
+
// NEW: RESOLVE URL
|
|
1509
|
+
// ==================
|
|
1510
|
+
async handleResolve(url) {
|
|
1511
|
+
if (!url) return ui.error('Usage: resolve <url>');
|
|
1512
|
+
if (!url.startsWith('http')) url = 'https://' + url;
|
|
1513
|
+
|
|
1514
|
+
const loader = showLoading(`Resolving ${url}...`);
|
|
1515
|
+
try {
|
|
1516
|
+
const res = await axios.head(url, {
|
|
1517
|
+
maxRedirects: 10,
|
|
1518
|
+
timeout: 10000,
|
|
1519
|
+
validateStatus: () => true,
|
|
1520
|
+
});
|
|
1521
|
+
loader.stop();
|
|
1522
|
+
const finalUrl = res.request?.res?.responseUrl || url;
|
|
1523
|
+
ui.header('URL Resolution');
|
|
1524
|
+
ui.keyValue('Original', url);
|
|
1525
|
+
ui.keyValue('Final URL', finalUrl);
|
|
1526
|
+
ui.keyValue('Status', `${res.status} ${res.statusText}`);
|
|
1527
|
+
ui.keyValue('Server', res.headers?.server || 'Unknown');
|
|
1528
|
+
ui.keyValue('Content-Type', res.headers?.['content-type'] || 'Unknown');
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
loader.stop();
|
|
1531
|
+
ui.error(`Resolve failed: ${err.message}`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1262
1535
|
getAutocompletions(partial) {
|
|
1263
1536
|
const commands = [
|
|
1264
1537
|
'search', 'open', 'link', 'back', 'forward', 'home', 'refresh',
|
|
@@ -1267,6 +1540,7 @@ class Browser {
|
|
|
1267
1540
|
'download', 'downloads', 'summary', 'ask', 'translate',
|
|
1268
1541
|
'theme', 'proxy', 'ua', 'plugin', 'session', 'stats',
|
|
1269
1542
|
'help', 'clear', 'exit', 'image', 'login', 'color', 'style',
|
|
1543
|
+
'source', 'links', 'find', 'top', 'whois', 'page', 'extract', 'resolve',
|
|
1270
1544
|
];
|
|
1271
1545
|
|
|
1272
1546
|
if (!partial) return commands;
|
package/src/index.js
CHANGED
|
@@ -9,14 +9,16 @@ export async function startBrowser() {
|
|
|
9
9
|
// Show banner
|
|
10
10
|
console.clear();
|
|
11
11
|
ui.banner();
|
|
12
|
-
|
|
13
|
-
ui.info('
|
|
12
|
+
console.log();
|
|
13
|
+
ui.info('Welcome to CLI Browser — your Terminal Internet!');
|
|
14
|
+
ui.info('Type anything to search, or "help" for all commands.');
|
|
14
15
|
console.log();
|
|
15
16
|
|
|
17
|
+
// Use a manual async input loop (question-based)
|
|
18
|
+
// This prevents ora spinner from permanently breaking the prompt
|
|
16
19
|
const rl = createInterface({
|
|
17
20
|
input: process.stdin,
|
|
18
21
|
output: process.stdout,
|
|
19
|
-
prompt: ui.prompt(),
|
|
20
22
|
completer: (line) => {
|
|
21
23
|
const completions = browser.getAutocompletions(line);
|
|
22
24
|
return [completions, line];
|
|
@@ -24,22 +26,42 @@ export async function startBrowser() {
|
|
|
24
26
|
terminal: true,
|
|
25
27
|
});
|
|
26
28
|
|
|
27
|
-
rl
|
|
29
|
+
browser.rl = rl;
|
|
30
|
+
|
|
31
|
+
const askQuestion = () => {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
rl.question(ui.prompt(), (answer) => {
|
|
34
|
+
resolve(answer);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
};
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
// Robust async REPL loop
|
|
40
|
+
const loop = async () => {
|
|
41
|
+
while (browser.running) {
|
|
42
|
+
try {
|
|
43
|
+
const input = await askQuestion();
|
|
44
|
+
const trimmed = (input || '').trim();
|
|
45
|
+
if (trimmed) {
|
|
46
|
+
await browser.handleCommand(trimmed);
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err?.code === 'ERR_USE_AFTER_CLOSE') {
|
|
50
|
+
browser.handleExit();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
33
54
|
}
|
|
34
|
-
|
|
35
|
-
});
|
|
55
|
+
};
|
|
36
56
|
|
|
37
57
|
rl.on('close', () => {
|
|
38
58
|
browser.handleExit();
|
|
39
59
|
});
|
|
40
60
|
|
|
41
|
-
// Handle SIGINT gracefully
|
|
42
61
|
process.on('SIGINT', () => {
|
|
62
|
+
console.log();
|
|
43
63
|
browser.handleExit();
|
|
44
64
|
});
|
|
65
|
+
|
|
66
|
+
await loop();
|
|
45
67
|
}
|
package/src/ui.js
CHANGED
|
@@ -9,20 +9,21 @@ export function banner() {
|
|
|
9
9
|
const logo = `
|
|
10
10
|
██████╗██╗ ██╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗███████╗██████╗
|
|
11
11
|
██╔════╝██║ ██║ ██╔══██╗██╔══██╗██╔═══██╗██║ ██║██╔════╝██╔════╝██╔══██╗
|
|
12
|
-
██║ ██║ ██║ ██████╔╝██████╔╝██║ ██║██║ █╗
|
|
13
|
-
██║ ██║ ██║ ██╔══██╗██╔══██╗██║
|
|
14
|
-
╚██████╗███████╗██║ ██████╔╝██║
|
|
12
|
+
██║ ██║ ██║ ██████╔╝██████╔╝██║ ██║██║ █╗ ██║███████╗█████╗ ██████╔╝
|
|
13
|
+
██║ ██║ ██║ ██╔══██╗██╔══██╗██║ ██║██║███╗██║╚════██║██╔══╝ ██╔══██╗
|
|
14
|
+
╚██████╗███████╗██║ ██████╔╝██║ ██║╚██████╔╝╚███╔███╔╝███████║███████╗██║ ██║
|
|
15
15
|
╚═════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚══════╝╚═╝ ╚═╝`;
|
|
16
16
|
|
|
17
17
|
console.log(theme.primary(logo));
|
|
18
18
|
console.log();
|
|
19
19
|
console.log(
|
|
20
20
|
boxen(
|
|
21
|
-
theme.accent(' Terminal Internet ') + '\n' +
|
|
22
|
-
theme.muted(' Browse the web from your
|
|
21
|
+
theme.accent(' ⚡ Terminal Internet ⚡ ') + '\n' +
|
|
22
|
+
theme.muted(' Browse the web entirely from your CLI ') + '\n' +
|
|
23
|
+
theme.info(' v1.0.0 '),
|
|
23
24
|
{
|
|
24
|
-
padding: { top: 0, bottom: 0, left:
|
|
25
|
-
borderStyle: '
|
|
25
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
26
|
+
borderStyle: 'double',
|
|
26
27
|
borderColor: t.primary,
|
|
27
28
|
textAlignment: 'center',
|
|
28
29
|
}
|
|
@@ -172,7 +173,7 @@ export function statusBar(tabs, currentTab, url) {
|
|
|
172
173
|
|
|
173
174
|
export function prompt() {
|
|
174
175
|
const t = theme.getTheme();
|
|
175
|
-
return theme.accent(`${figures.pointer} `) + theme.primary('
|
|
176
|
+
return theme.accent(`${figures.pointer} `) + theme.primary('cb') + theme.muted(' \u276f ');
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
export function helpBox(title, commands) {
|