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 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.0.0",
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
- import ora from 'ora';
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 spinner = ora({
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
- spinner.stop();
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
- spinner.stop();
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 spinner = ora({
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
- spinner.stop();
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
- spinner.stop();
358
+ loader.stop();
324
359
  ui.error(`Search failed: ${err.message}`);
325
360
  }
326
361
  }
327
362
 
328
363
  async handleMultiSearch(query) {
329
- const spinner = ora({
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
- spinner.stop();
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
- spinner.stop();
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 spinner = ora({
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
- spinner.stop();
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
- spinner.stop();
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 spinner = ora({
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
- spinner.stop();
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
- spinner.stop();
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 spinner = ora({ text: theme.muted('Fetching robots.txt...'), spinner: 'dots', color: 'cyan' }).start();
806
+ const loader = showLoading('Fetching robots.txt...');
784
807
  try {
785
808
  const content = await developer.getRobots(tab.url);
786
- spinner.stop();
809
+ loader.stop();
787
810
  ui.header('robots.txt');
788
811
  console.log(theme.secondary(content));
789
812
  } catch (err) {
790
- spinner.stop();
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 spinner = ora({ text: theme.muted('Fetching sitemap.xml...'), spinner: 'dots', color: 'cyan' }).start();
822
+ const loader = showLoading('Fetching sitemap.xml...');
800
823
  try {
801
824
  const data = await developer.getSitemap(tab.url);
802
- spinner.stop();
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
- spinner.stop();
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 spinner = ora({ text: theme.muted('Downloading...'), spinner: 'dots', color: 'cyan' }).start();
850
+ const loader = showLoading('Downloading...');
828
851
  try {
829
852
  const entry = await downloads.download(url);
830
- spinner.stop();
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
- spinner.stop();
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
- ui.muted?.call(ui, ` Session: ${s.searchCount} searches, ${s.pagesVisited} pages, ${s.sessionTime}`);
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
- ui.info('Type "help" for commands, or start searching!');
13
- ui.info('Type "search <query>" or just type anything to search.');
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.prompt();
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
- rl.on('line', async (line) => {
30
- const input = line.trim();
31
- if (input) {
32
- await browser.handleCommand(input);
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
- rl.prompt();
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 terminal '),
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: 1, right: 1 },
25
- borderStyle: 'round',
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('cli-browser') + theme.muted(' > ');
176
+ return theme.accent(`${figures.pointer} `) + theme.primary('cb') + theme.muted(' \u276f ');
176
177
  }
177
178
 
178
179
  export function helpBox(title, commands) {