@startanaicompany/cli 1.6.0 → 1.8.0

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/bin/saac.js CHANGED
@@ -205,8 +205,9 @@ program
205
205
 
206
206
  program
207
207
  .command('deploy')
208
- .description('Deploy current application')
209
- .option('-f, --force', 'Force deployment')
208
+ .description('Deploy current application (streams build logs by default)')
209
+ .option('--no-stream', 'Skip streaming and return immediately after queuing')
210
+ .option('--no-cache', 'Rebuild without Docker cache (slower but fresh build)')
210
211
  .action(deploy);
211
212
 
212
213
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Official CLI for StartAnAiCompany.com - Deploy AI recruitment sites with ease",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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;
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