nikcli-remote 1.0.1 → 1.0.3

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.
@@ -1548,7 +1548,7 @@ var TunnelManager = class {
1548
1548
  */
1549
1549
  createLocaltunnelCli(port) {
1550
1550
  return new Promise((resolve, reject) => {
1551
- this.process = spawn2("npx", ["localtunnel", "--port", port.toString()], {
1551
+ this.process = spawn2("npx", ["localtunnel", "--port", port.toString(), "--print-requests", "false"], {
1552
1552
  stdio: ["pipe", "pipe", "pipe"],
1553
1553
  shell: true
1554
1554
  });
@@ -1565,8 +1565,7 @@ var TunnelManager = class {
1565
1565
  resolve(match[1]);
1566
1566
  }
1567
1567
  });
1568
- this.process.stderr?.on("data", (data) => {
1569
- output += data.toString();
1568
+ this.process.stderr?.on("data", () => {
1570
1569
  });
1571
1570
  this.process.on("error", (error) => {
1572
1571
  clearTimeout(timeout);
@@ -1587,7 +1586,7 @@ var TunnelManager = class {
1587
1586
  return new Promise((resolve, reject) => {
1588
1587
  this.process = spawn2(
1589
1588
  "cloudflared",
1590
- ["tunnel", "--url", `http://localhost:${port}`],
1589
+ ["tunnel", "--url", `http://localhost:${port}`, "--metrics", "localhost:0"],
1591
1590
  {
1592
1591
  stdio: ["pipe", "pipe", "pipe"]
1593
1592
  }
@@ -1624,7 +1623,7 @@ var TunnelManager = class {
1624
1623
  */
1625
1624
  createNgrok(port) {
1626
1625
  return new Promise((resolve, reject) => {
1627
- this.process = spawn2("ngrok", ["http", port.toString(), "--log=stdout"], {
1626
+ this.process = spawn2("ngrok", ["http", port.toString(), "--log=stdout", "--log-level=info"], {
1628
1627
  stdio: ["pipe", "pipe", "pipe"]
1629
1628
  });
1630
1629
  let output = "";
@@ -1640,8 +1639,7 @@ var TunnelManager = class {
1640
1639
  resolve(match[1]);
1641
1640
  }
1642
1641
  });
1643
- this.process.stderr?.on("data", (data) => {
1644
- output += data.toString();
1642
+ this.process.stderr?.on("data", () => {
1645
1643
  });
1646
1644
  this.process.on("error", (error) => {
1647
1645
  clearTimeout(timeout);
@@ -1656,40 +1654,6 @@ var TunnelManager = class {
1656
1654
  });
1657
1655
  }
1658
1656
  };
1659
- async function checkTunnelAvailability(provider) {
1660
- try {
1661
- const { execSync } = await import("child_process");
1662
- switch (provider) {
1663
- case "localtunnel":
1664
- try {
1665
- await import("./localtunnel-XT32JGNN.js");
1666
- return true;
1667
- } catch {
1668
- execSync("npx localtunnel --version", { stdio: "pipe" });
1669
- return true;
1670
- }
1671
- case "cloudflared":
1672
- execSync("cloudflared --version", { stdio: "pipe" });
1673
- return true;
1674
- case "ngrok":
1675
- execSync("ngrok version", { stdio: "pipe" });
1676
- return true;
1677
- default:
1678
- return false;
1679
- }
1680
- } catch {
1681
- return false;
1682
- }
1683
- }
1684
- async function findAvailableTunnel() {
1685
- const providers = ["localtunnel", "cloudflared", "ngrok"];
1686
- for (const provider of providers) {
1687
- if (await checkTunnelAvailability(provider)) {
1688
- return provider;
1689
- }
1690
- }
1691
- return null;
1692
- }
1693
1657
 
1694
1658
  // src/web-client.ts
1695
1659
  function getWebClient() {
@@ -1697,649 +1661,298 @@ function getWebClient() {
1697
1661
  <html lang="en">
1698
1662
  <head>
1699
1663
  <meta charset="UTF-8">
1700
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
1664
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
1701
1665
  <meta name="apple-mobile-web-app-capable" content="yes">
1702
- <meta name="mobile-web-app-capable" content="yes">
1703
- <meta name="theme-color" content="#0d1117">
1666
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
1704
1667
  <title>NikCLI Remote</title>
1668
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
1705
1669
  <style>
1670
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1706
1671
  :root {
1707
- --bg: #0d1117;
1672
+ --bg-primary: #0d1117;
1708
1673
  --bg-secondary: #161b22;
1709
- --fg: #e6edf3;
1710
- --fg-muted: #8b949e;
1711
1674
  --accent: #58a6ff;
1712
1675
  --success: #3fb950;
1713
- --warning: #d29922;
1714
- --error: #f85149;
1715
1676
  --border: #30363d;
1716
- --font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
1717
- }
1718
-
1719
- * {
1720
- box-sizing: border-box;
1721
- margin: 0;
1722
- padding: 0;
1723
- -webkit-tap-highlight-color: transparent;
1724
1677
  }
1725
-
1726
- html, body {
1727
- height: 100%;
1728
- background: var(--bg);
1729
- color: var(--fg);
1730
- font-family: var(--font-mono);
1731
- font-size: 14px;
1732
- overflow: hidden;
1733
- touch-action: manipulation;
1734
- }
1735
-
1736
- #app {
1678
+ html, body { height: 100%; overflow: hidden; touch-action: manipulation; }
1679
+ body {
1680
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1681
+ background: var(--bg-primary);
1682
+ color: #e6edf3;
1737
1683
  display: flex;
1738
1684
  flex-direction: column;
1739
- height: 100%;
1740
- height: 100dvh;
1741
- }
1742
-
1743
- /* Header */
1744
- #header {
1745
- display: flex;
1746
- align-items: center;
1747
- justify-content: space-between;
1748
- padding: 12px 16px;
1749
- background: var(--bg-secondary);
1750
- border-bottom: 1px solid var(--border);
1751
- flex-shrink: 0;
1752
- }
1753
-
1754
- #header h1 {
1755
- font-size: 16px;
1756
- font-weight: 600;
1757
- color: var(--accent);
1758
- display: flex;
1759
- align-items: center;
1760
- gap: 8px;
1761
- }
1762
-
1763
- #header h1::before {
1764
- content: '';
1765
- width: 10px;
1766
- height: 10px;
1767
- background: var(--accent);
1768
- border-radius: 2px;
1769
- }
1770
-
1771
- #status {
1772
- display: flex;
1773
- align-items: center;
1774
- gap: 6px;
1775
- font-size: 12px;
1776
- color: var(--fg-muted);
1777
- }
1778
-
1779
- #status-dot {
1780
- width: 8px;
1781
- height: 8px;
1782
- border-radius: 50%;
1783
- background: var(--error);
1784
- transition: background 0.3s;
1785
- }
1786
-
1787
- #status-dot.connected {
1788
- background: var(--success);
1789
- }
1790
-
1791
- #status-dot.connecting {
1792
- background: var(--warning);
1793
- animation: pulse 1s infinite;
1794
- }
1795
-
1796
- @keyframes pulse {
1797
- 0%, 100% { opacity: 1; }
1798
- 50% { opacity: 0.5; }
1799
- }
1800
-
1801
- /* Terminal */
1802
- #terminal-container {
1803
- flex: 1;
1804
- overflow: hidden;
1805
- position: relative;
1806
- }
1807
-
1808
- #terminal {
1809
- height: 100%;
1810
- padding: 12px;
1811
- overflow-y: auto;
1812
- overflow-x: hidden;
1813
- font-size: 13px;
1814
- line-height: 1.5;
1815
- white-space: pre-wrap;
1816
- word-break: break-all;
1817
- -webkit-overflow-scrolling: touch;
1818
- }
1819
-
1820
- #terminal::-webkit-scrollbar {
1821
- width: 6px;
1822
- }
1823
-
1824
- #terminal::-webkit-scrollbar-track {
1825
- background: var(--bg);
1826
- }
1827
-
1828
- #terminal::-webkit-scrollbar-thumb {
1829
- background: var(--border);
1830
- border-radius: 3px;
1831
- }
1832
-
1833
- .cursor {
1834
- display: inline-block;
1835
- width: 8px;
1836
- height: 16px;
1837
- background: var(--fg);
1838
- animation: blink 1s step-end infinite;
1839
- vertical-align: text-bottom;
1840
- }
1841
-
1842
- @keyframes blink {
1843
- 50% { opacity: 0; }
1844
- }
1845
-
1846
- /* Notifications */
1847
- #notifications {
1848
- position: fixed;
1849
- top: 60px;
1850
- left: 12px;
1851
- right: 12px;
1852
- z-index: 1000;
1853
- pointer-events: none;
1854
- }
1855
-
1856
- .notification {
1857
- background: var(--bg-secondary);
1858
- border: 1px solid var(--border);
1859
- border-radius: 8px;
1860
- padding: 12px 16px;
1861
- margin-bottom: 8px;
1862
- animation: slideIn 0.3s ease;
1863
- pointer-events: auto;
1864
- box-shadow: 0 4px 12px rgba(0,0,0,0.4);
1865
- }
1866
-
1867
- .notification.success { border-left: 3px solid var(--success); }
1868
- .notification.error { border-left: 3px solid var(--error); }
1869
- .notification.warning { border-left: 3px solid var(--warning); }
1870
- .notification.info { border-left: 3px solid var(--accent); }
1871
-
1872
- .notification h4 {
1873
- font-size: 14px;
1874
- font-weight: 600;
1875
- margin-bottom: 4px;
1876
- }
1877
-
1878
- .notification p {
1879
- font-size: 12px;
1880
- color: var(--fg-muted);
1881
1685
  }
1882
-
1883
- @keyframes slideIn {
1884
- from { transform: translateY(-20px); opacity: 0; }
1885
- to { transform: translateY(0); opacity: 1; }
1886
- }
1887
-
1888
- /* Quick Keys */
1889
- #quickkeys {
1890
- display: grid;
1891
- grid-template-columns: repeat(6, 1fr);
1892
- gap: 6px;
1893
- padding: 8px 12px;
1894
- background: var(--bg-secondary);
1895
- border-top: 1px solid var(--border);
1896
- flex-shrink: 0;
1897
- }
1898
-
1899
- .qkey {
1900
- background: var(--bg);
1901
- border: 1px solid var(--border);
1902
- border-radius: 6px;
1903
- padding: 10px 4px;
1904
- color: var(--fg);
1905
- font-size: 11px;
1906
- font-family: var(--font-mono);
1907
- text-align: center;
1908
- cursor: pointer;
1909
- user-select: none;
1910
- transition: background 0.1s, transform 0.1s;
1911
- }
1912
-
1913
- .qkey:active {
1914
- background: var(--border);
1915
- transform: scale(0.95);
1916
- }
1917
-
1918
- .qkey.wide {
1919
- grid-column: span 2;
1920
- }
1921
-
1922
- .qkey.accent {
1923
- background: var(--accent);
1924
- border-color: var(--accent);
1925
- color: #fff;
1926
- }
1927
-
1928
- /* Input */
1929
- #input-container {
1930
- padding: 12px;
1686
+ #terminal { flex: 1; overflow: hidden; background: var(--bg-primary); padding: 8px; }
1687
+ #input-area {
1931
1688
  background: var(--bg-secondary);
1932
1689
  border-top: 1px solid var(--border);
1690
+ padding: 12px 16px;
1933
1691
  flex-shrink: 0;
1692
+ padding-bottom: env(safe-area-inset-bottom, 12px);
1934
1693
  }
1935
-
1936
- #input-row {
1937
- display: flex;
1938
- gap: 8px;
1939
- }
1940
-
1941
- #input {
1694
+ .input-row { display: flex; gap: 8px; align-items: center; }
1695
+ .prompt { color: var(--success); font-family: 'SF Mono', Monaco, monospace; font-size: 14px; white-space: nowrap; }
1696
+ #cmd-input {
1942
1697
  flex: 1;
1943
- background: var(--bg);
1698
+ background: #21262d;
1944
1699
  border: 1px solid var(--border);
1945
- border-radius: 8px;
1946
- padding: 12px 14px;
1947
- color: var(--fg);
1948
- font-family: var(--font-mono);
1700
+ border-radius: 12px;
1701
+ padding: 12px 16px;
1702
+ color: #e6edf3;
1949
1703
  font-size: 16px;
1704
+ font-family: 'SF Mono', Monaco, monospace;
1950
1705
  outline: none;
1951
- transition: border-color 0.2s;
1952
- }
1953
-
1954
- #input:focus {
1955
- border-color: var(--accent);
1706
+ -webkit-appearance: none;
1956
1707
  }
1957
-
1958
- #input::placeholder {
1959
- color: var(--fg-muted);
1960
- }
1961
-
1962
- #send {
1708
+ #cmd-input:focus { border-color: var(--accent); }
1709
+ #send-btn {
1963
1710
  background: var(--accent);
