nikcli-remote 1.0.1 → 1.0.2

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,278 @@ 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>
1705
1668
  <style>
1669
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1706
1670
  :root {
1707
- --bg: #0d1117;
1671
+ --bg-primary: #0d1117;
1708
1672
  --bg-secondary: #161b22;
1709
- --fg: #e6edf3;
1710
- --fg-muted: #8b949e;
1711
1673
  --accent: #58a6ff;
1712
1674
  --success: #3fb950;
1713
- --warning: #d29922;
1714
- --error: #f85149;
1715
1675
  --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
1676
  }
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 {
1677
+ html, body { height: 100%; overflow: hidden; touch-action: manipulation; }
1678
+ body {
1679
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1680
+ background: var(--bg-primary);
1681
+ color: #e6edf3;
1737
1682
  display: flex;
1738
1683
  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
1684
  }
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;
1685
+ #terminal { flex: 1; overflow: hidden; background: var(--bg-primary); }
1686
+ #input-area {
1931
1687
  background: var(--bg-secondary);
1932
1688
  border-top: 1px solid var(--border);
1689
+ padding: 12px 16px;
1933
1690
  flex-shrink: 0;
1691
+ padding-bottom: env(safe-area-inset-bottom, 12px);
1934
1692
  }
1935
-
1936
- #input-row {
1937
- display: flex;
1938
- gap: 8px;
1939
- }
1940
-
1941
- #input {
1693
+ .input-row { display: flex; gap: 8px; align-items: center; }
1694
+ .prompt { color: var(--success); font-family: 'SF Mono', Monaco, monospace; font-size: 14px; white-space: nowrap; }
1695
+ #cmd-input {
1942
1696
  flex: 1;
1943
- background: var(--bg);
1697
+ background: #21262d;
1944
1698
  border: 1px solid var(--border);
1945
- border-radius: 8px;
1946
- padding: 12px 14px;
1947
- color: var(--fg);
1948
- font-family: var(--font-mono);
1699
+ border-radius: 12px;
1700
+ padding: 12px 16px;
1701
+ color: #e6edf3;
1949
1702
  font-size: 16px;
1703
+ font-family: 'SF Mono', Monaco, monospace;
1950
1704
  outline: none;
1951
- transition: border-color 0.2s;
1952
- }
1953
-
1954
- #input:focus {
1955
- border-color: var(--accent);
1705
+ -webkit-appearance: none;
1956
1706
  }
1957
-
1958
- #input::placeholder {
1959
- color: var(--fg-muted);
1960
- }
1961
-
1962
- #send {
1707
+ #cmd-input:focus { border-color: var(--accent); }
1708
+ #send-btn {
1963
1709
  background: var(--accent);
1964
- color: #fff;
1710
+ color: white;
1965
1711
  border: none;
1966
- border-radius: 8px;
1967
- padding: 12px 20px;
1968
- font-size: 14px;
1712
+ border-radius: 12px;
1713
+ padding: 12px 24px;
1714
+ font-size: 16px;
1969
1715
  font-weight: 600;
1970
- font-family: var(--font-mono);
1971
1716
  cursor: pointer;
1972
- transition: opacity 0.2s, transform 0.1s;
1973
- }
1974
-
1975
- #send:active {
1976
- opacity: 0.8;
1977
- transform: scale(0.98);
1717
+ -webkit-tap-highlight-color: transparent;
1978
1718
  }
1979
-
1980
- /* Auth Screen */
1981
- #auth-screen {
1982
- position: fixed;
1983
- inset: 0;
1984
- background: var(--bg);
1719
+ #send-btn:active { opacity: 0.7; }
1720
+ #status-bar {
1721
+ background: var(--bg-secondary);
1722
+ border-bottom: 1px solid var(--border);
1723
+ padding: 8px 16px;
1985
1724
  display: flex;
1986
- flex-direction: column;
1725
+ justify-content: space-between;
1987
1726
  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
