nikcli-remote 1.0.7 → 1.0.9

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/dist/index.cjs CHANGED
@@ -5503,292 +5503,649 @@ function getWebClient() {
5503
5503
  <html lang="en">
5504
5504
  <head>
5505
5505
  <meta charset="UTF-8">
5506
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
5506
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
5507
5507
  <meta name="apple-mobile-web-app-capable" content="yes">
5508
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
5508
+ <meta name="mobile-web-app-capable" content="yes">
5509
+ <meta name="theme-color" content="#0d1117">
5509
5510
  <title>NikCLI Remote</title>
5510
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
5511
5511
  <style>
5512
- * { box-sizing: border-box; margin: 0; padding: 0; }
5513
5512
  :root {
5514
- --bg-primary: #0d1117;
5513
+ --bg: #0d1117;
5515
5514
  --bg-secondary: #161b22;
5515
+ --fg: #e6edf3;
5516
+ --fg-muted: #8b949e;
5516
5517
  --accent: #58a6ff;
5517
5518
  --success: #3fb950;
5519
+ --warning: #d29922;
5520
+ --error: #f85149;
5518
5521
  --border: #30363d;
5522
+ --font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
5519
5523
  }
5520
- html, body { height: 100%; overflow: hidden; touch-action: manipulation; }
5521
- body {
5522
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
5523
- background: var(--bg-primary);
5524
- color: #e6edf3;
5524
+
5525
+ * {
5526
+ box-sizing: border-box;
5527
+ margin: 0;
5528
+ padding: 0;
5529
+ -webkit-tap-highlight-color: transparent;
5530
+ }
5531
+
5532
+ html, body {
5533
+ height: 100%;
5534
+ background: var(--bg);
5535
+ color: var(--fg);
5536
+ font-family: var(--font-mono);
5537
+ font-size: 14px;
5538
+ overflow: hidden;
5539
+ touch-action: manipulation;
5540
+ }
5541
+
5542
+ #app {
5525
5543
  display: flex;
5526
5544
  flex-direction: column;
5545
+ height: 100%;
5546
+ height: 100dvh;
5527
5547
  }
5528
- #terminal { flex: 1; overflow: hidden; background: var(--bg-primary); padding: 8px; }
5529
- #input-area {
5530
- background: var(--bg-secondary);
5531
- border-top: 1px solid var(--border);
5548
+
5549
+ /* Header */
5550
+ #header {
5551
+ display: flex;
5552
+ align-items: center;
5553
+ justify-content: space-between;
5532
5554
  padding: 12px 16px;
5555
+ background: var(--bg-secondary);
5556
+ border-bottom: 1px solid var(--border);
5533
5557
  flex-shrink: 0;
5534
- padding-bottom: env(safe-area-inset-bottom, 12px);
5535
5558
  }
5536
- .input-row { display: flex; gap: 8px; align-items: center; }
5537
- .prompt { color: var(--success); font-family: 'SF Mono', Monaco, monospace; font-size: 14px; white-space: nowrap; }
5538
- #cmd-input {
5559
+
5560
+ #header h1 {
5561
+ font-size: 16px;
5562
+ font-weight: 600;
5563
+ color: var(--accent);
5564
+ display: flex;
5565
+ align-items: center;
5566
+ gap: 8px;
5567
+ }
5568
+
5569
+ #header h1::before {
5570
+ content: '';
5571
+ width: 10px;
5572
+ height: 10px;
5573
+ background: var(--accent);
5574
+ border-radius: 2px;
5575
+ }
5576
+
5577
+ #status {
5578
+ display: flex;
5579
+ align-items: center;
5580
+ gap: 6px;
5581
+ font-size: 12px;
5582
+ color: var(--fg-muted);
5583
+ }
5584
+
5585
+ #status-dot {
5586
+ width: 8px;
5587
+ height: 8px;
5588
+ border-radius: 50%;
5589
+ background: var(--error);
5590
+ transition: background 0.3s;
5591
+ }
5592
+
5593
+ #status-dot.connected {
5594
+ background: var(--success);
5595
+ }
5596
+
5597
+ #status-dot.connecting {
5598
+ background: var(--warning);
5599
+ animation: pulse 1s infinite;
5600
+ }
5601
+
5602
+ @keyframes pulse {
5603
+ 0%, 100% { opacity: 1; }
5604
+ 50% { opacity: 0.5; }
5605
+ }
5606
+
5607
+ /* Terminal */
5608
+ #terminal-container {
5539
5609
  flex: 1;
