lunel-cli 0.1.13 → 0.1.15
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/index.js +240 -276
- package/package.json +1 -3
package/dist/index.js
CHANGED
|
@@ -4,213 +4,72 @@ import qrcode from "qrcode-terminal";
|
|
|
4
4
|
import Ignore from "ignore";
|
|
5
5
|
const ignore = Ignore.default;
|
|
6
6
|
import * as fs from "fs/promises";
|
|
7
|
-
import * as fsSync from "fs";
|
|
8
7
|
import * as path from "path";
|
|
9
8
|
import * as os from "os";
|
|
10
9
|
import { spawn, execSync } from "child_process";
|
|
11
|
-
import
|
|
12
|
-
import { createServer } from "net";
|
|
13
|
-
import { fileURLToPath } from "url";
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = path.dirname(__filename);
|
|
10
|
+
import { createServer, createConnection } from "net";
|
|
16
11
|
const PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
|
|
17
|
-
const VERSION =
|
|
12
|
+
const VERSION = "0.1.3";
|
|
18
13
|
// Root directory - sandbox all file operations to this
|
|
19
14
|
const ROOT_DIR = process.cwd();
|
|
20
|
-
//
|
|
21
|
-
class AnsiParser {
|
|
22
|
-
buffer;
|
|
23
|
-
cursorX = 0;
|
|
24
|
-
cursorY = 0;
|
|
25
|
-
cols;
|
|
26
|
-
rows;
|
|
27
|
-
currentFg = '#ffffff';
|
|
28
|
-
currentBg = '#000000';
|
|
29
|
-
scrollbackBuffer = [];
|
|
30
|
-
// ANSI color codes to hex
|
|
31
|
-
colors = {
|
|
32
|
-
0: '#000000', 1: '#cc0000', 2: '#4e9a06', 3: '#c4a000',
|
|
33
|
-
4: '#3465a4', 5: '#75507b', 6: '#06989a', 7: '#d3d7cf',
|
|
34
|
-
8: '#555753', 9: '#ef2929', 10: '#8ae234', 11: '#fce94f',
|
|
35
|
-
12: '#729fcf', 13: '#ad7fa8', 14: '#34e2e2', 15: '#eeeeec',
|
|
36
|
-
};
|
|
37
|
-
constructor(cols, rows) {
|
|
38
|
-
this.cols = cols;
|
|
39
|
-
this.rows = rows;
|
|
40
|
-
this.buffer = this.createEmptyBuffer();
|
|
41
|
-
}
|
|
42
|
-
createEmptyBuffer() {
|
|
43
|
-
return Array.from({ length: this.rows }, () => Array.from({ length: this.cols }, () => ({
|
|
44
|
-
char: ' ',
|
|
45
|
-
fg: '#ffffff',
|
|
46
|
-
bg: '#000000',
|
|
47
|
-
})));
|
|
48
|
-
}
|
|
49
|
-
scrollUp() {
|
|
50
|
-
// Move first line to scrollback
|
|
51
|
-
this.scrollbackBuffer.push(this.buffer.shift());
|
|
52
|
-
// Keep scrollback limited
|
|
53
|
-
if (this.scrollbackBuffer.length > 1000) {
|
|
54
|
-
this.scrollbackBuffer.shift();
|
|
55
|
-
}
|
|
56
|
-
// Add new empty line at bottom
|
|
57
|
-
this.buffer.push(Array.from({ length: this.cols }, () => ({
|
|
58
|
-
char: ' ',
|
|
59
|
-
fg: '#ffffff',
|
|
60
|
-
bg: '#000000',
|
|
61
|
-
})));
|
|
62
|
-
}
|
|
63
|
-
write(data) {
|
|
64
|
-
let i = 0;
|
|
65
|
-
while (i < data.length) {
|
|
66
|
-
const char = data[i];
|
|
67
|
-
// Check for escape sequence
|
|
68
|
-
if (char === '\x1b' && data[i + 1] === '[') {
|
|
69
|
-
// Parse CSI sequence
|
|
70
|
-
let j = i + 2;
|
|
71
|
-
let params = '';
|
|
72
|
-
while (j < data.length && /[0-9;]/.test(data[j])) {
|
|
73
|
-
params += data[j];
|
|
74
|
-
j++;
|
|
75
|
-
}
|
|
76
|
-
const cmd = data[j];
|
|
77
|
-
i = j + 1;
|
|
78
|
-
this.handleCSI(params, cmd);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
// Handle special characters
|
|
82
|
-
if (char === '\n') {
|
|
83
|
-
this.cursorY++;
|
|
84
|
-
if (this.cursorY >= this.rows) {
|
|
85
|
-
this.scrollUp();
|
|
86
|
-
this.cursorY = this.rows - 1;
|
|
87
|
-
}
|
|
88
|
-
i++;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (char === '\r') {
|
|
92
|
-
this.cursorX = 0;
|
|
93
|
-
i++;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (char === '\b') {
|
|
97
|
-
if (this.cursorX > 0)
|
|
98
|
-
this.cursorX--;
|
|
99
|
-
i++;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (char === '\t') {
|
|
103
|
-
this.cursorX = Math.min(this.cols - 1, (Math.floor(this.cursorX / 8) + 1) * 8);
|
|
104
|
-
i++;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (char === '\x07') { // Bell
|
|
108
|
-
i++;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
// Skip other control characters
|
|
112
|
-
if (char.charCodeAt(0) < 32) {
|
|
113
|
-
i++;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
// Regular character
|
|
117
|
-
if (this.cursorX >= this.cols) {
|
|
118
|
-
this.cursorX = 0;
|
|
119
|
-
this.cursorY++;
|
|
120
|
-
if (this.cursorY >= this.rows) {
|
|
121
|
-
this.scrollUp();
|
|
122
|
-
this.cursorY = this.rows - 1;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (this.cursorY >= 0 && this.cursorY < this.rows &&
|
|
126
|
-
this.cursorX >= 0 && this.cursorX < this.cols) {
|
|
127
|
-
this.buffer[this.cursorY][this.cursorX] = {
|
|
128
|
-
char,
|
|
129
|
-
fg: this.currentFg,
|
|
130
|
-
bg: this.currentBg,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
this.cursorX++;
|
|
134
|
-
i++;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
handleCSI(params, cmd) {
|
|
138
|
-
const args = params.split(';').map(p => parseInt(p) || 0);
|
|
139
|
-
switch (cmd) {
|
|
140
|
-
case 'A': // Cursor up
|
|
141
|
-
this.cursorY = Math.max(0, this.cursorY - (args[0] || 1));
|
|
142
|
-
break;
|
|
143
|
-
case 'B': // Cursor down
|
|
144
|
-
this.cursorY = Math.min(this.rows - 1, this.cursorY + (args[0] || 1));
|
|
145
|
-
break;
|
|
146
|
-
case 'C': // Cursor forward
|
|
147
|
-
this.cursorX = Math.min(this.cols - 1, this.cursorX + (args[0] || 1));
|
|
148
|
-
break;
|
|
149
|
-
case 'D': // Cursor back
|
|
150
|
-
this.cursorX = Math.max(0, this.cursorX - (args[0] || 1));
|
|
151
|
-
break;
|
|
152
|
-
case 'H': // Cursor position
|
|
153
|
-
case 'f':
|
|
154
|
-
this.cursorY = Math.min(this.rows - 1, Math.max(0, (args[0] || 1) - 1));
|
|
155
|
-
this.cursorX = Math.min(this.cols - 1, Math.max(0, (args[1] || 1) - 1));
|
|
156
|
-
break;
|
|
157
|
-
case 'J': // Erase display
|
|
158
|
-
if (args[0] === 2) {
|
|
159
|
-
this.buffer = this.createEmptyBuffer();
|
|
160
|
-
this.cursorX = 0;
|
|
161
|
-
this.cursorY = 0;
|
|
162
|
-
}
|
|
163
|
-
break;
|
|
164
|
-
case 'K': // Erase line
|
|
165
|
-
for (let x = this.cursorX; x < this.cols; x++) {
|
|
166
|
-
this.buffer[this.cursorY][x] = { char: ' ', fg: this.currentFg, bg: this.currentBg };
|
|
167
|
-
}
|
|
168
|
-
break;
|
|
169
|
-
case 'm': // SGR - Select Graphic Rendition
|
|
170
|
-
for (const arg of args) {
|
|
171
|
-
if (arg === 0) {
|
|
172
|
-
this.currentFg = '#ffffff';
|
|
173
|
-
this.currentBg = '#000000';
|
|
174
|
-
}
|
|
175
|
-
else if (arg >= 30 && arg <= 37) {
|
|
176
|
-
this.currentFg = this.colors[arg - 30] || '#ffffff';
|
|
177
|
-
}
|
|
178
|
-
else if (arg >= 40 && arg <= 47) {
|
|
179
|
-
this.currentBg = this.colors[arg - 40] || '#000000';
|
|
180
|
-
}
|
|
181
|
-
else if (arg >= 90 && arg <= 97) {
|
|
182
|
-
this.currentFg = this.colors[arg - 90 + 8] || '#ffffff';
|
|
183
|
-
}
|
|
184
|
-
else if (arg >= 100 && arg <= 107) {
|
|
185
|
-
this.currentBg = this.colors[arg - 100 + 8] || '#000000';
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
resize(cols, rows) {
|
|
192
|
-
const newBuffer = Array.from({ length: rows }, (_, y) => Array.from({ length: cols }, (_, x) => (y < this.rows && x < this.cols) ? this.buffer[y][x] : { char: ' ', fg: '#ffffff', bg: '#000000' }));
|
|
193
|
-
this.cols = cols;
|
|
194
|
-
this.rows = rows;
|
|
195
|
-
this.buffer = newBuffer;
|
|
196
|
-
this.cursorX = Math.min(this.cursorX, cols - 1);
|
|
197
|
-
this.cursorY = Math.min(this.cursorY, rows - 1);
|
|
198
|
-
}
|
|
199
|
-
getState() {
|
|
200
|
-
return {
|
|
201
|
-
buffer: this.buffer,
|
|
202
|
-
cursorX: this.cursorX,
|
|
203
|
-
cursorY: this.cursorY,
|
|
204
|
-
cols: this.cols,
|
|
205
|
-
rows: this.rows,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
}
|
|
15
|
+
// Terminal sessions
|
|
209
16
|
const terminals = new Map();
|
|
210
17
|
const processes = new Map();
|
|
211
18
|
const processOutputBuffers = new Map();
|
|
212
19
|
// CPU usage tracking
|
|
213
20
|
let lastCpuInfo = null;
|
|
21
|
+
// Proxy tunnel management
|
|
22
|
+
let currentSessionCode = null;
|
|
23
|
+
const activeTunnels = new Map();
|
|
24
|
+
// Popular development server ports to scan on connect
|
|
25
|
+
const DEV_PORTS = [
|
|
26
|
+
1234, // Parcel
|
|
27
|
+
1313, // Hugo
|
|
28
|
+
2000, // Deno (alt)
|
|
29
|
+
3000, // Next.js / CRA / Remix / Express
|
|
30
|
+
3001, // Next.js (alt)
|
|
31
|
+
3002, // Dev server (alt)
|
|
32
|
+
3003, // Dev server (alt)
|
|
33
|
+
3333, // AdonisJS / Nitro
|
|
34
|
+
4000, // Gatsby / Redwood
|
|
35
|
+
4200, // Angular CLI
|
|
36
|
+
4321, // Astro
|
|
37
|
+
4173, // Vite preview
|
|
38
|
+
4444, // Selenium
|
|
39
|
+
4567, // Sinatra
|
|
40
|
+
5000, // Flask / Sails.js
|
|
41
|
+
5001, // Flask (alt)
|
|
42
|
+
5173, // Vite
|
|
43
|
+
5174, // Vite (alt)
|
|
44
|
+
5175, // Vite (alt)
|
|
45
|
+
5500, // Live Server (VS Code)
|
|
46
|
+
5555, // Prisma Studio
|
|
47
|
+
6006, // Storybook
|
|
48
|
+
7000, // Hapi
|
|
49
|
+
7070, // Dev server
|
|
50
|
+
7777, // Dev server
|
|
51
|
+
8000, // Django / Laravel / Gatsby
|
|
52
|
+
8001, // Django (alt)
|
|
53
|
+
8008, // Dev server
|
|
54
|
+
8010, // Dev server
|
|
55
|
+
8080, // Vue CLI / Webpack Dev Server / Spring Boot
|
|
56
|
+
8081, // Metro Bundler
|
|
57
|
+
8082, // Dev server (alt)
|
|
58
|
+
8100, // Ionic
|
|
59
|
+
8200, // Vault
|
|
60
|
+
8443, // HTTPS dev server
|
|
61
|
+
8787, // Wrangler (Cloudflare Workers)
|
|
62
|
+
8888, // Jupyter Notebook
|
|
63
|
+
8899, // Dev server
|
|
64
|
+
9000, // PHP / SonarQube
|
|
65
|
+
9090, // Prometheus / Cockpit
|
|
66
|
+
9200, // Elasticsearch
|
|
67
|
+
9229, // Node.js debugger
|
|
68
|
+
9292, // Rack (Ruby)
|
|
69
|
+
10000, // Webmin
|
|
70
|
+
19006, // Expo web
|
|
71
|
+
24678, // Vite HMR WebSocket
|
|
72
|
+
];
|
|
214
73
|
// ============================================================================
|
|
215
74
|
// Path Safety
|
|
216
75
|
// ============================================================================
|
|
@@ -674,78 +533,37 @@ async function handleGitDiscard(payload) {
|
|
|
674
533
|
// ============================================================================
|
|
675
534
|
let dataChannel = null;
|
|
676
535
|
function handleTerminalSpawn(payload) {
|
|
536
|
+
const shell = payload.shell || process.env.SHELL || "/bin/sh";
|
|
677
537
|
const cols = payload.cols || 80;
|
|
678
538
|
const rows = payload.rows || 24;
|
|
679
|
-
// Find a working shell
|
|
680
|
-
let shell = payload.shell || process.env.SHELL || '';
|
|
681
|
-
const possibleShells = os.platform() === 'win32'
|
|
682
|
-
? ['powershell.exe', 'cmd.exe']
|
|
683
|
-
: [shell, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
|
|
684
|
-
shell = '';
|
|
685
|
-
for (const candidate of possibleShells) {
|
|
686
|
-
if (!candidate)
|
|
687
|
-
continue;
|
|
688
|
-
try {
|
|
689
|
-
if (os.platform() !== 'win32') {
|
|
690
|
-
execSync(`test -x "${candidate}"`, { stdio: 'ignore' });
|
|
691
|
-
}
|
|
692
|
-
shell = candidate;
|
|
693
|
-
break;
|
|
694
|
-
}
|
|
695
|
-
catch {
|
|
696
|
-
// Try next
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
if (!shell) {
|
|
700
|
-
throw Object.assign(new Error('No valid shell found'), { code: 'ENOSHELL' });
|
|
701
|
-
}
|
|
702
539
|
const terminalId = `term-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
703
|
-
|
|
704
|
-
// Create ANSI parser for terminal state
|
|
705
|
-
const parser = new AnsiParser(cols, rows);
|
|
706
|
-
// Spawn PTY with node-pty
|
|
707
|
-
const ptyProcess = pty.spawn(shell, [], {
|
|
708
|
-
name: 'xterm-256color',
|
|
709
|
-
cols,
|
|
710
|
-
rows,
|
|
540
|
+
const proc = spawn(shell, [], {
|
|
711
541
|
cwd: ROOT_DIR,
|
|
712
|
-
env:
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
rows,
|
|
720
|
-
buffer: parser.getState().buffer,
|
|
721
|
-
cursorX: 0,
|
|
722
|
-
cursorY: 0,
|
|
723
|
-
parser,
|
|
542
|
+
env: {
|
|
543
|
+
...process.env,
|
|
544
|
+
TERM: "xterm-256color",
|
|
545
|
+
COLUMNS: cols.toString(),
|
|
546
|
+
LINES: rows.toString(),
|
|
547
|
+
},
|
|
548
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
724
549
|
});
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const state = parser.getState();
|
|
550
|
+
terminals.set(terminalId, proc);
|
|
551
|
+
// Stream output to app via data channel
|
|
552
|
+
const sendOutput = (data) => {
|
|
729
553
|
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
730
554
|
const msg = {
|
|
731
555
|
v: 1,
|
|
732
556
|
id: `evt-${Date.now()}`,
|
|
733
557
|
ns: "terminal",
|
|
734
|
-
action: "
|
|
735
|
-
payload: {
|
|
736
|
-
terminalId,
|
|
737
|
-
buffer: state.buffer,
|
|
738
|
-
cursorX: state.cursorX,
|
|
739
|
-
cursorY: state.cursorY,
|
|
740
|
-
cols: state.cols,
|
|
741
|
-
rows: state.rows,
|
|
742
|
-
},
|
|
558
|
+
action: "output",
|
|
559
|
+
payload: { terminalId, data: data.toString() },
|
|
743
560
|
};
|
|
744
561
|
dataChannel.send(JSON.stringify(msg));
|
|
745
562
|
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
|
|
563
|
+
};
|
|
564
|
+
proc.stdout?.on("data", sendOutput);
|
|
565
|
+
proc.stderr?.on("data", sendOutput);
|
|
566
|
+
proc.on("close", (code) => {
|
|
749
567
|
terminals.delete(terminalId);
|
|
750
568
|
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
751
569
|
const msg = {
|
|
@@ -753,12 +571,12 @@ function handleTerminalSpawn(payload) {
|
|
|
753
571
|
id: `evt-${Date.now()}`,
|
|
754
572
|
ns: "terminal",
|
|
755
573
|
action: "exit",
|
|
756
|
-
payload: { terminalId, code
|
|
574
|
+
payload: { terminalId, code },
|
|
757
575
|
};
|
|
758
576
|
dataChannel.send(JSON.stringify(msg));
|
|
759
577
|
}
|
|
760
578
|
});
|
|
761
|
-
return { terminalId
|
|
579
|
+
return { terminalId };
|
|
762
580
|
}
|
|
763
581
|
function handleTerminalWrite(payload) {
|
|
764
582
|
const terminalId = payload.terminalId;
|
|
@@ -767,10 +585,10 @@ function handleTerminalWrite(payload) {
|
|
|
767
585
|
throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
|
|
768
586
|
if (typeof data !== "string")
|
|
769
587
|
throw Object.assign(new Error("data is required"), { code: "EINVAL" });
|
|
770
|
-
const
|
|
771
|
-
if (!
|
|
588
|
+
const proc = terminals.get(terminalId);
|
|
589
|
+
if (!proc)
|
|
772
590
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
773
|
-
|
|
591
|
+
proc.stdin?.write(data);
|
|
774
592
|
return {};
|
|
775
593
|
}
|
|
776
594
|
function handleTerminalResize(payload) {
|
|
@@ -779,27 +597,21 @@ function handleTerminalResize(payload) {
|
|
|
779
597
|
const rows = payload.rows;
|
|
780
598
|
if (!terminalId)
|
|
781
599
|
throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const session = terminals.get(terminalId);
|
|
785
|
-
if (!session)
|
|
600
|
+
const proc = terminals.get(terminalId);
|
|
601
|
+
if (!proc)
|
|
786
602
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
// Resize parser buffer
|
|
790
|
-
session.parser.resize(cols, rows);
|
|
791
|
-
session.cols = cols;
|
|
792
|
-
session.rows = rows;
|
|
603
|
+
// Note: For proper PTY resize, you'd need node-pty
|
|
604
|
+
// This is a simplified version
|
|
793
605
|
return {};
|
|
794
606
|
}
|
|
795
607
|
function handleTerminalKill(payload) {
|
|
796
608
|
const terminalId = payload.terminalId;
|
|
797
609
|
if (!terminalId)
|
|
798
610
|
throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
|
|
799
|
-
const
|
|
800
|
-
if (!
|
|
611
|
+
const proc = terminals.get(terminalId);
|
|
612
|
+
if (!proc)
|
|
801
613
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
802
|
-
|
|
614
|
+
proc.kill();
|
|
803
615
|
terminals.delete(terminalId);
|
|
804
616
|
return {};
|
|
805
617
|
}
|
|
@@ -809,7 +621,7 @@ function handleTerminalKill(payload) {
|
|
|
809
621
|
function handleSystemCapabilities() {
|
|
810
622
|
return {
|
|
811
623
|
version: VERSION,
|
|
812
|
-
namespaces: ["fs", "git", "terminal", "processes", "ports", "monitor", "http"],
|
|
624
|
+
namespaces: ["fs", "git", "terminal", "processes", "ports", "monitor", "http", "proxy"],
|
|
813
625
|
platform: os.platform(),
|
|
814
626
|
rootDir: ROOT_DIR,
|
|
815
627
|
hostname: os.hostname(),
|
|
@@ -1273,6 +1085,128 @@ async function handleHttpRequest(payload) {
|
|
|
1273
1085
|
}
|
|
1274
1086
|
}
|
|
1275
1087
|
// ============================================================================
|
|
1088
|
+
// Proxy Handlers
|
|
1089
|
+
// ============================================================================
|
|
1090
|
+
async function scanDevPorts() {
|
|
1091
|
+
const openPorts = [];
|
|
1092
|
+
const checks = DEV_PORTS.map((port) => {
|
|
1093
|
+
return new Promise((resolve) => {
|
|
1094
|
+
const socket = createConnection({ port, host: "127.0.0.1" });
|
|
1095
|
+
socket.setTimeout(200);
|
|
1096
|
+
socket.on("connect", () => {
|
|
1097
|
+
openPorts.push(port);
|
|
1098
|
+
socket.destroy();
|
|
1099
|
+
resolve();
|
|
1100
|
+
});
|
|
1101
|
+
socket.on("timeout", () => {
|
|
1102
|
+
socket.destroy();
|
|
1103
|
+
resolve();
|
|
1104
|
+
});
|
|
1105
|
+
socket.on("error", () => {
|
|
1106
|
+
resolve();
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
});
|
|
1110
|
+
await Promise.all(checks);
|
|
1111
|
+
return openPorts.sort((a, b) => a - b);
|
|
1112
|
+
}
|
|
1113
|
+
async function handleProxyConnect(payload) {
|
|
1114
|
+
const tunnelId = payload.tunnelId;
|
|
1115
|
+
const port = payload.port;
|
|
1116
|
+
if (!tunnelId)
|
|
1117
|
+
throw Object.assign(new Error("tunnelId is required"), { code: "EINVAL" });
|
|
1118
|
+
if (!port)
|
|
1119
|
+
throw Object.assign(new Error("port is required"), { code: "EINVAL" });
|
|
1120
|
+
if (!currentSessionCode)
|
|
1121
|
+
throw Object.assign(new Error("no active session"), { code: "ENOENT" });
|
|
1122
|
+
// 1. Open TCP connection to the local service
|
|
1123
|
+
const tcpSocket = createConnection({ port, host: "127.0.0.1" });
|
|
1124
|
+
await new Promise((resolve, reject) => {
|
|
1125
|
+
const timeout = setTimeout(() => {
|
|
1126
|
+
tcpSocket.destroy();
|
|
1127
|
+
reject(Object.assign(new Error(`TCP connect timeout to localhost:${port}`), { code: "ETIMEOUT" }));
|
|
1128
|
+
}, 5000);
|
|
1129
|
+
tcpSocket.on("connect", () => {
|
|
1130
|
+
clearTimeout(timeout);
|
|
1131
|
+
resolve();
|
|
1132
|
+
});
|
|
1133
|
+
tcpSocket.on("error", (err) => {
|
|
1134
|
+
clearTimeout(timeout);
|
|
1135
|
+
reject(Object.assign(new Error(`TCP connect failed: ${err.message}`), { code: "ECONNREFUSED" }));
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
// 2. Open proxy WebSocket to gateway
|
|
1139
|
+
const wsBase = PROXY_URL.replace(/^http/, "ws");
|
|
1140
|
+
const proxyWsUrl = `${wsBase}/v1/ws/proxy?code=${currentSessionCode}&tunnelId=${tunnelId}&role=cli`;
|
|
1141
|
+
const proxyWs = new WebSocket(proxyWsUrl);
|
|
1142
|
+
await new Promise((resolve, reject) => {
|
|
1143
|
+
const timeout = setTimeout(() => {
|
|
1144
|
+
proxyWs.close();
|
|
1145
|
+
tcpSocket.destroy();
|
|
1146
|
+
reject(Object.assign(new Error("Proxy WS connect timeout"), { code: "ETIMEOUT" }));
|
|
1147
|
+
}, 5000);
|
|
1148
|
+
proxyWs.on("open", () => {
|
|
1149
|
+
clearTimeout(timeout);
|
|
1150
|
+
resolve();
|
|
1151
|
+
});
|
|
1152
|
+
proxyWs.on("error", (err) => {
|
|
1153
|
+
clearTimeout(timeout);
|
|
1154
|
+
tcpSocket.destroy();
|
|
1155
|
+
reject(Object.assign(new Error(`Proxy WS failed: ${err.message}`), { code: "ECONNREFUSED" }));
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
// 3. Store the tunnel
|
|
1159
|
+
activeTunnels.set(tunnelId, { tunnelId, port, tcpSocket, proxyWs });
|
|
1160
|
+
// 4. Pipe: TCP data -> proxy WS (as binary)
|
|
1161
|
+
tcpSocket.on("data", (chunk) => {
|
|
1162
|
+
if (proxyWs.readyState === WebSocket.OPEN) {
|
|
1163
|
+
proxyWs.send(chunk);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
// 5. Pipe: proxy WS -> TCP socket (as binary)
|
|
1167
|
+
proxyWs.on("message", (data) => {
|
|
1168
|
+
if (!tcpSocket.destroyed) {
|
|
1169
|
+
tcpSocket.write(data);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
// 6. Close cascade: TCP closes -> close WS
|
|
1173
|
+
tcpSocket.on("close", () => {
|
|
1174
|
+
activeTunnels.delete(tunnelId);
|
|
1175
|
+
if (proxyWs.readyState === WebSocket.OPEN || proxyWs.readyState === WebSocket.CONNECTING) {
|
|
1176
|
+
proxyWs.close();
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
tcpSocket.on("error", () => {
|
|
1180
|
+
activeTunnels.delete(tunnelId);
|
|
1181
|
+
if (proxyWs.readyState === WebSocket.OPEN || proxyWs.readyState === WebSocket.CONNECTING) {
|
|
1182
|
+
proxyWs.close();
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
// 7. Close cascade: WS closes -> close TCP
|
|
1186
|
+
proxyWs.on("close", () => {
|
|
1187
|
+
activeTunnels.delete(tunnelId);
|
|
1188
|
+
if (!tcpSocket.destroyed) {
|
|
1189
|
+
tcpSocket.destroy();
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
proxyWs.on("error", () => {
|
|
1193
|
+
activeTunnels.delete(tunnelId);
|
|
1194
|
+
if (!tcpSocket.destroyed) {
|
|
1195
|
+
tcpSocket.destroy();
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
return { tunnelId, port };
|
|
1199
|
+
}
|
|
1200
|
+
function cleanupAllTunnels() {
|
|
1201
|
+
for (const [, tunnel] of activeTunnels) {
|
|
1202
|
+
tunnel.tcpSocket.destroy();
|
|
1203
|
+
if (tunnel.proxyWs.readyState === WebSocket.OPEN) {
|
|
1204
|
+
tunnel.proxyWs.close();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
activeTunnels.clear();
|
|
1208
|
+
}
|
|
1209
|
+
// ============================================================================
|
|
1276
1210
|
// Message Router
|
|
1277
1211
|
// ============================================================================
|
|
1278
1212
|
async function processMessage(message) {
|
|
@@ -1460,6 +1394,15 @@ async function processMessage(message) {
|
|
|
1460
1394
|
throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
|
|
1461
1395
|
}
|
|
1462
1396
|
break;
|
|
1397
|
+
case "proxy":
|
|
1398
|
+
switch (action) {
|
|
1399
|
+
case "connect":
|
|
1400
|
+
result = await handleProxyConnect(payload);
|
|
1401
|
+
break;
|
|
1402
|
+
default:
|
|
1403
|
+
throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
|
|
1404
|
+
}
|
|
1405
|
+
break;
|
|
1463
1406
|
default:
|
|
1464
1407
|
throw Object.assign(new Error(`Unknown namespace: ${ns}`), { code: "EINVAL" });
|
|
1465
1408
|
}
|
|
@@ -1506,6 +1449,7 @@ function displayQR(code) {
|
|
|
1506
1449
|
});
|
|
1507
1450
|
}
|
|
1508
1451
|
function connectWebSocket(code) {
|
|
1452
|
+
currentSessionCode = code;
|
|
1509
1453
|
const wsBase = PROXY_URL.replace(/^http/, "ws");
|
|
1510
1454
|
const controlUrl = `${wsBase}/v1/ws/cli/control?code=${code}`;
|
|
1511
1455
|
const dataUrl = `${wsBase}/v1/ws/cli/data?code=${code}`;
|
|
@@ -1537,6 +1481,23 @@ function connectWebSocket(code) {
|
|
|
1537
1481
|
}
|
|
1538
1482
|
if (message.type === "peer_connected") {
|
|
1539
1483
|
console.log("App connected!\n");
|
|
1484
|
+
// Scan dev ports and notify app
|
|
1485
|
+
scanDevPorts().then((openPorts) => {
|
|
1486
|
+
if (openPorts.length > 0) {
|
|
1487
|
+
console.log(`Found ${openPorts.length} open dev port(s): ${openPorts.join(", ")}`);
|
|
1488
|
+
}
|
|
1489
|
+
if (controlWs.readyState === WebSocket.OPEN) {
|
|
1490
|
+
controlWs.send(JSON.stringify({
|
|
1491
|
+
v: 1,
|
|
1492
|
+
id: `evt-${Date.now()}`,
|
|
1493
|
+
ns: "proxy",
|
|
1494
|
+
action: "ports_discovered",
|
|
1495
|
+
payload: { ports: openPorts },
|
|
1496
|
+
}));
|
|
1497
|
+
}
|
|
1498
|
+
}).catch((err) => {
|
|
1499
|
+
console.error("Port scan failed:", err);
|
|
1500
|
+
});
|
|
1540
1501
|
return;
|
|
1541
1502
|
}
|
|
1542
1503
|
if (message.type === "peer_disconnected") {
|
|
@@ -1555,6 +1516,7 @@ function connectWebSocket(code) {
|
|
|
1555
1516
|
});
|
|
1556
1517
|
controlWs.on("close", (code, reason) => {
|
|
1557
1518
|
console.log(`\nControl channel disconnected (${code}: ${reason.toString()})`);
|
|
1519
|
+
cleanupAllTunnels();
|
|
1558
1520
|
dataWs.close();
|
|
1559
1521
|
process.exit(0);
|
|
1560
1522
|
});
|
|
@@ -1585,6 +1547,7 @@ function connectWebSocket(code) {
|
|
|
1585
1547
|
});
|
|
1586
1548
|
dataWs.on("close", (code, reason) => {
|
|
1587
1549
|
console.log(`\nData channel disconnected (${code}: ${reason.toString()})`);
|
|
1550
|
+
cleanupAllTunnels();
|
|
1588
1551
|
controlWs.close();
|
|
1589
1552
|
process.exit(0);
|
|
1590
1553
|
});
|
|
@@ -1595,8 +1558,8 @@ function connectWebSocket(code) {
|
|
|
1595
1558
|
process.on("SIGINT", () => {
|
|
1596
1559
|
console.log("\nShutting down...");
|
|
1597
1560
|
// Kill all terminals
|
|
1598
|
-
for (const [id,
|
|
1599
|
-
|
|
1561
|
+
for (const [id, proc] of terminals) {
|
|
1562
|
+
proc.kill();
|
|
1600
1563
|
}
|
|
1601
1564
|
terminals.clear();
|
|
1602
1565
|
// Kill all managed processes
|
|
@@ -1605,6 +1568,7 @@ function connectWebSocket(code) {
|
|
|
1605
1568
|
}
|
|
1606
1569
|
processes.clear();
|
|
1607
1570
|
processOutputBuffers.clear();
|
|
1571
|
+
cleanupAllTunnels();
|
|
1608
1572
|
controlWs.close();
|
|
1609
1573
|
dataWs.close();
|
|
1610
1574
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lunel-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"author": [
|
|
5
5
|
{
|
|
6
6
|
"name": "Soham Bharambe",
|
|
@@ -25,9 +25,7 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@xterm/headless": "^5.5.0",
|
|
29
28
|
"ignore": "^6.0.2",
|
|
30
|
-
"node-pty": "^1.1.0",
|
|
31
29
|
"qrcode-terminal": "^0.12.0",
|
|
32
30
|
"ws": "^8.18.0"
|
|
33
31
|
},
|