lunel-cli 0.1.32 → 0.1.34
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 +203 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ const ignore = Ignore.default;
|
|
|
8
8
|
import * as fs from "fs/promises";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import * as os from "os";
|
|
11
|
-
import { spawn, execSync } from "child_process";
|
|
11
|
+
import { spawn, execSync, execFileSync } from "child_process";
|
|
12
12
|
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";
|
|
@@ -17,6 +17,9 @@ const CLI_ARGS = process.argv.slice(2);
|
|
|
17
17
|
import { createRequire } from "module";
|
|
18
18
|
const __require = createRequire(import.meta.url);
|
|
19
19
|
const VERSION = __require("../package.json").version;
|
|
20
|
+
const PTY_RELEASE_INFO_URL = process.env.LUNEL_PTY_INFO_URL ||
|
|
21
|
+
"https://raw.githubusercontent.com/ssbharambe-m/pty-releases/refs/heads/main/info.json";
|
|
22
|
+
const VERBOSE_AI_LOGS = process.env.LUNEL_DEBUG_AI === "1";
|
|
20
23
|
// Root directory - sandbox all file operations to this
|
|
21
24
|
const ROOT_DIR = process.cwd();
|
|
22
25
|
// Terminal sessions (managed by Rust PTY binary)
|
|
@@ -44,6 +47,13 @@ const PORT_SYNC_INTERVAL_MS = 30_000;
|
|
|
44
47
|
let portSyncTimer = null;
|
|
45
48
|
let portScanInFlight = false;
|
|
46
49
|
let lastDiscoveredPorts = [];
|
|
50
|
+
function redactSensitive(input) {
|
|
51
|
+
const text = typeof input === "string" ? input : JSON.stringify(input);
|
|
52
|
+
return text
|
|
53
|
+
.replace(/([A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,})/g, "[redacted_jwt]")
|
|
54
|
+
.replace(/(password|token|authorization|resumeToken|x-manager-password)\s*[:=]\s*["']?[^"',\s}]+/gi, "$1=[redacted]")
|
|
55
|
+
.replace(/[A-Za-z0-9+/=_-]{40,}/g, "[redacted_secret]");
|
|
56
|
+
}
|
|
47
57
|
// Popular development server ports to scan on connect
|
|
48
58
|
const DEV_PORTS = [
|
|
49
59
|
1234, // Parcel
|
|
@@ -595,14 +605,157 @@ async function handleGitDiscard(payload) {
|
|
|
595
605
|
// Terminal Handlers (delegates to Rust PTY binary)
|
|
596
606
|
// ============================================================================
|
|
597
607
|
let dataChannel = null;
|
|
608
|
+
let ensurePtyBinaryPromise = null;
|
|
609
|
+
function normalizeJsonWithTrailingCommas(text) {
|
|
610
|
+
return text.replace(/,\s*([}\]])/g, "$1");
|
|
611
|
+
}
|
|
612
|
+
function compareSemver(a, b) {
|
|
613
|
+
const aParts = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
614
|
+
const bParts = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
615
|
+
const max = Math.max(aParts.length, bParts.length);
|
|
616
|
+
for (let i = 0; i < max; i++) {
|
|
617
|
+
const left = aParts[i] ?? 0;
|
|
618
|
+
const right = bParts[i] ?? 0;
|
|
619
|
+
if (left !== right)
|
|
620
|
+
return left > right ? 1 : -1;
|
|
621
|
+
}
|
|
622
|
+
return 0;
|
|
623
|
+
}
|
|
624
|
+
function getLunelConfigDir() {
|
|
625
|
+
const platform = os.platform();
|
|
626
|
+
if (platform === "win32") {
|
|
627
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
628
|
+
return path.join(appData, "lunel");
|
|
629
|
+
}
|
|
630
|
+
if (platform === "darwin") {
|
|
631
|
+
return path.join(os.homedir(), "Library", "Application Support", "lunel");
|
|
632
|
+
}
|
|
633
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
634
|
+
return path.join(xdg, "lunel");
|
|
635
|
+
}
|
|
598
636
|
function getPtyBinaryPath() {
|
|
599
|
-
|
|
600
|
-
return path.join(
|
|
637
|
+
const binName = os.platform() === "win32" ? "lunel-pty.exe" : "lunel-pty";
|
|
638
|
+
return path.join(getLunelConfigDir(), "pty-releases", binName);
|
|
639
|
+
}
|
|
640
|
+
function getPtyPlatformKey() {
|
|
641
|
+
const platform = os.platform();
|
|
642
|
+
if (platform === "win32")
|
|
643
|
+
return "windows";
|
|
644
|
+
if (platform === "linux")
|
|
645
|
+
return "linux";
|
|
646
|
+
if (platform === "darwin")
|
|
647
|
+
return "macos";
|
|
648
|
+
throw new Error(`Unsupported platform for PTY: ${platform}`);
|
|
649
|
+
}
|
|
650
|
+
function getPtyArchKey() {
|
|
651
|
+
const arch = os.arch();
|
|
652
|
+
if (arch === "arm64" || arch === "arm")
|
|
653
|
+
return "arm";
|
|
654
|
+
if (arch === "x64" || arch === "ia32")
|
|
655
|
+
return "x86";
|
|
656
|
+
throw new Error(`Unsupported architecture for PTY: ${arch}`);
|
|
657
|
+
}
|
|
658
|
+
async function fetchPtyReleaseInfo() {
|
|
659
|
+
const response = await fetch(PTY_RELEASE_INFO_URL);
|
|
660
|
+
if (!response.ok) {
|
|
661
|
+
throw new Error(`Failed to fetch PTY release info (${response.status})`);
|
|
662
|
+
}
|
|
663
|
+
const raw = await response.text();
|
|
664
|
+
const parsed = JSON.parse(raw);
|
|
665
|
+
if (!parsed?.version) {
|
|
666
|
+
throw new Error("Invalid PTY release info: missing version");
|
|
667
|
+
}
|
|
668
|
+
return parsed;
|
|
669
|
+
}
|
|
670
|
+
function readInstalledPtyVersion(binPath) {
|
|
671
|
+
try {
|
|
672
|
+
const output = execFileSync(binPath, ["--version"], {
|
|
673
|
+
encoding: "utf8",
|
|
674
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
675
|
+
});
|
|
676
|
+
const version = output.trim();
|
|
677
|
+
return version || null;
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async function downloadPtyBinary(url, destination) {
|
|
684
|
+
const tempPath = `${destination}.download`;
|
|
685
|
+
console.log("[pty] Downloading PTY [downloading...]");
|
|
686
|
+
const response = await fetch(url);
|
|
687
|
+
if (!response.ok) {
|
|
688
|
+
throw new Error(`Failed to download PTY binary (${response.status})`);
|
|
689
|
+
}
|
|
690
|
+
if (!response.body) {
|
|
691
|
+
throw new Error("PTY download response had no body");
|
|
692
|
+
}
|
|
693
|
+
const reader = response.body.getReader();
|
|
694
|
+
const chunks = [];
|
|
695
|
+
let totalBytes = 0;
|
|
696
|
+
while (true) {
|
|
697
|
+
const { done, value } = await reader.read();
|
|
698
|
+
if (done)
|
|
699
|
+
break;
|
|
700
|
+
if (value) {
|
|
701
|
+
chunks.push(value);
|
|
702
|
+
totalBytes += value.byteLength;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const binary = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
|
706
|
+
await fs.writeFile(tempPath, binary);
|
|
707
|
+
if (os.platform() !== "win32") {
|
|
708
|
+
await fs.chmod(tempPath, 0o755);
|
|
709
|
+
}
|
|
710
|
+
await fs.rename(tempPath, destination);
|
|
711
|
+
console.log(`[pty] Downloaded PTY (${Math.max(1, Math.round(totalBytes / 1024))} KB)`);
|
|
712
|
+
}
|
|
713
|
+
async function ensurePtyBinaryReady() {
|
|
714
|
+
if (ensurePtyBinaryPromise)
|
|
715
|
+
return ensurePtyBinaryPromise;
|
|
716
|
+
ensurePtyBinaryPromise = (async () => {
|
|
717
|
+
const binPath = getPtyBinaryPath();
|
|
718
|
+
await fs.mkdir(path.dirname(binPath), { recursive: true });
|
|
719
|
+
const releaseInfo = await fetchPtyReleaseInfo();
|
|
720
|
+
const platformKey = getPtyPlatformKey();
|
|
721
|
+
const archKey = getPtyArchKey();
|
|
722
|
+
const downloadUrl = releaseInfo[platformKey]?.[archKey] || null;
|
|
723
|
+
if (!downloadUrl) {
|
|
724
|
+
throw new Error(`PTY binary is not available for ${platformKey}/${archKey} in release ${releaseInfo.version}`);
|
|
725
|
+
}
|
|
726
|
+
let hasBinary = true;
|
|
727
|
+
try {
|
|
728
|
+
await fs.access(binPath);
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
hasBinary = false;
|
|
732
|
+
}
|
|
733
|
+
const installedVersion = hasBinary ? readInstalledPtyVersion(binPath) : null;
|
|
734
|
+
const shouldDownload = !hasBinary ||
|
|
735
|
+
!installedVersion ||
|
|
736
|
+
compareSemver(installedVersion, releaseInfo.version) < 0;
|
|
737
|
+
if (!shouldDownload)
|
|
738
|
+
return binPath;
|
|
739
|
+
if (!hasBinary) {
|
|
740
|
+
console.log(`[pty] PTY missing. Installing ${releaseInfo.version}...`);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
console.log(`[pty] PTY outdated (${installedVersion} -> ${releaseInfo.version}). Updating...`);
|
|
744
|
+
}
|
|
745
|
+
await downloadPtyBinary(downloadUrl, binPath);
|
|
746
|
+
return binPath;
|
|
747
|
+
})();
|
|
748
|
+
try {
|
|
749
|
+
return await ensurePtyBinaryPromise;
|
|
750
|
+
}
|
|
751
|
+
finally {
|
|
752
|
+
ensurePtyBinaryPromise = null;
|
|
753
|
+
}
|
|
601
754
|
}
|
|
602
|
-
function ensurePtyProcess() {
|
|
755
|
+
async function ensurePtyProcess() {
|
|
603
756
|
if (ptyProcess && ptyProcess.exitCode === null)
|
|
604
757
|
return;
|
|
605
|
-
const binPath =
|
|
758
|
+
const binPath = await ensurePtyBinaryReady();
|
|
606
759
|
ptyProcess = spawn(binPath, [], {
|
|
607
760
|
cwd: ROOT_DIR,
|
|
608
761
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -690,7 +843,7 @@ function sendToPty(cmd) {
|
|
|
690
843
|
ptyProcess.stdin.write(JSON.stringify(cmd) + "\n");
|
|
691
844
|
}
|
|
692
845
|
async function handleTerminalSpawn(payload) {
|
|
693
|
-
ensurePtyProcess();
|
|
846
|
+
await ensurePtyProcess();
|
|
694
847
|
const shell = payload.shell || process.env.SHELL || "/bin/sh";
|
|
695
848
|
const cols = payload.cols || 80;
|
|
696
849
|
const rows = payload.rows || 24;
|
|
@@ -1230,30 +1383,36 @@ function requireData(response, label) {
|
|
|
1230
1383
|
const errMsg = response.error
|
|
1231
1384
|
? (typeof response.error === "string" ? response.error : JSON.stringify(response.error))
|
|
1232
1385
|
: `${label} returned no data`;
|
|
1233
|
-
console.error(`[ai] ${label} failed:`, errMsg, "raw response:", JSON.stringify(response).substring(0, 500));
|
|
1386
|
+
console.error(`[ai] ${label} failed:`, redactSensitive(errMsg), "raw response:", redactSensitive(JSON.stringify(response).substring(0, 500)));
|
|
1234
1387
|
throw new Error(errMsg);
|
|
1235
1388
|
}
|
|
1236
1389
|
return response.data;
|
|
1237
1390
|
}
|
|
1238
1391
|
async function handleAiCreateSession(payload) {
|
|
1239
1392
|
const title = payload.title || undefined;
|
|
1240
|
-
|
|
1393
|
+
if (VERBOSE_AI_LOGS)
|
|
1394
|
+
console.log("[ai] createSession called");
|
|
1241
1395
|
try {
|
|
1242
1396
|
const response = await opencodeClient.session.create({ body: { title } });
|
|
1243
|
-
|
|
1397
|
+
if (VERBOSE_AI_LOGS) {
|
|
1398
|
+
console.log("[ai] createSession response ok:", !!response.data, "error:", response.error ? redactSensitive(JSON.stringify(response.error).substring(0, 200)) : "none");
|
|
1399
|
+
}
|
|
1244
1400
|
return { session: requireData(response, "session.create") };
|
|
1245
1401
|
}
|
|
1246
1402
|
catch (err) {
|
|
1247
|
-
console.error("[ai] createSession exception:", err.message
|
|
1403
|
+
console.error("[ai] createSession exception:", redactSensitive(err.message));
|
|
1248
1404
|
throw err;
|
|
1249
1405
|
}
|
|
1250
1406
|
}
|
|
1251
1407
|
async function handleAiListSessions() {
|
|
1252
|
-
|
|
1408
|
+
if (VERBOSE_AI_LOGS)
|
|
1409
|
+
console.log("[ai] listSessions called");
|
|
1253
1410
|
try {
|
|
1254
1411
|
const response = await opencodeClient.session.list();
|
|
1255
1412
|
const data = requireData(response, "session.list");
|
|
1256
|
-
|
|
1413
|
+
if (VERBOSE_AI_LOGS) {
|
|
1414
|
+
console.log("[ai] listSessions returned", Array.isArray(data) ? data.length : typeof data, "sessions");
|
|
1415
|
+
}
|
|
1257
1416
|
return { sessions: data };
|
|
1258
1417
|
}
|
|
1259
1418
|
catch (err) {
|
|
@@ -1275,7 +1434,8 @@ async function handleAiDeleteSession(payload) {
|
|
|
1275
1434
|
}
|
|
1276
1435
|
async function handleAiGetMessages(payload) {
|
|
1277
1436
|
const id = payload.id;
|
|
1278
|
-
|
|
1437
|
+
if (VERBOSE_AI_LOGS)
|
|
1438
|
+
console.log("[ai] getMessages called");
|
|
1279
1439
|
try {
|
|
1280
1440
|
const response = await opencodeClient.session.messages({ path: { id } });
|
|
1281
1441
|
const raw = requireData(response, "session.messages");
|
|
@@ -1287,7 +1447,8 @@ async function handleAiGetMessages(payload) {
|
|
|
1287
1447
|
parts: m.parts || [],
|
|
1288
1448
|
time: m.info.time,
|
|
1289
1449
|
}));
|
|
1290
|
-
|
|
1450
|
+
if (VERBOSE_AI_LOGS)
|
|
1451
|
+
console.log("[ai] getMessages returned", messages.length, "messages");
|
|
1291
1452
|
return { messages };
|
|
1292
1453
|
}
|
|
1293
1454
|
catch (err) {
|
|
@@ -1300,7 +1461,14 @@ async function handleAiPrompt(payload) {
|
|
|
1300
1461
|
const text = payload.text;
|
|
1301
1462
|
const model = payload.model;
|
|
1302
1463
|
const agent = payload.agent;
|
|
1303
|
-
|
|
1464
|
+
if (VERBOSE_AI_LOGS) {
|
|
1465
|
+
console.log("[ai] prompt called", {
|
|
1466
|
+
hasSessionId: Boolean(sessionId),
|
|
1467
|
+
model: redactSensitive(JSON.stringify(model || {})),
|
|
1468
|
+
hasAgent: Boolean(agent),
|
|
1469
|
+
textLength: typeof text === "string" ? text.length : 0,
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1304
1472
|
// Fire and forget — results stream via SSE events forwarded on data channel
|
|
1305
1473
|
opencodeClient.session.prompt({
|
|
1306
1474
|
path: { id: sessionId },
|
|
@@ -1332,11 +1500,14 @@ async function handleAiAbort(payload) {
|
|
|
1332
1500
|
return {};
|
|
1333
1501
|
}
|
|
1334
1502
|
async function handleAiAgents() {
|
|
1335
|
-
|
|
1503
|
+
if (VERBOSE_AI_LOGS)
|
|
1504
|
+
console.log("[ai] getAgents called");
|
|
1336
1505
|
try {
|
|
1337
1506
|
const response = await opencodeClient.app.agents();
|
|
1338
1507
|
const data = requireData(response, "app.agents");
|
|
1339
|
-
|
|
1508
|
+
if (VERBOSE_AI_LOGS) {
|
|
1509
|
+
console.log("[ai] getAgents returned:", redactSensitive(JSON.stringify(data).substring(0, 300)));
|
|
1510
|
+
}
|
|
1340
1511
|
return { agents: data };
|
|
1341
1512
|
}
|
|
1342
1513
|
catch (err) {
|
|
@@ -1345,11 +1516,14 @@ async function handleAiAgents() {
|
|
|
1345
1516
|
}
|
|
1346
1517
|
}
|
|
1347
1518
|
async function handleAiProviders() {
|
|
1348
|
-
|
|
1519
|
+
if (VERBOSE_AI_LOGS)
|
|
1520
|
+
console.log("[ai] getProviders called");
|
|
1349
1521
|
try {
|
|
1350
1522
|
const response = await opencodeClient.config.providers();
|
|
1351
1523
|
const data = requireData(response, "config.providers");
|
|
1352
|
-
|
|
1524
|
+
if (VERBOSE_AI_LOGS) {
|
|
1525
|
+
console.log("[ai] getProviders returned", data.providers?.length, "providers, defaults:", redactSensitive(JSON.stringify(data.default)));
|
|
1526
|
+
}
|
|
1353
1527
|
return { providers: data.providers, default: data.default };
|
|
1354
1528
|
}
|
|
1355
1529
|
catch (err) {
|
|
@@ -1427,7 +1601,7 @@ async function subscribeToOpenCodeEvents(client) {
|
|
|
1427
1601
|
? parsed.payload
|
|
1428
1602
|
: parsed;
|
|
1429
1603
|
if (!base || typeof base.type !== "string") {
|
|
1430
|
-
console.warn("[sse] Dropped malformed event:", JSON.stringify(parsed).substring(0, 200));
|
|
1604
|
+
console.warn("[sse] Dropped malformed event:", redactSensitive(JSON.stringify(parsed).substring(0, 200)));
|
|
1431
1605
|
continue;
|
|
1432
1606
|
}
|
|
1433
1607
|
console.log("[sse]", base.type);
|
|
@@ -1639,7 +1813,7 @@ async function processMessage(message) {
|
|
|
1639
1813
|
}
|
|
1640
1814
|
// Validate required fields
|
|
1641
1815
|
if (!ns || !action) {
|
|
1642
|
-
console.warn("[router] Ignoring message with missing ns/action:", JSON.stringify(message).substring(0, 300));
|
|
1816
|
+
console.warn("[router] Ignoring message with missing ns/action:", redactSensitive(JSON.stringify(message).substring(0, 300)));
|
|
1643
1817
|
return {
|
|
1644
1818
|
v: 1,
|
|
1645
1819
|
id,
|
|
@@ -2053,7 +2227,11 @@ async function connectWebSocket() {
|
|
|
2053
2227
|
return;
|
|
2054
2228
|
}
|
|
2055
2229
|
if (message.type === "close_connection") {
|
|
2056
|
-
|
|
2230
|
+
const reason = message.reason || "expired";
|
|
2231
|
+
console.log(`[session] closed by gateway: ${reason}`);
|
|
2232
|
+
if (reason === "session ended from app") {
|
|
2233
|
+
console.log("[session] Run `npx lunel-cli` again and scan the new QR code to reconnect.");
|
|
2234
|
+
}
|
|
2057
2235
|
gracefulShutdown();
|
|
2058
2236
|
return;
|
|
2059
2237
|
}
|
|
@@ -2152,6 +2330,9 @@ async function main() {
|
|
|
2152
2330
|
console.log(`Extra ports enabled: ${EXTRA_PORTS.join(", ")}`);
|
|
2153
2331
|
}
|
|
2154
2332
|
try {
|
|
2333
|
+
console.log("Checking PTY runtime...");
|
|
2334
|
+
await ensurePtyBinaryReady();
|
|
2335
|
+
console.log("PTY runtime ready.\n");
|
|
2155
2336
|
// Generate auth credentials (like CodeNomad does)
|
|
2156
2337
|
const opencodeUsername = "lunel";
|
|
2157
2338
|
const opencodePassword = crypto.randomBytes(32).toString("base64url");
|