@startanaicompany/cli 1.0.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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Sessions command - List active sessions
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { isAuthenticated } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+ const { table } = require('table');
9
+
10
+ async function sessions() {
11
+ try {
12
+ // Check authentication
13
+ if (!isAuthenticated()) {
14
+ logger.error('Not logged in');
15
+ logger.newline();
16
+ logger.info('Run:');
17
+ logger.log(' saac login -e <email> -k <api-key>');
18
+ process.exit(1);
19
+ }
20
+
21
+ logger.section('Active Sessions');
22
+
23
+ const spin = logger.spinner('Fetching sessions...').start();
24
+
25
+ try {
26
+ const client = api.createClient();
27
+ const response = await client.get('/auth/sessions');
28
+ const { sessions: sessionList, total } = response.data;
29
+
30
+ spin.succeed(`Found ${total} active session(s)`);
31
+
32
+ if (total === 0) {
33
+ logger.newline();
34
+ logger.info('No active sessions');
35
+ return;
36
+ }
37
+
38
+ logger.newline();
39
+
40
+ // Format sessions as table
41
+ const data = [
42
+ ['Created', 'Last Used', 'IP Address', 'Expires'],
43
+ ];
44
+
45
+ sessionList.forEach((session) => {
46
+ const created = new Date(session.created_at).toLocaleDateString();
47
+ const lastUsed = new Date(session.last_used_at).toLocaleString();
48
+ const expires = new Date(session.expires_at).toLocaleDateString();
49
+ const ip = session.created_ip || 'Unknown';
50
+
51
+ data.push([created, lastUsed, ip, expires]);
52
+ });
53
+
54
+ console.log(table(data, {
55
+ header: {
56
+ alignment: 'center',
57
+ content: `${total} Active Session(s)`,
58
+ },
59
+ }));
60
+
61
+ logger.newline();
62
+ logger.info('Tip: Use `saac logout` to logout from current device');
63
+ logger.info(' Use `saac logout-all` to logout from all devices');
64
+
65
+ } catch (error) {
66
+ spin.fail('Failed to fetch sessions');
67
+ throw error;
68
+ }
69
+ } catch (error) {
70
+ logger.error(error.response?.data?.message || error.message);
71
+ process.exit(1);
72
+ }
73
+ }
74
+
75
+ module.exports = sessions;
@@ -1,4 +1,164 @@
1
- // TODO: Implement status command
2
- module.exports = async function() {
3
- console.log('status command - Coming soon!');
4
- };
1
+ /**
2
+ * Status command - Show current login and account status
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { getUser, isAuthenticated, isTokenExpiringSoon } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+ const { table } = require('table');
9
+
10
+ async function status() {
11
+ try {
12
+ logger.section('SAAC Status');
13
+ logger.newline();
14
+
15
+ // Check if logged in locally (silently)
16
+ if (!isAuthenticated()) {
17
+ logger.error('Not logged in');
18
+ logger.newline();
19
+ logger.info('Run:');
20
+ logger.log(' saac login -e <email> -k <api-key>');
21
+ process.exit(1);
22
+ }
23
+
24
+ const user = getUser();
25
+
26
+ // Verify session with server first
27
+ const spin = logger.spinner('Verifying session...').start();
28
+
29
+ try {
30
+ const client = api.createClient();
31
+ const [userInfo, appsResponse] = await Promise.all([
32
+ client.get('/users/me'),
33
+ client.get('/applications')
34
+ ]);
35
+
36
+ spin.succeed('Session verified');
37
+
38
+ const userData = userInfo.data;
39
+ const applicationsData = appsResponse.data;
40
+ const applications = Array.isArray(applicationsData) ? applicationsData :
41
+ (applicationsData.applications || []);
42
+
43
+ logger.newline();
44
+
45
+ // NOW show login status (after successful verification)
46
+ logger.field('Status', logger.chalk.green('✓ Logged in'));
47
+ logger.field('Email', user.email);
48
+ logger.field('Verified', user.verified ? logger.chalk.green('Yes') : logger.chalk.red('No'));
49
+
50
+ // Show session info
51
+ if (user.sessionToken) {
52
+ const expiresAt = new Date(user.expiresAt);
53
+ const now = new Date();
54
+ const daysUntilExpiry = Math.floor((expiresAt - now) / (1000 * 60 * 60 * 24));
55
+
56
+ logger.field('Session expires', expiresAt.toLocaleDateString());
57
+
58
+ if (isTokenExpiringSoon()) {
59
+ logger.field('Warning', logger.chalk.yellow(`⚠ Session expires in ${daysUntilExpiry} days`));
60
+ } else {
61
+ logger.field('Days until expiry', daysUntilExpiry);
62
+ }
63
+ }
64
+
65
+ logger.newline();
66
+
67
+ // Show account info
68
+ logger.field('User ID', userData.id);
69
+ logger.field('Git Username', userData.git_username || 'Not set');
70
+ logger.field('Applications', `${userData.application_count} / ${userData.max_applications}`);
71
+
72
+ logger.newline();
73
+
74
+ // Show applications (max 5)
75
+ if (applications.length === 0) {
76
+ logger.info('No applications yet');
77
+ logger.newline();
78
+ logger.info('Create one with: ' + logger.chalk.cyan('saac create <name>'));
79
+ } else {
80
+ const displayApps = applications.slice(0, 5);
81
+ const hasMore = applications.length > 5;
82
+
83
+ const data = [
84
+ ['Name', 'Domain', 'Status', 'Created'],
85
+ ];
86
+
87
+ displayApps.forEach((app) => {
88
+ const created = new Date(app.created_at).toLocaleDateString();
89
+ const status = app.status || 'unknown';
90
+
91
+ // Status with icons (handle both Coolify format and documented format)
92
+ let statusDisplay;
93
+ if (status.startsWith('running')) {
94
+ statusDisplay = logger.chalk.green('Running ✓');
95
+ } else if (status.startsWith('stopped')) {
96
+ statusDisplay = logger.chalk.yellow('Stopped');
97
+ } else {
98
+ switch (status) {
99
+ case 'active':
100
+ statusDisplay = logger.chalk.green('Active ✓');
101
+ break;
102
+ case 'creating':
103
+ statusDisplay = logger.chalk.yellow('Creating...');
104
+ break;
105
+ case 'error':
106
+ statusDisplay = logger.chalk.red('Error ✗');
107
+ break;
108
+ case 'suspended':
109
+ statusDisplay = logger.chalk.yellow('Suspended ⚠');
110
+ break;
111
+ default:
112
+ statusDisplay = logger.chalk.gray(status);
113
+ }
114
+ }
115
+
116
+ data.push([
117
+ app.name,
118
+ app.domain || `${app.subdomain}.startanaicompany.com`,
119
+ statusDisplay,
120
+ created
121
+ ]);
122
+ });
123
+
124
+ console.log(table(data, {
125
+ header: {
126
+ alignment: 'center',
127
+ content: `Applications (showing ${displayApps.length} of ${applications.length})`,
128
+ },
129
+ }));
130
+
131
+ if (hasMore) {
132
+ logger.warn(`Showing first 5 applications only. You have ${applications.length - 5} more.`);
133
+ logger.info('Run ' + logger.chalk.cyan('saac list') + ' to see all applications');
134
+ }
135
+ }
136
+
137
+ } catch (error) {
138
+ spin.fail('Session verification failed');
139
+
140
+ logger.newline();
141
+
142
+ if (error.response?.status === 401) {
143
+ logger.error('Your session has expired or is invalid');
144
+ logger.newline();
145
+ logger.field('Email', user.email);
146
+ logger.field('Local session expires', new Date(user.expiresAt).toLocaleDateString());
147
+ logger.newline();
148
+ logger.warn('The session token is no longer valid on the server');
149
+ logger.info('Please login again:');
150
+ logger.log(' saac login -e ' + user.email + ' -k <api-key>');
151
+ } else {
152
+ logger.error('Failed to connect to server');
153
+ logger.error(error.message);
154
+ }
155
+ process.exit(1);
156
+ }
157
+
158
+ } catch (error) {
159
+ logger.error(error.response?.data?.message || error.message);
160
+ process.exit(1);
161
+ }
162
+ }
163
+
164
+ module.exports = status;
@@ -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;
@@ -8,10 +8,24 @@ const logger = require('../lib/logger');
8
8
 
9
9
  async function verify(code) {
10
10
  try {
11
+ if (!code) {
12
+ logger.error('Verification code is required');
13
+ logger.newline();
14
+ logger.info('Usage:');
15
+ logger.log(' saac verify <code>');
16
+ logger.newline();
17
+ logger.info('Example:');
18
+ logger.log(' saac verify 123456');
19
+ process.exit(1);
20
+ }
21
+
11
22
  const user = getUser();
12
23
 
13
24
  if (!user || !user.email) {
14
- logger.error('No user found. Please register first with: saac register');
25
+ logger.error('No user found. Please register first');
26
+ logger.newline();
27
+ logger.info('Run:');
28
+ logger.log(' saac register -e <email>');
15
29
  process.exit(1);
16
30
  }
17
31
 
@@ -29,16 +43,30 @@ async function verify(code) {
29
43
 
30
44
  spin.succeed('Email verified successfully!');
31
45
 
32
- // Update user verification status
33
- saveUser({
46
+ // Update user verification status and session token if provided
47
+ const updatedUser = {
34
48
  ...user,
35
49
  verified: true,
36
- });
50
+ };
51
+
52
+ // If backend returns new session token after verification, update it
53
+ if (result.session_token) {
54
+ updatedUser.sessionToken = result.session_token;
55
+ updatedUser.expiresAt = result.expires_at;
56
+ }
57
+
58
+ saveUser(updatedUser);
37
59
 
38
60
  logger.newline();
39
61
  logger.success('Your account is now verified!');
40
62
  logger.info('You can now create and deploy applications.');
41
63
 
64
+ // Show expiration if session token was updated
65
+ if (result.expires_at) {
66
+ const expirationDate = new Date(result.expires_at);
67
+ logger.field('Session expires', expirationDate.toLocaleDateString());
68
+ }
69
+
42
70
  } catch (error) {
43
71
  spin.fail('Verification failed');
44
72
  throw error;
package/src/lib/api.js CHANGED
@@ -3,22 +3,56 @@
3
3
  */
