@startanaicompany/cli 1.6.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 +13 -1
- package/CLAUDE.md +143 -1300
- package/bin/saac.js +3 -2
- package/package.json +1 -1
- package/src/commands/deploy.js +194 -30
- package/src/lib/api.js +2 -2
package/bin/saac.js
CHANGED
|
@@ -205,8 +205,9 @@ program
|
|
|
205
205
|
|
|
206
206
|
program
|
|
207
207
|
.command('deploy')
|
|
208
|
-
.description('Deploy current application')
|
|
209
|
-
.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)')
|
|
210
211
|
.action(deploy);
|
|
211
212
|
|
|
212
213
|
program
|
package/package.json
CHANGED
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/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
|
|