@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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__hive__send_direct_message",
5
+ "mcp__hive__hive"
6
+ ],
7
+ "deny": [],
8
+ "ask": []
9
+ }
10
+ }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.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
 
@@ -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();