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.
- package/dist/{chunk-QLGPGAPC.js → chunk-ONRUR3Z7.js} +243 -637
- package/dist/index.cjs +244 -771
- package/dist/index.d.cts +3 -78
- package/dist/index.d.ts +3 -78
- package/dist/index.js +4 -135
- package/dist/{server-ISK4MDQQ.js → server-MURDBK6L.js} +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -61
- package/src/server.ts +3 -6
- package/src/tunnel.ts +11 -7
- package/src/web-client.ts +236 -593
package/src/web-client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @nikcli/remote -
|
|
3
|
-
*
|
|
2
|
+
* @nikcli/remote - Ghostty-web Style Terminal Client
|
|
3
|
+
* Uses xterm.js with full terminal emulation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export function getWebClient(): string {
|
|
@@ -8,650 +8,293 @@ 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, viewport-fit=cover">
|
|
12
12
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
13
|
-
<meta name="mobile-web-app-
|
|
14
|
-
<meta name="theme-color" content="#0d1117">
|
|
13
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
15
14
|
<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; }
|
|
17
18
|
:root {
|
|
18
|
-
--bg: #0d1117;
|
|
19
|
+
--bg-primary: #0d1117;
|
|
19
20
|
--bg-secondary: #161b22;
|
|
20
|
-
--fg: #e6edf3;
|
|
21
|
-
--fg-muted: #8b949e;
|
|
22
21
|
--accent: #58a6ff;
|
|
23
22
|
--success: #3fb950;
|
|
24
|
-
--warning: #d29922;
|
|
25
|
-
--error: #f85149;
|
|
26
23
|
--border: #30363d;
|
|
27
|
-
--font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
28
24
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 {
|
|
25
|
+
html, body { height: 100%; overflow: hidden; touch-action: manipulation; }
|
|
26
|
+
body {
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
28
|
+
background: var(--bg-primary);
|
|
29
|
+
color: #e6edf3;
|
|
48
30
|
display: flex;
|
|
49
31
|
flex-direction: column;
|
|
50
|
-
height: 100%;
|
|
51
|
-
height: 100dvh;
|
|
52
32
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#header {
|
|
56
|
-
display: flex;
|
|
57
|
-
align-items: center;
|
|
58
|
-
justify-content: space-between;
|
|
59
|
-
padding: 12px 16px;
|
|
60
|
-
background: var(--bg-secondary);
|
|
61
|
-
border-bottom: 1px solid var(--border);
|
|
62
|
-
flex-shrink: 0;
|
|
63
|
-
}
|
|
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 {
|
|
114
|
-
flex: 1;
|
|
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);
|
|
169
|
-
border: 1px solid var(--border);
|
|
170
|
-
border-radius: 8px;
|
|
171
|
-
padding: 12px 16px;
|
|
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;
|
|
33
|
+
#terminal { flex: 1; overflow: hidden; background: var(--bg-primary); padding: 8px; }
|
|
34
|
+
#input-area {
|
|
242
35
|
background: var(--bg-secondary);
|
|
243
36
|
border-top: 1px solid var(--border);
|
|
37
|
+
padding: 12px 16px;
|
|
244
38
|
flex-shrink: 0;
|
|
39
|
+
padding-bottom: env(safe-area-inset-bottom, 12px);
|
|
245
40
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
gap: 8px;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
#input {
|
|
41
|
+
.input-row { display: flex; gap: 8px; align-items: center; }
|
|
42
|
+
.prompt { color: var(--success); font-family: 'SF Mono', Monaco, monospace; font-size: 14px; white-space: nowrap; }
|
|
43
|
+
#cmd-input {
|
|
253
44
|
flex: 1;
|
|
254
|
-
background:
|
|
45
|
+
background: #21262d;
|
|
255
46
|
border: 1px solid var(--border);
|
|
256
|
-
border-radius:
|
|
257
|
-
padding: 12px
|
|
258
|
-
color:
|
|
259
|
-
font-family: var(--font-mono);
|
|
47
|
+
border-radius: 12px;
|
|
48
|
+
padding: 12px 16px;
|
|
49
|
+
color: #e6edf3;
|
|
260
50
|
font-size: 16px;
|
|
51
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
261
52
|
outline: none;
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
#input:focus {
|
|
266
|
-
border-color: var(--accent);
|
|
53
|
+
-webkit-appearance: none;
|
|
267
54
|
}
|
|
268
|
-
|
|
269
|
-
#
|
|
270
|
-
color: var(--fg-muted);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
#send {
|
|
55
|
+
#cmd-input:focus { border-color: var(--accent); }
|
|
56
|
+
#send-btn {
|
|
274
57
|
background: var(--accent);
|
|
275
|
-
color:
|
|
58
|
+
color: white;
|
|
276
59
|
border: none;
|
|
277
|
-
border-radius:
|
|
278
|
-
padding: 12px
|
|
279
|
-
font-size:
|
|
60
|
+
border-radius: 12px;
|
|
61
|
+
padding: 12px 24px;
|
|
62
|
+
font-size: 16px;
|
|
280
63
|
font-weight: 600;
|
|
281
|
-
font-family: var(--font-mono);
|
|
282
64
|
cursor: pointer;
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
#send:active {
|
|
287
|
-
opacity: 0.8;
|
|
288
|
-
transform: scale(0.98);
|
|
65
|
+
-webkit-tap-highlight-color: transparent;
|
|
289
66
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
background: var(--bg);
|
|
67
|
+
#send-btn:active { opacity: 0.7; }
|
|
68
|
+
#status-bar {
|
|
69
|
+
background: var(--bg-secondary);
|
|
70
|
+
border-bottom: 1px solid var(--border);
|
|
71
|
+
padding: 8px 16px;
|
|
296
72
|
display: flex;
|
|
297
|
-
|
|
73
|
+
justify-content: space-between;
|
|
298
74
|
align-items: center;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
@
|
|
332
|
-
#input-
|
|
333
|
-
|
|
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
|
-
}
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
padding-top: env(safe-area-inset-top, 8px);
|
|
77
|
+
}
|
|
78
|
+
.status-row { display: flex; align-items: center; gap: 8px; }
|
|
79
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #8b949e; }
|
|
80
|
+
.status-dot.connected { background: var(--success); box-shadow: 0 0 8px var(--success); }
|
|
81
|
+
.status-dot.connecting { background: var(--accent); animation: pulse 1s infinite; }
|
|
82
|
+
.status-dot.disconnected { background: #f85149; }
|
|
83
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
84
|
+
#auth-overlay {
|
|
85
|
+
position: fixed; inset: 0; background: var(--bg-primary);
|
|
86
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
87
|
+
padding: 20px; z-index: 100;
|
|
88
|
+
}
|
|
89
|
+
#auth-overlay.hidden { display: none; }
|
|
90
|
+
.auth-title { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
|
|
91
|
+
.auth-subtitle { color: #8b949e; font-size: 16px; margin-bottom: 24px; }
|
|
92
|
+
.auth-msg {
|
|
93
|
+
background: var(--bg-secondary); padding: 20px 28px;
|
|
94
|
+
border-radius: 16px; border: 1px solid var(--border); text-align: center;
|
|
95
|
+
}
|
|
96
|
+
.auth-msg.error { color: #f85149; border-color: #f85149; }
|
|
97
|
+
.quick-btns {
|
|
98
|
+
display: flex; gap: 8px; margin-top: 20px; flex-wrap: wrap; justify-content: center;
|
|
99
|
+
}
|
|
100
|
+
.quick-btn {
|
|
101
|
+
background: #21262d; border: 1px solid var(--border); color: #e6edf3;
|
|
102
|
+
padding: 10px 16px; border-radius: 8px; font-size: 14px;
|
|
103
|
+
font-family: 'SF Mono', Monaco, monospace; cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
.quick-btn:active { background: #30363d; }
|
|
106
|
+
.hint { font-size: 12px; color: #8b949e; margin-top: 12px; }
|
|
107
|
+
@media (max-width: 600px) {
|
|
108
|
+
#input-area { padding: 10px 12px; }
|
|
109
|
+
.quick-btn { padding: 8px 12px; font-size: 12px; }
|
|
350
110
|
}
|
|
351
111
|
</style>
|
|
352
112
|
</head>
|
|
353
113
|
<body>
|
|
354
|
-
<div id="
|
|
355
|
-
<div
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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>
|
|
114
|
+
<div id="auth-overlay">
|
|
115
|
+
<div class="auth-title">📱 NikCLI Remote</div>
|
|
116
|
+
<div class="auth-subtitle">Full terminal emulation</div>
|
|
117
|
+
<div id="auth-msg" class="auth-msg">
|
|
118
|
+
<div id="auth-text">Connecting...</div>
|
|
370
119
|
</div>
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<button class="
|
|
376
|
-
<button class="
|
|
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>
|
|
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>
|
|
386
126
|
</div>
|
|
127
|
+
<div class="hint">Mobile keyboard to type commands</div>
|
|
128
|
+
</div>
|
|
387
129
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
</div>
|
|
130
|
+
<div id="status-bar">
|
|
131
|
+
<div class="status-row">
|
|
132
|
+
<span class="status-dot" id="status-dot"></span>
|
|
133
|
+
<span id="status-text">Disconnected</span>
|
|
393
134
|
</div>
|
|
135
|
+
<span id="session-id" style="color: #8b949e;"></span>
|
|
394
136
|
</div>
|
|
395
137
|
|
|
396
|
-
<
|
|
397
|
-
(function() {
|
|
398
|
-
'use strict';
|
|
399
|
-
|
|
400
|
-
// Parse URL params
|
|
401
|
-
const params = new URLSearchParams(location.search);
|
|
402
|
-
const token = params.get('t');
|
|
403
|
-
const sessionId = params.get('s');
|
|
404
|
-
|
|
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');
|
|
414
|
-
|
|
415
|
-
// State
|
|
416
|
-
let ws = null;
|
|
417
|
-
let reconnectAttempts = 0;
|
|
418
|
-
const maxReconnectAttempts = 5;
|
|
419
|
-
let terminalEnabled = true;
|
|
420
|
-
|
|
421
|
-
// Connect to WebSocket
|
|
422
|
-
function connect() {
|
|
423
|
-
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
424
|
-
ws = new WebSocket(protocol + '//' + location.host);
|
|
425
|
-
|
|
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
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Handle incoming message
|
|
459
|
-
function handleMessage(msg) {
|
|
460
|
-
switch (msg.type) {
|
|
461
|
-
case 'auth:required':
|
|
462
|
-
// Already sent auth on open
|
|
463
|
-
break;
|
|
464
|
-
|
|
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;
|
|
474
|
-
|
|
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;
|
|
498
|
-
|
|
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;
|
|
514
|
-
}
|
|
138
|
+
<div id="terminal"></div>
|
|
515
139
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
let result = '';
|
|
526
|
-
let currentStyle = '';
|
|
140
|
+
<div id="input-area">
|
|
141
|
+
<form class="input-row" onsubmit="return handleSubmit(event)">
|
|
142
|
+
<span class="prompt">$</span>
|
|
143
|
+
<input type="text" id="cmd-input" placeholder="Type command..." autocomplete="off" enterkeyhint="send" inputmode="text">
|
|
144
|
+
<button type="submit" id="send-btn">Send</button>
|
|
145
|
+
</form>
|
|
146
|
+
</div>
|
|
527
147
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
+
<script>
|
|
151
|
+
let ws = null, term = null, fitAddon = null, connected = false, reconnectAttempts = 0;
|
|
152
|
+
const token = new URLSearchParams(location.search).get('t') || '';
|
|
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('\x1b[32mInitializing NikCLI Remote...\x1b[0m');
|
|
198
|
+
term.writeln('\x1b[90mType commands below\x1b[0m');
|
|
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
|
+
}
|
|
553
213
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
214
|
+
function connect() {
|
|
215
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
216
|
+
ws = new WebSocket(protocol + '//' + location.host + '/ws');
|
|
557
217
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
.replace(/>/g, '>')
|
|
564
|
-
.replace(/"/g, '"')
|
|
565
|
-
.replace(/\\n/g, '<br>')
|
|
566
|
-
.replace(/ /g, ' ');
|
|
567
|
-
}
|
|
218
|
+
ws.onopen = () => {
|
|
219
|
+
setStatus('connecting', 'Authenticating...');
|
|
220
|
+
ws.send(JSON.stringify({ type: 'auth', token }));
|
|
221
|
+
reconnectAttempts = 0;
|
|
222
|
+
};
|
|
568
223
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
state === 'connecting' ? 'connecting' : '';
|
|
573
|
-
statusText.textContent = text;
|
|
574
|
-
}
|
|
224
|
+
ws.onmessage = (e) => {
|
|
225
|
+
try { handleMessage(JSON.parse(e.data)); } catch (err) { console.error(err); }
|
|
226
|
+
};
|
|
575
227
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
228
|
+
ws.onclose = () => {
|
|
229
|
+
setStatus('disconnected', 'Disconnected');
|
|
230
|
+
connected = false;
|
|
231
|
+
reconnectAttempts++;
|
|
232
|
+
setTimeout(connect, Math.min(3000, reconnectAttempts * 500));
|
|
233
|
+
};
|
|
582
234
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
235
|
+
ws.onerror = () => setStatus('disconnected', 'Connection error');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function handleMessage(msg) {
|
|
239
|
+
switch (msg.type) {
|
|
240
|
+
case 'auth:required':
|
|
241
|
+
ws.send(JSON.stringify({ type: 'auth', token }));
|
|
242
|
+
break;
|
|
243
|
+
case 'auth:success':
|
|
244
|
+
connected = true;
|
|
245
|
+
authOverlay.classList.add('hidden');
|
|
246
|
+
setStatus('connected', 'Connected');
|
|
247
|
+
sessionSpan.textContent = sessionId ? 'Session: ' + sessionId : '';
|
|
248
|
+
term.writeln('\n\x1b[32m✓ Connected to NikCLI\x1b[0m\n');
|
|
249
|
+
break;
|
|
250
|
+
case 'auth:failed':
|
|
251
|
+
authMsg.classList.add('error');
|
|
252
|
+
authText.textContent = 'Authentication failed';
|
|
253
|
+
break;
|
|
254
|
+
case 'terminal:output':
|
|
255
|
+
if (msg.payload?.data) term.write(msg.payload.data);
|
|
256
|
+
break;
|
|
257
|
+
case 'terminal:exit':
|
|
258
|
+
term.writeln('\n\x1b[33m[Process exited: ' + (msg.payload?.code || 0) + ']\x1b[0m\n');
|
|
259
|
+
break;
|
|
592
260
|
}
|
|
261
|
+
}
|
|
593
262
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
};
|
|
263
|
+
function setStatus(state, text) {
|
|
264
|
+
statusDot.className = 'status-dot ' + state;
|
|
265
|
+
statusText.textContent = text;
|
|
266
|
+
}
|
|
610
267
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
.replace(/\\\\n/g, '\\n');
|
|
622
|
-
send(decoded);
|
|
623
|
-
input.focus();
|
|
624
|
-
};
|
|
625
|
-
});
|
|
268
|
+
function handleSubmit(e) {
|
|
269
|
+
e?.preventDefault();
|
|
270
|
+
const value = cmdInput.value.trim();
|
|
271
|
+
if (!value || !connected) return;
|
|
272
|
+
cmdInput.value = '';
|
|
273
|
+
term.write('$ ' + value + '\r\n');
|
|
274
|
+
ws.send(JSON.stringify({ type: 'terminal:input', data: value + '\r' }));
|
|
275
|
+
setTimeout(() => cmdInput.focus(), 50);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
626
278
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}, 25000);
|
|
279
|
+
function send(cmd) {
|
|
280
|
+
if (!connected) return;
|
|
281
|
+
term.write('$ ' + cmd + '\r\n');
|
|
282
|
+
ws.send(JSON.stringify({ type: 'terminal:input', data: cmd + '\r' }));
|
|
283
|
+
}
|
|
633
284
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const rows = Math.floor(terminal.clientHeight / 18);
|
|
639
|
-
ws.send(JSON.stringify({ type: 'terminal:resize', cols: cols, rows: rows }));
|
|
640
|
-
}
|
|
285
|
+
cmdInput.addEventListener('keydown', (e) => {
|
|
286
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
handleSubmit();
|
|
641
289
|
}
|
|
290
|
+
});
|
|
642
291
|
|
|
643
|
-
|
|
644
|
-
|
|
292
|
+
document.getElementById('terminal')?.addEventListener('click', () => {
|
|
293
|
+
if (connected) cmdInput.focus();
|
|
294
|
+
});
|
|
645
295
|
|
|
646
|
-
|
|
647
|
-
if (token) {
|
|
648
|
-
connect();
|
|
649
|
-
} else {
|
|
650
|
-
authStatus.textContent = 'Invalid session URL';
|
|
651
|
-
authStatus.classList.add('error');
|
|
652
|
-
}
|
|
653
|
-
})();
|
|
296
|
+
connect();
|
|
654
297
|
</script>
|
|
655
298
|
</body>
|
|
656
|
-
</html
|
|
299
|
+
</html>`;
|
|
657
300
|
}
|