nikcli-remote 1.0.8 → 1.0.10
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/chunk-GI5RMYH6.js +37 -0
- package/dist/chunk-TIYMAVGV.js +1126 -0
- package/dist/index.cjs +1820 -2881
- package/dist/index.d.cts +91 -65
- package/dist/index.d.ts +91 -65
- package/dist/index.js +228 -7
- package/dist/{localtunnel-XT32JGNN.js → localtunnel-6DCQIYU6.js} +1 -1
- package/dist/server-O3KTQ4KJ.js +7 -0
- package/package.json +1 -1
- package/src/index.ts +57 -12
- package/src/server.ts +83 -110
- package/src/tunnel.ts +24 -0
- package/src/web-client.ts +593 -236
- package/dist/chunk-FYVPBMXV.js +0 -2390
- package/dist/chunk-MCKGQKYU.js +0 -15
- package/dist/server-OYMSDTRP.js +0 -7
package/src/web-client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @nikcli/remote -
|
|
3
|
-
*
|
|
2
|
+
* @nikcli/remote - Mobile Web Client
|
|
3
|
+
* Touch-friendly terminal interface for mobile devices
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export function getWebClient(): string {
|
|
@@ -8,293 +8,650 @@ export function getWebClient(): string {
|
|
|
8
8
|
<html lang="en">
|
|
9
9
|
<head>
|
|
10
10
|
<meta charset="UTF-8">
|
|
11
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
12
12
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
13
|
-
<meta name="
|
|
13
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
14
|
+
<meta name="theme-color" content="#0d1117">
|
|
14
15
|
<title>NikCLI Remote</title>
|
|
15
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
|
16
16
|
<style>
|
|
17
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
18
17
|
:root {
|
|
19
|
-
--bg
|
|
18
|
+
--bg: #0d1117;
|
|
20
19
|
--bg-secondary: #161b22;
|
|
20
|
+
--fg: #e6edf3;
|
|
21
|
+
--fg-muted: #8b949e;
|
|
21
22
|
--accent: #58a6ff;
|
|
22
23
|
--success: #3fb950;
|
|
24
|
+
--warning: #d29922;
|
|
25
|
+
--error: #f85149;
|
|
23
26
|
--border: #30363d;
|
|
27
|
+
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
* {
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
-webkit-tap-highlight-color: transparent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
html, body {
|
|
38
|
+
height: 100%;
|
|
39
|
+
background: var(--bg);
|
|
40
|
+
color: var(--fg);
|
|
41
|
+
font-family: var(--font-mono);
|
|
42
|
+
font-size: 14px;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
touch-action: manipulation;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#app {
|
|
30
48
|
display: flex;
|
|
31
49
|
flex-direction: column;
|
|
50
|
+
height: 100%;
|
|
51
|
+
height: 100dvh;
|
|
32
52
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
|
|
54
|
+
/* Header */
|
|
55
|
+
#header {
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: space-between;
|
|
37
59
|
padding: 12px 16px;
|
|
60
|
+
background: var(--bg-secondary);
|
|
61
|
+
border-bottom: 1px solid var(--border);
|
|
38
62
|
flex-shrink: 0;
|
|
39
|
-
padding-bottom: env(safe-area-inset-bottom, 12px);
|
|
40
63
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
|
|
65
|
+
#header h1 {
|
|
66
|
+
font-size: 16px;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
color: var(--accent);
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: 8px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#header h1::before {
|
|
75
|
+
content: '';
|
|
76
|
+
width: 10px;
|
|
77
|
+
height: 10px;
|
|
78
|
+
background: var(--accent);
|
|
79
|
+
border-radius: 2px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#status {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 6px;
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
color: var(--fg-muted);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#status-dot {
|
|
91
|
+
width: 8px;
|
|
92
|
+
height: 8px;
|
|
93
|
+
border-radius: 50%;
|
|
94
|
+
background: var(--error);
|
|
95
|
+
transition: background 0.3s;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#status-dot.connected {
|
|
99
|
+
background: var(--success);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#status-dot.connecting {
|
|
103
|
+
background: var(--warning);
|
|
104
|
+
animation: pulse 1s infinite;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@keyframes pulse {
|
|
108
|
+
0%, 100% { opacity: 1; }
|
|
109
|
+
50% { opacity: 0.5; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Terminal */
|
|
113
|
+
#terminal-container {
|
|
44
114
|
flex: 1;
|
|
45
|
-
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
position: relative;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#terminal {
|
|
120
|
+
height: 100%;
|
|
121
|
+
padding: 12px;
|
|
122
|
+
overflow-y: auto;
|
|
123
|
+
overflow-x: hidden;
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
line-height: 1.5;
|
|
126
|
+
white-space: pre-wrap;
|
|
127
|
+
word-break: break-all;
|
|
128
|
+
-webkit-overflow-scrolling: touch;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#terminal::-webkit-scrollbar {
|
|
132
|
+
width: 6px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#terminal::-webkit-scrollbar-track {
|
|
136
|
+
background: var(--bg);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#terminal::-webkit-scrollbar-thumb {
|
|
140
|
+
background: var(--border);
|
|
141
|
+
border-radius: 3px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.cursor {
|
|
145
|
+
display: inline-block;
|
|
146
|
+
width: 8px;
|
|
147
|
+
height: 16px;
|
|
148
|
+
background: var(--fg);
|
|
149
|
+
animation: blink 1s step-end infinite;
|
|
150
|
+
vertical-align: text-bottom;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@keyframes blink {
|
|
154
|
+
50% { opacity: 0; }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Notifications */
|
|
158
|
+
#notifications {
|
|
159
|
+
position: fixed;
|
|
160
|
+
top: 60px;
|
|
161
|
+
left: 12px;
|
|
162
|
+
right: 12px;
|
|
163
|
+
z-index: 1000;
|
|
164
|
+
pointer-events: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.notification {
|
|
168
|
+
background: var(--bg-secondary);
|
|
46
169
|
border: 1px solid var(--border);
|
|
47
|
-
border-radius:
|
|
170
|
+
border-radius: 8px;
|
|
48
171
|
padding: 12px 16px;
|
|
49
|
-
|
|
172
|
+
margin-bottom: 8px;
|
|
173
|
+
animation: slideIn 0.3s ease;
|
|
174
|
+
pointer-events: auto;
|
|
175
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.notification.success { border-left: 3px solid var(--success); }
|
|
179
|
+
.notification.error { border-left: 3px solid var(--error); }
|
|
180
|
+
.notification.warning { border-left: 3px solid var(--warning); }
|
|
181
|
+
.notification.info { border-left: 3px solid var(--accent); }
|
|
182
|
+
|
|
183
|
+
.notification h4 {
|
|
184
|
+
font-size: 14px;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
margin-bottom: 4px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.notification p {
|
|
190
|
+
font-size: 12px;
|
|
191
|
+
color: var(--fg-muted);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@keyframes slideIn {
|
|
195
|
+
from { transform: translateY(-20px); opacity: 0; }
|
|
196
|
+
to { transform: translateY(0); opacity: 1; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Quick Keys */
|
|
200
|
+
#quickkeys {
|
|
201
|
+
display: grid;
|
|
202
|
+
grid-template-columns: repeat(6, 1fr);
|
|
203
|
+
gap: 6px;
|
|
204
|
+
padding: 8px 12px;
|
|
205
|
+
background: var(--bg-secondary);
|
|
206
|
+
border-top: 1px solid var(--border);
|
|
207
|
+
flex-shrink: 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.qkey {
|
|
211
|
+
background: var(--bg);
|
|
212
|
+
border: 1px solid var(--border);
|
|
213
|
+
border-radius: 6px;
|
|
214
|
+
padding: 10px 4px;
|
|
215
|
+
color: var(--fg);
|
|
216
|
+
font-size: 11px;
|
|
217
|
+
font-family: var(--font-mono);
|
|
218
|
+
text-align: center;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
user-select: none;
|
|
221
|
+
transition: background 0.1s, transform 0.1s;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.qkey:active {
|
|
225
|
+
background: var(--border);
|
|
226
|
+
transform: scale(0.95);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.qkey.wide {
|
|
230
|
+
grid-column: span 2;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.qkey.accent {
|
|
234
|
+
background: var(--accent);
|
|
235
|
+
border-color: var(--accent);
|
|
236
|
+
color: #fff;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Input */
|
|
240
|
+
#input-container {
|
|
241
|
+
padding: 12px;
|
|
242
|
+
background: var(--bg-secondary);
|
|
243
|
+
border-top: 1px solid var(--border);
|
|
244
|
+
flex-shrink: 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#input-row {
|
|
248
|
+
display: flex;
|
|
249
|
+
gap: 8px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#input {
|
|
253
|
+
flex: 1;
|
|
254
|
+
background: var(--bg);
|
|
255
|
+
border: 1px solid var(--border);
|
|
256
|
+
border-radius: 8px;
|
|
257
|
+
padding: 12px 14px;
|
|
258
|
+
color: var(--fg);
|
|
259
|
+
font-family: var(--font-mono);
|
|
50
260
|
font-size: 16px;
|
|
51
|
-
font-family: 'SF Mono', Monaco, monospace;
|
|
52
261
|
outline: none;
|
|
53
|
-
|
|
262
|
+
transition: border-color 0.2s;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#input:focus {
|
|
266
|
+
border-color: var(--accent);
|
|
54
267
|
}
|
|
55
|
-
|
|
56
|
-
#
|
|
268
|
+
|
|
269
|
+
#input::placeholder {
|
|
270
|
+
color: var(--fg-muted);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#send {
|
|
57
274
|
background: var(--accent);
|
|
58
|
-
color:
|
|
275
|
+
color: #fff;
|
|
59
276
|
border: none;
|
|
60
|
-
border-radius:
|
|
61
|
-
padding: 12px
|
|
62
|
-
font-size:
|
|
277
|
+
border-radius: 8px;
|
|
278
|
+
padding: 12px 20px;
|
|
279
|
+
font-size: 14px;
|
|
63
280
|
font-weight: 600;
|
|
281
|
+
font-family: var(--font-mono);
|
|
64
282
|
cursor: pointer;
|
|
65
|
-
|
|
283
|
+
transition: opacity 0.2s, transform 0.1s;
|
|
66
284
|
}
|
|
67
|
-
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
285
|
+
|
|
286
|
+
#send:active {
|
|
287
|
+
opacity: 0.8;
|
|
288
|
+
transform: scale(0.98);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Auth Screen */
|
|
292
|
+
#auth-screen {
|
|
293
|
+
position: fixed;
|
|
294
|
+
inset: 0;
|
|
295
|
+
background: var(--bg);
|
|
72
296
|
display: flex;
|
|
73
|
-
|
|
297
|
+
flex-direction: column;
|
|
74
298
|
align-items: center;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@
|
|
108
|
-
#input-
|
|
109
|
-
|
|
299
|
+
justify-content: center;
|
|
300
|
+
gap: 20px;
|
|
301
|
+
z-index: 2000;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#auth-screen.hidden {
|
|
305
|
+
display: none;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.spinner {
|
|
309
|
+
width: 40px;
|
|
310
|
+
height: 40px;
|
|
311
|
+
border: 3px solid var(--border);
|
|
312
|
+
border-top-color: var(--accent);
|
|
313
|
+
border-radius: 50%;
|
|
314
|
+
animation: spin 1s linear infinite;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@keyframes spin {
|
|
318
|
+
to { transform: rotate(360deg); }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#auth-screen p {
|
|
322
|
+
color: var(--fg-muted);
|
|
323
|
+
font-size: 14px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#auth-screen .error {
|
|
327
|
+
color: var(--error);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* Safe area padding for notched devices */
|
|
331
|
+
@supports (padding: env(safe-area-inset-bottom)) {
|
|
332
|
+
#input-container {
|
|
333
|
+
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Landscape adjustments */
|
|
338
|
+
@media (max-height: 500px) {
|
|
339
|
+
#quickkeys {
|
|
340
|
+
grid-template-columns: repeat(12, 1fr);
|
|
341
|
+
padding: 6px 8px;
|
|
342
|
+
}
|
|
343
|
+
.qkey {
|
|
344
|
+
padding: 8px 2px;
|
|
345
|
+
font-size: 10px;
|
|
346
|
+
}
|
|
347
|
+
#terminal {
|
|
348
|
+
font-size: 12px;
|
|
349
|
+
}
|
|
110
350
|
}
|
|
111
351
|
</style>
|
|
112
352
|
</head>
|
|
113
353
|
<body>
|
|
114
|
-
<div id="
|
|
115
|
-
<div
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<div id="auth-text">Connecting...</div>
|
|
119
|
-
</div>
|
|
120
|
-
<div class="quick-btns">
|
|
121
|
-
<button class="quick-btn" onclick="send('help')">/help</button>
|
|
122
|
-
<button class="quick-btn" onclick="send('ls -la')">ls -la</button>
|
|
123
|
-
<button class="quick-btn" onclick="send('pwd')">pwd</button>
|
|
124
|
-
<button class="quick-btn" onclick="send('whoami')">whoami</button>
|
|
125
|
-
<button class="quick-btn" onclick="send('clear')">clear</button>
|
|
354
|
+
<div id="app">
|
|
355
|
+
<div id="auth-screen">
|
|
356
|
+
<div class="spinner"></div>
|
|
357
|
+
<p id="auth-status">Connecting to NikCLI...</p>
|
|
126
358
|
</div>
|
|
127
|
-
<div class="hint">Mobile keyboard to type commands</div>
|
|
128
|
-
</div>
|
|
129
359
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
|
|
360
|
+
<header id="header">
|
|
361
|
+
<h1>NikCLI Remote</h1>
|
|
362
|
+
<div id="status">
|
|
363
|
+
<span id="status-dot" class="connecting"></span>
|
|
364
|
+
<span id="status-text">Connecting</span>
|
|
365
|
+
</div>
|
|
366
|
+
</header>
|
|
367
|
+
|
|
368
|
+
<div id="terminal-container">
|
|
369
|
+
<div id="terminal"></div>
|
|
134
370
|
</div>
|
|
135
|
-
<span id="session-id" style="color: #8b949e;"></span>
|
|
136
|
-
</div>
|
|
137
371
|
|
|
138
|
-
|
|
372
|
+
<div id="notifications"></div>
|
|
373
|
+
|
|
374
|
+
<div id="quickkeys">
|
|
375
|
+
<button class="qkey" data-key="\\t">Tab</button>
|
|
376
|
+
<button class="qkey" data-key="\\x1b[A">↑</button>
|
|
377
|
+
<button class="qkey" data-key="\\x1b[B">↓</button>
|
|
378
|
+
<button class="qkey" data-key="\\x1b[D">←</button>
|
|
379
|
+
<button class="qkey" data-key="\\x1b[C">→</button>
|
|
380
|
+
<button class="qkey" data-key="\\x1b">Esc</button>
|
|
381
|
+
<button class="qkey" data-key="\\x03">^C</button>
|
|
382
|
+
<button class="qkey" data-key="\\x04">^D</button>
|
|
383
|
+
<button class="qkey" data-key="\\x1a">^Z</button>
|
|
384
|
+
<button class="qkey" data-key="\\x0c">^L</button>
|
|
385
|
+
<button class="qkey wide accent" data-key="\\r">Enter ⏎</button>
|
|
386
|
+
</div>
|
|
139
387
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
</
|
|
388
|
+
<div id="input-container">
|
|
389
|
+
<div id="input-row">
|
|
390
|
+
<input type="text" id="input" placeholder="Type command..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
|
391
|
+
<button id="send">Send</button>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
146
394
|
</div>
|
|
147
395
|
|
|
148
|
-
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
|
149
|
-
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
|
150
396
|
<script>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const sessionId = new URLSearchParams(location.search).get('s') || '';
|
|
154
|
-
|
|
155
|
-
const authOverlay = document.getElementById('auth-overlay');
|
|
156
|
-
const authMsg = document.getElementById('auth-msg');
|
|
157
|
-
const authText = document.getElementById('auth-text');
|
|
158
|
-
const statusDot = document.getElementById('status-dot');
|
|
159
|
-
const statusText = document.getElementById('status-text');
|
|
160
|
-
const sessionSpan = document.getElementById('session-id');
|
|
161
|
-
const cmdInput = document.getElementById('cmd-input');
|
|
162
|
-
|
|
163
|
-
// Initialize xterm.js
|
|
164
|
-
term = new Terminal({
|
|
165
|
-
cursorBlink: true,
|
|
166
|
-
fontSize: 14,
|
|
167
|
-
fontFamily: '"SF Mono", Monaco, Consolas, monospace',
|
|
168
|
-
theme: {
|
|
169
|
-
background: '#0d1117',
|
|
170
|
-
foreground: '#e6edf3',
|
|
171
|
-
cursor: '#3fb950',
|
|
172
|
-
selectionBackground: '#264f78',
|
|
173
|
-
black: '#484f58',
|
|
174
|
-
red: '#f85149',
|
|
175
|
-
green: '#3fb950',
|
|
176
|
-
yellow: '#d29922',
|
|
177
|
-
blue: '#58a6ff',
|
|
178
|
-
magenta: '#a371f7',
|
|
179
|
-
cyan: '#39c5cf',
|
|
180
|
-
white: '#e6edf3',
|
|
181
|
-
brightBlack: '#6e7681',
|
|
182
|
-
brightRed: '#ffa198',
|
|
183
|
-
brightGreen: '#7ee787',
|
|
184
|
-
brightYellow: '#f0883e',
|
|
185
|
-
brightBlue: '#79c0ff',
|
|
186
|
-
brightMagenta: '#d2a8ff',
|
|
187
|
-
brightCyan: '#56d4db',
|
|
188
|
-
brightWhite: '#f0f6fc'
|
|
189
|
-
},
|
|
190
|
-
convertEol: true
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
fitAddon = new FitAddon.FitAddon();
|
|
194
|
-
term.loadAddon(fitAddon);
|
|
195
|
-
term.open(document.getElementById('terminal'));
|
|
196
|
-
fitAddon.fit();
|
|
197
|
-
term.writeln('Initializing NikCLI Remote...');
|
|
198
|
-
term.writeln('Type commands below');
|
|
199
|
-
|
|
200
|
-
// Resize handler
|
|
201
|
-
window.addEventListener('resize', () => {
|
|
202
|
-
clearTimeout(window.resizeTimer);
|
|
203
|
-
window.resizeTimer = setTimeout(() => fitAddon.fit(), 100);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Visual viewport for mobile keyboard
|
|
207
|
-
if (window.visualViewport) {
|
|
208
|
-
window.visualViewport.addEventListener('resize', () => {
|
|
209
|
-
clearTimeout(window.resizeTimer);
|
|
210
|
-
window.resizeTimer = setTimeout(() => fitAddon.fit(), 100);
|
|
211
|
-
});
|
|
212
|
-
}
|
|
397
|
+
(function() {
|
|
398
|
+
'use strict';
|
|
213
399
|
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
400
|
+
// Parse URL params
|
|
401
|
+
const params = new URLSearchParams(location.search);
|
|
402
|
+
const token = params.get('t');
|
|
403
|
+
const sessionId = params.get('s');
|
|
217
404
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
405
|
+
// DOM elements
|
|
406
|
+
const terminal = document.getElementById('terminal');
|
|
407
|
+
const input = document.getElementById('input');
|
|
408
|
+
const sendBtn = document.getElementById('send');
|
|
409
|
+
const statusDot = document.getElementById('status-dot');
|
|
410
|
+
const statusText = document.getElementById('status-text');
|
|
411
|
+
const authScreen = document.getElementById('auth-screen');
|
|
412
|
+
const authStatus = document.getElementById('auth-status');
|
|
413
|
+
const notifications = document.getElementById('notifications');
|
|
223
414
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
415
|
+
// State
|
|
416
|
+
let ws = null;
|
|
417
|
+
let reconnectAttempts = 0;
|
|
418
|
+
const maxReconnectAttempts = 5;
|
|
419
|
+
let terminalEnabled = true;
|
|
227
420
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
setTimeout(connect, Math.min(3000, reconnectAttempts * 500));
|
|
233
|
-
};
|
|
421
|
+
// Connect to WebSocket
|
|
422
|
+
function connect() {
|
|
423
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
424
|
+
ws = new WebSocket(protocol + '//' + location.host);
|
|
234
425
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
426
|
+
ws.onopen = function() {
|
|
427
|
+
setStatus('connecting', 'Authenticating...');
|
|
428
|
+
ws.send(JSON.stringify({ type: 'auth', token: token }));
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
ws.onmessage = function(event) {
|
|
432
|
+
try {
|
|
433
|
+
const msg = JSON.parse(event.data);
|
|
434
|
+
handleMessage(msg);
|
|
435
|
+
} catch (e) {
|
|
436
|
+
console.error('Parse error:', e);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
ws.onclose = function() {
|
|
441
|
+
setStatus('disconnected', 'Disconnected');
|
|
442
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
443
|
+
reconnectAttempts++;
|
|
444
|
+
const delay = Math.min(2000 * reconnectAttempts, 10000);
|
|
445
|
+
setTimeout(connect, delay);
|
|
446
|
+
} else {
|
|
447
|
+
authStatus.textContent = 'Connection failed. Refresh to retry.';
|
|
448
|
+
authStatus.classList.add('error');
|
|
449
|
+
authScreen.classList.remove('hidden');
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
ws.onerror = function() {
|
|
454
|
+
console.error('WebSocket error');
|
|
455
|
+
};
|
|
260
456
|
}
|
|
261
|
-
}
|
|
262
457
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
458
|
+
// Handle incoming message
|
|
459
|
+
function handleMessage(msg) {
|
|
460
|
+
switch (msg.type) {
|
|
461
|
+
case 'auth:required':
|
|
462
|
+
// Already sent auth on open
|
|
463
|
+
break;
|
|
267
464
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
465
|
+
case 'auth:success':
|
|
466
|
+
authScreen.classList.add('hidden');
|
|
467
|
+
setStatus('connected', 'Connected');
|
|
468
|
+
reconnectAttempts = 0;
|
|
469
|
+
terminalEnabled = msg.payload?.terminalEnabled !== false;
|
|
470
|
+
if (terminalEnabled) {
|
|
471
|
+
appendOutput('\\x1b[32mConnected to NikCLI\\x1b[0m\\n\\n');
|
|
472
|
+
}
|
|
473
|
+
break;
|
|
278
474
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
475
|
+
case 'auth:failed':
|
|
476
|
+
authStatus.textContent = 'Authentication failed';
|
|
477
|
+
authStatus.classList.add('error');
|
|
478
|
+
break;
|
|
479
|
+
|
|
480
|
+
case 'terminal:output':
|
|
481
|
+
if (msg.payload?.data) {
|
|
482
|
+
appendOutput(msg.payload.data);
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case 'terminal:exit':
|
|
487
|
+
appendOutput('\\n\\x1b[33m[Process exited with code ' + (msg.payload?.code || 0) + ']\\x1b[0m\\n');
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case 'notification':
|
|
491
|
+
showNotification(msg.payload);
|
|
492
|
+
break;
|
|
493
|
+
|
|
494
|
+
case 'session:end':
|
|
495
|
+
appendOutput('\\n\\x1b[31m[Session ended]\\x1b[0m\\n');
|
|
496
|
+
setStatus('disconnected', 'Session ended');
|
|
497
|
+
break;
|
|
284
498
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
499
|
+
case 'pong':
|
|
500
|
+
// Heartbeat response
|
|
501
|
+
break;
|
|
502
|
+
|
|
503
|
+
default:
|
|
504
|
+
console.log('Unknown message:', msg.type);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Append text to terminal with ANSI support
|
|
509
|
+
function appendOutput(text) {
|
|
510
|
+
// Simple ANSI to HTML conversion
|
|
511
|
+
const html = ansiToHtml(text);
|
|
512
|
+
terminal.innerHTML += html;
|
|
513
|
+
terminal.scrollTop = terminal.scrollHeight;
|
|
289
514
|
}
|
|
290
|
-
});
|
|
291
515
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
516
|
+
// Basic ANSI to HTML
|
|
517
|
+
function ansiToHtml(text) {
|
|
518
|
+
const ansiColors = {
|
|
519
|
+
'30': '#6e7681', '31': '#f85149', '32': '#3fb950', '33': '#d29922',
|
|
520
|
+
'34': '#58a6ff', '35': '#bc8cff', '36': '#76e3ea', '37': '#e6edf3',
|
|
521
|
+
'90': '#6e7681', '91': '#f85149', '92': '#3fb950', '93': '#d29922',
|
|
522
|
+
'94': '#58a6ff', '95': '#bc8cff', '96': '#76e3ea', '97': '#ffffff'
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
let result = '';
|
|
526
|
+
let currentStyle = '';
|
|
527
|
+
|
|
528
|
+
const parts = text.split(/\\x1b\\[([0-9;]+)m/);
|
|
529
|
+
for (let i = 0; i < parts.length; i++) {
|
|
530
|
+
if (i % 2 === 0) {
|
|
531
|
+
// Text content
|
|
532
|
+
result += escapeHtml(parts[i]);
|
|
533
|
+
} else {
|
|
534
|
+
// ANSI code
|
|
535
|
+
const codes = parts[i].split(';');
|
|
536
|
+
for (const code of codes) {
|
|
537
|
+
if (code === '0') {
|
|
538
|
+
if (currentStyle) {
|
|
539
|
+
result += '</span>';
|
|
540
|
+
currentStyle = '';
|
|
541
|
+
}
|
|
542
|
+
} else if (code === '1') {
|
|
543
|
+
currentStyle = 'font-weight:bold;';
|
|
544
|
+
result += '<span style="' + currentStyle + '">';
|
|
545
|
+
} else if (ansiColors[code]) {
|
|
546
|
+
if (currentStyle) result += '</span>';
|
|
547
|
+
currentStyle = 'color:' + ansiColors[code] + ';';
|
|
548
|
+
result += '<span style="' + currentStyle + '">';
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
295
553
|
|
|
296
|
-
|
|
554
|
+
if (currentStyle) result += '</span>';
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Escape HTML
|
|
559
|
+
function escapeHtml(text) {
|
|
560
|
+
return text
|
|
561
|
+
.replace(/&/g, '&')
|
|
562
|
+
.replace(/</g, '<')
|
|
563
|
+
.replace(/>/g, '>')
|
|
564
|
+
.replace(/"/g, '"')
|
|
565
|
+
.replace(/\\n/g, '<br>')
|
|
566
|
+
.replace(/ /g, ' ');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Set connection status
|
|
570
|
+
function setStatus(state, text) {
|
|
571
|
+
statusDot.className = state === 'connected' ? 'connected' :
|
|
572
|
+
state === 'connecting' ? 'connecting' : '';
|
|
573
|
+
statusText.textContent = text;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Send data to terminal
|
|
577
|
+
function send(data) {
|
|
578
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
579
|
+
ws.send(JSON.stringify({ type: 'terminal:input', data: data }));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Show notification
|
|
584
|
+
function showNotification(n) {
|
|
585
|
+
if (!n) return;
|
|
586
|
+
const el = document.createElement('div');
|
|
587
|
+
el.className = 'notification ' + (n.type || 'info');
|
|
588
|
+
el.innerHTML = '<h4>' + escapeHtml(n.title || 'Notification') + '</h4>' +
|
|
589
|
+
'<p>' + escapeHtml(n.body || '') + '</p>';
|
|
590
|
+
notifications.appendChild(el);
|
|
591
|
+
setTimeout(function() { el.remove(); }, 5000);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Event: Send button
|
|
595
|
+
sendBtn.onclick = function() {
|
|
596
|
+
if (input.value) {
|
|
597
|
+
send(input.value + '\\r');
|
|
598
|
+
input.value = '';
|
|
599
|
+
}
|
|
600
|
+
input.focus();
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// Event: Enter key in input
|
|
604
|
+
input.onkeydown = function(e) {
|
|
605
|
+
if (e.key === 'Enter') {
|
|
606
|
+
e.preventDefault();
|
|
607
|
+
sendBtn.click();
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// Event: Quick keys
|
|
612
|
+
document.querySelectorAll('.qkey').forEach(function(btn) {
|
|
613
|
+
btn.onclick = function() {
|
|
614
|
+
const key = btn.dataset.key;
|
|
615
|
+
const decoded = key
|
|
616
|
+
.replace(/\\\\x([0-9a-f]{2})/gi, function(_, hex) {
|
|
617
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
618
|
+
})
|
|
619
|
+
.replace(/\\\\t/g, '\\t')
|
|
620
|
+
.replace(/\\\\r/g, '\\r')
|
|
621
|
+
.replace(/\\\\n/g, '\\n');
|
|
622
|
+
send(decoded);
|
|
623
|
+
input.focus();
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Heartbeat
|
|
628
|
+
setInterval(function() {
|
|
629
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
630
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
631
|
+
}
|
|
632
|
+
}, 25000);
|
|
633
|
+
|
|
634
|
+
// Handle resize
|
|
635
|
+
function sendResize() {
|
|
636
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
637
|
+
const cols = Math.floor(terminal.clientWidth / 8);
|
|
638
|
+
const rows = Math.floor(terminal.clientHeight / 18);
|
|
639
|
+
ws.send(JSON.stringify({ type: 'terminal:resize', cols: cols, rows: rows }));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
window.addEventListener('resize', sendResize);
|
|
644
|
+
setTimeout(sendResize, 1000);
|
|
645
|
+
|
|
646
|
+
// Start connection
|
|
647
|
+
if (token) {
|
|
648
|
+
connect();
|
|
649
|
+
} else {
|
|
650
|
+
authStatus.textContent = 'Invalid session URL';
|
|
651
|
+
authStatus.classList.add('error');
|
|
652
|
+
}
|
|
653
|
+
})();
|
|
297
654
|
</script>
|
|
298
655
|
</body>
|
|
299
|
-
</html
|
|
656
|
+
</html>`
|
|
300
657
|
}
|