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.
Files changed (2) hide show
  1. package/dist/index.js +107 -55
  2. 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
- const lines = statusResult.stdout.trim().split("\n").filter(Boolean);
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 = DEV_PORTS.map((port) => {
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 discoverGateways() {
1828
- try {
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
- const data = (await response.json());
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(qrPayload, { small: true }, (qr) => {
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
- scanDevPorts().then((openPorts) => {
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 gateways = await discoverGateways();
2130
- currentPrimaryGateway = gateways.primary;
2131
- currentBackupGateway = gateways.backup;
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
- const code = await createSession(currentPrimaryGateway);
2134
- currentSessionCode = code;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",