@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.
- package/CLAUDE.md +379 -29
- package/bin/saac.js +58 -4
- package/package.json +1 -1
- package/src/commands/delete.js +191 -4
- package/src/commands/deployments.js +130 -0
- package/src/commands/domain.js +204 -3
- package/src/commands/env.js +264 -3
- package/src/commands/exec.js +277 -0
- package/src/commands/logs.js +232 -4
- package/src/commands/run.js +170 -0
- package/src/commands/shell.js +166 -0
- package/src/commands/whoami.js +90 -4
- package/src/lib/api.js +54 -0
package/src/commands/logs.js
CHANGED
|
@@ -1,4 +1,232 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
package/src/commands/whoami.js
CHANGED
|
@@ -1,4 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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;
|