@startanaicompany/cli 1.1.0 → 1.3.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.md +112 -7
- package/README.md +62 -8
- package/bin/saac.js +59 -2
- package/create-application-update.md +759 -0
- package/package.json +2 -2
- package/src/commands/create.js +278 -4
- package/src/commands/status.js +164 -4
- package/src/commands/update.js +284 -0
- package/src/lib/api.js +10 -0
- package/src/lib/config.js +8 -7
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update command - Update application configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const api = require('../lib/api');
|
|
6
|
+
const { isAuthenticated, getProjectConfig } = require('../lib/config');
|
|
7
|
+
const logger = require('../lib/logger');
|
|
8
|
+
|
|
9
|
+
async function update(options) {
|
|
10
|
+
try {
|
|
11
|
+
// Check authentication
|
|
12
|
+
if (!isAuthenticated()) {
|
|
13
|
+
logger.error('Not logged in');
|
|
14
|
+
logger.newline();
|
|
15
|
+
logger.info('Run:');
|
|
16
|
+
logger.log(' saac login -e <email> -k <api-key>');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Get project config
|
|
21
|
+
const projectConfig = getProjectConfig();
|
|
22
|
+
if (!projectConfig) {
|
|
23
|
+
logger.error('Not in a SAAC project directory');
|
|
24
|
+
logger.newline();
|
|
25
|
+
logger.info('Run this command in a directory initialized with:');
|
|
26
|
+
logger.log(' saac init');
|
|
27
|
+
logger.log(' saac create <name>');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
logger.section(`Updating Application: ${projectConfig.applicationName}`);
|
|
32
|
+
logger.newline();
|
|
33
|
+
|
|
34
|
+
// Build update payload from options
|
|
35
|
+
const updateData = {};
|
|
36
|
+
let hasChanges = false;
|
|
37
|
+
|
|
38
|
+
// Basic fields
|
|
39
|
+
if (options.name) {
|
|
40
|
+
updateData.name = options.name;
|
|
41
|
+
hasChanges = true;
|
|
42
|
+
}
|
|
43
|
+
if (options.branch) {
|
|
44
|
+
updateData.git_branch = options.branch;
|
|
45
|
+
hasChanges = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Port configuration
|
|
49
|
+
if (options.port) {
|
|
50
|
+
updateData.ports_exposes = options.port;
|
|
51
|
+
hasChanges = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build pack
|
|
55
|
+
if (options.buildPack) {
|
|
56
|
+
const validBuildPacks = ['dockercompose', 'nixpacks', 'dockerfile', 'static'];
|
|
57
|
+
if (!validBuildPacks.includes(options.buildPack)) {
|
|
58
|
+
logger.error(`Invalid build pack: ${options.buildPack}`);
|
|
59
|
+
logger.info(`Must be one of: ${validBuildPacks.join(', ')}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
updateData.build_pack = options.buildPack;
|
|
63
|
+
hasChanges = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Custom commands
|
|
67
|
+
if (options.installCmd) {
|
|
68
|
+
updateData.install_command = options.installCmd;
|
|
69
|
+
hasChanges = true;
|
|
70
|
+
}
|
|
71
|
+
if (options.buildCmd) {
|
|
72
|
+
updateData.build_command = options.buildCmd;
|
|
73
|
+
hasChanges = true;
|
|
74
|
+
}
|
|
75
|
+
if (options.startCmd) {
|
|
76
|
+
updateData.start_command = options.startCmd;
|
|
77
|
+
hasChanges = true;
|
|
78
|
+
}
|
|
79
|
+
if (options.preDeployCmd) {
|
|
80
|
+
updateData.pre_deployment_command = options.preDeployCmd;
|
|
81
|
+
hasChanges = true;
|
|
82
|
+
}
|
|
83
|
+
if (options.postDeployCmd) {
|
|
84
|
+
updateData.post_deployment_command = options.postDeployCmd;
|
|
85
|
+
hasChanges = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Resource limits
|
|
89
|
+
if (options.cpuLimit) {
|
|
90
|
+
updateData.cpu_limit = options.cpuLimit;
|
|
91
|
+
hasChanges = true;
|
|
92
|
+
}
|
|
93
|
+
if (options.memoryLimit) {
|
|
94
|
+
updateData.memory_limit = options.memoryLimit;
|
|
95
|
+
hasChanges = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Health check configuration
|
|
99
|
+
if (options.healthCheck !== undefined) {
|
|
100
|
+
updateData.health_check_enabled = options.healthCheck;
|
|
101
|
+
hasChanges = true;
|
|
102
|
+
}
|
|
103
|
+
if (options.healthPath) {
|
|
104
|
+
updateData.health_check_path = options.healthPath;
|
|
105
|
+
hasChanges = true;
|
|
106
|
+
}
|
|
107
|
+
if (options.healthInterval) {
|
|
108
|
+
updateData.health_check_interval = parseInt(options.healthInterval, 10);
|
|
109
|
+
hasChanges = true;
|
|
110
|
+
}
|
|
111
|
+
if (options.healthTimeout) {
|
|
112
|
+
updateData.health_check_timeout = parseInt(options.healthTimeout, 10);
|
|
113
|
+
hasChanges = true;
|
|
114
|
+
}
|
|
115
|
+
if (options.healthRetries) {
|
|
116
|
+
const retries = parseInt(options.healthRetries, 10);
|
|
117
|
+
if (retries < 1 || retries > 10) {
|
|
118
|
+
logger.error('Health check retries must be between 1 and 10');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
updateData.health_check_retries = retries;
|
|
122
|
+
hasChanges = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Restart policy
|
|
126
|
+
if (options.restart) {
|
|
127
|
+
const validRestartPolicies = ['always', 'on-failure', 'unless-stopped', 'no'];
|
|
128
|
+
if (!validRestartPolicies.includes(options.restart)) {
|
|
129
|
+
logger.error(`Invalid restart policy: ${options.restart}`);
|
|
130
|
+
logger.info(`Must be one of: ${validRestartPolicies.join(', ')}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
updateData.restart = options.restart;
|
|
134
|
+
hasChanges = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Environment variables
|
|
138
|
+
if (options.env) {
|
|
139
|
+
const envVars = {};
|
|
140
|
+
const envArray = Array.isArray(options.env) ? options.env : [options.env];
|
|
141
|
+
|
|
142
|
+
for (const envStr of envArray) {
|
|
143
|
+
const [key, ...valueParts] = envStr.split('=');
|
|
144
|
+
const value = valueParts.join('='); // Handle values with '=' in them
|
|
145
|
+
|
|
146
|
+
if (!key || value === undefined) {
|
|
147
|
+
logger.error(`Invalid environment variable format: ${envStr}`);
|
|
148
|
+
logger.info('Use format: KEY=VALUE');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
envVars[key] = value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (Object.keys(envVars).length > 50) {
|
|
156
|
+
logger.error('Maximum 50 environment variables allowed');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
updateData.environment_variables = envVars;
|
|
161
|
+
hasChanges = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check if any changes were provided
|
|
165
|
+
if (!hasChanges) {
|
|
166
|
+
logger.error('No configuration changes specified');
|
|
167
|
+
logger.newline();
|
|
168
|
+
logger.info('Usage:');
|
|
169
|
+
logger.log(' saac update [options]');
|
|
170
|
+
logger.newline();
|
|
171
|
+
logger.info('Available options:');
|
|
172
|
+
logger.log(' -n, --name <name> Application name');
|
|
173
|
+
logger.log(' -b, --branch <branch> Git branch');
|
|
174
|
+
logger.log(' -p, --port <port> Port to expose');
|
|
175
|
+
logger.log(' --build-pack <pack> Build pack: dockercompose, nixpacks, dockerfile, static');
|
|
176
|
+
logger.log(' --install-cmd <command> Install command');
|
|
177
|
+
logger.log(' --build-cmd <command> Build command');
|
|
178
|
+
logger.log(' --start-cmd <command> Start command');
|
|
179
|
+
logger.log(' --pre-deploy-cmd <command> Pre-deployment command');
|
|
180
|
+
logger.log(' --post-deploy-cmd <command> Post-deployment command');
|
|
181
|
+
logger.log(' --cpu-limit <limit> CPU limit (e.g., "1", "2.5")');
|
|
182
|
+
logger.log(' --memory-limit <limit> Memory limit (e.g., "512M", "2G")');
|
|
183
|
+
logger.log(' --health-check Enable health checks');
|
|
184
|
+
logger.log(' --no-health-check Disable health checks');
|
|
185
|
+
logger.log(' --health-path <path> Health check path');
|
|
186
|
+
logger.log(' --health-interval <seconds> Health check interval in seconds');
|
|
187
|
+
logger.log(' --health-timeout <seconds> Health check timeout in seconds');
|
|
188
|
+
logger.log(' --health-retries <count> Health check retries (1-10)');
|
|
189
|
+
logger.log(' --restart <policy> Restart policy: always, on-failure, unless-stopped, no');
|
|
190
|
+
logger.log(' --env <KEY=VALUE> Environment variable (can be used multiple times)');
|
|
191
|
+
logger.newline();
|
|
192
|
+
logger.info('Example:');
|
|
193
|
+
logger.log(' saac update --port 8080 --health-check --health-path /api/health');
|
|
194
|
+
logger.log(' saac update --build-pack nixpacks --cpu-limit 2 --memory-limit 2G');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Show configuration changes
|
|
199
|
+
logger.info('Configuration changes:');
|
|
200
|
+
Object.entries(updateData).forEach(([key, value]) => {
|
|
201
|
+
const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
202
|
+
let displayValue = value;
|
|
203
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
204
|
+
displayValue = `${Object.keys(value).length} variable(s)`;
|
|
205
|
+
}
|
|
206
|
+
logger.field(displayKey, displayValue);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
logger.newline();
|
|
210
|
+
|
|
211
|
+
const spin = logger.spinner('Updating application configuration...').start();
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const result = await api.updateApplication(projectConfig.applicationUuid, updateData);
|
|
215
|
+
|
|
216
|
+
spin.succeed('Configuration updated successfully!');
|
|
217
|
+
|
|
218
|
+
logger.newline();
|
|
219
|
+
|
|
220
|
+
if (result.updated_fields && result.updated_fields.length > 0) {
|
|
221
|
+
logger.success(`Updated ${result.updated_fields.length} field(s)`);
|
|
222
|
+
logger.newline();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Warn if tier limits were applied
|
|
226
|
+
if (result.applied_tier_limits) {
|
|
227
|
+
logger.warn('Resource limits were capped at your tier maximum');
|
|
228
|
+
logger.info('Free tier: 1 vCPU, 1024M RAM');
|
|
229
|
+
logger.newline();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Show next steps
|
|
233
|
+
logger.info('Next steps:');
|
|
234
|
+
logger.log(' 1. Review changes above');
|
|
235
|
+
logger.log(` 2. Deploy to apply: ${logger.chalk.cyan('saac deploy')}`);
|
|
236
|
+
logger.log(` 3. Monitor deployment: ${logger.chalk.cyan('saac logs --follow')}`);
|
|
237
|
+
logger.newline();
|
|
238
|
+
|
|
239
|
+
logger.warn('Note: Configuration changes require redeployment to take effect');
|
|
240
|
+
|
|
241
|
+
} catch (error) {
|
|
242
|
+
spin.fail('Configuration update failed');
|
|
243
|
+
|
|
244
|
+
if (error.response?.status === 403) {
|
|
245
|
+
const data = error.response.data;
|
|
246
|
+
logger.newline();
|
|
247
|
+
logger.error('Quota exceeded');
|
|
248
|
+
if (data.current_tier) {
|
|
249
|
+
logger.field('Current Tier', data.current_tier);
|
|
250
|
+
}
|
|
251
|
+
logger.newline();
|
|
252
|
+
logger.warn(data.error || data.message);
|
|
253
|
+
if (data.upgrade_info) {
|
|
254
|
+
logger.info(data.upgrade_info);
|
|
255
|
+
}
|
|
256
|
+
} else if (error.response?.status === 400) {
|
|
257
|
+
const data = error.response.data;
|
|
258
|
+
logger.newline();
|
|
259
|
+
logger.error('Validation failed');
|
|
260
|
+
if (data.details) {
|
|
261
|
+
logger.newline();
|
|
262
|
+
Object.entries(data.details).forEach(([field, message]) => {
|
|
263
|
+
logger.log(` ${logger.chalk.yellow(field)}: ${message}`);
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
logger.log(` ${data.message || data.error}`);
|
|
267
|
+
}
|
|
268
|
+
} else if (error.response?.status === 404) {
|
|
269
|
+
logger.newline();
|
|
270
|
+
logger.error('Application not found');
|
|
271
|
+
logger.info('The application may have been deleted');
|
|
272
|
+
} else {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
} catch (error) {
|
|
279
|
+
logger.error(error.response?.data?.message || error.message);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = update;
|
package/src/lib/api.js
CHANGED
|
@@ -133,6 +133,15 @@ async function getApplicationLogs(uuid, params = {}) {
|
|
|
133
133
|
return response.data;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Update application configuration
|
|
138
|
+
*/
|
|
139
|
+
async function updateApplication(uuid, updateData) {
|
|
140
|
+
const client = createClient();
|
|
141
|
+
const response = await client.patch(`/applications/${uuid}`, updateData);
|
|
142
|
+
return response.data;
|
|
143
|
+
}
|
|
144
|
+
|
|
136
145
|
/**
|
|
137
146
|
* Update environment variables
|
|
138
147
|
*/
|
|
@@ -181,6 +190,7 @@ module.exports = {
|
|
|
181
190
|
verifyEmail,
|
|
182
191
|
getUserInfo,
|
|
183
192
|
createApplication,
|
|
193
|
+
updateApplication,
|
|
184
194
|
listApplications,
|
|
185
195
|
getApplication,
|
|
186
196
|
deployApplication,
|
package/src/lib/config.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Configuration management
|
|
3
|
-
* Handles both global (~/.
|
|
3
|
+
* Handles both global (~/.config/startanaicompany/config.json) and project (.saac/config.json) configs
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const Conf = require('conf');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
9
10
|
|
|
10
11
|
// Global configuration (user credentials, API settings)
|
|
11
12
|
const globalConfig = new Conf({
|
|
12
|
-
|
|
13
|
+
cwd: path.join(os.homedir(), '.config', 'startanaicompany'),
|
|
13
14
|
configName: 'config',
|
|
14
15
|
defaults: {
|
|
15
16
|
apiUrl: 'https://apps.startanaicompany.com/api/v1',
|
|
16
|
-
|
|
17
|
+
gitUrl: 'https://git.startanaicompany.com/api/v1',
|
|
17
18
|
},
|
|
18
19
|
});
|
|
19
20
|
|
|
@@ -134,10 +135,10 @@ function getApiUrl() {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
/**
|
|
137
|
-
* Get
|
|
138
|
+
* Get Git URL
|
|
138
139
|
*/
|
|
139
|
-
function
|
|
140
|
-
return globalConfig.get('
|
|
140
|
+
function getGitUrl() {
|
|
141
|
+
return globalConfig.get('gitUrl');
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
module.exports = {
|
|
@@ -151,5 +152,5 @@ module.exports = {
|
|
|
151
152
|
saveUser,
|
|
152
153
|
clearUser,
|
|
153
154
|
getApiUrl,
|
|
154
|
-
|
|
155
|
+
getGitUrl,
|
|
155
156
|
};
|