@startanaicompany/cli 1.6.0 → 1.9.1

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.
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Deploy command
2
+ * Deploy command with streaming support
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { getProjectConfig, ensureAuthenticated } = require('../lib/config');
6
+ const { getProjectConfig, ensureAuthenticated, getUser } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
  const errorDisplay = require('../lib/errorDisplay');
9
9
 
@@ -30,57 +30,55 @@ async function deploy(options) {
30
30
  logger.section(`Deploying ${applicationName}`);
31
31
  logger.newline();
32
32
 
33
- const spin = logger.spinner('Deploying application (waiting for completion, up to 5 minutes)...').start();
33
+ // Default to streaming mode (agents and users need visibility)
34
+ // Use fire-and-forget mode only if --no-stream is explicitly set
35
+ if (options.stream !== false) {
36
+ return await deployWithStreaming(applicationUuid, applicationName, options);
37
+ }
38
+
39
+ // Fire-and-forget mode (only when --no-stream is used)
40
+ const spin = logger.spinner('Queueing deployment...').start();
34
41
 
35
42
  try {
36
- const result = await api.deployApplication(applicationUuid);
43
+ const deployOptions = {};
44
+ if (options.noCache) {
45
+ deployOptions.no_cache = true;
46
+ }
47
+
48
+ const result = await api.deployApplication(applicationUuid, deployOptions);
37
49
 
38
50
  // Check if deployment failed
39
51
  if (result.success === false) {
40
52
  spin.fail('Deployment failed');
41
-
42
- // Display detailed error information
43
53
  errorDisplay.displayDeploymentError(result, logger);
44
-
45
- // Handle timeout specifically
46
- if (result.status === 'timeout') {
47
- errorDisplay.displayTimeoutInstructions(logger);
48
- }
49
-
50
54
  process.exit(1);
51
55
  }
52
56
 
53
- // SUCCESS: Deployment completed
54
- spin.succeed('Deployment completed successfully!');
57
+ // SUCCESS: Deployment queued
58
+ spin.succeed('Deployment queued');
55
59
 
56
60
  logger.newline();
57
- logger.success('Your application has been deployed!');
61
+ logger.success('Deployment has been queued!');
58
62
  logger.newline();
59
63
  logger.field('Application', applicationName);
60
- logger.field('Status', result.status);
64
+ logger.field('Status', 'queued (daemon will build within 30 seconds)');
61
65
  if (result.git_branch) {
62
66
  logger.field('Branch', result.git_branch);
63
67
  }
64
68
  if (result.domain) {
65
69
  logger.field('Domain', result.domain);
66
70
  }
67
- if (result.deployment_uuid || result.deployment_id) {
68
- logger.field('Deployment ID', result.deployment_uuid || result.deployment_id);
71
+ if (options.noCache) {
72
+ logger.field('Build Mode', 'No cache (full rebuild)');
69
73
  }
70
74
  logger.newline();
71
75
 
72
- // Show Traefik status if present
73
- if (result.traefik_status === 'queued') {
74
- logger.info('Routing configuration is being applied (may take a few seconds)');
75
- logger.newline();
76
- } else if (result.traefik_status === 'failed') {
77
- logger.warn('Routing configuration failed - application may not be accessible');
78
- logger.newline();
79
- }
80
-
81
- logger.info('Useful commands:');
82
- logger.log(` saac logs --follow View live deployment logs`);
83
- logger.log(` saac status Check application status`);
76
+ logger.info('The daemon will pick up this deployment shortly and begin building.');
77
+ logger.newline();
78
+ logger.info('Monitor deployment progress:');
79
+ logger.log(` saac deploy Stream build logs in real-time (default)`);
80
+ logger.log(` saac logs --deployment View deployment logs after completion`);
81
+ logger.log(` saac status Check application status`);
84
82
 
85
83
  } catch (error) {
86
84
  spin.fail('Deployment request failed');
@@ -92,4 +90,170 @@ async function deploy(options) {
92
90
  }
93
91
  }
94
92
 
93
+ /**
94
+ * Deploy with SSE streaming
95
+ */
96
+ async function deployWithStreaming(applicationUuid, applicationName, options) {
97
+ const user = getUser();
98
+ const config = require('../lib/config');
99
+ const baseUrl = config.getApiUrl();
100
+
101
+ logger.info('Initiating deployment with build log streaming...');
102
+ logger.newline();
103
+
104
+ try {
105
+ const headers = {
106
+ 'Accept': 'text/event-stream',
107
+ 'Content-Type': 'application/json',
108
+ };
109
+
110
+ // Add authentication header
111
+ if (process.env.SAAC_API_KEY) {
112
+ headers['X-API-Key'] = process.env.SAAC_API_KEY;
113
+ } else if (user.sessionToken) {
114
+ headers['X-Session-Token'] = user.sessionToken;
115
+ } else if (user.apiKey) {
116
+ headers['X-API-Key'] = user.apiKey;
117
+ }
118
+
119
+ const body = { stream: true };
120
+ if (options.noCache) {
121
+ body.no_cache = true;
122
+ }
123
+
124
+ const url = `${baseUrl}/applications/${applicationUuid}/deploy`;
125
+ const response = await fetch(url, {
126
+ method: 'POST',
127
+ headers,
128
+ body: JSON.stringify(body),
129
+ });
130
+
131
+ if (!response.ok) {
132
+ const errorText = await response.text();
133
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
134
+ }
135
+
136
+ if (!response.body) {
137
+ throw new Error('Response body is null');
138
+ }
139
+
140
+ const reader = response.body.getReader();
141
+ const decoder = new TextDecoder();
142
+ let buffer = '';
143
+ let deploymentQueued = false;
144
+
145
+ // Handle Ctrl+C gracefully
146
+ const cleanup = () => {
147
+ reader.cancel();
148
+ logger.newline();
149
+ logger.info('Stream closed');
150
+ process.exit(0);
151
+ };
152
+ process.on('SIGINT', cleanup);
153
+ process.on('SIGTERM', cleanup);
154
+
155
+ while (true) {
156
+ const { done, value } = await reader.read();
157
+
158
+ if (done) {
159
+ break;
160
+ }
161
+
162
+ buffer += decoder.decode(value, { stream: true });
163
+ const lines = buffer.split('\n');
164
+ buffer = lines.pop() || '';
165
+
166
+ for (const line of lines) {
167
+ // Skip empty lines and comments (keepalive)
168
+ if (!line.trim() || line.startsWith(':')) {
169
+ continue;
170
+ }
171
+
172
+ // Parse SSE data lines
173
+ if (line.startsWith('data: ')) {
174
+ try {
175
+ const data = JSON.parse(line.slice(6));
176
+
177
+ // Handle deploy_queued event
178
+ if (data.event === 'deploy_queued') {
179
+ logger.success('✓ Deployment queued');
180
+ logger.newline();
181
+ logger.field('Application', applicationName);
182
+ logger.field('Branch', data.git_branch || 'master');
183
+ if (data.domain) {
184
+ logger.field('Domain', data.domain);
185
+ }
186
+ if (options.noCache) {
187
+ logger.field('Build Mode', 'No cache (full rebuild)');
188
+ }
189
+ logger.newline();
190
+ logger.info('Waiting for daemon to start build...');
191
+ logger.newline();
192
+ deploymentQueued = true;
193
+ continue;
194
+ }
195
+
196
+ // Handle deploy_finished event
197
+ if (data.event === 'deploy_finished') {
198
+ logger.newline();
199
+ logger.log('─'.repeat(60));
200
+ logger.newline();
201
+
202
+ if (data.status === 'running') {
203
+ logger.success('✓ Deployment completed successfully!');
204
+ } else if (data.status === 'failed') {
205
+ logger.error('✗ Deployment failed');
206
+ } else {
207
+ logger.info(`Deployment status: ${data.status}`);
208
+ }
209
+
210
+ logger.newline();
211
+ logger.field('Final Status', data.status);
212
+ if (data.deployment_uuid) {
213
+ logger.field('Deployment UUID', data.deployment_uuid);
214
+ }
215
+ logger.newline();
216
+
217
+ if (data.status === 'running') {
218
+ logger.info('Your application is now running!');
219
+ logger.newline();
220
+ logger.info('Next steps:');
221
+ logger.log(` saac status Check application status`);
222
+ logger.log(` saac logs --follow View live application logs`);
223
+ } else if (data.status === 'failed') {
224
+ logger.info('View full deployment logs:');
225
+ logger.log(` saac logs --deployment View complete build logs`);
226
+ }
227
+
228
+ // Clean exit
229
+ process.removeListener('SIGINT', cleanup);
230
+ process.removeListener('SIGTERM', cleanup);
231
+ process.exit(data.status === 'running' ? 0 : 1);
232
+ }
233
+
234
+ // Handle build log messages
235
+ if (data.type === 'build' && data.message) {
236
+ const timestamp = new Date(data.timestamp).toLocaleTimeString();
237
+ const service = logger.chalk.cyan(`[${data.service}]`);
238
+ console.log(`${logger.chalk.gray(timestamp)} ${service} ${data.message}`);
239
+ }
240
+ } catch (parseError) {
241
+ logger.warn(`Failed to parse event: ${line}`);
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ // Stream ended without deploy_finished event
248
+ logger.newline();
249
+ logger.warn('Build stream ended unexpectedly');
250
+ logger.info('Check deployment status with: saac status');
251
+
252
+ } catch (error) {
253
+ logger.error('Failed to stream deployment');
254
+ logger.error(error.message);
255
+ process.exit(1);
256
+ }
257
+ }
258
+
95
259
  module.exports = deploy;
@@ -7,6 +7,45 @@ const { getProjectConfig, ensureAuthenticated } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
  const { table } = require('table');
9
9
 
10
+ /**
11
+ * Poll for command result with timeout
12
+ */
13
+ async function pollForResult(applicationUuid, commandId, maxWaitSeconds = 120) {
14
+ const startTime = Date.now();
15
+ const pollInterval = 1000; // 1 second
16
+
17
+ while (true) {
18
+ const elapsed = (Date.now() - startTime) / 1000;
19
+
20
+ if (elapsed > maxWaitSeconds) {
21
+ throw new Error(`Command timed out after ${maxWaitSeconds} seconds`);
22
+ }
23
+
24
+ try {
25
+ const result = await api.getDbCommandResult(applicationUuid, 'exec', commandId);
26
+
27
+ if (result.status === 'completed') {
28
+ return result;
29
+ }
30
+
31
+ if (result.status === 'failed') {
32
+ const errorMsg = result.result?.error || result.error || 'Command failed';
33
+ throw new Error(errorMsg);
34
+ }
35
+
36
+ // Still pending, wait and retry
37
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
38
+ } catch (error) {
39
+ // If error is not a 404 (command not found yet), rethrow
40
+ if (error.response?.status !== 404) {
41
+ throw error;
42
+ }
43
+ // 404 means command not processed yet, keep polling
44
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
45
+ }
46
+ }
47
+ }
48
+
10
49
  /**
11
50
  * Execute a command in the remote container
12
51
  * @param {string} command - Command to execute
@@ -39,7 +78,7 @@ async function exec(command, options = {}) {
39
78
  const execRequest = {
40
79
  command,
41
80
  workdir: options.workdir || '/app',
42
- timeout: options.timeout || 30
81
+ timeout: parseInt(options.timeout) || 30
43
82
  };
44
83
 
45
84
  // Validate timeout
@@ -56,11 +95,20 @@ async function exec(command, options = {}) {
56
95
  logger.field(' Timeout', `${execRequest.timeout}s`);
57
96
  logger.newline();
58
97
 
59
- const spin = logger.spinner('Executing command in container...').start();
98
+ const spin = logger.spinner('Queueing command...').start();
60
99
 
100
+ let response;
61
101
  let result;
62
102
  try {
63
- result = await api.executeCommand(applicationUuid, execRequest);
103
+ // Queue the command
104
+ response = await api.executeCommand(applicationUuid, execRequest);
105
+ const commandId = response.command_id;
106
+
107
+ spin.text = 'Waiting for daemon to execute command...';
108
+
109
+ // Poll for result with timeout buffer
110
+ result = await pollForResult(applicationUuid, commandId, execRequest.timeout + 30);
111
+
64
112
  spin.succeed('Command executed');
65
113
  } catch (error) {
66
114
  spin.fail('Command execution failed');
@@ -105,37 +153,50 @@ async function exec(command, options = {}) {
105
153
  logger.newline();
106
154
 
107
155
  // Display execution results
108
- logger.success(`✓ Execution ID: ${result.execution_id}`);
109
- logger.newline();
156
+ const execResult = result.result || {};
157
+
158
+ // Calculate duration
159
+ let duration = 'N/A';
160
+ if (result.created_at && result.completed_at) {
161
+ const start = new Date(result.created_at);
162
+ const end = new Date(result.completed_at);
163
+ duration = `${end - start}ms`;
164
+ }
110
165
 
111
- logger.field('Exit Code', result.exit_code === 0
112
- ? logger.chalk.green(result.exit_code)
113
- : logger.chalk.red(result.exit_code)
166
+ logger.field('Exit Code', execResult.exit_code !== undefined
167
+ ? (execResult.exit_code === 0
168
+ ? logger.chalk.green(execResult.exit_code)
169
+ : logger.chalk.red(execResult.exit_code))
170
+ : 'N/A'
114
171
  );
115
- logger.field('Duration', `${result.duration_ms}ms`);
116
- logger.field('Started', new Date(result.started_at).toLocaleString());
117
- logger.field('Completed', new Date(result.completed_at).toLocaleString());
172
+ logger.field('Duration', duration);
173
+ if (result.created_at) {
174
+ logger.field('Started', new Date(result.created_at).toLocaleString());
175
+ }
176
+ if (result.completed_at) {
177
+ logger.field('Completed', new Date(result.completed_at).toLocaleString());
178
+ }
118
179
 
119
180
  // Display stdout
120
- if (result.stdout) {
181
+ if (execResult.stdout) {
121
182
  logger.newline();
122
183
  logger.info('Standard Output:');
123
184
  logger.section('─'.repeat(60));
124
- console.log(result.stdout.trim());
185
+ console.log(execResult.stdout.trim());
125
186
  logger.section('─'.repeat(60));
126
187
  }
127
188
 
128
189
  // Display stderr
129
- if (result.stderr) {
190
+ if (execResult.stderr) {
130
191
  logger.newline();
131
192
  logger.warn('Standard Error:');
132
193
  logger.section('─'.repeat(60));
133
- console.error(result.stderr.trim());
194
+ console.error(execResult.stderr.trim());
134
195
  logger.section('─'.repeat(60));
135
196
  }
136
197
 
137
198
  // If no output
138
- if (!result.stdout && !result.stderr) {
199
+ if (!execResult.stdout && !execResult.stderr) {
139
200
  logger.newline();
140
201
  logger.info('(No output)');
141
202
  }
package/src/lib/api.js CHANGED
@@ -120,10 +120,10 @@ async function getApplication(uuid) {
120
120
  * Deploy application
121
121
  * Note: This waits for deployment to complete (up to 5 minutes)
122
122
  */
123
- async function deployApplication(uuid) {
123
+ async function deployApplication(uuid, options = {}) {
124
124
  // Use 5-minute timeout for deployment waiting
125
125
  const client = createClient(300000); // 5 minutes
126
- const response = await client.post(`/applications/${uuid}/deploy`);
126
+ const response = await client.post(`/applications/${uuid}/deploy`, options);
127
127
  return response.data;
128
128
  }
129
129
 
@@ -319,6 +319,65 @@ async function listGitRepositories(gitHost, options = {}) {
319
319
  return response.data;
320
320
  }
321
321
 
322
+ /**
323
+ * List database containers for an application
324
+ * @param {string} uuid - Application UUID
325
+ * @returns {Promise<object>} - { command_id, status }
326
+ */
327
+ async function listDbContainers(uuid) {
328
+ const client = createClient();
329
+ const response = await client.get(`/applications/${uuid}/db/containers`);
330
+ return response.data;
331
+ }
332
+
333
+ /**
334
+ * Execute SQL query on application database
335
+ * @param {string} uuid - Application UUID
336
+ * @param {object} queryData - { query, db_name?, allow_writes? }
337
+ * @returns {Promise<object>} - { command_id, status }
338
+ */
339
+ async function executeSql(uuid, queryData) {
340
+ const client = createClient();
341
+ const response = await client.post(`/applications/${uuid}/db/sql`, queryData);
342
+ return response.data;
343
+ }
344
+
345
+ /**
346
+ * Get result of a database command (universal endpoint for all command types)
347
+ * @param {string} uuid - Application UUID
348
+ * @param {string} commandType - 'sql', 'redis', 'containers' (not used, kept for API compatibility)
349
+ * @param {string} commandId - Command ID
350
+ * @returns {Promise<object>} - { status, result, command_type, created_at, completed_at }
351
+ */
352
+ async function getDbCommandResult(uuid, commandType, commandId) {
353
+ const client = createClient();
354
+ const response = await client.get(`/applications/${uuid}/db/result/${commandId}`);
355
+ return response.data;
356
+ }
357
+
358
+ /**
359
+ * Execute Redis command on application database
360
+ * @param {string} uuid - Application UUID
361
+ * @param {object} commandData - { command }
362
+ * @returns {Promise<object>} - { command_id, status }
363
+ */
364
+ async function executeRedis(uuid, commandData) {
365
+ const client = createClient();
366
+ const response = await client.post(`/applications/${uuid}/db/redis`, commandData);
367
+ return response.data;
368
+ }
369
+
370
+ /**
371
+ * Get database connection information
372
+ * @param {string} uuid - Application UUID
373
+ * @returns {Promise<object>} - { postgres: {...}, redis: {...} }
374
+ */
375
+ async function getDbInfo(uuid) {
376
+ const client = createClient();
377
+ const response = await client.get(`/applications/${uuid}/db/info`);
378
+ return response.data;
379
+ }
380
+
322
381
  module.exports = {
323
382
  createClient,
324
383
  login,
@@ -345,4 +404,9 @@ module.exports = {
345
404
  executeCommand,
346
405
  getExecutionHistory,
347
406
  listGitRepositories,
407
+ listDbContainers,
408
+ executeSql,
409
+ getDbCommandResult,
410
+ executeRedis,
411
+ getDbInfo,
348
412
  };