@mjasano/devtunnel 1.0.0 → 1.2.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/CHANGELOG.md +44 -0
- package/CLAUDE.md +18 -0
- package/package.json +2 -2
- package/public/index.html +830 -32
- package/server.js +453 -6
package/public/index.html
CHANGED
|
@@ -407,6 +407,336 @@
|
|
|
407
407
|
border-color: #f85149;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/* Tab Bar */
|
|
411
|
+
.tab-bar {
|
|
412
|
+
display: flex;
|
|
413
|
+
background-color: #161b22;
|
|
414
|
+
border-bottom: 1px solid #30363d;
|
|
415
|
+
padding: 0 12px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.tab {
|
|
419
|
+
padding: 10px 20px;
|
|
420
|
+
font-size: 13px;
|
|
421
|
+
color: #8b949e;
|
|
422
|
+
cursor: pointer;
|
|
423
|
+
border-bottom: 2px solid transparent;
|
|
424
|
+
transition: all 0.15s ease;
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 8px;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.tab:hover {
|
|
431
|
+
color: #c9d1d9;
|
|
432
|
+
background-color: #21262d;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.tab.active {
|
|
436
|
+
color: #c9d1d9;
|
|
437
|
+
border-bottom-color: #58a6ff;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.tab-icon {
|
|
441
|
+
font-size: 14px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Editor Container */
|
|
445
|
+
#editor-container {
|
|
446
|
+
flex: 1;
|
|
447
|
+
display: none;
|
|
448
|
+
flex-direction: column;
|
|
449
|
+
background-color: #0d1117;
|
|
450
|
+
overflow: hidden;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
#editor-container.active {
|
|
454
|
+
display: flex;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#terminal-container.hidden {
|
|
458
|
+
display: none;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* Terminal Tabs */
|
|
462
|
+
.terminal-tabs {
|
|
463
|
+
display: flex;
|
|
464
|
+
background-color: #161b22;
|
|
465
|
+
border-bottom: 1px solid #30363d;
|
|
466
|
+
overflow-x: auto;
|
|
467
|
+
min-height: 35px;
|
|
468
|
+
align-items: center;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.terminal-tab {
|
|
472
|
+
display: flex;
|
|
473
|
+
align-items: center;
|
|
474
|
+
gap: 8px;
|
|
475
|
+
padding: 8px 12px;
|
|
476
|
+
font-size: 12px;
|
|
477
|
+
color: #8b949e;
|
|
478
|
+
background-color: #0d1117;
|
|
479
|
+
border-right: 1px solid #30363d;
|
|
480
|
+
cursor: pointer;
|
|
481
|
+
white-space: nowrap;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.terminal-tab:hover {
|
|
485
|
+
background-color: #161b22;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.terminal-tab.active {
|
|
489
|
+
color: #c9d1d9;
|
|
490
|
+
background-color: #161b22;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.terminal-tab-close {
|
|
494
|
+
width: 16px;
|
|
495
|
+
height: 16px;
|
|
496
|
+
display: flex;
|
|
497
|
+
align-items: center;
|
|
498
|
+
justify-content: center;
|
|
499
|
+
border-radius: 4px;
|
|
500
|
+
font-size: 14px;
|
|
501
|
+
line-height: 1;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.terminal-tab-close:hover {
|
|
505
|
+
background-color: #30363d;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.terminal-tab-new {
|
|
509
|
+
width: 28px;
|
|
510
|
+
height: 28px;
|
|
511
|
+
margin: 0 4px;
|
|
512
|
+
background-color: transparent;
|
|
513
|
+
border: 1px solid #30363d;
|
|
514
|
+
border-radius: 4px;
|
|
515
|
+
color: #8b949e;
|
|
516
|
+
font-size: 16px;
|
|
517
|
+
cursor: pointer;
|
|
518
|
+
display: flex;
|
|
519
|
+
align-items: center;
|
|
520
|
+
justify-content: center;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.terminal-tab-new:hover {
|
|
524
|
+
background-color: #21262d;
|
|
525
|
+
color: #c9d1d9;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.terminal-tab .tmux-badge {
|
|
529
|
+
color: #3fb950;
|
|
530
|
+
font-size: 10px;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.editor-tabs {
|
|
534
|
+
display: flex;
|
|
535
|
+
background-color: #161b22;
|
|
536
|
+
border-bottom: 1px solid #30363d;
|
|
537
|
+
overflow-x: auto;
|
|
538
|
+
min-height: 35px;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.editor-tab {
|
|
542
|
+
display: flex;
|
|
543
|
+
align-items: center;
|
|
544
|
+
gap: 8px;
|
|
545
|
+
padding: 8px 12px;
|
|
546
|
+
font-size: 12px;
|
|
547
|
+
color: #8b949e;
|
|
548
|
+
background-color: #0d1117;
|
|
549
|
+
border-right: 1px solid #30363d;
|
|
550
|
+
cursor: pointer;
|
|
551
|
+
white-space: nowrap;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.editor-tab:hover {
|
|
555
|
+
background-color: #161b22;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.editor-tab.active {
|
|
559
|
+
color: #c9d1d9;
|
|
560
|
+
background-color: #161b22;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.editor-tab.modified::after {
|
|
564
|
+
content: '';
|
|
565
|
+
width: 6px;
|
|
566
|
+
height: 6px;
|
|
567
|
+
background-color: #d29922;
|
|
568
|
+
border-radius: 50%;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.editor-tab-close {
|
|
572
|
+
width: 16px;
|
|
573
|
+
height: 16px;
|
|
574
|
+
display: flex;
|
|
575
|
+
align-items: center;
|
|
576
|
+
justify-content: center;
|
|
577
|
+
border-radius: 4px;
|
|
578
|
+
font-size: 14px;
|
|
579
|
+
line-height: 1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.editor-tab-close:hover {
|
|
583
|
+
background-color: #30363d;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#monaco-editor {
|
|
587
|
+
flex: 1;
|
|
588
|
+
min-height: 0;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.editor-empty {
|
|
592
|
+
flex: 1;
|
|
593
|
+
display: flex;
|
|
594
|
+
flex-direction: column;
|
|
595
|
+
align-items: center;
|
|
596
|
+
justify-content: center;
|
|
597
|
+
color: #6e7681;
|
|
598
|
+
font-size: 14px;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.editor-empty-icon {
|
|
602
|
+
font-size: 48px;
|
|
603
|
+
margin-bottom: 16px;
|
|
604
|
+
opacity: 0.5;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* File Tree */
|
|
608
|
+
.file-tree {
|
|
609
|
+
font-size: 12px;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.file-item {
|
|
613
|
+
display: flex;
|
|
614
|
+
align-items: center;
|
|
615
|
+
gap: 6px;
|
|
616
|
+
padding: 4px 8px;
|
|
617
|
+
cursor: pointer;
|
|
618
|
+
border-radius: 4px;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.file-item:hover {
|
|
622
|
+
background-color: #21262d;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.file-item.active {
|
|
626
|
+
background-color: rgba(56, 139, 253, 0.15);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.file-item-icon {
|
|
630
|
+
width: 16px;
|
|
631
|
+
text-align: center;
|
|
632
|
+
flex-shrink: 0;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.file-item-name {
|
|
636
|
+
overflow: hidden;
|
|
637
|
+
text-overflow: ellipsis;
|
|
638
|
+
white-space: nowrap;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.file-item.directory > .file-item-icon {
|
|
642
|
+
color: #58a6ff;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.file-item.file > .file-item-icon {
|
|
646
|
+
color: #8b949e;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.file-children {
|
|
650
|
+
margin-left: 12px;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.breadcrumb {
|
|
654
|
+
display: flex;
|
|
655
|
+
align-items: center;
|
|
656
|
+
gap: 4px;
|
|
657
|
+
padding: 8px 12px;
|
|
658
|
+
font-size: 11px;
|
|
659
|
+
color: #8b949e;
|
|
660
|
+
border-bottom: 1px solid #30363d;
|
|
661
|
+
overflow-x: auto;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.breadcrumb-item {
|
|
665
|
+
cursor: pointer;
|
|
666
|
+
padding: 2px 4px;
|
|
667
|
+
border-radius: 4px;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.breadcrumb-item:hover {
|
|
671
|
+
background-color: #21262d;
|
|
672
|
+
color: #c9d1d9;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.breadcrumb-sep {
|
|
676
|
+
color: #484f58;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* System Monitor */
|
|
680
|
+
.system-stats {
|
|
681
|
+
padding: 8px 12px;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.stat-row {
|
|
685
|
+
display: flex;
|
|
686
|
+
justify-content: space-between;
|
|
687
|
+
align-items: center;
|
|
688
|
+
padding: 6px 0;
|
|
689
|
+
font-size: 12px;
|
|
690
|
+
border-bottom: 1px solid #21262d;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.stat-row:last-child {
|
|
694
|
+
border-bottom: none;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.stat-label {
|
|
698
|
+
color: #8b949e;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.stat-value {
|
|
702
|
+
color: #c9d1d9;
|
|
703
|
+
font-family: monospace;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.stat-bar {
|
|
707
|
+
width: 100%;
|
|
708
|
+
height: 6px;
|
|
709
|
+
background-color: #21262d;
|
|
710
|
+
border-radius: 3px;
|
|
711
|
+
margin-top: 4px;
|
|
712
|
+
overflow: hidden;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.stat-bar-fill {
|
|
716
|
+
height: 100%;
|
|
717
|
+
border-radius: 3px;
|
|
718
|
+
transition: width 0.3s ease;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.stat-bar-fill.cpu {
|
|
722
|
+
background: linear-gradient(90deg, #58a6ff, #8b5cf6);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.stat-bar-fill.memory {
|
|
726
|
+
background: linear-gradient(90deg, #3fb950, #58a6ff);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.stat-bar-fill.high {
|
|
730
|
+
background: linear-gradient(90deg, #d29922, #f85149);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.system-hostname {
|
|
734
|
+
font-size: 11px;
|
|
735
|
+
color: #6e7681;
|
|
736
|
+
padding: 4px 12px 8px;
|
|
737
|
+
border-bottom: 1px solid #21262d;
|
|
738
|
+
}
|
|
739
|
+
|
|
410
740
|
@keyframes slideIn {
|
|
411
741
|
from {
|
|
412
742
|
transform: translateY(20px);
|
|
@@ -454,22 +784,80 @@
|
|
|
454
784
|
</div>
|
|
455
785
|
</div>
|
|
456
786
|
|
|
787
|
+
<!-- Tab Bar -->
|
|
788
|
+
<div class="tab-bar">
|
|
789
|
+
<div class="tab active" data-tab="terminal" onclick="switchTab('terminal')">
|
|
790
|
+
<span class="tab-icon">></span>
|
|
791
|
+
Terminal
|
|
792
|
+
</div>
|
|
793
|
+
<div class="tab" data-tab="editor" onclick="switchTab('editor')">
|
|
794
|
+
<span class="tab-icon">{}</span>
|
|
795
|
+
Editor
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
|
|
457
799
|
<div class="main-container">
|
|
458
800
|
<div id="terminal-container">
|
|
801
|
+
<div class="terminal-tabs" id="terminal-tabs">
|
|
802
|
+
<button class="terminal-tab-new" onclick="createNewSession()" title="New Shell">+</button>
|
|
803
|
+
</div>
|
|
459
804
|
<div id="terminal"></div>
|
|
460
805
|
</div>
|
|
461
806
|
|
|
807
|
+
<div id="editor-container">
|
|
808
|
+
<div class="editor-tabs" id="editor-tabs"></div>
|
|
809
|
+
<div id="monaco-editor"></div>
|
|
810
|
+
<div class="editor-empty" id="editor-empty">
|
|
811
|
+
<div class="editor-empty-icon">{}</div>
|
|
812
|
+
<div>Select a file to edit</div>
|
|
813
|
+
<div style="margin-top: 8px; font-size: 12px; color: #484f58;">
|
|
814
|
+
Ctrl+S to save
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
</div>
|
|
818
|
+
|
|
462
819
|
<div class="sidebar">
|
|
463
|
-
<!--
|
|
820
|
+
<!-- System Monitor Section -->
|
|
821
|
+
<div class="sidebar-section">
|
|
822
|
+
<div class="sidebar-header" onclick="toggleSection('system')">
|
|
823
|
+
<h2>System</h2>
|
|
824
|
+
</div>
|
|
825
|
+
<div id="system-content">
|
|
826
|
+
<div class="system-hostname" id="system-hostname">Loading...</div>
|
|
827
|
+
<div class="system-stats" id="system-stats">
|
|
828
|
+
<div class="stat-row">
|
|
829
|
+
<span class="stat-label">CPU</span>
|
|
830
|
+
<span class="stat-value" id="cpu-value">--%</span>
|
|
831
|
+
</div>
|
|
832
|
+
<div class="stat-bar"><div class="stat-bar-fill cpu" id="cpu-bar" style="width: 0%"></div></div>
|
|
833
|
+
<div class="stat-row" style="margin-top: 8px;">
|
|
834
|
+
<span class="stat-label">Memory</span>
|
|
835
|
+
<span class="stat-value" id="mem-value">--%</span>
|
|
836
|
+
</div>
|
|
837
|
+
<div class="stat-bar"><div class="stat-bar-fill memory" id="mem-bar" style="width: 0%"></div></div>
|
|
838
|
+
<div class="stat-row" style="margin-top: 8px;">
|
|
839
|
+
<span class="stat-label">Uptime</span>
|
|
840
|
+
<span class="stat-value" id="uptime-value">--</span>
|
|
841
|
+
</div>
|
|
842
|
+
<div class="stat-row">
|
|
843
|
+
<span class="stat-label">Load</span>
|
|
844
|
+
<span class="stat-value" id="load-value">--</span>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
|
|
850
|
+
<!-- Files Section -->
|
|
464
851
|
<div class="sidebar-section">
|
|
465
|
-
<div class="sidebar-header" onclick="toggleSection('
|
|
466
|
-
<h2>
|
|
467
|
-
<span class="count" id="session-count">0</span>
|
|
852
|
+
<div class="sidebar-header" onclick="toggleSection('files')">
|
|
853
|
+
<h2>Files</h2>
|
|
468
854
|
</div>
|
|
469
|
-
<div class="
|
|
470
|
-
<
|
|
471
|
-
|
|
472
|
-
|
|
855
|
+
<div class="breadcrumb" id="file-breadcrumb">
|
|
856
|
+
<span class="breadcrumb-item" onclick="navigateToPath('')">~</span>
|
|
857
|
+
</div>
|
|
858
|
+
<div class="sidebar-content" id="files-content" style="max-height: 300px;">
|
|
859
|
+
<div class="file-tree" id="file-tree">
|
|
860
|
+
<div class="empty-state">Loading...</div>
|
|
473
861
|
</div>
|
|
474
862
|
</div>
|
|
475
863
|
</div>
|
|
@@ -508,6 +896,7 @@
|
|
|
508
896
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
509
897
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
510
898
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
|
|
899
|
+
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
|
511
900
|
<script>
|
|
512
901
|
const terminalContainer = document.getElementById('terminal');
|
|
513
902
|
const statusDot = document.getElementById('status-dot');
|
|
@@ -522,8 +911,312 @@
|
|
|
522
911
|
|
|
523
912
|
let tunnels = [];
|
|
524
913
|
let sessions = [];
|
|
914
|
+
let tmuxSessions = [];
|
|
525
915
|
let currentSessionId = localStorage.getItem('devtunnel-session-id');
|
|
526
916
|
|
|
917
|
+
// Editor state
|
|
918
|
+
let monacoEditor = null;
|
|
919
|
+
let openFiles = new Map(); // path -> { content, originalContent, model }
|
|
920
|
+
let activeFilePath = null;
|
|
921
|
+
let currentBrowsePath = '';
|
|
922
|
+
|
|
923
|
+
// Initialize Monaco Editor
|
|
924
|
+
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' }});
|
|
925
|
+
require(['vs/editor/editor.main'], function() {
|
|
926
|
+
monaco.editor.defineTheme('github-dark', {
|
|
927
|
+
base: 'vs-dark',
|
|
928
|
+
inherit: true,
|
|
929
|
+
rules: [],
|
|
930
|
+
colors: {
|
|
931
|
+
'editor.background': '#0d1117',
|
|
932
|
+
'editor.foreground': '#c9d1d9',
|
|
933
|
+
'editorCursor.foreground': '#58a6ff',
|
|
934
|
+
'editor.lineHighlightBackground': '#161b22',
|
|
935
|
+
'editorLineNumber.foreground': '#6e7681',
|
|
936
|
+
'editor.selectionBackground': '#264f78',
|
|
937
|
+
'editorIndentGuide.background': '#21262d',
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
monacoEditor = monaco.editor.create(document.getElementById('monaco-editor'), {
|
|
942
|
+
value: '',
|
|
943
|
+
language: 'plaintext',
|
|
944
|
+
theme: 'github-dark',
|
|
945
|
+
fontSize: 14,
|
|
946
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
947
|
+
minimap: { enabled: false },
|
|
948
|
+
automaticLayout: true,
|
|
949
|
+
scrollBeyondLastLine: false,
|
|
950
|
+
wordWrap: 'on',
|
|
951
|
+
tabSize: 2,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// Ctrl+S to save
|
|
955
|
+
monacoEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
|
956
|
+
if (activeFilePath) saveFile(activeFilePath);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Track changes
|
|
960
|
+
monacoEditor.onDidChangeModelContent(() => {
|
|
961
|
+
if (activeFilePath && openFiles.has(activeFilePath)) {
|
|
962
|
+
const file = openFiles.get(activeFilePath);
|
|
963
|
+
file.content = monacoEditor.getValue();
|
|
964
|
+
renderEditorTabs();
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// Hide editor initially
|
|
969
|
+
document.getElementById('monaco-editor').style.display = 'none';
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// Tab switching
|
|
973
|
+
function switchTab(tab) {
|
|
974
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
975
|
+
document.querySelector(`.tab[data-tab="${tab}"]`).classList.add('active');
|
|
976
|
+
|
|
977
|
+
const terminalContainer = document.getElementById('terminal-container');
|
|
978
|
+
const editorContainer = document.getElementById('editor-container');
|
|
979
|
+
|
|
980
|
+
if (tab === 'terminal') {
|
|
981
|
+
terminalContainer.classList.remove('hidden');
|
|
982
|
+
editorContainer.classList.remove('active');
|
|
983
|
+
setTimeout(() => fitAddon.fit(), 0);
|
|
984
|
+
term.focus();
|
|
985
|
+
} else {
|
|
986
|
+
terminalContainer.classList.add('hidden');
|
|
987
|
+
editorContainer.classList.add('active');
|
|
988
|
+
if (monacoEditor) monacoEditor.focus();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// File browser functions
|
|
993
|
+
async function loadFiles(path = '') {
|
|
994
|
+
try {
|
|
995
|
+
const res = await fetch(`/api/files?path=${encodeURIComponent(path)}`);
|
|
996
|
+
const data = await res.json();
|
|
997
|
+
|
|
998
|
+
if (data.error) {
|
|
999
|
+
showToast(data.error, 'error');
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
currentBrowsePath = path;
|
|
1004
|
+
renderBreadcrumb(path);
|
|
1005
|
+
renderFileTree(data.items || []);
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
showToast('Failed to load files', 'error');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function renderBreadcrumb(path) {
|
|
1012
|
+
const container = document.getElementById('file-breadcrumb');
|
|
1013
|
+
const parts = path ? path.split('/').filter(Boolean) : [];
|
|
1014
|
+
|
|
1015
|
+
let html = '<span class="breadcrumb-item" onclick="navigateToPath(\'\')">~</span>';
|
|
1016
|
+
|
|
1017
|
+
let currentPath = '';
|
|
1018
|
+
parts.forEach((part, i) => {
|
|
1019
|
+
currentPath += (currentPath ? '/' : '') + part;
|
|
1020
|
+
const p = currentPath;
|
|
1021
|
+
html += `<span class="breadcrumb-sep">/</span>`;
|
|
1022
|
+
html += `<span class="breadcrumb-item" onclick="navigateToPath('${p}')">${part}</span>`;
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
container.innerHTML = html;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
let currentFileItems = []; // Store current items for click handling
|
|
1029
|
+
|
|
1030
|
+
function renderFileTree(items) {
|
|
1031
|
+
const container = document.getElementById('file-tree');
|
|
1032
|
+
currentFileItems = items;
|
|
1033
|
+
|
|
1034
|
+
if (items.length === 0) {
|
|
1035
|
+
container.innerHTML = '<div class="empty-state">Empty directory</div>';
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
container.innerHTML = items.map((item, index) => `
|
|
1040
|
+
<div class="file-item ${item.isDirectory ? 'directory' : 'file'} ${activeFilePath === item.path ? 'active' : ''}"
|
|
1041
|
+
data-index="${index}">
|
|
1042
|
+
<span class="file-item-icon">${item.isDirectory ? '📁' : '📄'}</span>
|
|
1043
|
+
<span class="file-item-name">${item.name}</span>
|
|
1044
|
+
</div>
|
|
1045
|
+
`).join('');
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Event delegation for file tree clicks
|
|
1049
|
+
document.getElementById('file-tree').addEventListener('click', (e) => {
|
|
1050
|
+
const fileItem = e.target.closest('.file-item');
|
|
1051
|
+
if (!fileItem) return;
|
|
1052
|
+
|
|
1053
|
+
const index = parseInt(fileItem.dataset.index);
|
|
1054
|
+
const item = currentFileItems[index];
|
|
1055
|
+
if (!item) return;
|
|
1056
|
+
|
|
1057
|
+
if (item.isDirectory) {
|
|
1058
|
+
navigateToPath(item.path);
|
|
1059
|
+
} else {
|
|
1060
|
+
openFile(item.path);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
function navigateToPath(path) {
|
|
1065
|
+
loadFiles(path);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// File operations
|
|
1069
|
+
async function openFile(path) {
|
|
1070
|
+
// Switch to editor tab
|
|
1071
|
+
switchTab('editor');
|
|
1072
|
+
|
|
1073
|
+
// Check if already open
|
|
1074
|
+
if (openFiles.has(path)) {
|
|
1075
|
+
activateFile(path);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
try {
|
|
1080
|
+
const res = await fetch(`/api/files/read?path=${encodeURIComponent(path)}`);
|
|
1081
|
+
const data = await res.json();
|
|
1082
|
+
|
|
1083
|
+
if (data.error) {
|
|
1084
|
+
showToast(data.error, 'error');
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Detect language
|
|
1089
|
+
const ext = path.split('.').pop().toLowerCase();
|
|
1090
|
+
const langMap = {
|
|
1091
|
+
js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
|
|
1092
|
+
py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java',
|
|
1093
|
+
c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp',
|
|
1094
|
+
html: 'html', css: 'css', scss: 'scss', less: 'less',
|
|
1095
|
+
json: 'json', xml: 'xml', yaml: 'yaml', yml: 'yaml',
|
|
1096
|
+
md: 'markdown', sql: 'sql', sh: 'shell', bash: 'shell',
|
|
1097
|
+
dockerfile: 'dockerfile', makefile: 'makefile'
|
|
1098
|
+
};
|
|
1099
|
+
const language = langMap[ext] || 'plaintext';
|
|
1100
|
+
|
|
1101
|
+
// Create model
|
|
1102
|
+
const model = monaco.editor.createModel(data.content, language);
|
|
1103
|
+
|
|
1104
|
+
openFiles.set(path, {
|
|
1105
|
+
content: data.content,
|
|
1106
|
+
originalContent: data.content,
|
|
1107
|
+
model,
|
|
1108
|
+
language
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
activateFile(path);
|
|
1112
|
+
showToast(`Opened ${path.split('/').pop()}`);
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
showToast('Failed to open file', 'error');
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function activateFile(path) {
|
|
1119
|
+
activeFilePath = path;
|
|
1120
|
+
const file = openFiles.get(path);
|
|
1121
|
+
|
|
1122
|
+
if (file && monacoEditor) {
|
|
1123
|
+
monacoEditor.setModel(file.model);
|
|
1124
|
+
document.getElementById('monaco-editor').style.display = 'block';
|
|
1125
|
+
document.getElementById('editor-empty').style.display = 'none';
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
renderEditorTabs();
|
|
1129
|
+
// Re-render file tree to update active state
|
|
1130
|
+
if (currentFileItems.length > 0) {
|
|
1131
|
+
renderFileTree(currentFileItems);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function renderEditorTabs() {
|
|
1136
|
+
const container = document.getElementById('editor-tabs');
|
|
1137
|
+
|
|
1138
|
+
if (openFiles.size === 0) {
|
|
1139
|
+
container.innerHTML = '';
|
|
1140
|
+
document.getElementById('monaco-editor').style.display = 'none';
|
|
1141
|
+
document.getElementById('editor-empty').style.display = 'flex';
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
container.innerHTML = Array.from(openFiles.entries()).map(([path, file]) => {
|
|
1146
|
+
const name = path.split('/').pop();
|
|
1147
|
+
const isModified = file.content !== file.originalContent;
|
|
1148
|
+
const isActive = path === activeFilePath;
|
|
1149
|
+
|
|
1150
|
+
return `
|
|
1151
|
+
<div class="editor-tab ${isActive ? 'active' : ''} ${isModified ? 'modified' : ''}" onclick="activateFile('${path}')">
|
|
1152
|
+
<span>${name}</span>
|
|
1153
|
+
<span class="editor-tab-close" onclick="event.stopPropagation(); closeFile('${path}')">x</span>
|
|
1154
|
+
</div>
|
|
1155
|
+
`;
|
|
1156
|
+
}).join('');
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async function saveFile(path) {
|
|
1160
|
+
const file = openFiles.get(path);
|
|
1161
|
+
if (!file) return;
|
|
1162
|
+
|
|
1163
|
+
try {
|
|
1164
|
+
const res = await fetch('/api/files/write', {
|
|
1165
|
+
method: 'POST',
|
|
1166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1167
|
+
body: JSON.stringify({ path, content: file.content })
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
const data = await res.json();
|
|
1171
|
+
|
|
1172
|
+
if (data.error) {
|
|
1173
|
+
showToast(data.error, 'error');
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
file.originalContent = file.content;
|
|
1178
|
+
renderEditorTabs();
|
|
1179
|
+
showToast(`Saved ${path.split('/').pop()}`);
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
showToast('Failed to save file', 'error');
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function closeFile(path) {
|
|
1186
|
+
const file = openFiles.get(path);
|
|
1187
|
+
if (!file) return;
|
|
1188
|
+
|
|
1189
|
+
// Check for unsaved changes
|
|
1190
|
+
if (file.content !== file.originalContent) {
|
|
1191
|
+
if (!confirm(`${path.split('/').pop()} has unsaved changes. Close anyway?`)) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
file.model.dispose();
|
|
1197
|
+
openFiles.delete(path);
|
|
1198
|
+
|
|
1199
|
+
if (activeFilePath === path) {
|
|
1200
|
+
const remaining = Array.from(openFiles.keys());
|
|
1201
|
+
if (remaining.length > 0) {
|
|
1202
|
+
activateFile(remaining[remaining.length - 1]);
|
|
1203
|
+
} else {
|
|
1204
|
+
activeFilePath = null;
|
|
1205
|
+
renderEditorTabs();
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
renderEditorTabs();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Export functions
|
|
1213
|
+
window.switchTab = switchTab;
|
|
1214
|
+
window.navigateToPath = navigateToPath;
|
|
1215
|
+
window.openFile = openFile;
|
|
1216
|
+
window.activateFile = activateFile;
|
|
1217
|
+
window.closeFile = closeFile;
|
|
1218
|
+
window.saveFile = saveFile;
|
|
1219
|
+
|
|
527
1220
|
// Initialize xterm.js
|
|
528
1221
|
const term = new Terminal({
|
|
529
1222
|
cursorBlink: true,
|
|
@@ -580,33 +1273,119 @@
|
|
|
580
1273
|
return date.toLocaleTimeString();
|
|
581
1274
|
}
|
|
582
1275
|
|
|
583
|
-
function
|
|
584
|
-
const
|
|
585
|
-
|
|
1276
|
+
function formatUptime(seconds) {
|
|
1277
|
+
const days = Math.floor(seconds / 86400);
|
|
1278
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
1279
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
1280
|
+
if (days > 0) return `${days}d ${hours}h`;
|
|
1281
|
+
if (hours > 0) return `${hours}h ${mins}m`;
|
|
1282
|
+
return `${mins}m`;
|
|
1283
|
+
}
|
|
586
1284
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1285
|
+
function formatBytes(bytes) {
|
|
1286
|
+
const gb = bytes / (1024 * 1024 * 1024);
|
|
1287
|
+
return gb.toFixed(1) + ' GB';
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async function updateSystemInfo() {
|
|
1291
|
+
try {
|
|
1292
|
+
const res = await fetch('/api/system');
|
|
1293
|
+
const data = await res.json();
|
|
1294
|
+
|
|
1295
|
+
document.getElementById('system-hostname').textContent =
|
|
1296
|
+
`${data.hostname} (${data.platform}/${data.arch})`;
|
|
1297
|
+
|
|
1298
|
+
// CPU
|
|
1299
|
+
const cpuValue = document.getElementById('cpu-value');
|
|
1300
|
+
const cpuBar = document.getElementById('cpu-bar');
|
|
1301
|
+
cpuValue.textContent = `${data.cpu.usage}%`;
|
|
1302
|
+
cpuBar.style.width = `${data.cpu.usage}%`;
|
|
1303
|
+
cpuBar.className = `stat-bar-fill ${data.cpu.usage > 80 ? 'high' : 'cpu'}`;
|
|
1304
|
+
|
|
1305
|
+
// Memory
|
|
1306
|
+
const memValue = document.getElementById('mem-value');
|
|
1307
|
+
const memBar = document.getElementById('mem-bar');
|
|
1308
|
+
memValue.textContent = `${data.memory.usage}% (${formatBytes(data.memory.used)}/${formatBytes(data.memory.total)})`;
|
|
1309
|
+
memBar.style.width = `${data.memory.usage}%`;
|
|
1310
|
+
memBar.className = `stat-bar-fill ${data.memory.usage > 80 ? 'high' : 'memory'}`;
|
|
1311
|
+
|
|
1312
|
+
// Uptime
|
|
1313
|
+
document.getElementById('uptime-value').textContent = formatUptime(data.uptime);
|
|
1314
|
+
|
|
1315
|
+
// Load average
|
|
1316
|
+
document.getElementById('load-value').textContent =
|
|
1317
|
+
data.loadavg.map(l => l.toFixed(2)).join(' ');
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
console.error('Failed to fetch system info:', err);
|
|
590
1320
|
}
|
|
1321
|
+
}
|
|
591
1322
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1323
|
+
// Update system info every 3 seconds
|
|
1324
|
+
updateSystemInfo();
|
|
1325
|
+
setInterval(updateSystemInfo, 3000);
|
|
1326
|
+
|
|
1327
|
+
function renderTerminalTabs() {
|
|
1328
|
+
const container = document.getElementById('terminal-tabs');
|
|
1329
|
+
|
|
1330
|
+
// Build tabs HTML
|
|
1331
|
+
let tabsHtml = '';
|
|
1332
|
+
|
|
1333
|
+
// Tmux sessions first
|
|
1334
|
+
tmuxSessions.forEach(session => {
|
|
1335
|
+
const isActive = currentSessionId && sessionIdDisplay.textContent === `tmux:${session.name}`;
|
|
1336
|
+
tabsHtml += `
|
|
1337
|
+
<div class="terminal-tab ${isActive ? 'active' : ''}" onclick="attachToTmux('${session.name}')">
|
|
1338
|
+
<span class="tmux-badge">tmux</span>
|
|
1339
|
+
<span>${session.name}</span>
|
|
602
1340
|
</div>
|
|
603
|
-
|
|
604
|
-
|
|
1341
|
+
`;
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
// Shell sessions
|
|
1345
|
+
sessions.forEach(session => {
|
|
1346
|
+
const isActive = session.id === currentSessionId;
|
|
1347
|
+
tabsHtml += `
|
|
1348
|
+
<div class="terminal-tab ${isActive ? 'active' : ''}" onclick="attachToSession('${session.id}')">
|
|
1349
|
+
<span>${session.id.slice(0, 8)}</span>
|
|
1350
|
+
<span class="terminal-tab-close" onclick="event.stopPropagation(); killSession('${session.id}')">×</span>
|
|
605
1351
|
</div>
|
|
606
|
-
|
|
607
|
-
|
|
1352
|
+
`;
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// Add new button at the end
|
|
1356
|
+
tabsHtml += `<button class="terminal-tab-new" onclick="createNewSession()" title="New Shell">+</button>`;
|
|
1357
|
+
tabsHtml += `<button class="terminal-tab-new" onclick="refreshTmuxSessions()" title="Refresh tmux" style="font-size: 12px;">↻</button>`;
|
|
1358
|
+
|
|
1359
|
+
container.innerHTML = tabsHtml;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function renderSessions() {
|
|
1363
|
+
renderTerminalTabs();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function renderTmuxSessions() {
|
|
1367
|
+
renderTerminalTabs();
|
|
608
1368
|
}
|
|
609
1369
|
|
|
1370
|
+
function attachToTmux(sessionName) {
|
|
1371
|
+
// Detach from current session first
|
|
1372
|
+
if (currentSessionId) {
|
|
1373
|
+
ws.send(JSON.stringify({ type: 'detach' }));
|
|
1374
|
+
}
|
|
1375
|
+
currentSessionId = null;
|
|
1376
|
+
localStorage.removeItem('devtunnel-session-id');
|
|
1377
|
+
term.clear();
|
|
1378
|
+
ws.send(JSON.stringify({ type: 'attach-tmux', tmuxSession: sessionName }));
|
|
1379
|
+
switchTab('terminal');
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function refreshTmuxSessions() {
|
|
1383
|
+
ws.send(JSON.stringify({ type: 'refresh-tmux' }));
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
window.attachToTmux = attachToTmux;
|
|
1387
|
+
window.refreshTmuxSessions = refreshTmuxSessions;
|
|
1388
|
+
|
|
610
1389
|
function renderTunnels() {
|
|
611
1390
|
const list = document.getElementById('tunnel-list');
|
|
612
1391
|
document.getElementById('tunnel-count').textContent = tunnels.length;
|
|
@@ -665,18 +1444,28 @@
|
|
|
665
1444
|
}
|
|
666
1445
|
|
|
667
1446
|
function createNewSession() {
|
|
1447
|
+
// Detach from current session first
|
|
1448
|
+
if (currentSessionId) {
|
|
1449
|
+
ws.send(JSON.stringify({ type: 'detach' }));
|
|
1450
|
+
}
|
|
668
1451
|
currentSessionId = null;
|
|
669
1452
|
localStorage.removeItem('devtunnel-session-id');
|
|
670
1453
|
term.clear();
|
|
671
1454
|
ws.send(JSON.stringify({ type: 'attach', sessionId: null }));
|
|
1455
|
+
switchTab('terminal');
|
|
672
1456
|
}
|
|
673
1457
|
|
|
674
1458
|
function attachToSession(sessionId) {
|
|
675
1459
|
if (sessionId === currentSessionId) return;
|
|
1460
|
+
// Detach from current session first
|
|
1461
|
+
if (currentSessionId) {
|
|
1462
|
+
ws.send(JSON.stringify({ type: 'detach' }));
|
|
1463
|
+
}
|
|
676
1464
|
currentSessionId = sessionId;
|
|
677
1465
|
localStorage.setItem('devtunnel-session-id', sessionId);
|
|
678
1466
|
term.clear();
|
|
679
1467
|
ws.send(JSON.stringify({ type: 'attach', sessionId }));
|
|
1468
|
+
switchTab('terminal');
|
|
680
1469
|
}
|
|
681
1470
|
|
|
682
1471
|
function killSession(sessionId) {
|
|
@@ -705,8 +1494,8 @@
|
|
|
705
1494
|
statusText.textContent = 'Connected';
|
|
706
1495
|
reconnectOverlay.classList.remove('show');
|
|
707
1496
|
|
|
708
|
-
//
|
|
709
|
-
|
|
1497
|
+
// Don't auto-attach - let user choose session manually
|
|
1498
|
+
term.write('\x1b[90mClick + to create a new session.\x1b[0m\r\n');
|
|
710
1499
|
};
|
|
711
1500
|
|
|
712
1501
|
ws.onmessage = (event) => {
|
|
@@ -718,7 +1507,7 @@
|
|
|
718
1507
|
currentSessionId = msg.sessionId;
|
|
719
1508
|
localStorage.setItem('devtunnel-session-id', msg.sessionId);
|
|
720
1509
|
sessionIndicator.style.display = 'flex';
|
|
721
|
-
sessionIdDisplay.textContent = msg.sessionId.slice(0, 8);
|
|
1510
|
+
sessionIdDisplay.textContent = msg.tmuxSession ? `tmux:${msg.tmuxSession}` : msg.sessionId.slice(0, 8);
|
|
722
1511
|
|
|
723
1512
|
// Send initial size
|
|
724
1513
|
ws.send(JSON.stringify({
|
|
@@ -727,7 +1516,10 @@
|
|
|
727
1516
|
rows: term.rows
|
|
728
1517
|
}));
|
|
729
1518
|
|
|
730
|
-
|
|
1519
|
+
// Update terminal tabs to show active state
|
|
1520
|
+
renderTerminalTabs();
|
|
1521
|
+
|
|
1522
|
+
showToast(msg.tmuxSession ? `Attached to tmux: ${msg.tmuxSession}` : 'Session attached');
|
|
731
1523
|
break;
|
|
732
1524
|
|
|
733
1525
|
case 'output':
|
|
@@ -748,6 +1540,11 @@
|
|
|
748
1540
|
renderTunnels();
|
|
749
1541
|
break;
|
|
750
1542
|
|
|
1543
|
+
case 'tmux-sessions':
|
|
1544
|
+
tmuxSessions = msg.data;
|
|
1545
|
+
renderTmuxSessions();
|
|
1546
|
+
break;
|
|
1547
|
+
|
|
751
1548
|
case 'tunnel-created':
|
|
752
1549
|
showToast(`Tunnel created for port ${msg.data.port}`);
|
|
753
1550
|
createTunnelBtn.disabled = false;
|
|
@@ -809,6 +1606,7 @@
|
|
|
809
1606
|
|
|
810
1607
|
connect();
|
|
811
1608
|
term.focus();
|
|
1609
|
+
loadFiles(); // Load initial file list
|
|
812
1610
|
</script>
|
|
813
1611
|
</body>
|
|
814
1612
|
</html>
|