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.
Files changed (2) hide show
  1. package/dist/index.js +305 -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,29 +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
+ // 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
- terminals.set(terminalId, { proc, shell });
522
- // Stream output to app
523
- const sendOutput = (data) => {
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: "output",
530
- payload: { terminalId, data: data.toString() },
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', sendOutput);
536
- proc.stderr?.on('data', sendOutput);
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
- return { terminalId, shell };
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
- // No-op for simple terminal - resize not supported without PTY
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.7",
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",