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.
Files changed (2) hide show
  1. package/dist/index.js +304 -16
  2. 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'] = 'dumb'; // Simple terminal - no escape codes
515
- // Spawn shell process
516
- const proc = spawn(shell, ['-i'], {
517
- cwd: ROOT_DIR,
518
- env: cleanEnv,
519
- stdio: ['pipe', 'pipe', 'pipe'],
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
- terminals.set(terminalId, { proc, shell });
522
- // Stream output to app
523
- const sendOutput = (data) => {
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: "output",
530
- payload: { terminalId, data: data.toString() },
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', sendOutput);
536
- proc.stderr?.on('data', sendOutput);
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
- return { terminalId, shell };
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
- // No-op for simple terminal - resize not supported without PTY
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.7",
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",