lunel-cli 0.1.4 → 0.1.6

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 +170 -53
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -8,11 +8,11 @@ import * as path from "path";
8
8
  import * as os from "os";
9
9
  import { spawn, execSync } from "child_process";
10
10
  import { createServer } from "net";
11
+ import * as pty from "node-pty";
11
12
  const PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
12
- const VERSION = "0.1.3";
13
+ const VERSION = "0.1.4";
13
14
  // Root directory - sandbox all file operations to this
14
15
  const ROOT_DIR = process.cwd();
15
- // Terminal sessions
16
16
  const terminals = new Map();
17
17
  const processes = new Map();
18
18
  const processOutputBuffers = new Map();
@@ -481,49 +481,162 @@ async function handleGitDiscard(payload) {
481
481
  // ============================================================================
482
482
  let dataChannel = null;
483
483
  function handleTerminalSpawn(payload) {
484
- const shell = payload.shell || process.env.SHELL || "/bin/sh";
484
+ // Determine shell - prefer user's shell, fallback to common defaults
485
+ let shell = payload.shell || process.env.SHELL;
486
+ // Validate and find a working shell
487
+ const possibleShells = os.platform() === 'win32'
488
+ ? ['powershell.exe', 'cmd.exe']
489
+ : [shell, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
490
+ shell = '';
491
+ for (const candidate of possibleShells) {
492
+ if (!candidate)
493
+ continue;
494
+ try {
495
+ if (os.platform() === 'win32') {
496
+ // On Windows, just use the shell name
497
+ shell = candidate;
498
+ break;
499
+ }
500
+ else {
501
+ // On Unix, check if file exists and is executable
502
+ execSync(`test -x "${candidate}"`, { stdio: 'ignore' });
503
+ shell = candidate;
504
+ break;
505
+ }
506
+ }
507
+ catch {
508
+ // Try next shell
509
+ }
510
+ }
511
+ if (!shell) {
512
+ throw Object.assign(new Error('No valid shell found'), { code: 'ENOSHELL' });
513
+ }
485
514
  const cols = payload.cols || 80;
486
515
  const rows = payload.rows || 24;
487
516
  const terminalId = `term-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
488
- const proc = spawn(shell, [], {
489
- cwd: ROOT_DIR,
490
- env: {
491
- ...process.env,
492
- TERM: "xterm-256color",
493
- COLUMNS: cols.toString(),
494
- LINES: rows.toString(),
495
- },
496
- stdio: ["pipe", "pipe", "pipe"],
497
- });
498
- terminals.set(terminalId, proc);
499
- // Stream output to app via data channel
500
- const sendOutput = (data) => {
501
- if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
502
- const msg = {
503
- v: 1,
504
- id: `evt-${Date.now()}`,
505
- ns: "terminal",
506
- action: "output",
507
- payload: { terminalId, data: data.toString() },
508
- };
509
- dataChannel.send(JSON.stringify(msg));
517
+ // Filter out undefined values from env
518
+ const cleanEnv = {};
519
+ for (const [key, value] of Object.entries(process.env)) {
520
+ if (value !== undefined) {
521
+ cleanEnv[key] = value;
510
522
  }
511
- };
512
- proc.stdout?.on("data", sendOutput);
513
- proc.stderr?.on("data", sendOutput);
514
- proc.on("close", (code) => {
515
- terminals.delete(terminalId);
516
- if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
517
- const msg = {
518
- v: 1,
519
- id: `evt-${Date.now()}`,
520
- ns: "terminal",
521
- action: "exit",
522
- payload: { terminalId, code },
523
- };
524
- dataChannel.send(JSON.stringify(msg));
525
- }
526
- });
523
+ }
524
+ cleanEnv['TERM'] = 'xterm-256color';
525
+ // Try PTY first, fallback to regular spawn if PTY fails (e.g., Node 24 compatibility)
526
+ let ptyProcess = null;
527
+ let fallbackProc = null;
528
+ try {
529
+ ptyProcess = pty.spawn(shell, [], {
530
+ name: 'xterm-256color',
531
+ cols,
532
+ rows,
533
+ cwd: ROOT_DIR,
534
+ env: cleanEnv,
535
+ });
536
+ }
537
+ catch (ptyErr) {
538
+ console.warn('PTY spawn failed, falling back to regular spawn:', ptyErr.message);
539
+ // Fallback to regular child_process spawn
540
+ fallbackProc = spawn(shell, [], {
541
+ cwd: ROOT_DIR,
542
+ env: cleanEnv,
543
+ stdio: ['pipe', 'pipe', 'pipe'],
544
+ });
545
+ }
546
+ if (ptyProcess) {
547
+ terminals.set(terminalId, {
548
+ pty: ptyProcess,
549
+ cols,
550
+ rows,
551
+ });
552
+ // Stream output to app via data channel
553
+ ptyProcess.onData((data) => {
554
+ if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
555
+ const msg = {
556
+ v: 1,
557
+ id: `evt-${Date.now()}`,
558
+ ns: "terminal",
559
+ action: "output",
560
+ payload: { terminalId, data },
561
+ };
562
+ dataChannel.send(JSON.stringify(msg));
563
+ }
564
+ });
565
+ ptyProcess.onExit(({ exitCode }) => {
566
+ terminals.delete(terminalId);
567
+ if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
568
+ const msg = {
569
+ v: 1,
570
+ id: `evt-${Date.now()}`,
571
+ ns: "terminal",
572
+ action: "exit",
573
+ payload: { terminalId, code: exitCode },
574
+ };
575
+ dataChannel.send(JSON.stringify(msg));
576
+ }
577
+ });
578
+ }
579
+ else if (fallbackProc) {
580
+ // Store fallback process with a wrapper that mimics PTY interface
581
+ const wrapperPty = {
582
+ write: (data) => fallbackProc.stdin?.write(data),
583
+ resize: (_cols, _rows) => { },
584
+ kill: (signal) => fallbackProc.kill(signal),
585
+ pid: fallbackProc.pid || 0,
586
+ cols,
587
+ rows,
588
+ process: shell,
589
+ handleFlowControl: false,
590
+ onData: (cb) => {
591
+ fallbackProc.stdout?.on('data', (d) => cb(d.toString()));
592
+ fallbackProc.stderr?.on('data', (d) => cb(d.toString()));
593
+ return { dispose: () => { } };
594
+ },
595
+ onExit: (cb) => {
596
+ fallbackProc.on('close', (code) => cb({ exitCode: code || 0 }));
597
+ return { dispose: () => { } };
598
+ },
599
+ clear: () => { },
600
+ pause: () => { },
601
+ resume: () => { },
602
+ };
603
+ terminals.set(terminalId, {
604
+ pty: wrapperPty,
605
+ cols,
606
+ rows,
607
+ });
608
+ // Stream output
609
+ const sendOutput = (data) => {
610
+ if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
611
+ const msg = {
612
+ v: 1,
613
+ id: `evt-${Date.now()}`,
614
+ ns: "terminal",
615
+ action: "output",
616
+ payload: { terminalId, data: data.toString() },
617
+ };
618
+ dataChannel.send(JSON.stringify(msg));
619
+ }
620
+ };
621
+ fallbackProc.stdout?.on('data', sendOutput);
622
+ fallbackProc.stderr?.on('data', sendOutput);
623
+ fallbackProc.on('close', (code) => {
624
+ terminals.delete(terminalId);
625
+ if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
626
+ const msg = {
627
+ v: 1,
628
+ id: `evt-${Date.now()}`,
629
+ ns: "terminal",
630
+ action: "exit",
631
+ payload: { terminalId, code: code || 0 },
632
+ };
633
+ dataChannel.send(JSON.stringify(msg));
634
+ }
635
+ });
636
+ }
637
+ else {
638
+ throw Object.assign(new Error('Failed to spawn terminal'), { code: 'ESPAWN' });
639
+ }
527
640
  return { terminalId };
528
641
  }
529
642
  function handleTerminalWrite(payload) {
@@ -533,10 +646,10 @@ function handleTerminalWrite(payload) {
533
646
  throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
534
647
  if (typeof data !== "string")
535
648
  throw Object.assign(new Error("data is required"), { code: "EINVAL" });
536
- const proc = terminals.get(terminalId);
537
- if (!proc)
649
+ const session = terminals.get(terminalId);
650
+ if (!session)
538
651
  throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
539
- proc.stdin?.write(data);
652
+ session.pty.write(data);
540
653
  return {};
541
654
  }
542
655
  function handleTerminalResize(payload) {
@@ -545,21 +658,25 @@ function handleTerminalResize(payload) {
545
658
  const rows = payload.rows;
546
659
  if (!terminalId)
547
660
  throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
548
- const proc = terminals.get(terminalId);
549
- if (!proc)
661
+ if (!cols || !rows)
662
+ throw Object.assign(new Error("cols and rows are required"), { code: "EINVAL" });
663
+ const session = terminals.get(terminalId);
664
+ if (!session)
550
665
  throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
551
- // Note: For proper PTY resize, you'd need node-pty
552
- // This is a simplified version
666
+ // Resize PTY
667
+ session.pty.resize(cols, rows);
668
+ session.cols = cols;
669
+ session.rows = rows;
553
670
  return {};
554
671
  }
555
672
  function handleTerminalKill(payload) {
556
673
  const terminalId = payload.terminalId;
557
674
  if (!terminalId)
558
675
  throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
559
- const proc = terminals.get(terminalId);
560
- if (!proc)
676
+ const session = terminals.get(terminalId);
677
+ if (!session)
561
678
  throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
562
- proc.kill();
679
+ session.pty.kill();
563
680
  terminals.delete(terminalId);
564
681
  return {};
565
682
  }
@@ -1355,8 +1472,8 @@ function connectWebSocket(code) {
1355
1472
  process.on("SIGINT", () => {
1356
1473
  console.log("\nShutting down...");
1357
1474
  // Kill all terminals
1358
- for (const [id, proc] of terminals) {
1359
- proc.kill();
1475
+ for (const [id, session] of terminals) {
1476
+ session.pty.kill();
1360
1477
  }
1361
1478
  terminals.clear();
1362
1479
  // Kill all managed processes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "ignore": "^6.0.2",
29
+ "node-pty": "^1.0.0",
29
30
  "qrcode-terminal": "^0.12.0",
30
31
  "ws": "^8.18.0"
31
32
  },