@startanaicompany/cli 1.0.0 → 1.1.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/bin/saac.js CHANGED
@@ -14,6 +14,8 @@ const register = require('../src/commands/register');
14
14
  const login = require('../src/commands/login');
15
15
  const verify = require('../src/commands/verify');
16
16
  const logout = require('../src/commands/logout');
17
+ const logoutAll = require('../src/commands/logoutAll');
18
+ const sessions = require('../src/commands/sessions');
17
19
  const init = require('../src/commands/init');
18
20
  const create = require('../src/commands/create');
19
21
  const deploy = require('../src/commands/deploy');
@@ -54,9 +56,20 @@ program
54
56
 
55
57
  program
56
58
  .command('logout')
57
- .description('Clear saved credentials')
59
+ .description('Logout from current device')
58
60
  .action(logout);
59
61
 
62
+ program
63
+ .command('logout-all')
64
+ .description('Logout from all devices (revoke all sessions)')
65
+ .option('-y, --yes', 'Skip confirmation')
66
+ .action(logoutAll);
67
+
68
+ program
69
+ .command('sessions')
70
+ .description('List all active sessions')
71
+ .action(sessions);
72
+
60
73
  // Application management
61
74
  program
62
75
  .command('init')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Official CLI for StartAnAiCompany.com - Deploy AI recruitment sites with ease",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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');
@@ -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;
@@ -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 with: saac register');
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
- saveUser({
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
- ...(user?.apiKey && { 'X-API-Key': user.apiKey }),
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
  /**
@@ -141,6 +175,8 @@ async function healthCheck() {
141
175
  }
142
176
 
143
177
  module.exports = {
178
+ createClient,
179
+ login,
144
180
  register,
145
181
  verifyEmail,
146
182
  getUserInfo,
package/src/lib/config.js CHANGED
@@ -51,12 +51,58 @@ function saveProjectConfig(config) {
51
51
  }
52
52
 
53
53
  /**
54
- * Check if user is authenticated
54
+ * Check if user is authenticated and token is valid
55
55
  */
56
56
  function isAuthenticated() {
57
- const apiKey = globalConfig.get('user.apiKey');
58
- const email = globalConfig.get('user.email');
59
- return !!(apiKey && email);
57
+ const user = getUser();
58
+
59
+ if (!user || !user.email) {
60
+ return false;
61
+ }
62
+
63
+ // Check for session token
64
+ if (user.sessionToken) {
65
+ // Check if token is expired
66
+ if (user.expiresAt) {
67
+ const expirationDate = new Date(user.expiresAt);
68
+ const now = new Date();
69
+
70
+ if (now >= expirationDate) {
71
+ return false; // Token expired
72
+ }
73
+ }
74
+ return true;
75
+ }
76
+
77
+ // Fallback: Check for API key (for backward compatibility)
78
+ return !!user.apiKey;
79
+ }
80
+
81
+ /**
82
+ * Check if session token is expired
83
+ */
84
+ function isTokenExpired() {
85
+ const user = getUser();
86
+ if (!user?.expiresAt) return false;
87
+
88
+ const expirationDate = new Date(user.expiresAt);
89
+ const now = new Date();
90
+
91
+ return now >= expirationDate;
92
+ }
93
+
94
+ /**
95
+ * Check if token expires soon (within 7 days)
96
+ */
97
+ function isTokenExpiringSoon() {
98
+ const user = getUser();
99
+ if (!user?.expiresAt) return false;
100
+
101
+ const expirationDate = new Date(user.expiresAt);
102
+ const now = new Date();
103
+ const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
104
+
105
+ return expirationDate <= sevenDaysFromNow && !isTokenExpired();
60
106
  }
61
107
 
62
108
  /**
@@ -99,6 +145,8 @@ module.exports = {
99
145
  getProjectConfig,
100
146
  saveProjectConfig,
101
147
  isAuthenticated,
148
+ isTokenExpired,
149
+ isTokenExpiringSoon,
102
150
  getUser,
103
151
  saveUser,
104
152
  clearUser,