- }
1727
+ font-size: 12px;
1728
+ padding-top: env(safe-area-inset-top, 8px);
1729
+ }
1730
+ .status-row { display: flex; align-items: center; gap: 8px; }
1731
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #8b949e; }
1732
+ .status-dot.connected { background: var(--success); box-shadow: 0 0 8px var(--success); }
1733
+ .status-dot.connecting { background: var(--accent); animation: pulse 1s infinite; }
1734
+ .status-dot.disconnected { background: #f85149; }
1735
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
1736
+ #auth-overlay {
1737
+ position: fixed; inset: 0; background: var(--bg-primary);
1738
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
1739
+ padding: 20px; z-index: 100;
1740
+ }
1741
+ #auth-overlay.hidden { display: none; }
1742
+ .auth-title { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
1743
+ .auth-subtitle { color: #8b949e; font-size: 16px; margin-bottom: 24px; }
1744
+ .auth-msg {
1745
+ background: var(--bg-secondary); padding: 20px 28px;
1746
+ border-radius: 16px; border: 1px solid var(--border); text-align: center;
1747
+ }
1748
+ .auth-msg.error { color: #f85149; border-color: #f85149; }
1749
+ .quick-btns {
1750
+ display: flex; gap: 8px; margin-top: 20px; flex-wrap: wrap; justify-content: center;
1751
+ }
1752
+ .quick-btn {
1753
+ background: #21262d; border: 1px solid var(--border); color: #e6edf3;
1754
+ padding: 10px 16px; border-radius: 8px; font-size: 14px;
1755
+ font-family: 'SF Mono', Monaco, monospace; cursor: pointer;
1756
+ }
1757
+ .quick-btn:active { background: #30363d; }
1758
+ .hint { font-size: 12px; color: #8b949e; margin-top: 12px; }
1759
+ @media (max-width: 600px) {
1760
+ #input-area { padding: 10px 12px; }
1761
+ .quick-btn { padding: 8px 12px; font-size: 12px; }
2039
1762
  }
2040
1763
  </style>
2041
1764
  </head>
2042
1765
  <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>
1766
+ <div id="auth-overlay">
1767
+ <div class="auth-title">\u{1F4F1} NikCLI Remote</div>
1768
+ <div class="auth-subtitle">Full terminal emulation</div>
1769
+ <div id="auth-msg" class="auth-msg">
1770
+ <div id="auth-text">Connecting...</div>
2059
1771
  </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>
1772
+ <div class="quick-btns">
1773
+ <button class="quick-btn" onclick="send('help')">/help</button>
1774
+ <button class="quick-btn" onclick="send('ls -la')">ls -la</button>
1775
+ <button class="quick-btn" onclick="send('pwd')">pwd</button>
1776
+ <button class="quick-btn" onclick="send('whoami')">whoami</button>
1777
+ <button class="quick-btn" onclick="send('clear')">clear</button>
2075
1778
  </div>
1779
+ <div class="hint">Mobile keyboard to type commands</div>
1780
+ </div>
2076
1781
 
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>
1782
+ <div id="status-bar">
1783
+ <div class="status-row">
1784
+ <span class="status-dot" id="status-dot"></span>
1785
+ <span id="status-text">Disconnected</span>
2082
1786
  </div>
1787
+ <span id="session-id" style="color: #8b949e;"></span>
2083
1788
  </div>
2084
1789
 
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;
2187
-
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
- }
2204
-
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
- };
2213
-
2214
- let result = '';
2215
- let currentStyle = '';
2216
-
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
- }
1790
+ <div id="terminal"></div>
2242
1791
 
2243
- if (currentStyle) result += '</span>';
2244
- return result;
2245
- }
2246
-
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
- }
1792
+ <div id="input-area">
1793
+ <form class="input-row" onsubmit="return handleSubmit(event)">
1794
+ <span class="prompt">$</span>
1795
+ <input type="text" id="cmd-input" placeholder="Type command..." autocomplete="off" enterkeyhint="send" inputmode="text">
1796
+ <button type="submit" id="send-btn">Send</button>
1797
+ </form>
1798
+ </div>
2257
1799
 
