lunel-cli 0.1.29 → 0.1.31
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 +329 -135
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,9 @@ import * as os from "os";
|
|
|
11
11
|
import { spawn, execSync } from "child_process";
|
|
12
12
|
import { createServer, createConnection } from "net";
|
|
13
13
|
import { createInterface } from "readline";
|
|
14
|
-
const
|
|
14
|
+
const DEFAULT_PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
|
|
15
|
+
const MANAGER_URL = process.env.LUNEL_MANAGER_URL || "https://manager.lunel.dev";
|
|
16
|
+
const CLI_ARGS = process.argv.slice(2);
|
|
15
17
|
import { createRequire } from "module";
|
|
16
18
|
const __require = createRequire(import.meta.url);
|
|
17
19
|
const VERSION = __require("../package.json").version;
|
|
@@ -30,7 +32,18 @@ let lastCpuInfo = null;
|
|
|
30
32
|
let opencodeClient = null;
|
|
31
33
|
// Proxy tunnel management
|
|
32
34
|
let currentSessionCode = null;
|
|
35
|
+
let currentSessionPassword = null;
|
|
36
|
+
let currentPrimaryGateway = DEFAULT_PROXY_URL;
|
|
37
|
+
let currentBackupGateway = null;
|
|
38
|
+
let activeGatewayUrl = DEFAULT_PROXY_URL;
|
|
39
|
+
let shuttingDown = false;
|
|
40
|
+
let activeControlWs = null;
|
|
41
|
+
let activeDataWs = null;
|
|
33
42
|
const activeTunnels = new Map();
|
|
43
|
+
const PORT_SYNC_INTERVAL_MS = 30_000;
|
|
44
|
+
let portSyncTimer = null;
|
|
45
|
+
let portScanInFlight = false;
|
|
46
|
+
let lastDiscoveredPorts = [];
|
|
34
47
|
// Popular development server ports to scan on connect
|
|
35
48
|
const DEV_PORTS = [
|
|
36
49
|
1234, // Parcel
|
|
@@ -80,6 +93,44 @@ const DEV_PORTS = [
|
|
|
80
93
|
19006, // Expo web
|
|
81
94
|
24678, // Vite HMR WebSocket
|
|
82
95
|
];
|
|
96
|
+
function parseExtraPortsFromArgs(args) {
|
|
97
|
+
const values = [];
|
|
98
|
+
for (let i = 0; i < args.length; i++) {
|
|
99
|
+
const arg = args[i];
|
|
100
|
+
if (arg.startsWith("--extra-ports=")) {
|
|
101
|
+
values.push(arg.slice("--extra-ports=".length));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (arg === "--extra-ports" && i + 1 < args.length) {
|
|
105
|
+
values.push(args[i + 1]);
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const parsed = new Set();
|
|
110
|
+
for (const value of values) {
|
|
111
|
+
for (const piece of value.split(",")) {
|
|
112
|
+
const trimmed = piece.trim();
|
|
113
|
+
if (!trimmed)
|
|
114
|
+
continue;
|
|
115
|
+
const num = Number(trimmed);
|
|
116
|
+
if (!Number.isInteger(num) || num < 1 || num > 65535)
|
|
117
|
+
continue;
|
|
118
|
+
parsed.add(num);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return Array.from(parsed).sort((a, b) => a - b);
|
|
122
|
+
}
|
|
123
|
+
const EXTRA_PORTS = parseExtraPortsFromArgs(CLI_ARGS);
|
|
124
|
+
const SCAN_PORTS = Array.from(new Set([...DEV_PORTS, ...EXTRA_PORTS])).sort((a, b) => a - b);
|
|
125
|
+
function samePortSet(a, b) {
|
|
126
|
+
if (a.length !== b.length)
|
|
127
|
+
return false;
|
|
128
|
+
for (let i = 0; i < a.length; i++) {
|
|
129
|
+
if (a[i] !== b[i])
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
83
134
|
// ============================================================================
|
|
84
135
|
// Path Safety
|
|
85
136
|
// ============================================================================
|
|
@@ -1405,7 +1456,7 @@ async function subscribeToOpenCodeEvents(client) {
|
|
|
1405
1456
|
// ============================================================================
|
|
1406
1457
|
async function scanDevPorts() {
|
|
1407
1458
|
const openPorts = [];
|
|
1408
|
-
const checks =
|
|
1459
|
+
const checks = SCAN_PORTS.map((port) => {
|
|
1409
1460
|
return new Promise((resolve) => {
|
|
1410
1461
|
const socket = createConnection({ port, host: "127.0.0.1" });
|
|
1411
1462
|
socket.setTimeout(200);
|
|
@@ -1426,6 +1477,48 @@ async function scanDevPorts() {
|
|
|
1426
1477
|
await Promise.all(checks);
|
|
1427
1478
|
return openPorts.sort((a, b) => a - b);
|
|
1428
1479
|
}
|
|
1480
|
+
async function publishDiscoveredPorts(ws, force = false) {
|
|
1481
|
+
if (portScanInFlight)
|
|
1482
|
+
return;
|
|
1483
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
1484
|
+
return;
|
|
1485
|
+
portScanInFlight = true;
|
|
1486
|
+
try {
|
|
1487
|
+
const openPorts = await scanDevPorts();
|
|
1488
|
+
if (!force && samePortSet(openPorts, lastDiscoveredPorts)) {
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
lastDiscoveredPorts = openPorts;
|
|
1492
|
+
ws.send(JSON.stringify({
|
|
1493
|
+
v: 1,
|
|
1494
|
+
id: `evt-${Date.now()}`,
|
|
1495
|
+
ns: "proxy",
|
|
1496
|
+
action: "ports_discovered",
|
|
1497
|
+
payload: { ports: openPorts },
|
|
1498
|
+
}));
|
|
1499
|
+
console.log(`[proxy] ports updated (${openPorts.length}): ${openPorts.join(", ") || "-"}`);
|
|
1500
|
+
}
|
|
1501
|
+
catch (err) {
|
|
1502
|
+
console.error("Port scan failed:", err);
|
|
1503
|
+
}
|
|
1504
|
+
finally {
|
|
1505
|
+
portScanInFlight = false;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function stopPortSync() {
|
|
1509
|
+
if (portSyncTimer) {
|
|
1510
|
+
clearInterval(portSyncTimer);
|
|
1511
|
+
portSyncTimer = null;
|
|
1512
|
+
}
|
|
1513
|
+
portScanInFlight = false;
|
|
1514
|
+
}
|
|
1515
|
+
function startPortSync(ws) {
|
|
1516
|
+
stopPortSync();
|
|
1517
|
+
void publishDiscoveredPorts(ws, true);
|
|
1518
|
+
portSyncTimer = setInterval(() => {
|
|
1519
|
+
void publishDiscoveredPorts(ws, false);
|
|
1520
|
+
}, PORT_SYNC_INTERVAL_MS);
|
|
1521
|
+
}
|
|
1429
1522
|
async function handleProxyConnect(payload) {
|
|
1430
1523
|
const tunnelId = payload.tunnelId;
|
|
1431
1524
|
const port = payload.port;
|
|
@@ -1433,7 +1526,7 @@ async function handleProxyConnect(payload) {
|
|
|
1433
1526
|
throw Object.assign(new Error("tunnelId is required"), { code: "EINVAL" });
|
|
1434
1527
|
if (!port)
|
|
1435
1528
|
throw Object.assign(new Error("port is required"), { code: "EINVAL" });
|
|
1436
|
-
if (!currentSessionCode)
|
|
1529
|
+
if (!currentSessionCode && !currentSessionPassword)
|
|
1437
1530
|
throw Object.assign(new Error("no active session"), { code: "ENOENT" });
|
|
1438
1531
|
// 1. Open TCP connection to the local service
|
|
1439
1532
|
const tcpSocket = createConnection({ port, host: "127.0.0.1" });
|
|
@@ -1452,8 +1545,11 @@ async function handleProxyConnect(payload) {
|
|
|
1452
1545
|
});
|
|
1453
1546
|
});
|
|
1454
1547
|
// 2. Open proxy WebSocket to gateway
|
|
1455
|
-
const wsBase =
|
|
1456
|
-
const
|
|
1548
|
+
const wsBase = activeGatewayUrl.replace(/^http/, "ws");
|
|
1549
|
+
const authQuery = currentSessionPassword
|
|
1550
|
+
? `password=${encodeURIComponent(currentSessionPassword)}`
|
|
1551
|
+
: `code=${encodeURIComponent(currentSessionCode)}`;
|
|
1552
|
+
const proxyWsUrl = `${wsBase}/v1/ws/proxy?${authQuery}&tunnelId=${encodeURIComponent(tunnelId)}&role=cli`;
|
|
1457
1553
|
const proxyWs = new WebSocket(proxyWsUrl);
|
|
1458
1554
|
await new Promise((resolve, reject) => {
|
|
1459
1555
|
const timeout = setTimeout(() => {
|
|
@@ -1808,160 +1904,251 @@ async function processMessage(message) {
|
|
|
1808
1904
|
};
|
|
1809
1905
|
}
|
|
1810
1906
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1907
|
+
function normalizeGatewayUrl(input) {
|
|
1908
|
+
if (/^https?:\/\//.test(input))
|
|
1909
|
+
return input.replace(/\/+$/, "");
|
|
1910
|
+
return `https://${input}`.replace(/\/+$/, "");
|
|
1911
|
+
}
|
|
1912
|
+
async function createSessionFromManager() {
|
|
1913
|
+
const response = await fetch(`${MANAGER_URL}/v1/session`, {
|
|
1816
1914
|
method: "POST",
|
|
1817
1915
|
headers: { "Content-Type": "application/json" },
|
|
1818
1916
|
});
|
|
1819
1917
|
if (!response.ok) {
|
|
1820
|
-
throw new Error(`Failed to create session: ${response.status}`);
|
|
1918
|
+
throw new Error(`Failed to create session from manager: ${response.status}`);
|
|
1821
1919
|
}
|
|
1822
|
-
|
|
1823
|
-
return data.code;
|
|
1920
|
+
return (await response.json());
|
|
1824
1921
|
}
|
|
1825
|
-
function displayQR(code) {
|
|
1922
|
+
function displayQR(primaryGateway, backupGateway, code) {
|
|
1826
1923
|
console.log("\n");
|
|
1827
1924
|
qrcode.generate(code, { small: true }, (qr) => {
|
|
1828
1925
|
console.log(qr);
|
|
1829
1926
|
console.log(`\n Session code: ${code}\n`);
|
|
1927
|
+
console.log(` Primary gateway: ${primaryGateway}`);
|
|
1928
|
+
if (backupGateway) {
|
|
1929
|
+
console.log(` Backup gateway: ${backupGateway}`);
|
|
1930
|
+
}
|
|
1830
1931
|
console.log(` Root directory: ${ROOT_DIR}\n`);
|
|
1831
1932
|
console.log(" Scan the QR code with the Lunel app to connect.");
|
|
1832
1933
|
console.log(" Press Ctrl+C to exit.\n");
|
|
1833
1934
|
});
|
|
1834
1935
|
}
|
|
1835
|
-
function
|
|
1836
|
-
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
const dataWs = new WebSocket(dataUrl);
|
|
1846
|
-
let dataConnected = false;
|
|
1847
|
-
// Store data channel reference for terminal output
|
|
1848
|
-
dataChannel = dataWs;
|
|
1849
|
-
function checkFullyConnected() {
|
|
1850
|
-
if (controlConnected && dataConnected) {
|
|
1851
|
-
console.log("Connected to proxy (control + data channels). Waiting for app...\n");
|
|
1936
|
+
function buildWsUrl(gatewayUrl, role, channel) {
|
|
1937
|
+
const wsBase = gatewayUrl.replace(/^http/, "ws");
|
|
1938
|
+
const query = new URLSearchParams();
|
|
1939
|
+
if (currentSessionPassword) {
|
|
1940
|
+
query.set("password", currentSessionPassword);
|
|
1941
|
+
}
|
|
1942
|
+
else if (currentSessionCode) {
|
|
1943
|
+
query.set("code", currentSessionCode);
|
|
1944
|
+
if (currentBackupGateway) {
|
|
1945
|
+
query.set("backup", currentBackupGateway);
|
|
1852
1946
|
}
|
|
1853
1947
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1948
|
+
else {
|
|
1949
|
+
throw new Error("missing code/password for websocket connect");
|
|
1950
|
+
}
|
|
1951
|
+
return `${wsBase}/v1/ws/${role}/${channel}?${query.toString()}`;
|
|
1952
|
+
}
|
|
1953
|
+
function gracefulShutdown() {
|
|
1954
|
+
shuttingDown = true;
|
|
1955
|
+
console.log("\nShutting down...");
|
|
1956
|
+
stopPortSync();
|
|
1957
|
+
if (ptyProcess) {
|
|
1958
|
+
ptyProcess.kill();
|
|
1959
|
+
ptyProcess = null;
|
|
1960
|
+
}
|
|
1961
|
+
terminals.clear();
|
|
1962
|
+
for (const [pid, managedProc] of processes) {
|
|
1963
|
+
managedProc.proc.kill();
|
|
1964
|
+
}
|
|
1965
|
+
processes.clear();
|
|
1966
|
+
processOutputBuffers.clear();
|
|
1967
|
+
cleanupAllTunnels();
|
|
1968
|
+
if (activeControlWs && (activeControlWs.readyState === WebSocket.OPEN || activeControlWs.readyState === WebSocket.CONNECTING)) {
|
|
1969
|
+
activeControlWs.close();
|
|
1970
|
+
}
|
|
1971
|
+
if (activeDataWs && (activeDataWs.readyState === WebSocket.OPEN || activeDataWs.readyState === WebSocket.CONNECTING)) {
|
|
1972
|
+
activeDataWs.close();
|
|
1973
|
+
}
|
|
1974
|
+
process.exit(0);
|
|
1975
|
+
}
|
|
1976
|
+
async function connectWebSocket() {
|
|
1977
|
+
const gateways = currentBackupGateway ? [currentPrimaryGateway, currentBackupGateway] : [currentPrimaryGateway];
|
|
1978
|
+
const uniqueGateways = Array.from(new Set(gateways));
|
|
1979
|
+
for (const gatewayUrl of uniqueGateways) {
|
|
1860
1980
|
try {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1981
|
+
await new Promise((resolve, reject) => {
|
|
1982
|
+
activeGatewayUrl = gatewayUrl;
|
|
1983
|
+
const controlUrl = buildWsUrl(gatewayUrl, "cli", "control");
|
|
1984
|
+
const dataUrl = buildWsUrl(gatewayUrl, "cli", "data");
|
|
1985
|
+
console.log(`Connecting to gateway ${gatewayUrl}...`);
|
|
1986
|
+
const controlWs = new WebSocket(controlUrl);
|
|
1987
|
+
const dataWs = new WebSocket(dataUrl);
|
|
1988
|
+
activeControlWs = controlWs;
|
|
1989
|
+
activeDataWs = dataWs;
|
|
1990
|
+
dataChannel = dataWs;
|
|
1991
|
+
let controlConnected = false;
|
|
1992
|
+
let dataConnected = false;
|
|
1993
|
+
let settled = false;
|
|
1994
|
+
let closeHandled = false;
|
|
1995
|
+
let closeReason = "";
|
|
1996
|
+
const failConnection = (reason) => {
|
|
1997
|
+
if (settled)
|
|
1998
|
+
return;
|
|
1999
|
+
settled = true;
|
|
2000
|
+
reject(new Error(reason));
|
|
2001
|
+
};
|
|
2002
|
+
const checkFullyConnected = () => {
|
|
2003
|
+
if (controlConnected && dataConnected && !settled) {
|
|
2004
|
+
settled = true;
|
|
2005
|
+
console.log("Connected to gateway (control + data channels).\n");
|
|
2006
|
+
resolve();
|
|
1872
2007
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2008
|
+
};
|
|
2009
|
+
const handleClose = (reason) => {
|
|
2010
|
+
if (closeHandled || shuttingDown)
|
|
2011
|
+
return;
|
|
2012
|
+
closeHandled = true;
|
|
2013
|
+
closeReason = reason;
|
|
2014
|
+
stopPortSync();
|
|
2015
|
+
cleanupAllTunnels();
|
|
2016
|
+
setTimeout(() => {
|
|
2017
|
+
if (shuttingDown)
|
|
2018
|
+
return;
|
|
2019
|
+
void handleConnectionDrop(closeReason);
|
|
2020
|
+
}, 50);
|
|
2021
|
+
};
|
|
2022
|
+
controlWs.on("open", () => {
|
|
2023
|
+
controlConnected = true;
|
|
2024
|
+
checkFullyConnected();
|
|
2025
|
+
});
|
|
2026
|
+
controlWs.on("message", async (data) => {
|
|
2027
|
+
try {
|
|
2028
|
+
const message = JSON.parse(data.toString());
|
|
2029
|
+
if ("type" in message) {
|
|
2030
|
+
if (message.type === "connected")
|
|
2031
|
+
return;
|
|
2032
|
+
if (message.type === "session_password" && message.password) {
|
|
2033
|
+
currentSessionPassword = message.password;
|
|
2034
|
+
console.log("[session] received reconnect password");
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
if (message.type === "peer_connected") {
|
|
2038
|
+
console.log("App connected!\n");
|
|
2039
|
+
startPortSync(controlWs);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (message.type === "peer_disconnected") {
|
|
2043
|
+
console.log("App disconnected. Waiting for reconnect window.\n");
|
|
2044
|
+
stopPortSync();
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
if (message.type === "app_disconnected") {
|
|
2048
|
+
if (message.reconnectDeadline) {
|
|
2049
|
+
console.log(`[session] app disconnected, waiting until ${new Date(message.reconnectDeadline).toISOString()}`);
|
|
2050
|
+
}
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (message.type === "close_connection") {
|
|
2054
|
+
console.log(`[session] closed by gateway: ${message.reason || "expired"}`);
|
|
2055
|
+
gracefulShutdown();
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if (message.v === 1) {
|
|
2060
|
+
const response = await processMessage(message);
|
|
2061
|
+
controlWs.send(JSON.stringify(response));
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
catch (error) {
|
|
2065
|
+
console.error("Error processing control message:", error);
|
|
1881
2066
|
}
|
|
1882
|
-
}).catch((err) => {
|
|
1883
|
-
console.error("Port scan failed:", err);
|
|
1884
2067
|
});
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
dataWs.on("error", (error) => {
|
|
1939
|
-
console.error("Data WebSocket error:", error.message);
|
|
1940
|
-
});
|
|
1941
|
-
// Handle graceful shutdown
|
|
1942
|
-
process.on("SIGINT", () => {
|
|
1943
|
-
console.log("\nShutting down...");
|
|
1944
|
-
// Kill PTY process (kills all terminals)
|
|
1945
|
-
if (ptyProcess) {
|
|
1946
|
-
ptyProcess.kill();
|
|
1947
|
-
ptyProcess = null;
|
|
2068
|
+
controlWs.on("close", (code, reason) => {
|
|
2069
|
+
if (!settled) {
|
|
2070
|
+
failConnection(`control close before ready (${code}: ${reason.toString()})`);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
handleClose(`control closed (${code}: ${reason.toString()})`);
|
|
2074
|
+
});
|
|
2075
|
+
controlWs.on("error", (error) => {
|
|
2076
|
+
if (!settled) {
|
|
2077
|
+
failConnection(`control ws error: ${error.message}`);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
console.error("Control WebSocket error:", error.message);
|
|
2081
|
+
});
|
|
2082
|
+
dataWs.on("open", () => {
|
|
2083
|
+
dataConnected = true;
|
|
2084
|
+
checkFullyConnected();
|
|
2085
|
+
});
|
|
2086
|
+
dataWs.on("message", async (data) => {
|
|
2087
|
+
try {
|
|
2088
|
+
const message = JSON.parse(data.toString());
|
|
2089
|
+
if (message.type === "connected")
|
|
2090
|
+
return;
|
|
2091
|
+
if (message.v === 1) {
|
|
2092
|
+
const response = await processMessage(message);
|
|
2093
|
+
dataWs.send(JSON.stringify(response));
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
catch (error) {
|
|
2097
|
+
console.error("Error processing data message:", error);
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
dataWs.on("close", (code, reason) => {
|
|
2101
|
+
if (!settled) {
|
|
2102
|
+
failConnection(`data close before ready (${code}: ${reason.toString()})`);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
handleClose(`data closed (${code}: ${reason.toString()})`);
|
|
2106
|
+
});
|
|
2107
|
+
dataWs.on("error", (error) => {
|
|
2108
|
+
if (!settled) {
|
|
2109
|
+
failConnection(`data ws error: ${error.message}`);
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
console.error("Data WebSocket error:", error.message);
|
|
2113
|
+
});
|
|
2114
|
+
setTimeout(() => {
|
|
2115
|
+
if (!settled) {
|
|
2116
|
+
failConnection("connection timeout");
|
|
2117
|
+
}
|
|
2118
|
+
}, 10000);
|
|
2119
|
+
});
|
|
2120
|
+
return;
|
|
1948
2121
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
for (const [pid, managedProc] of processes) {
|
|
1952
|
-
managedProc.proc.kill();
|
|
2122
|
+
catch (err) {
|
|
2123
|
+
console.error(`[gateway] failed ${gatewayUrl}: ${err.message}`);
|
|
1953
2124
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
});
|
|
2125
|
+
}
|
|
2126
|
+
throw new Error("unable to connect to any gateway");
|
|
2127
|
+
}
|
|
2128
|
+
async function handleConnectionDrop(reason) {
|
|
2129
|
+
if (shuttingDown)
|
|
2130
|
+
return;
|
|
2131
|
+
console.log(`\nDisconnected: ${reason}`);
|
|
2132
|
+
if (!currentSessionPassword) {
|
|
2133
|
+
console.error("No reconnect password available. Exiting.");
|
|
2134
|
+
gracefulShutdown();
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
try {
|
|
2138
|
+
await connectWebSocket();
|
|
2139
|
+
console.log(`[reconnect] connected via ${activeGatewayUrl}`);
|
|
2140
|
+
}
|
|
2141
|
+
catch (err) {
|
|
2142
|
+
console.error(`[reconnect] failed on all gateways: ${err.message}`);
|
|
2143
|
+
gracefulShutdown();
|
|
2144
|
+
}
|
|
1961
2145
|
}
|
|
1962
2146
|
async function main() {
|
|
1963
2147
|
console.log("Lunel CLI v" + VERSION);
|
|
1964
2148
|
console.log("=".repeat(20) + "\n");
|
|
2149
|
+
if (EXTRA_PORTS.length > 0) {
|
|
2150
|
+
console.log(`Extra ports enabled: ${EXTRA_PORTS.join(", ")}`);
|
|
2151
|
+
}
|
|
1965
2152
|
try {
|
|
1966
2153
|
// Generate auth credentials (like CodeNomad does)
|
|
1967
2154
|
const opencodeUsername = "lunel";
|
|
@@ -1989,9 +2176,14 @@ async function main() {
|
|
|
1989
2176
|
console.log("OpenCode ready.\n");
|
|
1990
2177
|
// Subscribe to OpenCode events
|
|
1991
2178
|
subscribeToOpenCodeEvents(client);
|
|
1992
|
-
const
|
|
1993
|
-
|
|
1994
|
-
|
|
2179
|
+
const session = await createSessionFromManager();
|
|
2180
|
+
currentPrimaryGateway = normalizeGatewayUrl(session.primary);
|
|
2181
|
+
currentBackupGateway = session.backup ? normalizeGatewayUrl(session.backup) : null;
|
|
2182
|
+
currentSessionPassword = session.password;
|
|
2183
|
+
activeGatewayUrl = currentPrimaryGateway;
|
|
2184
|
+
currentSessionCode = session.code;
|
|
2185
|
+
displayQR(currentPrimaryGateway, currentBackupGateway, session.code);
|
|
2186
|
+
await connectWebSocket();
|
|
1995
2187
|
}
|
|
1996
2188
|
catch (error) {
|
|
1997
2189
|
if (error instanceof Error) {
|
|
@@ -2005,4 +2197,6 @@ async function main() {
|
|
|
2005
2197
|
process.exit(1);
|
|
2006
2198
|
}
|
|
2007
2199
|
}
|
|
2200
|
+
process.on("SIGINT", gracefulShutdown);
|
|
2201
|
+
process.on("SIGTERM", gracefulShutdown);
|
|
2008
2202
|
main();
|