@startanaicompany/cli 1.1.0 → 1.3.1

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,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 (~/.saac/config.json) and project (.saac/config.json) configs
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
- projectName: 'saac',
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
- giteaUrl: 'https://git.startanaicompany.com/api/v1',
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 Gitea URL
138
+ * Get Git URL
138
139
  */
139
- function getGiteaUrl() {
140
- return globalConfig.get('giteaUrl');
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
- getGiteaUrl,
155
+ getGitUrl,
155
156
  };