@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.
- package/CLAUDE.md +358 -0
- package/README.md +98 -14
- package/auth_session_update.md +785 -0
- package/bin/saac.js +73 -3
- package/create-application-update.md +759 -0
- package/package.json +2 -2
- package/src/commands/create.js +278 -4
- package/src/commands/login.js +38 -44
- package/src/commands/logout.js +41 -3
- package/src/commands/logoutAll.js +74 -0
- package/src/commands/register.js +46 -34
- package/src/commands/sessions.js +75 -0
- package/src/commands/status.js +164 -4
- package/src/commands/update.js +284 -0
- package/src/commands/verify.js +32 -4
- package/src/lib/api.js +47 -1
- package/src/lib/config.js +60 -11
- package/test-session-token.js +117 -0
|
@@ -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;
|
package/src/commands/status.js
CHANGED
|
@@ -1,4 +1,164 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
package/src/commands/verify.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|