@startanaicompany/cli 1.4.18 → 1.4.20

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/CLAUDE.md CHANGED
@@ -838,58 +838,102 @@ saac run -q npm test # Quiet mode (suppress warnings)
838
838
 
839
839
  ---
840
840
 
841
- #### Shell Command Implementation
841
+ #### Shell Command Implementation (Project Aurora)
842
842
 
843
- The `shell` command opens an interactive shell with remote environment variables loaded.
843
+ The `shell` command provides a **TRUE remote shell** - you're actually inside the Docker container, not just a local shell with env vars loaded. This is powered by Project Aurora (WebSocket + Docker + tmux).
844
+
845
+ **Key Difference from Phase 1:**
846
+ - **OLD (Phase 1)**: Local shell with remote env vars → wrong filesystem, wrong tools, wrong paths
847
+ - **NEW (Aurora)**: Remote shell inside container → TRUE remote experience like SSH/Railway
844
848
 
845
849
  **Usage:**
846
850
  ```bash
847
- saac shell # Open shell (uses $SHELL or /bin/bash)
848
- saac shell --cmd zsh # Use specific shell
849
- saac shell --sync # Force refresh env vars (skip cache)
851
+ saac shell # Connect to remote container shell
850
852
  ```
851
853
 
852
- **Implementation Details:**
853
- - Fetches env vars from `GET /applications/:uuid/env/export`
854
- - Uses 5-minute in-memory cache (same as `run`)
855
- - Creates temp file for env vars at `/tmp/saac-env-{uuid}.sh`
856
- - Special handling for bash and zsh (custom RC files)
857
- - Sources environment before shell starts
858
- - Automatic cleanup when shell exits
859
-
860
- **Shell-Specific Behavior:**
861
- - **bash:** Uses `--rcfile` with temporary RC that sources env + user's `.bashrc`
862
- - **zsh:** Uses `ZDOTDIR` with temporary `.zshrc` that sources env + user's `.zshrc`
863
- - **other:** Fallback to bash with source then exec
854
+ **Architecture:**
855
+ - WebSocket client connects to backend server
856
+ - Backend creates Docker container with tmux session
857
+ - Commands sent via WebSocket, executed in container
858
+ - Terminal output captured from tmux and streamed back
859
+ - Session persists even after disconnect (up to 1 hour idle)
864
860
 
865
- **Security Features:**
866
- - Same as `run` command (0600 permissions, cleanup, warnings)
867
- - Sets environment variables: `SAAC_ENV_LOADED=1`, `SAAC_APP_NAME`, `SAAC_APP_UUID`
861
+ **Implementation Details:**
862
+ - Uses `ws` package for WebSocket client
863
+ - Connects to: `wss://apps.startanaicompany.com/api/v1/shell/connect`
864
+ - Authentication via session token or API key
865
+ - Readline interface for local terminal interaction
866
+ - Automatic reconnection on disconnect (up to 3 attempts)
867
+ - Heartbeat ping/pong every 30 seconds
868
+
869
+ **Session Lifecycle:**
870
+ 1. CLI connects via WebSocket with auth token
871
+ 2. Backend creates session record in database (status: 'creating')
872
+ 3. Python daemon creates Docker container with tmux
873
+ 4. Container ready, status updates to 'active'
874
+ 5. User interacts with remote shell
875
+ 6. On disconnect, session marked 'idle' (persists for 1 hour)
876
+ 7. Idle timeout or manual termination cleans up container
877
+
878
+ **Features:**
879
+ - ✅ TRUE remote shell (inside container, not local)
880
+ - ✅ Persistent session (survives reconnections)
881
+ - ✅ Interactive commands (vim, nano, etc work!)
882
+ - ✅ Working directory persists between commands
883
+ - ✅ Command history (up arrow works)
884
+ - ✅ Real-time output streaming
885
+ - ✅ Ctrl+C sends to remote shell
886
+
887
+ **Connection Handling:**
888
+ - 30-second timeout for initial container creation
889
+ - Auto-reconnect on network issues (3 attempts, exponential backoff)
890
+ - Graceful cleanup on exit (Ctrl+D or "exit" command)
868
891
 
