@startanaicompany/cli 1.4.21 → 1.6.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.
@@ -3,14 +3,16 @@
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { getProjectConfig, isAuthenticated } = require('../lib/config');
6
+ const { getProjectConfig, ensureAuthenticated } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
 
9
9
  async function logs(deploymentUuidArg, options) {
10
10
  try {
11
11
  // Check authentication
12
- if (!isAuthenticated()) {
13
- logger.error('Not logged in. Run: saac login');
12
+ if (!(await ensureAuthenticated())) {
13
+ logger.error('Not logged in');
14
+ logger.info('Run: saac login -e <email> -k <api-key>');
15
+ logger.info('Or set: SAAC_USER_API_KEY and SAAC_USER_EMAIL');
14
16
  process.exit(1);
15
17
  }
16
18
 
@@ -166,20 +168,26 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
166
168
  logger.section(`Runtime Logs: ${applicationName}`);
167
169
  logger.newline();
168
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
169
177
  const spin = logger.spinner('Fetching runtime logs...').start();
170
178
 
171
179
  try {
172
180
  // Build query parameters
173
181
  const params = {};
174
- if (options.follow) {
175
- params.follow = true;
176
- }
177
182
  if (options.tail) {
178
183
  params.tail = parseInt(options.tail, 10);
179
184
  }
180
185
  if (options.since) {
181
186
  params.since = options.since;
182
187
  }
188
+ if (options.type) {
189
+ params.type = options.type;
190
+ }
183
191
 
184
192
  const result = await api.getApplicationLogs(applicationUuid, params);
185
193
 
@@ -190,9 +198,16 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
190
198
  // Display logs
191
199
  if (result.logs) {
192
200
  if (Array.isArray(result.logs)) {
193
- // Logs is an array
201
+ // Logs is an array of objects
194
202
  result.logs.forEach(log => {
195
- 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
+ }
196
211
  });
197
212
  } else if (typeof result.logs === 'string') {
198
213
  // Logs is a string (most common format from backend)
@@ -210,13 +225,6 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
210
225
  logger.log(' saac deploy');
211
226
  }
212
227
 
213
- // Note about follow mode
214
- if (options.follow) {
215
- logger.newline();
216
- logger.info('Note: Follow mode (--follow) for live logs is not yet implemented');
217
- logger.info('This command shows recent logs only');
218
- }
219
-
220
228
  } catch (error) {
221
229
  spin.fail('Failed to fetch runtime logs');
222
230
 
@@ -238,4 +246,123 @@ async function getRuntimeLogs(applicationUuid, applicationName, options) {
238
246
  }
239
247
  }
240
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
+
241
368
  module.exports = logs;
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { getProjectConfig, isAuthenticated } = require('../lib/config');
6
+ const { getProjectConfig, ensureAuthenticated } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
  const { spawn } = require('child_process');
9
9
  const fs = require('fs');
@@ -55,8 +55,10 @@ async function getEnvironmentVariables(appUuid, forceRefresh = false) {
55
55
  async function run(command, options = {}) {
56
56
  try {
57
57
  // Check authentication
58
- if (!isAuthenticated()) {
59
- logger.error('Not logged in. Run: saac login');
58
+ if (!(await ensureAuthenticated())) {
59
+ logger.error('Not logged in');
60
+ logger.info('Run: saac login -e <email> -k <api-key>');
61
+ logger.info('Or set: SAAC_USER_API_KEY and SAAC_USER_EMAIL');
60
62
  process.exit(1);
61
63
  }
62
64
 
@@ -3,18 +3,17 @@
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { isAuthenticated } = require('../lib/config');
6
+ const { ensureAuthenticated } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
  const { table } = require('table');
9
9
 
10
10
  async function sessions() {
11
11
  try {
12
12
  // Check authentication
13
- if (!isAuthenticated()) {
13
+ if (!(await ensureAuthenticated())) {
14
14
  logger.error('Not logged in');
15
- logger.newline();
16
- logger.info('Run:');
17
- logger.log(' saac login -e <email> -k <api-key>');
15
+ logger.info('Run: saac login -e <email> -k <api-key>');
16
+ logger.info('Or set: SAAC_USER_API_KEY and SAAC_USER_EMAIL');
18
17
  process.exit(1);
19
18
  }
20
19
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  const WebSocket = require('ws');
9
9
  const readline = require('readline');
10
- const { getProjectConfig, isAuthenticated, getUser, getApiUrl } = require('../lib/config');
10
+ const { getProjectConfig, ensureAuthenticated, getUser, getApiUrl } = require('../lib/config');
11
11
  const logger = require('../lib/logger');
12
12
 
13
13
  /**
@@ -320,8 +320,10 @@ class ShellClient {
320
320
  async function shell(options = {}) {
321
321
  try {
322
322
  // Check authentication
323
- if (!isAuthenticated()) {
324
- logger.error('Not logged in. Run: saac login');
323
+ if (!(await ensureAuthenticated())) {
324
+ logger.error('Not logged in');
325
+ logger.info('Run: saac login -e <email> -k <api-key>');
326
+ logger.info('Or set: SAAC_USER_API_KEY and SAAC_USER_EMAIL');
325
327
  process.exit(1);
326
328
  }
327
329
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { getUser, isAuthenticated, isTokenExpiringSoon } = require('../lib/config');
6
+ const { getUser, ensureAuthenticated, isTokenExpiringSoon } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
  const { table } = require('table');
9
9
 
@@ -13,11 +13,10 @@ async function status() {
13
13
  logger.newline();
14
14
 
15
15
  // Check if logged in locally (silently)
16
- if (!isAuthenticated()) {
16
+ if (!(await ensureAuthenticated())) {
17
17
  logger.error('Not logged in');
18
- logger.newline();
19
- logger.info('Run:');
20
- logger.log(' saac login -e <email> -k <api-key>');
18
+ logger.info('Run: saac login -e <email> -k <api-key>');
19
+ logger.info('Or set: SAAC_USER_API_KEY and SAAC_USER_EMAIL');
21
20
  process.exit(1);
22
21
  }
23
22
 
@@ -3,17 +3,16 @@
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { isAuthenticated, getProjectConfig } = require('../lib/config');
6
+ const { ensureAuthenticated, getProjectConfig } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
 
9
9
  async function update(options) {
10
10
  try {
11
- // Check authentication
12
- if (!isAuthenticated()) {
11
+ // Check authentication (with auto-login support)
12
+ if (!(await ensureAuthenticated())) {
13
13
  logger.error('Not logged in');
14
- logger.newline();
15
- logger.info('Run:');
16
- logger.log(' saac login -e <email> -k <api-key>');
14
+ logger.info('Run: saac login -e <email> -k <api-key>');
15
+ logger.info('Or set: SAAC_USER_API_KEY and SAAC_USER_EMAIL');
17
16
  process.exit(1);
18
17
  }
19
18
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { isAuthenticated } = require('../lib/config');
6
+ const { ensureAuthenticated } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
 
9
9
  /**
@@ -12,11 +12,10 @@ const logger = require('../lib/logger');
12
12
  async function whoami() {
13
13
  try {
14
14
  // Check authentication
15
- if (!isAuthenticated()) {
15
+ if (!(await ensureAuthenticated())) {
16
16
  logger.error('Not logged in');
17
- logger.newline();
18
- logger.info('Run:');
19
- logger.log(' saac login -e <email> -k <api-key>');
17
+ logger.info('Run: saac login -e <email> -k <api-key>');
18
+ logger.info('Or set: SAAC_USER_API_KEY and SAAC_USER_EMAIL');
20
19
  process.exit(1);
21
20
  }
22
21
 
@@ -34,7 +33,7 @@ async function whoami() {
34
33
 
35
34
  logger.field('Email', user.email);
36
35
  logger.field('User ID', user.id);
37
- 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 ✗'));
38
37
  logger.field('Member Since', formatDate(user.created_at));
39
38
 
40
39
  logger.newline();
package/src/lib/config.js CHANGED
@@ -106,6 +106,52 @@ function isTokenExpiringSoon() {
106
106
  return expirationDate <= sevenDaysFromNow && !isTokenExpired();
107
107
  }
108
108
 
109
+ /**
110
+ * Ensure user is authenticated, with auto-login support via environment variables
111
+ * Checks for SAAC_USER_API_KEY and SAAC_USER_EMAIL environment variables
112
+ * If present and user is not authenticated, attempts automatic login
113
+ *
114
+ * @returns {Promise<boolean>} - True if authenticated, false otherwise
115
+ */
116
+ async function ensureAuthenticated() {
117
+ // Step 1: Check if already authenticated (fast path)
118
+ if (isAuthenticated()) {
119
+ return true;
120
+ }
121
+
122
+ // Step 2: Check for environment variables
123
+ const apiKey = process.env.SAAC_USER_API_KEY;
124
+ const email = process.env.SAAC_USER_EMAIL;
125
+
126
+ if (!apiKey || !email) {
127
+ // No environment variables - cannot auto-login
128
+ return false;
129
+ }
130
+
131
+ // Step 3: Attempt auto-login via API
132
+ try {
133
+ // Dynamically require to avoid circular dependency
134
+ const api = require('./api');
135
+ const result = await api.login(email, apiKey);
136
+
137
+ // Step 4: Save session token to config
138
+ saveUser({
139
+ email: result.user.email,
140
+ userId: result.user.id,
141
+ sessionToken: result.session_token,
142
+ expiresAt: result.expires_at,
143
+ verified: result.user.verified,
144
+ });
145
+
146
+ // Auto-login successful
147
+ return true;
148
+
149
+ } catch (error) {
150
+ // Auto-login failed (invalid key, network error, etc.)
151
+ return false;
152
+ }
153
+ }
154
+
109
155
  /**
110
156
  * Get user info
111
157
  */
@@ -146,6 +192,7 @@ module.exports = {
146
192
  getProjectConfig,
147
193
  saveProjectConfig,
148
194
  isAuthenticated,
195
+ ensureAuthenticated,
149
196
  isTokenExpired,
150
197
  isTokenExpiringSoon,
151
198
  getUser,