5540
- background: #21262d;
5610
+ overflow: hidden;
5611
+ position: relative;
5612
+ }
5613
+
5614
+ #terminal {
5615
+ height: 100%;
5616
+ padding: 12px;
5617
+ overflow-y: auto;
5618
+ overflow-x: hidden;
5619
+ font-size: 13px;
5620
+ line-height: 1.5;
5621
+ white-space: pre-wrap;
5622
+ word-break: break-all;
5623
+ -webkit-overflow-scrolling: touch;
5624
+ }
5625
+
5626
+ #terminal::-webkit-scrollbar {
5627
+ width: 6px;
5628
+ }
5629
+
5630
+ #terminal::-webkit-scrollbar-track {
5631
+ background: var(--bg);
5632
+ }
5633
+
5634
+ #terminal::-webkit-scrollbar-thumb {
5635
+ background: var(--border);
5636
+ border-radius: 3px;
5637
+ }
5638
+
5639
+ .cursor {
5640
+ display: inline-block;
5641
+ width: 8px;
5642
+ height: 16px;
5643
+ background: var(--fg);
5644
+ animation: blink 1s step-end infinite;
5645
+ vertical-align: text-bottom;
5646
+ }
5647
+
5648
+ @keyframes blink {
5649
+ 50% { opacity: 0; }
5650
+ }
5651
+
5652
+ /* Notifications */
5653
+ #notifications {
5654
+ position: fixed;
5655
+ top: 60px;
5656
+ left: 12px;
5657
+ right: 12px;
5658
+ z-index: 1000;
5659
+ pointer-events: none;
5660
+ }
5661
+
5662
+ .notification {
5663
+ background: var(--bg-secondary);
5541
5664
  border: 1px solid var(--border);
5542
- border-radius: 12px;
5665
+ border-radius: 8px;
5543
5666
  padding: 12px 16px;
5544
- color: #e6edf3;
5667
+ margin-bottom: 8px;
5668
+ animation: slideIn 0.3s ease;
5669
+ pointer-events: auto;
5670
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
5671
+ }
5672
+
5673
+ .notification.success { border-left: 3px solid var(--success); }
5674
+ .notification.error { border-left: 3px solid var(--error); }
5675
+ .notification.warning { border-left: 3px solid var(--warning); }
5676
+ .notification.info { border-left: 3px solid var(--accent); }
5677
+
5678
+ .notification h4 {
5679
+ font-size: 14px;
5680
+ font-weight: 600;
5681
+ margin-bottom: 4px;
5682
+ }
5683
+
5684
+ .notification p {
5685
+ font-size: 12px;
5686
+ color: var(--fg-muted);
5687
+ }
5688
+
5689
+ @keyframes slideIn {
5690
+ from { transform: translateY(-20px); opacity: 0; }
5691
+ to { transform: translateY(0); opacity: 1; }
5692
+ }
5693
+
5694
+ /* Quick Keys */
5695
+ #quickkeys {
5696
+ display: grid;
5697
+ grid-template-columns: repeat(6, 1fr);
5698
+ gap: 6px;
5699
+ padding: 8px 12px;
5700
+ background: var(--bg-secondary);
5701
+ border-top: 1px solid var(--border);
5702
+ flex-shrink: 0;
5703
+ }
5704
+
5705
+ .qkey {
5706
+ background: var(--bg);
5707
+ border: 1px solid var(--border);
5708
+ border-radius: 6px;
5709
+ padding: 10px 4px;
5710
+ color: var(--fg);
5711
+ font-size: 11px;
5712
+ font-family: var(--font-mono);
5713
+ text-align: center;
5714
+ cursor: pointer;
5715
+ user-select: none;
5716
+ transition: background 0.1s, transform 0.1s;
5717
+ }
5718
+
5719
+ .qkey:active {
5720
+ background: var(--border);
5721
+ transform: scale(0.95);
5722
+ }
5723
+
5724
+ .qkey.wide {
5725
+ grid-column: span 2;
5726
+ }
5727
+
5728
+ .qkey.accent {
5729
+ background: var(--accent);
5730
+ border-color: var(--accent);
5731
+ color: #fff;
5732
+ }
5733
+
5734
+ /* Input */
5735
+ #input-container {
5736
+ padding: 12px;
5737
+ background: var(--bg-secondary);
5738
+ border-top: 1px solid var(--border);
5739
+ flex-shrink: 0;
5740
+ }
5741
+
5742
+ #input-row {
5743
+ display: flex;
5744
+ gap: 8px;
5745
+ }
5746
+
5747
+ #input {
5748
+ flex: 1;
5749
+ background: var(--bg);
5750
+ border: 1px solid var(--border);
5751
+ border-radius: 8px;
5752
+ padding: 12px 14px;
5753
+ color: var(--fg);
5754
+ font-family: var(--font-mono);
5545
5755
  font-size: 16px;
