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/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
- * 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
- }
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
+ }