@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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.1.0",
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": "./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,160 @@
1
- // TODO: Implement init command
2
- module.exports = async function() {
3
- console.log('init command - Coming soon!');
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;
@@ -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;