lunel-cli 0.1.7 → 0.1.8
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 +305 -16
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -12,6 +12,195 @@ const PROXY_URL = process.env.LUNEL_PROXY_URL || "https://gateway.lunel.dev";
|
|
|
12
12
|
const VERSION = "0.1.6";
|
|
13
13
|
// Root directory - sandbox all file operations to this
|
|
14
14
|
const ROOT_DIR = process.cwd();
|
|
15
|
+
// Simple ANSI parser for terminal state
|
|
16
|
+
class AnsiParser {
|
|
17
|
+
buffer;
|
|
18
|
+
cursorX = 0;
|
|
19
|
+
cursorY = 0;
|
|
20
|
+
cols;
|
|
21
|
+
rows;
|
|
22
|
+
currentFg = '#ffffff';
|
|
23
|
+
currentBg = '#000000';
|
|
24
|
+
scrollbackBuffer = [];
|
|
25
|
+
// ANSI color codes to hex
|
|
26
|
+
colors = {
|
|
27
|
+
0: '#000000', 1: '#cc0000', 2: '#4e9a06', 3: '#c4a000',
|
|
28
|
+
4: '#3465a4', 5: '#75507b', 6: '#06989a', 7: '#d3d7cf',
|
|
29
|
+
8: '#555753', 9: '#ef2929', 10: '#8ae234', 11: '#fce94f',
|
|
30
|
+
12: '#729fcf', 13: '#ad7fa8', 14: '#34e2e2', 15: '#eeeeec',
|
|
31
|
+
};
|
|
32
|
+
constructor(cols, rows) {
|
|
33
|
+
this.cols = cols;
|
|
34
|
+
this.rows = rows;
|
|
35
|
+
this.buffer = this.createEmptyBuffer();
|
|
36
|
+
}
|
|
37
|
+
createEmptyBuffer() {
|
|
38
|
+
return Array.from({ length: this.rows }, () => Array.from({ length: this.cols }, () => ({
|
|
39
|
+
char: ' ',
|
|
40
|
+
fg: '#ffffff',
|
|
41
|
+
bg: '#000000',
|
|
42
|
+
})));
|
|
43
|
+
}
|
|
44
|
+
scrollUp() {
|
|
45
|
+
// Move first line to scrollback
|
|
46
|
+
this.scrollbackBuffer.push(this.buffer.shift());
|
|
47
|
+
// Keep scrollback limited
|
|
48
|
+
if (this.scrollbackBuffer.length > 1000) {
|
|
49
|
+
this.scrollbackBuffer.shift();
|
|
50
|
+
}
|
|
51
|
+
// Add new empty line at bottom
|
|
52
|
+
this.buffer.push(Array.from({ length: this.cols }, () => ({
|
|
53
|
+
char: ' ',
|
|
54
|
+
fg: '#ffffff',
|
|
55
|
+
bg: '#000000',
|
|
56
|
+
})));
|
|
57
|
+
}
|
|
58
|
+
write(data) {
|
|
59
|
+
let i = 0;
|
|
60
|
+
while (i < data.length) {
|
|
61
|
+
const char = data[i];
|
|
62
|
+
// Check for escape sequence
|
|
63
|
+
if (char === '\x1b' && data[i + 1] === '[') {
|
|
64
|
+
// Parse CSI sequence
|
|
65
|
+
let j = i + 2;
|
|
66
|
+
let params = '';
|
|
67
|
+
while (j < data.length && /[0-9;]/.test(data[j])) {
|
|
68
|
+
params += data[j];
|
|
69
|
+
j++;
|
|
70
|
+
}
|
|
71
|
+
const cmd = data[j];
|
|
72
|
+
i = j + 1;
|
|
73
|
+
this.handleCSI(params, cmd);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Handle special characters
|
|
77
|
+
if (char === '\n') {
|
|
78
|
+
this.cursorY++;
|
|
79
|
+
if (this.cursorY >= this.rows) {
|
|
80
|
+
this.scrollUp();
|
|
81
|
+
this.cursorY = this.rows - 1;
|
|
82
|
+
}
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char === '\r') {
|
|
87
|
+
this.cursorX = 0;
|
|
88
|
+
i++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (char === '\b') {
|
|
92
|
+
if (this.cursorX > 0)
|
|
93
|
+
this.cursorX--;
|
|
94
|
+
i++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === '\t') {
|
|
98
|
+
this.cursorX = Math.min(this.cols - 1, (Math.floor(this.cursorX / 8) + 1) * 8);
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (char === '\x07') { // Bell
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Skip other control characters
|
|
107
|
+
if (char.charCodeAt(0) < 32) {
|
|
108
|
+
i++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Regular character
|
|
112
|
+
if (this.cursorX >= this.cols) {
|
|
113
|
+
this.cursorX = 0;
|
|
114
|
+
this.cursorY++;
|
|
115
|
+
if (this.cursorY >= this.rows) {
|
|
116
|
+
this.scrollUp();
|
|
117
|
+
this.cursorY = this.rows - 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (this.cursorY >= 0 && this.cursorY < this.rows &&
|
|
121
|
+
this.cursorX >= 0 && this.cursorX < this.cols) {
|
|
122
|
+
this.buffer[this.cursorY][this.cursorX] = {
|
|
123
|
+
char,
|
|
124
|
+
fg: this.currentFg,
|
|
125
|
+
bg: this.currentBg,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
this.cursorX++;
|
|
129
|
+
i++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
handleCSI(params, cmd) {
|
|
133
|
+
const args = params.split(';').map(p => parseInt(p) || 0);
|
|
134
|
+
switch (cmd) {
|
|
135
|
+
case 'A': // Cursor up
|
|
136
|
+
this.cursorY = Math.max(0, this.cursorY - (args[0] || 1));
|
|
137
|
+
break;
|
|
138
|
+
case 'B': // Cursor down
|
|
139
|
+
this.cursorY = Math.min(this.rows - 1, this.cursorY + (args[0] || 1));
|
|
140
|
+
break;
|
|
141
|
+
case 'C': // Cursor forward
|
|
142
|
+
this.cursorX = Math.min(this.cols - 1, this.cursorX + (args[0] || 1));
|
|
143
|
+
break;
|
|
144
|
+
case 'D': // Cursor back
|
|
145
|
+
this.cursorX = Math.max(0, this.cursorX - (args[0] || 1));
|
|
146
|
+
break;
|
|
147
|
+
case 'H': // Cursor position
|
|
148
|
+
case 'f':
|
|
149
|
+
this.cursorY = Math.min(this.rows - 1, Math.max(0, (args[0] || 1) - 1));
|
|
150
|
+
this.cursorX = Math.min(this.cols - 1, Math.max(0, (args[1] || 1) - 1));
|
|
151
|
+
break;
|
|
152
|
+
case 'J': // Erase display
|
|
153
|
+
if (args[0] === 2) {
|
|
154
|
+
this.buffer = this.createEmptyBuffer();
|
|
155
|
+
this.cursorX = 0;
|
|
156
|
+
this.cursorY = 0;
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
case 'K': // Erase line
|
|
160
|
+
for (let x = this.cursorX; x < this.cols; x++) {
|
|
161
|
+
this.buffer[this.cursorY][x] = { char: ' ', fg: this.currentFg, bg: this.currentBg };
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case 'm': // SGR - Select Graphic Rendition
|
|
165
|
+
for (const arg of args) {
|
|
166
|
+
if (arg === 0) {
|
|
167
|
+
this.currentFg = '#ffffff';
|
|
168
|
+
this.currentBg = '#000000';
|
|
169
|
+
}
|
|
170
|
+
else if (arg >= 30 && arg <= 37) {
|
|
171
|
+
this.currentFg = this.colors[arg - 30] || '#ffffff';
|
|
172
|
+
}
|
|
173
|
+
else if (arg >= 40 && arg <= 47) {
|
|
174
|
+
this.currentBg = this.colors[arg - 40] || '#000000';
|
|
175
|
+
}
|
|
176
|
+
else if (arg >= 90 && arg <= 97) {
|
|
177
|
+
this.currentFg = this.colors[arg - 90 + 8] || '#ffffff';
|
|
178
|
+
}
|
|
179
|
+
else if (arg >= 100 && arg <= 107) {
|
|
180
|
+
this.currentBg = this.colors[arg - 100 + 8] || '#000000';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
resize(cols, rows) {
|
|
187
|
+
const newBuffer = Array.from({ length: rows }, (_, y) => Array.from({ length: cols }, (_, x) => (y < this.rows && x < this.cols) ? this.buffer[y][x] : { char: ' ', fg: '#ffffff', bg: '#000000' }));
|
|
188
|
+
this.cols = cols;
|
|
189
|
+
this.rows = rows;
|
|
190
|
+
this.buffer = newBuffer;
|
|
191
|
+
this.cursorX = Math.min(this.cursorX, cols - 1);
|
|
192
|
+
this.cursorY = Math.min(this.cursorY, rows - 1);
|
|
193
|
+
}
|
|
194
|
+
getState() {
|
|
195
|
+
return {
|
|
196
|
+
buffer: this.buffer,
|
|
197
|
+
cursorX: this.cursorX,
|
|
198
|
+
cursorY: this.cursorY,
|
|
199
|
+
cols: this.cols,
|
|
200
|
+
rows: this.rows,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
15
204
|
const terminals = new Map();
|
|
16
205
|
const processes = new Map();
|
|
17
206
|
const processOutputBuffers = new Map();
|
|
@@ -480,6 +669,8 @@ async function handleGitDiscard(payload) {
|
|
|
480
669
|
// ============================================================================
|
|
481
670
|
let dataChannel = null;
|
|
482
671
|
function handleTerminalSpawn(payload) {
|
|
672
|
+
const cols = payload.cols || 80;
|
|
673
|
+
const rows = payload.rows || 24;
|
|
483
674
|
// Find a working shell
|
|
484
675
|
let shell = payload.shell || process.env.SHELL || '';
|
|
485
676
|
const possibleShells = os.platform() === 'win32'
|
|
@@ -511,29 +702,72 @@ function handleTerminalSpawn(payload) {
|
|
|
511
702
|
cleanEnv[key] = value;
|
|
512
703
|
}
|
|
513
704
|
}
|
|
514
|
-
cleanEnv['TERM'] = '
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
705
|
+
cleanEnv['TERM'] = 'xterm-256color';
|
|
706
|
+
cleanEnv['COLUMNS'] = cols.toString();
|
|
707
|
+
cleanEnv['LINES'] = rows.toString();
|
|
708
|
+
// Create ANSI parser for terminal state
|
|
709
|
+
const parser = new AnsiParser(cols, rows);
|
|
710
|
+
// Use 'script' command to create a PTY (works on macOS/Linux)
|
|
711
|
+
let proc;
|
|
712
|
+
if (os.platform() === 'darwin') {
|
|
713
|
+
// macOS: script -q /dev/null shell
|
|
714
|
+
proc = spawn('script', ['-q', '/dev/null', shell], {
|
|
715
|
+
cwd: ROOT_DIR,
|
|
716
|
+
env: cleanEnv,
|
|
717
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
else if (os.platform() === 'linux') {
|
|
721
|
+
// Linux: script -q -c shell /dev/null
|
|
722
|
+
proc = spawn('script', ['-q', '-c', shell, '/dev/null'], {
|
|
723
|
+
cwd: ROOT_DIR,
|
|
724
|
+
env: cleanEnv,
|
|
725
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
// Windows or other: fall back to direct spawn
|
|
730
|
+
proc = spawn(shell, ['-i'], {
|
|
731
|
+
cwd: ROOT_DIR,
|
|
732
|
+
env: cleanEnv,
|
|
733
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
terminals.set(terminalId, {
|
|
737
|
+
proc,
|
|
738
|
+
shell,
|
|
739
|
+
cols,
|
|
740
|
+
rows,
|
|
741
|
+
buffer: parser.getState().buffer,
|
|
742
|
+
cursorX: 0,
|
|
743
|
+
cursorY: 0,
|
|
744
|
+
parser,
|
|
520
745
|
});
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
746
|
+
// Process output and send grid state
|
|
747
|
+
const processOutput = (data) => {
|
|
748
|
+
const str = data.toString();
|
|
749
|
+
parser.write(str);
|
|
750
|
+
const state = parser.getState();
|
|
524
751
|
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
525
752
|
const msg = {
|
|
526
753
|
v: 1,
|
|
527
754
|
id: `evt-${Date.now()}`,
|
|
528
755
|
ns: "terminal",
|
|
529
|
-
action: "
|
|
530
|
-
payload: {
|
|
756
|
+
action: "state",
|
|
757
|
+
payload: {
|
|
758
|
+
terminalId,
|
|
759
|
+
buffer: state.buffer,
|
|
760
|
+
cursorX: state.cursorX,
|
|
761
|
+
cursorY: state.cursorY,
|
|
762
|
+
cols: state.cols,
|
|
763
|
+
rows: state.rows,
|
|
764
|
+
},
|
|
531
765
|
};
|
|
532
766
|
dataChannel.send(JSON.stringify(msg));
|
|
533
767
|
}
|
|
534
768
|
};
|
|
535
|
-
proc.stdout?.on('data',
|
|
536
|
-
proc.stderr?.on('data',
|
|
769
|
+
proc.stdout?.on('data', processOutput);
|
|
770
|
+
proc.stderr?.on('data', processOutput);
|
|
537
771
|
proc.on('close', (code) => {
|
|
538
772
|
terminals.delete(terminalId);
|
|
539
773
|
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
@@ -547,7 +781,28 @@ function handleTerminalSpawn(payload) {
|
|
|
547
781
|
dataChannel.send(JSON.stringify(msg));
|
|
548
782
|
}
|
|
549
783
|
});
|
|
550
|
-
|
|
784
|
+
// Send initial state
|
|
785
|
+
setTimeout(() => {
|
|
786
|
+
const state = parser.getState();
|
|
787
|
+
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
788
|
+
const msg = {
|
|
789
|
+
v: 1,
|
|
790
|
+
id: `evt-${Date.now()}`,
|
|
791
|
+
ns: "terminal",
|
|
792
|
+
action: "state",
|
|
793
|
+
payload: {
|
|
794
|
+
terminalId,
|
|
795
|
+
buffer: state.buffer,
|
|
796
|
+
cursorX: state.cursorX,
|
|
797
|
+
cursorY: state.cursorY,
|
|
798
|
+
cols: state.cols,
|
|
799
|
+
rows: state.rows,
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
dataChannel.send(JSON.stringify(msg));
|
|
803
|
+
}
|
|
804
|
+
}, 100);
|
|
805
|
+
return { terminalId, shell, cols, rows };
|
|
551
806
|
}
|
|
552
807
|
function handleTerminalWrite(payload) {
|
|
553
808
|
const terminalId = payload.terminalId;
|
|
@@ -563,7 +818,41 @@ function handleTerminalWrite(payload) {
|
|
|
563
818
|
return {};
|
|
564
819
|
}
|
|
565
820
|
function handleTerminalResize(payload) {
|
|
566
|
-
|
|
821
|
+
const terminalId = payload.terminalId;
|
|
822
|
+
const cols = payload.cols;
|
|
823
|
+
const rows = payload.rows;
|
|
824
|
+
if (!terminalId)
|
|
825
|
+
throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
|
|
826
|
+
if (!cols || !rows)
|
|
827
|
+
throw Object.assign(new Error("cols and rows are required"), { code: "EINVAL" });
|
|
828
|
+
const session = terminals.get(terminalId);
|
|
829
|
+
if (!session)
|
|
830
|
+
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
831
|
+
// Resize parser buffer
|
|
832
|
+
session.parser.resize(cols, rows);
|
|
833
|
+
session.cols = cols;
|
|
834
|
+
session.rows = rows;
|
|
835
|
+
// Send SIGWINCH to process (resize signal) - won't work without real PTY
|
|
836
|
+
// But we update our internal state
|
|
837
|
+
// Send updated state
|
|
838
|
+
const state = session.parser.getState();
|
|
839
|
+
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
840
|
+
const msg = {
|
|
841
|
+
v: 1,
|
|
842
|
+
id: `evt-${Date.now()}`,
|
|
843
|
+
ns: "terminal",
|
|
844
|
+
action: "state",
|
|
845
|
+
payload: {
|
|
846
|
+
terminalId,
|
|
847
|
+
buffer: state.buffer,
|
|
848
|
+
cursorX: state.cursorX,
|
|
849
|
+
cursorY: state.cursorY,
|
|
850
|
+
cols: state.cols,
|
|
851
|
+
rows: state.rows,
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
dataChannel.send(JSON.stringify(msg));
|
|
855
|
+
}
|
|
567
856
|
return {};
|
|
568
857
|
}
|
|
569
858
|
function handleTerminalKill(payload) {
|
|
@@ -573,7 +862,7 @@ function handleTerminalKill(payload) {
|
|
|
573
862
|
const session = terminals.get(terminalId);
|
|
574
863
|
if (!session)
|
|
575
864
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
576
|
-
session.proc.kill();
|
|
865
|
+
session.proc.kill('SIGKILL');
|
|
577
866
|
terminals.delete(terminalId);
|
|
578
867
|
return {};
|
|
579
868
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lunel-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"author": [
|
|
5
5
|
{
|
|
6
6
|
"name": "Soham Bharambe",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"ignore": "^6.0.2",
|
|
29
29
|
"qrcode-terminal": "^0.12.0",
|
|
30
|
-
"ws": "^8.18.0"
|
|
30
|
+
"ws": "^8.18.0",
|
|
31
|
+
"@xterm/headless": "^5.5.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/node": "^20.0.0",
|