@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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.0.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;
@@ -10,65 +10,59 @@ const logger = require('../lib/logger');
10
10
 
11
11
  async function login(options) {
12
12
  try {
13
- logger.section('Login to StartAnAiCompany');
14
-
15
- // Get email
16
- let email = options.email;
17
- if (!email) {
18
- const answers = await inquirer.prompt([
19
- {
20
- type: 'input',
21
- name: 'email',
22
- message: 'Email address:',
23
- validate: (input) => {
24
- if (!validator.isEmail(input)) {
25
- return 'Please enter a valid email address';
26
- }
27
- return true;
28
- },
29
- },
30
- ]);
31
- email = answers.email;
13
+ // Require both email and API key flags (no interactive prompts)
14
+ if (!options.email || !options.apiKey) {
15
+ logger.error('Email and API key are required');
16
+ logger.newline();
17
+ logger.info('Usage:');
18
+ logger.log(' saac login -e <email> -k <api-key>');
19
+ logger.log(' saac login --email <email> --api-key <api-key>');
20
+ logger.newline();
21
+ logger.info('Example:');
22
+ logger.log(' saac login -e user@example.com -k cw_your_api_key');
23
+ process.exit(1);
32
24
  }
33
25
 
34
- // Get API key
35
- let apiKey = options.apiKey;
36
- if (!apiKey) {
37
- const answers = await inquirer.prompt([
38
- {
39
- type: 'password',
40
- name: 'apiKey',
41
- message: 'API Key:',
42
- mask: '*',
43
- },
44
- ]);
45
- apiKey = answers.apiKey;
26
+ const email = options.email;
27
+ const apiKey = options.apiKey;
28
+
29
+ // Validate email format
30
+ if (!validator.isEmail(email)) {
31
+ logger.error('Invalid email address');
32
+ process.exit(1);
46
33
  }
47
34
 
48
- // Verify credentials by getting user info
49
- const spin = logger.spinner('Verifying credentials...').start();
35
+ logger.section('Login to StartAnAiCompany');
50
36
 
51
- try {
52
- // Save temporarily to make API call
53
- saveUser({ email, apiKey });
37
+ // Login and get session token
38
+ const spin = logger.spinner('Logging in...').start();
54
39
 
55
- const userInfo = await api.getUserInfo();
40
+ try {
41
+ // Call /auth/login endpoint to get session token
42
+ const result = await api.login(email, apiKey);
56
43
 
57
44
  spin.succeed('Login successful!');
58
45
 
59
- // Update with full user info
46
+ // Save session token and expiration
60
47
  saveUser({
61
- email: userInfo.email,
62
- userId: userInfo.id,
63
- apiKey,
64
- verified: userInfo.email_verified,
48
+ email: result.user.email || email,
49
+ userId: result.user.id,
50
+ sessionToken: result.session_token,
51
+ expiresAt: result.expires_at,
52
+ verified: result.user.verified,
65
53
  });
66
54
 
67
55
  logger.newline();
68
56
  logger.success('You are now logged in!');
69
57
  logger.newline();
70
- logger.field('Email', userInfo.email);
71
- logger.field('Verified', userInfo.email_verified ? 'Yes' : 'No');
58
+ logger.field('Email', result.user.email || email);
59
+ logger.field('Verified', result.user.verified ? 'Yes' : 'No');
60
+
61
+ // Show expiration date
62
+ if (result.expires_at) {
63
+ const expirationDate = new Date(result.expires_at);
64
+ logger.field('Session expires', expirationDate.toLocaleDateString());
65
+ }
72
66
 
73
67
  } catch (error) {
74
68
  spin.fail('Login failed');
@@ -1,9 +1,47 @@
1
- const { clearUser } = require('../lib/config');
1
+ const api = require('../lib/api');
2
+ const { clearUser, getUser } = require('../lib/config');
2
3
  const logger = require('../lib/logger');
3
4
 
4
5
  async function logout() {
5
- clearUser();
6
- logger.success('Logged out successfully');
6
+ try {
7
+ const user = getUser();
8
+
9
+ if (!user || (!user.sessionToken && !user.apiKey)) {
10
+ logger.warn('You are not logged in');
11
+ return;
12
+ }
13
+
14
+ logger.section('Logout from StartAnAiCompany');
15
+
16
+ const spin = logger.spinner('Logging out...').start();
17
+
18
+ try {
19
+ // Try to revoke session on server
20
+ const client = api.createClient();
21
+ await client.post('/auth/logout');
22
+
23
+ // Clear local config
24
+ clearUser();
25
+
26
+ spin.succeed('Logout successful!');
27
+ logger.success('You have been logged out from this device');
28
+
29
+ } catch (error) {
30
+ // Even if server call fails, clear local config
31
+ clearUser();
32
+
33
+ if (error.response?.status === 401) {
34
+ // Session already invalid
35
+ spin.succeed('Logged out locally (session was expired)');
36
+ } else {
37
+ spin.warn('Logged out locally (server error)');
38
+ logger.warn('Session may still be active on server');
39
+ }
40
+ }
41
+ } catch (error) {
42
+ logger.error(error.message);
43
+ process.exit(1);
44
+ }
7
45
  }
8
46
 
9
47
  module.exports = logout;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Logout All command - Revoke all sessions
3
+ */
4
+
5
+ const inquirer = require('inquirer');
6
+ const api = require('../lib/api');
7
+ const { clearUser, isAuthenticated } = require('../lib/config');
8
+ const logger = require('../lib/logger');
9
+
10
+ async function logoutAll(options) {
11
+ try {
12
+ // Check authentication
13
+ if (!isAuthenticated()) {
14
+ logger.error('Not logged in');
15
+ process.exit(1);
16
+ }
17
+
18
+ logger.section('Logout from All Devices');
19
+
20
+ // Confirm unless --yes flag is provided
21
+ if (!options.yes) {
22
+ logger.warn('This will logout from ALL devices where you are logged in');
23
+ logger.newline();
24
+
25
+ const answers = await inquirer.prompt([
26
+ {
27
+ type: 'confirm',
28
+ name: 'confirm',
29
+ message: 'Are you sure you want to continue?',
30
+ default: false,
31
+ },
32
+ ]);
33
+
34
+ if (!answers.confirm) {
35
+ logger.info('Cancelled');
36
+ return;
37
+ }
38
+ }
39
+
40
+ const spin = logger.spinner('Revoking all sessions...').start();
41
+
42
+ try {
43
+ const client = api.createClient();
44
+ const response = await client.post('/auth/logout-all');
45
+
46
+ // Clear local config
47
+ clearUser();
48
+
49
+ const count = response.data.sessions_revoked || 0;
50
+ spin.succeed(`Logged out from ${count} device(s)!`);
51
+
52
+ logger.newline();
53
+ logger.success('All sessions have been revoked');
54
+ logger.info('You will need to login again on all devices');
55
+
56
+ } catch (error) {
57
+ // Even if server call fails, clear local config
58
+ clearUser();
59
+
60
+ if (error.response?.status === 401) {
61
+ spin.succeed('Logged out locally (session was expired)');
62
+ } else {
63
+ spin.fail('Failed to revoke sessions on server');
64
+ logger.warn('Logged out locally, but server sessions may still be active');
65
+ throw error;
66
+ }
67
+ }
68
+ } catch (error) {
69
+ logger.error(error.response?.data?.message || error.message);
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ module.exports = logoutAll;
@@ -10,40 +10,32 @@ const logger = require('../lib/logger');
10
10
 
11
11
  async function register(options) {
12
12
  try {
13
- logger.section('Register for StartAnAiCompany');
14
-
15
- // Get email (from flag or prompt)
16
- let email = options.email;
17
- if (!email) {
18
- const answers = await inquirer.prompt([
19
- {
20
- type: 'input',
21
- name: 'email',
22
- message: 'Email address:',
23
- validate: (input) => {
24
- if (!validator.isEmail(input)) {
25
- return 'Please enter a valid email address';
26
- }
27
- return true;
28
- },
29
- },
30
- ]);
31
- email = answers.email;
13
+ // Require email flag (no interactive prompt)
14
+ if (!options.email) {
15
+ logger.error('Email is required');
16
+ logger.newline();
17
+ logger.info('Usage:');
18
+ logger.log(' saac register -e <email>');
19
+ logger.log(' saac register --email <email> [--gitea-username <username>]');
20
+ logger.newline();
21
+ logger.info('Example:');
22
+ logger.log(' saac register -e user@example.com');
23
+ logger.log(' saac register -e user@example.com --gitea-username myuser');
24
+ process.exit(1);
32
25
  }
33
26
 
34
- // Get Gitea username (optional, auto-detected from git)
35
- let giteaUsername = options.giteaUsername;
36
- if (!giteaUsername) {
37
- const answers = await inquirer.prompt([
38
- {
39
- type: 'input',
40
- name: 'giteaUsername',
41
- message: 'Gitea username (optional, press Enter to skip):',
42
- },
43
- ]);
44
- giteaUsername = answers.giteaUsername || undefined;
27
+ const email = options.email;
28
+
29
+ // Validate email format
30
+ if (!validator.isEmail(email)) {
31
+ logger.error('Invalid email address');
32
+ process.exit(1);
45
33
  }
46
34
 
35
+ const giteaUsername = options.giteaUsername || undefined;
36
+
37
+ logger.section('Register for StartAnAiCompany');
38
+
47
39
  // Register via API
48
40
  const spin = logger.spinner('Creating account...').start();
49
41
 
@@ -53,12 +45,22 @@ async function register(options) {
53
45
  spin.succeed('Account created successfully!');
54
46
 
55
47
  // Save user info
56
- saveUser({
48
+ // If backend returns session_token, use it; otherwise fall back to api_key
49
+ const userData = {
57
50
  email: result.email || email,
58
51
  userId: result.user_id,
59
- apiKey: result.api_key,
60
52
  verified: result.verified || false,
61
- });
53
+ };
54
+
55
+ if (result.session_token) {
56
+ userData.sessionToken = result.session_token;
57
+ userData.expiresAt = result.expires_at;
58
+ } else {
59
+ // Backward compatibility: use API key if session token not provided
60
+ userData.apiKey = result.api_key;
61
+ }
62
+
63
+ saveUser(userData);
62
64
 
63
65
  logger.newline();
64
66
  logger.success('Registration complete!');
@@ -81,7 +83,17 @@ async function register(options) {
81
83
 
82
84
  logger.newline();
83
85
  logger.field('Email', email);
84
- logger.field('API Key', result.api_key.substring(0, 20) + '...');
86
+
87
+ // Show session token or API key info
88
+ if (result.session_token) {
89
+ logger.field('Session Token', result.session_token.substring(0, 20) + '...');
90
+ if (result.expires_at) {
91
+ const expirationDate = new Date(result.expires_at);
92
+ logger.field('Expires', expirationDate.toLocaleDateString());
93
+ }
94
+ } else if (result.api_key) {
95
+ logger.field('API Key', result.api_key.substring(0, 20) + '...');
96
+ }
85
97
 
86
98
  } catch (error) {
87
99
  spin.fail('Registration failed');