4
4
 
5
5
  const axios = require('axios');
6
+ const os = require('os');
6
7
  const { getApiUrl, getUser } = require('./config');
8
+ const pkg = require('../../package.json');
7
9
 
8
10
  /**
9
11
  * Create axios instance with base configuration
10
12
  */
11
13
  function createClient() {
12
14
  const user = getUser();
15
+ const envApiKey = process.env.SAAC_API_KEY; // For CI/CD
16
+
17
+ const headers = {
18
+ 'Content-Type': 'application/json',
19
+ 'User-Agent': `saac-cli/${pkg.version} (${os.platform()}; ${os.arch()})`,
20
+ };
21
+
22
+ // Priority order:
23
+ // 1. Environment variable (for CI/CD, scripts)
24
+ // 2. Session token (for CLI users)
25
+ // 3. API key (backward compatibility)
26
+ if (envApiKey) {
27
+ headers['X-API-Key'] = envApiKey;
28
+ } else if (user?.sessionToken) {
29
+ headers['X-Session-Token'] = user.sessionToken;
30
+ } else if (user?.apiKey) {
31
+ headers['X-API-Key'] = user.apiKey;
32
+ }
13
33
 
14
34
  return axios.create({
35
+ baseURL: getApiUrl(),
36
+ timeout: 30000,
37
+ headers,
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Login and get session token
43
+ */
44
+ async function login(email, apiKey) {
45
+ const client = axios.create({
15
46
  baseURL: getApiUrl(),
16
47
  timeout: 30000,
17
48
  headers: {
18
49
  'Content-Type': 'application/json',
19
- ...(user?.apiKey && { 'X-API-Key': user.apiKey }),
50
+ 'X-API-Key': apiKey, // Use API key for login
20
51
  },
21
52
  });
53
+
54
+ const response = await client.post('/auth/login', { email });
55
+ return response.data;
22
56
  }
23
57
 
24
58
  /**
@@ -99,6 +133,15 @@ async function getApplicationLogs(uuid, params = {}) {
99
133
  return response.data;
100
134
  }
101
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
+
102
145
  /**
103
146
  * Update environment variables
104
147
  */
@@ -141,10 +184,13 @@ async function healthCheck() {
141
184
  }
142
185
 
143
186
  module.exports = {
187
+ createClient,
188
+ login,
144
189
  register,
145
190
  verifyEmail,
146
191
  getUserInfo,
147
192
  createApplication,
193
+ updateApplication,
148
194
  listApplications,
149
195
  getApplication,
150
196
  deployApplication,