2258
- // Set connection status
2259
- function setStatus(state, text) {
2260
- statusDot.className = state === 'connected' ? 'connected' :
2261
- state === 'connecting' ? 'connecting' : '';
2262
- statusText.textContent = text;
1800
+ <!-- hterm from Google's Chromium -->
1801
+ <script src="https://cdn.jsdelivr.net/npm/hterm@1.1.1/lib/hterm.js"></script>
1802
+ <script>
1803
+ let ws = null, term = null, connected = false, reconnectAttempts = 0;
1804
+ const token = new URLSearchParams(location.search).get('t') || '';
1805
+ const sessionId = new URLSearchParams(location.search).get('s') || '';
1806
+
1807
+ const authOverlay = document.getElementById('auth-overlay');
1808
+ const authMsg = document.getElementById('auth-msg');
1809
+ const authText = document.getElementById('auth-text');
1810
+ const statusDot = document.getElementById('status-dot');
1811
+ const statusText = document.getElementById('status-text');
1812
+ const sessionSpan = document.getElementById('session-id');
1813
+ const cmdInput = document.getElementById('cmd-input');
1814
+
1815
+ // Initialize hterm
1816
+ hterm.DefaultCharWidth = 8.53;
1817
+ hterm.DefaultRowHeight = 17;
1818
+
1819
+ const storage = new hterm.Storage.Memory();
1820
+ const prefs = new hterm.Preferences(storage);
1821
+ prefs.set('font-size', 14);
1822
+ prefs.set('font-family', '"SF Mono", Monaco, Consolas, monospace');
1823
+ prefs.set('background-color', '#0d1117');
1824
+ prefs.set('foreground-color', '#e6edf3');
1825
+ prefs.set('cursor-color', '#3fb950');
1826
+ prefs.set('selection-color', '#264f78');
1827
+ prefs.set('scrollbar-visible', false);
1828
+ prefs.set('cursor-blink', true);
1829
+
1830
+ term = new hterm.Terminal(storage, prefs);
1831
+ term.onTerminalReady = function() {
1832
+ term.io.writeUTF8('\x1B[32mInitializing NikCLI Remote...\x1B[0m\r
1833
+ ');
1834
+ term.io.writeUTF8('\x1B[90mType commands below\x1B[0m\r
1835
+ ');
1836
+ connect();
1837
+ };
1838
+ term.decorate(document.getElementById('terminal'));
1839
+ term.start();
1840
+
1841
+ // Resize handler
1842
+ window.addEventListener('resize', () => {
1843
+ const el = document.getElementById('terminal');
1844
+ if (term && el) {
1845
+ term.setHeightAndWidth(el.clientHeight / 17, el.clientWidth / 8.53);
2263
1846
  }
1847
+ });
2264
1848
 
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
- }
1849
+ function connect() {
1850
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1851
+ ws = new WebSocket(protocol + '//' + location.host + '/ws');
2271
1852
 
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);
2281
- }
1853
+ ws.onopen = () => {
1854
+ setStatus('connecting', 'Authenticating...');
1855
+ ws.send(JSON.stringify({ type: 'auth', token }));
1856
+ reconnectAttempts = 0;
1857
+ };
2282
1858
 
2283
- // Event: Send button
2284
- sendBtn.onclick = function() {
2285
- if (input.value) {
2286
- send(input.value + '\\r');
2287
- input.value = '';
2288
- }
2289
- input.focus();
1859
+ ws.onmessage = (e) => {
1860
+ try { handleMessage(JSON.parse(e.data)); } catch (err) { console.error(err); }
2290
1861
  };
2291
1862
 
2292
- // Event: Enter key in input
2293
- input.onkeydown = function(e) {
2294
- if (e.key === 'Enter') {
2295
- e.preventDefault();
2296
- sendBtn.click();
2297
- }
1863
+ ws.onclose = () => {
1864
+ setStatus('disconnected', 'Disconnected');
1865
+ connected = false;
1866
+ reconnectAttempts++;
1867
+ setTimeout(connect, Math.min(3000, reconnectAttempts * 500));
2298
1868
  };
2299
1869
 
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
- });
1870
+ ws.onerror = () => setStatus('disconnected', 'Connection error');
1871
+ }
1872
+
1873
+ function handleMessage(msg) {
1874
+ switch (msg.type) {
1875
+ case 'auth:required':
1876
+ ws.send(JSON.stringify({ type: 'auth', token }));
1877
+ break;
1878
+ case 'auth:success':
1879
+ connected = true;
1880
+ authOverlay.classList.add('hidden');
1881
+ setStatus('connected', 'Connected');
1882
+ sessionSpan.textContent = sessionId ? 'Session: ' + sessionId : '';
1883
+ term.io.writeUTF8('\r
1884
+ \x1B[32m\u2713 Connected to NikCLI\x1B[0m\r
1885
+ ');
1886
+ break;
1887
+ case 'auth:failed':
1888
+ authMsg.classList.add('error');
1889
+ authText.textContent = 'Authentication failed';
1890
+ break;
1891
+ case 'terminal:output':
1892
+ if (msg.payload?.data) term.io.writeUTF8(msg.payload.data);
1893
+ break;
1894
+ case 'terminal:exit':
1895
+ term.io.writeUTF8('\r
1896
+ \x1B[33m[Process exited: ' + (msg.payload?.code || 0) + ']\x1B[0m\r
1897
+ ');
1898
+ break;
1899
+ }
1900
+ }
2315
1901
 
2316
- // Heartbeat
2317
- setInterval(function() {
2318
- if (ws && ws.readyState === WebSocket.OPEN) {
2319
- ws.send(JSON.stringify({ type: 'ping' }));
2320
- }
2321
- }, 25000);
1902
+ function setStatus(state, text) {
1903
+ statusDot.className = 'status-dot ' + state;
1904
+ statusText.textContent = text;
1905
+ }
2322
1906
 
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
- }
2330
- }
1907
+ function handleSubmit(e) {
1908
+ e?.preventDefault();
1909
+ const value = cmdInput.value.trim();
1910
+ if (!value || !connected) return;
1911
+ cmdInput.value = '';
1912
+ term.io.writeUTF8('$ ' + value + '\r
1913
+ ');
1914
+ ws.send(JSON.stringify({ type: 'terminal:input', data: value + '\r' }));
1915
+ setTimeout(() => cmdInput.focus(), 50);
1916
+ return false;
1917
+ }
2331
1918
 
2332
- window.addEventListener('resize', sendResize);
2333
- setTimeout(sendResize, 1000);
1919
+ function send(cmd) {
1920
+ if (!connected) return;
1921
+ term.io.writeUTF8('$ ' + cmd + '\r
1922
+ ');
1923
+ ws.send(JSON.stringify({ type: 'terminal:input', data: cmd + '\r' }));
1924
+ }
2334
1925
 
2335
- // Start connection
2336
- if (token) {
2337
- connect();
2338
- } else {
2339
- authStatus.textContent = 'Invalid session URL';
2340
- authStatus.classList.add('error');
1926
+ cmdInput.addEventListener('keydown', (e) => {
1927
+ if (e.key === 'Enter' && !e.shiftKey) {
1928
+ e.preventDefault();
1929
+ handleSubmit();
2341
1930
  }
2342
- })();
1931
+ });
1932
+
1933
+ document.getElementById('terminal')?.addEventListener('click', () => {
1934
+ if (connected) cmdInput.focus();
1935
+ });
2343
1936
  </script>
2344
1937
  </body>
2345
1938
  </html>`;
@@ -2419,6 +2012,7 @@ var RemoteServer = class extends EventEmitter2 {
2419
2012
  });
2420
2013
  this.terminal.on("data", (data) => {
2421
2014
  this.broadcast({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data } });
2015
+ this.emit("terminal:output", data);
2422
2016
  });
2423
2017
  this.terminal.on("exit", (code) => {
2424
2018
  this.broadcast({ type: MessageTypes.TERMINAL_EXIT, payload: { code } });
@@ -2635,9 +2229,6 @@ var RemoteServer = class extends EventEmitter2 {
2635
2229
  * Handle authentication
2636
2230
  */
2637
2231
  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
2232
  if (token === this.sessionSecret) {
2642
2233
  client.authenticated = true;
2643
2234
  if (this.session) {
@@ -2658,9 +2249,7 @@ var RemoteServer = class extends EventEmitter2 {
2658
2249
  this.terminal?.start();
2659
2250
  }
2660
2251
  this.emit("client:connected", client.device);
2661
- console.log(`[Tunnel] Auth success for ${client.id}`);
2662
2252
  } else {
2663
- console.log(`[Tunnel] Auth failed for ${client.id}`);
2664
2253
  client.ws.send(JSON.stringify({ type: MessageTypes.AUTH_FAILED, timestamp: Date.now() }));
2665
2254
  setTimeout(() => client.ws.close(1008, "Authentication failed"), 100);
2666
2255
  }
@@ -2779,9 +2368,6 @@ export {
2779
2368
  DEFAULT_CONFIG,
2780
2369
  MessageTypes,
2781
2370
  TerminalManager,
2782
- TunnelManager,
2783
- checkTunnelAvailability,
2784
- findAvailableTunnel,
2785
2371
  getWebClient,
2786
2372
  RemoteServer
2787
2373
  };