@startanaicompany/cli 1.5.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
@@ -96,12 +96,6 @@ keysCommand
96
96
  .description('Generate a new API key (invalidates old one)')
97
97
  .action(keys.regenerate);
98
98
 
99
- keysCommand
100
- .command('show')
101
- .alias('info')
102
- .description('Show API key information')
103
- .action(keys.show);
104
-
105
99
  // Git OAuth commands
106
100
  const gitCommand = program
107
101
  .command('git')
@@ -150,6 +144,7 @@ program
150
144
  // Required options
151
145
  .option('-s, --subdomain <subdomain>', 'Subdomain')
152
146
  .option('-r, --repository <url>', 'Git repository URL (SSH format)')
147
+ .option('--org <organization_id>', 'Organization ID (required)')
153
148
  // Basic options
154
149
  .option('-b, --branch <branch>', 'Git branch', 'master')
155
150
  .option('-d, --domain-suffix <suffix>', 'Domain suffix', 'startanaicompany.com')
@@ -210,8 +205,9 @@ program
210
205
 
211
206
  program
212
207
  .command('deploy')
213
- .description('Deploy current application')
214
- .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)')
215
211
  .action(deploy);
216
212
 
217
213
  program
@@ -231,6 +227,7 @@ program
231
227
  .option('-t, --tail <lines>', 'Number of lines to show (runtime logs only)', '100')
232
228
  .option('-f, --follow', 'Follow log output (runtime logs only)')
233
229
  .option('--since <time>', 'Show logs since timestamp (runtime logs only)')
230
+ .option('--type <type>', 'Filter by log type: build, runtime, access (runtime logs only)')
234
231
  .action(logs);
235
232
 
236
233
  // Local development commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.5.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": {
