lunel-cli 0.1.30 → 0.1.32
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 +107 -55
- 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
|
// ============================================================================
|
|
@@ -353,7 +396,9 @@ async function handleGitStatus() {
|
|
|
353
396
|
const branch = branchResult.stdout.trim();
|
|
354
397
|
// Get status
|
|
355
398
|
const statusResult = await runGit(["status", "--porcelain", "-uall"]);
|
|
356
|
-
|
|
399
|
+
// Preserve leading whitespace in each porcelain line.
|
|
400
|
+
// Example: " M file" (unstaged-only) starts with a space that is semantically important.
|
|
401
|
+
const lines = statusResult.stdout.split(/\r?\n/).filter((line) => line.length > 0);
|
|
357
402
|
const staged = [];
|
|
358
403
|
const unstaged = [];
|
|
359
404
|
const untracked = [];
|
|
@@ -1413,7 +1458,7 @@ async function subscribeToOpenCodeEvents(client) {
|
|
|
1413
1458
|
// ============================================================================
|
|
1414
1459
|
async function scanDevPorts() {
|
|
1415
1460
|
const openPorts = [];
|
|
1416
|
-
const checks =
|
|
1461
|
+
const checks = SCAN_PORTS.map((port) => {
|
|
1417
1462
|
return new Promise((resolve) => {
|
|
1418
1463
|
const socket = createConnection({ port, host: "127.0.0.1" });
|
|
1419
1464
|
socket.setTimeout(200);
|
|
@@ -1434,6 +1479,48 @@ async function scanDevPorts() {
|
|
|
1434
1479
|
await Promise.all(checks);
|
|
1435
1480
|
return openPorts.sort((a, b) => a - b);
|
|
1436
1481
|
}
|
|
1482
|
+
async function publishDiscoveredPorts(ws, force = false) {
|
|
1483
|
+
if (portScanInFlight)
|
|
1484
|
+
return;
|
|
1485
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
1486
|
+
return;
|
|
1487
|
+
portScanInFlight = true;
|
|
1488
|
+
try {
|
|
1489
|
+
const openPorts = await scanDevPorts();
|
|
1490
|
+
if (!force && samePortSet(openPorts, lastDiscoveredPorts)) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
lastDiscoveredPorts = openPorts;
|
|
1494
|
+
ws.send(JSON.stringify({
|
|
1495
|
+
v: 1,
|
|
1496
|
+
id: `evt-${Date.now()}`,
|
|
1497
|
+
ns: "proxy",
|
|
1498
|
+
action: "ports_discovered",
|
|
1499
|
+
payload: { ports: openPorts },
|
|
1500
|
+
}));
|
|
1501
|
+
console.log(`[proxy] ports updated (${openPorts.length}): ${openPorts.join(", ") || "-"}`);
|
|
1502
|
+
}
|
|
1503
|
+
catch (err) {
|
|
1504
|
+
console.error("Port scan failed:", err);
|
|
1505
|
+
}
|
|
1506
|
+
finally {
|
|
1507
|
+
portScanInFlight = false;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function stopPortSync() {
|
|
1511
|
+
if (portSyncTimer) {
|
|
1512
|
+
clearInterval(portSyncTimer);
|
|
1513
|
+
portSyncTimer = null;
|
|
1514
|
+
}
|
|
1515
|
+
portScanInFlight = false;
|
|
1516
|
+
}
|
|
1517
|
+
function startPortSync(ws) {
|
|
1518
|
+
stopPortSync();
|
|
1519
|
+
void publishDiscoveredPorts(ws, true);
|
|
1520
|
+
portSyncTimer = setInterval(() => {
|
|
1521
|
+
void publishDiscoveredPorts(ws, false);
|
|
1522
|
+
}, PORT_SYNC_INTERVAL_MS);
|
|
1523
|
+
}
|
|
1437
1524
|
async function handleProxyConnect(payload) {
|
|
1438
1525
|
const tunnelId = payload.tunnelId;
|
|
1439
1526
|
const port = payload.port;
|
|
@@ -1824,45 +1911,19 @@ function normalizeGatewayUrl(input) {
|
|
|
1824
1911
|
return input.replace(/\/+$/, "");
|
|
1825
1912
|
return `https://${input}`.replace(/\/+$/, "");
|
|
1826
1913
|
}
|
|
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`, {
|
|
1914
|
+
async function createSessionFromManager() {
|
|
1915
|
+
const response = await fetch(`${MANAGER_URL}/v1/session`, {
|
|
1851
1916
|
method: "POST",
|
|
1852
1917
|
headers: { "Content-Type": "application/json" },
|
|
1853
1918
|
});
|
|
1854
1919
|
if (!response.ok) {
|
|
1855
|
-
throw new Error(`Failed to create session: ${response.status}`);
|
|
1920
|
+
throw new Error(`Failed to create session from manager: ${response.status}`);
|
|
1856
1921
|
}
|
|
1857
|
-
|
|
1858
|
-
return data.code;
|
|
1922
|
+
return (await response.json());
|
|
1859
1923
|
}
|
|
1860
1924
|
function displayQR(primaryGateway, backupGateway, code) {
|
|
1861
|
-
const qrPayload = backupGateway
|
|
1862
|
-
? `${primaryGateway},${backupGateway},${code}`
|
|
1863
|
-
: `${primaryGateway},${code}`;
|
|
1864
1925
|
console.log("\n");
|
|
1865
|
-
qrcode.generate(
|
|
1926
|
+
qrcode.generate(code, { small: true }, (qr) => {
|
|
1866
1927
|
console.log(qr);
|
|
1867
1928
|
console.log(`\n Session code: ${code}\n`);
|
|
1868
1929
|
console.log(` Primary gateway: ${primaryGateway}`);
|
|
@@ -1894,6 +1955,7 @@ function buildWsUrl(gatewayUrl, role, channel) {
|
|
|
1894
1955
|
function gracefulShutdown() {
|
|
1895
1956
|
shuttingDown = true;
|
|
1896
1957
|
console.log("\nShutting down...");
|
|
1958
|
+
stopPortSync();
|
|
1897
1959
|
if (ptyProcess) {
|
|
1898
1960
|
ptyProcess.kill();
|
|
1899
1961
|
ptyProcess = null;
|
|
@@ -1951,6 +2013,7 @@ async function connectWebSocket() {
|
|
|
1951
2013
|
return;
|
|
1952
2014
|
closeHandled = true;
|
|
1953
2015
|
closeReason = reason;
|
|
2016
|
+
stopPortSync();
|
|
1954
2017
|
cleanupAllTunnels();
|
|
1955
2018
|
setTimeout(() => {
|
|
1956
2019
|
if (shuttingDown)
|
|
@@ -1975,26 +2038,12 @@ async function connectWebSocket() {
|
|
|
1975
2038
|
}
|
|
1976
2039
|
if (message.type === "peer_connected") {
|
|
1977
2040
|
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
|
-
});
|
|
2041
|
+
startPortSync(controlWs);
|
|
1994
2042
|
return;
|
|
1995
2043
|
}
|
|
1996
2044
|
if (message.type === "peer_disconnected") {
|
|
1997
2045
|
console.log("App disconnected. Waiting for reconnect window.\n");
|
|
2046
|
+
stopPortSync();
|
|
1998
2047
|
return;
|
|
1999
2048
|
}
|
|
2000
2049
|
if (message.type === "app_disconnected") {
|
|
@@ -2099,6 +2148,9 @@ async function handleConnectionDrop(reason) {
|
|
|
2099
2148
|
async function main() {
|
|
2100
2149
|
console.log("Lunel CLI v" + VERSION);
|
|
2101
2150
|
console.log("=".repeat(20) + "\n");
|
|
2151
|
+
if (EXTRA_PORTS.length > 0) {
|
|
2152
|
+
console.log(`Extra ports enabled: ${EXTRA_PORTS.join(", ")}`);
|
|
2153
|
+
}
|
|
2102
2154
|
try {
|
|
2103
2155
|
// Generate auth credentials (like CodeNomad does)
|
|
2104
2156
|
const opencodeUsername = "lunel";
|
|
@@ -2126,13 +2178,13 @@ async function main() {
|
|
|
2126
2178
|
console.log("OpenCode ready.\n");
|
|
2127
2179
|
// Subscribe to OpenCode events
|
|
2128
2180
|
subscribeToOpenCodeEvents(client);
|
|
2129
|
-
const
|
|
2130
|
-
currentPrimaryGateway =
|
|
2131
|
-
currentBackupGateway =
|
|
2181
|
+
const session = await createSessionFromManager();
|
|
2182
|
+
currentPrimaryGateway = normalizeGatewayUrl(session.primary);
|
|
2183
|
+
currentBackupGateway = session.backup ? normalizeGatewayUrl(session.backup) : null;
|
|
2184
|
+
currentSessionPassword = session.password;
|
|
2132
2185
|
activeGatewayUrl = currentPrimaryGateway;
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
displayQR(currentPrimaryGateway, currentBackupGateway, code);
|
|
2186
|
+
currentSessionCode = session.code;
|
|
2187
|
+
displayQR(currentPrimaryGateway, currentBackupGateway, session.code);
|
|
2136
2188
|
await connectWebSocket();
|
|
2137
2189
|
}
|
|
2138
2190
|
catch (error) {
|