1964
- color: #fff;
1711
+ color: white;
1965
1712
  border: none;
1966
- border-radius: 8px;
1967
- padding: 12px 20px;
1968
- font-size: 14px;
1713
+ border-radius: 12px;
1714
+ padding: 12px 24px;
1715
+ font-size: 16px;
1969
1716
  font-weight: 600;
1970
- font-family: var(--font-mono);
1971
1717
  cursor: pointer;
1972
- transition: opacity 0.2s, transform 0.1s;
1973
- }
1974
-
1975
- #send:active {
1976
- opacity: 0.8;
1977
- transform: scale(0.98);
1718
+ -webkit-tap-highlight-color: transparent;
1978
1719
  }
1979
-
1980
- /* Auth Screen */
1981
- #auth-screen {
1982
- position: fixed;
1983
- inset: 0;
1984
- background: var(--bg);
1720
+ #send-btn:active { opacity: 0.7; }
1721
+ #status-bar {
1722
+ background: var(--bg-secondary);
1723
+ border-bottom: 1px solid var(--border);
1724
+ padding: 8px 16px;
1985
1725
  display: flex;
1986
- flex-direction: column;
1726
+ justify-content: space-between;
1987
1727
  align-items: center;
1988
- justify-content: center;
1989
- gap: 20px;
1990
- z-index: 2000;
1991
- }
1992
-
1993
- #auth-screen.hidden {
1994
- display: none;
1995
- }
1996
-
1997
- .spinner {
1998
- width: 40px;
1999
- height: 40px;
2000
- border: 3px solid var(--border);
2001
- border-top-color: var(--accent);
2002
- border-radius: 50%;
2003
- animation: spin 1s linear infinite;
2004
- }
2005
-
2006
- @keyframes spin {
2007
- to { transform: rotate(360deg); }
2008
- }
2009
-
2010
- #auth-screen p {
2011
- color: var(--fg-muted);
2012
- font-size: 14px;
2013
- }
2014
-
2015
- #auth-screen .error {
2016
- color: var(--error);
2017
- }
2018
-
2019
- /* Safe area padding for notched devices */
2020
- @supports (padding: env(safe-area-inset-bottom)) {
2021
- #input-container {
2022
- padding-bottom: calc(12px + env(safe-area-inset-bottom));
2023
- }
2024
- }
2025
-
2026
- /* Landscape adjustments */
2027
- @media (max-height: 500px) {
2028
- #quickkeys {
2029
- grid-template-columns: repeat(12, 1fr);
2030
- padding: 6px 8px;
2031
- }
2032
- .qkey {
2033
- padding: 8px 2px;
2034
- font-size: 10px;
2035
- }
2036
- #terminal {
2037
- font-size: 12px;
2038
- }
1728
+ font-size: 12px;
1729
+ padding-top: env(safe-area-inset-top, 8px);
1730
+ }
1731
+ .status-row { display: flex; align-items: center; gap: 8px; }
1732
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #8b949e; }
1733
+ .status-dot.connected { background: var(--success); box-shadow: 0 0 8px var(--success); }
1734
+ .status-dot.connecting { background: var(--accent); animation: pulse 1s infinite; }
1735
+ .status-dot.disconnected { background: #f85149; }
1736
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
1737
+ #auth-overlay {
1738
+ position: fixed; inset: 0; background: var(--bg-primary);
1739
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
1740
+ padding: 20px; z-index: 100;
1741
+ }
1742
+ #auth-overlay.hidden { display: none; }
1743
+ .auth-title { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
1744
+ .auth-subtitle { color: #8b949e; font-size: 16px; margin-bottom: 24px; }
1745
+ .auth-msg {
1746
+ background: var(--bg-secondary); padding: 20px 28px;
1747
+ border-radius: 16px; border: 1px solid var(--border); text-align: center;
1748
+ }
1749
+ .auth-msg.error { color: #f85149; border-color: #f85149; }
1750
+ .quick-btns {
1751
+ display: flex; gap: 8px; margin-top: 20px; flex-wrap: wrap; justify-content: center;
1752
+ }
1753
+ .quick-btn {
1754
+ background: #21262d; border: 1px solid var(--border); color: #e6edf3;
1755
+ padding: 10px 16px; border-radius: 8px; font-size: 14px;
1756
+ font-family: 'SF Mono', Monaco, monospace; cursor: pointer;
1757
+ }
1758
+ .quick-btn:active { background: #30363d; }
1759
+ .hint { font-size: 12px; color: #8b949e; margin-top: 12px; }
1760
+ @media (max-width: 600px) {
1761
+ #input-area { padding: 10px 12px; }
1762
+ .quick-btn { padding: 8px 12px; font-size: 12px; }
2039
1763
  }
2040
1764
  </style>
2041
1765
  </head>
2042
1766
  <body>
2043
- <div id="app">
2044
- <div id="auth-screen">
2045
- <div class="spinner"></div>
2046
- <p id="auth-status">Connecting to NikCLI...</p>
2047
- </div>
2048
-
2049
- <header id="header">
2050
- <h1>NikCLI Remote</h1>
2051
- <div id="status">
2052
- <span id="status-dot" class="connecting"></span>
2053
- <span id="status-text">Connecting</span>
2054
- </div>
2055
- </header>
2056
-
2057
- <div id="terminal-container">
2058
- <div id="terminal"></div>
1767
+ <div id="auth-overlay">
1768
+ <div class="auth-title">\u{1F4F1} NikCLI Remote</div>
1769
+ <div class="auth-subtitle">Full terminal emulation</div>
1770
+ <div id="auth-msg" class="auth-msg">
1771
+ <div id="auth-text">Connecting...</div>
2059
1772
  </div>
2060
-
2061
- <div id="notifications"></div>
2062
-
2063
- <div id="quickkeys">
2064
- <button class="qkey" data-key="\\t">Tab</button>
2065
- <button class="qkey" data-key="\\x1b[A">\u2191</button>
2066
- <button class="qkey" data-key="\\x1b[B">\u2193</button>
2067
- <button class="qkey" data-key="\\x1b[D">\u2190</button>
2068
- <button class="qkey" data-key="\\x1b[C">\u2192</button>
2069
- <button class="qkey" data-key="\\x1b">Esc</button>
2070
- <button class="qkey" data-key="\\x03">^C</button>
2071
- <button class="qkey" data-key="\\x04">^D</button>
2072
- <button class="qkey" data-key="\\x1a">^Z</button>
2073
- <button class="qkey" data-key="\\x0c">^L</button>
2074
- <button class="qkey wide accent" data-key="\\r">Enter \u23CE</button>
1773
+ <div class="quick-btns">
1774
+ <button class="quick-btn" onclick="send('help')">/help</button>
1775
+ <button class="quick-btn" onclick="send('ls -la')">ls -la</button>
1776
+ <button class="quick-btn" onclick="send('pwd')">pwd</button>
1777
+ <button class="quick-btn" onclick="send('whoami')">whoami</button>
1778
+ <button class="quick-btn" onclick="send('clear')">clear</button>
2075
1779
  </div>
1780
+ <div class="hint">Mobile keyboard to type commands</div>
1781
+ </div>
2076
1782
 
2077
- <div id="input-container">
2078
- <div id="input-row">
2079
- <input type="text" id="input" placeholder="Type command..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
2080
- <button id="send">Send</button>
2081
- </div>
1783
+ <div id="status-bar">
1784
+ <div class="status-row">
1785
+ <span class="status-dot" id="status-dot"></span>
1786
+ <span id="status-text">Disconnected</span>
2082
1787
  </div>
1788
+ <span id="session-id" style="color: #8b949e;"></span>
2083
1789
  </div>
2084
1790
 
2085
- <script>
2086
- (function() {
2087
- 'use strict';
2088
-
2089
- // Parse URL params
2090
- const params = new URLSearchParams(location.search);
2091
- const token = params.get('t');
2092
- const sessionId = params.get('s');
2093
-
2094
- // DOM elements
2095
- const terminal = document.getElementById('terminal');
2096
- const input = document.getElementById('input');
2097
- const sendBtn = document.getElementById('send');
2098
- const statusDot = document.getElementById('status-dot');
2099
- const statusText = document.getElementById('status-text');
2100
- const authScreen = document.getElementById('auth-screen');
2101
- const authStatus = document.getElementById('auth-status');
2102
- const notifications = document.getElementById('notifications');
2103
-
2104
- // State
2105
- let ws = null;
2106
- let reconnectAttempts = 0;
2107
- const maxReconnectAttempts = 5;
2108
- let terminalEnabled = true;
2109
-
2110
- // Connect to WebSocket
2111
- function connect() {
2112
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
2113
- ws = new WebSocket(protocol + '//' + location.host);
2114
-
2115
- ws.onopen = function() {
2116
- setStatus('connecting', 'Authenticating...');
2117
- ws.send(JSON.stringify({ type: 'auth', token: token }));
2118
- };
2119
-
2120
- ws.onmessage = function(event) {
2121
- try {
2122
- const msg = JSON.parse(event.data);
2123
- handleMessage(msg);
2124
- } catch (e) {
2125
- console.error('Parse error:', e);
2126
- }
2127
- };
2128
-
2129
- ws.onclose = function() {
2130
- setStatus('disconnected', 'Disconnected');
2131
- if (reconnectAttempts < maxReconnectAttempts) {
2132
- reconnectAttempts++;
2133
- const delay = Math.min(2000 * reconnectAttempts, 10000);
2134
- setTimeout(connect, delay);
2135
- } else {
2136
- authStatus.textContent = 'Connection failed. Refresh to retry.';
2137
- authStatus.classList.add('error');
2138
- authScreen.classList.remove('hidden');
2139
- }
2140
- };
2141
-
2142
- ws.onerror = function() {
2143
- console.error('WebSocket error');
2144
- };
2145
- }
2146
-
2147
- // Handle incoming message
2148
- function handleMessage(msg) {
2149
- switch (msg.type) {
2150
- case 'auth:required':
2151
- // Already sent auth on open
2152
- break;
2153
-
2154
- case 'auth:success':
2155
- authScreen.classList.add('hidden');
2156
- setStatus('connected', 'Connected');
2157
- reconnectAttempts = 0;
2158
- terminalEnabled = msg.payload?.terminalEnabled !== false;
2159
- if (terminalEnabled) {
2160
- appendOutput('\\x1b[32mConnected to NikCLI\\x1b[0m\\n\\n');
2161
- }
2162
- break;
2163
-
2164
- case 'auth:failed':
2165
- authStatus.textContent = 'Authentication failed';
2166
- authStatus.classList.add('error');
2167
- break;
2168
-
2169
- case 'terminal:output':
2170
- if (msg.payload?.data) {
2171
- appendOutput(msg.payload.data);
2172
- }
2173
- break;
2174
-
2175
- case 'terminal:exit':
2176
- appendOutput('\\n\\x1b[33m[Process exited with code ' + (msg.payload?.code || 0) + ']\\x1b[0m\\n');
2177
- break;
2178
-
2179
- case 'notification':
2180
- showNotification(msg.payload);
2181
- break;
2182
-
2183
- case 'session:end':
2184
- appendOutput('\\n\\x1b[31m[Session ended]\\x1b[0m\\n');
2185
- setStatus('disconnected', 'Session ended');
2186
- break;
1791
+ <div id="terminal"></div>
2187
1792
 
2188
- case 'pong':
2189
- // Heartbeat response
2190
- break;
2191
-
2192
- default:
2193
- console.log('Unknown message:', msg.type);
2194
- }
2195
- }
2196
-
2197
- // Append text to terminal with ANSI support
2198
- function appendOutput(text) {
2199
- // Simple ANSI to HTML conversion
2200
- const html = ansiToHtml(text);
2201
- terminal.innerHTML += html;
2202
- terminal.scrollTop = terminal.scrollHeight;
2203
- }
1793
+ <div id="input-area">
1794
+ <form class="input-row" onsubmit="return handleSubmit(event)">
1795
+ <span class="prompt">$</span>
1796
+ <input type="text" id="cmd-input" placeholder="Type command..." autocomplete="off" enterkeyhint="send" inputmode="text">
1797
+ <button type="submit" id="send-btn">Send</button>
1798
+ </form>
1799
+ </div>
2204
1800
 
2205
- // Basic ANSI to HTML
2206
- function ansiToHtml(text) {
2207
- const ansiColors = {
2208
- '30': '#6e7681', '31': '#f85149', '32': '#3fb950', '33': '#d29922',
2209
- '34': '#58a6ff', '35': '#bc8cff', '36': '#76e3ea', '37': '#e6edf3',
2210
- '90': '#6e7681', '91': '#f85149', '92': '#3fb950', '93': '#d29922',
2211
- '94': '#58a6ff', '95': '#bc8cff', '96': '#76e3ea', '97': '#ffffff'
2212
- };
1801
+ <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
1802
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
1803
+ <script>
1804
+ let ws = null, term = null, fitAddon = null, connected = false, reconnectAttempts = 0;
1805
+ const token = new URLSearchParams(location.search).get('t') || '';
1806
+ const sessionId = new URLSearchParams(location.search).get('s') || '';
1807
+
1808
+ const authOverlay = document.getElementById('auth-overlay');
1809
+ const authMsg = document.getElementById('auth-msg');
1810
+ const authText = document.getElementById('auth-text');
1811
+ const statusDot = document.getElementById('status-dot');
1812
+ const statusText = document.getElementById('status-text');
1813
+ const sessionSpan = document.getElementById('session-id');
1814
+ const cmdInput = document.getElementById('cmd-input');
1815
+
1816
+ // Initialize xterm.js
1817
+ term = new Terminal({
1818
+ cursorBlink: true,
1819
+ fontSize: 14,
1820
+ fontFamily: '"SF Mono", Monaco, Consolas, monospace',
1821
+ theme: {
1822
+ background: '#0d1117',
1823
+ foreground: '#e6edf3',
1824
+ cursor: '#3fb950',
1825
+ selectionBackground: '#264f78',
1826
+ black: '#484f58',
1827
+ red: '#f85149',
1828
+ green: '#3fb950',
1829
+ yellow: '#d29922',
1830
+ blue: '#58a6ff',
1831
+ magenta: '#a371f7',
1832
+ cyan: '#39c5cf',
1833
+ white: '#e6edf3',
1834
+ brightBlack: '#6e7681',
1835
+ brightRed: '#ffa198',
1836
+ brightGreen: '#7ee787',
1837
+ brightYellow: '#f0883e',
1838
+ brightBlue: '#79c0ff',
1839
+ brightMagenta: '#d2a8ff',
1840
+ brightCyan: '#56d4db',
1841
+ brightWhite: '#f0f6fc'
1842
+ },
1843
+ convertEol: true
1844
+ });
2213
1845
 
2214
- let result = '';
2215
- let currentStyle = '';
1846
+ fitAddon = new FitAddon.FitAddon();
1847
+ term.loadAddon(fitAddon);
1848
+ term.open(document.getElementById('terminal'));
1849
+ fitAddon.fit();
1850
+ term.writeln('\x1B[32mInitializing NikCLI Remote...\x1B[0m');
1851
+ term.writeln('\x1B[90mType commands below\x1B[0m');
1852
+
1853
+ // Resize handler
1854
+ window.addEventListener('resize', () => {
1855
+ clearTimeout(window.resizeTimer);
1856
+ window.resizeTimer = setTimeout(() => fitAddon.fit(), 100);
1857
+ });
2216
1858
 
2217
- const parts = text.split(/\\x1b\\[([0-9;]+)m/);
2218
- for (let i = 0; i < parts.length; i++) {
2219
- if (i % 2 === 0) {
2220
- // Text content
2221
- result += escapeHtml(parts[i]);
2222
- } else {
2223
- // ANSI code
2224
- const codes = parts[i].split(';');
2225
- for (const code of codes) {
2226
- if (code === '0') {
2227
- if (currentStyle) {
2228
- result += '</span>';
2229
- currentStyle = '';
2230
- }
2231
- } else if (code === '1') {
2232
- currentStyle = 'font-weight:bold;';
2233
- result += '<span style="' + currentStyle + '">';
2234
- } else if (ansiColors[code]) {
2235
- if (currentStyle) result += '</span>';
2236
- currentStyle = 'color:' + ansiColors[code] + ';';
2237
- result += '<span style="' + currentStyle + '">';
2238
- }
2239
- }
2240
- }
2241
- }
1859
+ // Visual viewport for mobile keyboard
1860
+ if (window.visualViewport) {
1861
+ window.visualViewport.addEventListener('resize', () => {
1862
+ clearTimeout(window.resizeTimer);
1863
+ window.resizeTimer = setTimeout(() => fitAddon.fit(), 100);
1864
+ });
1865
+ }
2242
1866
 
2243
- if (currentStyle) result += '</span>';
2244
- return result;
2245
- }
1867
+ function connect() {
1868
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1869
+ ws = new WebSocket(protocol + '//' + location.host + '/ws');
2246
1870
 
2247
- // Escape HTML
2248
- function escapeHtml(text) {
2249
- return text
2250
- .replace(/&/g, '&amp;')
2251
- .replace(/</g, '&lt;')
2252
- .replace(/>/g, '&gt;')
2253
- .replace(/"/g, '&quot;')
2254
- .replace(/\\n/g, '<br>')
2255
- .replace(/ /g, '&nbsp;');
2256
- }
1871
+ ws.onopen = () => {
1872
+ setStatus('connecting', 'Authenticating...');
1873
+ ws.send(JSON.stringify({ type: 'auth', token }));
1874
+ reconnectAttempts = 0;
1875
+ };
2257
1876
 
2258
- // Set connection status
2259
- function setStatus(state, text) {
2260
- statusDot.className = state === 'connected' ? 'connected' :
2261
- state === 'connecting' ? 'connecting' : '';
2262
- statusText.textContent = text;
2263
- }
1877
+ ws.onmessage = (e) => {
1878
+ try { handleMessage(JSON.parse(e.data)); } catch (err) { console.error(err); }
1879
+ };
2264
1880
 
2265
- // Send data to terminal
2266
- function send(data) {
2267
- if (ws && ws.readyState === WebSocket.OPEN) {
2268
- ws.send(JSON.stringify({ type: 'terminal:input', data: data }));
2269
- }
2270
- }
1881
+ ws.onclose = () => {
1882
+ setStatus('disconnected', 'Disconnected');
1883
+ connected = false;
1884
+ reconnectAttempts++;
1885
+ setTimeout(connect, Math.min(3000, reconnectAttempts * 500));
1886
+ };
2271
1887
 
2272
- // Show notification
2273
- function showNotification(n) {
2274
- if (!n) return;
2275
- const el = document.createElement('div');
2276
- el.className = 'notification ' + (n.type || 'info');
2277
- el.innerHTML = '<h4>' + escapeHtml(n.title || 'Notification') + '</h4>' +
2278
- '<p>' + escapeHtml(n.body || '') + '</p>';
2279
- notifications.appendChild(el);
2280
- setTimeout(function() { el.remove(); }, 5000);
1888
+ ws.onerror = () => setStatus('disconnected', 'Connection error');
1889
+ }
1890
+
1891
+ function handleMessage(msg) {
1892
+ switch (msg.type) {
1893
+ case 'auth:required':
1894
+ ws.send(JSON.stringify({ type: 'auth', token }));
1895
+ break;
1896
+ case 'auth:success':
1897
+ connected = true;
1898
+ authOverlay.classList.add('hidden');
1899
+ setStatus('connected', 'Connected');
1900
+ sessionSpan.textContent = sessionId ? 'Session: ' + sessionId : '';
1901
+ term.writeln('
1902
+ \x1B[32m\u2713 Connected to NikCLI\x1B[0m
1903
+ ');
1904
+ break;
1905
+ case 'auth:failed':
1906
+ authMsg.classList.add('error');
1907
+ authText.textContent = 'Authentication failed';
1908
+ break;
1909
+ case 'terminal:output':
1910
+ if (msg.payload?.data) term.write(msg.payload.data);
1911
+ break;
1912
+ case 'terminal:exit':
1913
+ term.writeln('
1914
+ \x1B[33m[Process exited: ' + (msg.payload?.code || 0) + ']\x1B[0m
1915
+ ');
1916
+ break;
2281
1917
  }
1918
+ }
2282
1919
 
2283
- // Event: Send button
2284
- sendBtn.onclick = function() {
2285
- if (input.value) {
2286
- send(input.value + '\\r');
2287
- input.value = '';
2288
- }
2289
- input.focus();
2290
- };
2291
-
2292
- // Event: Enter key in input
2293
- input.onkeydown = function(e) {
2294
- if (e.key === 'Enter') {
2295
- e.preventDefault();
2296
- sendBtn.click();
2297
- }
2298
- };
1920
+ function setStatus(state, text) {
1921
+ statusDot.className = 'status-dot ' + state;
1922
+ statusText.textContent = text;
1923
+ }
2299
1924
 
2300
- // Event: Quick keys
2301
- document.querySelectorAll('.qkey').forEach(function(btn) {
2302
- btn.onclick = function() {
2303
- const key = btn.dataset.key;
2304
- const decoded = key
2305
- .replace(/\\\\x([0-9a-f]{2})/gi, function(_, hex) {
2306
- return String.fromCharCode(parseInt(hex, 16));
2307
- })
2308
- .replace(/\\\\t/g, '\\t')
2309
- .replace(/\\\\r/g, '\\r')
2310
- .replace(/\\\\n/g, '\\n');
2311
- send(decoded);
2312
- input.focus();
2313
- };
2314
- });
1925
+ function handleSubmit(e) {
1926
+ e?.preventDefault();
1927
+ const value = cmdInput.value.trim();
1928
+ if (!value || !connected) return;
1929
+ cmdInput.value = '';
1930
+ term.write('$ ' + value + '\r
1931
+ ');
1932
+ ws.send(JSON.stringify({ type: 'terminal:input', data: value + '\r' }));
1933
+ setTimeout(() => cmdInput.focus(), 50);
1934
+ return false;
1935
+ }
2315
1936
 
2316
- // Heartbeat
2317
- setInterval(function() {
2318
- if (ws && ws.readyState === WebSocket.OPEN) {
2319
- ws.send(JSON.stringify({ type: 'ping' }));
2320
- }
2321
- }, 25000);
1937
+ function send(cmd) {
1938
+ if (!connected) return;
1939
+ term.write('$ ' + cmd + '\r
1940
+ ');
1941
+ ws.send(JSON.stringify({ type: 'terminal:input', data: cmd + '\r' }));
1942
+ }
2322
1943
 
2323
- // Handle resize
2324
- function sendResize() {
2325
- if (ws && ws.readyState === WebSocket.OPEN) {
2326
- const cols = Math.floor(terminal.clientWidth / 8);
2327
- const rows = Math.floor(terminal.clientHeight / 18);
2328
- ws.send(JSON.stringify({ type: 'terminal:resize', cols: cols, rows: rows }));
2329
- }
1944
+ cmdInput.addEventListener('keydown', (e) => {
1945
+ if (e.key === 'Enter' && !e.shiftKey) {
1946
+ e.preventDefault();
1947
+ handleSubmit();
2330
1948
  }
1949
+ });
2331
1950
 
2332
- window.addEventListener('resize', sendResize);
2333
- setTimeout(sendResize, 1000);
1951
+ document.getElementById('terminal')?.addEventListener('click', () => {
1952
+ if (connected) cmdInput.focus();
1953
+ });
2334
1954
 
2335
- // Start connection
2336
- if (token) {
2337
- connect();
2338
- } else {
2339
- authStatus.textContent = 'Invalid session URL';
2340
- authStatus.classList.add('error');
2341
- }
2342
- })();
1955
+ connect();
2343
1956
  </script>
2344
1957
  </body>
2345
1958
  </html>`;
@@ -2419,6 +2032,7 @@ var RemoteServer = class extends EventEmitter2 {
2419
2032
  });
2420
2033
  this.terminal.on("data", (data) => {
2421
2034
  this.broadcast({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data } });
2035
+ this.emit("terminal:output", data);
2422
2036
  });
2423
2037
  this.terminal.on("exit", (code) => {
2424
2038
  this.broadcast({ type: MessageTypes.TERMINAL_EXIT, payload: { code } });
@@ -2635,9 +2249,6 @@ var RemoteServer = class extends EventEmitter2 {
2635
2249
  * Handle authentication
2636
2250
  */
2637
2251
  handleAuth(client, token) {
2638
- console.log(`[Tunnel] Auth attempt from ${client.id}, token length: ${token?.length || 0}`);
2639
- console.log(`[Tunnel] Expected secret: ${this.sessionSecret?.substring(0, 8)}...`);
2640
- console.log(`[Tunnel] Received token: ${token?.substring(0, 8)}...`);
2641
2252
  if (token === this.sessionSecret) {
2642
2253
  client.authenticated = true;
2643
2254
  if (this.session) {
@@ -2658,9 +2269,7 @@ var RemoteServer = class extends EventEmitter2 {
2658
2269
  this.terminal?.start();
2659
2270
  }
2660
2271
  this.emit("client:connected", client.device);
2661
- console.log(`[Tunnel] Auth success for ${client.id}`);
2662
2272
  } else {
2663
- console.log(`[Tunnel] Auth failed for ${client.id}`);
2664
2273
  client.ws.send(JSON.stringify({ type: MessageTypes.AUTH_FAILED, timestamp: Date.now() }));
2665
2274
  setTimeout(() => client.ws.close(1008, "Authentication failed"), 100);
2666
2275
  }
@@ -2779,9 +2388,6 @@ export {
2779
2388
  DEFAULT_CONFIG,
2780
2389
  MessageTypes,
2781
2390
  TerminalManager,
2782
- TunnelManager,
2783
- checkTunnelAvailability,
2784
- findAvailableTunnel,
2785
2391
  getWebClient,
2786
2392
  RemoteServer
2787
2393
  };