@startanaicompany/cli 1.3.0 → 1.4.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Official CLI for StartAnAiCompany.com - Deploy AI recruitment sites with ease",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -35,14 +35,15 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "axios": "^1.6.0",
38
+ "boxen": "^5.1.2",
38
39
  "chalk": "^4.1.2",
39
40
  "commander": "^11.1.0",
40
41
  "conf": "^10.2.0",
42
+ "dotenv": "^16.3.1",
41
43
  "inquirer": "^8.2.5",
44
+ "open": "^8.4.2",
42
45
  "ora": "^5.4.1",
43
- "boxen": "^5.1.2",
44
46
  "table": "^6.8.1",
45
- "dotenv": "^16.3.1",
46
47
  "validator": "^13.11.0"
47
48
  },
48
49
  "devDependencies": {
@@ -3,8 +3,10 @@
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { isAuthenticated, saveProjectConfig } = require('../lib/config');
6
+ const { isAuthenticated, saveProjectConfig, getUser } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
+ const oauth = require('../lib/oauth');
9
+ const inquirer = require('inquirer');
8
10
 
9
11
  async function create(name, options) {
10
12
  try {
@@ -27,7 +29,7 @@ async function create(name, options) {
27
29
  logger.info('Required options:');
28
30
  logger.log(' -s, --subdomain <subdomain> Subdomain for your app');
29
31
  logger.log(' -r, --repository <url> Git repository URL (SSH format)');
30
- logger.log(' -t, --git-token <token> Git API token');
32
+ logger.log(' -t, --git-token <token> Git API token (optional if OAuth connected)');
31
33
  logger.newline();
32
34
  logger.info('Optional options:');
33
35
  logger.log(' -b, --branch <branch> Git branch (default: master)');
@@ -55,17 +57,58 @@ async function create(name, options) {
55
57
  process.exit(1);
56
58
  }
57
59
 
58
- if (!options.subdomain || !options.repository || !options.gitToken) {
59
- logger.error('Missing required options: subdomain, repository, and git-token are required');
60
+ if (!options.subdomain || !options.repository) {
61
+ logger.error('Missing required options: subdomain and repository are required');
60
62
  logger.newline();
61
63
  logger.info('Example:');
62
- logger.log(` saac create ${name} -s myapp -r git@git.startanaicompany.com:user/repo.git -t your_token`);
64
+ logger.log(` saac create ${name} -s myapp -r git@git.startanaicompany.com:user/repo.git`);
65
+ logger.log(` saac create ${name} -s myapp -r git@git... -t your_token # With manual token`);
63
66
  process.exit(1);
64
67
  }
65
68
 
66
69
  logger.section(`Creating Application: ${name}`);
67
70
  logger.newline();
68
71
 
72
+ // OAuth: Check if user has connected Git account for this repository
73
+ const user = getUser();
74
+ const gitHost = oauth.extractGitHost(options.repository);
75
+ const connection = await oauth.getConnection(gitHost, user.sessionToken || user.apiKey);
76
+
77
+ if (connection) {
78
+ logger.success(`Using connected account: ${connection.gitUsername}@${connection.gitHost}`);
79
+ logger.newline();
80
+ } else if (!options.gitToken) {
81
+ // No OAuth connection AND no manual token provided
82
+ logger.warn(`Git account not connected for ${gitHost}`);
83
+ logger.newline();
84
+
85
+ const { shouldConnect } = await inquirer.prompt([
86
+ {
87
+ type: 'confirm',
88
+ name: 'shouldConnect',
89
+ message: 'Would you like to connect now?',
90
+ default: true,
91
+ },
92
+ ]);
93
+
94
+ if (!shouldConnect) {
95
+ logger.newline();
96
+ logger.error('Cannot create application without Git authentication');
97
+ logger.newline();
98
+ logger.info('Options:');
99
+ logger.log(' 1. Connect Git account: saac git connect');
100
+ logger.log(' 2. Provide token: saac create ... --git-token <token>');
101
+ process.exit(1);
102
+ }
103
+
104
+ // Initiate OAuth flow
105
+ await oauth.connectGitAccount(gitHost, user.sessionToken || user.apiKey);
106
+
107
+ logger.newline();
108
+ logger.section('Continuing with application creation');
109
+ logger.newline();
110
+ }
111
+
69
112
  // Build application payload
70
113
  const appData = {
71
114
  name: name,
@@ -73,9 +116,13 @@ async function create(name, options) {
73
116
  domain_suffix: options.domainSuffix || 'startanaicompany.com',
74
117
  git_repository: options.repository,
75
118
  git_branch: options.branch || 'master',
76
- git_api_token: options.gitToken,
77
119
  };
78
120
 
121
+ // Only include git_api_token if provided (OAuth will be used if available)
122
+ if (options.gitToken) {
123
+ appData.git_api_token = options.gitToken;
124
+ }
125
+
79
126
  // Optional: Port configuration
80
127
  if (options.port) {
81
128
  appData.ports_exposes = options.port;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Git command - Manage Git account connections (OAuth)
3
+ */
4
+
5
+ const oauth = require('../lib/oauth');
6
+ const { isAuthenticated, getUser } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+ const { table } = require('table');
9
+ const inquirer = require('inquirer');
10
+
11
+ /**
12
+ * Connect Git account via OAuth
13
+ */
14
+ async function connect(host) {
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
+ const user = getUser();
26
+ let gitHost;
27
+
28
+ if (!host) {
29
+ // No argument - ask user which provider
30
+ logger.section('Connect Git Account');
31
+ logger.newline();
32
+
33
+ const { choice } = await inquirer.prompt([
34
+ {
35
+ type: 'list',
36
+ name: 'choice',
37
+ message: 'Select Git provider:',
38
+ choices: [
39
+ { name: 'git.startanaicompany.com (Gitea)', value: 'git.startanaicompany.com' },
40
+ { name: 'github.com', value: 'github.com' },
41
+ { name: 'gitlab.com', value: 'gitlab.com' },
42
+ { name: 'Custom host', value: 'custom' },
43
+ ],
44
+ },
45
+ ]);
46
+
47
+ if (choice === 'custom') {
48
+ const { customHost } = await inquirer.prompt([
49
+ {
50
+ type: 'input',
51
+ name: 'customHost',
52
+ message: 'Enter Git host domain:',
53
+ validate: (input) => input.length > 0 || 'Host cannot be empty',
54
+ },
55
+ ]);
56
+ gitHost = customHost;
57
+ } else {
58
+ gitHost = choice;
59
+ }
60
+ } else if (host.includes('git@') || host.includes('http')) {
61
+ // Repository URL provided
62
+ gitHost = oauth.extractGitHost(host);
63
+ } else {
64
+ // Host domain provided
65
+ gitHost = host;
66
+ }
67
+
68
+ // Check if already connected
69
+ const existing = await oauth.getConnection(gitHost, user.sessionToken || user.apiKey);
70
+ if (existing) {
71
+ logger.warn(`Already connected to ${gitHost}`);
72
+ logger.newline();
73
+ logger.field('Username', existing.gitUsername);
74
+ logger.field('Provider', existing.providerType);
75
+ logger.field('Expires', new Date(existing.expiresAt).toLocaleString());
76
+ logger.newline();
77
+
78
+ const { reconnect } = await inquirer.prompt([
79
+ {
80
+ type: 'confirm',
81
+ name: 'reconnect',
82
+ message: 'Do you want to reconnect?',
83
+ default: false,
84
+ },
85
+ ]);
86
+
87
+ if (!reconnect) {
88
+ logger.info('Keeping existing connection');
89
+ return;
90
+ }
91
+
92
+ // Revoke and reconnect
93
+ await oauth.revokeConnection(gitHost, user.sessionToken || user.apiKey);
94
+ logger.newline();
95
+ }
96
+
97
+ // Initiate OAuth flow
98
+ await oauth.connectGitAccount(gitHost, user.sessionToken || user.apiKey);
99
+
100
+ logger.newline();
101
+ logger.success('Git account connected successfully!');
102
+ logger.newline();
103
+ logger.info('You can now create applications without providing --git-token:');
104
+ logger.log(` saac create my-app -s myapp -r git@${gitHost}:user/repo.git`);
105
+
106
+ } catch (error) {
107
+ logger.error(error.message);
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * List connected Git accounts
114
+ */
115
+ async function list() {
116
+ try {
117
+ // Check authentication
118
+ if (!isAuthenticated()) {
119
+ logger.error('Not logged in');
120
+ logger.newline();
121
+ logger.info('Run:');
122
+ logger.log(' saac login -e <email> -k <api-key>');
123
+ process.exit(1);
124
+ }
125
+
126
+ const user = getUser();
127
+ const spin = logger.spinner('Fetching Git connections...').start();
128
+
129
+ try {
130
+ const connections = await oauth.listConnections(user.sessionToken || user.apiKey);
131
+
132
+ spin.succeed(`Found ${connections.length} connection(s)`);
133
+
134
+ if (connections.length === 0) {
135
+ logger.newline();
136
+ logger.warn('No Git accounts connected');
137
+ logger.newline();
138
+ logger.info('Connect an account with:');
139
+ logger.log(' saac git connect');
140
+ logger.log(' saac git connect git.startanaicompany.com');
141
+ logger.log(' saac git connect git@git.startanaicompany.com:user/repo.git');
142
+ return;
143
+ }
144
+
145
+ logger.newline();
146
+
147
+ // Build table data
148
+ const data = [
149
+ ['Git Host', 'Username', 'Provider', 'Expires', 'Last Used'],
150
+ ];
151
+
152
+ connections.forEach((conn) => {
153
+ const expires = new Date(conn.expiresAt).toLocaleDateString();
154
+ const lastUsed = conn.lastUsedAt
155
+ ? new Date(conn.lastUsedAt).toLocaleDateString()
156
+ : 'Never';
157
+
158
+ data.push([
159
+ conn.gitHost,
160
+ conn.gitUsername,
161
+ conn.providerType,
162
+ expires,
163
+ lastUsed,
164
+ ]);
165
+ });
166
+
167
+ console.log(table(data, {
168
+ header: {
169
+ alignment: 'center',
170
+ content: `Connected Git Accounts (${connections.length} total)`,
171
+ },
172
+ }));
173
+
174
+ logger.info('Commands:');
175
+ logger.log(' saac git connect <host> Connect another account');
176
+ logger.log(' saac git disconnect <host> Disconnect account');
177
+
178
+ } catch (error) {
179
+ spin.fail('Failed to fetch connections');
180
+ throw error;
181
+ }
182
+
183
+ } catch (error) {
184
+ logger.error(error.response?.data?.message || error.message);
185
+ process.exit(1);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Disconnect Git account
191
+ */
192
+ async function disconnect(host) {
193
+ try {
194
+ // Check authentication
195
+ if (!isAuthenticated()) {
196
+ logger.error('Not logged in');
197
+ logger.newline();
198
+ logger.info('Run:');
199
+ logger.log(' saac login -e <email> -k <api-key>');
200
+ process.exit(1);
201
+ }
202
+
203
+ if (!host) {
204
+ logger.error('Git host is required');
205
+ logger.newline();
206
+ logger.info('Usage:');
207
+ logger.log(' saac git disconnect <host>');
208
+ logger.newline();
209
+ logger.info('Example:');
210
+ logger.log(' saac git disconnect git.startanaicompany.com');
211
+ logger.newline();
212
+ logger.info('To see connected accounts:');
213
+ logger.log(' saac git list');
214
+ process.exit(1);
215
+ }
216
+
217
+ const user = getUser();
218
+ const spin = logger.spinner(`Disconnecting from ${host}...`).start();
219
+
220
+ try {
221
+ await oauth.revokeConnection(host, user.sessionToken || user.apiKey);
222
+
223
+ spin.succeed(`Disconnected from ${host}`);
224
+
225
+ logger.newline();
226
+ logger.info('To reconnect:');
227
+ logger.log(` saac git connect ${host}`);
228
+
229
+ } catch (error) {
230
+ spin.fail('Disconnect failed');
231
+
232
+ if (error.response?.status === 404) {
233
+ logger.newline();
234
+ logger.error(`No connection found for ${host}`);
235
+ logger.newline();
236
+ logger.info('To see connected accounts:');
237
+ logger.log(' saac git list');
238
+ } else {
239
+ throw error;
240
+ }
241
+ process.exit(1);
242
+ }
243
+
244
+ } catch (error) {
245
+ logger.error(error.response?.data?.message || error.message);
246
+ process.exit(1);
247
+ }
248
+ }
249
+
250
+ module.exports = {
251
+ connect,
252
+ list,
253
+ disconnect,
254
+ };
@@ -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,109 @@
1
- // TODO: Implement list command
2
- module.exports = async function() {
3
- console.log('list command - Coming soon!');
4
- };
1
+ /**
2
+ * List command - List all user applications
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 list() {
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
+ const spin = logger.spinner('Fetching applications...').start();
22
+
23
+ try {
24
+ const result = await api.listApplications();
25
+ const applications = Array.isArray(result) ? result : (result.applications || []);
26
+
27
+ spin.succeed(`Found ${applications.length} application(s)`);
28
+
29
+ if (applications.length === 0) {
30
+ logger.newline();
31
+ logger.info('No applications yet');
32
+ logger.newline();
33
+ logger.info('Create one with:');
34
+ logger.log(' saac create <name> -s <subdomain> -r <repository> -t <git-token>');
35
+ logger.newline();
36
+ logger.info('Example:');
37
+ logger.log(' saac create my-app -s myapp -r git@git.startanaicompany.com:user/repo.git -t abc123');
38
+ return;
39
+ }
40
+
41
+ logger.newline();
42
+
43
+ // Build table data
44
+ const data = [
45
+ ['Name', 'Domain', 'Status', 'Branch', 'Created'],
46
+ ];
47
+
48
+ applications.forEach((app) => {
49
+ const created = new Date(app.created_at).toLocaleDateString();
50
+ const status = app.status || 'unknown';
51
+
52
+ // Status with icons (handle both Coolify format and documented format)
53
+ let statusDisplay;
54
+ if (status.startsWith('running')) {
55
+ statusDisplay = logger.chalk.green('Running ✓');
56
+ } else if (status.startsWith('stopped')) {
57
+ statusDisplay = logger.chalk.yellow('Stopped');
58
+ } else {
59
+ switch (status) {
60
+ case 'active':
61
+ statusDisplay = logger.chalk.green('Active ✓');
62
+ break;
63
+ case 'creating':
64
+ statusDisplay = logger.chalk.yellow('Creating...');
65
+ break;
66
+ case 'error':
67
+ statusDisplay = logger.chalk.red('Error ✗');
68
+ break;
69
+ case 'suspended':
70
+ statusDisplay = logger.chalk.yellow('Suspended ⚠');
71
+ break;
72
+ default:
73
+ statusDisplay = logger.chalk.gray(status);
74
+ }
75
+ }
76
+
77
+ data.push([
78
+ app.name,
79
+ app.domain || `${app.subdomain}.startanaicompany.com`,
80
+ statusDisplay,
81
+ app.git_branch || 'master',
82
+ created
83
+ ]);
84
+ });
85
+
86
+ console.log(table(data, {
87
+ header: {
88
+ alignment: 'center',
89
+ content: `Your Applications (${applications.length} total)`,
90
+ },
91
+ }));
92
+
93
+ logger.info('Commands:');
94
+ logger.log(' saac init Link application to current directory');
95
+ logger.log(' saac status Show detailed status');
96
+ logger.log(' saac create <name> ... Create new application');
97
+
98
+ } catch (error) {
99
+ spin.fail('Failed to fetch applications');
100
+ throw error;
101
+ }
102
+
103
+ } catch (error) {
104
+ logger.error(error.response?.data?.message || error.message);
105
+ process.exit(1);
106
+ }
107
+ }
108
+
109
+ module.exports = list;