@startanaicompany/cli 1.4.17 → 1.4.18

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.
@@ -1,4 +1,265 @@
1
- // TODO: Implement env command
2
- module.exports = async function() {
3
- console.log('env command - Coming soon!');
1
+ /**
2
+ * Environment Variables Commands - Manage application environment variables
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { getProjectConfig, isAuthenticated } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+ const { table } = require('table');
9
+
10
+ /**
11
+ * Get environment variable(s)
12
+ * @param {string} key - Optional specific variable key to retrieve
13
+ */
14
+ async function get(key) {
15
+ try {
16
+ // Check authentication
17
+ if (!isAuthenticated()) {
18
+ logger.error('Not logged in. Run: saac login');
19
+ process.exit(1);
20
+ }
21
+
22
+ // Check for project config
23
+ const projectConfig = getProjectConfig();
24
+ if (!projectConfig || !projectConfig.applicationUuid) {
25
+ logger.error('No application found in current directory');
26
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
27
+ logger.newline();
28
+ logger.info('Or initialize with:');
29
+ logger.log(' saac init');
30
+ process.exit(1);
31
+ }
32
+
33
+ const { applicationUuid, applicationName } = projectConfig;
34
+
35
+ const spin = logger.spinner('Fetching environment variables...').start();
36
+
37
+ try {
38
+ const result = await api.getEnvironmentVariables(applicationUuid);
39
+
40
+ spin.succeed('Environment variables retrieved');
41
+
42
+ logger.newline();
43
+
44
+ if (!key) {
45
+ // Display all variables
46
+ if (Object.keys(result.variables).length === 0) {
47
+ logger.warn('No environment variables set');
48
+ logger.newline();
49
+ logger.info('Set variables with:');
50
+ logger.log(' saac env set KEY=value');
51
+ return;
52
+ }
53
+
54
+ logger.section(`Environment Variables: ${applicationName}`);
55
+ logger.newline();
56
+
57
+ // Create table data
58
+ const data = [
59
+ ['Key', 'Value'],
60
+ ];
61
+
62
+ for (const [envKey, value] of Object.entries(result.variables)) {
63
+ const maskedValue = maskSensitiveValue(envKey, value);
64
+ data.push([envKey, maskedValue]);
65
+ }
66
+
67
+ console.log(table(data));
68
+
69
+ logger.info(`Total: ${result.variable_count} / ${result.max_variables} variables`);
70
+ } else {
71
+ // Display specific variable
72
+ if (result.variables[key]) {
73
+ logger.section(`Environment Variable: ${key}`);
74
+ logger.newline();
75
+ logger.field('Key', key);
76
+ logger.field('Value', result.variables[key]);
77
+ } else {
78
+ logger.error(`Variable '${key}' not found`);
79
+ logger.newline();
80
+ logger.info('List all variables:');
81
+ logger.log(' saac env list');
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ } catch (error) {
87
+ spin.fail('Failed to fetch environment variables');
88
+ throw error;
89
+ }
90
+
91
+ } catch (error) {
92
+ logger.error(error.response?.data?.message || error.message);
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * List all environment variables (alias for get with no key)
99
+ */
100
+ async function list() {
101
+ return get();
102
+ }
103
+
104
+ /**
105
+ * Set environment variables
106
+ * @param {string[]} vars - Array of KEY=VALUE pairs
107
+ */
108
+ async function set(...vars) {
109
+ try {
110
+ // Check authentication
111
+ if (!isAuthenticated()) {
112
+ logger.error('Not logged in. Run: saac login');
113
+ process.exit(1);
114
+ }
115
+
116
+ // Check for project config
117
+ const projectConfig = getProjectConfig();
118
+ if (!projectConfig || !projectConfig.applicationUuid) {
119
+ logger.error('No application found in current directory');
120
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
121
+ logger.newline();
122
+ logger.info('Or initialize with:');
123
+ logger.log(' saac init');
124
+ process.exit(1);
125
+ }
126
+
127
+ const { applicationUuid } = projectConfig;
128
+
129
+ // Validate arguments
130
+ if (vars.length === 0) {
131
+ logger.error('No variables specified');
132
+ logger.newline();
133
+ logger.info('Usage:');
134
+ logger.log(' saac env set KEY=VALUE [KEY2=VALUE2 ...]');
135
+ logger.newline();
136
+ logger.info('Examples:');
137
+ logger.log(' saac env set NODE_ENV=production');
138
+ logger.log(' saac env set LOG_LEVEL=debug API_URL=https://api.example.com');
139
+ process.exit(1);
140
+ }
141
+
142
+ // Parse KEY=VALUE pairs
143
+ const variables = {};
144
+ for (const arg of vars) {
145
+ const equalIndex = arg.indexOf('=');
146
+ if (equalIndex === -1) {
147
+ logger.error(`Invalid format: ${arg}`);
148
+ logger.info('Variables must be in KEY=VALUE format');
149
+ logger.newline();
150
+ logger.info('Example:');
151
+ logger.log(' saac env set NODE_ENV=production');
152
+ process.exit(1);
153
+ }
154
+
155
+ const key = arg.substring(0, equalIndex).trim();
156
+ const value = arg.substring(equalIndex + 1);
157
+
158
+ if (!key) {
159
+ logger.error(`Empty key in: ${arg}`);
160
+ process.exit(1);
161
+ }
162
+
163
+ // Validate key format (uppercase alphanumeric + underscore)
164
+ const keyRegex = /^[A-Z0-9_]+$/;
165
+ if (!keyRegex.test(key)) {
166
+ logger.error(`Invalid key format: ${key}`);
167
+ logger.info('Keys must be uppercase alphanumeric with underscores only');
168
+ logger.newline();
169
+ logger.info('Valid examples:');
170
+ logger.log(' NODE_ENV, LOG_LEVEL, DATABASE_URL, API_KEY');
171
+ logger.newline();
172
+ logger.info('Invalid examples:');
173
+ logger.log(' node-env (lowercase/hyphen), 123ABC (starts with number)');
174
+ process.exit(1);
175
+ }
176
+
177
+ variables[key] = value;
178
+ }
179
+
180
+ logger.section('Updating Environment Variables');
181
+ logger.newline();
182
+
183
+ // Show what will be updated
184
+ logger.info('Variables to set:');
185
+ for (const [key, value] of Object.entries(variables)) {
186
+ const displayValue = maskSensitiveValue(key, value);
187
+ logger.field(` ${key}`, displayValue);
188
+ }
189
+
190
+ logger.newline();
191
+
192
+ const spin = logger.spinner('Updating environment variables...').start();
193
+
194
+ try {
195
+ await api.updateEnvironmentVariables(applicationUuid, variables);
196
+
197
+ spin.succeed('Environment variables updated successfully!');
198
+
199
+ logger.newline();
200
+
201
+ logger.success(`Set ${Object.keys(variables).length} variable(s)`);
202
+
203
+ logger.newline();
204
+ logger.warn('Changes require redeployment to take effect');
205
+ logger.info('Run:');
206
+ logger.log(' saac deploy');
207
+
208
+ } catch (error) {
209
+ spin.fail('Failed to update environment variables');
210
+
211
+ if (error.response?.status === 400) {
212
+ logger.newline();
213
+ const data = error.response.data;
214
+ if (data.error === 'QUOTA_EXCEEDED') {
215
+ logger.warn('Maximum number of environment variables exceeded');
216
+ logger.newline();
217
+ logger.info('Details:');
218
+ logger.field(' Limit', data.details?.limit || 50);
219
+ logger.field(' Current', data.details?.current || 'unknown');
220
+ logger.field(' Requested', data.details?.requested || vars.length);
221
+ }
222
+ }
223
+
224
+ throw error;
225
+ }
226
+
227
+ } catch (error) {
228
+ logger.error(error.response?.data?.message || error.message);
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Mask sensitive values for display
235
+ * @param {string} key - Variable key
236
+ * @param {string} value - Variable value
237
+ * @returns {string} Masked value if sensitive, original otherwise
238
+ */
239
+ function maskSensitiveValue(key, value) {
240
+ const sensitivePatterns = [
241
+ 'PASSWORD', 'SECRET', 'KEY', 'TOKEN',
242
+ 'DATABASE_URL', 'DB_URL', 'PRIVATE', 'AUTH'
243
+ ];
244
+
245
+ const isSensitive = sensitivePatterns.some(pattern =>
246
+ key.toUpperCase().includes(pattern)
247
+ );
248
+
249
+ if (!isSensitive) {
250
+ return value;
251
+ }
252
+
253
+ // Mask sensitive values
254
+ if (value.length <= 8) {
255
+ return '***';
256
+ }
257
+
258
+ return value.substring(0, 4) + '***' + value.substring(value.length - 4);
259
+ }
260
+
261
+ module.exports = {
262
+ get,
263
+ list,
264
+ set,
4
265
  };
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Exec Command - Execute commands in remote container
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { getProjectConfig, isAuthenticated } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+ const { table } = require('table');
9
+
10
+ /**
11
+ * Execute a command in the remote container
12
+ * @param {string} command - Command to execute
13
+ * @param {object} options - Command options
14
+ */
15
+ async function exec(command, options = {}) {
16
+ try {
17
+ // Check authentication
18
+ if (!isAuthenticated()) {
19
+ logger.error('Not logged in. Run: saac login');
20
+ process.exit(1);
21
+ }
22
+
23
+ // Check for project config
24
+ const projectConfig = getProjectConfig();
25
+ if (!projectConfig || !projectConfig.applicationUuid) {
26
+ logger.error('No application found in current directory');
27
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
28
+ logger.newline();
29
+ logger.info('Or initialize with:');
30
+ logger.log(' saac init');
31
+ process.exit(1);
32
+ }
33
+
34
+ const { applicationUuid, applicationName } = projectConfig;
35
+
36
+ // Build exec request
37
+ const execRequest = {
38
+ command,
39
+ workdir: options.workdir || '/app',
40
+ timeout: options.timeout || 30
41
+ };
42
+
43
+ // Validate timeout
44
+ if (execRequest.timeout > 300) {
45
+ logger.error('Timeout cannot exceed 300 seconds (5 minutes)');
46
+ process.exit(1);
47
+ }
48
+
49
+ logger.newline();
50
+ logger.section(`Executing Command: ${applicationName}`);
51
+ logger.newline();
52
+ logger.field(' Command', command);
53
+ logger.field(' Working Directory', execRequest.workdir);
54
+ logger.field(' Timeout', `${execRequest.timeout}s`);
55
+ logger.newline();
56
+
57
+ const spin = logger.spinner('Executing command in container...').start();
58
+
59
+ let result;
60
+ try {
61
+ result = await api.executeCommand(applicationUuid, execRequest);
62
+ spin.succeed('Command executed');
63
+ } catch (error) {
64
+ spin.fail('Command execution failed');
65
+
66
+ if (error.response?.status === 400) {
67
+ const data = error.response.data;
68
+ logger.newline();
69
+
70
+ if (data.error === 'VALIDATION_ERROR') {
71
+ logger.error('Command validation failed');
72
+ logger.newline();
73
+ logger.warn(data.message);
74
+
75
+ if (data.message.includes('not in allowlist')) {
76
+ logger.newline();
77
+ logger.info('Allowed commands include:');
78
+ logger.log(' Node.js: npm, node, npx, yarn, pnpm');
79
+ logger.log(' Python: python, python3, pip, poetry');
80
+ logger.log(' Ruby: bundle, rake, rails');
81
+ logger.log(' Shell: sh, bash, echo, cat, ls, pwd');
82
+ logger.log(' Database: psql, mysql, mongosh');
83
+ }
84
+ }
85
+ } else if (error.response?.status === 408) {
86
+ logger.newline();
87
+ logger.error('Command execution timed out');
88
+ logger.info(`Try increasing timeout with: --timeout ${execRequest.timeout * 2}`);
89
+ } else if (error.response?.status === 429) {
90
+ logger.newline();
91
+ logger.error('Rate limit exceeded');
92
+ logger.info('Limit: 30 exec commands per 5 minutes');
93
+ logger.info('Please wait a few minutes and try again');
94
+ } else if (error.response?.status === 503) {
95
+ logger.newline();
96
+ logger.error('Container is not running');
97
+ logger.info('Check application status with: saac status');
98
+ }
99
+
100
+ throw error;
101
+ }
102
+
103
+ logger.newline();
104
+
105
+ // Display execution results
106
+ logger.success(`✓ Execution ID: ${result.execution_id}`);
107
+ logger.newline();
108
+
109
+ logger.field('Exit Code', result.exit_code === 0
110
+ ? logger.chalk.green(result.exit_code)
111
+ : logger.chalk.red(result.exit_code)
112
+ );
113
+ logger.field('Duration', `${result.duration_ms}ms`);
114
+ logger.field('Started', new Date(result.started_at).toLocaleString());
115
+ logger.field('Completed', new Date(result.completed_at).toLocaleString());
116
+
117
+ // Display stdout
118
+ if (result.stdout) {
119
+ logger.newline();
120
+ logger.info('Standard Output:');
121
+ logger.section('─'.repeat(60));
122
+ console.log(result.stdout.trim());
123
+ logger.section('─'.repeat(60));
124
+ }
125
+
126
+ // Display stderr
127
+ if (result.stderr) {
128
+ logger.newline();
129
+ logger.warn('Standard Error:');
130
+ logger.section('─'.repeat(60));
131
+ console.error(result.stderr.trim());
132
+ logger.section('─'.repeat(60));
133
+ }
134
+
135
+ // If no output
136
+ if (!result.stdout && !result.stderr) {
137
+ logger.newline();
138
+ logger.info('(No output)');
139
+ }
140
+
141
+ logger.newline();
142
+
143
+ // Exit with same code as remote command
144
+ process.exit(result.exit_code);
145
+
146
+ } catch (error) {
147
+ logger.error(error.response?.data?.message || error.message);
148
+ process.exit(1);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * View execution history
154
+ * @param {object} options - Command options
155
+ */
156
+ async function history(options = {}) {
157
+ try {
158
+ // Check authentication
159
+ if (!isAuthenticated()) {
160
+ logger.error('Not logged in. Run: saac login');
161
+ process.exit(1);
162
+ }
163
+
164
+ // Check for project config
165
+ const projectConfig = getProjectConfig();
166
+ if (!projectConfig || !projectConfig.applicationUuid) {
167
+ logger.error('No application found in current directory');
168
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
169
+ logger.newline();
170
+ logger.info('Or initialize with:');
171
+ logger.log(' saac init');
172
+ process.exit(1);
173
+ }
174
+
175
+ const { applicationUuid, applicationName } = projectConfig;
176
+
177
+ const params = {
178
+ limit: options.limit || 20,
179
+ offset: options.offset || 0
180
+ };
181
+
182
+ // Validate params
183
+ if (params.limit > 100) {
184
+ logger.error('Limit cannot exceed 100');
185
+ process.exit(1);
186
+ }
187
+
188
+ logger.newline();
189
+ const spin = logger.spinner('Fetching execution history...').start();
190
+
191
+ let result;
192
+ try {
193
+ result = await api.getExecutionHistory(applicationUuid, params);
194
+ spin.succeed('Execution history retrieved');
195
+ } catch (error) {
196
+ spin.fail('Failed to fetch execution history');
197
+ throw error;
198
+ }
199
+
200
+ logger.newline();
201
+
202
+ if (result.executions.length === 0) {
203
+ logger.warn('No execution history found');
204
+ logger.newline();
205
+ logger.info('Run a command first:');
206
+ logger.log(' saac exec "npm run db:migrate"');
207
+ return;
208
+ }
209
+
210
+ logger.section(`Execution History: ${applicationName}`);
211
+ logger.newline();
212
+
213
+ // Create table data
214
+ const data = [
215
+ ['ID', 'Command', 'Status', 'Exit Code', 'Duration', 'Started'],
216
+ ];
217
+
218
+ for (const execution of result.executions) {
219
+ const shortId = execution.id.substring(0, 8);
220
+ const command = execution.command.length > 40
221
+ ? execution.command.substring(0, 37) + '...'
222
+ : execution.command;
223
+
224
+ let statusDisplay;
225
+ if (execution.status === 'completed') {
226
+ statusDisplay = execution.exit_code === 0
227
+ ? logger.chalk.green('✓ completed')
228
+ : logger.chalk.red('✗ completed');
229
+ } else if (execution.status === 'failed') {
230
+ statusDisplay = logger.chalk.red('✗ failed');
231
+ } else if (execution.status === 'timeout') {
232
+ statusDisplay = logger.chalk.yellow('⏱ timeout');
233
+ } else if (execution.status === 'running') {
234
+ statusDisplay = logger.chalk.blue('▸ running');
235
+ } else {
236
+ statusDisplay = logger.chalk.gray('○ pending');
237
+ }
238
+
239
+ const exitCode = execution.exit_code !== null && execution.exit_code !== undefined
240
+ ? (execution.exit_code === 0 ? logger.chalk.green(execution.exit_code) : logger.chalk.red(execution.exit_code))
241
+ : logger.chalk.gray('-');
242
+
243
+ const duration = execution.duration_seconds !== null && execution.duration_seconds !== undefined
244
+ ? `${execution.duration_seconds}s`
245
+ : logger.chalk.gray('-');
246
+
247
+ const startedAt = execution.started_at
248
+ ? new Date(execution.started_at).toLocaleString()
249
+ : logger.chalk.gray('Not started');
250
+
251
+ data.push([shortId, command, statusDisplay, exitCode, duration, startedAt]);
252
+ }
253
+
254
+ console.log(table(data));
255
+
256
+ logger.info(`Showing ${result.executions.length} of ${result.total} executions`);
257
+
258
+ if (result.offset + result.limit < result.total) {
259
+ logger.newline();
260
+ logger.info('View more:');
261
+ logger.log(` saac exec --history --offset ${result.offset + result.limit} --limit ${result.limit}`);
262
+ }
263
+
264
+ logger.newline();
265
+ logger.info('View details of a specific execution:');
266
+ logger.log(' saac logs # View application logs');
267
+
268
+ } catch (error) {
269
+ logger.error(error.response?.data?.message || error.message);
270
+ process.exit(1);
271
+ }
272
+ }
273
+
274
+ module.exports = {
275
+ exec,
276
+ history,
277
+ };