lunel-cli 0.1.7 → 0.1.9
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 +304 -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,30 +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
|
+
// Spawn shell directly with interactive flag
|
|
711
|
+
let proc;
|
|
712
|
+
const isWindows = os.platform() === 'win32';
|
|
713
|
+
console.log(`[Terminal] Spawning shell: ${shell} in ${ROOT_DIR}`);
|
|
714
|
+
if (isWindows) {
|
|
715
|
+
proc = spawn(shell, [], {
|
|
716
|
+
cwd: ROOT_DIR,
|
|
717
|
+
env: cleanEnv,
|
|
718
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
719
|
+
shell: true,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
// Unix: spawn shell with interactive flag
|
|
724
|
+
proc = spawn(shell, ['-i'], {
|
|
725
|
+
cwd: ROOT_DIR,
|
|
726
|
+
env: cleanEnv,
|
|
727
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
console.log(`[Terminal] Spawned with PID: ${proc.pid}`);
|
|
731
|
+
terminals.set(terminalId, {
|
|
732
|
+
proc,
|
|
733
|
+
shell,
|
|
734
|
+
cols,
|
|
735
|
+
rows,
|
|
736
|
+
buffer: parser.getState().buffer,
|
|
737
|
+
cursorX: 0,
|
|
738
|
+
cursorY: 0,
|
|
739
|
+
parser,
|
|
520
740
|
});
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
741
|
+
// Process output and send grid state
|
|
742
|
+
const processOutput = (data) => {
|
|
743
|
+
const str = data.toString();
|
|
744
|
+
parser.write(str);
|
|
745
|
+
const state = parser.getState();
|
|
524
746
|
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
525
747
|
const msg = {
|
|
526
748
|
v: 1,
|
|
527
749
|
id: `evt-${Date.now()}`,
|
|
528
750
|
ns: "terminal",
|
|
529
|
-
action: "
|
|
530
|
-
payload: {
|
|
751
|
+
action: "state",
|
|
752
|
+
payload: {
|
|
753
|
+
terminalId,
|
|
754
|
+
buffer: state.buffer,
|
|
755
|
+
cursorX: state.cursorX,
|
|
756
|
+
cursorY: state.cursorY,
|
|
757
|
+
cols: state.cols,
|
|
758
|
+
rows: state.rows,
|
|
759
|
+
},
|
|
531
760
|
};
|
|
532
761
|
dataChannel.send(JSON.stringify(msg));
|
|
533
762
|
}
|
|
534
763
|
};
|
|
535
|
-
proc.stdout?.on('data',
|
|
536
|
-
proc.stderr?.on('data',
|
|
764
|
+
proc.stdout?.on('data', processOutput);
|
|
765
|
+
proc.stderr?.on('data', processOutput);
|
|
766
|
+
proc.on('error', (err) => {
|
|
767
|
+
console.error(`[Terminal] Process error:`, err);
|
|
768
|
+
});
|
|
537
769
|
proc.on('close', (code) => {
|
|
770
|
+
console.log(`[Terminal] Process closed with code: ${code}`);
|
|
538
771
|
terminals.delete(terminalId);
|
|
539
772
|
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
540
773
|
const msg = {
|
|
@@ -547,7 +780,28 @@ function handleTerminalSpawn(payload) {
|
|
|
547
780
|
dataChannel.send(JSON.stringify(msg));
|
|
548
781
|
}
|
|
549
782
|
});
|
|
550
|
-
|
|
783
|
+
// Send initial state
|
|
784
|
+
setTimeout(() => {
|
|
785
|
+
const state = parser.getState();
|
|
786
|
+
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
787
|
+
const msg = {
|
|
788
|
+
v: 1,
|
|
789
|
+
id: `evt-${Date.now()}`,
|
|
790
|
+
ns: "terminal",
|
|
791
|
+
action: "state",
|
|
792
|
+
payload: {
|
|
793
|
+
terminalId,
|
|
794
|
+
buffer: state.buffer,
|
|
795
|
+
cursorX: state.cursorX,
|
|
796
|
+
cursorY: state.cursorY,
|
|
797
|
+
cols: state.cols,
|
|
798
|
+
rows: state.rows,
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
dataChannel.send(JSON.stringify(msg));
|
|
802
|
+
}
|
|
803
|
+
}, 100);
|
|
804
|
+
return { terminalId, shell, cols, rows };
|
|
551
805
|
}
|
|
552
806
|
function handleTerminalWrite(payload) {
|
|
553
807
|
const terminalId = payload.terminalId;
|
|
@@ -563,7 +817,41 @@ function handleTerminalWrite(payload) {
|
|
|
563
817
|
return {};
|
|
564
818
|
}
|
|
565
819
|
function handleTerminalResize(payload) {
|
|
566
|
-
|
|
820
|
+
const terminalId = payload.terminalId;
|
|
821
|
+
const cols = payload.cols;
|
|
822
|
+
const rows = payload.rows;
|
|
823
|
+
if (!terminalId)
|
|
824
|
+
throw Object.assign(new Error("terminalId is required"), { code: "EINVAL" });
|
|
825
|
+
if (!cols || !rows)
|
|
826
|
+
throw Object.assign(new Error("cols and rows are required"), { code: "EINVAL" });
|
|
827
|
+
const session = terminals.get(terminalId);
|
|
828
|
+
if (!session)
|
|
829
|
+
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
830
|
+
// Resize parser buffer
|
|
831
|
+
session.parser.resize(cols, rows);
|
|
832
|
+
session.cols = cols;
|
|
833
|
+
session.rows = rows;
|
|
834
|
+
// Send SIGWINCH to process (resize signal) - won't work without real PTY
|
|
835
|
+
// But we update our internal state
|
|
836
|
+
// Send updated state
|
|
837
|
+
const state = session.parser.getState();
|
|
838
|
+
if (dataChannel && dataChannel.readyState === WebSocket.OPEN) {
|
|
839
|
+
const msg = {
|
|
840
|
+
v: 1,
|
|
841
|
+
id: `evt-${Date.now()}`,
|
|
842
|
+
ns: "terminal",
|
|
843
|
+
action: "state",
|
|
844
|
+
payload: {
|
|
845
|
+
terminalId,
|
|
846
|
+
buffer: state.buffer,
|
|
847
|
+
cursorX: state.cursorX,
|
|
848
|
+
cursorY: state.cursorY,
|
|
849
|
+
cols: state.cols,
|
|
850
|
+
rows: state.rows,
|
|
851
|
+
},
|
|
852
|
+
};
|
|
853
|
+
dataChannel.send(JSON.stringify(msg));
|
|
854
|
+
}
|
|
567
855
|
return {};
|
|
568
856
|
}
|
|
569
857
|
function handleTerminalKill(payload) {
|
|
@@ -573,7 +861,7 @@ function handleTerminalKill(payload) {
|
|
|
573
861
|
const session = terminals.get(terminalId);
|
|
574
862
|
if (!session)
|
|
575
863
|
throw Object.assign(new Error("Terminal not found"), { code: "ENOTERM" });
|
|
576
|
-
session.proc.kill();
|
|
864
|
+
session.proc.kill('SIGKILL');
|
|
577
865
|
terminals.delete(terminalId);
|
|
578
866
|
return {};
|
|
579
867
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lunel-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|