@startanaicompany/cli 1.3.1 → 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/CLAUDE.md +52 -0
- package/README.md +90 -1
- package/bin/saac.js +22 -0
- package/git_auth.md +961 -0
- package/package.json +4 -3
- package/src/commands/create.js +53 -6
- package/src/commands/git.js +254 -0
- package/src/commands/list.js +109 -4
- package/src/lib/oauth.js +205 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startanaicompany/cli",
|
|
3
|
-
"version": "1.
|
|
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": {
|
package/src/commands/create.js
CHANGED
|
@@ -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
|
|
59
|
-
logger.error('Missing required options: subdomain
|
|
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
|
|
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
|
+
};
|
package/src/commands/list.js
CHANGED
|
@@ -1,4 +1,109 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
package/src/lib/oauth.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Helper Module
|
|
3
|
+
* Handles Git OAuth authentication flow for SAAC CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const open = require('open');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const logger = require('./logger');
|
|
10
|
+
const { getApiUrl } = require('./config');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract git_host from repository URL
|
|
14
|
+
* @param {string} gitUrl - Git repository URL (SSH or HTTPS)
|
|
15
|
+
* @returns {string} - Git host domain
|
|
16
|
+
*/
|
|
17
|
+
function extractGitHost(gitUrl) {
|
|
18
|
+
// SSH format: git@git.startanaicompany.com:user/repo.git
|
|
19
|
+
const sshMatch = gitUrl.match(/git@([^:]+):/);
|
|
20
|
+
if (sshMatch) {
|
|
21
|
+
return sshMatch[1];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// HTTPS format: https://git.startanaicompany.com/user/repo.git
|
|
25
|
+
const httpsMatch = gitUrl.match(/https?:\/\/([^/]+)/);
|
|
26
|
+
if (httpsMatch) {
|
|
27
|
+
return httpsMatch[1];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new Error('Invalid Git repository URL format. Expected SSH (git@host:user/repo.git) or HTTPS (https://host/user/repo.git)');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initiate OAuth flow for a git_host
|
|
35
|
+
* @param {string} gitHost - Git host domain
|
|
36
|
+
* @param {string} apiKey - User's API key
|
|
37
|
+
* @returns {Promise<object>} - { gitUsername, gitHost }
|
|
38
|
+
*/
|
|
39
|
+
async function connectGitAccount(gitHost, apiKey) {
|
|
40
|
+
const sessionId = crypto.randomBytes(16).toString('hex');
|
|
41
|
+
|
|
42
|
+
logger.newline();
|
|
43
|
+
logger.section(`Connecting to ${gitHost}`);
|
|
44
|
+
logger.newline();
|
|
45
|
+
logger.field('Session ID', sessionId);
|
|
46
|
+
logger.newline();
|
|
47
|
+
|
|
48
|
+
// Build authorization URL
|
|
49
|
+
const baseUrl = getApiUrl().replace('/api/v1', ''); // Remove /api/v1 suffix
|
|
50
|
+
const authUrl = `${baseUrl}/oauth/authorize?git_host=${encodeURIComponent(gitHost)}&session_id=${sessionId}`;
|
|
51
|
+
|
|
52
|
+
logger.info('Opening browser for authentication...');
|
|
53
|
+
logger.newline();
|
|
54
|
+
logger.warn('If browser doesn\'t open, visit:');
|
|
55
|
+
logger.log(` ${authUrl}`);
|
|
56
|
+
logger.newline();
|
|
57
|
+
|
|
58
|
+
// Open browser
|
|
59
|
+
try {
|
|
60
|
+
await open(authUrl);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.warn('Could not open browser automatically');
|
|
63
|
+
logger.info('Please open the URL above manually');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const spin = logger.spinner('Waiting for authorization...').start();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Poll for completion
|
|
70
|
+
const result = await pollForCompletion(sessionId, apiKey);
|
|
71
|
+
|
|
72
|
+
spin.succeed(`Connected to ${gitHost} as ${result.gitUsername}`);
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
spin.fail('Authorization failed');
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Poll OAuth session until completed
|
|
83
|
+
* @param {string} sessionId - Session ID
|
|
84
|
+
* @param {string} apiKey - User's API key
|
|
85
|
+
* @returns {Promise<object>} - { gitUsername, gitHost }
|
|
86
|
+
*/
|
|
87
|
+
async function pollForCompletion(sessionId, apiKey) {
|
|
88
|
+
const pollInterval = 2000; // 2 seconds
|
|
89
|
+
const maxAttempts = 150; // 5 minutes total (150 * 2s)
|
|
90
|
+
|
|
91
|
+
const baseUrl = getApiUrl().replace('/api/v1', ''); // Remove /api/v1 suffix
|
|
92
|
+
|
|
93
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
94
|
+
await sleep(pollInterval);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const response = await axios.get(
|
|
98
|
+
`${baseUrl}/oauth/poll/${sessionId}`,
|
|
99
|
+
{
|
|
100
|
+
headers: {
|
|
101
|
+
'X-API-Key': apiKey,
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const { status, gitUsername, gitHost } = response.data;
|
|
107
|
+
|
|
108
|
+
if (status === 'completed') {
|
|
109
|
+
return { gitUsername, gitHost };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (status === 'failed') {
|
|
113
|
+
throw new Error('OAuth authorization failed');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Still pending, continue polling
|
|
117
|
+
// Silent polling - spinner shows progress
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error.response?.status === 404) {
|
|
120
|
+
throw new Error('OAuth session not found or expired');
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw new Error('OAuth authorization timed out (5 minutes). Please try again.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if user has OAuth connection for git_host
|
|
131
|
+
* @param {string} gitHost - Git host domain
|
|
132
|
+
* @param {string} apiKey - User's API key
|
|
133
|
+
* @returns {Promise<object|null>} - Connection object or null
|
|
134
|
+
*/
|
|
135
|
+
async function getConnection(gitHost, apiKey) {
|
|
136
|
+
try {
|
|
137
|
+
const response = await axios.get(
|
|
138
|
+
`${getApiUrl()}/users/me/oauth`,
|
|
139
|
+
{
|
|
140
|
+
headers: {
|
|
141
|
+
'X-API-Key': apiKey,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const connection = response.data.connections.find(
|
|
147
|
+
(conn) => conn.gitHost === gitHost
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return connection || null;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* List all OAuth connections
|
|
158
|
+
* @param {string} apiKey - User's API key
|
|
159
|
+
* @returns {Promise<array>} - Array of connection objects
|
|
160
|
+
*/
|
|
161
|
+
async function listConnections(apiKey) {
|
|
162
|
+
const response = await axios.get(
|
|
163
|
+
`${getApiUrl()}/users/me/oauth`,
|
|
164
|
+
{
|
|
165
|
+
headers: {
|
|
166
|
+
'X-API-Key': apiKey,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return response.data.connections || [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Revoke OAuth connection for git_host
|
|
176
|
+
* @param {string} gitHost - Git host domain
|
|
177
|
+
* @param {string} apiKey - User's API key
|
|
178
|
+
*/
|
|
179
|
+
async function revokeConnection(gitHost, apiKey) {
|
|
180
|
+
await axios.delete(
|
|
181
|
+
`${getApiUrl()}/users/me/oauth/${encodeURIComponent(gitHost)}`,
|
|
182
|
+
{
|
|
183
|
+
headers: {
|
|
184
|
+
'X-API-Key': apiKey,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sleep utility
|
|
192
|
+
* @param {number} ms - Milliseconds to sleep
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
function sleep(ms) {
|
|
196
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
extractGitHost,
|
|
201
|
+
connectGitAccount,
|
|
202
|
+
getConnection,
|
|
203
|
+
listConnections,
|
|
204
|
+
revokeConnection,
|
|
205
|
+
};
|