@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.
- package/.claude/settings.local.json +10 -0
- package/CLAUDE.md +67 -7
- package/README.md +453 -18
- package/bin/saac.js +2 -6
- package/package.json +1 -1
- package/src/commands/create.js +13 -12
- package/src/commands/delete.js +6 -4
- package/src/commands/deploy.js +6 -4
- package/src/commands/deployments.js +5 -3
- package/src/commands/domain.js +9 -5
- package/src/commands/env.js +9 -5
- package/src/commands/exec.js +9 -5
- package/src/commands/git.js +13 -17
- package/src/commands/init.js +116 -20
- package/src/commands/keys.js +6 -57
- package/src/commands/list.js +5 -6
- package/src/commands/logs.js +142 -15
- package/src/commands/run.js +5 -3
- package/src/commands/sessions.js +4 -5
- package/src/commands/shell.js +5 -3
- package/src/commands/status.js +4 -5
- package/src/commands/update.js +5 -6
- package/src/commands/whoami.js +5 -6
- package/src/lib/config.js +47 -0
package/src/commands/logs.js
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const api = require('../lib/api');
|
|
6
|
-
const { getProjectConfig,
|
|
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 (!
|
|
13
|
-
logger.error('Not logged in
|
|
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
|
-
|
|
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;
|
package/src/commands/run.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const api = require('../lib/api');
|
|
6
|
-
const { getProjectConfig,
|
|
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 (!
|
|
59
|
-
logger.error('Not logged in
|
|
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
|
|
package/src/commands/sessions.js
CHANGED
|
@@ -3,18 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const api = require('../lib/api');
|
|
6
|
-
const {
|
|
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 (!
|
|
13
|
+
if (!(await ensureAuthenticated())) {
|
|
14
14
|
logger.error('Not logged in');
|
|
15
|
-
logger.
|
|
16
|
-
logger.info('
|
|
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
|
|
package/src/commands/shell.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const WebSocket = require('ws');
|
|
9
9
|
const readline = require('readline');
|
|
10
|
-
const { getProjectConfig,
|
|
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 (!
|
|
324
|
-
logger.error('Not logged in
|
|
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
|
|
package/src/commands/status.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const api = require('../lib/api');
|
|
6
|
-
const { getUser,
|
|
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 (!
|
|
16
|
+
if (!(await ensureAuthenticated())) {
|
|
17
17
|
logger.error('Not logged in');
|
|
18
|
-
logger.
|
|
19
|
-
logger.info('
|
|
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
|
|
package/src/commands/update.js
CHANGED
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const api = require('../lib/api');
|
|
6
|
-
const {
|
|
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 (!
|
|
11
|
+
// Check authentication (with auto-login support)
|
|
12
|
+
if (!(await ensureAuthenticated())) {
|
|
13
13
|
logger.error('Not logged in');
|
|
14
|
-
logger.
|
|
15
|
-
logger.info('
|
|
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
|
|
package/src/commands/whoami.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const api = require('../lib/api');
|
|
6
|
-
const {
|
|
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 (!
|
|
15
|
+
if (!(await ensureAuthenticated())) {
|
|
16
16
|
logger.error('Not logged in');
|
|
17
|
-
logger.
|
|
18
|
-
logger.info('
|
|
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.
|
|
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,
|