@startanaicompany/cli 1.1.0 → 1.3.1
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 +145 -9
- package/README.md +75 -10
- package/bin/saac.js +59 -2
- package/create-application-update.md +759 -0
- package/package.json +2 -2
- package/src/commands/create.js +278 -4
- package/src/commands/init.js +160 -4
- package/src/commands/status.js +164 -4
- package/src/commands/update.js +284 -0
- package/src/lib/api.js +10 -0
- package/src/lib/config.js +8 -7
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startanaicompany/cli",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Official CLI for StartAnAiCompany.com - Deploy AI recruitment sites with ease",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"saac": "
|
|
7
|
+
"saac": "bin/saac.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"Error: no test specified\" && exit 1",
|
package/src/commands/create.js
CHANGED
|
@@ -1,4 +1,278 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Create command - Create a new application
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const api = require('../lib/api');
|
|
6
|
+
const { isAuthenticated, saveProjectConfig } = require('../lib/config');
|
|
7
|
+
const logger = require('../lib/logger');
|
|
8
|
+
|
|
9
|
+
async function create(name, 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
|
+
// Validate required fields
|
|
21
|
+
if (!name) {
|
|
22
|
+
logger.error('Application name is required');
|
|
23
|
+
logger.newline();
|
|
24
|
+
logger.info('Usage:');
|
|
25
|
+
logger.log(' saac create <name> [options]');
|
|
26
|
+
logger.newline();
|
|
27
|
+
logger.info('Required options:');
|
|
28
|
+
logger.log(' -s, --subdomain <subdomain> Subdomain for your app');
|
|
29
|
+
logger.log(' -r, --repository <url> Git repository URL (SSH format)');
|
|
30
|
+
logger.log(' -t, --git-token <token> Git API token');
|
|
31
|
+
logger.newline();
|
|
32
|
+
logger.info('Optional options:');
|
|
33
|
+
logger.log(' -b, --branch <branch> Git branch (default: master)');
|
|
34
|
+
logger.log(' -d, --domain-suffix <suffix> Domain suffix (default: startanaicompany.com)');
|
|
35
|
+
logger.log(' -p, --port <port> Port to expose (default: 3000)');
|
|
36
|
+
logger.log(' --build-pack <pack> Build pack: dockercompose, nixpacks, dockerfile, static');
|
|
37
|
+
logger.log(' --install-cmd <command> Install command (e.g., "pnpm install")');
|
|
38
|
+
logger.log(' --build-cmd <command> Build command (e.g., "npm run build")');
|
|
39
|
+
logger.log(' --start-cmd <command> Start command (e.g., "node server.js")');
|
|
40
|
+
logger.log(' --pre-deploy-cmd <command> Pre-deployment command (e.g., "npm run migrate")');
|
|
41
|
+
logger.log(' --post-deploy-cmd <command> Post-deployment command (e.g., "npm run seed")');
|
|
42
|
+
logger.log(' --health-check Enable health checks');
|
|
43
|
+
logger.log(' --health-path <path> Health check path (default: /health)');
|
|
44
|
+
logger.log(' --health-interval <seconds> Health check interval in seconds');
|
|
45
|
+
logger.log(' --health-timeout <seconds> Health check timeout in seconds');
|
|
46
|
+
logger.log(' --health-retries <count> Health check retries (1-10)');
|
|
47
|
+
logger.log(' --cpu-limit <limit> CPU limit (e.g., "1", "2.5")');
|
|
48
|
+
logger.log(' --memory-limit <limit> Memory limit (e.g., "512M", "2G")');
|
|
49
|
+
logger.log(' --env <KEY=VALUE> Environment variable (can be used multiple times)');
|
|
50
|
+
logger.newline();
|
|
51
|
+
logger.info('Example:');
|
|
52
|
+
logger.log(' saac create my-app -s myapp -r git@git.startanaicompany.com:user/repo.git -t abc123');
|
|
53
|
+
logger.log(' saac create api -s api -r git@git... -t abc123 --build-pack nixpacks --port 8080');
|
|
54
|
+
logger.log(' saac create web -s web -r git@git... -t abc123 --health-check --pre-deploy-cmd "npm run migrate"');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!options.subdomain || !options.repository || !options.gitToken) {
|
|
59
|
+
logger.error('Missing required options: subdomain, repository, and git-token are required');
|
|
60
|
+
logger.newline();
|
|
61
|
+
logger.info('Example:');
|
|
62
|
+
logger.log(` saac create ${name} -s myapp -r git@git.startanaicompany.com:user/repo.git -t your_token`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logger.section(`Creating Application: ${name}`);
|
|
67
|
+
logger.newline();
|
|
68
|
+
|
|
69
|
+
// Build application payload
|
|
70
|
+
const appData = {
|
|
71
|
+
name: name,
|
|
72
|
+
subdomain: options.subdomain,
|
|
73
|
+
domain_suffix: options.domainSuffix || 'startanaicompany.com',
|
|
74
|
+
git_repository: options.repository,
|
|
75
|
+
git_branch: options.branch || 'master',
|
|
76
|
+
git_api_token: options.gitToken,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Optional: Port configuration
|
|
80
|
+
if (options.port) {
|
|
81
|
+
appData.ports_exposes = options.port;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Optional: Build pack
|
|
85
|
+
if (options.buildPack) {
|
|
86
|
+
const validBuildPacks = ['dockercompose', 'nixpacks', 'dockerfile', 'static'];
|
|
87
|
+
if (!validBuildPacks.includes(options.buildPack)) {
|
|
88
|
+
logger.error(`Invalid build pack: ${options.buildPack}`);
|
|
89
|
+
logger.info(`Must be one of: ${validBuildPacks.join(', ')}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
appData.build_pack = options.buildPack;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Optional: Custom commands
|
|
96
|
+
if (options.installCmd) {
|
|
97
|
+
appData.install_command = options.installCmd;
|
|
98
|
+
}
|
|
99
|
+
if (options.buildCmd) {
|
|
100
|
+
appData.build_command = options.buildCmd;
|
|
101
|
+
}
|
|
102
|
+
if (options.startCmd) {
|
|
103
|
+
appData.start_command = options.startCmd;
|
|
104
|
+
}
|
|
105
|
+
if (options.preDeployCmd) {
|
|
106
|
+
appData.pre_deployment_command = options.preDeployCmd;
|
|
107
|
+
}
|
|
108
|
+
if (options.postDeployCmd) {
|
|
109
|
+
appData.post_deployment_command = options.postDeployCmd;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Optional: Resource limits
|
|
113
|
+
if (options.cpuLimit) {
|
|
114
|
+
appData.cpu_limit = options.cpuLimit;
|
|
115
|
+
}
|
|
116
|
+
if (options.memoryLimit) {
|
|
117
|
+
appData.memory_limit = options.memoryLimit;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Optional: Health check configuration
|
|
121
|
+
if (options.healthCheck) {
|
|
122
|
+
appData.health_check_enabled = true;
|
|
123
|
+
if (options.healthPath) {
|
|
124
|
+
appData.health_check_path = options.healthPath;
|
|
125
|
+
}
|
|
126
|
+
if (options.healthInterval) {
|
|
127
|
+
appData.health_check_interval = parseInt(options.healthInterval, 10);
|
|
128
|
+
}
|
|
129
|
+
if (options.healthTimeout) {
|
|
130
|
+
appData.health_check_timeout = parseInt(options.healthTimeout, 10);
|
|
131
|
+
}
|
|
132
|
+
if (options.healthRetries) {
|
|
133
|
+
const retries = parseInt(options.healthRetries, 10);
|
|
134
|
+
if (retries < 1 || retries > 10) {
|
|
135
|
+
logger.error('Health check retries must be between 1 and 10');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
appData.health_check_retries = retries;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Optional: Environment variables
|
|
143
|
+
if (options.env) {
|
|
144
|
+
const envVars = {};
|
|
145
|
+
const envArray = Array.isArray(options.env) ? options.env : [options.env];
|
|
146
|
+
|
|
147
|
+
for (const envStr of envArray) {
|
|
148
|
+
const [key, ...valueParts] = envStr.split('=');
|
|
149
|
+
const value = valueParts.join('='); // Handle values with '=' in them
|
|
150
|
+
|
|
151
|
+
if (!key || value === undefined) {
|
|
152
|
+
logger.error(`Invalid environment variable format: ${envStr}`);
|
|
153
|
+
logger.info('Use format: KEY=VALUE');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
envVars[key] = value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (Object.keys(envVars).length > 50) {
|
|
161
|
+
logger.error('Maximum 50 environment variables allowed');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
appData.environment_variables = envVars;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Show configuration summary
|
|
169
|
+
logger.info('Configuration:');
|
|
170
|
+
logger.field('Name', appData.name);
|
|
171
|
+
logger.field('Subdomain', `${appData.subdomain}.${appData.domain_suffix}`);
|
|
172
|
+
logger.field('Repository', appData.git_repository);
|
|
173
|
+
logger.field('Branch', appData.git_branch);
|
|
174
|
+
if (appData.ports_exposes) {
|
|
175
|
+
logger.field('Port', appData.ports_exposes);
|
|
176
|
+
}
|
|
177
|
+
if (appData.build_pack) {
|
|
178
|
+
logger.field('Build Pack', appData.build_pack);
|
|
179
|
+
}
|
|
180
|
+
if (appData.cpu_limit || appData.memory_limit) {
|
|
181
|
+
const limits = [];
|
|
182
|
+
if (appData.cpu_limit) limits.push(`CPU: ${appData.cpu_limit}`);
|
|
183
|
+
if (appData.memory_limit) limits.push(`Memory: ${appData.memory_limit}`);
|
|
184
|
+
logger.field('Resource Limits', limits.join(', '));
|
|
185
|
+
logger.warn('Note: Free tier limited to 1 vCPU, 1024M RAM');
|
|
186
|
+
}
|
|
187
|
+
if (appData.health_check_enabled) {
|
|
188
|
+
logger.field('Health Check', `Enabled on ${appData.health_check_path || '/health'}`);
|
|
189
|
+
}
|
|
190
|
+
if (appData.pre_deployment_command) {
|
|
191
|
+
logger.field('Pre-Deploy Hook', appData.pre_deployment_command);
|
|
192
|
+
}
|
|
193
|
+
if (appData.environment_variables) {
|
|
194
|
+
logger.field('Environment Vars', `${Object.keys(appData.environment_variables).length} variable(s)`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
logger.newline();
|
|
198
|
+
|
|
199
|
+
const spin = logger.spinner('Creating application...').start();
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const result = await api.createApplication(appData);
|
|
203
|
+
|
|
204
|
+
spin.succeed('Application created successfully!');
|
|
205
|
+
|
|
206
|
+
// Save project configuration
|
|
207
|
+
saveProjectConfig({
|
|
208
|
+
applicationUuid: result.coolify_app_uuid,
|
|
209
|
+
applicationName: result.app_name,
|
|
210
|
+
subdomain: result.subdomain,
|
|
211
|
+
domainSuffix: appData.domain_suffix,
|
|
212
|
+
gitRepository: appData.git_repository,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
logger.newline();
|
|
216
|
+
logger.success('Application created!');
|
|
217
|
+
logger.newline();
|
|
218
|
+
logger.field('Name', result.app_name);
|
|
219
|
+
logger.field('Domain', result.domain);
|
|
220
|
+
logger.field('UUID', result.coolify_app_uuid);
|
|
221
|
+
logger.field('Status', result.deployment_status);
|
|
222
|
+
logger.newline();
|
|
223
|
+
|
|
224
|
+
// Show next steps
|
|
225
|
+
if (result.next_steps && result.next_steps.length > 0) {
|
|
226
|
+
logger.info('Next Steps:');
|
|
227
|
+
result.next_steps.forEach((step, index) => {
|
|
228
|
+
logger.log(` ${index + 1}. ${step}`);
|
|
229
|
+
});
|
|
230
|
+
logger.newline();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
logger.info('Useful commands:');
|
|
234
|
+
logger.log(` saac deploy Deploy your application`);
|
|
235
|
+
logger.log(` saac logs --follow View deployment logs`);
|
|
236
|
+
logger.log(` saac status Check application status`);
|
|
237
|
+
logger.log(` saac env set KEY=VALUE Set environment variables`);
|
|
238
|
+
|
|
239
|
+
} catch (error) {
|
|
240
|
+
spin.fail('Application creation failed');
|
|
241
|
+
|
|
242
|
+
if (error.response?.status === 403) {
|
|
243
|
+
const data = error.response.data;
|
|
244
|
+
logger.newline();
|
|
245
|
+
logger.error('Quota exceeded');
|
|
246
|
+
if (data.current_tier) {
|
|
247
|
+
logger.field('Current Tier', data.current_tier);
|
|
248
|
+
}
|
|
249
|
+
logger.newline();
|
|
250
|
+
logger.warn(data.error || data.message);
|
|
251
|
+
if (data.upgrade_info) {
|
|
252
|
+
logger.info(data.upgrade_info);
|
|
253
|
+
}
|
|
254
|
+
} else if (error.response?.status === 400) {
|
|
255
|
+
const data = error.response.data;
|
|
256
|
+
logger.newline();
|
|
257
|
+
logger.error('Validation failed');
|
|
258
|
+
if (data.details) {
|
|
259
|
+
logger.newline();
|
|
260
|
+
Object.entries(data.details).forEach(([field, message]) => {
|
|
261
|
+
logger.log(` ${logger.chalk.yellow(field)}: ${message}`);
|
|
262
|
+
});
|
|
263
|
+
} else {
|
|
264
|
+
logger.log(` ${data.message || data.error}`);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
} catch (error) {
|
|
273
|
+
logger.error(error.response?.data?.message || error.message);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = create;
|
package/src/commands/init.js
CHANGED
|
@@ -1,4 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Init command - Initialize SAAC project in current directory
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. Interactive: Select from existing applications (no options provided)
|
|
6
|
+
* 2. Create: Create new application and link to directory (options provided)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const api = require('../lib/api');
|
|
10
|
+
const { isAuthenticated, saveProjectConfig, getProjectConfig } = require('../lib/config');
|
|
11
|
+
const logger = require('../lib/logger');
|
|
12
|
+
const inquirer = require('inquirer');
|
|
13
|
+
|
|
14
|
+
async function init(options) {
|
|
15
|
+
try {
|
|
16
|
+
// Check authentication
|
|
17
|
+
if (!isAuthenticated()) {
|
|
18
|
+
logger.error('Not logged in');
|
|
19
|
+
logger.newline();
|
|
20
|
+
logger.info('Run:');
|
|
21
|
+
logger.log(' saac login -e <email> -k <api-key>');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
logger.section('Initialize SAAC Project');
|
|
26
|
+
logger.newline();
|
|
27
|
+
|
|
28
|
+
// Check if already initialized
|
|
29
|
+
const existingConfig = getProjectConfig();
|
|
30
|
+
if (existingConfig) {
|
|
31
|
+
logger.warn('This directory is already linked to an application');
|
|
32
|
+
logger.newline();
|
|
33
|
+
logger.field('Application', existingConfig.applicationName);
|
|
34
|
+
logger.field('UUID', existingConfig.applicationUuid);
|
|
35
|
+
logger.field('Domain', `${existingConfig.subdomain}.${existingConfig.domainSuffix}`);
|
|
36
|
+
logger.newline();
|
|
37
|
+
|
|
38
|
+
const { overwrite } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'confirm',
|
|
41
|
+
name: 'overwrite',
|
|
42
|
+
message: 'Do you want to re-initialize this directory?',
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
if (!overwrite) {
|
|
48
|
+
logger.info('Keeping existing configuration');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
logger.newline();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Determine mode: Create new app OR link existing app
|
|
56
|
+
const hasCreateOptions = options.name || options.subdomain || options.repository;
|
|
57
|
+
|
|
58
|
+
if (hasCreateOptions) {
|
|
59
|
+
// CREATE MODE: Create a new application
|
|
60
|
+
await createAndInitialize(options);
|
|
61
|
+
} else {
|
|
62
|
+
// INTERACTIVE MODE: Link existing application
|
|
63
|
+
await linkExistingApplication();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.error(error.response?.data?.message || error.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new application and initialize directory
|
|
74
|
+
*/
|
|
75
|
+
async function createAndInitialize(options) {
|
|
76
|
+
logger.error('Create mode not yet implemented');
|
|
77
|
+
logger.newline();
|
|
78
|
+
logger.info('To create a new application, use:');
|
|
79
|
+
logger.log(' saac create <name> -s <subdomain> -r <repository> -t <git-token>');
|
|
80
|
+
logger.newline();
|
|
81
|
+
logger.info('To link an existing application, run:');
|
|
82
|
+
logger.log(' saac init');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
|
|
85
|
+
// TODO: Implement create-and-init flow
|
|
86
|
+
// This would call the create command functionality, then save the config
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Link an existing application to current directory (interactive)
|
|
91
|
+
*/
|
|
92
|
+
async function linkExistingApplication() {
|
|
93
|
+
|
|
94
|
+
// Fetch user's applications
|
|
95
|
+
const spin = logger.spinner('Fetching your applications...').start();
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await api.listApplications();
|
|
99
|
+
const applications = Array.isArray(result) ? result : (result.applications || []);
|
|
100
|
+
|
|
101
|
+
spin.succeed(`Found ${applications.length} application(s)`);
|
|
102
|
+
|
|
103
|
+
if (applications.length === 0) {
|
|
104
|
+
logger.newline();
|
|
105
|
+
logger.warn('You have no applications yet');
|
|
106
|
+
logger.newline();
|
|
107
|
+
logger.info('Create one with:');
|
|
108
|
+
logger.log(' saac create <name> -s <subdomain> -r <repository> -t <git-token>');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logger.newline();
|
|
113
|
+
|
|
114
|
+
// Interactive: Let user select application
|
|
115
|
+
const choices = applications.map(app => ({
|
|
116
|
+
name: `${app.name} - ${app.domain || `${app.subdomain}.startanaicompany.com`} (${app.status})`,
|
|
117
|
+
value: app,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const { selectedApp } = await inquirer.prompt([
|
|
121
|
+
{
|
|
122
|
+
type: 'list',
|
|
123
|
+
name: 'selectedApp',
|
|
124
|
+
message: 'Select application to link to this directory:',
|
|
125
|
+
choices: choices,
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
logger.newline();
|
|
130
|
+
|
|
131
|
+
// Save project configuration
|
|
132
|
+
saveProjectConfig({
|
|
133
|
+
applicationUuid: selectedApp.uuid,
|
|
134
|
+
applicationName: selectedApp.name,
|
|
135
|
+
subdomain: selectedApp.subdomain,
|
|
136
|
+
domainSuffix: selectedApp.domain_suffix || 'startanaicompany.com',
|
|
137
|
+
gitRepository: selectedApp.git_repository,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
logger.success('Project initialized!');
|
|
141
|
+
logger.newline();
|
|
142
|
+
logger.field('Application', selectedApp.name);
|
|
143
|
+
logger.field('UUID', selectedApp.uuid);
|
|
144
|
+
logger.field('Domain', selectedApp.domain || `${selectedApp.subdomain}.startanaicompany.com`);
|
|
145
|
+
logger.field('Status', selectedApp.status);
|
|
146
|
+
logger.newline();
|
|
147
|
+
|
|
148
|
+
logger.info('You can now use:');
|
|
149
|
+
logger.log(' saac deploy Deploy your application');
|
|
150
|
+
logger.log(' saac logs --follow View deployment logs');
|
|
151
|
+
logger.log(' saac status Check application status');
|
|
152
|
+
logger.log(' saac update --port 8080 Update configuration');
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
spin.fail('Failed to fetch applications');
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = init;
|
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;
|