@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/.claude/settings.local.json +22 -0
- package/CLAUDE.md +143 -1300
- package/bin/saac.js +5 -8
- package/package.json +1 -1
- package/src/commands/create.js +8 -6
- package/src/commands/delete.js +1 -1
- package/src/commands/deploy.js +194 -30
- package/src/commands/keys.js +0 -55
- package/src/commands/logs.js +137 -12
- package/src/commands/whoami.js +1 -1
- package/src/lib/api.js +2 -2
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('-
|
|
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
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/deploy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
54
|
-
spin.succeed('Deployment
|
|
57
|
+
// SUCCESS: Deployment queued
|
|
58
|
+
spin.succeed('Deployment queued');
|
|
55
59
|
|
|
56
60
|
logger.newline();
|
|
57
|
-
logger.success('
|
|
61
|
+
logger.success('Deployment has been queued!');
|
|
58
62
|
logger.newline();
|
|
59
63
|
logger.field('Application', applicationName);
|
|
60
|
-
logger.field('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 (
|
|
68
|
-
logger.field('
|
|
71
|
+
if (options.noCache) {
|
|
72
|
+
logger.field('Build Mode', 'No cache (full rebuild)');
|
|
69
73
|
}
|
|
70
74
|
logger.newline();
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
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();
|
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
|
|