5546
- font-family: 'SF Mono', Monaco, monospace;
5547
5756
  outline: none;
5548
- -webkit-appearance: none;
5757
+ transition: border-color 0.2s;
5758
+ }
5759
+
5760
+ #input:focus {
5761
+ border-color: var(--accent);
5549
5762
  }
5550
- #cmd-input:focus { border-color: var(--accent); }
5551
- #send-btn {
5763
+
5764
+ #input::placeholder {
5765
+ color: var(--fg-muted);
5766
+ }
5767
+
5768
+ #send {
5552
5769
  background: var(--accent);
5553
- color: white;
5770
+ color: #fff;
5554
5771
  border: none;
5555
- border-radius: 12px;
5556
- padding: 12px 24px;
5557
- font-size: 16px;
5772
+ border-radius: 8px;
5773
+ padding: 12px 20px;
5774
+ font-size: 14px;
5558
5775
  font-weight: 600;
5776
+ font-family: var(--font-mono);
5559
5777
  cursor: pointer;
5560
- -webkit-tap-highlight-color: transparent;
5778
+ transition: opacity 0.2s, transform 0.1s;
5561
5779
  }
5562
- #send-btn:active { opacity: 0.7; }
5563
- #status-bar {
5564
- background: var(--bg-secondary);
5565
- border-bottom: 1px solid var(--border);
5566
- padding: 8px 16px;
5780
+
5781
+ #send:active {
5782
+ opacity: 0.8;
5783
+ transform: scale(0.98);
5784
+ }
5785
+
5786
+ /* Auth Screen */
5787
+ #auth-screen {
5788
+ position: fixed;
5789
+ inset: 0;
5790
+ background: var(--bg);
5567
5791
  display: flex;
5568
- justify-content: space-between;
5792
+ flex-direction: column;
5569
5793
  align-items: center;