869
892
  **User Experience:**
870
893
  ```bash
871
894
  $ saac shell
872
- ✓ Environment variables retrieved
873
895
 
874
- 🚀 Opening shell with 6 environment variables loaded
896
+ Remote Shell: my-app
897
+
898
+ Connecting to container...
899
+ This may take up to 30 seconds for container creation.
875
900
 
876
- Application: my-app
877
- Shell: bash
878
- Variables: 6
901
+ ✓ Connected to remote container
902
+ Container is being created, please wait...
903
+ ✓ Container is ready!
904
+ Type commands below. Press Ctrl+D or type "exit" to quit.
879
905
 
880
- ⚠️ Secrets are exposed on local machine
881
- Temporary file: /tmp/saac-env-abc123.sh (will be deleted on exit)
906
+ root@container:/app# ls -la
907
+ total 48
908
+ drwxr-xr-x 6 root root 4096 Jan 28 20:00 .
909
+ drwxr-xr-x 18 root root 4096 Jan 28 20:00 ..
910
+ -rw-r--r-- 1 root root 220 Jan 28 20:00 package.json
911
+ drwxr-xr-x 2 root root 4096 Jan 28 20:00 src
882
912
 
883
- Type "exit" or press Ctrl+D to close the shell
884
- ────────────────────────────────────────────────────────────────
913
+ root@container:/app# cd src && pwd
914
+ /app/src
885
915
 
886
- bash-5.0$ echo $NODE_ENV
887
- production
888
- bash-5.0$ exit
916
+ root@container:/app/src# exit
889
917
 
890
- Shell closed, environment variables cleared
918
+ Disconnecting from remote shell...
891
919
  ```
892
920
 
921
+ **Error Handling:**
922
+ - `Authentication failed` → Token expired, run `saac login`
923
+ - `Access denied` → User doesn't own the application
924
+ - `Connection timeout` → Server unavailable or container failed to start
925
+ - `Max reconnection attempts` → Network issue, try again later
926
+
927
+ **Backend Requirements:**
928
+ - WebSocket server at `/api/v1/shell/connect` endpoint
929
+ - PostgreSQL tables: `shell_sessions`, `shell_commands`, `shell_output`
930
+ - Python daemon managing Docker containers with tmux
931
+ - Session cleanup after 1 hour idle or 4 hours total
932
+
933
+ **Location:** `src/commands/shell.js` (403 lines, complete rewrite for Aurora)
934
+
935
+ **Note:** Backend infrastructure (WebSocket server + Python daemon) must be deployed before this command works. CLI is ready, waiting for backend Phase 3.5 deployment.
936
+
893
937
  **Location:** `src/commands/shell.js`
894
938
 
895
939
  ---
@@ -1075,11 +1119,16 @@ The wrapper API expects Git repositories to be hosted on the StartAnAiCompany Gi
1075
1119
  - ✅ `saac env set/get/list` - Manage environment variables (fully implemented)
1076
1120
  - ✅ `saac domain set/show` - Manage application domain (fully implemented)
1077
1121
 
1078
- **Local Development (NEW in 1.5.0):**
1122
+ **Local Development (Phase 1):**
1079
1123
  - ✅ `saac run <command>` - Execute local command with remote environment variables
1080
- - ✅ `saac shell` - Open interactive shell with remote environment variables
1081
1124
  - Features: 5-minute caching, secure temp files (0600), automatic cleanup, rate limit handling
1082
1125
 
1126
+ **Remote Shell (Project Aurora - Phase 3.5):**
1127
+ - ✅ `saac shell` - TRUE remote shell inside Docker container via WebSocket
1128
+ - Features: Persistent sessions (tmux), interactive (vim/nano work), real-time output, auto-reconnect
1129
+ - Architecture: WebSocket client + PostgreSQL + Python daemon + Docker + tmux
1130
+ - **Status:** CLI ready, waiting for backend deployment
1131
+
1083
1132
  **Remote Execution (NEW in 1.6.0):**
