@startanaicompany/cli 1.4.16 → 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,232 @@
1
- // TODO: Implement logs command
2
- module.exports = async function() {
3
- console.log('logs command - Coming soon!');
4
- };
1
+ /**
2
+ * Logs command - View application runtime logs or deployment logs
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { getProjectConfig, isAuthenticated } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+
9
+ async function logs(deploymentUuidArg, options) {
10
+ try {
11
+ // Check authentication
12
+ if (!isAuthenticated()) {
13
+ logger.error('Not logged in. Run: saac login');
14
+ process.exit(1);
15
+ }
16
+
17
+ // Check for project config
18
+ const projectConfig = getProjectConfig();
19
+ if (!projectConfig || !projectConfig.applicationUuid) {
20
+ logger.error('No application found in current directory');
21
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
22
+ logger.newline();
23
+ logger.info('Or initialize with:');
24
+ logger.log(' saac init');
25
+ process.exit(1);
26
+ }
27
+
28
+ const { applicationUuid, applicationName } = projectConfig;
29
+
30
+ // Determine if we're fetching deployment logs or runtime logs
31
+ if (options.deployment !== undefined) {
32
+ // Deployment logs mode
33
+ return await getDeploymentLogs(applicationUuid, applicationName, deploymentUuidArg || options.deployment, options);
34
+ } else {
35
+ // Runtime logs mode (not implemented yet)
36
+ return await getRuntimeLogs(applicationUuid, applicationName, options);
37
+ }
38
+
39
+ } catch (error) {
40
+ logger.error(error.response?.data?.message || error.message);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Get deployment logs (build logs)
47
+ */
48
+ async function getDeploymentLogs(applicationUuid, applicationName, deploymentUuid, options) {
49
+ logger.section(`Deployment Logs: ${applicationName}`);
50
+ logger.newline();
51
+
52
+ const spin = logger.spinner('Fetching deployment logs...').start();
53
+
54
+ try {
55
+ // Build query parameters
56
+ const params = {};
57
+ if (deploymentUuid && deploymentUuid !== true) {
58
+ params.deployment_uuid = deploymentUuid;
59
+ }
60
+ if (options.raw) {
61
+ params.format = 'raw';
62
+ }
63
+ if (options.includeHidden) {
64
+ params.include_hidden = true;
65
+ }
66
+
67
+ const result = await api.getDeploymentLogs(applicationUuid, params);
68
+
69
+ // Update spinner with status
70
+ if (result.status === 'finished') {
71
+ spin.succeed('Deployment logs retrieved (finished)');
72
+ } else if (result.status === 'failed') {
73
+ spin.fail('Deployment logs retrieved (failed)');
74
+ } else {
75
+ spin.succeed(`Deployment logs retrieved (${result.status})`);
76
+ }
77
+
78
+ logger.newline();
79
+
80
+ // Display header information
81
+ logger.field('Deployment UUID', result.deployment_uuid || 'N/A');
82
+ logger.field('Application', result.application_name || applicationName);
83
+
84
+ // Status with color
85
+ let statusDisplay;
86
+ if (result.status === 'finished') {
87
+ statusDisplay = logger.chalk.green(result.status);
88
+ } else if (result.status === 'failed') {
89
+ statusDisplay = logger.chalk.red(result.status);
90
+ } else {
91
+ statusDisplay = logger.chalk.yellow(result.status);
92
+ }
93
+ logger.field('Status', statusDisplay);
94
+
95
+ if (result.commit) {
96
+ logger.field('Commit', result.commit);
97
+ }
98
+ if (result.commit_message) {
99
+ logger.field('Message', result.commit_message);
100
+ }
101
+ if (result.started_at) {
102
+ logger.field('Started', new Date(result.started_at).toLocaleString());
103
+ }
104
+ if (result.finished_at) {
105
+ logger.field('Finished', new Date(result.finished_at).toLocaleString());
106
+ }
107
+ if (result.duration_seconds !== undefined) {
108
+ logger.field('Duration', `${result.duration_seconds}s`);
109
+ }
110
+
111
+ logger.newline();
112
+ logger.info(`Log Output (${result.log_count || 0} lines):`);
113
+ logger.log('─'.repeat(60));
114
+ logger.newline();
115
+
116
+ // Display logs
117
+ if (options.raw || result.raw_logs) {
118
+ // Raw format - just print the text
119
+ console.log(result.raw_logs);
120
+ } else if (result.logs && result.logs.length > 0) {
121
+ // Parsed format - colorize stderr
122
+ result.logs.forEach(entry => {
123
+ if (entry.type === 'stderr') {
124
+ console.log(logger.chalk.red(entry.output));
125
+ } else {
126
+ console.log(entry.output);
127
+ }
128
+ });
129
+ } else {
130
+ logger.warn('No logs available');
131
+ }
132
+
133
+ // Display errors summary if present
134
+ if (result.errors && result.errors.length > 0) {
135
+ logger.newline();
136
+ logger.log('─'.repeat(60));
137
+ logger.newline();
138
+ logger.error('Errors Detected:');
139
+ result.errors.forEach(err => {
140
+ logger.log(` ${logger.chalk.red(`[${err.type}]`)} ${err.message}`);
141
+ if (err.detail) {
142
+ logger.log(` ${logger.chalk.gray(err.detail)}`);
143
+ }
144
+ });
145
+ }
146
+
147
+ } catch (error) {
148
+ spin.fail('Failed to fetch deployment logs');
149
+
150
+ if (error.response?.status === 404) {
151
+ logger.newline();
152
+ logger.warn('No deployments found for this application');
153
+ logger.newline();
154
+ logger.info('Deploy first with:');
155
+ logger.log(' saac deploy');
156
+ } else {
157
+ throw error;
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get runtime logs (container logs)
164
+ */
165
+ async function getRuntimeLogs(applicationUuid, applicationName, options) {
166
+ logger.section(`Runtime Logs: ${applicationName}`);
167
+ logger.newline();
168
+
169
+ const spin = logger.spinner('Fetching runtime logs...').start();
170
+
171
+ try {
172
+ // Build query parameters
173
+ const params = {};
174
+ if (options.follow) {
175
+ params.follow = true;
176
+ }
177
+ if (options.tail) {
178
+ params.tail = parseInt(options.tail, 10);
179
+ }
180
+ if (options.since) {
181
+ params.since = options.since;
182
+ }
183
+
184
+ const result = await api.getApplicationLogs(applicationUuid, params);
185
+
186
+ spin.succeed('Runtime logs retrieved');
187
+
188
+ logger.newline();
189
+
190
+ // Display logs
191
+ if (result.logs && result.logs.length > 0) {
192
+ result.logs.forEach(log => {
193
+ console.log(log);
194
+ });
195
+ } else if (typeof result === 'string') {
196
+ console.log(result);
197
+ } else {
198
+ logger.warn('No logs available');
199
+ logger.newline();
200
+ logger.info('Make sure your application is deployed:');
201
+ logger.log(' saac deploy');
202
+ }
203
+
204
+ // Note about follow mode
205
+ if (options.follow) {
206
+ logger.newline();
207
+ logger.info('Note: Follow mode (--follow) for live logs is not yet implemented');
208
+ logger.info('This command shows recent logs only');
209
+ }
210
+
211
+ } catch (error) {
212
+ spin.fail('Failed to fetch runtime logs');
213
+
214
+ if (error.response?.status === 404) {
215
+ logger.newline();
216
+ logger.warn('Application not found or no logs available');
217
+ logger.newline();
218
+ logger.info('Deploy first with:');
219
+ logger.log(' saac deploy');
220
+ } else if (error.response?.status === 501) {
221
+ logger.newline();
222
+ logger.warn('Runtime logs endpoint not implemented yet');
223
+ logger.newline();
224
+ logger.info('Use deployment logs instead:');
225
+ logger.log(' saac logs --deployment');
226
+ } else {
227
+ throw error;
228
+ }
229
+ }
230
+ }
231
+
232
+ module.exports = logs;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Run Command - Execute local command with remote 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 { spawn } = require('child_process');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ // In-memory cache for environment variables (5 minutes TTL)
14
+ const envCache = new Map();
15
+ const CACHE_TTL = 300000; // 5 minutes in milliseconds
16
+
17
+ /**
18
+ * Get environment variables (with caching)
19
+ * @param {string} appUuid - Application UUID
20
+ * @param {boolean} forceRefresh - Skip cache and fetch fresh
21
+ */
22
+ async function getEnvironmentVariables(appUuid, forceRefresh = false) {
23
+ // Check cache first
24
+ if (!forceRefresh) {
25
+ const cached = envCache.get(appUuid);
26
+ if (cached) {
27
+ const age = Date.now() - cached.timestamp;
28
+ if (age < CACHE_TTL) {
29
+ logger.info('📦 Using cached environment variables (updated <5 min ago)');
30
+ return cached.data;
31
+ }
32
+ // Cache expired
33
+ envCache.delete(appUuid);
34
+ }
35
+ }
36
+
37
+ // Fetch from API
38
+ const client = api.createClient();
39
+ const response = await client.get(`/applications/${appUuid}/env/export`);
40
+
41
+ // Cache for 5 minutes
42
+ envCache.set(appUuid, {
43
+ data: response.data,
44
+ timestamp: Date.now()
45
+ });
46
+
47
+ return response.data;
48
+ }
49
+
50
+ /**
51
+ * Run command with remote environment variables
52
+ * @param {string} command - Command to execute
53
+ * @param {object} options - Command options
54
+ */
55
+ async function run(command, options = {}) {
56
+ try {
57
+ // Check authentication
58
+ if (!isAuthenticated()) {
59
+ logger.error('Not logged in. Run: saac login');
60
+ process.exit(1);
61
+ }
62
+
63
+ // Check for project config
64
+ const projectConfig = getProjectConfig();
65
+ if (!projectConfig || !projectConfig.applicationUuid) {
66
+ logger.error('No application found in current directory');
67
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
68
+ logger.newline();
69
+ logger.info('Or initialize with:');
70
+ logger.log(' saac init');
71
+ process.exit(1);
72
+ }
73
+
74
+ const { applicationUuid, applicationName } = projectConfig;
75
+
76
+ // Fetch environment variables
77
+ logger.newline();
78
+ const spin = logger.spinner('Fetching environment variables...').start();
79
+
80
+ let envData;
81
+ try {
82
+ envData = await getEnvironmentVariables(applicationUuid, options.sync);
83
+ spin.succeed('Environment variables retrieved');
84
+ } catch (error) {
85
+ spin.fail('Failed to fetch environment variables');
86
+
87
+ if (error.response?.status === 429) {
88
+ const retryAfter = error.response.headers['retry-after'] || 60;
89
+ logger.newline();
90
+ logger.error(`Rate limit exceeded. Too many requests.`);
91
+ logger.info(`Retry in ${retryAfter} seconds.`);
92
+ logger.newline();
93
+ logger.info('Note: Environment variables are cached for 5 minutes.');
94
+ process.exit(1);
95
+ }
96
+
97
+ throw error;
98
+ }
99
+
100
+ logger.newline();
101
+
102
+ // Create temp file for environment variables
103
+ const tempDir = os.tmpdir();
104
+ const tempFile = path.join(tempDir, `saac-env-${applicationUuid}.sh`);
105
+
106
+ // Write export script to temp file with secure permissions
107
+ fs.writeFileSync(tempFile, envData.export_script, { mode: 0o600 });
108
+
109
+ // Setup cleanup handlers
110
+ const cleanup = () => {
111
+ try {
112
+ if (fs.existsSync(tempFile)) {
113
+ fs.unlinkSync(tempFile);
114
+ }
115
+ } catch (err) {
116
+ // Ignore cleanup errors
117
+ }
118
+ };
119
+
120
+ process.on('SIGINT', () => {
121
+ cleanup();
122
+ process.exit(130);
123
+ });
124
+
125
+ process.on('SIGTERM', () => {
126
+ cleanup();
127
+ process.exit(143);
128
+ });
129
+
130
+ // Display info
131
+ logger.info(`Running command with ${envData.variable_count} remote environment variables`);
132
+ logger.field(' Application', applicationName);
133
+ logger.field(' Command', command);
134
+
135
+ if (!options.quiet) {
136
+ logger.newline();
137
+ logger.warn('⚠️ Secrets are exposed on local machine');
138
+ logger.info(`Temporary file: ${tempFile} (will be deleted on exit)`);
139
+ }
140
+
141
+ logger.newline();
142
+ logger.section('Command Output');
143
+ logger.newline();
144
+
145
+ // Execute command with sourced environment
146
+ const shell = process.env.SHELL || '/bin/bash';
147
+ const proc = spawn(shell, ['-c', `source "${tempFile}" && ${command}`], {
148
+ stdio: 'inherit',
149
+ cwd: process.cwd()
150
+ });
151
+
152
+ proc.on('exit', (code) => {
153
+ cleanup();
154
+ process.exit(code || 0);
155
+ });
156
+
157
+ proc.on('error', (error) => {
158
+ cleanup();
159
+ logger.newline();
160
+ logger.error(`Failed to execute command: ${error.message}`);
161
+ process.exit(1);
162
+ });
163
+
164
+ } catch (error) {
165
+ logger.error(error.response?.data?.message || error.message);
166
+ process.exit(1);
167
+ }
168
+ }
169
+
170
+ module.exports = run;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Shell Command - Open interactive shell with remote 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 { spawn } = require('child_process');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ // In-memory cache for environment variables (5 minutes TTL)
14
+ const envCache = new Map();
15
+ const CACHE_TTL = 300000; // 5 minutes in milliseconds
16
+
17
+ /**
18
+ * Get environment variables (with caching)
19
+ * @param {string} appUuid - Application UUID
20
+ * @param {boolean} forceRefresh - Skip cache and fetch fresh
21
+ */
22
+ async function getEnvironmentVariables(appUuid, forceRefresh = false) {
23
+ // Check cache first
24
+ if (!forceRefresh) {
25
+ const cached = envCache.get(appUuid);
26
+ if (cached) {
27
+ const age = Date.now() - cached.timestamp;
28
+ if (age < CACHE_TTL) {
29
+ logger.info('📦 Using cached environment variables (updated <5 min ago)');
30
+ return cached.data;
31
+ }
32
+ // Cache expired
33
+ envCache.delete(appUuid);
34
+ }
35
+ }
36
+
37
+ // Fetch from API
38
+ const client = api.createClient();
39
+ const response = await client.get(`/applications/${appUuid}/env/export`);
40
+
41
+ // Cache for 5 minutes
42
+ envCache.set(appUuid, {
43
+ data: response.data,
44
+ timestamp: Date.now()
45
+ });
46
+
47
+ return response.data;
48
+ }
49
+
50
+ /**
51
+ * Open interactive shell with remote environment variables
52
+ * @param {object} options - Command options
53
+ */
54
+ async function shell(options = {}) {
55
+ try {
56
+ // Check authentication
57
+ if (!isAuthenticated()) {
58
+ logger.error('Not logged in. Run: saac login');
59
+ process.exit(1);
60
+ }
61
+
62
+ // Check for project config
63
+ const projectConfig = getProjectConfig();
64
+ if (!projectConfig || !projectConfig.applicationUuid) {
65
+ logger.error('No application found in current directory');
66
+ logger.info('Run this command from a project directory (must have .saac/config.json)');
67
+ logger.newline();
68
+ logger.info('Or initialize with:');
69
+ logger.log(' saac init');
70
+ process.exit(1);
71
+ }
72
+
73
+ const { applicationUuid, applicationName } = projectConfig;
74
+
75
+ // Fetch environment variables
76
+ logger.newline();
77
+ const spin = logger.spinner('Fetching environment variables...').start();
78
+
79
+ let envData;
80
+ try {
81
+ envData = await getEnvironmentVariables(applicationUuid, options.sync);
82
+ spin.succeed('Environment variables retrieved');
83
+ } catch (error) {
84
+ spin.fail('Failed to fetch environment variables');
85
+
86
+ if (error.response?.status === 429) {
87
+ const retryAfter = error.response.headers['retry-after'] || 60;
88
+ logger.newline();
89
+ logger.error(`Rate limit exceeded. Too many requests.`);
90
+ logger.info(`Retry in ${retryAfter} seconds.`);
91
+ logger.newline();
92
+ logger.info('Note: Environment variables are cached for 5 minutes.');
93
+ process.exit(1);
94
+ }
95
+
96
+ throw error;
97
+ }
98
+
99
+ logger.newline();
100
+
101
+ // Determine shell to use
102
+ const userShell = options.cmd || process.env.SHELL || '/bin/bash';
103
+ const shellName = path.basename(userShell);
104
+
105
+ // Display info
106
+ logger.success(`🚀 Opening shell with ${envData.variable_count} environment variables loaded`);
107
+ logger.newline();
108
+ logger.field(' Application', applicationName);
109
+ logger.field(' Shell', shellName);
110
+ logger.field(' Variables', envData.variable_count);
111
+ logger.newline();
112
+ logger.warn('⚠️ Secrets are exposed on local machine');
113
+ logger.newline();
114
+ logger.info('Type "exit" or press Ctrl+D to close the shell');
115
+ logger.section('─'.repeat(60));
116
+ logger.newline();
117
+
118
+ // Merge remote environment variables with current process env
119
+ const mergedEnv = {
120
+ ...process.env,
121
+ ...envData.environment,
122
+ SAAC_ENV_LOADED: '1',
123
+ SAAC_APP_NAME: applicationName,
124
+ SAAC_APP_UUID: applicationUuid
125
+ };
126
+
127
+ // Determine shell arguments for interactive mode
128
+ let shellArgs = [];
129
+ if (shellName === 'bash' || shellName === 'sh') {
130
+ shellArgs = ['-i']; // Interactive mode
131
+ } else if (shellName === 'zsh') {
132
+ shellArgs = ['-i']; // Interactive mode
133
+ } else if (shellName === 'fish') {
134
+ shellArgs = ['-i']; // Interactive mode
135
+ }
136
+ // If custom shell, try -i flag (most shells support it)
137
+ else if (!userShell.includes('/')) {
138
+ shellArgs = ['-i'];
139
+ }
140
+
141
+ // Spawn shell directly with merged environment
142
+ const shellProc = spawn(userShell, shellArgs, {
143
+ stdio: 'inherit',
144
+ cwd: process.cwd(),
145
+ env: mergedEnv
146
+ });
147
+
148
+ shellProc.on('exit', (code) => {
149
+ logger.newline();
150
+ logger.success('✓ Shell closed, environment variables cleared');
151
+ process.exit(code || 0);
152
+ });
153
+
154
+ shellProc.on('error', (error) => {
155
+ logger.newline();
156
+ logger.error(`Failed to open shell: ${error.message}`);
157
+ process.exit(1);
158
+ });
159
+
160
+ } catch (error) {
161
+ logger.error(error.response?.data?.message || error.message);
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ module.exports = shell;
@@ -1,4 +1,90 @@
1
- // TODO: Implement whoami command
2
- module.exports = async function() {
3
- console.log('whoami command - Coming soon!');
4
- };
1
+ /**
2
+ * Whoami Command - Show current user information
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { isAuthenticated } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+
9
+ /**
10
+ * Display current authenticated user information
11
+ */
12
+ async function whoami() {
13
+ try {
14
+ // Check authentication
15
+ if (!isAuthenticated()) {
16
+ logger.error('Not logged in');
17
+ logger.newline();
18
+ logger.info('Run:');
19
+ logger.log(' saac login -e <email> -k <api-key>');
20
+ process.exit(1);
21
+ }
22
+
23
+ const spin = logger.spinner('Fetching user information...').start();
24
+
25
+ try {
26
+ const user = await api.getUserInfo();
27
+
28
+ spin.succeed('User information retrieved');
29
+
30
+ logger.newline();
31
+
32
+ logger.section('Current User');
33
+ logger.newline();
34
+
35
+ logger.field('Email', user.email);
36
+ logger.field('User ID', user.id);
37
+ logger.field('Verified', user.verified ? logger.chalk.green('Yes ✓') : logger.chalk.red('No ✗'));
38
+ logger.field('Member Since', formatDate(user.created_at));
39
+
40
+ logger.newline();
41
+
42
+ // Git Connections
43
+ if (user.git_connections && user.git_connections.length > 0) {
44
+ logger.info('Git Connections:');
45
+ for (const conn of user.git_connections) {
46
+ logger.log(` • ${conn.gitUsername} @ ${conn.gitHost} (${conn.providerType})`);
47
+ logger.log(` Connected: ${formatDate(conn.connectedAt)}`);
48
+ }
49
+ logger.newline();
50
+ }
51
+
52
+ // Quotas
53
+ logger.info('Quotas:');
54
+ logger.field(' Applications', `${user.application_count} / ${user.max_applications}`);
55
+
56
+ logger.newline();
57
+
58
+ logger.info('Commands:');
59
+ logger.log(' View applications: ' + logger.chalk.cyan('saac list'));
60
+ logger.log(' View status: ' + logger.chalk.cyan('saac status'));
61
+ logger.log(' Logout: ' + logger.chalk.cyan('saac logout'));
62
+
63
+ } catch (error) {
64
+ spin.fail('Failed to fetch user information');
65
+ throw error;
66
+ }
67
+
68
+ } catch (error) {
69
+ logger.error(error.response?.data?.message || error.message);
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Format ISO date string to readable format
76
+ * @param {string} isoString - ISO 8601 date string
77
+ * @returns {string} Formatted date
78
+ */
79
+ function formatDate(isoString) {
80
+ if (!isoString) return 'N/A';
81
+
82
+ const date = new Date(isoString);
83
+ return date.toLocaleDateString('en-US', {
84
+ year: 'numeric',
85
+ month: 'short',
86
+ day: 'numeric'
87
+ });
88
+ }
89
+
90
+ module.exports = whoami;