budexp 0.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.
@@ -0,0 +1,1097 @@
1
+ const { execFileSync } = require('child_process');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const logger = require('./logger');
5
+
6
+ /**
7
+ * Run expo-doctor and generate HTML report
8
+ */
9
+ async function runExpoDoctor(options = {}) {
10
+ const ora = require('ora');
11
+
12
+ const spinner = ora({
13
+ text: 'Running budexp check...',
14
+ color: 'cyan',
15
+ spinner: 'dots',
16
+ }).start();
17
+
18
+ try {
19
+ // Run expo-doctor and capture output
20
+ const output = execFileSync('npx', ['expo-doctor'], {
21
+ encoding: 'utf8',
22
+ stdio: 'pipe',
23
+ });
24
+
25
+ spinner.text = 'Generating HTML report...';
26
+
27
+ // Generate HTML report
28
+ const htmlReport = generateHTMLReport(output);
29
+
30
+ // Save report to file
31
+ const reportPath = path.join(process.cwd(), 'expo-doctor-report.html');
32
+ fs.writeFileSync(reportPath, htmlReport);
33
+
34
+ spinner.succeed(`HTML report generated: ${reportPath}`);
35
+
36
+ // Prompt user to open report
37
+ await promptToOpenReport(reportPath, options);
38
+
39
+ return {
40
+ output,
41
+ reportPath,
42
+ hasIssues:
43
+ output.includes('✖') || output.includes('⚠') || output.toLowerCase().includes('error'),
44
+ };
45
+ } catch (e) {
46
+ spinner.text = 'Issues detected, generating report...';
47
+
48
+ const errorOutput = e.stdout || e.message;
49
+ const htmlReport = generateHTMLReport(errorOutput);
50
+ const reportPath = path.join(process.cwd(), 'expo-doctor-report.html');
51
+ fs.writeFileSync(reportPath, htmlReport);
52
+
53
+ spinner.warn('expo-doctor found issues. Check the report for details.');
54
+
55
+ // Prompt user to open report
56
+ await promptToOpenReport(reportPath, options);
57
+
58
+ return {
59
+ output: errorOutput,
60
+ reportPath,
61
+ hasIssues: true,
62
+ };
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Prompt user to open the report
68
+ */
69
+ async function promptToOpenReport(reportPath, options = {}) {
70
+ const readline = require('readline');
71
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
72
+
73
+ if (options.openReport === false) {
74
+ console.log('');
75
+ logger.info('Report saved. Open it later at:');
76
+ logger.info(reportPath);
77
+ return;
78
+ }
79
+
80
+ if (!isInteractive) {
81
+ console.log('');
82
+ logger.info('Non-interactive shell detected. Skipping report open prompt.');
83
+ logger.info('Report saved at:');
84
+ logger.info(reportPath);
85
+ return;
86
+ }
87
+
88
+ const rl = readline.createInterface({
89
+ input: process.stdin,
90
+ output: process.stdout,
91
+ });
92
+
93
+ return new Promise((resolve) => {
94
+ console.log('');
95
+ rl.question('Would you like to open the report? (y/n): ', (answer) => {
96
+ rl.close();
97
+
98
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
99
+ openReport(reportPath);
100
+ } else {
101
+ console.log('');
102
+ logger.info('Report saved. You can open it later at:');
103
+ logger.info(reportPath);
104
+ }
105
+
106
+ resolve();
107
+ });
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Open report in default browser
113
+ */
114
+ function openReport(reportPath) {
115
+ try {
116
+ const platform = process.platform;
117
+ let command;
118
+
119
+ // Determine the command based on the platform
120
+ if (platform === 'darwin') {
121
+ // macOS
122
+ command = ['open', [reportPath]];
123
+ } else if (platform === 'win32') {
124
+ // Windows
125
+ command = ['cmd', ['/c', 'start', '', reportPath]];
126
+ } else {
127
+ // Linux and others
128
+ command = ['xdg-open', [reportPath]];
129
+ }
130
+
131
+ execFileSync(command[0], command[1], { stdio: 'ignore' });
132
+ console.log('');
133
+ logger.success('Opening report in your default browser...');
134
+ } catch (e) {
135
+ console.log('');
136
+ logger.error('Failed to open report automatically');
137
+ logger.info('Please open it manually at:');
138
+ logger.info(reportPath);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Generate enhanced HTML report from expo-doctor output
144
+ */
145
+ function generateHTMLReport(output) {
146
+ const timestamp = new Date().toLocaleString();
147
+ const summary = getIssuesSummary(output);
148
+
149
+ // Escape HTML
150
+ const escapedOutput = output.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
151
+
152
+ // Split into lines for better rendering
153
+ const lines = escapedOutput.split('\n');
154
+ const renderedLines = lines.map((line, index) => {
155
+ // Remove ANSI codes
156
+ let cleanLine = line.replace(/\x1b\[[0-9;]*m/g, '');
157
+
158
+ // Determine line type and styling
159
+ let className = 'line-normal';
160
+ let icon = '';
161
+
162
+ if (cleanLine.includes('✖') || cleanLine.toLowerCase().includes('error')) {
163
+ className = 'line-error';
164
+ icon = '❌';
165
+ } else if (cleanLine.includes('⚠') || cleanLine.toLowerCase().includes('warning')) {
166
+ className = 'line-warning';
167
+ icon = '⚠️';
168
+ } else if (cleanLine.includes('✔') || cleanLine.toLowerCase().includes('success')) {
169
+ className = 'line-success';
170
+ icon = '✅';
171
+ } else if (cleanLine.includes('ℹ') || cleanLine.toLowerCase().includes('info')) {
172
+ className = 'line-info';
173
+ icon = 'ℹ️';
174
+ }
175
+
176
+ return {
177
+ number: index + 1,
178
+ text: cleanLine,
179
+ className,
180
+ icon,
181
+ };
182
+ });
183
+
184
+ // Generate grouped issues HTML (including suggestions)
185
+ const groupedIssuesHTML = generateGroupedIssues(summary, output);
186
+
187
+ return `<!DOCTYPE html>
188
+ <html lang="en">
189
+ <head>
190
+ <meta charset="UTF-8">
191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
192
+ <title>Budexp Report</title>
193
+ <style>
194
+ * {
195
+ margin: 0;
196
+ padding: 0;
197
+ box-sizing: border-box;
198
+ }
199
+
200
+ body {
201
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
202
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
203
+ min-height: 100vh;
204
+ padding: 40px 20px;
205
+ color: #2d3748;
206
+ }
207
+
208
+ .container {
209
+ max-width: 1400px;
210
+ margin: 0 auto;
211
+ }
212
+
213
+ .header {
214
+ background: white;
215
+ border-radius: 16px;
216
+ padding: 32px;
217
+ margin-bottom: 24px;
218
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
219
+ }
220
+
221
+ .header-top {
222
+ display: flex;
223
+ justify-content: space-between;
224
+ align-items: center;
225
+ margin-bottom: 24px;
226
+ flex-wrap: wrap;
227
+ gap: 16px;
228
+ }
229
+
230
+ .title {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 12px;
234
+ }
235
+
236
+ .title h1 {
237
+ font-size: 32px;
238
+ font-weight: 700;
239
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
240
+ -webkit-background-clip: text;
241
+ -webkit-text-fill-color: transparent;
242
+ background-clip: text;
243
+ }
244
+
245
+ .title-icon {
246
+ font-size: 40px;
247
+ }
248
+
249
+ .timestamp {
250
+ color: #718096;
251
+ font-size: 14px;
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 6px;
255
+ }
256
+
257
+ .status-badge {
258
+ display: inline-flex;
259
+ align-items: center;
260
+ gap: 8px;
261
+ padding: 12px 24px;
262
+ border-radius: 12px;
263
+ font-weight: 600;
264
+ font-size: 16px;
265
+ animation: slideIn 0.5s ease;
266
+ }
267
+
268
+ .status-badge.success {
269
+ background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
270
+ color: white;
271
+ }
272
+
273
+ .status-badge.error {
274
+ background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
275
+ color: white;
276
+ }
277
+
278
+ .stats-grid {
279
+ display: grid;
280
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
281
+ gap: 16px;
282
+ margin-top: 24px;
283
+ }
284
+
285
+ .stat-card {
286
+ background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
287
+ padding: 20px;
288
+ border-radius: 12px;
289
+ border: 2px solid transparent;
290
+ transition: all 0.3s ease;
291
+ }
292
+
293
+ .stat-card:hover {
294
+ transform: translateY(-2px);
295
+ border-color: #667eea;
296
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.2);
297
+ }
298
+
299
+ .stat-card.error {
300
+ border-color: #fc8181;
301
+ }
302
+
303
+ .stat-card.warning {
304
+ border-color: #f6ad55;
305
+ }
306
+
307
+ .stat-card.success {
308
+ border-color: #68d391;
309
+ }
310
+
311
+ .stat-label {
312
+ font-size: 13px;
313
+ color: #718096;
314
+ text-transform: uppercase;
315
+ font-weight: 600;
316
+ letter-spacing: 0.5px;
317
+ margin-bottom: 8px;
318
+ }
319
+
320
+ .stat-value {
321
+ font-size: 36px;
322
+ font-weight: 700;
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 8px;
326
+ }
327
+
328
+ .stat-value.error { color: #f56565; }
329
+ .stat-value.warning { color: #ed8936; }
330
+ .stat-value.success { color: #48bb78; }
331
+
332
+ .section {
333
+ background: white;
334
+ border-radius: 16px;
335
+ padding: 32px;
336
+ margin-bottom: 24px;
337
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
338
+ animation: fadeIn 0.5s ease;
339
+ }
340
+
341
+ .section-header {
342
+ display: flex;
343
+ justify-content: space-between;
344
+ align-items: center;
345
+ margin-bottom: 24px;
346
+ cursor: pointer;
347
+ user-select: none;
348
+ }
349
+
350
+ .section-title {
351
+ font-size: 24px;
352
+ font-weight: 700;
353
+ color: #2d3748;
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 12px;
357
+ }
358
+
359
+ .toggle-icon {
360
+ font-size: 20px;
361
+ transition: transform 0.3s ease;
362
+ }
363
+
364
+ .toggle-icon.collapsed {
365
+ transform: rotate(-90deg);
366
+ }
367
+
368
+ .section-content {
369
+ max-height: 2000px;
370
+ overflow: hidden;
371
+ transition: max-height 0.3s ease;
372
+ }
373
+
374
+ .section-content.collapsed {
375
+ max-height: 0;
376
+ }
377
+
378
+ .issues-group {
379
+ margin-bottom: 24px;
380
+ }
381
+
382
+ .issues-group:last-child {
383
+ margin-bottom: 0;
384
+ }
385
+
386
+ .issue-item {
387
+ background: #f7fafc;
388
+ border-left: 4px solid #e2e8f0;
389
+ padding: 16px;
390
+ margin-bottom: 12px;
391
+ border-radius: 8px;
392
+ transition: all 0.2s ease;
393
+ }
394
+
395
+ .issue-item:hover {
396
+ background: #edf2f7;
397
+ transform: translateX(4px);
398
+ }
399
+
400
+ .issue-item.error {
401
+ border-left-color: #f56565;
402
+ background: #fff5f5;
403
+ }
404
+
405
+ .issue-item.error:hover {
406
+ background: #fed7d7;
407
+ }
408
+
409
+ .issue-item.warning {
410
+ border-left-color: #ed8936;
411
+ background: #fffaf0;
412
+ }
413
+
414
+ .issue-item.warning:hover {
415
+ background: #feebc8;
416
+ }
417
+
418
+ .issue-header {
419
+ display: flex;
420
+ align-items: flex-start;
421
+ gap: 12px;
422
+ font-weight: 600;
423
+ margin-bottom: 4px;
424
+ }
425
+
426
+ .issue-icon {
427
+ font-size: 20px;
428
+ flex-shrink: 0;
429
+ }
430
+
431
+ .issue-text {
432
+ flex: 1;
433
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
434
+ font-size: 14px;
435
+ line-height: 1.6;
436
+ }
437
+
438
+ .terminal-output {
439
+ background: #1e1e1e;
440
+ border-radius: 12px;
441
+ overflow: hidden;
442
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
443
+ font-size: 13px;
444
+ }
445
+
446
+ .terminal-header {
447
+ background: #323233;
448
+ padding: 12px 16px;
449
+ display: flex;
450
+ justify-content: space-between;
451
+ align-items: center;
452
+ border-bottom: 1px solid #1e1e1e;
453
+ }
454
+
455
+ .terminal-title {
456
+ color: #d4d4d4;
457
+ font-weight: 600;
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 8px;
461
+ }
462
+
463
+ .terminal-buttons {
464
+ display: flex;
465
+ gap: 8px;
466
+ }
467
+
468
+ .terminal-button {
469
+ background: #4a5568;
470
+ color: white;
471
+ border: none;
472
+ padding: 6px 12px;
473
+ border-radius: 6px;
474
+ cursor: pointer;
475
+ font-size: 12px;
476
+ display: flex;
477
+ align-items: center;
478
+ gap: 6px;
479
+ transition: all 0.2s ease;
480
+ }
481
+
482
+ .terminal-button:hover {
483
+ background: #667eea;
484
+ }
485
+
486
+ .terminal-button:active {
487
+ transform: scale(0.95);
488
+ }
489
+
490
+ .terminal-body {
491
+ padding: 20px;
492
+ max-height: 600px;
493
+ overflow-y: auto;
494
+ background: #1e1e1e;
495
+ }
496
+
497
+ .terminal-line {
498
+ display: flex;
499
+ gap: 12px;
500
+ padding: 4px 0;
501
+ line-height: 1.6;
502
+ }
503
+
504
+ .line-number {
505
+ color: #6e7681;
506
+ user-select: none;
507
+ min-width: 40px;
508
+ text-align: right;
509
+ }
510
+
511
+ .line-content {
512
+ color: #d4d4d4;
513
+ flex: 1;
514
+ white-space: pre-wrap;
515
+ word-break: break-word;
516
+ }
517
+
518
+ .line-error .line-content {
519
+ color: #f87171;
520
+ }
521
+
522
+ .line-warning .line-content {
523
+ color: #fbbf24;
524
+ }
525
+
526
+ .line-success .line-content {
527
+ color: #4ade80;
528
+ }
529
+
530
+ .line-info .line-content {
531
+ color: #60a5fa;
532
+ }
533
+
534
+ .search-box {
535
+ margin-bottom: 16px;
536
+ }
537
+
538
+ .search-input {
539
+ width: 100%;
540
+ padding: 12px 16px;
541
+ border: 2px solid #e2e8f0;
542
+ border-radius: 8px;
543
+ font-size: 14px;
544
+ transition: all 0.2s ease;
545
+ }
546
+
547
+ .search-input:focus {
548
+ outline: none;
549
+ border-color: #667eea;
550
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
551
+ }
552
+
553
+ .footer {
554
+ text-align: center;
555
+ color: white;
556
+ margin-top: 40px;
557
+ font-size: 14px;
558
+ opacity: 0.9;
559
+ }
560
+
561
+ .footer a {
562
+ color: white;
563
+ text-decoration: underline;
564
+ }
565
+
566
+ @keyframes slideIn {
567
+ from {
568
+ opacity: 0;
569
+ transform: translateY(-10px);
570
+ }
571
+ to {
572
+ opacity: 1;
573
+ transform: translateY(0);
574
+ }
575
+ }
576
+
577
+ @keyframes fadeIn {
578
+ from {
579
+ opacity: 0;
580
+ }
581
+ to {
582
+ opacity: 1;
583
+ }
584
+ }
585
+
586
+ .highlight {
587
+ background: #fef3c7;
588
+ padding: 2px 4px;
589
+ border-radius: 3px;
590
+ }
591
+
592
+ .suggestions-intro {
593
+ background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
594
+ padding: 16px 20px;
595
+ border-radius: 8px;
596
+ margin-bottom: 20px;
597
+ border-left: 4px solid #667eea;
598
+ }
599
+
600
+ .suggestions-intro p {
601
+ margin: 0;
602
+ color: #4c51bf;
603
+ font-weight: 500;
604
+ font-size: 15px;
605
+ }
606
+
607
+ .suggestions-list {
608
+ display: flex;
609
+ flex-direction: column;
610
+ gap: 16px;
611
+ }
612
+
613
+ .suggestion-card {
614
+ display: flex;
615
+ gap: 16px;
616
+ background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
617
+ border: 2px solid #86efac;
618
+ border-radius: 12px;
619
+ padding: 20px;
620
+ transition: all 0.3s ease;
621
+ position: relative;
622
+ overflow: hidden;
623
+ }
624
+
625
+ .suggestion-card::before {
626
+ content: '';
627
+ position: absolute;
628
+ top: 0;
629
+ left: 0;
630
+ width: 4px;
631
+ height: 100%;
632
+ background: linear-gradient(180deg, #10b981 0%, #059669 100%);
633
+ }
634
+
635
+ .suggestion-card:hover {
636
+ transform: translateX(4px);
637
+ box-shadow: 0 8px 24px rgba(16, 185, 129, 0.2);
638
+ border-color: #10b981;
639
+ }
640
+
641
+ .suggestion-number {
642
+ flex-shrink: 0;
643
+ width: 32px;
644
+ height: 32px;
645
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
646
+ color: white;
647
+ border-radius: 50%;
648
+ display: flex;
649
+ align-items: center;
650
+ justify-content: center;
651
+ font-weight: 700;
652
+ font-size: 14px;
653
+ box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
654
+ }
655
+
656
+ .suggestion-content {
657
+ flex: 1;
658
+ display: flex;
659
+ flex-direction: column;
660
+ gap: 12px;
661
+ }
662
+
663
+ .suggestion-text {
664
+ color: #065f46;
665
+ font-size: 15px;
666
+ line-height: 1.6;
667
+ font-weight: 500;
668
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
669
+ }
670
+
671
+ .suggestion-link {
672
+ color: #059669;
673
+ text-decoration: none;
674
+ font-weight: 600;
675
+ font-size: 14px;
676
+ display: inline-flex;
677
+ align-items: center;
678
+ gap: 4px;
679
+ transition: all 0.2s ease;
680
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
681
+ }
682
+
683
+ .suggestion-link:hover {
684
+ color: #047857;
685
+ gap: 8px;
686
+ }
687
+
688
+ @media (max-width: 768px) {
689
+ body {
690
+ padding: 20px 12px;
691
+ }
692
+
693
+ .header {
694
+ padding: 20px;
695
+ }
696
+
697
+ .title h1 {
698
+ font-size: 24px;
699
+ }
700
+
701
+ .section {
702
+ padding: 20px;
703
+ }
704
+
705
+ .stats-grid {
706
+ grid-template-columns: 1fr;
707
+ }
708
+ }
709
+ </style>
710
+ </head>
711
+ <body>
712
+ <div class="container">
713
+ <!-- Header -->
714
+ <div class="header">
715
+ <div class="header-top">
716
+ <div class="title">
717
+ <h1>Budexp Report</h1>
718
+ </div>
719
+ <div class="timestamp">
720
+ ${timestamp}
721
+ </div>
722
+ </div>
723
+
724
+ <div class="stats-grid">
725
+ <div class="stat-card ${summary.errors && summary.errors.length > 0 ? 'error' : ''}">
726
+ <div class="stat-label">Errors</div>
727
+ <div class="stat-value error">
728
+ <span>❌</span>
729
+ <span>${summary.errors ? summary.errors.length : 0}</span>
730
+ </div>
731
+ </div>
732
+
733
+ <div class="stat-card ${summary.warnings && summary.warnings.length > 0 ? 'warning' : ''}">
734
+ <div class="stat-label">Warnings</div>
735
+ <div class="stat-value warning">
736
+ <span>⚠️</span>
737
+ <span>${summary.warnings ? summary.warnings.length : 0}</span>
738
+ </div>
739
+ </div>
740
+
741
+ <div class="stat-card success">
742
+ <div class="stat-label">Total Checks</div>
743
+ <div class="stat-value success">
744
+ <span>✅</span>
745
+ <span>${lines.length}</span>
746
+ </div>
747
+ </div>
748
+ </div>
749
+ </div>
750
+
751
+ <!-- Issues Section -->
752
+ ${groupedIssuesHTML}
753
+
754
+ <!-- Full Output Section -->
755
+ <div class="section">
756
+ <div class="section-header" onclick="toggleSection('output')">
757
+ <div class="section-title">
758
+ <span>Full Terminal Output</span>
759
+ </div>
760
+ <span class="toggle-icon" id="output-toggle">▼</span>
761
+ </div>
762
+
763
+ <div class="section-content" id="output-content">
764
+ <div class="search-box">
765
+ <input
766
+ type="text"
767
+ class="search-input"
768
+ id="search-input"
769
+ placeholder="🔍 Search in output..."
770
+ onkeyup="searchInOutput()"
771
+ />
772
+ </div>
773
+
774
+ <div class="terminal-output">
775
+ <div class="terminal-header">
776
+ <div class="terminal-title">
777
+ <span>💻</span>
778
+ <span>expo-doctor output</span>
779
+ </div>
780
+ <div class="terminal-buttons">
781
+ <button class="terminal-button" onclick="copyOutput()">
782
+ 📋 Copy
783
+ </button>
784
+ </div>
785
+ </div>
786
+ <div class="terminal-body" id="terminal-body">
787
+ ${renderedLines
788
+ .map(
789
+ (line) => `
790
+ <div class="terminal-line ${line.className}" data-line="${line.text.toLowerCase()}">
791
+ <span class="line-number">${line.number}</span>
792
+ <span class="line-content">${line.icon ? line.icon + ' ' : ''}${line.text || '&nbsp;'}</span>
793
+ </div>
794
+ `
795
+ )
796
+ .join('')}
797
+ </div>
798
+ </div>
799
+ </div>
800
+ </div>
801
+
802
+ <div class="footer">
803
+ Generated by <strong>budexp</strong> CLI tool
804
+ </div>
805
+ </div>
806
+
807
+ <script>
808
+ function toggleSection(sectionId) {
809
+ const content = document.getElementById(sectionId + '-content');
810
+ const toggle = document.getElementById(sectionId + '-toggle');
811
+
812
+ content.classList.toggle('collapsed');
813
+ toggle.classList.toggle('collapsed');
814
+ }
815
+
816
+ function copyOutput() {
817
+ const lines = document.querySelectorAll('.terminal-line .line-content');
818
+ const text = Array.from(lines).map(line => line.textContent).join('\\n');
819
+
820
+ navigator.clipboard.writeText(text).then(() => {
821
+ const button = event.target.closest('.terminal-button');
822
+ const originalText = button.innerHTML;
823
+ button.innerHTML = '✅ Copied!';
824
+ setTimeout(() => {
825
+ button.innerHTML = originalText;
826
+ }, 2000);
827
+ });
828
+ }
829
+
830
+ function searchInOutput() {
831
+ const searchTerm = document.getElementById('search-input').value.toLowerCase();
832
+ const lines = document.querySelectorAll('.terminal-line');
833
+
834
+ lines.forEach(line => {
835
+ const text = line.getAttribute('data-line');
836
+ const content = line.querySelector('.line-content');
837
+
838
+ if (searchTerm === '') {
839
+ line.style.display = 'flex';
840
+ content.innerHTML = content.textContent;
841
+ } else if (text.includes(searchTerm)) {
842
+ line.style.display = 'flex';
843
+
844
+ // Highlight matching text
845
+ const originalText = content.textContent;
846
+ const regex = new RegExp(\`(\${searchTerm})\`, 'gi');
847
+ content.innerHTML = originalText.replace(regex, '<span class="highlight">$1</span>');
848
+ } else {
849
+ line.style.display = 'none';
850
+ }
851
+ });
852
+ }
853
+ </script>
854
+ </body>
855
+ </html>`;
856
+ }
857
+
858
+ /**
859
+ * Extract suggestions/advice from output
860
+ */
861
+ function extractSuggestions(output) {
862
+ const suggestions = [];
863
+ const lines = output.split('\n');
864
+
865
+ for (let i = 0; i < lines.length; i++) {
866
+ const line = lines[i].trim();
867
+ const lowerLine = line.toLowerCase();
868
+
869
+ // Look for "Advice:" sections and collect following lines
870
+ if (lowerLine.startsWith('advice:')) {
871
+ // Get the next non-empty lines as suggestions
872
+ for (let j = i + 1; j < lines.length; j++) {
873
+ const suggestionLine = lines[j].trim();
874
+ if (!suggestionLine) continue;
875
+
876
+ // Stop if we hit another section or check
877
+ if (
878
+ suggestionLine.toLowerCase().startsWith('advice:') ||
879
+ suggestionLine.includes('✖') ||
880
+ suggestionLine.includes('✔') ||
881
+ suggestionLine.toLowerCase().includes('check ')
882
+ ) {
883
+ break;
884
+ }
885
+
886
+ suggestions.push({
887
+ text: suggestionLine,
888
+ type: 'advice',
889
+ });
890
+ break; // Only get the first line after "Advice:"
891
+ }
892
+ }
893
+
894
+ // Also capture lines that start with common suggestion patterns
895
+ if (
896
+ lowerLine.startsWith('use ') ||
897
+ lowerLine.startsWith('run ') ||
898
+ lowerLine.startsWith('resolve ') ||
899
+ lowerLine.startsWith('update ') ||
900
+ lowerLine.startsWith('install ')
901
+ ) {
902
+ // Avoid duplicates
903
+ if (!suggestions.some((s) => s.text === line)) {
904
+ suggestions.push({
905
+ text: line,
906
+ type: 'action',
907
+ });
908
+ }
909
+ }
910
+ }
911
+
912
+ return suggestions;
913
+ }
914
+
915
+ /**
916
+ * Generate grouped issues HTML section
917
+ */
918
+ function generateGroupedIssues(summary, output) {
919
+ if (!summary.hasIssues) {
920
+ return `
921
+ <div class="section">
922
+ <div class="section-title">
923
+ <span>Great News!</span>
924
+ </div>
925
+ <div class="section-content">
926
+ <p style="color: #48bb78; font-size: 18px; text-align: center; padding: 40px;">
927
+ All checks passed! Your Expo project is healthy and ready to go!
928
+ </p>
929
+ </div>
930
+ </div>
931
+ `;
932
+ }
933
+
934
+ let html = '';
935
+
936
+ // Suggestions/Recommendations section
937
+ const suggestions = extractSuggestions(output);
938
+ if (suggestions.length > 0) {
939
+ html += `
940
+ <div class="section">
941
+ <div class="section-header" onclick="toggleSection('suggestions')">
942
+ <div class="section-title">
943
+ <span>Recommendations (${suggestions.length})</span>
944
+ </div>
945
+ <span class="toggle-icon" id="suggestions-toggle">▼</span>
946
+ </div>
947
+ <div class="section-content" id="suggestions-content">
948
+ <div class="suggestions-intro">
949
+ <p>Here are actionable steps to resolve the issues found in your project:</p>
950
+ </div>
951
+ <div class="suggestions-list">
952
+ ${suggestions
953
+ .map((suggestion, index) => {
954
+ // Check if it contains a URL
955
+ const urlMatch = suggestion.text.match(/(https?:\/\/[^\s]+)/);
956
+ let displayText = suggestion.text;
957
+ let linkHtml = '';
958
+
959
+ if (urlMatch) {
960
+ const url = urlMatch[0];
961
+ displayText = suggestion.text.replace(url, '').trim();
962
+ linkHtml = `<a href="${url}" target="_blank" class="suggestion-link">📚 Learn more →</a>`;
963
+ }
964
+
965
+ return `
966
+ <div class="suggestion-card">
967
+ <div class="suggestion-number">${index + 1}</div>
968
+ <div class="suggestion-content">
969
+ <div class="suggestion-text">${displayText}</div>
970
+ ${linkHtml}
971
+ </div>
972
+ </div>
973
+ `;
974
+ })
975
+ .join('')}
976
+ </div>
977
+ </div>
978
+ </div>
979
+ `;
980
+ }
981
+
982
+ // Errors section
983
+ if (summary.errors && summary.errors.length > 0) {
984
+ html += `
985
+ <div class="section">
986
+ <div class="section-header" onclick="toggleSection('errors')">
987
+ <div class="section-title">
988
+ <span>Errors (${summary.errors.length})</span>
989
+ </div>
990
+ <span class="toggle-icon" id="errors-toggle">▼</span>
991
+ </div>
992
+ <div class="section-content" id="errors-content">
993
+ <div class="issues-group">
994
+ ${summary.errors
995
+ .map(
996
+ (issue) => `
997
+ <div class="issue-item error">
998
+ <div class="issue-header">
999
+ <span class="issue-icon">❌</span>
1000
+ <div class="issue-text">${issue.message}</div>
1001
+ </div>
1002
+ </div>
1003
+ `
1004
+ )
1005
+ .join('')}
1006
+ </div>
1007
+ </div>
1008
+ </div>
1009
+ `;
1010
+ }
1011
+
1012
+ // Warnings section
1013
+ if (summary.warnings && summary.warnings.length > 0) {
1014
+ html += `
1015
+ <div class="section">
1016
+ <div class="section-header" onclick="toggleSection('warnings')">
1017
+ <div class="section-title">
1018
+ <span>Warnings (${summary.warnings.length})</span>
1019
+ </div>
1020
+ <span class="toggle-icon" id="warnings-toggle">▼</span>
1021
+ </div>
1022
+ <div class="section-content" id="warnings-content">
1023
+ <div class="issues-group">
1024
+ ${summary.warnings
1025
+ .map(
1026
+ (issue) => `
1027
+ <div class="issue-item warning">
1028
+ <div class="issue-header">
1029
+ <span class="issue-icon">⚠️</span>
1030
+ <div class="issue-text">${issue.message}</div>
1031
+ </div>
1032
+ </div>
1033
+ `
1034
+ )
1035
+ .join('')}
1036
+ </div>
1037
+ </div>
1038
+ </div>
1039
+ `;
1040
+ }
1041
+
1042
+ return html;
1043
+ }
1044
+
1045
+ /**
1046
+ * Parse expo-doctor output for issues
1047
+ */
1048
+ function parseIssues(output) {
1049
+ const issues = [];
1050
+ const lines = output.split('\n');
1051
+
1052
+ for (let i = 0; i < lines.length; i++) {
1053
+ const line = lines[i];
1054
+ if (line.includes('✖') || line.includes('⚠') || line.toLowerCase().includes('error')) {
1055
+ issues.push({
1056
+ line: i + 1,
1057
+ message: line.trim(),
1058
+ severity: line.includes('✖') ? 'error' : 'warning',
1059
+ });
1060
+ }
1061
+ }
1062
+
1063
+ return issues;
1064
+ }
1065
+
1066
+ /**
1067
+ * Get human-readable summary of issues
1068
+ */
1069
+ function getIssuesSummary(output) {
1070
+ const issues = parseIssues(output);
1071
+
1072
+ if (issues.length === 0) {
1073
+ return {
1074
+ hasIssues: false,
1075
+ summary: 'All checks passed! Your Expo project is healthy.',
1076
+ issues: [],
1077
+ };
1078
+ }
1079
+
1080
+ const errors = issues.filter((i) => i.severity === 'error');
1081
+ const warnings = issues.filter((i) => i.severity === 'warning');
1082
+
1083
+ return {
1084
+ hasIssues: true,
1085
+ summary: `Found ${errors.length} error(s) and ${warnings.length} warning(s)`,
1086
+ errors,
1087
+ warnings,
1088
+ issues,
1089
+ };
1090
+ }
1091
+
1092
+ module.exports = {
1093
+ runExpoDoctor,
1094
+ generateHTMLReport,
1095
+ parseIssues,
1096
+ getIssuesSummary,
1097
+ };