@bamptee/aia-code 2.0.12 → 2.0.14

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/src/ui/server.js CHANGED
@@ -99,81 +99,111 @@ export async function startServer(preferredPort, root = process.cwd()) {
99
99
  // Setup WebSocket server for terminal
100
100
  const wss = new WebSocketServer({ noServer: true });
101
101
 
102
+ // Helper function to setup PTY terminal with WebSocket
103
+ const setupPtyTerminal = (ws, command, args, options) => {
104
+ import('node-pty').then(({ spawn: ptySpawn }) => {
105
+ const ptyProcess = ptySpawn(command, args, {
106
+ name: 'xterm-256color',
107
+ cols: 80,
108
+ rows: 24,
109
+ env: process.env,
110
+ ...options,
111
+ });
112
+
113
+ // Idle timeout - close terminal after 30 minutes of inactivity
114
+ const IDLE_TIMEOUT = 30 * 60 * 1000;
115
+ let idleTimer = setTimeout(() => {
116
+ ws.send('\r\n\x1b[33m[Session closed due to inactivity]\x1b[0m\r\n');
117
+ ws.close();
118
+ }, IDLE_TIMEOUT);
119
+
120
+ const resetIdleTimer = () => {
121
+ clearTimeout(idleTimer);
122
+ idleTimer = setTimeout(() => {
123
+ ws.send('\r\n\x1b[33m[Session closed due to inactivity]\x1b[0m\r\n');
124
+ ws.close();
125
+ }, IDLE_TIMEOUT);
126
+ };
127
+
128
+ ws.on('message', (data) => {
129
+ resetIdleTimer();
130
+ const msg = data.toString();
131
+ // Limit message size to 64KB
132
+ if (msg.length > 65536) return;
133
+ // Handle resize messages
134
+ if (msg.startsWith('\x1b[8;')) {
135
+ const match = msg.match(/\x1b\[8;(\d+);(\d+)t/);
136
+ if (match) {
137
+ ptyProcess.resize(parseInt(match[2], 10), parseInt(match[1], 10));
138
+ return;
139
+ }
140
+ }
141
+ ptyProcess.write(msg);
142
+ });
143
+
144
+ ptyProcess.onData((data) => {
145
+ resetIdleTimer();
146
+ try {
147
+ ws.send(data);
148
+ } catch {}
149
+ });
150
+
151
+ ws.on('close', () => {
152
+ clearTimeout(idleTimer);
153
+ ptyProcess.kill();
154
+ });
155
+
156
+ ws.on('error', () => {
157
+ clearTimeout(idleTimer);
158
+ ptyProcess.kill();
159
+ });
160
+ }).catch((err) => {
161
+ ws.send(`\r\n\x1b[31mError: ${err.message}\x1b[0m\r\n`);
162
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
163
+ ws.send(`\x1b[33mRun: npm install node-pty\x1b[0m\r\n`);
164
+ }
165
+ ws.close();
166
+ });
167
+ };
168
+
102
169
  server.on('upgrade', (req, socket, head) => {
103
170
  const url = new URL(req.url, `http://${req.headers.host}`);
104
171
 
105
172
  if (url.pathname === '/api/terminal') {
106
173
  wss.handleUpgrade(req, socket, head, (ws) => {
107
174
  const cwd = url.searchParams.get('cwd') || root;
175
+ const shell = process.platform === 'win32'
176
+ ? 'powershell.exe'
177
+ : process.env.SHELL || '/bin/bash';
178
+ setupPtyTerminal(ws, shell, [], { cwd });
179
+ });
180
+ } else if (url.pathname === '/api/terminal/compose') {
181
+ wss.handleUpgrade(req, socket, head, (ws) => {
182
+ const service = url.searchParams.get('service');
183
+ const cwd = url.searchParams.get('cwd');
108
184
 
109
- // Dynamically import node-pty (may not be installed)
110
- import('node-pty').then(({ spawn: ptySpawn }) => {
111
- // F6: Use user's preferred shell from environment
112
- const shell = process.platform === 'win32'
113
- ? 'powershell.exe'
114
- : process.env.SHELL || '/bin/bash';
115
- const ptyProcess = ptySpawn(shell, [], {
116
- name: 'xterm-256color',
117
- cols: 80,
118
- rows: 24,
119
- cwd,
120
- env: process.env,
121
- });
122
-
123
- // F7: Idle timeout - close terminal after 30 minutes of inactivity
124
- const IDLE_TIMEOUT = 30 * 60 * 1000;
125
- let idleTimer = setTimeout(() => {
126
- ws.send('\r\n\x1b[33m[Session closed due to inactivity]\x1b[0m\r\n');
127
- ws.close();
128
- }, IDLE_TIMEOUT);
129
-
130
- const resetIdleTimer = () => {
131
- clearTimeout(idleTimer);
132
- idleTimer = setTimeout(() => {
133
- ws.send('\r\n\x1b[33m[Session closed due to inactivity]\x1b[0m\r\n');
134
- ws.close();
135
- }, IDLE_TIMEOUT);
136
- };
137
-
138
- ws.on('message', (data) => {
139
- resetIdleTimer();
140
- const msg = data.toString();
141
- // F5: Limit message size to 64KB
142
- if (msg.length > 65536) return;
143
- // Handle resize messages
144
- if (msg.startsWith('\x1b[8;')) {
145
- const match = msg.match(/\x1b\[8;(\d+);(\d+)t/);
146
- if (match) {
147
- ptyProcess.resize(parseInt(match[2], 10), parseInt(match[1], 10));
148
- return;
149
- }
150
- }
151
- ptyProcess.write(msg);
152
- });
153
-
154
- ptyProcess.onData((data) => {
155
- resetIdleTimer();
156
- try {
157
- ws.send(data);
158
- } catch {}
159
- });
160
-
161
- ws.on('close', () => {
162
- clearTimeout(idleTimer);
163
- ptyProcess.kill();
164
- });
165
-
166
- ws.on('error', () => {
167
- clearTimeout(idleTimer);
168
- ptyProcess.kill();
169
- });
170
- }).catch((err) => {
171
- ws.send(`\r\n\x1b[31mError: ${err.message}\x1b[0m\r\n`);
172
- if (err.code === 'ERR_MODULE_NOT_FOUND') {
173
- ws.send(`\x1b[33mRun: npm install node-pty\x1b[0m\r\n`);
174
- }
185
+ if (!service) {
186
+ ws.send('\r\n\x1b[31mError: service parameter required\x1b[0m\r\n');
187
+ ws.close();
188
+ return;
189
+ }
190
+ if (!cwd) {
191
+ ws.send('\r\n\x1b[31mError: cwd parameter required\x1b[0m\r\n');
175
192
  ws.close();
176
- });
193
+ return;
194
+ }
195
+ // Validate service name (alphanumeric, dash, underscore)
196
+ if (!/^[a-zA-Z0-9_-]+$/.test(service)) {
197
+ ws.send('\r\n\x1b[31mError: invalid service name\x1b[0m\r\n');
198
+ ws.close();
199
+ return;
200
+ }
201
+ // Get shell from query param or default to sh (most compatible)
202
+ const shell = url.searchParams.get('shell') || 'sh';
203
+ const validShells = ['sh', 'bash', 'ash', 'zsh'];
204
+ const safeShell = validShells.includes(shell) ? shell : 'sh';
205
+ // Spawn docker-compose exec <service> <shell>
206
+ setupPtyTerminal(ws, 'docker-compose', ['-f', 'docker-compose.wt.yml', 'exec', service, safeShell], { cwd });
177
207
  });
178
208
  } else {
179
209
  socket.destroy();