lunel-cli 0.1.30 → 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 +104 -54
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createServer, createConnection } from "net";
|
|
|
13
13
|
import { createInterface } from "readline";
|
|
14
14
|
const DEFAULT_PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
|
|
15
15
|
const MANAGER_URL = process.env.LUNEL_MANAGER_URL || "https://manager.lunel.dev";
|
|
16
|
+
const CLI_ARGS = process.argv.slice(2);
|
|
16
17
|
import { createRequire } from "module";
|
|
17
18
|
const __require = createRequire(import.meta.url);
|
|
18
19
|
const VERSION = __require("../package.json").version;
|
|
@@ -39,6 +40,10 @@ let shuttingDown = false;
|
|
|
39
40
|
let activeControlWs = null;
|
|
40
41
|
let activeDataWs = null;
|
|
41
42
|
const activeTunnels = new Map();
|
|
43
|
+
const PORT_SYNC_INTERVAL_MS = 30_000;
|
|
44
|
+
let portSyncTimer = null;
|
|
45
|
+
let portScanInFlight = false;
|
|
46
|
+
let lastDiscoveredPorts = [];
|
|
42
47
|
// Popular development server ports to scan on connect
|
|
43
48
|
const DEV_PORTS = [
|
|
44
49
|
1234, // Parcel
|
|
@@ -88,6 +93,44 @@ const DEV_PORTS = [
|
|
|
88
93
|
19006, // Expo web
|
|
89
94
|
24678, // Vite HMR WebSocket
|
|
90
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
|
+
}
|
|
91
134
|
// ============================================================================
|
|
92
135
|
// Path Safety
|
|
93
136
|
// ============================================================================
|
|
@@ -1413,7 +1456,7 @@ async function subscribeToOpenCodeEvents(client) {
|
|
|
1413
1456
|
// ============================================================================
|
|
1414
1457
|
async function scanDevPorts() {
|
|
1415
1458
|
const openPorts = [];
|
|
1416
|
-
const checks =
|
|
1459
|
+
const checks = SCAN_PORTS.map((port) => {
|
|
1417
1460
|
return new Promise((resolve) => {
|
|
1418
1461
|
const socket = createConnection({ port, host: "127.0.0.1" });
|
|
1419
1462
|
socket.setTimeout(200);
|
|
@@ -1434,6 +1477,48 @@ async function scanDevPorts() {
|
|
|
1434
1477
|
await Promise.all(checks);
|
|
1435
1478
|
return openPorts.sort((a, b) => a - b);
|
|
1436
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
|
+
}
|
|
1437
1522
|
async function handleProxyConnect(payload) {
|
|
1438
1523
|
const tunnelId = payload.tunnelId;
|
|
1439
1524
|
const port = payload.port;
|
|
@@ -1824,45 +1909,19 @@ function normalizeGatewayUrl(input) {
|
|
|
1824
1909
|
return input.replace(/\/+$/, "");
|
|
1825
1910
|
return `https://${input}`.replace(/\/+$/, "");
|
|
1826
1911
|
}
|
|
1827
|
-
async function
|
|
1828
|
-
|
|
1829
|
-
const response = await fetch(`${MANAGER_URL}/v1/gateways`, {
|
|
1830
|
-
method: "GET",
|
|
1831
|
-
headers: { "Content-Type": "application/json" },
|
|
1832
|
-
});
|
|
1833
|
-
if (!response.ok) {
|
|
1834
|
-
throw new Error(`manager returned ${response.status}`);
|
|
1835
|
-
}
|
|
1836
|
-
const data = (await response.json());
|
|
1837
|
-
const gateways = (data.gateways || []).map((x) => normalizeGatewayUrl(x)).filter(Boolean);
|
|
1838
|
-
const primary = data.primary ? normalizeGatewayUrl(data.primary) : gateways[0];
|
|
1839
|
-
const backup = data.backup ? normalizeGatewayUrl(data.backup) : gateways[1] || null;
|
|
1840
|
-
if (!primary)
|
|
1841
|
-
throw new Error("manager returned no healthy gateways");
|
|
1842
|
-
return { primary, backup };
|
|
1843
|
-
}
|
|
1844
|
-
catch (err) {
|
|
1845
|
-
console.warn(`[manager] gateway discovery failed, using default: ${err.message}`);
|
|
1846
|
-
return { primary: DEFAULT_PROXY_URL, backup: null };
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
async function createSession(gatewayUrl) {
|
|
1850
|
-
const response = await fetch(`${gatewayUrl}/v1/session`, {
|
|
1912
|
+
async function createSessionFromManager() {
|
|
1913
|
+
const response = await fetch(`${MANAGER_URL}/v1/session`, {
|
|
1851
1914
|
method: "POST",
|
|
1852
1915
|
headers: { "Content-Type": "application/json" },
|
|
1853
1916
|
});
|
|
1854
1917
|
if (!response.ok) {
|
|
1855
|
-
throw new Error(`Failed to create session: ${response.status}`);
|
|
1918
|
+
throw new Error(`Failed to create session from manager: ${response.status}`);
|
|
1856
1919
|
}
|
|
1857
|
-
|
|
1858
|
-
return data.code;
|
|
1920
|
+
return (await response.json());
|
|
1859
1921
|
}
|
|
1860
1922
|
function displayQR(primaryGateway, backupGateway, code) {
|
|
1861
|
-
const qrPayload = backupGateway
|
|
1862
|
-
? `${primaryGateway},${backupGateway},${code}`
|
|
1863
|
-
: `${primaryGateway},${code}`;
|
|
1864
1923
|
console.log("\n");
|
|
1865
|
-
qrcode.generate(
|
|
1924
|
+
qrcode.generate(code, { small: true }, (qr) => {
|
|
1866
1925
|
console.log(qr);
|
|
1867
1926
|
console.log(`\n Session code: ${code}\n`);
|
|
1868
1927
|
console.log(` Primary gateway: ${primaryGateway}`);
|
|
@@ -1894,6 +1953,7 @@ function buildWsUrl(gatewayUrl, role, channel) {
|
|
|
1894
1953
|
function gracefulShutdown() {
|
|
1895
1954
|
shuttingDown = true;
|
|
1896
1955
|
console.log("\nShutting down...");
|
|
1956
|
+
stopPortSync();
|
|
1897
1957
|
if (ptyProcess) {
|
|
1898
1958
|
ptyProcess.kill();
|
|
1899
1959
|
ptyProcess = null;
|
|
@@ -1951,6 +2011,7 @@ async function connectWebSocket() {
|
|
|
1951
2011
|
return;
|
|
1952
2012
|
closeHandled = true;
|
|
1953
2013
|
closeReason = reason;
|
|
2014
|
+
stopPortSync();
|
|
1954
2015
|
cleanupAllTunnels();
|
|
1955
2016
|
setTimeout(() => {
|
|
1956
2017
|
if (shuttingDown)
|
|
@@ -1975,26 +2036,12 @@ async function connectWebSocket() {
|
|
|
1975
2036
|
}
|
|
1976
2037
|
if (message.type === "peer_connected") {
|
|
1977
2038
|
console.log("App connected!\n");
|
|
1978
|
-
|
|
1979
|
-
if (openPorts.length > 0) {
|
|
1980
|
-
console.log(`Found ${openPorts.length} open dev port(s): ${openPorts.join(", ")}`);
|
|
1981
|
-
}
|
|
1982
|
-
if (controlWs.readyState === WebSocket.OPEN) {
|
|
1983
|
-
controlWs.send(JSON.stringify({
|
|
1984
|
-
v: 1,
|
|
1985
|
-
id: `evt-${Date.now()}`,
|
|
1986
|
-
ns: "proxy",
|
|
1987
|
-
action: "ports_discovered",
|
|
1988
|
-
payload: { ports: openPorts },
|
|
1989
|
-
}));
|
|
1990
|
-
}
|
|
1991
|
-
}).catch((err) => {
|
|
1992
|
-
console.error("Port scan failed:", err);
|
|
1993
|
-
});
|
|
2039
|
+
startPortSync(controlWs);
|
|
1994
2040
|
return;
|
|
1995
2041
|
}
|
|
1996
2042
|
if (message.type === "peer_disconnected") {
|
|
1997
2043
|
console.log("App disconnected. Waiting for reconnect window.\n");
|
|
2044
|
+
stopPortSync();
|
|
1998
2045
|
return;
|
|
1999
2046
|
}
|
|
2000
2047
|
if (message.type === "app_disconnected") {
|
|
@@ -2099,6 +2146,9 @@ async function handleConnectionDrop(reason) {
|
|
|
2099
2146
|
async function main() {
|
|
2100
2147
|
console.log("Lunel CLI v" + VERSION);
|
|
2101
2148
|
console.log("=".repeat(20) + "\n");
|
|
2149
|
+
if (EXTRA_PORTS.length > 0) {
|
|
2150
|
+
console.log(`Extra ports enabled: ${EXTRA_PORTS.join(", ")}`);
|
|
2151
|
+
}
|
|
2102
2152
|
try {
|
|
2103
2153
|
// Generate auth credentials (like CodeNomad does)
|
|
2104
2154
|
const opencodeUsername = "lunel";
|
|
@@ -2126,13 +2176,13 @@ async function main() {
|
|
|
2126
2176
|
console.log("OpenCode ready.\n");
|
|
2127
2177
|
// Subscribe to OpenCode events
|
|
2128
2178
|
subscribeToOpenCodeEvents(client);
|
|
2129
|
-
const
|
|
2130
|
-
currentPrimaryGateway =
|
|
2131
|
-
currentBackupGateway =
|
|
2179
|
+
const session = await createSessionFromManager();
|
|
2180
|
+
currentPrimaryGateway = normalizeGatewayUrl(session.primary);
|
|
2181
|
+
currentBackupGateway = session.backup ? normalizeGatewayUrl(session.backup) : null;
|
|
2182
|
+
currentSessionPassword = session.password;
|
|
2132
2183
|
activeGatewayUrl = currentPrimaryGateway;
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
displayQR(currentPrimaryGateway, currentBackupGateway, code);
|
|
2184
|
+
currentSessionCode = session.code;
|
|
2185
|
+
displayQR(currentPrimaryGateway, currentBackupGateway, session.code);
|
|
2136
2186
|
await connectWebSocket();
|
|
2137
2187
|
}
|
|
2138
2188
|
catch (error) {
|