@@ -89,6 +89,7 @@ async function create(name, options) {
89
89
  logger.info('Required options:');
90
90
  logger.log(' -s, --subdomain <subdomain> Subdomain for your app');
91
91
  logger.log(' -r, --repository <url> Git repository URL (SSH format)');
92
+ logger.log(' --org <organization_id> Organization ID');
92
93
  logger.newline();
93
94
  logger.info('Optional options:');
94
95
  logger.log(' -b, --branch <branch> Git branch (default: master)');
@@ -110,17 +111,17 @@ async function create(name, options) {
110
111
  logger.log(' --env <KEY=VALUE> Environment variable (can be used multiple times)');
111
112
  logger.newline();
112
113
  logger.info('Example:');
113
- logger.log(' saac create my-app -s myapp -r git@git.startanaicompany.com:user/repo.git');
114
- logger.log(' saac create api -s api -r git@git... --build-pack nixpacks --port 8080');
115
- logger.log(' saac create web -s web -r git@git... --health-check --pre-deploy-cmd "npm run migrate"');
114
+ logger.log(' saac create my-app -s myapp -r git@git.startanaicompany.com:user/repo.git --org <org_id>');
115
+ logger.log(' saac create api -s api -r git@git... --org <org_id> --build-pack nixpacks --port 8080');
116
+ logger.log(' saac create web -s web -r git@git... --org <org_id> --health-check --pre-deploy-cmd "npm run migrate"');
116
117
  process.exit(1);
117
118
  }
118
119
 
119
- if (!options.subdomain || !options.repository) {
120
- logger.error('Missing required options: subdomain and repository are required');
120
+ if (!options.subdomain || !options.repository || !options.org) {
121
+ logger.error('Missing required options: subdomain, repository, and organization ID are required');
121
122
  logger.newline();
122
123
  logger.info('Example:');
123
- logger.log(` saac create ${name} -s myapp -r git@git.startanaicompany.com:user/repo.git`);
124
+ logger.log(` saac create ${name} -s myapp -r git@git.startanaicompany.com:user/repo.git --org <org_id>`);
124
125
  logger.newline();
125
126
  logger.info('Note: Git OAuth connection required. Connect with: saac git connect');
126
127
  process.exit(1);
@@ -156,6 +157,7 @@ async function create(name, options) {
156
157
  domain_suffix: options.domainSuffix || 'startanaicompany.com',
157
158
  git_repository: options.repository,
158
159
  git_branch: options.branch || 'master',
160
+ organization_id: options.org,
159
161
  };
160
162
 
161
163
  // OAuth tokens are retrieved from database by wrapper
@@ -113,7 +113,7 @@ async function deleteApp(options) {
113
113
 
114
114
  logger.newline();
115
115
 
116
- logger.success(`Application '${result.application_name}' has been permanently deleted.`);
116
+ logger.success(`Application '${app.name}' has been permanently deleted.`);
117
117
 
118
118
  logger.newline();
119
119
 
@@ -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;
@@ -87,61 +87,6 @@ async function regenerate() {
87
87
  }
88
88
  }
89
89
 
90
- /**
91
- * Show API key info (without revealing full key)
92
- */
93
- async function show() {
94
- try {
95
- if (!(await ensureAuthenticated())) {
96
- logger.error('Not logged in');
97
- logger.newline();
98
- logger.info('Login first:');
99
- logger.log(' saac login -e <email>');
100
- process.exit(1);
101
- }
102
-
103
- logger.section('API Key Information');
104
- logger.newline();
105
-
106
- const spin = logger.spinner('Fetching API key info...').start();
107
-
108
- try {
109
- const result = await api.getApiKeyInfo();
110
-
111
- spin.succeed('API key info retrieved');
112
-
113
- logger.newline();
114
- logger.field('Key Prefix', result.key_prefix); // e.g., "cw_RJ1gH8..."
115
- logger.field('Created', new Date(result.created_at).toLocaleDateString());
116
- logger.field('Last Used', result.last_used_at
117
- ? new Date(result.last_used_at).toLocaleString()
118
- : 'Never');
119
-
120
- logger.newline();
121
- logger.info('Commands:');
122
- logger.log(' saac keys regenerate Generate new API key');
123
- logger.log(' saac sessions View active sessions');
124
-
125
- } catch (error) {
126
- spin.fail('Failed to fetch API key info');
127
-
128
- // If endpoint doesn't exist yet, show helpful message
129
- if (error.response?.status === 404) {
130
- logger.newline();
131
- logger.warn('API key info endpoint not available yet');
132
- logger.info('You can still regenerate your key with:');
133
- logger.log(' saac keys regenerate');
134
- } else {
135
- throw error;
136
- }
137
- }
138
- } catch (error) {
139
- logger.error(error.response?.data?.message || error.message);
140
- process.exit(1);
141
- }
142
- }
143
-
144
90
  module.exports = {
145
91
  regenerate,
146
- show,
147
92
  };
@@ -168,20 +168,26 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
168
168
  logger.section(`Runtime Logs: ${applicationName}`);
169
169
  logger.newline();
170
170
 
171
+ // Follow mode - use SSE streaming
172
+ if (options.follow) {
173
+ return await streamRuntimeLogs(applicationUuid, applicationName, options);
174
+ }
175
+
176
+ // Regular mode - fetch logs once
171
177
  const spin = logger.spinner('Fetching runtime logs...').start();
172
178
 
173
179
  try {
174
180
  // Build query parameters
175
181
  const params = {};
176
- if (options.follow) {
177
- params.follow = true;
178
- }
179
182
  if (options.tail) {
180
183
  params.tail = parseInt(options.tail, 10);
181
184
  }
182
185
  if (options.since) {
183
186
  params.since = options.since;
184
187
  }
188
+ if (options.type) {
189
+ params.type = options.type;
190
+ }
185
191
 
186
192
  const result = await api.getApplicationLogs(applicationUuid, params);
187
193
 
@@ -192,9 +198,16 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
192
198
  // Display logs
193
199
  if (result.logs) {
194
200
  if (Array.isArray(result.logs)) {
195
- // Logs is an array
201
+ // Logs is an array of objects
196
202
  result.logs.forEach(log => {
197
- console.log(log);
203
+ if (log.message) {
204
+ // Format: [timestamp] [service] message
205
+ const timestamp = new Date(log.timestamp).toLocaleTimeString();
206
+ const service = logger.chalk.cyan(`[${log.service}]`);
207
+ console.log(`${logger.chalk.gray(timestamp)} ${service} ${log.message}`);
208
+ } else {
209
+ console.log(log);
210
+ }
198
211
  });
199
212
  } else if (typeof result.logs === 'string') {
200
213
  // Logs is a string (most common format from backend)
@@ -212,13 +225,6 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
212
225
  logger.log(' saac deploy');
213
226
  }
214
227
 
215
- // Note about follow mode
216
- if (options.follow) {
217
- logger.newline();
218
- logger.info('Note: Follow mode (--follow) for live logs is not yet implemented');
219
- logger.info('This command shows recent logs only');
220
- }
221
-
222
228
  } catch (error) {
223
229
  spin.fail('Failed to fetch runtime logs');
224
230
 
@@ -240,4 +246,123 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
240
246
  }
241
247
  }
242
248
 
249
+ /**
250
+ * Stream runtime logs via SSE (Server-Sent Events)
251
+ */
252
+ async function streamRuntimeLogs(applicationUuid, applicationName, options) {
253
+ const { getUser } = require('../lib/config');
254
+ const user = getUser();
255
+ const config = require('../lib/config');
256
+
257
+ // Get base URL from config
258
+ const baseUrl = config.getApiUrl();
259
+
260
+ // Build query parameters
261
+ const params = new URLSearchParams();
262
+ params.set('follow', 'true');
263
+ if (options.tail) {
264
+ params.set('tail', options.tail);
265
+ }
266
+ if (options.since) {
267
+ params.set('since', options.since);
268
+ }
269
+ if (options.type) {
270
+ params.set('type', options.type);
271
+ }
272
+
273
+ const url = `${baseUrl}/applications/${applicationUuid}/logs?${params.toString()}`;
274
+
275
+ logger.info('Streaming live logs... (Press Ctrl+C to stop)');
276
+ logger.newline();
277
+
278
+ try {
279
+ const headers = {
280
+ 'Accept': 'text/event-stream',
281
+ };
282
+
283
+ // Add authentication header
284
+ if (process.env.SAAC_API_KEY) {
285
+ headers['X-API-Key'] = process.env.SAAC_API_KEY;
286
+ } else if (user.sessionToken) {
287
+ headers['X-Session-Token'] = user.sessionToken;
288
+ } else if (user.apiKey) {
289
+ headers['X-API-Key'] = user.apiKey;
290
+ }
291
+
292
+ const response = await fetch(url, { headers });
293
+
294
+ if (!response.ok) {
295
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
296
+ }
297
+
298
+ if (!response.body) {
299
+ throw new Error('Response body is null');
300
+ }
301
+
302
+ const reader = response.body.getReader();
303
+ const decoder = new TextDecoder();
304
+ let buffer = '';
305
+
306
+ // Handle Ctrl+C gracefully
307
+ const cleanup = () => {
308
+ reader.cancel();
309
+ logger.newline();
310
+ logger.info('Stream closed');
311
+ process.exit(0);
312
+ };
313
+ process.on('SIGINT', cleanup);
314
+ process.on('SIGTERM', cleanup);
315
+
316
+ while (true) {
317
+ const { done, value } = await reader.read();
318
+
319
+ if (done) {
320
+ break;
321
+ }
322
+
323
+ buffer += decoder.decode(value, { stream: true });
324
+ const lines = buffer.split('\n');
325
+ buffer = lines.pop() || '';
326
+
327
+ for (const line of lines) {
328
+ // Skip empty lines and comments (keepalive)
329
+ if (!line.trim() || line.startsWith(':')) {
330
+ continue;
331
+ }
332
+
333
+ // Parse SSE data lines
334
+ if (line.startsWith('data: ')) {
335
+ try {
336
+ const data = JSON.parse(line.slice(6));
337
+
338
+ // Skip connection event
339
+ if (data.event === 'connected') {
340
+ logger.success('Connected to log stream');
341
+ logger.newline();
342
+ continue;
343
+ }
344
+
345
+ // Display log message
346
+ if (data.message) {
347
+ const timestamp = new Date(data.timestamp).toLocaleTimeString();
348
+ const service = logger.chalk.cyan(`[${data.service}]`);
349
+ console.log(`${logger.chalk.gray(timestamp)} ${service} ${data.message}`);
350
+ }
351
+ } catch (parseError) {
352
+ logger.warn(`Failed to parse log entry: ${line}`);
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ logger.newline();
359
+ logger.info('Stream ended');
360
+
361
+ } catch (error) {
362
+ logger.error('Failed to stream logs');
363
+ logger.error(error.message);
364
+ process.exit(1);
365
+ }
366
+ }
367
+
243
368
  module.exports = logs;
@@ -33,7 +33,7 @@ async function whoami() {
33
33
 
34
34
  logger.field('Email', user.email);
35
35
  logger.field('User ID', user.id);
36
- logger.field('Verified', user.verified ? logger.chalk.green('Yes ✓') : logger.chalk.red('No ✗'));
36
+ logger.field('Verified', user.email_verified ? logger.chalk.green('Yes ✓') : logger.chalk.red('No ✗'));
37
37
  logger.field('Member Since', formatDate(user.created_at));
38
38
 
39
39
  logger.newline();
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