git-push-time 1.1.2 → 1.1.4
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/.github/workflows/npm-publish.yml +59 -32
- package/README.md +110 -112
- package/bin/index.js +321 -339
- package/lib/commands/edit-time.js +141 -141
- package/lib/commands/fix.js +113 -113
- package/lib/commands/github.js +429 -445
- package/lib/commands/pull.js +90 -90
- package/lib/commands/push.js +720 -720
- package/lib/config.js +220 -220
- package/lib/fix.js +286 -286
- package/lib/git.js +525 -516
- package/lib/github.js +322 -322
- package/lib/prompt-helpers.js +19 -20
- package/lib/secrets.js +92 -92
- package/lib/ui.js +1057 -1063
- package/lib/utils.js +120 -120
- package/package.json +36 -36
package/lib/github.js
CHANGED
|
@@ -1,322 +1,322 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { getGitHubToken, saveGitHubToken } from './config.js';
|
|
3
|
-
|
|
4
|
-
const GITHUB_API_BASE = 'https://api.github.com';
|
|
5
|
-
const GITHUB_OAUTH_CLIENT_ID = '178c6fc778ccc68e1d6a';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
* @param {string} url
|
|
10
|
-
*/
|
|
11
|
-
export function openBrowser(url) {
|
|
12
|
-
try {
|
|
13
|
-
const platform = process.platform;
|
|
14
|
-
if (platform === 'win32') {
|
|
15
|
-
spawn('cmd', ['/c', 'start', '""', url], { detached: true, stdio: 'ignore' });
|
|
16
|
-
} else if (platform === 'darwin') {
|
|
17
|
-
spawn('open', [url], { detached: true, stdio: 'ignore' });
|
|
18
|
-
} else {
|
|
19
|
-
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
20
|
-
}
|
|
21
|
-
return true;
|
|
22
|
-
} catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
* @returns {Promise<{ ok: boolean, data?: { device_code: string, user_code: string, verification_uri: string, expires_in: number, interval: number }, error?: string }>}
|
|
30
|
-
*/
|
|
31
|
-
export async function initiateDeviceFlow() {
|
|
32
|
-
try {
|
|
33
|
-
const res = await fetch('https://github.com/login/device/code', {
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: {
|
|
36
|
-
'Accept': 'application/json',
|
|
37
|
-
'Content-Type': 'application/json'
|
|
38
|
-
},
|
|
39
|
-
body: JSON.stringify({
|
|
40
|
-
client_id: GITHUB_OAUTH_CLIENT_ID,
|
|
41
|
-
scope: 'repo read:org gist'
|
|
42
|
-
})
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (!res.ok) {
|
|
46
|
-
const err = await res.json().catch(() => ({}));
|
|
47
|
-
return { ok: false, error: err.error_description || err.message || `
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const data = await res.json();
|
|
51
|
-
return { ok: true, data };
|
|
52
|
-
} catch (err) {
|
|
53
|
-
return { ok: false, error: `
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
* @param {string} deviceCode
|
|
60
|
-
* @param {number} intervalSeconds
|
|
61
|
-
* @param {number} expiresInSeconds
|
|
62
|
-
* @param {(status: string) => void} onStatusUpdate
|
|
63
|
-
* @returns {Promise<{ ok: boolean, token?: string, error?: string }>}
|
|
64
|
-
*/
|
|
65
|
-
export async function pollDeviceToken(deviceCode, intervalSeconds = 5, expiresInSeconds = 900, onStatusUpdate = null) {
|
|
66
|
-
let interval = Math.max(2, intervalSeconds);
|
|
67
|
-
const startTime = Date.now();
|
|
68
|
-
const maxTime = expiresInSeconds * 1000;
|
|
69
|
-
let consecutiveNetErrors = 0;
|
|
70
|
-
|
|
71
|
-
while (Date.now() - startTime < maxTime) {
|
|
72
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
76
|
-
method: 'POST',
|
|
77
|
-
headers: {
|
|
78
|
-
'Accept': 'application/json',
|
|
79
|
-
'Content-Type': 'application/json'
|
|
80
|
-
},
|
|
81
|
-
body: JSON.stringify({
|
|
82
|
-
client_id: GITHUB_OAUTH_CLIENT_ID,
|
|
83
|
-
device_code: deviceCode,
|
|
84
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
85
|
-
})
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
consecutiveNetErrors = 0;
|
|
89
|
-
const data = await res.json();
|
|
90
|
-
|
|
91
|
-
if (data.access_token) {
|
|
92
|
-
return { ok: true, token: data.access_token };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (data.error === 'authorization_pending') {
|
|
96
|
-
if (onStatusUpdate) onStatusUpdate('pending');
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (data.error === 'slow_down') {
|
|
101
|
-
interval = (data.interval || interval) + 5;
|
|
102
|
-
if (onStatusUpdate) onStatusUpdate('slow_down');
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (data.error === 'expired_token') {
|
|
107
|
-
return { ok: false, error: '
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (data.error === 'access_denied') {
|
|
111
|
-
return { ok: false, error: '
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { ok: false, error: data.error_description || data.error || '
|
|
115
|
-
} catch (err) {
|
|
116
|
-
//
|
|
117
|
-
consecutiveNetErrors++;
|
|
118
|
-
if (consecutiveNetErrors >= 5) {
|
|
119
|
-
return { ok: false, error: `
|
|
120
|
-
}
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return { ok: false, error: '
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
*
|
|
130
|
-
* @param {string} token
|
|
131
|
-
* @returns {Record<string, string>}
|
|
132
|
-
*/
|
|
133
|
-
function getApiHeaders(token) {
|
|
134
|
-
return {
|
|
135
|
-
'Accept': 'application/vnd.github+json',
|
|
136
|
-
'Authorization': `Bearer ${token}`,
|
|
137
|
-
'X-GitHub-Api-Version': '2022-11-28',
|
|
138
|
-
'User-Agent': 'git-push-time-cli'
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
*
|
|
144
|
-
* @param {string} token
|
|
145
|
-
* @returns {Promise<{ ok: boolean, user?: any, error?: string }>}
|
|
146
|
-
*/
|
|
147
|
-
export async function verifyAndFetchUser(token) {
|
|
148
|
-
if (!token || !token.trim()) {
|
|
149
|
-
return { ok: false, error: 'Token
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const res = await fetch(`${GITHUB_API_BASE}/user`, {
|
|
154
|
-
method: 'GET',
|
|
155
|
-
headers: getApiHeaders(token.trim())
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
if (res.status === 401) {
|
|
159
|
-
return { ok: false, error: 'Token
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!res.ok) {
|
|
163
|
-
const errData = await res.json().catch(() => ({}));
|
|
164
|
-
return { ok: false, error: errData.message || `
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const user = await res.json();
|
|
168
|
-
return { ok: true, user };
|
|
169
|
-
} catch (err) {
|
|
170
|
-
return { ok: false, error: `
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
* @returns {Promise<{ authenticated: boolean, user?: any, source?: string, token?: string, error?: string }>}
|
|
177
|
-
*/
|
|
178
|
-
export async function getCurrentGitHubUser() {
|
|
179
|
-
const { token, source, user: cachedUser } = getGitHubToken();
|
|
180
|
-
if (!token) {
|
|
181
|
-
return { authenticated: false, error: '
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
const check = await verifyAndFetchUser(token);
|
|
186
|
-
if (!check.ok) {
|
|
187
|
-
//
|
|
188
|
-
if (cachedUser) {
|
|
189
|
-
return { authenticated: true, user: cachedUser, token, source, offline: true };
|
|
190
|
-
}
|
|
191
|
-
return { authenticated: false, error: check.error, source };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
if (source === 'config') {
|
|
196
|
-
const cachedLogin = cachedUser?.login;
|
|
197
|
-
if (!cachedLogin || cachedLogin !== check.user?.login || JSON.stringify(cachedUser) !== JSON.stringify({
|
|
198
|
-
login: check.user.login,
|
|
199
|
-
name: check.user.name,
|
|
200
|
-
avatar_url: check.user.avatar_url,
|
|
201
|
-
email: check.user.email,
|
|
202
|
-
bio: check.user.bio,
|
|
203
|
-
public_repos: check.user.public_repos,
|
|
204
|
-
total_private_repos: check.user.total_private_repos,
|
|
205
|
-
html_url: check.user.html_url
|
|
206
|
-
})) {
|
|
207
|
-
saveGitHubToken(token, check.user);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return { authenticated: true, user: check.user, token, source };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
*
|
|
216
|
-
* @param {object} options
|
|
217
|
-
* @param {number} options.limit
|
|
218
|
-
* @param {'all' | 'owner' | 'public' | 'private'} options.type
|
|
219
|
-
* @returns {Promise<{ ok: boolean, repos?: any[], error?: string }>}
|
|
220
|
-
*/
|
|
221
|
-
export async function listUserRepos({ limit = 30, type = 'owner' } = {}) {
|
|
222
|
-
const { token } = getGitHubToken();
|
|
223
|
-
if (!token) {
|
|
224
|
-
return { ok: false, error: '
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
const url = new URL(`${GITHUB_API_BASE}/user/repos`);
|
|
229
|
-
url.searchParams.set('sort', 'updated');
|
|
230
|
-
url.searchParams.set('direction', 'desc');
|
|
231
|
-
url.searchParams.set('per_page', String(Math.min(100, Math.max(1, limit))));
|
|
232
|
-
url.searchParams.set('type', type);
|
|
233
|
-
|
|
234
|
-
const res = await fetch(url.toString(), {
|
|
235
|
-
method: 'GET',
|
|
236
|
-
headers: getApiHeaders(token)
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
if (!res.ok) {
|
|
240
|
-
const errData = await res.json().catch(() => ({}));
|
|
241
|
-
return { ok: false, error: errData.message || `
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const repos = await res.json();
|
|
245
|
-
return { ok: true, repos };
|
|
246
|
-
} catch (err) {
|
|
247
|
-
return { ok: false, error: `
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
*
|
|
253
|
-
* @param {object} params
|
|
254
|
-
* @param {string} params.name
|
|
255
|
-
* @param {string} [params.description]
|
|
256
|
-
* @param {boolean} [params.isPrivate]
|
|
257
|
-
* @param {boolean} [params.autoInit]
|
|
258
|
-
* @returns {Promise<{ ok: boolean, repo?: any, error?: string }>}
|
|
259
|
-
*/
|
|
260
|
-
export async function createGitHubRepo({ name, description = '', isPrivate = false, autoInit = false }) {
|
|
261
|
-
const { token } = getGitHubToken();
|
|
262
|
-
if (!token) {
|
|
263
|
-
return { ok: false, error: '
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const cleanName = String(name || '').trim();
|
|
267
|
-
if (!cleanName) {
|
|
268
|
-
return { ok: false, error: '
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (!/^[a-zA-Z0-9_.-]+$/.test(cleanName)) {
|
|
272
|
-
return { ok: false, error: '
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
const payload = {
|
|
277
|
-
name: cleanName,
|
|
278
|
-
description: (description || '').trim(),
|
|
279
|
-
private: Boolean(isPrivate),
|
|
280
|
-
auto_init: Boolean(autoInit)
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const res = await fetch(`${GITHUB_API_BASE}/user/repos`, {
|
|
284
|
-
method: 'POST',
|
|
285
|
-
headers: {
|
|
286
|
-
...getApiHeaders(token),
|
|
287
|
-
'Content-Type': 'application/json'
|
|
288
|
-
},
|
|
289
|
-
body: JSON.stringify(payload)
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (res.status === 422) {
|
|
293
|
-
const errData = await res.json().catch(() => ({}));
|
|
294
|
-
const msg = (errData.errors && errData.errors[0]?.message) || errData.message || '
|
|
295
|
-
return { ok: false, error: `
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!res.ok) {
|
|
299
|
-
const errData = await res.json().catch(() => ({}));
|
|
300
|
-
return { ok: false, error: errData.message || `
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const repo = await res.json();
|
|
304
|
-
return {
|
|
305
|
-
ok: true,
|
|
306
|
-
repo: {
|
|
307
|
-
id: repo.id,
|
|
308
|
-
name: repo.name,
|
|
309
|
-
fullName: repo.full_name,
|
|
310
|
-
isPrivate: repo.private,
|
|
311
|
-
description: repo.description || '',
|
|
312
|
-
htmlUrl: repo.html_url,
|
|
313
|
-
cloneUrl: repo.clone_url,
|
|
314
|
-
sshUrl: repo.ssh_url,
|
|
315
|
-
defaultBranch: repo.default_branch || 'main',
|
|
316
|
-
createdAt: repo.created_at
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
} catch (err) {
|
|
320
|
-
return { ok: false, error: `
|
|
321
|
-
}
|
|
322
|
-
}
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { getGitHubToken, saveGitHubToken } from './config.js';
|
|
3
|
+
|
|
4
|
+
const GITHUB_API_BASE = 'https://api.github.com';
|
|
5
|
+
const GITHUB_OAUTH_CLIENT_ID = '178c6fc778ccc68e1d6a';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Open a URL in the system's default web browser.
|
|
9
|
+
* @param {string} url
|
|
10
|
+
*/
|
|
11
|
+
export function openBrowser(url) {
|
|
12
|
+
try {
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
if (platform === 'win32') {
|
|
15
|
+
spawn('cmd', ['/c', 'start', '""', url], { detached: true, stdio: 'ignore' });
|
|
16
|
+
} else if (platform === 'darwin') {
|
|
17
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' });
|
|
18
|
+
} else {
|
|
19
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start the GitHub OAuth Device Authorization Flow.
|
|
29
|
+
* @returns {Promise<{ ok: boolean, data?: { device_code: string, user_code: string, verification_uri: string, expires_in: number, interval: number }, error?: string }>}
|
|
30
|
+
*/
|
|
31
|
+
export async function initiateDeviceFlow() {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch('https://github.com/login/device/code', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
'Accept': 'application/json',
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
client_id: GITHUB_OAUTH_CLIENT_ID,
|
|
41
|
+
scope: 'repo read:org gist'
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
const err = await res.json().catch(() => ({}));
|
|
47
|
+
return { ok: false, error: err.error_description || err.message || `API error (${res.status})` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
return { ok: true, data };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { ok: false, error: `Network error: ${err.message}` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Poll until the user authorizes the application on GitHub.
|
|
59
|
+
* @param {string} deviceCode
|
|
60
|
+
* @param {number} intervalSeconds
|
|
61
|
+
* @param {number} expiresInSeconds
|
|
62
|
+
* @param {(status: string) => void} onStatusUpdate
|
|
63
|
+
* @returns {Promise<{ ok: boolean, token?: string, error?: string }>}
|
|
64
|
+
*/
|
|
65
|
+
export async function pollDeviceToken(deviceCode, intervalSeconds = 5, expiresInSeconds = 900, onStatusUpdate = null) {
|
|
66
|
+
let interval = Math.max(2, intervalSeconds);
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
const maxTime = expiresInSeconds * 1000;
|
|
69
|
+
let consecutiveNetErrors = 0;
|
|
70
|
+
|
|
71
|
+
while (Date.now() - startTime < maxTime) {
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Accept': 'application/json',
|
|
79
|
+
'Content-Type': 'application/json'
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
client_id: GITHUB_OAUTH_CLIENT_ID,
|
|
83
|
+
device_code: deviceCode,
|
|
84
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
consecutiveNetErrors = 0;
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
|
|
91
|
+
if (data.access_token) {
|
|
92
|
+
return { ok: true, token: data.access_token };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (data.error === 'authorization_pending') {
|
|
96
|
+
if (onStatusUpdate) onStatusUpdate('pending');
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (data.error === 'slow_down') {
|
|
101
|
+
interval = (data.interval || interval) + 5;
|
|
102
|
+
if (onStatusUpdate) onStatusUpdate('slow_down');
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (data.error === 'expired_token') {
|
|
107
|
+
return { ok: false, error: 'The verification code expired. Please try again.' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (data.error === 'access_denied') {
|
|
111
|
+
return { ok: false, error: 'You denied authorization on GitHub.' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ok: false, error: data.error_description || data.error || 'Unknown authorization error' };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// Retry intermittent network errors and exit early after repeated failures.
|
|
117
|
+
consecutiveNetErrors++;
|
|
118
|
+
if (consecutiveNetErrors >= 5) {
|
|
119
|
+
return { ok: false, error: `Repeated network error (${err.message}). Check your connection and try again.` };
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { ok: false, error: 'Authorization timed out.' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create standard headers for the GitHub REST API.
|
|
130
|
+
* @param {string} token
|
|
131
|
+
* @returns {Record<string, string>}
|
|
132
|
+
*/
|
|
133
|
+
function getApiHeaders(token) {
|
|
134
|
+
return {
|
|
135
|
+
'Accept': 'application/vnd.github+json',
|
|
136
|
+
'Authorization': `Bearer ${token}`,
|
|
137
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
138
|
+
'User-Agent': 'git-push-time-cli'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Verify a token and fetch the GitHub account profile.
|
|
144
|
+
* @param {string} token
|
|
145
|
+
* @returns {Promise<{ ok: boolean, user?: any, error?: string }>}
|
|
146
|
+
*/
|
|
147
|
+
export async function verifyAndFetchUser(token) {
|
|
148
|
+
if (!token || !token.trim()) {
|
|
149
|
+
return { ok: false, error: 'Token cannot be empty.' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(`${GITHUB_API_BASE}/user`, {
|
|
154
|
+
method: 'GET',
|
|
155
|
+
headers: getApiHeaders(token.trim())
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (res.status === 401) {
|
|
159
|
+
return { ok: false, error: 'Token is invalid or expired (401 Unauthorized).' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const errData = await res.json().catch(() => ({}));
|
|
164
|
+
return { ok: false, error: errData.message || `GitHub API error (${res.status})` };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const user = await res.json();
|
|
168
|
+
return { ok: true, user };
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return { ok: false, error: `Network error: ${err.message}` };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the current user profile from the active token.
|
|
176
|
+
* @returns {Promise<{ authenticated: boolean, user?: any, source?: string, token?: string, error?: string }>}
|
|
177
|
+
*/
|
|
178
|
+
export async function getCurrentGitHubUser() {
|
|
179
|
+
const { token, source, user: cachedUser } = getGitHubToken();
|
|
180
|
+
if (!token) {
|
|
181
|
+
return { authenticated: false, error: 'No GitHub account is connected.' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Verify the token with the API.
|
|
185
|
+
const check = await verifyAndFetchUser(token);
|
|
186
|
+
if (!check.ok) {
|
|
187
|
+
// Fall back to the cached profile if the network request fails.
|
|
188
|
+
if (cachedUser) {
|
|
189
|
+
return { authenticated: true, user: cachedUser, token, source, offline: true };
|
|
190
|
+
}
|
|
191
|
+
return { authenticated: false, error: check.error, source };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update the cached profile only when it changes to avoid disk I/O on each menu render.
|
|
195
|
+
if (source === 'config') {
|
|
196
|
+
const cachedLogin = cachedUser?.login;
|
|
197
|
+
if (!cachedLogin || cachedLogin !== check.user?.login || JSON.stringify(cachedUser) !== JSON.stringify({
|
|
198
|
+
login: check.user.login,
|
|
199
|
+
name: check.user.name,
|
|
200
|
+
avatar_url: check.user.avatar_url,
|
|
201
|
+
email: check.user.email,
|
|
202
|
+
bio: check.user.bio,
|
|
203
|
+
public_repos: check.user.public_repos,
|
|
204
|
+
total_private_repos: check.user.total_private_repos,
|
|
205
|
+
html_url: check.user.html_url
|
|
206
|
+
})) {
|
|
207
|
+
saveGitHubToken(token, check.user);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { authenticated: true, user: check.user, token, source };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* List repositories for the current user.
|
|
216
|
+
* @param {object} options
|
|
217
|
+
* @param {number} options.limit Maximum number of repositories to return
|
|
218
|
+
* @param {'all' | 'owner' | 'public' | 'private'} options.type
|
|
219
|
+
* @returns {Promise<{ ok: boolean, repos?: any[], error?: string }>}
|
|
220
|
+
*/
|
|
221
|
+
export async function listUserRepos({ limit = 30, type = 'owner' } = {}) {
|
|
222
|
+
const { token } = getGitHubToken();
|
|
223
|
+
if (!token) {
|
|
224
|
+
return { ok: false, error: 'No GitHub token found. Connect an account first.' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const url = new URL(`${GITHUB_API_BASE}/user/repos`);
|
|
229
|
+
url.searchParams.set('sort', 'updated');
|
|
230
|
+
url.searchParams.set('direction', 'desc');
|
|
231
|
+
url.searchParams.set('per_page', String(Math.min(100, Math.max(1, limit))));
|
|
232
|
+
url.searchParams.set('type', type);
|
|
233
|
+
|
|
234
|
+
const res = await fetch(url.toString(), {
|
|
235
|
+
method: 'GET',
|
|
236
|
+
headers: getApiHeaders(token)
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!res.ok) {
|
|
240
|
+
const errData = await res.json().catch(() => ({}));
|
|
241
|
+
return { ok: false, error: errData.message || `Could not load repositories (${res.status})` };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const repos = await res.json();
|
|
245
|
+
return { ok: true, repos };
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return { ok: false, error: `Connection error: ${err.message}` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Create a new GitHub repository.
|
|
253
|
+
* @param {object} params
|
|
254
|
+
* @param {string} params.name Repository name (required)
|
|
255
|
+
* @param {string} [params.description] Repository description
|
|
256
|
+
* @param {boolean} [params.isPrivate] Whether the repository is private
|
|
257
|
+
* @param {boolean} [params.autoInit] Whether to create an initial README.md (defaults to false for existing projects)
|
|
258
|
+
* @returns {Promise<{ ok: boolean, repo?: any, error?: string }>}
|
|
259
|
+
*/
|
|
260
|
+
export async function createGitHubRepo({ name, description = '', isPrivate = false, autoInit = false }) {
|
|
261
|
+
const { token } = getGitHubToken();
|
|
262
|
+
if (!token) {
|
|
263
|
+
return { ok: false, error: 'No GitHub token found. Connect an account first.' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const cleanName = String(name || '').trim();
|
|
267
|
+
if (!cleanName) {
|
|
268
|
+
return { ok: false, error: 'Repository name cannot be empty.' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(cleanName)) {
|
|
272
|
+
return { ok: false, error: 'Repository names may contain only letters, numbers, hyphens (-), underscores (_), and periods (.).' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const payload = {
|
|
277
|
+
name: cleanName,
|
|
278
|
+
description: (description || '').trim(),
|
|
279
|
+
private: Boolean(isPrivate),
|
|
280
|
+
auto_init: Boolean(autoInit)
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const res = await fetch(`${GITHUB_API_BASE}/user/repos`, {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: {
|
|
286
|
+
...getApiHeaders(token),
|
|
287
|
+
'Content-Type': 'application/json'
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify(payload)
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (res.status === 422) {
|
|
293
|
+
const errData = await res.json().catch(() => ({}));
|
|
294
|
+
const msg = (errData.errors && errData.errors[0]?.message) || errData.message || 'Repository already exists or the name is invalid.';
|
|
295
|
+
return { ok: false, error: `Could not create repository: ${msg}` };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!res.ok) {
|
|
299
|
+
const errData = await res.json().catch(() => ({}));
|
|
300
|
+
return { ok: false, error: errData.message || `GitHub API error (${res.status})` };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const repo = await res.json();
|
|
304
|
+
return {
|
|
305
|
+
ok: true,
|
|
306
|
+
repo: {
|
|
307
|
+
id: repo.id,
|
|
308
|
+
name: repo.name,
|
|
309
|
+
fullName: repo.full_name,
|
|
310
|
+
isPrivate: repo.private,
|
|
311
|
+
description: repo.description || '',
|
|
312
|
+
htmlUrl: repo.html_url,
|
|
313
|
+
cloneUrl: repo.clone_url,
|
|
314
|
+
sshUrl: repo.ssh_url,
|
|
315
|
+
defaultBranch: repo.default_branch || 'main',
|
|
316
|
+
createdAt: repo.created_at
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return { ok: false, error: `Connection error while creating repository: ${err.message}` };
|
|
321
|
+
}
|
|
322
|
+
}
|