@startanaicompany/cli 1.1.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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
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": "./bin/saac.js"
7
+ "saac": "bin/saac.js"
8
8
  },
9
9
  "scripts": {
10
10
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -1,4 +1,278 @@
1
- // TODO: Implement create command
2
- module.exports = async function() {
3
- console.log('create command - Coming soon!');
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;
@@ -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;