nikcli-remote 1.0.9 → 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 +4688 -6106
- package/dist/index.d.cts +89 -63
- package/dist/index.d.ts +89 -63
- 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/dist/chunk-3IFHAOGG.js +0 -2747
- package/dist/chunk-MCKGQKYU.js +0 -15
- package/dist/server-MBJQBTJF.js +0 -7
package/src/server.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @nikcli/remote -
|
|
3
|
-
* WebSocket
|
|
2
|
+
* @nikcli/remote - Direct Terminal Streaming Server
|
|
3
|
+
* Proxies NikCLI's stdin/stdout to WebSocket clients for true real-time terminal access
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { EventEmitter } from 'node:events'
|
|
7
7
|
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
8
|
-
import { WebSocketServer, WebSocket } from 'ws'
|
|
8
|
+
import { WebSocketServer, WebSocket, type RawData } from 'ws'
|
|
9
9
|
import crypto from 'node:crypto'
|
|
10
10
|
import os from 'node:os'
|
|
11
11
|
import type {
|
|
@@ -16,12 +16,8 @@ import type {
|
|
|
16
16
|
RemoteNotification,
|
|
17
17
|
ClientConnection,
|
|
18
18
|
ClientMessage,
|
|
19
|
-
TunnelProvider,
|
|
20
19
|
} from './types'
|
|
21
20
|
import { DEFAULT_CONFIG, MessageTypes } from './types'
|
|
22
|
-
import { TerminalManager } from './terminal'
|
|
23
|
-
import { TunnelManager } from './tunnel'
|
|
24
|
-
import { getWebClient } from './web-client'
|
|
25
21
|
|
|
26
22
|
export interface RemoteServerEvents {
|
|
27
23
|
started: (session: RemoteSession) => void
|
|
@@ -31,9 +27,9 @@ export interface RemoteServerEvents {
|
|
|
31
27
|
'client:error': (clientId: string, error: Error) => void
|
|
32
28
|
'tunnel:connected': (url: string) => void
|
|
33
29
|
'tunnel:error': (error: Error) => void
|
|
34
|
-
command: (cmd: { clientId: string; command: string; args?: string[] }) => void
|
|
35
30
|
message: (client: ClientConnection, message: ClientMessage) => void
|
|
36
31
|
error: (error: Error) => void
|
|
32
|
+
'terminal:output': (data: string) => void
|
|
37
33
|
}
|
|
38
34
|
|
|
39
35
|
export class RemoteServer extends EventEmitter {
|
|
@@ -42,13 +38,15 @@ export class RemoteServer extends EventEmitter {
|
|
|
42
38
|
private wss: WebSocketServer | null = null
|
|
43
39
|
private clients: Map<string, ClientConnection & { ws: WebSocket }> = new Map()
|
|
44
40
|
private session: RemoteSession | null = null
|
|
45
|
-
private terminal: TerminalManager | null = null
|
|
46
|
-
private tunnel: TunnelManager | null = null
|
|
47
41
|
private heartbeatTimer: NodeJS.Timeout | null = null
|
|
48
42
|
private sessionTimeoutTimer: NodeJS.Timeout | null = null
|
|
49
43
|
private isRunning = false
|
|
50
44
|
private sessionSecret: string
|
|
51
45
|
|
|
46
|
+
// stdin/stdout proxy state
|
|
47
|
+
private originalStdoutWrite: ((data: string | Uint8Array, ...args: unknown[]) => boolean) | null = null
|
|
48
|
+
private originalStdinOn: ((event: string, listener: (...args: unknown[]) => void) => void) | null = null
|
|
49
|
+
|
|
52
50
|
constructor(config: Partial<ServerConfig> = {}) {
|
|
53
51
|
super()
|
|
54
52
|
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
@@ -56,19 +54,26 @@ export class RemoteServer extends EventEmitter {
|
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
/**
|
|
59
|
-
*
|
|
57
|
+
* Generate a random secret
|
|
58
|
+
*/
|
|
59
|
+
private generateSecret(): string {
|
|
60
|
+
return crypto.randomBytes(16).toString('hex')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Start the remote server - creates WebSocket server that proxies stdin/stdout
|
|
60
65
|
*/
|
|
61
|
-
async start(options: { name?: string } = {}): Promise<RemoteSession> {
|
|
66
|
+
async start(options: { name?: string; processForStreaming?: { stdout: NodeJS.WriteStream; stdin: NodeJS.ReadStream } } = {}): Promise<RemoteSession> {
|
|
62
67
|
if (this.isRunning) {
|
|
63
68
|
throw new Error('Server already running')
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
const sessionId = this.generateSessionId()
|
|
67
72
|
|
|
68
|
-
// Create HTTP server
|
|
73
|
+
// Create HTTP server for web client and health checks
|
|
69
74
|
this.httpServer = createServer((req, res) => this.handleHttpRequest(req, res))
|
|
70
75
|
|
|
71
|
-
// Create WebSocket server
|
|
76
|
+
// Create WebSocket server for terminal streaming
|
|
72
77
|
this.wss = new WebSocketServer({ server: this.httpServer })
|
|
73
78
|
this.setupWebSocketHandlers()
|
|
74
79
|
|
|
@@ -85,57 +90,23 @@ export class RemoteServer extends EventEmitter {
|
|
|
85
90
|
const localIp = this.getLocalIP()
|
|
86
91
|
const localUrl = `http://${localIp}:${port}`
|
|
87
92
|
|
|
88
|
-
// Create
|
|
93
|
+
// Create session
|
|
89
94
|
this.session = {
|
|
90
95
|
id: sessionId,
|
|
91
96
|
name: options.name || `nikcli-${sessionId}`,
|
|
92
97
|
qrCode: '',
|
|
93
|
-
qrUrl: localUrl
|
|
98
|
+
qrUrl: `${localUrl}?s=${sessionId}&t=${this.sessionSecret}`,
|
|
94
99
|
localUrl,
|
|
95
|
-
status: '
|
|
100
|
+
status: 'waiting',
|
|
96
101
|
connectedDevices: [],
|
|
97
102
|
startedAt: new Date(),
|
|
98
103
|
lastActivity: new Date(),
|
|
99
104
|
port,
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
//
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
this.tunnel = new TunnelManager(this.config.tunnelProvider)
|
|
106
|
-
const tunnelUrl = await this.tunnel.create(port)
|
|
107
|
-
this.session.tunnelUrl = tunnelUrl
|
|
108
|
-
this.session.qrUrl = `${tunnelUrl}?s=${sessionId}&t=${this.sessionSecret}`
|
|
109
|
-
this.emit('tunnel:connected', tunnelUrl)
|
|
110
|
-
} catch (error: any) {
|
|
111
|
-
this.emit('tunnel:error', error)
|
|
112
|
-
// Continue with local URL
|
|
113
|
-
this.session.qrUrl = `${localUrl}?s=${sessionId}&t=${this.sessionSecret}`
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
this.session.qrUrl = `${localUrl}?s=${sessionId}&t=${this.sessionSecret}`
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Initialize terminal if enabled
|
|
120
|
-
if (this.config.enableTerminal) {
|
|
121
|
-
this.terminal = new TerminalManager({
|
|
122
|
-
shell: this.config.shell,
|
|
123
|
-
cols: this.config.cols,
|
|
124
|
-
rows: this.config.rows,
|
|
125
|
-
cwd: this.config.cwd,
|
|
126
|
-
env: this.config.env,
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
this.terminal.on('data', (data: string) => {
|
|
130
|
-
// Broadcast to all connected clients
|
|
131
|
-
this.broadcast({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data } })
|
|
132
|
-
// Also emit for NikCLI to capture
|
|
133
|
-
this.emit('terminal:output', data)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
this.terminal.on('exit', (code: number) => {
|
|
137
|
-
this.broadcast({ type: MessageTypes.TERMINAL_EXIT, payload: { code } })
|
|
138
|
-
})
|
|
107
|
+
// Setup stdin/stdout proxy if process provided
|
|
108
|
+
if (options.processForStreaming) {
|
|
109
|
+
this.setupStdioProxy(options.processForStreaming.stdout, options.processForStreaming.stdin)
|
|
139
110
|
}
|
|
140
111
|
|
|
141
112
|
// Start heartbeat
|
|
@@ -153,6 +124,46 @@ export class RemoteServer extends EventEmitter {
|
|
|
153
124
|
return this.session
|
|
154
125
|
}
|
|
155
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Setup stdin/stdout proxy to forward to WebSocket clients
|
|
129
|
+
*/
|
|
130
|
+
private setupStdioProxy(stdout: NodeJS.WriteStream, stdin: NodeJS.ReadStream): void {
|
|
131
|
+
// Store original write method
|
|
132
|
+
const originalWrite = stdout.write.bind(stdout)
|
|
133
|
+
|
|
134
|
+
// Override stdout.write to broadcast to all clients
|
|
135
|
+
stdout.write = ((
|
|
136
|
+
data: string | Uint8Array,
|
|
137
|
+
encoding?: BufferEncoding | undefined,
|
|
138
|
+
cb?: (err?: Error | null) => void
|
|
139
|
+
): boolean => {
|
|
140
|
+
// Call original write
|
|
141
|
+
const result = originalWrite(data, encoding, cb)
|
|
142
|
+
|
|
143
|
+
// Broadcast to all authenticated clients
|
|
144
|
+
const text = data instanceof Buffer ? data.toString() : data
|
|
145
|
+
this.broadcastToAll({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data: text } })
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
}) as typeof stdout.write
|
|
149
|
+
|
|
150
|
+
// Listen for client messages and forward to stdin
|
|
151
|
+
this.wss?.on('connection', (ws) => {
|
|
152
|
+
ws.on('message', (rawData: RawData) => {
|
|
153
|
+
try {
|
|
154
|
+
const msg = JSON.parse(rawData.toString())
|
|
155
|
+
if (msg.type === MessageTypes.TERMINAL_INPUT && msg.data) {
|
|
156
|
+
// Forward client input to stdin
|
|
157
|
+
const inputData = Buffer.from(msg.data as string)
|
|
158
|
+
stdin.emit('data', inputData)
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Invalid message, ignore
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
156
167
|
/**
|
|
157
168
|
* Stop the server
|
|
158
169
|
*/
|
|
@@ -161,6 +172,11 @@ export class RemoteServer extends EventEmitter {
|
|
|
161
172
|
|
|
162
173
|
this.isRunning = false
|
|
163
174
|
|
|
175
|
+
// Restore original stdout.write
|
|
176
|
+
if (this.originalStdoutWrite && this.session) {
|
|
177
|
+
// We can't restore the global process.stdout, but we stop broadcasting
|
|
178
|
+
}
|
|
179
|
+
|
|
164
180
|
// Clear timers
|
|
165
181
|
if (this.heartbeatTimer) {
|
|
166
182
|
clearInterval(this.heartbeatTimer)
|
|
@@ -172,7 +188,7 @@ export class RemoteServer extends EventEmitter {
|
|
|
172
188
|
}
|
|
173
189
|
|
|
174
190
|
// Notify clients
|
|
175
|
-
this.
|
|
191
|
+
this.broadcastToAll({ type: MessageTypes.SESSION_END, payload: {} })
|
|
176
192
|
|
|
177
193
|
// Close all connections
|
|
178
194
|
for (const client of this.clients.values()) {
|
|
@@ -180,18 +196,6 @@ export class RemoteServer extends EventEmitter {
|
|
|
180
196
|
}
|
|
181
197
|
this.clients.clear()
|
|
182
198
|
|
|
183
|
-
// Stop terminal
|
|
184
|
-
if (this.terminal) {
|
|
185
|
-
this.terminal.destroy()
|
|
186
|
-
this.terminal = null
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Stop tunnel
|
|
190
|
-
if (this.tunnel) {
|
|
191
|
-
await this.tunnel.close()
|
|
192
|
-
this.tunnel = null
|
|
193
|
-
}
|
|
194
|
-
|
|
195
199
|
// Close WebSocket server
|
|
196
200
|
if (this.wss) {
|
|
197
201
|
this.wss.close()
|
|
@@ -216,7 +220,7 @@ export class RemoteServer extends EventEmitter {
|
|
|
216
220
|
/**
|
|
217
221
|
* Broadcast message to all authenticated clients
|
|
218
222
|
*/
|
|
219
|
-
|
|
223
|
+
private broadcastToAll(message: BroadcastMessage): void {
|
|
220
224
|
const data = JSON.stringify({
|
|
221
225
|
type: message.type,
|
|
222
226
|
payload: message.payload,
|
|
@@ -234,7 +238,7 @@ export class RemoteServer extends EventEmitter {
|
|
|
234
238
|
* Send notification to clients
|
|
235
239
|
*/
|
|
236
240
|
notify(notification: RemoteNotification): void {
|
|
237
|
-
this.
|
|
241
|
+
this.broadcastToAll({
|
|
238
242
|
type: MessageTypes.NOTIFICATION,
|
|
239
243
|
payload: notification,
|
|
240
244
|
})
|
|
@@ -266,17 +270,17 @@ export class RemoteServer extends EventEmitter {
|
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
/**
|
|
269
|
-
* Write to
|
|
273
|
+
* Write data to all connected clients (for manual output streaming)
|
|
270
274
|
*/
|
|
271
|
-
|
|
272
|
-
this.
|
|
275
|
+
writeToClients(data: string): void {
|
|
276
|
+
this.broadcastToAll({ type: MessageTypes.TERMINAL_OUTPUT, payload: { data } })
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
/**
|
|
276
|
-
*
|
|
280
|
+
* Alias for writeToClients - for compatibility
|
|
277
281
|
*/
|
|
278
|
-
|
|
279
|
-
this.
|
|
282
|
+
writeToTerminal(data: string): void {
|
|
283
|
+
this.writeToClients(data)
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
/**
|
|
@@ -366,37 +370,18 @@ export class RemoteServer extends EventEmitter {
|
|
|
366
370
|
break
|
|
367
371
|
|
|
368
372
|
case MessageTypes.TERMINAL_INPUT:
|
|
369
|
-
|
|
370
|
-
this.terminal?.write(message.data as string)
|
|
371
|
-
}
|
|
373
|
+
// Input is handled in setupStdioProxy via ws message handler
|
|
372
374
|
break
|
|
373
375
|
|
|
374
376
|
case MessageTypes.TERMINAL_RESIZE:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
break
|
|
379
|
-
|
|
380
|
-
case MessageTypes.TERMINAL_CLEAR:
|
|
381
|
-
if (client.authenticated) {
|
|
382
|
-
this.terminal?.clear()
|
|
383
|
-
}
|
|
377
|
+
// Resize can be forwarded if needed
|
|
378
|
+
this.emit('message', client, message)
|
|
384
379
|
break
|
|
385
380
|
|
|
386
381
|
case MessageTypes.PING:
|
|
387
382
|
client.ws.send(JSON.stringify({ type: MessageTypes.PONG, timestamp: Date.now() }))
|
|
388
383
|
break
|
|
389
384
|
|
|
390
|
-
case MessageTypes.COMMAND:
|
|
391
|
-
if (client.authenticated) {
|
|
392
|
-
this.emit('command', {
|
|
393
|
-
clientId: client.id,
|
|
394
|
-
command: message.command as string,
|
|
395
|
-
args: message.args as string[] | undefined,
|
|
396
|
-
})
|
|
397
|
-
}
|
|
398
|
-
break
|
|
399
|
-
|
|
400
385
|
default:
|
|
401
386
|
this.emit('message', client, message)
|
|
402
387
|
}
|
|
@@ -419,17 +404,11 @@ export class RemoteServer extends EventEmitter {
|
|
|
419
404
|
type: MessageTypes.AUTH_SUCCESS,
|
|
420
405
|
payload: {
|
|
421
406
|
sessionId: this.session?.id,
|
|
422
|
-
terminalEnabled: this.config.enableTerminal,
|
|
423
407
|
},
|
|
424
408
|
timestamp: Date.now(),
|
|
425
409
|
})
|
|
426
410
|
)
|
|
427
411
|
|
|
428
|
-
// Start terminal if needed
|
|
429
|
-
if (this.config.enableTerminal && !this.terminal?.isRunning()) {
|
|
430
|
-
this.terminal?.start()
|
|
431
|
-
}
|
|
432
|
-
|
|
433
412
|
this.emit('client:connected', client.device)
|
|
434
413
|
} else {
|
|
435
414
|
client.ws.send(JSON.stringify({ type: MessageTypes.AUTH_FAILED, timestamp: Date.now() }))
|
|
@@ -457,6 +436,7 @@ export class RemoteServer extends EventEmitter {
|
|
|
457
436
|
|
|
458
437
|
// Serve web client
|
|
459
438
|
if (path === '/' || path === '/index.html') {
|
|
439
|
+
const { getWebClient } = require('./web-client')
|
|
460
440
|
res.writeHead(200, {
|
|
461
441
|
'Content-Type': 'text/html; charset=utf-8',
|
|
462
442
|
'Content-Security-Policy': "default-src 'self' 'unsafe-inline' 'unsafe-eval' https: data: ws: wss:",
|
|
@@ -558,11 +538,4 @@ export class RemoteServer extends EventEmitter {
|
|
|
558
538
|
private generateClientId(): string {
|
|
559
539
|
return 'c_' + crypto.randomBytes(4).toString('hex')
|
|
560
540
|
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Generate secret
|
|
564
|
-
*/
|
|
565
|
-
private generateSecret(): string {
|
|
566
|
-
return crypto.randomBytes(16).toString('hex')
|
|
567
|
-
}
|
|
568
541
|
}
|
package/src/tunnel.ts
CHANGED
|
@@ -6,6 +6,30 @@
|
|
|
6
6
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
7
7
|
import type { TunnelProvider } from './types'
|
|
8
8
|
|
|
9
|
+
export type { TunnelProvider }
|
|
10
|
+
|
|
11
|
+
export interface TunnelResult {
|
|
12
|
+
url: string
|
|
13
|
+
provider: TunnelProvider
|
|
14
|
+
close: () => Promise<void>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a tunnel for the given port using the specified provider
|
|
19
|
+
*/
|
|
20
|
+
export async function createTunnel(
|
|
21
|
+
port: number,
|
|
22
|
+
provider: TunnelProvider = 'cloudflared'
|
|
23
|
+
): Promise<TunnelResult> {
|
|
24
|
+
const manager = new TunnelManager(provider)
|
|
25
|
+
const url = await manager.create(port)
|
|
26
|
+
return {
|
|
27
|
+
url,
|
|
28
|
+
provider,
|
|
29
|
+
close: () => manager.close(),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
export class TunnelManager {
|
|
10
34
|
private provider: TunnelProvider
|
|
11
35
|
private process: ChildProcess | null = null
|