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.
- package/dist/index.js +170 -53
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
537
|
-
if (!
|
|
649
|
+
const session = terminals.get(terminalId);
|
|
650
|
+
if (!session)
|
|
538
651
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
539
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
//
|
|
552
|
-
|
|
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
|
|
560
|
-
if (!
|
|
676
|
+
const session = terminals.get(terminalId);
|
|
677
|
+
if (!session)
|
|
561
678
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
562
|
-
|
|
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,
|
|
1359
|
-
|
|
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.
|
|
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
|
},
|