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.
Files changed (2) hide show
  1. package/dist/index.js +116 -55
  2. 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 Map();
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 handleTerminalSpawn(payload) {
541
- const shell = payload.shell || process.env.SHELL || "/bin/sh";
542
- const cols = payload.cols || 80;
543
- const rows = payload.rows || 24;
544
- const terminalId = `term-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
545
- const proc = spawn(shell, [], {
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
- terminals.set(terminalId, proc);
556
- // Stream output to app via data channel
557
- const sendOutput = (data) => {
558
- if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
559
- const msg = {
560
- v: 1,
561
- id: `evt-${Date.now()}`,
562
- ns: "terminal",
563
- action: "output",
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
- proc.stdout?.on("data", sendOutput);
570
- proc.stderr?.on("data", sendOutput);
571
- proc.on("close", (code) => {
572
- terminals.delete(terminalId);
573
- if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
574
- const msg = {
575
- v: 1,
576
- id: `evt-${Date.now()}`,
577
- ns: "terminal",
578
- action: "exit",
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
- const proc = terminals.get(terminalId);
594
- if (!proc)
657
+ if (!terminals.has(terminalId))
595
658
  throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
596
- proc.stdin?.write(data);
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
- const proc = terminals.get(terminalId);
606
- if (!proc)
668
+ if (!terminals.has(terminalId))
607
669
  throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
608
- // Note: For proper PTY resize, you'd need node-pty
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
- const proc = terminals.get(terminalId);
617
- if (!proc)
677
+ if (!terminals.has(terminalId))
618
678
  throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
619
- proc.kill();
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
- for (const [id, proc] of terminals) {
1757
- proc.kill();
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.16",
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",