5570
- font-size: 12px;
5571
- padding-top: env(safe-area-inset-top, 8px);
5572
- }
5573
- .status-row { display: flex; align-items: center; gap: 8px; }
5574
- .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #8b949e; }
5575
- .status-dot.connected { background: var(--success); box-shadow: 0 0 8px var(--success); }
5576
- .status-dot.connecting { background: var(--accent); animation: pulse 1s infinite; }
5577
- .status-dot.disconnected { background: #f85149; }
5578
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
5579
- #auth-overlay {
5580
- position: fixed; inset: 0; background: var(--bg-primary);
5581
- display: flex; flex-direction: column; align-items: center; justify-content: center;
5582
- padding: 20px; z-index: 100;
5583
- }
5584
- #auth-overlay.hidden { display: none; }
5585
- .auth-title { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
5586
- .auth-subtitle { color: #8b949e; font-size: 16px; margin-bottom: 24px; }
5587
- .auth-msg {
5588
- background: var(--bg-secondary); padding: 20px 28px;
5589
- border-radius: 16px; border: 1px solid var(--border); text-align: center;
5590
- }
5591
- .auth-msg.error { color: #f85149; border-color: #f85149; }
5592
- .quick-btns {
5593
- display: flex; gap: 8px; margin-top: 20px; flex-wrap: wrap; justify-content: center;
5594
- }
5595
- .quick-btn {
5596
- background: #21262d; border: 1px solid var(--border); color: #e6edf3;
5597
- padding: 10px 16px; border-radius: 8px; font-size: 14px;
5598
- font-family: 'SF Mono', Monaco, monospace; cursor: pointer;
5599
- }
5600
- .quick-btn:active { background: #30363d; }
5601
- .hint { font-size: 12px; color: #8b949e; margin-top: 12px; }
5602
- @media (max-width: 600px) {
5603
- #input-area { padding: 10px 12px; }
5604
- .quick-btn { padding: 8px 12px; font-size: 12px; }
5794
+ justify-content: center;
5795
+ gap: 20px;
5796
+ z-index: 2000;
5797
+ }
5798
+
5799
+ #auth-screen.hidden {
5800
+ display: none;
5801
+ }
5802
+
5803
+ .spinner {
5804
+ width: 40px;
5805
+ height: 40px;
5806
+ border: 3px solid var(--border);
5807
+ border-top-color: var(--accent);
5808
+ border-radius: 50%;
5809
+ animation: spin 1s linear infinite;
5810
+ }
5811
+
5812
+ @keyframes spin {
5813
+ to { transform: rotate(360deg); }
5814
+ }
5815
+
5816
+ #auth-screen p {
5817
+ color: var(--fg-muted);
5818
+ font-size: 14px;
5819
+ }
5820
+
5821
+ #auth-screen .error {
5822
+ color: var(--error);
5823
+ }
5824
+
5825
+ /* Safe area padding for notched devices */
5826
+ @supports (padding: env(safe-area-inset-bottom)) {
5827
+ #input-container {
5828
+ padding-bottom: calc(12px + env(safe-area-inset-bottom));
5829
+ }
5830
+ }
5831
+
5832
+ /* Landscape adjustments */
5833
+ @media (max-height: 500px) {
5834
+ #quickkeys {
5835
+ grid-template-columns: repeat(12, 1fr);
5836
+ padding: 6px 8px;
5837
+ }
5838
+ .qkey {
5839
+ padding: 8px 2px;
5840
+ font-size: 10px;
5841
+ }
5842
+ #terminal {
5843
+ font-size: 12px;
5844
+ }
5605
5845
  }
5606
5846
  </style>
5607
5847
  </head>
5608
5848
  <body>
5609
- <div id="auth-overlay">
5610
- <div class="auth-title">\u{1F4F1} NikCLI Remote</div>
5611
- <div class="auth-subtitle">Full terminal emulation</div>
5612
- <div id="auth-msg" class="auth-msg">
5613
- <div id="auth-text">Connecting...</div>
5849
+ <div id="app">
5850
+ <div id="auth-screen">
5851
+ <div class="spinner"></div>
5852
+ <p id="auth-status">Connecting to NikCLI...</p>
5614
5853
  </div>
5615
- <div class="quick-btns">
5616
- <button class="quick-btn" onclick="send('help')">/help</button>
5617
- <button class="quick-btn" onclick="send('ls -la')">ls -la</button>
5618
- <button class="quick-btn" onclick="send('pwd')">pwd</button>
5619
- <button class="quick-btn" onclick="send('whoami')">whoami</button>
5620
- <button class="quick-btn" onclick="send('clear')">clear</button>
5621
- </div>
5622
- <div class="hint">Mobile keyboard to type commands</div>
5623
- </div>
5624
5854
 
5625
- <div id="status-bar">
5626
- <div class="status-row">
5627
- <span class="status-dot" id="status-dot"></span>
5628
- <span id="status-text">Disconnected</span>
5855
+ <header id="header">
5856
+ <h1>NikCLI Remote</h1>
5857
+ <div id="status">
5858
+ <span id="status-dot" class="connecting"></span>
5859
+ <span id="status-text">Connecting</span>
5860
+ </div>
5861
+ </header>
5862
+
5863
+ <div id="terminal-container">
5864
+ <div id="terminal"></div>
5629
5865
  </div>
5630
- <span id="session-id" style="color: #8b949e;"></span>
5631
- </div>
5632
5866
 
5633
- <div id="terminal"></div>
5867
+ <div id="notifications"></div>
5634
5868
 
