nikcli-remote 1.0.0
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-MCKGQKYU.js +15 -0
- package/dist/chunk-TJTKYXIZ.js +2782 -0
- package/dist/index.cjs +6804 -0
- package/dist/index.d.cts +372 -0
- package/dist/index.d.ts +372 -0
- package/dist/index.js +155 -0
- package/dist/localtunnel-XT32JGNN.js +3805 -0
- package/dist/server-XW6FLG7E.js +7 -0
- package/package.json +58 -0
- package/src/index.ts +82 -0
- package/src/qrcode.ts +164 -0
- package/src/server.ts +562 -0
- package/src/terminal.ts +163 -0
- package/src/tunnel.ts +258 -0
- package/src/types.ts +169 -0
- package/src/web-client.ts +657 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nikcli-remote",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Native remote terminal server for NikCLI - Mobile control via WebSocket",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"remote",
|
|
28
|
+
"terminal",
|
|
29
|
+
"mobile",
|
|
30
|
+
"websocket",
|
|
31
|
+
"pty",
|
|
32
|
+
"nikcli"
|
|
33
|
+
],
|
|
34
|
+
"author": "NikCLI Team",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"qrcode": "^1.5.4",
|
|
38
|
+
"ws": "^8.18.0"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"localtunnel": "^2.0.2",
|
|
42
|
+
"node-pty": "^1.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.19.3",
|
|
46
|
+
"@types/qrcode": "^1.5.5",
|
|
47
|
+
"@types/ws": "^8.18.1",
|
|
48
|
+
"tsup": "^8.3.5",
|
|
49
|
+
"typescript": "^5.7.3",
|
|
50
|
+
"vitest": "^2.1.8"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"typescript": ">=5.0.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nikcli/remote
|
|
3
|
+
* Native remote terminal server for NikCLI
|
|
4
|
+
*
|
|
5
|
+
* Provides WebSocket-based remote access to NikCLI from mobile devices.
|
|
6
|
+
* Features:
|
|
7
|
+
* - PTY-based terminal with full color support
|
|
8
|
+
* - Mobile-friendly web client
|
|
9
|
+
* - Tunnel support for public access (localtunnel/cloudflared/ngrok)
|
|
10
|
+
* - QR code generation for easy mobile connection
|
|
11
|
+
* - Real-time notifications and events
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { RemoteServer } from '@nikcli/remote'
|
|
16
|
+
*
|
|
17
|
+
* const server = new RemoteServer({
|
|
18
|
+
* enableTunnel: true,
|
|
19
|
+
* tunnelProvider: 'localtunnel'
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* const session = await server.start({ name: 'my-session' })
|
|
23
|
+
* console.log('Connect at:', session.qrUrl)
|
|
24
|
+
*
|
|
25
|
+
* // Send notifications
|
|
26
|
+
* server.notify({
|
|
27
|
+
* type: 'success',
|
|
28
|
+
* title: 'Task Complete',
|
|
29
|
+
* body: 'Build finished successfully'
|
|
30
|
+
* })
|
|
31
|
+
*
|
|
32
|
+
* // Stop when done
|
|
33
|
+
* await server.stop()
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Main server
|
|
38
|
+
export { RemoteServer, type RemoteServerEvents } from './server'
|
|
39
|
+
|
|
40
|
+
// Terminal management
|
|
41
|
+
export { TerminalManager, type TerminalConfig } from './terminal'
|
|
42
|
+
|
|
43
|
+
// Tunnel management
|
|
44
|
+
export { TunnelManager, checkTunnelAvailability, findAvailableTunnel } from './tunnel'
|
|
45
|
+
|
|
46
|
+
// QR code generation
|
|
47
|
+
export { generateQR, generateQRDataURL, renderSessionCard, progressBar } from './qrcode'
|
|
48
|
+
|
|
49
|
+
// Web client
|
|
50
|
+
export { getWebClient } from './web-client'
|
|
51
|
+
|
|
52
|
+
// Types
|
|
53
|
+
export type {
|
|
54
|
+
SessionStatus,
|
|
55
|
+
TunnelProvider,
|
|
56
|
+
DeviceInfo,
|
|
57
|
+
RemoteSession,
|
|
58
|
+
ServerConfig,
|
|
59
|
+
BroadcastMessage,
|
|
60
|
+
RemoteNotification,
|
|
61
|
+
ClientMessage,
|
|
62
|
+
ServerMessage,
|
|
63
|
+
TerminalData,
|
|
64
|
+
TerminalResize,
|
|
65
|
+
CommandMessage,
|
|
66
|
+
ClientConnection,
|
|
67
|
+
} from './types'
|
|
68
|
+
|
|
69
|
+
export { DEFAULT_CONFIG, MessageTypes } from './types'
|
|
70
|
+
|
|
71
|
+
// Convenience function to create and start server
|
|
72
|
+
export async function createRemoteServer(
|
|
73
|
+
config: Partial<import('./types').ServerConfig> = {}
|
|
74
|
+
): Promise<{
|
|
75
|
+
server: import('./server').RemoteServer
|
|
76
|
+
session: import('./types').RemoteSession
|
|
77
|
+
}> {
|
|
78
|
+
const { RemoteServer } = await import('./server')
|
|
79
|
+
const server = new RemoteServer(config)
|
|
80
|
+
const session = await server.start()
|
|
81
|
+
return { server, session }
|
|
82
|
+
}
|
package/src/qrcode.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nikcli/remote - QR Code Generator
|
|
3
|
+
* Terminal QR code rendering for session URLs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RemoteSession } from './types'
|
|
7
|
+
|
|
8
|
+
// Try to import qrcode
|
|
9
|
+
let QRCode: typeof import('qrcode') | null = null
|
|
10
|
+
try {
|
|
11
|
+
QRCode = require('qrcode')
|
|
12
|
+
} catch {
|
|
13
|
+
// qrcode not available
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface QROptions {
|
|
17
|
+
small?: boolean
|
|
18
|
+
margin?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate QR code string for terminal display
|
|
23
|
+
*/
|
|
24
|
+
export async function generateQR(url: string, options: QROptions = {}): Promise<string> {
|
|
25
|
+
if (!QRCode) {
|
|
26
|
+
return generateFallbackQR(url)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const qrString = await QRCode.toString(url, {
|
|
31
|
+
type: 'terminal',
|
|
32
|
+
small: options.small ?? true,
|
|
33
|
+
margin: options.margin ?? 1,
|
|
34
|
+
})
|
|
35
|
+
return qrString
|
|
36
|
+
} catch {
|
|
37
|
+
return generateFallbackQR(url)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate QR code as data URL (for web/image)
|
|
43
|
+
*/
|
|
44
|
+
export async function generateQRDataURL(url: string): Promise<string | null> {
|
|
45
|
+
if (!QRCode) return null
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return await QRCode.toDataURL(url, {
|
|
49
|
+
margin: 2,
|
|
50
|
+
width: 256,
|
|
51
|
+
color: {
|
|
52
|
+
dark: '#000000',
|
|
53
|
+
light: '#ffffff',
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
} catch {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fallback QR display when qrcode package not available
|
|
63
|
+
*/
|
|
64
|
+
function generateFallbackQR(url: string): string {
|
|
65
|
+
return `
|
|
66
|
+
┌─────────────────────────────────────┐
|
|
67
|
+
│ │
|
|
68
|
+
│ QR Code generation unavailable │
|
|
69
|
+
│ │
|
|
70
|
+
│ Install 'qrcode' package or │
|
|
71
|
+
│ visit the URL directly: │
|
|
72
|
+
│ │
|
|
73
|
+
│ ${url.substring(0, 35)}${url.length > 35 ? '...' : ''}
|
|
74
|
+
│ │
|
|
75
|
+
└─────────────────────────────────────┘
|
|
76
|
+
`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Render session info with QR code
|
|
81
|
+
*/
|
|
82
|
+
export async function renderSessionCard(session: RemoteSession): Promise<string> {
|
|
83
|
+
const qr = await generateQR(session.qrUrl)
|
|
84
|
+
const statusIcon = getStatusIcon(session.status)
|
|
85
|
+
const statusColor = getStatusColor(session.status)
|
|
86
|
+
|
|
87
|
+
const lines = [
|
|
88
|
+
'',
|
|
89
|
+
'╭─────────────────────────────────────────────╮',
|
|
90
|
+
'│ NikCLI Remote Session │',
|
|
91
|
+
'╰─────────────────────────────────────────────╯',
|
|
92
|
+
'',
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
// Add QR code
|
|
96
|
+
const qrLines = qr.split('\n').filter(l => l.trim())
|
|
97
|
+
for (const line of qrLines) {
|
|
98
|
+
lines.push(' ' + line)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push('')
|
|
102
|
+
lines.push('─────────────────────────────────────────────')
|
|
103
|
+
lines.push('')
|
|
104
|
+
lines.push(` Session: ${session.id}`)
|
|
105
|
+
lines.push(` Status: ${statusColor}${statusIcon} ${session.status}\x1b[0m`)
|
|
106
|
+
lines.push(` Devices: ${session.connectedDevices.length} connected`)
|
|
107
|
+
lines.push('')
|
|
108
|
+
|
|
109
|
+
if (session.tunnelUrl) {
|
|
110
|
+
lines.push(` \x1b[36mPublic URL:\x1b[0m`)
|
|
111
|
+
lines.push(` ${session.tunnelUrl}`)
|
|
112
|
+
} else {
|
|
113
|
+
lines.push(` \x1b[36mLocal URL:\x1b[0m`)
|
|
114
|
+
lines.push(` ${session.localUrl}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
lines.push('')
|
|
118
|
+
lines.push(` \x1b[90mScan QR code or open URL on your phone\x1b[0m`)
|
|
119
|
+
lines.push('')
|
|
120
|
+
lines.push('─────────────────────────────────────────────')
|
|
121
|
+
lines.push(' [q] Stop [r] Refresh [c] Copy URL')
|
|
122
|
+
lines.push('')
|
|
123
|
+
|
|
124
|
+
return lines.join('\n')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get status icon
|
|
129
|
+
*/
|
|
130
|
+
function getStatusIcon(status: string): string {
|
|
131
|
+
const icons: Record<string, string> = {
|
|
132
|
+
starting: '◯',
|
|
133
|
+
waiting: '◉',
|
|
134
|
+
connected: '●',
|
|
135
|
+
stopped: '○',
|
|
136
|
+
error: '✖',
|
|
137
|
+
}
|
|
138
|
+
return icons[status] || '?'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get status ANSI color
|
|
143
|
+
*/
|
|
144
|
+
function getStatusColor(status: string): string {
|
|
145
|
+
const colors: Record<string, string> = {
|
|
146
|
+
starting: '\x1b[33m', // Yellow
|
|
147
|
+
waiting: '\x1b[33m', // Yellow
|
|
148
|
+
connected: '\x1b[32m', // Green
|
|
149
|
+
stopped: '\x1b[90m', // Gray
|
|
150
|
+
error: '\x1b[31m', // Red
|
|
151
|
+
}
|
|
152
|
+
return colors[status] || ''
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Simple progress bar
|
|
157
|
+
*/
|
|
158
|
+
export function progressBar(current: number, total: number, width: number = 30): string {
|
|
159
|
+
const percent = Math.round((current / total) * 100)
|
|
160
|
+
const filled = Math.round((current / total) * width)
|
|
161
|
+
const empty = width - filled
|
|
162
|
+
|
|
163
|
+
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${percent}%`
|
|
164
|
+
}
|