lunel-cli 0.1.16 → 0.1.18
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 +116 -55
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -9,14 +9,18 @@ import * as path from "path";
|
|
|
9
9
|
import * as os from "os";
|
|
10
10
|
import { spawn, execSync } from "child_process";
|
|
11
11
|
import { createServer, createConnection } from "net";
|
|
12
|
+
import { createInterface } from "readline";
|
|
12
13
|
const PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
|
|
13
14
|
import { createRequire } from "module";
|
|
14
15
|
const __require = createRequire(import.meta.url);
|
|
15
16
|
const VERSION = __require("../package.json").version;
|
|
16
17
|
// Root directory - sandbox all file operations to this
|
|
17
18
|
const ROOT_DIR = process.cwd();
|
|
18
|
-
// Terminal sessions
|
|
19
|
-
const terminals = new
|
|
19
|
+
// Terminal sessions (managed by Rust PTY binary)
|
|
20
|
+
const terminals = new Set();
|
|
21
|
+
// PTY binary process
|
|
22
|
+
let ptyProcess = null;
|
|
23
|
+
const ptyPendingSpawns = new Map();
|
|
20
24
|
const processes = new Map();
|
|
21
25
|
const processOutputBuffers = new Map();
|
|
22
26
|
// CPU usage tracking
|
|
@@ -534,53 +538,113 @@ async function handleGitDiscard(payload) {
|
|
|
534
538
|
return {};
|
|
535
539
|
}
|
|
536
540
|
// ============================================================================
|
|
537
|
-
// Terminal Handlers
|
|
541
|
+
// Terminal Handlers (delegates to Rust PTY binary)
|
|
538
542
|
// ============================================================================
|
|
539
543
|
let dataChannel = null;
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
544
|
+
function getPtyBinaryPath() {
|
|
545
|
+
// TODO: Switch to GitHub releases download for production
|
|
546
|
+
return path.join(os.homedir(), "lunel-pty");
|
|
547
|
+
}
|
|
548
|
+
function ensurePtyProcess() {
|
|
549
|
+
if (ptyProcess && ptyProcess.exitCode === null)
|
|
550
|
+
return;
|
|
551
|
+
const binPath = getPtyBinaryPath();
|
|
552
|
+
ptyProcess = spawn(binPath, [], {
|
|
546
553
|
cwd: ROOT_DIR,
|
|
547
|
-
env: {
|
|
548
|
-
...process.env,
|
|
549
|
-
TERM: "xterm-256color",
|
|
550
|
-
COLUMNS: cols.toString(),
|
|
551
|
-
LINES: rows.toString(),
|
|
552
|
-
},
|
|
553
554
|
stdio: ["pipe", "pipe", "pipe"],
|
|
554
555
|
});
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
payload: { terminalId, data: data.toString() },
|
|
565
|
-
};
|
|
566
|
-
dataChannel.send(JSON.stringify(msg));
|
|
556
|
+
ptyProcess.stderr?.on("data", (data) => {
|
|
557
|
+
console.error("[pty]", data.toString().trim());
|
|
558
|
+
});
|
|
559
|
+
ptyProcess.on("exit", (code) => {
|
|
560
|
+
console.log(`[pty] PTY process exited with code ${code}`);
|
|
561
|
+
ptyProcess = null;
|
|
562
|
+
// Reject all pending spawns
|
|
563
|
+
for (const [id, pending] of ptyPendingSpawns) {
|
|
564
|
+
pending.reject(new Error("PTY process exited"));
|
|
567
565
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
payload: { terminalId, code },
|
|
580
|
-
};
|
|
581
|
-
dataChannel.send(JSON.stringify(msg));
|
|
566
|
+
ptyPendingSpawns.clear();
|
|
567
|
+
});
|
|
568
|
+
// Parse stdout line by line for events from the Rust binary
|
|
569
|
+
const rl = createInterface({ input: ptyProcess.stdout });
|
|
570
|
+
rl.on("line", (line) => {
|
|
571
|
+
let event;
|
|
572
|
+
try {
|
|
573
|
+
event = JSON.parse(line);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return;
|
|
582
577
|
}
|
|
578
|
+
if (event.event === "spawned") {
|
|
579
|
+
const pending = ptyPendingSpawns.get(event.id);
|
|
580
|
+
if (pending) {
|
|
581
|
+
pending.resolve();
|
|
582
|
+
ptyPendingSpawns.delete(event.id);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else if (event.event === "state") {
|
|
586
|
+
// Forward screen state to app via data channel
|
|
587
|
+
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
588
|
+
const msg = {
|
|
589
|
+
v: 1,
|
|
590
|
+
id: `evt-${Date.now()}`,
|
|
591
|
+
ns: "terminal",
|
|
592
|
+
action: "state",
|
|
593
|
+
payload: {
|
|
594
|
+
terminalId: event.id,
|
|
595
|
+
cells: event.cells,
|
|
596
|
+
cursorX: event.cursorX,
|
|
597
|
+
cursorY: event.cursorY,
|
|
598
|
+
cols: event.cols,
|
|
599
|
+
rows: event.rows,
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
dataChannel.send(JSON.stringify(msg));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else if (event.event === "exit") {
|
|
606
|
+
terminals.delete(event.id);
|
|
607
|
+
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
608
|
+
const msg = {
|
|
609
|
+
v: 1,
|
|
610
|
+
id: `evt-${Date.now()}`,
|
|
611
|
+
ns: "terminal",
|
|
612
|
+
action: "exit",
|
|
613
|
+
payload: { terminalId: event.id, code: event.code },
|
|
614
|
+
};
|
|
615
|
+
dataChannel.send(JSON.stringify(msg));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else if (event.event === "error") {
|
|
619
|
+
console.error(`[pty] Error for ${event.id}: ${event.message}`);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function sendToPty(cmd) {
|
|
624
|
+
if (!ptyProcess || !ptyProcess.stdin) {
|
|
625
|
+
throw Object.assign(new Error("PTY process not running"), { code: "ENOPTY" });
|
|
626
|
+
}
|
|
627
|
+
ptyProcess.stdin.write(JSON.stringify(cmd) + "\n");
|
|
628
|
+
}
|
|
629
|
+
async function handleTerminalSpawn(payload) {
|
|
630
|
+
ensurePtyProcess();
|
|
631
|
+
const shell = payload.shell || process.env.SHELL || "/bin/sh";
|
|
632
|
+
const cols = payload.cols || 80;
|
|
633
|
+
const rows = payload.rows || 24;
|
|
634
|
+
const terminalId = `term-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
635
|
+
// Wait for the Rust binary to confirm spawn
|
|
636
|
+
const spawnPromise = new Promise((resolve, reject) => {
|
|
637
|
+
ptyPendingSpawns.set(terminalId, { resolve, reject });
|
|
638
|
+
setTimeout(() => {
|
|
639
|
+
if (ptyPendingSpawns.has(terminalId)) {
|
|
640
|
+
ptyPendingSpawns.delete(terminalId);
|
|
641
|
+
reject(new Error("Spawn timed out"));
|
|
642
|
+
}
|
|
643
|
+
}, 10000);
|
|
583
644
|
});
|
|
645
|
+
sendToPty({ cmd: "spawn", id: terminalId, shell, cols, rows });
|
|
646
|
+
await spawnPromise;
|
|
647
|
+
terminals.add(terminalId);
|
|
584
648
|
return { terminalId };
|
|
585
649
|
}
|
|
586
650
|
function handleTerminalWrite(payload) {
|
|
@@ -590,10 +654,9 @@ function handleTerminalWrite(payload) {
|
|
|
590
654
|
throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
|
|
591
655
|
if (typeof data !== "string")
|
|
592
656
|
throw Object.assign(new Error("data is required"), { code: "EINVAL" });
|
|
593
|
-
|
|
594
|
-
if (!proc)
|
|
657
|
+
if (!terminals.has(terminalId))
|
|
595
658
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
596
|
-
|
|
659
|
+
sendToPty({ cmd: "write", id: terminalId, data });
|
|
597
660
|
return {};
|
|
598
661
|
}
|
|
599
662
|
function handleTerminalResize(payload) {
|
|
@@ -602,21 +665,18 @@ function handleTerminalResize(payload) {
|
|
|
602
665
|
const rows = payload.rows;
|
|
603
666
|
if (!terminalId)
|
|
604
667
|
throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
|
|
605
|
-
|
|
606
|
-
if (!proc)
|
|
668
|
+
if (!terminals.has(terminalId))
|
|
607
669
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
608
|
-
|
|
609
|
-
// This is a simplified version
|
|
670
|
+
sendToPty({ cmd: "resize", id: terminalId, cols, rows });
|
|
610
671
|
return {};
|
|
611
672
|
}
|
|
612
673
|
function handleTerminalKill(payload) {
|
|
613
674
|
const terminalId = payload.terminalId;
|
|
614
675
|
if (!terminalId)
|
|
615
676
|
throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
|
|
616
|
-
|
|
617
|
-
if (!proc)
|
|
677
|
+
if (!terminals.has(terminalId))
|
|
618
678
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
619
|
-
|
|
679
|
+
sendToPty({ cmd: "kill", id: terminalId });
|
|
620
680
|
terminals.delete(terminalId);
|
|
621
681
|
return {};
|
|
622
682
|
}
|
|
@@ -1457,7 +1517,7 @@ async function processMessage(message) {
|
|
|
1457
1517
|
case "terminal":
|
|
1458
1518
|
switch (action) {
|
|
1459
1519
|
case "spawn":
|
|
1460
|
-
result = handleTerminalSpawn(payload);
|
|
1520
|
+
result = await handleTerminalSpawn(payload);
|
|
1461
1521
|
break;
|
|
1462
1522
|
case "write":
|
|
1463
1523
|
result = handleTerminalWrite(payload);
|
|
@@ -1752,9 +1812,10 @@ function connectWebSocket(code) {
|
|
|
1752
1812
|
// Handle graceful shutdown
|
|
1753
1813
|
process.on("SIGINT", () => {
|
|
1754
1814
|
console.log("\nShutting down...");
|
|
1755
|
-
// Kill all terminals
|
|
1756
|
-
|
|
1757
|
-
|
|
1815
|
+
// Kill PTY process (kills all terminals)
|
|
1816
|
+
if (ptyProcess) {
|
|
1817
|
+
ptyProcess.kill();
|
|
1818
|
+
ptyProcess = null;
|
|
1758
1819
|
}
|
|
1759
1820
|
terminals.clear();
|
|
1760
1821
|
// Kill all managed processes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lunel-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"author": [
|
|
5
5
|
{
|
|
6
6
|
"name": "Soham Bharambe",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"lunel-cli": "./dist/index.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"bin"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "tsc",
|