5635
- <div id="input-area">
5636
- <form class="input-row" onsubmit="return handleSubmit(event)">
5637
- <span class="prompt">$</span>
5638
- <input type="text" id="cmd-input" placeholder="Type command..." autocomplete="off" enterkeyhint="send" inputmode="text">
5639
- <button type="submit" id="send-btn">Send</button>
5640
- </form>
5869
+ <div id="quickkeys">
5870
+ <button class="qkey" data-key="\\t">Tab</button>
5871
+ <button class="qkey" data-key="\\x1b[A">\u2191</button>
5872
+ <button class="qkey" data-key="\\x1b[B">\u2193</button>
5873
+ <button class="qkey" data-key="\\x1b[D">\u2190</button>
5874
+ <button class="qkey" data-key="\\x1b[C">\u2192</button>
5875
+ <button class="qkey" data-key="\\x1b">Esc</button>
5876
+ <button class="qkey" data-key="\\x03">^C</button>
5877
+ <button class="qkey" data-key="\\x04">^D</button>
5878
+ <button class="qkey" data-key="\\x1a">^Z</button>
5879
+ <button class="qkey" data-key="\\x0c">^L</button>
5880
+ <button class="qkey wide accent" data-key="\\r">Enter \u23CE</button>
5881
+ </div>
5882
+
5883
+ <div id="input-container">
5884
+ <div id="input-row">
5885
+ <input type="text" id="input" placeholder="Type command..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
5886
+ <button id="send">Send</button>
5887
+ </div>
5888
+ </div>
5641
5889
  </div>
5642
5890
 
5643
- <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
5644
- <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
5645
5891
  <script>
