@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/CLAUDE.md +85 -2
- package/README.md +103 -3
- 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/init.js +160 -4
- package/src/commands/list.js +109 -4
- package/src/lib/oauth.js +205 -0
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
|
+
};
|