@startanaicompany/cli 1.5.0 → 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/bin/saac.js +2 -6
- package/package.json +1 -1
- package/src/commands/create.js +8 -6
- package/src/commands/delete.js +1 -1
- package/src/commands/keys.js +0 -55
- package/src/commands/logs.js +137 -12
- package/src/commands/whoami.js +1 -1
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')
|
|
@@ -231,6 +226,7 @@ program
|
|
|
231
226
|
.option('-t, --tail <lines>', 'Number of lines to show (runtime logs only)', '100')
|
|
232
227
|
.option('-f, --follow', 'Follow log output (runtime logs only)')
|
|
233
228
|
.option('--since <time>', 'Show logs since timestamp (runtime logs only)')
|
|
229
|
+
.option('--type <type>', 'Filter by log type: build, runtime, access (runtime logs only)')
|
|
234
230
|
.action(logs);
|
|
235
231
|
|
|
236
232
|
// Local development commands
|
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -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
|
|
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
|
package/src/commands/delete.js
CHANGED
|
@@ -113,7 +113,7 @@ async function deleteApp(options) {
|
|
|
113
113
|
|
|
114
114
|
logger.newline();
|
|
115
115
|
|
|
116
|
-
logger.success(`Application '${
|
|
116
|
+
logger.success(`Application '${app.name}' has been permanently deleted.`);
|
|
117
117
|
|
|
118
118
|
logger.newline();
|
|
119
119
|
|
package/src/commands/keys.js
CHANGED
|
@@ -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
|
};
|
package/src/commands/logs.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/commands/whoami.js
CHANGED
|
@@ -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.
|
|
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();
|