1084
1133
  - ✅ `saac exec <command>` - Execute commands in remote container
1085
1134
  - ✅ `saac exec --history` - View execution history
package/bin/saac.js CHANGED
@@ -232,9 +232,7 @@ program
232
232
 
233
233
  program
234
234
  .command('shell')
235
- .description('Open interactive shell with remote environment variables')
236
- .option('--cmd <shell>', 'Shell to use (default: $SHELL or /bin/bash)')
237
- .option('--sync', 'Force refresh environment variables (skip cache)')
235
+ .description('Open interactive remote shell (inside container via WebSocket)')
238
236
  .action(shell);
239
237
 
240
238
  // Remote execution commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.4.18",
3
+ "version": "1.4.20",
4
4
  "description": "Official CLI for StartAnAiCompany.com - Deploy AI recruitment sites with ease",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -44,7 +44,8 @@
44
44
  "open": "^8.4.2",
45
45
  "ora": "^5.4.1",
46
46
  "table": "^6.8.1",
47
- "validator": "^13.11.0"
47
+ "validator": "^13.11.0",
48
+ "ws": "^8.19.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "eslint": "^8.54.0"
@@ -105,7 +105,7 @@ async function list() {
105
105
  * Set environment variables
106
106
  * @param {string[]} vars - Array of KEY=VALUE pairs
107
107
  */
108
- async function set(...vars) {
108
+ async function set(vars) {
109
109
  try {
110
110
  // Check authentication
111
111
  if (!isAuthenticated()) {
@@ -188,11 +188,20 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
188
188
  logger.newline();
189
189
 
190
190
  // Display logs
191
- if (result.logs && result.logs.length > 0) {
192
- result.logs.forEach(log => {
193
- console.log(log);
194
- });
191
+ if (result.logs) {
192
+ if (Array.isArray(result.logs)) {
193
+ // Logs is an array
194
+ result.logs.forEach(log => {
195
+ console.log(log);
196
+ });
197
+ } else if (typeof result.logs === 'string') {
198
+ // Logs is a string (most common format from backend)
199
+ console.log(result.logs);
200
+ } else {
201
+ logger.warn('Unexpected log format');
202
+ }
195
203
  } else if (typeof result === 'string') {
204
+ // Entire result is a string
196
205
  console.log(result);
197
206
  } else {
198
207
  logger.warn('No logs available');
@@ -1,55 +1,321 @@
1
1
  /**
2
- * Shell Command - Open interactive shell with remote environment variables
2
+ * Shell Command - Interactive remote shell via WebSocket (Project Aurora)
3
+ *
4
+ * This command provides a TRUE remote shell experience - you're actually inside
5
+ * the container, not just a local shell with env vars loaded.
3
6
  */
4
7
 
5
- const api = require('../lib/api');
6
- const { getProjectConfig, isAuthenticated } = require('../lib/config');
8
+ const WebSocket = require('ws');
9
+ const readline = require('readline');
10
+ const { getProjectConfig, isAuthenticated, getUser, getApiUrl } = require('../lib/config');
7
11
  const logger = require('../lib/logger');
8
- const { spawn } = require('child_process');
9
- const fs = require('fs');
10
- const path = require('path');
11
- const os = require('os');
12
-
13
- // In-memory cache for environment variables (5 minutes TTL)
14
- const envCache = new Map();
15
- const CACHE_TTL = 300000; // 5 minutes in milliseconds
16
12
 
17
13
  /**
18
- * Get environment variables (with caching)
19
- * @param {string} appUuid - Application UUID
20
- * @param {boolean} forceRefresh - Skip cache and fetch fresh
14
+ * WebSocket Shell Client
15
+ * Connects to backend WebSocket server and provides interactive shell
21
16
  */
22
- async function getEnvironmentVariables(appUuid, forceRefresh = false) {
23
- // Check cache first
24
- if (!forceRefresh) {
25
- const cached = envCache.get(appUuid);
26
- if (cached) {
27
- const age = Date.now() - cached.timestamp;
28
- if (age < CACHE_TTL) {
29
- logger.info('📦 Using cached environment variables (updated <5 min ago)');
30
- return cached.data;
17
+ class ShellClient {
18
+ constructor(serverUrl, token, applicationUuid) {
19
+ this.serverUrl = serverUrl;
20
+ this.token = token;
21
+ this.applicationUuid = applicationUuid;
22
+ this.ws = null;
23
+ this.sessionId = null;
24
+ this.connected = false;
25
+ this.rl = null;
26
+ this.lastScreen = '';
27
+ this.reconnectAttempts = 0;
28
+ this.maxReconnectAttempts = 3;
29
+ this.shouldReconnect = true;
30
+ }
31
+
32
+ async connect() {
33
+ return new Promise((resolve, reject) => {
34
+ // Build WebSocket URL
35
+ const wsUrl = this.buildWebSocketUrl();
36
+
37
+ logger.info('Connecting to remote shell...');
38
+
39
+ this.ws = new WebSocket(wsUrl, {
40
+ headers: {
41
+ 'X-Session-Token': this.token,
42
+ },
43
+ // Handle HTTPS/WSS
44
+ rejectUnauthorized: process.env.NODE_ENV === 'production'
45
+ });
46
+
47
+ // Connection timeout
48
+ const timeout = setTimeout(() => {
49
+ reject(new Error('Connection timeout'));
50
+ if (this.ws) {
51
+ this.ws.close();
52
+ }
53
+ }, 30000); // 30 second timeout for container creation
54
+
55
+ this.ws.on('open', () => {
56
+ clearTimeout(timeout);
57
+ this.connected = true;
58
+ this.reconnectAttempts = 0;
59
+ logger.success('Connected to remote container');
60
+ resolve();
61
+ });
62
+
63
+ this.ws.on('message', (data) => {
64
+ try {
65
+ const message = JSON.parse(data.toString());
66
+ this.handleMessage(message);
67
+ } catch (err) {
68
+ logger.error('Error parsing message:', err.message);
69
+ }
70
+ });
71
+
72
+ this.ws.on('close', (code, reason) => {
73
+ this.connected = false;
74
+ const reasonText = reason ? reason.toString() : '';
75
+
76
+ if (code === 1000) {
77
+ // Normal closure
78
+ logger.info('Disconnected from remote shell');
79
+ } else {
80
+ logger.warn(`Disconnected: ${code}${reasonText ? ' - ' + reasonText : ''}`);
81
+
82
+ if (this.shouldReconnect && code !== 4001 && code !== 4003) {
83
+ this.handleReconnect();
84
+ }
85
+ }
86
+ });
87
+
88
+ this.ws.on('error', (error) => {
89
+ clearTimeout(timeout);
90
+
91
+ // Check for specific error codes
92
+ if (error.message.includes('401') || error.message.includes('Unauthorized')) {
93
+ logger.error('Authentication failed');
94
+ reject(new Error('Authentication failed'));
95
+ } else if (error.message.includes('403') || error.message.includes('Forbidden')) {
96
+ logger.error('Access denied - you do not own this application');
97
+ reject(new Error('Access denied'));
98
+ } else {
99
+ logger.error('WebSocket error:', error.message);
100
+ reject(error);
101
+ }
102
+ });
103
+ });
104
+ }
105
+
106
+ buildWebSocketUrl() {
107
+ // Convert HTTP(S) URL to WS(S) URL
108
+ const apiUrl = this.serverUrl;
109
+ let wsUrl = apiUrl.replace(/^http:\/\//, 'ws://').replace(/^https:\/\//, 'wss://');
110
+
111
+ // Remove /api/v1 suffix if present
112
+ wsUrl = wsUrl.replace(/\/api\/v1$/, '');
113
+
114
+ // Build shell connect endpoint
115
+ wsUrl = `${wsUrl}/api/v1/shell/connect?app=${this.applicationUuid}&token=${this.token}`;
116
+
117
+ return wsUrl;
118
+ }
119
+
120
+ handleMessage(message) {
121
+ switch (message.type) {
122
+ case 'control':
123
+ this.handleControlMessage(message);
124
+ break;
125
+
126
+ case 'output':
127
+ this.handleOutputMessage(message);
128
+ break;
129
+
130
+ case 'pong':
131
+ // Heartbeat response
132
+ break;
133
+
134
+ default:
135
+ logger.warn('Unknown message type:', message.type);
136
+ }
137
+ }
138
+
139
+ handleControlMessage(message) {
140
+ if (message.action === 'session_ready') {
141
+ this.sessionId = message.data.session_id;
142
+ const status = message.data.status;
143
+
144
+ if (status === 'creating') {
145
+ logger.info('Container is being created, please wait...');
146
+ } else if (status === 'active') {
147
+ logger.success('Shell session ready!');
148
+ logger.info('Type commands below. Press Ctrl+D or type "exit" to quit.');
149
+ logger.newline();
150
+
151
+ // Start terminal interface
152
+ this.startTerminal();
153
+ }
154
+ } else if (message.action === 'session_active') {
155
+ logger.success('Container is ready!');
156
+ logger.info('Type commands below. Press Ctrl+D or type "exit" to quit.');
157
+ logger.newline();
158
+
159
+ // Start terminal interface
160
+ if (!this.rl) {
161
+ this.startTerminal();
162
+ }
163
+ } else if (message.action === 'error') {
164
+ logger.error('Server error:', message.data.message);
165
+ }
166
+ }
167
+
168
+ handleOutputMessage(message) {
169
+ const screen = message.data.screen;
170
+
171
+ // Only update if screen changed
172
+ if (screen !== this.lastScreen) {
173
+ // Clear current readline prompt
174
+ if (this.rl) {
175
+ readline.clearLine(process.stdout, 0);
176
+ readline.cursorTo(process.stdout, 0);
177
+ }
178
+
179
+ // Display full screen content
180
+ // For better UX, only show last 40 lines
181
+ const lines = screen.split('\n');
182
+ const displayLines = lines.slice(-40);
183
+
184
+ console.log(displayLines.join('\n'));
185
+
186
+ this.lastScreen = screen;
187
+ }
188
+
189
+ // Show prompt again
190
+ if (this.rl) {
191
+ this.rl.prompt(true);
192
+ }
193
+ }
194
+
195
+ startTerminal() {
196
+ // Create readline interface
197
+ this.rl = readline.createInterface({
198
+ input: process.stdin,
199
+ output: process.stdout,
200
+ prompt: '', // No prompt - we get it from the container
201
+ terminal: true,
202
+ historySize: 1000
203
+ });
204
+
205
+ // Handle user input
206
+ this.rl.on('line', (input) => {
207
+ const command = input.trim();
208
+
209
+ // Local exit command
210
+ if (command === 'exit' || command === 'quit') {
211
+ this.cleanup();
212
+ return;
213
+ }
214
+
215
+ // Send command to server
216
+ if (this.connected && this.ws.readyState === WebSocket.OPEN) {
217
+ this.sendCommand(command);
218
+ } else {
219
+ logger.error('Not connected to server');
220
+ }
221
+ });
222
+
223
+ // Handle Ctrl+C (SIGINT)
224
+ this.rl.on('SIGINT', () => {
225
+ // Send Ctrl+C to remote shell
226
+ if (this.connected) {
227
+ this.sendCommand('\x03'); // ASCII ETX (Ctrl+C)
31
228
  }
32
- // Cache expired
33
- envCache.delete(appUuid);
229
+ this.rl.prompt();
230
+ });
231
+
232
+ // Handle Ctrl+D (EOF) - exit
233
+ process.stdin.on('keypress', (str, key) => {
234
+ if (key && key.ctrl && key.name === 'd') {
235
+ this.cleanup();
236
+ }
237
+ });
238
+
239
+ // Enable keypress events
240
+ if (process.stdin.isTTY) {
241
+ process.stdin.setRawMode(false); // Keep cooked mode for line editing
242
+ }
243
+ }
244
+
245
+ sendCommand(command) {
246
+ if (!this.connected || this.ws.readyState !== WebSocket.OPEN) {
247
+ logger.error('Not connected to server');
248
+ return false;
249
+ }
250
+
251
+ const message = {
252
+ type: 'command',
253
+ data: {
254
+ command: command
255
+ }
256
+ };
257
+
258
+ this.ws.send(JSON.stringify(message));
259
+ return true;
260
+ }
261
+
262
+ async handleReconnect() {
263
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
264
+ logger.error('Max reconnection attempts reached. Exiting.');
265
+ this.cleanup();
266
+ return;
267
+ }
268
+
269
+ this.reconnectAttempts++;
270
+ const delay = 2000 * this.reconnectAttempts; // Exponential backoff
271
+
272
+ logger.info(`Reconnecting in ${delay/1000}s... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
273
+
274
+ await new Promise(resolve => setTimeout(resolve, delay));
275
+
276
+ try {
277
+ await this.connect();
278
+ } catch (err) {
279
+ logger.error('Reconnection failed:', err.message);
34
280
  }
35
281
  }
36
282
 
37
- // Fetch from API
38
- const client = api.createClient();
39
- const response = await client.get(`/applications/${appUuid}/env/export`);
283
+ cleanup() {
284
+ logger.newline();
285
+ logger.info('Disconnecting from remote shell...');
286
+
287
+ this.shouldReconnect = false;
288
+
289
+ if (this.rl) {
290
+ this.rl.close();
291
+ this.rl = null;
292
+ }
40
293
 
41
- // Cache for 5 minutes
42
- envCache.set(appUuid, {
43
- data: response.data,
44
- timestamp: Date.now()
45
- });
294
+ if (this.ws) {
295
+ this.ws.close(1000, 'Client closing');
296
+ this.ws = null;
297
+ }
298
+
299
+ // Restore terminal
300
+ if (process.stdin.isTTY) {
301
+ process.stdin.setRawMode(false);
302
+ }
46
303
 
47
- return response.data;
304
+ process.exit(0);
305
+ }
306
+
307
+ startHeartbeat() {
308
+ // Send ping every 30 seconds to keep connection alive
309
+ setInterval(() => {
310
+ if (this.connected && this.ws.readyState === WebSocket.OPEN) {
311
+ this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
312
+ }
313
+ }, 30000);
314
+ }
48
315
  }
49
316
 
50
317
  /**
51
- * Open interactive shell with remote environment variables
52
- * @param {object} options - Command options
318
+ * Shell command main function
53
319
  */
54
320
  async function shell(options = {}) {
55
321
  try {
@@ -72,93 +338,64 @@ async function shell(options = {}) {
72
338
 
73
339
  const { applicationUuid, applicationName } = projectConfig;
74
340
 
75
- // Fetch environment variables
76
- logger.newline();
77
- const spin = logger.spinner('Fetching environment variables...').start();
78
-
79
- let envData;
80
- try {
81
- envData = await getEnvironmentVariables(applicationUuid, options.sync);
82
- spin.succeed('Environment variables retrieved');
83
- } catch (error) {
84
- spin.fail('Failed to fetch environment variables');
85
-
86
- if (error.response?.status === 429) {
87
- const retryAfter = error.response.headers['retry-after'] || 60;
88
- logger.newline();
89
- logger.error(`Rate limit exceeded. Too many requests.`);
90
- logger.info(`Retry in ${retryAfter} seconds.`);
91
- logger.newline();
92
- logger.info('Note: Environment variables are cached for 5 minutes.');
93
- process.exit(1);
94
- }
341
+ // Get user and token
342
+ const user = getUser();
343
+ const token = user.sessionToken || user.apiKey;
95
344
 
96
- throw error;
345
+ if (!token) {
346
+ logger.error('No authentication token found. Please login again.');
347
+ process.exit(1);
97
348
  }
98
349
 
99
- logger.newline();
100
-
101
- // Determine shell to use
102
- const userShell = options.cmd || process.env.SHELL || '/bin/bash';
103
- const shellName = path.basename(userShell);
350
+ // Get server URL
351
+ const serverUrl = getApiUrl();
104
352
 
105
- // Display info
106
- logger.success(`🚀 Opening shell with ${envData.variable_count} environment variables loaded`);
353
+ // Show banner
107
354
  logger.newline();
108
- logger.field(' Application', applicationName);
109
- logger.field(' Shell', shellName);
110
- logger.field(' Variables', envData.variable_count);
355
+ logger.section(`Remote Shell: ${applicationName}`);
111
356
  logger.newline();
112
- logger.warn('⚠️ Secrets are exposed on local machine');
113
- logger.newline();
114
- logger.info('Type "exit" or press Ctrl+D to close the shell');
115
- logger.section('─'.repeat(60));
357
+ logger.info('Connecting to container...');
358
+ logger.info('This may take up to 30 seconds for container creation.');
116
359
  logger.newline();
117
360
 
118
- // Merge remote environment variables with current process env
119
- const mergedEnv = {
120
- ...process.env,
121
- ...envData.environment,
122
- SAAC_ENV_LOADED: '1',
123
- SAAC_APP_NAME: applicationName,
124
- SAAC_APP_UUID: applicationUuid
125
- };
361
+ // Create shell client
362
+ const client = new ShellClient(serverUrl, token, applicationUuid);
126
363
 
127
- // Determine shell arguments for interactive mode
128
- let shellArgs = [];
129
- if (shellName === 'bash' || shellName === 'sh') {
130
- shellArgs = ['-i']; // Interactive mode
131
- } else if (shellName === 'zsh') {
132
- shellArgs = ['-i']; // Interactive mode
133
- } else if (shellName === 'fish') {
134
- shellArgs = ['-i']; // Interactive mode
135
- }
136
- // If custom shell, try -i flag (most shells support it)
137
- else if (!userShell.includes('/')) {
138
- shellArgs = ['-i'];
139
- }
140
-
141
- // Spawn shell directly with merged environment
142
- const shellProc = spawn(userShell, shellArgs, {
143
- stdio: 'inherit',
144
- cwd: process.cwd(),
145
- env: mergedEnv
364
+ // Handle process termination
365
+ process.on('SIGTERM', () => {
366
+ client.cleanup();
146
367
  });
147
368
 
148
- shellProc.on('exit', (code) => {
149
- logger.newline();
150
- logger.success('✓ Shell closed, environment variables cleared');
151
- process.exit(code || 0);
369
+ process.on('SIGINT', () => {
370
+ // Let readline handle SIGINT
152
371
  });
153
372
 
154
- shellProc.on('error', (error) => {
155
- logger.newline();
156
- logger.error(`Failed to open shell: ${error.message}`);
373
+ // Connect to server
374
+ try {
375
+ await client.connect();
376
+
377
+ // Start heartbeat
378
+ client.startHeartbeat();
379
+
380
+ } catch (err) {
381
+ if (err.message === 'Authentication failed') {
382
+ logger.error('Authentication failed. Please login again:');
383
+ logger.log(' saac login');
384
+ } else if (err.message === 'Access denied') {
385
+ logger.error('Access denied. You do not own this application.');
386
+ } else if (err.message === 'Connection timeout') {
387
+ logger.error('Connection timeout. The server may be unavailable or the container failed to start.');
388
+ logger.info('Please try again later or check application status:');
389
+ logger.log(' saac status');
390
+ } else {
391
+ logger.error('Failed to connect:', err.message);
392
+ logger.info('Please check your network connection and try again.');
393
+ }
157
394
  process.exit(1);
158
- });
395
+ }
159
396
 
160
397
  } catch (error) {
161
- logger.error(error.response?.data?.message || error.message);
398
+ logger.error(error.message);
162
399
  process.exit(1);
163
400
  }
164
401
  }