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.
Files changed (2) hide show
  1. package/dist/index.js +240 -276
  2. 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 * as pty from "node-pty";
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 = JSON.parse(fsSync.readFileSync(path.join(__dirname, "../package.json"), "utf-8")).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
- // Simple ANSI parser for terminal state
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
- console.log(`[Terminal] Spawning PTY shell: ${shell} (${cols}x${rows}) in ${ROOT_DIR}`);
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: process.env,
713
- });
714
- console.log(`[Terminal] PTY spawned with PID: ${ptyProcess.pid}`);
715
- terminals.set(terminalId, {
716
- ptyProcess,
717
- shell,
718
- cols,
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
- // Process output and send grid state
726
- ptyProcess.onData((data) => {
727
- parser.write(data);
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: "state",
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
- ptyProcess.onExit(({ exitCode }) => {
748
- console.log(`[Terminal] PTY exited with code: ${exitCode}`);
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: exitCode },
574
+ payload: { terminalId, code },
757
575
  };
758
576
  dataChannel.send(JSON.stringify(msg));
759
577
  }
760
578
  });
761
- return { terminalId, shell, cols, rows };
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 session = terminals.get(terminalId);
771
- if (!session)
588
+ const proc = terminals.get(terminalId);
589
+ if (!proc)
772
590
  throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
773
- session.ptyProcess.write(data);
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
- if (!cols || !rows)
783
- throw Object.assign(new Error("cols and rows are required"), { code: "EINVAL" });
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
- // Resize PTY (sends SIGWINCH)
788
- session.ptyProcess.resize(cols, rows);
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 session = terminals.get(terminalId);
800
- if (!session)
611
+ const proc = terminals.get(terminalId);
612
+ if (!proc)
801
613
  throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
802
- session.ptyProcess.kill();
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, session] of terminals) {
1599
- session.ptyProcess.kill();
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.13",
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
  },