5646
- let ws = null, term = null, fitAddon = null, connected = false, reconnectAttempts = 0;
5647
- const token = new URLSearchParams(location.search).get('t') || '';
5648
- const sessionId = new URLSearchParams(location.search).get('s') || '';
5649
-
5650
- const authOverlay = document.getElementById('auth-overlay');
5651
- const authMsg = document.getElementById('auth-msg');
5652
- const authText = document.getElementById('auth-text');
5653
- const statusDot = document.getElementById('status-dot');
5654
- const statusText = document.getElementById('status-text');
5655
- const sessionSpan = document.getElementById('session-id');
5656
- const cmdInput = document.getElementById('cmd-input');
5657
-
5658
- // Initialize xterm.js
5659
- term = new Terminal({
5660
- cursorBlink: true,
5661
- fontSize: 14,
5662
- fontFamily: '"SF Mono", Monaco, Consolas, monospace',
5663
- theme: {
5664
- background: '#0d1117',
5665
- foreground: '#e6edf3',
5666
- cursor: '#3fb950',
5667
- selectionBackground: '#264f78',
5668
- black: '#484f58',
5669
- red: '#f85149',
5670
- green: '#3fb950',
5671
- yellow: '#d29922',
5672
- blue: '#58a6ff',
5673
- magenta: '#a371f7',
5674
- cyan: '#39c5cf',
5675
- white: '#e6edf3',
5676
- brightBlack: '#6e7681',
5677
- brightRed: '#ffa198',
5678
- brightGreen: '#7ee787',
5679
- brightYellow: '#f0883e',
5680
- brightBlue: '#79c0ff',
5681
- brightMagenta: '#d2a8ff',
5682
- brightCyan: '#56d4db',
5683
- brightWhite: '#f0f6fc'
5684
- },
5685
- convertEol: true
5686
- });
5892
+ (function() {
5893
+ 'use strict';
5687
5894
 
5688
- fitAddon = new FitAddon.FitAddon();
5689
- term.loadAddon(fitAddon);
5690
- term.open(document.getElementById('terminal'));
5691
- fitAddon.fit();
5692
- term.writeln('Initializing NikCLI Remote...);
5693
- term.writeln('Type commands below');
5694
-
5695
- // Resize handler
5696
- window.addEventListener('resize', () => {
5697
- clearTimeout(window.resizeTimer);
5698
- window.resizeTimer = setTimeout(() => fitAddon.fit(), 100);
5699
- });
5895
+ // Parse URL params
5896
+ const params = new URLSearchParams(location.search);
5897
+ const token = params.get('t');
5898
+ const sessionId = params.get('s');
5700
5899
 
5701
- // Visual viewport for mobile keyboard
5702
- if (window.visualViewport) {
5703
- window.visualViewport.addEventListener('resize', () => {
5704
- clearTimeout(window.resizeTimer);
5705
- window.resizeTimer = setTimeout(() => fitAddon.fit(), 100);
5706
- });
5707
- }
5900
+ // DOM elements
5901
+ const terminal = document.getElementById('terminal');
5902
+ const input = document.getElementById('input');
5903
+ const sendBtn = document.getElementById('send');
5904
+ const statusDot = document.getElementById('status-dot');
5905
+ const statusText = document.getElementById('status-text');
5906
+ const authScreen = document.getElementById('auth-screen');
5907
+ const authStatus = document.getElementById('auth-status');
5908
+ const notifications = document.getElementById('notifications');
5708
5909
 
5709
- function connect() {
5710
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
5711
- ws = new WebSocket(protocol + '//' + location.host + '/ws');
5910
+ // State
5911
+ let ws = null;
5912
+ let reconnectAttempts = 0;
5913
+ const maxReconnectAttempts = 5;
5914
+ let terminalEnabled = true;
5712
5915
 
5713
- ws.onopen = () => {
5714
- setStatus('connecting', 'Authenticating...');
5715
- ws.send(JSON.stringify({ type: 'auth', token }));
5716
- reconnectAttempts = 0;
5717
- };
5916
+ // Connect to WebSocket
5917
+ function connect() {
5918
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
5919
+ ws = new WebSocket(protocol + '//' + location.host);
5718
5920
 
5719
- ws.onmessage = (e) => {
5720
- try { handleMessage(JSON.parse(e.data)); } catch (err) { console.error(err); }
5721
- };
5921
+ ws.onopen = function() {
5922
+ setStatus('connecting', 'Authenticating...');
5923
+ ws.send(JSON.stringify({ type: 'auth', token: token }));
5924
+ };
5722
5925
 
5723
- ws.onclose = () => {
5724
- setStatus('disconnected', 'Disconnected');
5725
- connected = false;
5726
- reconnectAttempts++;
5727
- setTimeout(connect, Math.min(3000, reconnectAttempts * 500));
5728
- };
5926
+ ws.onmessage = function(event) {
5927
+ try {
5928
+ const msg = JSON.parse(event.data);
5929
+ handleMessage(msg);
5930
+ } catch (e) {
5931
+ console.error('Parse error:', e);
5932
+ }
5933
+ };
5729
5934
 
5730
- ws.onerror = () => setStatus('disconnected', 'Connection error');
5731
- }
5935
+ ws.onclose = function() {
5936
+ setStatus('disconnected', 'Disconnected');
5937
+ if (reconnectAttempts < maxReconnectAttempts) {
5938
+ reconnectAttempts++;
5939
+ const delay = Math.min(2000 * reconnectAttempts, 10000);
5940
+ setTimeout(connect, delay);
5941
+ } else {
5942
+ authStatus.textContent = 'Connection failed. Refresh to retry.';
5943
+ authStatus.classList.add('error');
5944
+ authScreen.classList.remove('hidden');
5945
+ }
5946
+ };
5732
5947
 
5733
- function handleMessage(msg) {
5734
- switch (msg.type) {
5735
- case 'auth:required':
5736
- ws.send(JSON.stringify({ type: 'auth', token }));
5737
- break;
5738
- case 'auth:success':
5739
- connected = true;
5740
- authOverlay.classList.add('hidden');
5741
- setStatus('connected', 'Connected');
5742
- sessionSpan.textContent = sessionId ? 'Session: ' + sessionId : '';
5743
- term.writeln('\u2713 Connected to NikCLI');
5744
- break;
5745
- case 'auth:failed':
5746
- authMsg.classList.add('error');
5747
- authText.textContent = 'Authentication failed';
5748
- break;
5749
- case 'terminal:output':
5750
- if (msg.payload?.data) term.write(msg.payload.data);
5751
- break;
5752
- case 'terminal:exit':
5753
- term.writeln('Process exited: ' + (msg.payload?.code || 0) + '');
5754
- break;
5948
+ ws.onerror = function() {
5949
+ console.error('WebSocket error');
5950
+ };
5755
5951
  }
5756
- }
5757
5952
 
5758
- function setStatus(state, text) {
5759
- statusDot.className = 'status-dot ' + state;
5760
- statusText.textContent = text;
5761
- }
5953
+ // Handle incoming message
5954
+ function handleMessage(msg) {
5955
+ switch (msg.type) {
5956
+ case 'auth:required':
5957
+ // Already sent auth on open
5958
+ break;
5762
5959
 
5763
- function handleSubmit(e) {
5764
- if (e) e.preventDefault();
5765
- const value = cmdInput.value.trim();
5766
- if (!value || !connected) return;
5767
- cmdInput.value = '';
5768
- term.write('$ ' + value );
5769
- ws.send(JSON.stringify({ type: 'terminal:input', data: value + '\r' }));
5770
- setTimeout(() => cmdInput.focus(), 50);
5771
- return false;
5772
- }
5960
+ case 'auth:success':
5961
+ authScreen.classList.add('hidden');
5962
+ setStatus('connected', 'Connected');
5963
+ reconnectAttempts = 0;
5964
+ terminalEnabled = msg.payload?.terminalEnabled !== false;
5965
+ if (terminalEnabled) {
5966
+ appendOutput('\\x1b[32mConnected to NikCLI\\x1b[0m\\n\\n');
5967
+ }
5968
+ break;
5773
5969
 
5774
- function send(cmd) {
5775
- if (!connected) return;
5776
- term.write('$ ' + cmd + );
5777
- ws.send(JSON.stringify({ type: 'terminal:input', data: cmd + '\r' }));
5778
- }
5970
+ case 'auth:failed':
5971
+ authStatus.textContent = 'Authentication failed';
5972
+ authStatus.classList.add('error');
5973
+ break;
5974
+
5975
+ case 'terminal:output':
5976
+ if (msg.payload?.data) {
5977
+ appendOutput(msg.payload.data);
5978
+ }
5979
+ break;
5980
+
5981
+ case 'terminal:exit':
5982
+ appendOutput('\\n\\x1b[33m[Process exited with code ' + (msg.payload?.code || 0) + ']\\x1b[0m\\n');
5983
+ break;
5984
+
5985
+ case 'notification':
5986
+ showNotification(msg.payload);
5987
+ break;
5988
+
5989
+ case 'session:end':
5990
+ appendOutput('\\n\\x1b[31m[Session ended]\\x1b[0m\\n');
5991
+ setStatus('disconnected', 'Session ended');
5992
+ break;
5779
5993
 
5780
- cmdInput.addEventListener('keydown', (e) => {
5781
- if (e.key === 'Enter' && !e.shiftKey) {
5782
- e.preventDefault();
5783
- handleSubmit();
5994
+ case 'pong':
5995
+ // Heartbeat response
5996
+ break;
5997
+
5998
+ default:
5999
+ console.log('Unknown message:', msg.type);
6000
+ }
5784
6001
  }
5785
- });
5786
6002
 
5787
- document.getElementById('terminal')?.addEventListener('click', () => {
5788
- if (connected) cmdInput.focus();
5789
- });
6003
+ // Append text to terminal with ANSI support
6004
+ function appendOutput(text) {
6005
+ // Simple ANSI to HTML conversion
6006
+ const html = ansiToHtml(text);
6007
+ terminal.innerHTML += html;
6008
+ terminal.scrollTop = terminal.scrollHeight;
6009
+ }
6010
+
6011
+ // Basic ANSI to HTML
6012
+ function ansiToHtml(text) {
6013
+ const ansiColors = {
6014
+ '30': '#6e7681', '31': '#f85149', '32': '#3fb950', '33': '#d29922',
6015
+ '34': '#58a6ff', '35': '#bc8cff', '36': '#76e3ea', '37': '#e6edf3',
6016
+ '90': '#6e7681', '91': '#f85149', '92': '#3fb950', '93': '#d29922',
6017
+ '94': '#58a6ff', '95': '#bc8cff', '96': '#76e3ea', '97': '#ffffff'
6018
+ };
6019
+
6020
+ let result = '';
6021
+ let currentStyle = '';
6022
+
6023
+ const parts = text.split(/\\x1b\\[([0-9;]+)m/);
6024
+ for (let i = 0; i < parts.length; i++) {
6025
+ if (i % 2 === 0) {
6026
+ // Text content
6027
+ result += escapeHtml(parts[i]);
6028
+ } else {
6029
+ // ANSI code
6030
+ const codes = parts[i].split(';');
6031
+ for (const code of codes) {
6032
+ if (code === '0') {
6033
+ if (currentStyle) {
6034
+ result += '</span>';
6035
+ currentStyle = '';
6036
+ }
6037
+ } else if (code === '1') {
6038
+ currentStyle = 'font-weight:bold;';
6039
+ result += '<span style="' + currentStyle + '">';
6040
+ } else if (ansiColors[code]) {
6041
+ if (currentStyle) result += '</span>';
6042
+ currentStyle = 'color:' + ansiColors[code] + ';';
6043
+ result += '<span style="' + currentStyle + '">';
6044
+ }
6045
+ }
6046
+ }
6047
+ }
6048
+
6049
+ if (currentStyle) result += '</span>';
6050
+ return result;
6051
+ }
6052
+
6053
+ // Escape HTML
6054
+ function escapeHtml(text) {
6055
+ return text
6056
+ .replace(/&/g, '&amp;')
6057
+ .replace(/</g, '&lt;')
6058
+ .replace(/>/g, '&gt;')
6059
+ .replace(/"/g, '&quot;')
6060
+ .replace(/\\n/g, '<br>')
6061
+ .replace(/ /g, '&nbsp;');
6062
+ }
5790
6063
 
5791
- connect();
6064
+ // Set connection status
6065
+ function setStatus(state, text) {
6066
+ statusDot.className = state === 'connected' ? 'connected' :
6067
+ state === 'connecting' ? 'connecting' : '';
6068
+ statusText.textContent = text;
6069
+ }
6070
+
6071
+ // Send data to terminal
6072
+ function send(data) {
6073
+ if (ws && ws.readyState === WebSocket.OPEN) {
6074
+ ws.send(JSON.stringify({ type: 'terminal:input', data: data }));
6075
+ }
6076
+ }
6077
+
6078
+ // Show notification
6079
+ function showNotification(n) {
6080
+ if (!n) return;
6081
+ const el = document.createElement('div');
6082
+ el.className = 'notification ' + (n.type || 'info');
6083
+ el.innerHTML = '<h4>' + escapeHtml(n.title || 'Notification') + '</h4>' +
6084
+ '<p>' + escapeHtml(n.body || '') + '</p>';
6085
+ notifications.appendChild(el);
6086
+ setTimeout(function() { el.remove(); }, 5000);
6087
+ }
6088
+
6089
+ // Event: Send button
6090
+ sendBtn.onclick = function() {
6091
+ if (input.value) {
6092
+ send(input.value + '\\r');
6093
+ input.value = '';
6094
+ }
6095
+ input.focus();
6096
+ };
6097
+
6098
+ // Event: Enter key in input
6099
+ input.onkeydown = function(e) {
6100
+ if (e.key === 'Enter') {
6101
+ e.preventDefault();
6102
+ sendBtn.click();
6103
+ }
6104
+ };
6105
+
6106
+ // Event: Quick keys
6107
+ document.querySelectorAll('.qkey').forEach(function(btn) {
6108
+ btn.onclick = function() {
6109
+ const key = btn.dataset.key;
6110
+ const decoded = key
6111
+ .replace(/\\\\x([0-9a-f]{2})/gi, function(_, hex) {
6112
+ return String.fromCharCode(parseInt(hex, 16));
6113
+ })
6114
+ .replace(/\\\\t/g, '\\t')
6115
+ .replace(/\\\\r/g, '\\r')
6116
+ .replace(/\\\\n/g, '\\n');
6117
+ send(decoded);
6118
+ input.focus();
6119
+ };
6120
+ });
6121
+
6122
+ // Heartbeat
6123
+ setInterval(function() {
6124
+ if (ws && ws.readyState === WebSocket.OPEN) {
6125
+ ws.send(JSON.stringify({ type: 'ping' }));
6126
+ }
6127
+ }, 25000);
6128
+
6129
+ // Handle resize
6130
+ function sendResize() {
6131
+ if (ws && ws.readyState === WebSocket.OPEN) {
6132
+ const cols = Math.floor(terminal.clientWidth / 8);
6133
+ const rows = Math.floor(terminal.clientHeight / 18);
6134
+ ws.send(JSON.stringify({ type: 'terminal:resize', cols: cols, rows: rows }));
6135
+ }
6136
+ }
6137
+
6138
+ window.addEventListener('resize', sendResize);
6139
+ setTimeout(sendResize, 1000);
6140
+
6141
+ // Start connection
6142
+ if (token) {
6143
+ connect();
6144
+ } else {
6145
+ authStatus.textContent = 'Invalid session URL';
6146
+ authStatus.classList.add('error');
6147
+ }
6148
+ })();
5792
6149
  </script>
5793
6150
  </body>
5794
6151
  </html>`;