git-push-time 1.1.0 → 1.1.1
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 +33 -0
- package/README.md +215 -215
- package/bin/index.js +337 -291
- package/lib/commands/edit-time.js +141 -127
- package/lib/commands/fix.js +113 -105
- package/lib/commands/github.js +439 -436
- package/lib/commands/pull.js +89 -88
- package/lib/commands/push.js +673 -653
- package/lib/config.js +220 -210
- package/lib/fix.js +286 -260
- package/lib/git.js +492 -452
- package/lib/github.js +322 -304
- package/lib/prompt-helpers.js +62 -62
- package/lib/secrets.js +92 -87
- package/lib/ui.js +804 -844
- package/lib/utils.js +120 -106
- package/package.json +35 -32
package/lib/github.js
CHANGED
|
@@ -1,304 +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
|
-
* Mở URL trên trình duyệt web mặc định của hệ thống
|
|
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
|
-
* Khởi tạo 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 || `Lỗi API (${res.status})` };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const data = await res.json();
|
|
51
|
-
return { ok: true, data };
|
|
52
|
-
} catch (err) {
|
|
53
|
-
return { ok: false, error: `Lỗi kết nối mạng: ${err.message}` };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Polling kiểm tra người dùng đã Authorize trên GitHub chưa
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
if (!
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return { ok:
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
* Mở URL trên trình duyệt web mặc định của hệ thống
|
|
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
|
+
* Khởi tạo 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 || `Lỗi API (${res.status})` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
return { ok: true, data };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { ok: false, error: `Lỗi kết nối mạng: ${err.message}` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Polling kiểm tra người dùng đã Authorize trên GitHub chưa
|
|
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: 'Mã xác thực đã hết hạn. Vui lòng thử lại.' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (data.error === 'access_denied') {
|
|
111
|
+
return { ok: false, error: 'Bạn đã từ chối cấp quyền trên GitHub.' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ok: false, error: data.error_description || data.error || 'Lỗi xác thực không xác định' };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// Mạng chập chờn: thử lại vài lần, lỗi liên tục thì thoát sớm thay vì chờ hết 15 phút
|
|
117
|
+
consecutiveNetErrors++;
|
|
118
|
+
if (consecutiveNetErrors >= 5) {
|
|
119
|
+
return { ok: false, error: `Lỗi kết nối mạng lặp lại (${err.message}). Vui lòng kiểm tra mạng rồi thử lại.` };
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { ok: false, error: 'Hết thời gian chờ cấp quyền (Timeout).' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Tạo headers chuẩn cho 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
|
+
* Kiểm tra token và lấy thông tin tài khoản GitHub
|
|
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 không được để trống.' };
|
|
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 không hợp lệ hoặc đã hết hạn (401 Unauthorized).' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const errData = await res.json().catch(() => ({}));
|
|
164
|
+
return { ok: false, error: errData.message || `Lỗi kết nối GitHub API (${res.status})` };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const user = await res.json();
|
|
168
|
+
return { ok: true, user };
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return { ok: false, error: `Lỗi kết nối mạng: ${err.message}` };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Lấy thông tin user hiện tại từ token đang kích hoạt
|
|
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: 'Chưa kết nối tài khoản GitHub' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Thử verify với API
|
|
185
|
+
const check = await verifyAndFetchUser(token);
|
|
186
|
+
if (!check.ok) {
|
|
187
|
+
// Nếu lỗi mạng nhưng có cache user
|
|
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
|
+
// Cập nhật lại cache user vào config — chỉ ghi khi có thay đổi để tránh I/O mỗi lần render menu
|
|
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
|
+
* Lấy danh sách repository của user hiện tại
|
|
216
|
+
* @param {object} options
|
|
217
|
+
* @param {number} options.limit Số lượng repo tối đa
|
|
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: 'Chưa có GitHub Token. Vui lòng kết nối tài khoản trước.' };
|
|
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 || `Không thể tải danh sách repo (${res.status})` };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const repos = await res.json();
|
|
245
|
+
return { ok: true, repos };
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return { ok: false, error: `Lỗi kết nối: ${err.message}` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Tạo repository mới trên GitHub
|
|
253
|
+
* @param {object} params
|
|
254
|
+
* @param {string} params.name Tên repo (bắt buộc)
|
|
255
|
+
* @param {string} [params.description] Mô tả repo
|
|
256
|
+
* @param {boolean} [params.isPrivate] Repo riêng tư hay công khai
|
|
257
|
+
* @param {boolean} [params.autoInit] Có tự tạo README.md ban đầu không (mặc định false để push project có sẵn)
|
|
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: 'Chưa có GitHub Token. Vui lòng kết nối tài khoản trước.' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const cleanName = String(name || '').trim();
|
|
267
|
+
if (!cleanName) {
|
|
268
|
+
return { ok: false, error: 'Tên repository không được để trống.' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(cleanName)) {
|
|
272
|
+
return { ok: false, error: 'Tên repo chỉ được chứa chữ cái, số, dấu gạch nối (-), gạch dưới (_), hoặc dấu chấm (.).' };
|
|
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 || 'Tên repository đã tồn tại hoặc không hợp lệ.';
|
|
295
|
+
return { ok: false, error: `Không thể tạo repo: ${msg}` };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!res.ok) {
|
|
299
|
+
const errData = await res.json().catch(() => ({}));
|
|
300
|
+
return { ok: false, error: errData.message || `Lỗi từ GitHub API (${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: `Lỗi kết nối khi tạo repo: ${err.message}` };
|
|
321
|
+
}
|
|
322
|
+
}
|